Featured image of post  使用自签名证书SAN为gRPC建立TLS 连接

使用自签名证书SAN为gRPC建立TLS 连接

 

gRPC 是Google开发的一个高性能RPC框架,gRPC 默认内置了两种认证方式:

  • SSL/TLS 认证方式
  • 基于 Token 的认证方式
    没有启用证书的gRPC服务和客户端进行的是明文通信,信息面临被任何第三方监听的风险。为了保证gRPC通信不被第三方监听、篡改或伪造,可以对服务器启动TLS加密特性。
    从 go 1.15 版本开始废弃 CommonName,因此推荐使用 SAN 证书。如果按照之前的步骤通过 OpenSSL 来生成密钥、CSR、证书,会出现这样的错误:
1
rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs instead"|

This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.

什么是SAN

SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。
通俗点就是,在 SAN 证书中,可以有多个完整的 CN(CommonName),这样只需要购买一个证书就可以用在多个 URL。比如 skype.com 的证书,它就有很多 SAN。

在本地创建SAN 证书

下面 我们将用一个例子在本地生成 客户端&服务端双向SAN 证书。
假设gRPC服务端的主机名为localhost,需要为gRPC服务端和客户端之间的通信配置tls双向认证加密。

  1. 新建 openssl.conf 来放相关信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[req]  
req_extensions = v3_req  
distinguished_name = req_distinguished_name  
prompt = no  
  
[req_distinguished_name]  
countryName = CN  
stateOrProvinceName = state  
localityName = city  
organizationName = huizhou92  
commonName = hello-world  
  
[v3_req]  
subjectAltName = @alt_names  
  
[alt_names]  
DNS.1 = localhost

其中内容 跟 以前创建ca的时候差不多。
2. 生成ca根证书

1
openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -subj "/CN=localhost" -days 3650  -nodes

-nodes 是忽略密码,方便使用,但是请注意,这可能会降低私钥的安全性,因为任何人都可以读取未加密的私钥。
3. 生成服务端证书

1
2
 openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=localhost" -config openssl.cnf 
 openssl x509 -req -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -extensions v3_req -extfile openssl.cnf
  1. 生成客户端证书
1
2
3
openssl req -newkey rsa:2048 -nodes -keyout client.key -out client.csr -subj "/CN=localhost" -config openssl.cnf

openssl x509 -req -in client.csr -out client.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -extensions v3_req -extfile openssl.cnf

最终生成的结果如下

1
2
➜  keys git:(day1) ✗ ls 
ca.crt      ca.key      ca.srl      client.crt  client.csr  client.key  openssl.cnf server.crt  server.csr  server.key

测试

我们定义一个最简单的grpc接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// helloworld.proto  
syntax = "proto3";  
option go_package = "./api;api";  
package api;  
  
// The greeting service definition.  
service Greeter {  
  // Sends a greeting  
  rpc SayHello (HelloRequest) returns (HelloReply) {}  
}  
  
// The request message containing the user's name.  
message HelloRequest {  
  string name = 1;  
}  
  
// The response message containing the greetings  
message HelloReply {  
  string message = 1;  
}

服务端实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package main  
  
import (  
    "context"  
    "crypto/tls"    
    "crypto/x509"    
    "fmt"    
    "google.golang.org/genproto/googleapis/rpc/errdetails"    
    "google.golang.org/grpc"    
    "google.golang.org/grpc/codes"    
    "google.golang.org/grpc/credentials"    
    "google.golang.org/grpc/status"    
    "hello-world/api"    
    "io"    
    "log"    
    "net"    
    "os"    
    "time"
)  
  
type server struct {  
    api.UnimplementedGreeterServer  
}  
  
func (s *server) SayHello(ctx context.Context, in *api.HelloRequest) (*api.HelloReply, error) {  
    log.Printf("Received: %v", in.GetName())  
    select {  
    case <-ctx.Done():  
       log.Println("client timeout return")  
       return nil, ErrorWithDetails()  
    case <-time.After(3 * time.Second):  
       return &api.HelloReply{Message: "Hello " + in.GetName()}, nil  
    }  
}  

func main() {  
  
    certificate, err := tls.LoadX509KeyPair("./keys/server.crt", "./keys/server.key")  
    if err != nil {  
       log.Fatalf("Failed to load key pair: %v", err)  
    }  
    // 通过CA创建证书池  
    certPool := x509.NewCertPool()  
    ca, err := os.ReadFile("./keys/ca.crt")  
    if err != nil {  
       log.Fatalf("Failed to read ca: %v", err)  
    }  
  
    // 将来自CA的客户端证书附加到证书池  
    if ok := certPool.AppendCertsFromPEM(ca); !ok {  
       log.Fatalf("Failed to append ca certificate")  
    }  
  
    opts := []grpc.ServerOption{  
       grpc.Creds( // 为所有传入的连接启用TLS  
          credentials.NewTLS(&tls.Config{  
             ClientAuth:   tls.RequireAndVerifyClientCert,  
             Certificates: []tls.Certificate{certificate},  
             ClientCAs:    certPool,  
          },  
          )),  
    }  
  
    listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", 50051))  
    if err != nil {  
       log.Fatalf("failed to listen %d port", 50051)  
    }  
    // 通过传入的TLS服务器凭证创建新的gRPC服务实例  
    s := grpc.NewServer(opts...)  
    api.RegisterGreeterServer(s, &server{})  
    log.Printf("server listening at %v", listen.Addr())  
    if err := s.Serve(listen); err != nil {  
       log.Fatalf("Failed to serve: %v", err)  
    }  
  
}  
  
func ErrorWithDetails() error {  
    st := status.Newf(codes.Internal, fmt.Sprintf("something went wrong: %v", "api.Getter"))  
    v := &errdetails.PreconditionFailure_Violation{ //errDetails  
       Type:        "test",  
       Subject:     "12",  
       Description: "32",  
    }  
    br := &errdetails.PreconditionFailure{}  
    br.Violations = append(br.Violations, v)  
    st, _ = st.WithDetails(br)  
    return st.Err()  
}

我们直接运行服务端 go run main.go

客户端

首先我们使用一个不带证书的请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func Test_server_SayHello_No_Cert(t *testing.T) {  
    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))  
    if err != nil {  
       log.Fatalf("Connect to %s failed", "localhost:50051")  
    }  
    defer conn.Close()  
  
    client := api.NewGreeterClient(conn)  
    // 创建带有超时时间的上下文, cancel可以取消上下文  
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)  
    defer cancel()  
    // 业务代码处理部分 ...    r, err := client.SayHello(ctx, &api.HelloRequest{Name: "Hello"})  
    if err != nil {  
       log.Printf("Failed to greet, error: %v", err)  
    } else {  
       log.Printf("Greeting: %v", r.GetMessage())  
    }  
    // Set up a connection to the server.  
    log.Printf("Greeting: %s", r.GetMessage())  
}

输出

1
2024/05/12 19:18:51 Failed to greet, error: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: EOF"

服务不可用

我们再使用一个携带证书的请请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func Test_server_SayHello(t *testing.T) {  
    certificate, err := tls.LoadX509KeyPair("./keys/client.crt", "./keys/client.key")  
    if err != nil {  
       log.Fatalf("Failed to load client key pair, %v", err)  
    }  
  
    certPool := x509.NewCertPool()  
    ca, err := os.ReadFile("./keys/ca.crt")  
    if err != nil {  
       log.Fatalf("Failed to read %s, error: %v", "./keys/ca.crt", err)  
    }  
  
    if ok := certPool.AppendCertsFromPEM(ca); !ok {  
       log.Fatalf("Failed to append ca certs")  
    }  
  
    opts := []grpc.DialOption{  
       grpc.WithTransportCredentials(credentials.NewTLS(  
          &tls.Config{  
             ServerName:   "localhost",  
             Certificates: []tls.Certificate{certificate},  
             RootCAs:      certPool,  
          })),  
    }  
  
    // conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))  
    conn, err := grpc.Dial("localhost:50051", opts...)  
    if err != nil {  
       log.Fatalf("Connect to %s failed", "localhost:50051")  
    }  
    defer conn.Close()  
  
    client := api.NewGreeterClient(conn)  
    // 创建带有超时时间的上下文, cancel可以取消上下文  
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)  
    defer cancel()  
    // 业务代码处理部分 ...    r, err := client.SayHello(ctx, &api.HelloRequest{Name: "Hello"})  
    if err != nil {  
       log.Printf("Failed to greet, error: %v", err)  
    } else {  
       log.Printf("Greeting: %v", r.GetMessage())  
    }  
    // Set up a connection to the server.  
    log.Printf("Greeting: %s", r.GetMessage())  
}

输出

1
2
=== RUN   Test_server_SayHello
2024/05/12 19:20:17 Greeting: Hello Hello

总结

  1. 我们可以使用tls实现gRPC 的加密通信,
  2. 从go1.15 开始,go不建议使用CA而是使用SAN证书
true
最后更新于 Jun 29, 2024 21:41 CST
使用 Hugo 构建
主题 StackJimmy 设计