写给go开发者的gRPC教程-通信安全
使用 TLS 安全传输数据
什么是 SSL/TLS
SSL 包含记录层(Record Layer)和传输层[1],记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509[2]认证,之后利用非对称加密演算来对通信方做身份认证,之后交换对称密匙作为会话密匙(Session key[3])。这个会谈密匙是用来将通信两方交换的资料做加密,保证两个应用间通信的保密性和可靠性,使客户与服务器应用之间的通信不被攻击者窃听。
--- 维基百科
简单点说就是:SSL/TLS 是一个安全协议,它通过一系列的手段、一系列的算法让客户端与服务端之间加密传输数据,避免数据被攻击者窃听。快速入门 TLS 可以参考:一文带你快速入门 TLS/SSL
SSL/TLS 分为单向认证和双向认证(mtls)
单向认证
在单向认证中,仅客户端验证服务端
当客户端和服务端建立连接之后,服务端会发送公开的证书给客户端,客户端验证证书后使用证书中包含的密钥信息来发送加密数据(实际要比这个复杂,这里简化了交互流程)
TLS 证书可以使用根证书创建子证书,因此对于证书有两种使用方式,一种是直接使用根证书,一种是使用由根证书签发的子证书
✨ 直接使用根证书
所以对于服务端来说需要两个文件
server.key
:RSA 的私钥,用来进行数字签名
server.pem/server.crt
:自签名的服务端证书,其中包含与私钥对应的公钥、网站域名、签名算法等信息
对于客户端来说不需要准备文件
服务端代码如下:
1)NewServerTLSFromFile
加载证书 2)NewServer
时指定 Creds
func main() {
l, err := net.Listen("tcp", ":8009")
if err != nil {
panic(err)
}
// method 1.
creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
if err != nil {
panic(err)
}
// method 2.
// cert , err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
// if err != nil {
// panic(err)
// }
// creds := credentials.NewServerTLSFromCert(&cert)
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterOrderManagementServer(s, &server{})
if err := s.Serve(l); err != nil {
panic(err)
}
}
客户端代码如下:
1)NewClientTLSFromFile
指定使用 CA 证书来校验服务端的证书有效性。
- 注意:第二个参数域名就是服务端证书时的 CN 参数
2)建立连接时 指定建立安全连接WithTransportCredentials
func main() {
creds, err := credentials.NewClientTLSFromFile("./x509/server.crt", "www.example.com")
if err != nil {
panic(err)
}
conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer conn.Close()
client := pb.NewOrderManagementClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Get Order
retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
if err != nil {
panic(err)
}
log.Print("GetOrder Response -> : ", retrievedOrder)
}
✨ 根证书模式
在生产环境通常是生成一个 CA 根证书,然后使用 CA 根证书去签名多个服务端的证书。这种方式可以一次管理多个证书,也比较贴近真实情况,当然也可以用来做证书过期、更新等试验,缺点是操作起来略微麻烦一点。
rootCA.key
:ca 机构的私钥,用来给服务端签发证书
rootCA.crt
:ca 的证书,用来给客户端验证服务端证书
server.key
:RSA 的私钥,用来进行数字签名
server.pem/server.crt
:由 ca 签发的服务端证书
服务端代码如下:
1)NewServerTLSFromFile
加载证书 2)NewServer
时指定 Creds
func main() {
l, err := net.Listen("tcp", ":8009")
if err != nil {
panic(err)
}
// method 1.
creds, err := credentials.NewServerTLSFromFile("./x509/server.crt", "./x509/server.key")
if err != nil {
panic(err)
}
// method 2.
// cert , err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
// if err != nil {
// panic(err)
// }
// creds := credentials.NewServerTLSFromCert(&cert)
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterOrderManagementServer(s, &server{})
if err := s.Serve(l); err != nil {
panic(err)
}
}
客户端代码如下:
1)NewClientTLSFromFile
指定使用CA 根证书来校验服务端的证书有效性。
- 注意:第二个参数域名就是服务端证书时的 CN 参数
2)建立连接时 指定建立安全连接WithTransportCredentials
func main() {
creds, err := credentials.NewClientTLSFromFile("./x509/rootCa.crt", "www.example.com")
if err != nil {
panic(err)
}
conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer conn.Close()
client := pb.NewOrderManagementClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Get Order
retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
if err != nil {
panic(err)
}
log.Print("GetOrder Response -> : ", retrievedOrder)
}
双向认证(mTLS)
server-side TLS 中虽然服务端使用了证书,但是客户端却没有使用证书,本章节会给客户端也生成一个证书,并完成 mutual TLS
直接使用根证书
在 mTLS 中很少会有直接使用根证书的场景,这里仅放一个交互图,不放代码了
根证书模式
rootCA.key
:ca 机构的私钥,用来给服务端签发证书
rootCA.crt
:ca 的证书,用来给客户端验证服务端证书
server.key
:服务端 RSA 的私钥,用来进行数字签名
server.pem/server.crt
:由 ca 签发的服务端证书
client.key
: 客户端 RSA 私钥,用来进行数字签名
client.pem/client.crt
: 由 ca 签发的客户端证书
服务端代码如下:
1)加载服务端证书
2)构建用于校验客户端证书的CertPool
3)使用上面的参数构建一个TransportCredentials
4)newServer
是指定使用前面创建的creds
看似改动很大,其实如果你仔细查看了前面
NewServerTLSFromFile
方法做的事的话,就会发现是差不多的,只有极个别参数不同。修改点如下:
1)
tls.Config
的参数ClientAuth
,这里改成了tls.RequireAndVerifyClientCert
,即服务端也必须校验客户端的证书,之前使用的默认值(即不校验)2)
tls.Config
的参数ClientCAs
,由于之前都不校验客户端证书,所以也没有指定用什么证书来校验
func main() {
l, err := net.Listen("tcp", ":8009")
if err != nil {
panic(err)
}
certificate, err := tls.LoadX509KeyPair("./x509/server.crt", "./x509/server.key")
if err != nil {
panic(err)
}
// 创建CertPool,后续就用池里的证书来校验客户端证书有效性
// 所以如果有多个客户端 可以给每个客户端使用不同的 CA 证书,来实现分别校验的目的
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("./x509/rootCa.crt")
if err != nil {
panic(err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatal("failed to append certs")
}
// 构建基于 TLS 的 TransportCredentials
creds := credentials.NewTLS(&tls.Config{
// 设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{certificate},
// 要求必须校验客户端的证书 可以根据实际情况选用其他参数
ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
// 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
ClientCAs: certPool,
})
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterOrderManagementServer(s, &server{})
if err := s.Serve(l); err != nil {
panic(err)
}
}
客户端代码如下:
客户端改动和前面服务端差不多,具体步骤都一样,除了不能指定校验策略之外基本一样。
func main() {
// 加载客户端证书
certificate, err := tls.LoadX509KeyPair("x509/client.crt", "x509/client.key")
if err != nil {
log.Fatal(err)
}
// 构建CertPool以校验服务端证书有效性
b, err := ioutil.ReadFile("./x509/rootCa.crt")
if err != nil {
log.Fatal(err)
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
log.Fatal("credentials: failed to append certificates")
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ServerName: "www.example.com",
RootCAs: cp,
})
conn, err := grpc.Dial("localhost:8009", grpc.WithTransportCredentials(creds))
if err != nil {
panic(err)
}
defer conn.Close()
client := pb.NewOrderManagementClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Get Order
retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
if err != nil {
panic(err)
}
log.Print("GetOrder Response -> : ", retrievedOrder)
}
可能遇到的问题
报错:transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs instead
如果出现上述报错,是因为 go 1.15 版本开始废弃 CommonName[4],因此推荐使用 SAN 证书。如果想兼容之前的方式,需要设置环境变量 GODEBUG 为 GODEBUG=x509ignoreCN=0
。
什么是 SAN?SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。
SAN 证书包含 Subject Alternative Name 部分
openssl x509 -text -noout -in server.crt | grep -A 1 "Subject Alternative Name"
X509v3 Subject Alternative Name:
DNS:www.example.com, DNS:localhost, DNS:127.0.0.1, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
证书生成
📖 方式一:生成自签名的证书
openssl req -config conf/openssl.cnf -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout server.key \
-out server.crt
📖 方式二:根证书签发子证书
1、创建自签名的 ca 根证书
openssl req -config conf/openssl.cnf -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout rootCa.key \
-out rootCa.crt
2、创建服务端私钥和 CSR(证书签名请求),这里没有指定-x509, 说明这不是一个自签署的证书,而是要交给 CA 签名认证
openssl req -config conf/openssl.cnf -newkey rsa:2048 -nodes \
-keyout server.key \
-out server.csr
通过下列命令检查一下生成的 csr 文件是否信息正确无误。
openssl req -text -noout -verify -in server.csr
3、使用证书机构的私钥、根证书和 example.com 的 CSR 创建数字证书
⚠️ 不加-extfile 和-extensions 参数是不行的
openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-extfile conf/openssl.cnf \
-extensions v3_req \
-out server.crt
签署后,查看证书的内容:
openssl x509 -text -noout -in server.crt