在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 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证书:
# 生成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
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)
}
}
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=client.com
# 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不在白名单中,则拒绝连接。
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”,连接无法建立,此时会报错如下:
hellosvc_test.go:: rpc error: code =
Unavailable desc = connection error: desc =
"error reading server preface: remote error: tls: bad certificate"
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:%v\n",
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.key
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 尝试连接服务。因为证书不合法,会直接报错:
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
也可以通过校验,与服务建立连接