前言
为了保证 gRPC 服务不被第三方监听和调用, 防止通信被篡改或伪造, 需要对 gRPC 服务添加身份验证机制.
目前 gRPC 内置了三种身份验证机制:
- SSL/TLS: gRPC 集成了 SSL/TLS, 提倡使用 SSL/TLS 对服务器进行身份验证, 并对客户端与服务器之间交换的所有数据进行加密。同时可选让客户端提供用于相互验证的证书.
- 基于 Token 的身份验证: gRPC 提供了一种可将基于元数据的凭证附加到请求和响应中的机制. 这种机制必须与 SSL/TLS 同时使用, 以提供完整的身份验证和加密通信.
- ATLS: Google 自家的双向身份验证和传输安全系统, 通常也用于在 Google 家的平台上构建服务时.
本文介绍 gRPC 中的 SSL/TLS.
TIP提示
本文环境
- Debian 12 amd64
- Go 1.21.3
SAN, CA 和证书
使用 TLS 就需要证书. 在 Go 1.15 版本之后废弃了 CommonName
, 而是要使用 Subject Alternative Name
(SAN) 来验证证书的有效性. 因此如果按照以前的方法生成证书, 大概率会导致下面的错误:
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"
SAN 是 x509 定义中的一个扩展, 用于指定证书的使用范围. 使用了 SAN 扩展的证书, 可以在证书中指定多个域名或 IP 地址, 从而可以在一个证书中同时包含多个域名或 IP 地址.
CA (Certificate Authority), 即证书颁发机构, 用于管理和签发证书. 作用是检查证书持有者的合法性, 并颁发证书, 防止证书被伪造.
CA 自己也需要有证书, 称为根证书, 用于签发其他证书. 一般情况下, 根证书是自签名的, 即自己给自己签发证书. 根证书是信任链的起点, 有了根证书之后 CA 才能给其他人签发证书.
因此, 要想在 gRPC 中使用 TLS, 需要准备三组证书:
- CA 根证书
- 服务端证书
- 客户端证书
生成证书
生成 CA 证书
在任意目录下创建 ca.conf
文件:
[ req ]
default_bits = 4096
distinguished_name = moe
[ moe ]
countryName = GB
countryName_default = BeiJing
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = BeiJing
localityName = Locality Name (eg, city)
localityName_default = NanJing
organizationName = Organization Name (eg, company)
organizationName_default = Kompany
commonName = krau.top
commonName_max = 64
commonName_default = krau.top
执行下面的命令生成 CA 私钥:
openssl genrsa -out ca.key 4096
然后生成 CA 证书:
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config ca.conf
根据提示输入各种信息, 最后会生成 ca.crt
文件.
生成服务端证书
新建 server.conf
:
[ req ]
default_bits = 2048
distinguished_name = moe
[ moe ]
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 = ovocom
commonName = CommonName (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = localhost # 此值应该包含在 [alt_names] 中
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP = 127.0.0.1
WARNING注意
commonName_default
的值应该包含在 [alt_names]
中, 并且在连接时, 客户端的值需要与其匹配.
生成私钥:
openssl genrsa -out server.key 2048
生成 csr:
openssl req -new -key server.key -out server.csr -config server.conf
请求 CA 签发证书:
openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extensions req_ext -extfile server.conf
生成客户端证书
客户端不需要配置文件, 直接生成私钥:
openssl genrsa -out client.key 2048
生成 csr:
openssl req -new -key client.key -out client.csr
请求 CA 签发证书:
openssl x509 -req -days 3650 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
现在已经准备好证书了. 服务端需要 server.crt
和 server.key
, 客户端需要 client.crt
和 client.key
. 此外, 两者都需要 ca.crt
.
实现
服务端
关注高亮行
package main
import (
"crypto/tls"
"crypto/x509"
"net"
"os"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"example/proto"
)
func main() {
pair, err := tls.LoadX509KeyPair("./server.crt", "./server.key")
if err != nil {
fmt.Printf("Failed to load certificates: %s", err)
return
}
certPool := x509.NewCertPool()
ca, err := os.ReadFile("./ca.crt") // 读取根证书
if err != nil {
fmt.Printf("Failed to read ca certificate: %s", err)
return
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
fmt.Println("Failed to append ca certificate")
return
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{pair},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
})
s := grpc.NewServer(grpc.Creds(creds))
proto.RegisterArtworkServiceServer(s, &server{})
lis, err := net.Listen("tcp", ":39010")
if err != nil {
fmt.Printf("Failed to listen: %s", err)
return
}
if err := s.Serve(lis); err != nil {
fmt.Printf("Failed to serve: %s", err)
return
}
}
客户端
关注高亮行
package main
import (
"crypto/tls"
"crypto/x509"
"os"
"example/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
pair, err := tls.LoadX509KeyPair("./client.crt", "./client.key")
if err != nil {
fmt.Printf("Failed to load certificates: %s", err)
return
}
certPool := x509.NewCertPool()
ca, err := os.ReadFile("./ca.crt")
if err != nil {
fmt.Printf("Failed to read ca certificate: %s", err)
return
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
fmt.Println("Failed to append ca certificate")
return
}
cred := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{pair},
ServerName: "localhost", // 服务端生成证书时的 commonName
RootCAs: certPool,
})
conn, err := grpc.Dial("127.0.0.1:39010", grpc.WithTransportCredentials(cred))
if err != nil {
fmt.Printf("Failed to dial: %s", err)
return
}
defer conn.Close()
client := proto.NewArtworkServiceClient(conn)
// ...
}
参考
To Be Continued.