Skip to content

gRPC中的证书认证

1. 介绍

gRPC建立在HTTP/2协议之上,对TLS提供了很好的支持。当不需要证书认证时,可通过grpc.WithInsecure()选项跳过了对服务器证书的验证,没有启用证书的gRPC服务和客户端进行的是明文通信,信息面临被任何第三方监听的风险。为了保证gRPC通信不被第三方监听、篡改或伪造,可以对服务器启动TLS加密特性。

2. 概念简述

2.1 什么是CA

CACertificate Authority的缩写,也叫“证书授权中心”。它是负责管理和签发证书的第三方机构,作用是检查证书持有者身份的合法性,并签发证书,以防证书被伪造或篡改。

CA实际上是一个机构,负责“证件”印制核发。就像负责颁发身份证的公安局、负责发放行驶证、驾驶证的车管所。

2.2 什么是CA证书

CA 证书就是CA颁发的证书。我们常听到的数字证书就是CA证书,CA证书包含信息有: 证书拥有者的身份信息,CA机构的签名,公钥和私钥 ,

  • 身份信息: 用于证明证书持有者的身份
  • CA机构的签名: 用于保证身份的真实性
  • 公钥和私钥: 用于通信过程中加解密,从而保证通讯信息的安全性

2.3 什么是SAN

SAN(Subject Alternative Name)SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。

2.4 为什么需要SAN

先看下不通过SAN生成的证书,会报的错误信息。

go rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"

出现上述错误,原因是因为 从go 1.15 版本开始废弃 CommonName,因此推荐使用 SAN 证书。

3. 证书生成流程

3.1 生成CA证书

1. 新增ca.conf

[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = GB
countryName_default         = BeiJing
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = JiangSu
localityName                = Locality Name (eg, city)
localityName_default        = NanJing
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = liuqh.icu
commonName_max              = 64
commonName_default          = liuqh.icu

2. 生成CA私钥

# 生成CA私钥
$ openssl genrsa -out ca.key 4096

3. 生成CA证书

$ openssl req -new -x509 -days 365 -subj "/C=GB/L=Beijing/O=github/CN=liuqh.icu" \
-key ca.key -out ca.crt -config ca.conf
  • C=GB: C代表的是国家名称代码。
  • L=Beijing: 代表地方名称,例如城市。
  • O=gobook: 代表组织单位名称。
  • CN=liuqh.icu: 代表关联的域名,

更多参数含义:https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm#CreatingYourCSR

3.2 生成服务端证书

1. 新增server.conf

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = JiangSu
localityName                = Locality Name (eg, city)
localityName_default        = NanJing
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = XXX(自定义,客户端需要此字段做匹配)
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1   = liuqh.icu
IP      = 127.0.0.1

2.生成公私钥

$ openssl genrsa -out server.key 2048

3. 生成CSR

$ openssl req -new  -subj "/C=GB/L=Beijing/O=github/CN=liuqh.icu" \
-key server.key -out server.csr -config server.conf

4. 基于CA签发证书

$ openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 \
-in server.csr -out server.crt -extensions req_ext -extfile server.conf

3.4 生成客户端证书

1. 生成公私钥

$ openssl genrsa -out client.key 2048

2. 生成CSR

$ openssl req -new -subj "/C=GB/L=Beijing/O=github/CN=liuqh.icu"  \
-key client.key -out client.csr 

3.基于CA签发证书

$ openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 \
-in client.csr -out client.crt

4. 目录结构

图片

5.代码实现

5.1 服务端

package main

import (
 "52lu/go-rpc/server/tslservice"
 "crypto/tls"
 "crypto/x509"
 "fmt"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials"
 "io/ioutil"
 "net"
)

func main() {
 // 公钥中读取和解析公钥/私钥对
 pair, err := tls.LoadX509KeyPair("./pem/server.crt", "./pem/server.key")
 if err != nil {
  fmt.Println("LoadX509KeyPair error", err)
  return
 }
 // 创建一组根证书
 certPool := x509.NewCertPool()
 ca, err := ioutil.ReadFile("./pem/ca.crt")
 if err != nil {
  fmt.Println("read ca pem error ", err)
  return
 }
 // 解析证书
 if ok := certPool.AppendCertsFromPEM(ca); !ok {
  fmt.Println("AppendCertsFromPEM error ")
  return
 }
 cred := credentials.NewTLS(&tls.Config{
  Certificates: []tls.Certificate{pair},
  ClientAuth:   tls.RequireAndVerifyClientCert,
  ClientCAs:    certPool,
 })
 grpcServer := grpc.NewServer(grpc.Creds(cred))
 // 注册服务
 tslservice.RegisterTslServiceServer(grpcServer, new(tslservice.UnimplementedTslServiceServer))
 // 监听端口
 listen, err := net.Listen("tcp", ":1234")
 if err != nil {
  fmt.Println(err)
  return
 }
 fmt.Println("服务启动成功....")
 // 启动服务
 grpcServer.Serve(listen)
}

5.2 客户端

package main

import (
 "52lu/go-rpc/server/tslservice"
 "context"
 "crypto/tls"
 "crypto/x509"
 "fmt"
 "google.golang.org/grpc"
 "google.golang.org/grpc/credentials"
 wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
 "io/ioutil"
)

func main() {
 // 公钥中读取和解析公钥/私钥对
 pair, err := tls.LoadX509KeyPair("./pem/client.crt", "./pem/client.key")
 if err != nil {
  fmt.Println("LoadX509KeyPair error ", err)
  return
 }
 // 创建一组根证书
 certPool := x509.NewCertPool()
 ca, err := ioutil.ReadFile("./pem/ca.crt")
 if err != nil {
  fmt.Println("ReadFile ca.crt error ", err)
  return
 }
 // 解析证书
 if ok := certPool.AppendCertsFromPEM(ca); !ok {
  fmt.Println("certPool.AppendCertsFromPEM error ")
  return
 }
 cred := credentials.NewTLS(&tls.Config{
  Certificates: []tls.Certificate{pair},
  //ServerName:   "liuqh.icu",
  ServerName: "test.com",
  RootCAs:    certPool,
 })
 conn, err := grpc.Dial(":1234", grpc.WithTransportCredentials(cred))
 if err != nil {
  fmt.Println("dial error ", err)
  return
 }
 defer conn.Close()
 // 实例化客户端
 client := tslservice.NewTslServiceClient(conn)
 test, err := client.Test(context.TODO(), &wrapperspb.Int32Value{Value: 1})
 fmt.Println("res:", test)
 fmt.Println("err:", err)
}

5.3 发起请求

# 证书正常时
$ go run client.go
res: code:200 msg:"success"
err: <nil>

# 故意写错客户端中的 ServerName:test.com
$  go run client.go
res: <nil>
err: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate is valid for liuqh.icu, not test.com"