GOLANG:自建gRPC的TLS双向验证(OpenSSL实现)

Posted 大悦天

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GOLANG:自建gRPC的TLS双向验证(OpenSSL实现)相关的知识,希望对你有一定的参考价值。

哈哈哈,没想到吧,勤(HAO)劳(CHI)勇(LAN)敢(ZUO)的键者还活着吧?今天键者就来梳理一下数字签名和数字证书篇在 gRPC中的应用。

才不是因为键者周末无意中翻出了这个年初写了一半的稿子……

众所周知, gRPC是谷歌开源的一套远程调用框架,它在应用层复用 HTTP2协议,而自建服务时同样需要处理之前提到的权限问题,客户端怎么证明自己有权限调用,服务器又怎么证明自己不是伪造的服务器呢?

背景

首先, gRPC设计之初,就已经考虑了这个问题,它同时支持 TLSToken两种实现鉴权逻辑(而且两者可以共存)。其中 Token的形式主要通过 Interceptor+ 自建鉴权的方式实现,相信有对接过三方接口(例如微博)或者开发过类似服务的童鞋都很好理解,无非鉴权管理策略的实现和管控,这块需要结合各自的业务情况规划,键者就不多说了。

注意 TLS方案和 Token方案是互补的方案,而不是互斥的哦。TLS更注重 Connection的身份鉴别,以及通讯内容加密。Token可以映射到具体的用户状态,以及 Chanel级别的权限控制.

键者今天就讨论一下 gRPC基于 TLS的双向鉴权实现。

虽然 TLS听起来很陌生,但其实我们Web开发中接触得并不少,那就是 HTTPS,其中的 S有一种解释就是 HTTPOverTLS。而 gRPC本身是复用了 HTTP2协议的,所以所谓 TLS的实现,可以简单理解为在 gRPC启动时配置合适的数字证书和私钥,以便在建立连接时实现 ServerClient的认证,或者 ClientServer的认证。

写给新童鞋:更多的细节请参见拙作《》、《》。

一般来说,我们用 HTTPS仅用了对服务端的认证,以及其附带的信道加密功能,甚至表现上看起来,仅用了加密的部分。但其实基于 TLS,我们不仅可以对服务端进行认证,还可以对客户端进行认证,这在处理自建服务的场景下非常有用。

自建证书与OpenSSL

我们先来考虑 Server鉴定 Client的场景,首先我们要引入一个 CAServer要信任这个 CA,而 CA则需要给 Client签发 证书Client建立连接时,凭着 证书Server证明自己的身份。

那么第一个问题,我们怎么引入 CA

考虑当前是自建业务的场景,我们不可能信任三方的 CA,而是需要自己创建,还记得 CA的核心是什么吗?就是保管好自己的 私钥,并给符合需求的 证书请求签发 证书

所以这里先简单介绍一个工具 OpenSSLOpenSSLTLS的开源实现,实现了相关的加密解密算法,数字证书管理等功能。

安装方式略,相信在搜索引擎发达的今天有了关键字这都不是问题~

自建CA

那么,首先我们要创建的是一对 不对称密钥,这对密钥是给 CA用的,命令如下:

 
   
   
 
  1. openssl genrsa -out ca.key 4096

  2. - `genrsa` 生成 RSA 密钥对。

  3. - `-out` 令生成的密钥对保存到文件

  4. - `4096` RSA 模数位数,一般应大于1024

此时,会生成 ca.key文件,但注意,这个可不只是 私钥哦。,但 OpenSSL计算出的 ca.key文件中,并不止 私钥本身,还有包括 pq在内的其他参数,所以通过 ca.key直接推导出 公钥

虽然理论上, RSA算法中的 公钥私钥是没有本质区别的,仅知道任何一个都无法推导另一个出来,但如果有了 pq等参数就不一样了。 所以请妥善保管 ca.key,这个文件其实等价于同时包含了 公钥私钥

然后我们需要将 CA的公钥公开,但实际上,公开的方式并不是直接生成 ca.pub。还记得么,我们生成 crt的时候, crt本身就是包含 公钥的。作为 CA,其本身其实也有一张 数字证书,这张证书是 CA自己颁发给自己的,也就是所谓的自签名证书。

为什么会有这么个设计呢?这里先暂时按下不表……

所以第二步需要生成一份 ca.csrcsr其实就是当前“用户”的 基本信息,以及当前用户的 公钥,所以生成命令如下:

 
   
   
 
  1. openssl req -new -key ca.key -out ca.csr

  2. - `req` 用于生成'证书请求' OpenSSL 命令。

  3. - `-new` 生成一个新的证书请求。该参数将令 OpenSSL 在证书请求生成过程中要求用户填写一些相应的字段。

  4. - `-key` 用于签名的CA私钥。

  5. - `-out` 将生成的请求保存到文件。

执行这个命令的时候,会提示要求输入国家代码(Country Name)等信息,这是数字证书标准中要求 CSR附带的基本信息,所以根据自己的情况填写即可,完成后即可得到 ca.csr

其中填写 CommonName时注意,这里请填写的运行服务的主机可以正确解析的域名,本地调试的话可以使用 localhost,否则可能会导致 gRPC无法工作。 这个参数描述的是对自己身份的声明, gRPCClient启动时会对 Server的这个参数进行校验。

注意,这里虽然传入了 ca.key,但其实本质上是为了将 ca.pub写入 ca.csr中,以便后续生成证书哦,并不是把 私钥写入了 ca.csr中。

下一步就是生成证书了,生成证书其实就是用 私钥证书请求进行签名,所以命令如下:

 
   
   
 
  1. openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt

  2. - `x509` 操作符合x509标准的数字证书

  3. - `-req` 传入一个`证书请求`,对其签名并输出`证书`

  4. - `-int` 待签名的`证书请求`文件。

  5. - `-signkey` 用于签名的私钥。

  6. - `-out` 将生成的证书保存到文件

x509其实是数字证书的文件格式标准,一般我们用的数字证书都是符合x509标准的。

OK,此时我们终于拿到了 CA的证书,这个证书实际包含的内容有 CA的基础信息CA的公钥

另外我们要注意这三个步骤,其实签发证书基本就是这三个步骤的重复:

  1. 生成当前用户的 密钥对(这里描述的 用户可能是 CA、 Server、 Client等)。

  2. 根据 公钥及用户信息,生成 证书请求

  3. 使用 私钥对 证书请求进行 签名并生成 数字证书

当然,也有更丰富的参数可以控制生成的结果。例如,控制证书多久过期。

应用的关键在于:

  • 要信任谁,则表现为信任其证书,本质是信任证书的签发机构。

  • 要为谁做担保,则使用私钥为其请求签名,请求应包含被担保人的公钥。

所以,这个环节生成 CA的证书后,我们可以推导后续的生成目标。

回顾一下我们的需求, Server需要鉴别 Client是否获得了指定 CA认可。所以 Server需要信任该 CA的公钥,具体表现为 ca.crt

Client需要生成自己的 证书请求,并获得 CA的签名,得到 cli.crt

因此,我们可以推导过程如下:

 
   
   
 
  1. # 生成Client的`密钥对`

  2. openssl genrsa -out client.key 4096

  3. # 生成Client的`证书请求`

  4. openssl req -new -key client.key -out client.csr

  5. # 用CA的私钥为Client的`证书请求`签名,生成`数字证书`

  6. openssl x509 -req -in client.csr -signkey ca.key -out client.crt

其实还是那三个命令,只是参数发生了变化。

基于Golang的gRPC实现

那么,我们现在应该已经有了以下的文件:

 
   
   
 
  1. - ca.key

  2. - ca.crt

  3. - client.key

  4. - client.crt

此时我们启动 gRPC-Server的核心方法如下:

 
   
   
 
  1. import (

  2.    "crypto/tls"

  3.    "crypto/x509"

  4.    "io/ioutil"

  5.    "google.golang.org/grpc"

  6.    "google.golang.org/grpc/credentials"

  7. )

  8. func NewServer()*grpc.Server{

  9.    // 准备一个信任的cert pool,pool中的存有当前server信任的所有的签发机构的证书。

  10.    certPool := x509.NewCertPool()

  11.    pemCACrtData, _ := ioutil.ReadFile("./ca.crt")

  12.    certPool.AppendCertsFromPEM(pemCACrtData)

  13.    // 生成gRPC的`TLS`配置

  14.    tlsConfig := tls.Config{

  15.        ClientCAs: certPool,

  16.        ClientAuth: tls.RequireAndVerifyClientCert, // 强制要求客户端提供证书进行身份认证,还有其他可选策略,有兴趣童鞋可以自己看看

  17.    }

  18.    creds := credentials.NewTLS(tlsConfig)

  19.    return grpc.NewServer(creds)

  20. }

对应客户端的核心代码如下:

 
   
   
 
  1. import (

  2.    "crypto/tls"

  3.    "crypto/x509"

  4.    "io/ioutil"

  5.    "google.golang.org/grpc"

  6.    "google.golang.org/grpc/credentials"

  7. )

  8. func NewClientConn()(*grpc.ClientConn,error){

  9.    // 读取CA签发给Client的证书

  10.    pemCliCrtData, _ := ioutil.ReadFile("./client.crt")

  11.    // 读取Client自己的私钥

  12.    pemCliKeyData, _ := ioutil.ReadFile("./client.key")

  13.    // 生成认证配置

  14.  cliCert,_ := tls.X509KeyPair(pemCliCrtData, pemCliKeyData)

  15.    // 生成tls配置

  16.    tlsConfig := &tls.Config{

  17.        Certificates: []tls.Certificate{cliCert},

  18.    }

  19.    creds := credentials.NewTLS(tlsConfig)  

  20.    // 生成ClientConn

  21.  return grpc.Dial(

  22.        "your-grpc-server",

  23.        grpc.WithTransportCredentials(creds),

  24.    )

  25. }

这里给出的是分解步骤,更详细,是为后续带密钥的双向验证做准备, tls包本身有封装好方法可以简化过程。

关于私钥的密码

这个问题其实是应用上的一个小坑,很多时候我们为了保护 私钥,会另外使用加密算法对私钥再做一次加密( PEM也定义了相关的加密结果存储格式)。这样,使用的时候就必须先把 私钥解密,然后才能正常使用,而这一功能并没有直接在 crypto中提供,需要进行一些小小的转换:

 
   
   
 
  1. // 这段算法参考自`github.com/cloudflare/cfssl`的开源代码

  2. import (

  3.    "crypto/x509"

  4.    "encoding/pem"

  5.    "errors"

  6.    "io/ioutil"

  7. )

  8. // 简单说,pemPass就是用来加密的密钥

  9. func ReadPEMData(pemFile string, pemPass []byte) ([]byte, error) {

  10.    pemData, err := ioutil.ReadFile(pemFile)

  11.    if err != nil {

  12.        return pemData, err

  13.    }

  14.    // 先解析出pem block

  15.    pemBlock, _ := pem.Decode(pemData)

  16.    if pemBlock == nil {

  17.        return nil, errors.New("PEM Data Not Found")

  18.    }

  19.    // 判断这个block是否被加密过

  20.    if x509.IsEncryptedPEMBlock(pemBlock) {

  21.        // 解密并获得`ASN.1 DER`编码的数据

  22.        pemData, err = x509.DecryptPEMBlock(pemBlock, pemPass)

  23.        if err != nil {

  24.            return nil, err

  25.        }

  26.        // 重新生成一个没加密pem block

  27.        var newBlock pem.Block

  28.        newBlock.Type = pemBlock.Type

  29.        newBlock.Bytes = pemData

  30.        // 把未加密的pem block编码成tls可以直接解析的pemData

  31.        pemData = pem.EncodeToMemory(&newBlock)

  32.    }

  33.    return pemData, nil

  34. }

事实上,这个加密并不只是 私钥可以加密, 证书也可以,只是没有什么意义罢了(毕竟本身就是要公开的东西)。

小结

这篇东西应该算是 数字证书篇的结尾,也是键者为什么开始鼓捣数字证书的原因,没想到转眼半年过去了,才终于写完。

其实东西早用上了,就是键者拖延症末期,没救了……

数字证书的思想本身是对加密算法的巧妙应用,解决的信任与可靠的问题,而现在(应该是早就)大火的区块链概念,也是在此基础上的扩展应用。

万丈高楼平地起,盘龙卧虎高山齐,各位小伙伴在下半年也要继续加油!

特别的PS:祝愿键者圈里水逆的童鞋早日土顺;高原反应的童鞋早日回到平原;减肥的童鞋,嗯嗯……冤冤相报何时了

以上是关于GOLANG:自建gRPC的TLS双向验证(OpenSSL实现)的主要内容,如果未能解决你的问题,请参考以下文章

Golang TLS双向身份认证DoS漏洞分析(CVE-2018-16875)

golang grpc 双向

Grpc 在golang中的使用

gRPC应用golang

从gRPC安全设计理解双向证书方案

Linux apache自建证书搭建https