生成证书
在go 1.15以上版本,必须使用SAN方式,否则会报"transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs instead"
下面的示例在 go 1.19版本验证
rsa证书:
# 生成ca证书 ca.key是私钥,服务管理员必须好好保管openssl genrsa -out ca.key 2048openssl 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.csropenssl 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 2048openssl 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证书:
# 生成ca证书 ca.key是私钥,服务管理员必须好好保管openssl ecparam -name prime256v1 -genkey -noout -out ca.keyopenssl 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.keyopenssl req -new -sha256 -key server/server.key -subj "/C=CN/ST=GD/L=SZ/O=Hello/CN=hellosvc.com"-out server/server.csropenssl 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.keyopenssl 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
双向认证
服务端代码
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:%vn",6688)-
if err := s.Serve(lis); err !=nil{ log.Fatalf("failed to serve: %v", err)-
} }
客户端代码
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
# CN=client.comopenssl 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不在白名单中,则拒绝连接。
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") -
} -
} -
} -
returnnil -
}, -
}
注意,chains里会包含CA的信息,所以把CA的common name Hello Root CA 也包含进去。
有了这个验证,即使证书是CA签发的,只要common name != “client.com”,连接无法建立,此时会报错如下:
hellosvc_test.go:: rpc error: code =Unavailable desc = connection error: desc ="error reading server preface: remote error: tls: bad certificate"
仅服务端tls
服务端代码
lis, err := net.Listen("tcp", fmt.Sprintf(":%d",))-
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:%vn",6688)-
if err := s.Serve(lis); err !=nil{ log.Fatalf("failed to serve: %v", err)-
}
客户端代码
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证书,这个证书不可与上述的服务建立连接
openssl genrsa -out ca2.keyopenssl req -new -x509 -key ca2.key -subj "/C=CN/ST=GD/L=SZ/O=Test, Inc./CN=Test Root CA"-out ca2.crtopenssl genrsa -out client/client2.key 2048openssl 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.csropenssl x509 -req -sha256 -CA ca2.crt -CAkey ca2.key -CAcreateserial-days 365-in client/client2.csr -out client/client2.crt
在客户端代码中使用 client2.key 尝试连接服务。因为证书不合法,会直接报错:
hellosvc_test.go:: 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证书
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial-days-in client/client.csr -out client/client.crt
有时,命令行的方式并不方便,需要使用代码方式签发证书(如建立一个签发的api),示例如下:
参考:https://stackoverflow.com/questions/42643048/signing-certificate-request-with-certificate-authority
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也可以通过校验,与服务建立连接


