grpc使用自制CA证书校验公网上的连接请求

方法在最后一小节,前面都是用到的知识的总结,了解的可以跳过。


1. 对称加密和非对称加密

对称加密:加密和解密都用同一个密码
非对称加密:公钥对所有人公开,发送者加密用公钥,接收者唯一掌握私钥,对公钥加密内容的解密用且只能用私钥
非对称加密缺点:加密速度比对称慢
非对称加密优点:公钥本身就是公开给别人的,所以不用担心被窃取,私钥永远在自己手里,只有自己能解密消息

2. https和http

  https在http的基础上,对传输内容进行了加密。
  我们都知道http建立连接的三次握手和四次挥手过程。如果使用的是https,在三次握手过后,需要进行SSL握手,然后客户端和服务端通信过程中使用SSL(新版本叫TLS了)的互相加解密。通信完之后,在四次挥手之前,先关闭SSL连接。
  从这里能看出,https比http要慢(多了SSL握手、挥手,就多了好几次TCP通信,建立连接之后每次通信还要加解密,http/1.1开始支持长连接,以及http/2支持多路复用,都可以减少建立连接的次数,也就减少了多次http请求下每次http请求的平均耗时)。
  https相比http还可以防篡改。接收到消息之后计算出数字签名,对比收到的数字签名,就知道消息体是否被篡改了。(想了解数字签名可以参考阿里云视频监控产品OpenAPI的签名机制)

3. https单向认证和双向认证

图来自这里

https单向认证

https双向认证

  从上面对比可以看到,https使用的非对称加密是用在客户端和服务端交互SSL信息的过程中,SSL握手完成后,客户端和服务端通信使用的还是对称加密,对称加密的密钥在本次连接随机生成,其他的连接以及本次连接关闭后都不能使用。
  双向认证和单向认证的区别就在于,客户端需要向服务端发送自己的证书,服务端需要校验客户端的证书,以及服务端选择加密方案通知客户端时也是加密的。

4. CA证书和自制证书

  CA(Certificate Authority)是负责管理和签发证书的第三方权威机构,常见的有Symantec、GeoTrust、Comodo等等,他们是所有行业和公众都信任的、认可的,并负责审核向他们申请证书的网站的安全性。
  CA证书,就是CA颁发的证书,可用于验证网站是否可信(针对HTTPS)、验证某文件是否可信(是否被篡改)等,也可以用一个证书来证明另一个证书是真实可信,最顶级的证书称为根证书。除了根证书(自己证明自己是可靠),其它证书都要依靠上一级的证书,来证明自己。
  我们用的操作系统都预置了很多可信任的根证书,SSL握手时服务器会把它的服务器证书发给浏览器。例如CSDN的服务器证书是GeoTrust颁发的,本地的GeoTrust根证书可以证明CSDN的服务器证书是真的,值得信任,于是我们访问CSDN时浏览器就建立了和CSDN服务器的https连接。
  我们也可以自己做CA根证书,我们自己的机器或者访问我们服务的客户的机器,都安装上该CA根证书。12306以前就是这样干的(SRCA就是12306的根证书,现在已经换成DigiCert的证书了)。
  关于CA证书更多的解释,以及各种证书文件的区别,参考这里。
  自制CA证书可以用OpenSSL命令行工具,linux基本都自带,也可以使用GUI工具,参考这里。

5. 利用chrome和nginx实践一下单向认证和双向认证

  先假定我们已经做好了证书文件,包括根证书ca.pem、服务端证书server.pem、服务端私钥server.key、客户端证书client.pem、客户端私钥client.key,可以来这里下载我做好的,可以用于你自己测试,提取码:yd11。
  nginx加上配置文件:

server {
    listen       443 ssl; # ssl代表该端口监听启用https
    server_name  localhost;
    ssl_certificate      /data/sslKey/server.pem;  # 证书
    ssl_certificate_key  /data/sslKey/server.key;  # 私钥
    ssl_client_certificate /data/sslKey/ca.pem;    # 根证书,用于验证各个下级证书
    ssl_verify_client on; # 校验客户端证书开启
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

其中:

ssl_certificate      /data/sslKey/server.pem;  # server证书公钥
ssl_certificate_key  /data/sslKey/server.key;  # server私钥

很常见。
  通常我们需要让自己的网站变成https访问时都是这么做。使用nginx对外暴露https请求接口,nginx到后端的内网服务仍然是http,改动小,效率高。
  只有这两项配置就是单向认证,即客户端需要校验服务端的证书来确信服务端是安全网站。这时候访问https://localhost:443会提示不安全的网站(以下所有截图都是chrome浏览器的交互,其他浏览器可能不是这样,自行处理),因为服务端提供的证书是我们自制的,操作系统预存的证书无法识别该证书。

不安全的网站

  点击继续访问代表我们手动告诉浏览器信任这个网站,就可以继续访问了。
继续访问

再加上:

ssl_client_certificate /data/sslKey/ca.pem;    # 根证书,用于验证各个下级证书
ssl_verify_client on; # 校验客户端证书开启

这两项配置就是双向认证。
  服务端需要验证客户端的证书,我们直接访问就会得到报错400 No Required SSL certificate was sent,需要在chrome中导入自制的client.pem证书,导入方法参考这里。
  需要注意的是windows导入证书的格式不是pem和crt,需要转换一下才能导入,OpenSSL命令行工具可以转换,上面的GUI工具也可以在导出时选择指定格式。
  再次访问chrome会弹窗提示,

选择证书

  选择刚才导入的证书就能正常访问了。
nginx访问结果

6. 利用自制CA证书双向认证实现安全访问

  以go代码示例,仍然使用上面的证书文件,加载证书文件的代码:

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
    "google.golang.org/grpc/credentials"
)

// GetServerCredentials 服务端证书
func GetServerCredentials() credentials.TransportCredentials {
    cert, err := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
    if err != nil {
        log.Fatalf("加载服务端证书失败, err: %v\n", err)
    }
    certPool := x509.NewCertPool()
    ca, err := ioutil.ReadFile("cert/ca.pem")
    if err != nil {
        log.Fatalf("读取公钥文件失败: %v\n", err)
    }
    certPool.AppendCertsFromPEM(ca)
    creds := credentials.NewTLS(&tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    certPool,
    })
    return creds
}

// GetClientCredentials 客户端证书
func GetClientCredentials() credentials.TransportCredentials {
    cert, err := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
    if err != nil {
        log.Fatalf("加载客户端证书失败, err: %v\n", err)
    }
    certPool := x509.NewCertPool()
    ca, err := ioutil.ReadFile("cert/ca.pem")
    if err != nil {
        log.Fatalf("读取公钥文件失败: %v\n", err)
    }
    certPool.AppendCertsFromPEM(ca)
    creds := credentials.NewTLS(&tls.Config{
        Certificates: []tls.Certificate{cert},
        ServerName:   "localhost",
        RootCAs:      certPool,
    })
    return creds
}

  服务端加上:

opts := []grpc.ServerOption{
    grpc.Creds(utils.GetServerCredentials()),
}

  客户端加上:

grpc.Dial(
  "localhost:8080",
    grpc.WithTransportCredentials(cert.GetClientCredentials()),

  如果服务端使用了证书,客户端没有使用证书,在grpc.Dial()时连接可以建立成功,但是访问时会报错:

rpc error: code = Unavailable desc = connection closed

并且可以一直访问一直报错。
  这里涉及到gRPC连接机制的问题。调用Dial或者DialContext函数创建连接时,默认只是返回ClientConn结构体指针,同时会启动一个Goroutine异步的去建立连接,连接失败会一直重试,这个机制可以避免服务器因为连接空闲时间过长关闭连接、服务器重启等造成的客户端连接失效问题,可以完美的解决连接的超时与保活问题。
  如果想要等连接建立完再返回(起到创建连接时就检测连接是否可用的目的),可以指定grpc.WithBlock()。连接不上就会报错:

context deadline exceeded

  如果服务端没有使用证书,客户端使用了,也是可以成功建立连接,但是访问时报错:

connection error: desc = "transport: authentication handshake failed: tls: first record does not look like a TLS handshake"

  因为客户端收不到服务端的TLS握手信息(服务端不使用证书,根本就不知道要TLS握手)。

你可能感兴趣的:(grpc使用自制CA证书校验公网上的连接请求)