GOLANG:自建gRPC的TLS双向验证(OpenSSL实现)
Posted 大悦天
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GOLANG:自建gRPC的TLS双向验证(OpenSSL实现)相关的知识,希望对你有一定的参考价值。
哈哈哈,没想到吧,勤(HAO)劳(CHI)勇(LAN)敢(ZUO)的键者还活着吧?今天键者就来梳理一下数字签名和数字证书篇在 gRPC
中的应用。
才不是因为键者周末无意中翻出了这个年初写了一半的稿子……
众所周知, gRPC
是谷歌开源的一套远程调用框架,它在应用层复用 HTTP2
协议,而自建服务时同样需要处理之前提到的权限问题,客户端怎么证明自己有权限调用,服务器又怎么证明自己不是伪造的服务器呢?
背景
首先, gRPC
设计之初,就已经考虑了这个问题,它同时支持 TLS
和 Token
两种实现鉴权逻辑(而且两者可以共存)。其中 Token
的形式主要通过 Interceptor
+ 自建鉴权
的方式实现,相信有对接过三方接口(例如微博)或者开发过类似服务的童鞋都很好理解,无非鉴权管理策略的实现和管控,这块需要结合各自的业务情况规划,键者就不多说了。
注意
TLS
方案和Token
方案是互补的方案,而不是互斥的哦。TLS
更注重Connection
的身份鉴别,以及通讯内容加密。Token
可以映射到具体的用户状态,以及Chanel
级别的权限控制.
键者今天就讨论一下 gRPC
基于 TLS
的双向鉴权实现。
虽然 TLS
听起来很陌生,但其实我们Web开发中接触得并不少,那就是 HTTPS
,其中的 S
有一种解释就是 HTTPOverTLS
。而 gRPC
本身是复用了 HTTP2
协议的,所以所谓 TLS
的实现,可以简单理解为在 gRPC
启动时配置合适的数字证书和私钥,以便在建立连接时实现 Server
对 Client
的认证,或者 Client
对 Server
的认证。
写给新童鞋:更多的细节请参见拙作《》、《》。
一般来说,我们用 HTTPS
仅用了对服务端的认证,以及其附带的信道加密功能,甚至表现上看起来,仅用了加密的部分。但其实基于 TLS
,我们不仅可以对服务端进行认证,还可以对客户端进行认证,这在处理自建服务的场景下非常有用。
自建证书与OpenSSL
我们先来考虑 Server
鉴定 Client
的场景,首先我们要引入一个 CA
, Server
要信任这个 CA
,而 CA
则需要给 Client
签发 证书
, Client
建立连接时,凭着 证书
向 Server
证明自己的身份。
那么第一个问题,我们怎么引入 CA
?
考虑当前是自建业务的场景,我们不可能信任三方的 CA
,而是需要自己创建,还记得 CA
的核心是什么吗?就是保管好自己的 私钥
,并给符合需求的 证书请求
签发 证书
。
所以这里先简单介绍一个工具 OpenSSL
, OpenSSL
是 TLS
的开源实现,实现了相关的加密解密算法,数字证书管理等功能。
安装方式略,相信在搜索引擎发达的今天有了关键字这都不是问题~
自建CA
那么,首先我们要创建的是一对 不对称密钥
,这对密钥是给 CA
用的,命令如下:
openssl genrsa -out ca.key 4096
- `genrsa` 生成 RSA 密钥对。
- `-out` 令生成的密钥对保存到文件
- `4096` RSA 模数位数,一般应大于1024。
此时,会生成 ca.key
文件,但注意,这个可不只是 私钥
哦。,但 OpenSSL
计算出的 ca.key
文件中,并不止 私钥
本身,还有包括 p
、 q
在内的其他参数,所以通过 ca.key
直接推导出 公钥
。
虽然理论上,
RSA
算法中的公钥
和私钥
是没有本质区别的,仅知道任何一个都无法推导另一个出来,但如果有了p
、q
等参数就不一样了。 所以请妥善保管ca.key
,这个文件其实等价于同时包含了公钥
和私钥
!
然后我们需要将 CA
的公钥公开,但实际上,公开的方式并不是直接生成 ca.pub
。还记得么,我们生成 crt
的时候, crt
本身就是包含 公钥
的。作为 CA
,其本身其实也有一张 数字证书
,这张证书是 CA
自己颁发给自己的,也就是所谓的自签名证书。
为什么会有这么个设计呢?这里先暂时按下不表……
所以第二步需要生成一份 ca.csr
, csr
其实就是当前“用户”的 基本信息
,以及当前用户的 公钥
,所以生成命令如下:
openssl req -new -key ca.key -out ca.csr
- `req` 用于生成'证书请求'的 OpenSSL 命令。
- `-new` 生成一个新的证书请求。该参数将令 OpenSSL 在证书请求生成过程中要求用户填写一些相应的字段。
- `-key` 用于签名的CA私钥。
- `-out` 将生成的请求保存到文件。
执行这个命令的时候,会提示要求输入国家代码(Country Name)等信息,这是数字证书标准中要求 CSR
附带的基本信息,所以根据自己的情况填写即可,完成后即可得到 ca.csr
。
其中填写
CommonName
时注意,这里请填写的运行服务的主机可以正确解析的域名,本地调试的话可以使用localhost
,否则可能会导致gRPC
无法工作。 这个参数描述的是对自己身份的声明,gRPC
的Client
启动时会对Server
的这个参数进行校验。
注意,这里虽然传入了 ca.key
,但其实本质上是为了将 ca.pub
写入 ca.csr
中,以便后续生成证书哦,并不是把 私钥
写入了 ca.csr
中。
下一步就是生成证书了,生成证书其实就是用 私钥
对 证书请求
进行签名,所以命令如下:
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt
- `x509` 操作符合x509标准的数字证书
- `-req` 传入一个`证书请求`,对其签名并输出`证书`
- `-int` 待签名的`证书请求`文件。
- `-signkey` 用于签名的私钥。
- `-out` 将生成的证书保存到文件
x509其实是数字证书的文件格式标准,一般我们用的数字证书都是符合x509标准的。
OK,此时我们终于拿到了 CA
的证书,这个证书实际包含的内容有 CA的基础信息
、 CA的公钥
。
另外我们要注意这三个步骤,其实签发证书基本就是这三个步骤的重复:
生成当前用户的
密钥对
(这里描述的用户
可能是CA
、Server
、Client
等)。根据
公钥
及用户信息,生成证书请求
。使用
私钥
对证书请求
进行签名
并生成数字证书
。
当然,也有更丰富的参数可以控制生成的结果。例如,控制证书多久过期。
应用的关键在于:
要信任谁,则表现为信任其证书,本质是信任证书的签发机构。
要为谁做担保,则使用私钥为其请求签名,请求应包含被担保人的公钥。
所以,这个环节生成 CA
的证书后,我们可以推导后续的生成目标。
回顾一下我们的需求, Server
需要鉴别 Client
是否获得了指定 CA
认可。所以 Server
需要信任该 CA
的公钥,具体表现为 ca.crt
。
Client
需要生成自己的 证书请求
,并获得 CA
的签名,得到 cli.crt
。
因此,我们可以推导过程如下:
# 生成Client的`密钥对`
openssl genrsa -out client.key 4096
# 生成Client的`证书请求`
openssl req -new -key client.key -out client.csr
# 用CA的私钥为Client的`证书请求`签名,生成`数字证书`
openssl x509 -req -in client.csr -signkey ca.key -out client.crt
其实还是那三个命令,只是参数发生了变化。
基于Golang的gRPC实现
那么,我们现在应该已经有了以下的文件:
- ca.key
- ca.crt
- client.key
- client.crt
此时我们启动 gRPC-Server
的核心方法如下:
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func NewServer()*grpc.Server{
// 准备一个信任的cert pool,pool中的存有当前server信任的所有的签发机构的证书。
certPool := x509.NewCertPool()
pemCACrtData, _ := ioutil.ReadFile("./ca.crt")
certPool.AppendCertsFromPEM(pemCACrtData)
// 生成gRPC的`TLS`配置
tlsConfig := tls.Config{
ClientCAs: certPool,
ClientAuth: tls.RequireAndVerifyClientCert, // 强制要求客户端提供证书进行身份认证,还有其他可选策略,有兴趣童鞋可以自己看看
}
creds := credentials.NewTLS(tlsConfig)
return grpc.NewServer(creds)
}
对应客户端的核心代码如下:
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func NewClientConn()(*grpc.ClientConn,error){
// 读取CA签发给Client的证书
pemCliCrtData, _ := ioutil.ReadFile("./client.crt")
// 读取Client自己的私钥
pemCliKeyData, _ := ioutil.ReadFile("./client.key")
// 生成认证配置
cliCert,_ := tls.X509KeyPair(pemCliCrtData, pemCliKeyData)
// 生成tls配置
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cliCert},
}
creds := credentials.NewTLS(tlsConfig)
// 生成ClientConn
return grpc.Dial(
"your-grpc-server",
grpc.WithTransportCredentials(creds),
)
}
这里给出的是分解步骤,更详细,是为后续带密钥的双向验证做准备,
tls
包本身有封装好方法可以简化过程。
关于私钥的密码
这个问题其实是应用上的一个小坑,很多时候我们为了保护 私钥
,会另外使用加密算法对私钥再做一次加密( PEM
也定义了相关的加密结果存储格式)。这样,使用的时候就必须先把 私钥
解密,然后才能正常使用,而这一功能并没有直接在 crypto
中提供,需要进行一些小小的转换:
// 这段算法参考自`github.com/cloudflare/cfssl`的开源代码
import (
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
)
// 简单说,pemPass就是用来加密的密钥
func ReadPEMData(pemFile string, pemPass []byte) ([]byte, error) {
pemData, err := ioutil.ReadFile(pemFile)
if err != nil {
return pemData, err
}
// 先解析出pem block
pemBlock, _ := pem.Decode(pemData)
if pemBlock == nil {
return nil, errors.New("PEM Data Not Found")
}
// 判断这个block是否被加密过
if x509.IsEncryptedPEMBlock(pemBlock) {
// 解密并获得`ASN.1 DER`编码的数据
pemData, err = x509.DecryptPEMBlock(pemBlock, pemPass)
if err != nil {
return nil, err
}
// 重新生成一个没加密pem block
var newBlock pem.Block
newBlock.Type = pemBlock.Type
newBlock.Bytes = pemData
// 把未加密的pem block编码成tls可以直接解析的pemData
pemData = pem.EncodeToMemory(&newBlock)
}
return pemData, nil
}
事实上,这个加密并不只是
私钥
可以加密,证书
也可以,只是没有什么意义罢了(毕竟本身就是要公开的东西)。
小结
这篇东西应该算是 数字证书篇
的结尾,也是键者为什么开始鼓捣数字证书的原因,没想到转眼半年过去了,才终于写完。
其实东西早用上了,就是键者拖延症末期,没救了……
数字证书
的思想本身是对加密算法的巧妙应用,解决的信任与可靠的问题,而现在(应该是早就)大火的区块链概念,也是在此基础上的扩展应用。
万丈高楼平地起,盘龙卧虎高山齐,各位小伙伴在下半年也要继续加油!
特别的PS:祝愿键者圈里水逆的童鞋早日土顺;高原反应的童鞋早日回到平原;减肥的童鞋,嗯嗯……冤冤相报何时了
以上是关于GOLANG:自建gRPC的TLS双向验证(OpenSSL实现)的主要内容,如果未能解决你的问题,请参考以下文章