go: grpc tls 应用一览 ## 生成证书 在go 1.15以上版本,必须使用SAN方式,否则会报`"transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs instead"` 下面的示例在 go 1.19版本验证 rsa证书: ```bash # 生成ca证书 ca.key是私钥,服务管理员必须好好保管 openssl genrsa -out ca.key 2048 openssl req -days 3560 -new -x509 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Hello/CN=Hello Root CA" -out ca.crt # 生成服务端证书并使用ca证书签发,注意`CN=hellosvc.com`,后续代码中需要用到。 # server.key是私钥,必须好好保管 openssl req -newkey rsa:2048 -nodes -keyout ./server/server.key -subj "/C=CN/ST=GD/L=SZ/O=Hello/CN=hellosvc.com" -out ./server/server.csr openssl x509 -days 3560 -req -extfile <(printf "subjectAltName=DNS:hellosvc.com") -in ./server/server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out ./server/server.crt # 生成客户端证书,这两个步骤由客户执行。执行完后,将client.csr提交给服务管理员,等待签发 # CN=client.com 可以用做认证信息,以区分不同的客户 openssl genrsa -out client/client.key 2048 openssl req -new -key client/client.key -subj "/C=CN/ST=GD/L=SZ/O=HelloClient/CN=client.com/emailAddress=client@hello.com" -out client/client.csr # 使用ca签发,需要使用到ca.key,由服务管理员执行 openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -in client/client.csr -out client/client.crt ``` ecdsa证书: ```bash # 生成ca证书 ca.key是私钥,服务管理员必须好好保管 openssl ecparam -name prime256v1 -genkey -noout -out ca.key openssl req -days 3560 -new -x509 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Hello/CN=Hello Root CA" -out ca.crt # 生成服务端证书并使用ca证书签发,注意`CN=hellosvc.com`,后续代码中需要用到。 # server.key是私钥,必须好好保管 openssl ecparam -name prime256v1 -genkey -noout -out server/server.key openssl req -new -sha256 -key server/server.key -subj "/C=CN/ST=GD/L=SZ/O=Hello/CN=hellosvc.com" -out server/server.csr openssl x509 -days 3560 -req -extfile <(printf "subjectAltName=DNS:hellosvc.com") -in ./server/server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out ./server/server.crt # 生成客户端证书,这两个步骤由客户执行。执行完后,将client.csr提交给服务管理员,等待签发 # CN=client.com 可以用做认证信息,以区分不同的客户 openssl ecparam -name prime256v1 -genkey -noout -out client/client.key openssl req -new -sha256 -key client/client.key -subj "/C=CN/ST=GD/L=SZ/O=HelloClient/CN=client.com/emailAddress=client@hello.com" -out client/client.csr # 使用ca签发,需要使用到ca.key,由服务管理员执行 openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -in client/client.csr -out client/client.crt ``` ## 双向认证 ### 服务端代码 ```go func tlsGrpcSvr() { // 加载服务端私钥和证书 cert, err := tls.LoadX509KeyPair("./server/server.crt", "./server/server.key") if err != nil { panic(err) } // 生成证书池,将根证书加入证书池 certPool := x509.NewCertPool() rootBuf, err := ioutil.ReadFile("ca.crt") if err != nil { panic(err) } if !certPool.AppendCertsFromPEM(rootBuf) { panic("Fail to append ca") } // 初始化TLSConfig // ClientAuth有5种类型,如果要进行双向认证必须是RequireAndVerifyClientCert tlsConf := &tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, Certificates: []tls.Certificate{cert}, ClientCAs: certPool, } var keepAliveArgs = keepalive.ServerParameters{ Time: 10 * time.Second, Timeout: 20 * time.Second, MaxConnectionAge: 30 * time.Second, } lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 6688)) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer( grpc.KeepaliveParams(keepAliveArgs), grpc.MaxSendMsgSize(1024*1024*4), grpc.Creds(credentials.NewTLS(tlsConf)), ) // 注册服务 pb.RegisterHelloSvcServer(s, &services.HelloServer{}) reflection.Register(s) fmt.Printf("run:%v\n", 6688) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } ``` ### 客户端代码 ```go func TestSendHelloTls(t *testing.T) { // 加载客户端私钥和证书 cert, err := tls.LoadX509KeyPair("/root/workspace/personal/grpc_example/client/client.crt", "/root/workspace/personal/grpc_example/client/client.key") if err != nil { panic(err) } // 将根证书加入证书池 certPool := x509.NewCertPool() rootBuf, err := ioutil.ReadFile("/root/workspace/personal/grpc_example/ca.crt") if err != nil { panic(err) } if !certPool.AppendCertsFromPEM(rootBuf) { panic("Fail to append ca") } // 注意ServerName需要与服务器证书内的CN一致 creds := credentials.NewTLS(&tls.Config{ ServerName: "hellosvc.com", Certificates: []tls.Certificate{cert}, RootCAs: certPool, }) conn, err := grpc.Dial("127.0.0.1:6688", grpc.WithTransportCredentials(creds)) if err != nil { panic(err) } defer conn.Close() client := hello.NewHelloSvcClient(conn) // ... } ``` 此时,客户端必须要用ca签发的证书才能和服务端建立连接 ### 添加额外检验 - 只允许特定的CN(common name) 在创建客户端证书的时候,指定了CN=client.com ```bash # CN=client.com openssl req -new -key client/client.key -subj "/C=CN/ST=GD/L=SZ/O=HelloClient/CN=client.com/emailAddress=client@hello.com" -out client/client.csr ``` grpc允许在tls流程中对证书进行额外的检验,如下所示 定义`VerifyPeerCertificate`,当CN不在白名单中,则拒绝连接。 ```go tlsConf := &tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, Certificates: []tls.Certificate{cert}, ClientCAs: certPool, VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { for _, chain := range verifiedChains { for _, v := range chain { if !slices.Contains([]string{"client.com", "Hello Root CA"}, v.Subject.CommonName) { return fmt.Errorf("common name is invalid") } } } return nil }, } ``` 注意,chains里会包含CA的信息,所以把CA的common name `Hello Root CA` 也包含进去。 有了这个验证,即使证书是CA签发的,只要common name != "client.com",连接无法建立,此时会报错如下: ```bash hellosvc_test.go:60: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: remote error: tls: bad certificate" ``` ## 仅服务端tls ### 服务端代码 ```go lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 6688)) if err != nil { log.Fatalf("failed to listen: %v", err) } // 指定使用服务端证书创建一个 TLS credentials。 creds, err := credentials.NewServerTLSFromFile("./server/server.crt", "./server/server.key") if err != nil { log.Fatalf("failed to create credentials: %v", err) } // 指定使用 TLS credentials。 s := grpc.NewServer(grpc.Creds(creds)) pb.RegisterHelloSvcServer(s, &services.HelloServer{}) reflection.Register(s) fmt.Printf("run:%v\n", 6688) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } ``` ### 客户端代码 ```go root := "/root/workspace/personal/grpc_example/" // 客户端通过ca证书来验证服务的提供的证书 creds, err := credentials.NewClientTLSFromFile(root+"ca.crt", "hellosvc.com") if err != nil { log.Fatalf("failed to load credentials: %v", err) } // 建立连接时指定使用 TLS conn, err := grpc.Dial("127.0.0.1:6688", grpc.WithTransportCredentials(creds)) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() client := hello.NewHelloSvcClient(conn) ``` ## 错误的签发证书无法建立连接 尝试创建另一个ca2根证书,并签发client2证书,这个证书不可与上述的服务建立连接 ```bash openssl genrsa -out ca2.key 2048 openssl req -new -x509 -key ca2.key -subj "/C=CN/ST=GD/L=SZ/O=Test, Inc./CN=Test Root CA" -out ca2.crt openssl genrsa -out client/client2.key 2048 openssl req -new -key client/client2.key -subj "/C=CN/ST=GD/L=SZ/O=HelloClient/CN=client.com/emailAddress=client@hello.com" -out client/client2.csr openssl x509 -req -sha256 -CA ca2.crt -CAkey ca2.key -CAcreateserial -days 365 -in client/client2.csr -out client/client2.crt ``` 在客户端代码中使用 client2.key 尝试连接服务。因为证书不合法,会直接报错: ```bash hellosvc_test.go:60: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate signed by unknown authority (possibly because of \"crypto/rsa: verification error\" while trying to verify candidate authority certificate \"Test Root CA\")" ``` ## 使用代码签发证书 在上面的步骤,当客户提交了 csr 文件后,可以使用命令对csr进行签发,并生成crt证书 ```bash openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -in client/client.csr -out client/client.crt ``` 有时,命令行的方式并不方便,需要使用代码方式签发证书(如建立一个签发的api),示例如下: 参考:https://stackoverflow.com/questions/42643048/signing-certificate-request-with-certificate-authority ```go func TestSigningCsr(t *testing.T) { root := "/root/workspace/personal/grpc_example/" // load CA key pair // public key caPublicKeyFile, err := ioutil.ReadFile(root + "/ca.crt") if err != nil { panic(err) } pemBlock, _ := pem.Decode(caPublicKeyFile) if pemBlock == nil { panic("pem.Decode failed") } caCRT, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { panic(err) } // private key caPrivateKeyFile, err := ioutil.ReadFile(root + "ca.key") if err != nil { panic(err) } pemBlock, _ = pem.Decode(caPrivateKeyFile) if pemBlock == nil { panic("pem.Decode failed") } // der, err := x509.DecryptPEMBlock(pemBlock, []byte("ca private key password")) // if err != nil { // panic(err) // } // caPrivateKey, err := x509.ParsePKCS1PrivateKey(der) // 解析rsa证书 // caPrivateKey, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes) // 解析ec证书 caPrivateKey, err := x509.ParseECPrivateKey(pemBlock.Bytes) if err != nil { panic(err) } // load client certificate request clientCSRFile, err := ioutil.ReadFile(root + "client/client2.csr") if err != nil { panic(err) } pemBlock, _ = pem.Decode(clientCSRFile) if pemBlock == nil { panic("pem.Decode failed") } clientCSR, err := x509.ParseCertificateRequest(pemBlock.Bytes) if err != nil { panic(err) } if err = clientCSR.CheckSignature(); err != nil { panic(err) } // create client certificate template clientCRTTemplate := x509.Certificate{ Signature: clientCSR.Signature, SignatureAlgorithm: clientCSR.SignatureAlgorithm, PublicKeyAlgorithm: clientCSR.PublicKeyAlgorithm, PublicKey: clientCSR.PublicKey, SerialNumber: big.NewInt(2), Issuer: caCRT.Subject, Subject: clientCSR.Subject, NotBefore: time.Now(), NotAfter: time.Now().Add(24 * time.Hour * 365), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } // create client certificate from template and CA public key clientCRTRaw, err := x509.CreateCertificate(rand.Reader, &clientCRTTemplate, caCRT, clientCSR.PublicKey, caPrivateKey) if err != nil { panic(err) } // save the certificate clientCRTFile, err := os.Create(root + "client/client2.crt") if err != nil { panic(err) } pem.Encode(clientCRTFile, &pem.Block{Type: "CERTIFICATE", Bytes: clientCRTRaw}) clientCRTFile.Close() } ``` 生成的`client2.crt`也可以通过校验,与服务建立连接 来自 大脸猪 写于 2022-12-28 19:55 -- 更新于2022-12-29 12:58 -- 1 条评论