• gRPC源码分析1-SSL/TLS


    引子

    前几天看到微信后台团队分享了TLS相关文章,正好gRPC里TLS数据加密是很重要的一块,于是整理出了这篇文章。

    在gRPC里,如果仅仅是用来做后端微服务,可以考虑不加密。本文太长,先给个大纲。

    1. HTTPS,HTTP/2介绍

    2. TLS加密原理、实现库

    3. HTTP/2协议协商机制

    4. 自建数字证书(CA)

    5. gRPC使用TLS

    1. HTTP/1.x

    目前绝大多数网站和APP都是建立在HTTP之上的,所有的数据都是明文传输,没有任何安全可言。

    网图 

    2. HTTPS

    HTTPS(Hypertext Transfer Protocol over Secure Socket Layer)是以安全为目标的HTTP通道,即HTTP下加入SSL层,HTTPS的安全基础是SSL。用来保护用户隐私,防止流量劫持。


    (网图,懒得画了) 

    2.1 HTTPS的作用(来自百度)

    • 认证用户和服务器,确保数据发送到正确的客户机和服务器;(验证证书)

    • 加密数据以防止数据中途被窃取;(加密)

    • 维护数据的完整性,确保数据在传输过程中不被改变。(摘要算法)

    HTTPS之所以安全,就是HTTP建立在SSL/TLS之上的。

    (网图)

    SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。

    (1)如何保证公钥不被篡改?

    将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

    (2)公钥加密计算量太大,如何减少耗用的时间?

    每一次对话,客户端和服务器端都生成一个”对话密钥”,用它来加密信息。由于”对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密”对话密钥”本身,这样就减少了加密运算的消耗时间。

    也就是说,对于HTTPS,由于成本问题

    • 握手阶段(handshake)用的非对称加密

    • 数据通信用的是对称加密

    2.2 加密算法

    我们大致的讲一下加密相关术语。由于密码学太过复杂,我们不去深究,也千万别问我为什么公钥加密后,能够用私钥解密。

    (主要是数学太难,门槛太高,我也不懂,逃。。。) 

    对称密钥

    又称为共享密钥加密,对称密钥在加密和解密的过程中使用的密钥是相同的,常见的对称加密算法有DES、3DES、AES、RC5、RC6。对称密钥的优点是计算速度快,但是密钥需要在通讯的两端共享,让彼此知道密钥是什么对方才能正确解密,如果所有客户端都共享同一个密钥,那么这个密钥就像万能钥匙一样,可以凭借一个密钥破解所有人的密文了。

    非对称密钥

    服务端会生成一对密钥,一个私钥保存在服务端,仅自己知道,另一个是公钥,公钥可以自由发布供任何人使用。客户端的明文通过公钥加密后的密文需要用私钥解密。非对称密钥在加密和解密的过程的使用的密钥是不同的密钥,加密和解密是不对称的,所以称之为非对称加密。与对称密钥加密相比,非对称加密无需在客户端和服务端之间共享密钥,只要私钥不发给任何用户,即使公钥在网上被截获,也无法被解密,仅有被窃取的公钥是没有任何用处的。常见的非对称加密有RSA。

    数字签名

    数字签名就如同日常生活中的签名一样,这是任何人都没法仿造的。在计算机中的数字签名就是用于验证传输的内容是不是真实服务器发送的数据,发送的数据有没有被篡改过。

    数字证书

    数字证书简称CA,它由权威机构给某网站颁发的一种认可凭证,这个凭证是被大家(浏览器)所认可的。

    3. HTTP/2

    HTTP/2,主要是基于Google的SPDY协议,是自HTTP/1.1从1999年发布16年后的首次更新。Servlet4.0将完全支持HTTP/2。

    3.1 HTTP/1.1的问题

    • 假设一个网站需要加载几十个资源(css、js、jpg、等等),等到html文件加载成功后,浏览器会一个一个请求这些资源,并等待服务器按顺序一个一个返回。

    • 一个请求,一个应答
    • http header

    3.2 HTTP/2主要特性:

    • request/response多路复用(multiplexing)

    • 二进制帧传输(binary framing)

    • 数据流优先级(stream prioritization)

    • 服务器推送(server push)

    • 头信息压缩(header compression)

    HTTP/2是站在HTTP/1.1肩膀上的一个改进而已,跟HTTP/1.1相比:

    • 相同的request/response模式

    • 没有新的method

    • 没有新的header

    • 在应用层没有引入新的花样

    • 没有修改URL规范、没有修改其他底层规范

    HTTP/2仅是一个协议而已,它可以建立在TLS之上,也可以不。但是,根据 http://caniuse.com/,网站的统计,浏览器几乎只支持安全的HTTP/2,也就是说如果是网站的话,想要升到HTTP/2就必须支持HTTPS。当然如gRPC这种内部的服务开发,可以不用支持TLS。

    3.3 HTTP/2 的协议协商机制

    一个网站支不支持HTTP/2,对于浏览器来说是不知道的,只能通过两者的协商来确定是否使用HTTP/2协议,还是HTTP/1.1。我们分2种来讲。

    a. HTTP(without TLS)

    为了更方便地部署新协议,HTTP/1.1 引入了 Upgrade 机制,它使得客户端和服务端之间可以借助已有的 HTTP 语法升级到其它协议。

    如果大家之前使用过 WebSocket,应该已经对 HTTP Upgrade 机制有所了解。下面是建立 WebSocket 连接的 HTTP 请求

    GET ws://example.com/ HTTP/1.1

    Connection: Upgrade

    Upgrade: websocket

    Origin: http://example.com

    Sec-WebSocket-Version: 13

    Sec-WebSocket-Key: d4egt7snxxxxxx2WcaMQlA==

    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

    这是服务端同意升级的 HTTP 响应:

    HTTP/1.1 101 Switching Protocols

    Connection: Upgrade

    Upgrade: websocket

    Sec-WebSocket-Accept: gczJQPmQ4Ixxxxxx6pZO8U7UbZs=

    在这之后,客户端和服务端之间就可以使用 WebSocket 协议进行双向数据通讯,跟 HTTP/1.1 没关系了。可以看到,WebSocket 连接的建立就是典型的 HTTP Upgrade 机制。显然,这个机制也可以用做 HTTP/1.1 到 HTTP/2 的协议升级。

    b. HTTPS(with TLS)

    多了 TLS 之后,双方必须等到成功建立 TLS 连接之后才能发送应用数据。而要建立 TLS 连接,本来就要进行 CipherSuite 等参数的。引入 HTTP/2 之后,需要做的只是在原本的协商机制中把对 HTTP 协议的协商加进去。Google 在 SPDY 协议中开发了一个名为 NPN(Next Protocol Negotiation,下一代协议协商)的 TLS 扩展。随着 SPDY 被 HTTP/2 取代,NPN 也被官方修订为 ALPN(Application Layer Protocol Negotiation,应用层协议协商)。

    下图,是caniuse.com网站统计的支持HTTP/2的浏览器版本,以及支持的协商协议。可以看到chrome到41版本才支持,IE根本不支持。

    4. SSL/TLS

    互联网加密通信协议的历史,几乎与互联网一样长。

    • 1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但是未发布。

    • 1995年,NetScape公司发布SSL 2.0版,很快发现有严重漏洞。

    • 1996年,SSL 3.0版问世,得到大规模应用。

    • 1999年,互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。

    • 2006年和2008年,TLS进行了两次升级,分别为TLS 1.1版和TLS 1.2版。最新的变动是2011年TLS 1.2的修订版。

    目前常用的 HTTP 协议是 HTTP1.1,常用的 TLS 协议版本有如下几个:TLS1.2, TLS1.1, TLS1.0 和 SSL3.0。

    • 其中 SSL3.0 由于 POODLE 攻击已经被证明不安全

    • TLS1.0 也存在部分安全漏洞,比如 RC4 和 BEAST 攻击

    • TLS1.2 和 TLS1.1 暂时没有已知的安全漏洞,比较安全,同时有大量扩展提升速度和性能,推荐

    那么如何建立TLS链接的呢?大概步骤如下:

    (网图)

    1. 客户端将自己支持的一套加密算法、HASH算法发送给服务端

    2. 服务端从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给客户端。证书里面包含了服务端的地址(域名),加密公钥,以及证书的颁发机构等信息

    3. 客户端获得证书之后,开始验证证书的合法性,如果证书信任,则生成一串随机数字作为通讯过程中对称加密的秘钥。然后取出证书中的公钥,将这串数字以及HASH的结果进行加密,然后发给服务端

    4. 服务端接收客户端发来的数据之后,通过私钥进行解密,然后HASH校验,如果一致,则使用客户端发来的数字串加密一段握手消息发给客户端

    5. 客户端解密,并HASH校验,没有问题,则握手结束。接下来的传输过程将由之前客户端生成的随机密码并利用对称加密算法进行加密通信

    4.1 实现库

    TLS协议的设计目标是构建一个安全传输层(Transport Layer Security ),在基于连接的传输层(如tcp)之上提供。

    TLS是用来做加密数据传输的,因此它的主体当然是一个对称加密传输组件。为了给这个组件生成双方共享的密钥,因此就需要先搞一个认证密钥协商组件,TLS协议自然分为:

    1. 做对称加密传输的record协议 ,the record protocol

    2. 做认证密钥协商的handshake协议,the handshake protocol

    还有3个很简单的辅助协议:

    1. changecipher spec 协议,the changecipher spec protocol, 用来通知对端从handshake切换到record协议(有点冗余,在TLS1.3里面已经被删掉了)

    2. alert协议,the alert protocol, 用来通知各种返回码,

    3. application data协议, The application data protocol,就是把http,smtp等的数据流传入record层做处理并传输。

    (网图)

    如上看到,要实现TLS协议是很复杂的,目前他的实现也已经有很多了,当然最著名的当属 openssl 。在wikipedia里已经列的很详细了。gRPC里由于是基于netty的,netty里的TLS实现库主要是BoringSSL、OpenSSL

    大家可以参考

    https://en.wikipedia.org/wiki/Comparison_of_TLS_implementations

    5. CA(证书)

    它的作用就是提供证书(即服务器证书,由域名、公司信息、序列号和签名信息组成)加强服务端和客户端之间信息交互的安全性,以及证书运维相关服务。任何个体/组织都可以扮演 CA 的角色,只不过难以得到客户端的信任,能够受浏览器默认信任的 CA 大厂商有很多,其中 TOP5 是 Symantec、Comodo、Godaddy、GolbalSign 和 Digicert。

    证书也挺贵的,对于个人来说,还是算了。就是我们伟大的12306用的也是自建证书。

    5.1 证书标准

    X.509 - 这是一种证书标准,主要定义了证书中应该包含哪些内容.其详情可以参考RFC5280,SSL使用的就是这种证书标准.

    5.2 编码格式

    同样的X.509证书,可能有不同的编码格式

    • PEM - Privacy Enhanced Mail,打开看文本格式,以"-----BEGIN..."开头, "-----END..."结尾,内容是BASE64编码.
      查看PEM格式证书的信息:openssl x509 -in certificate.pem -text -noout
      Apache和*NIX服务器偏向于使用这种编码格式.

    • DER - Distinguished Encoding Rules,打开看是二进制格式,不可读.
      查看DER格式证书的信息:openssl x509 -in certificate.der -inform der -text -noout
      Java和Windows服务器偏向于使用这种编码格式.

    5.3 相关的文件扩展名

    虽然我们已经知道有PEM和DER这两种编码格式,但文件扩展名并不一定就叫"PEM"或者"DER",常见的扩展名除了PEM和DER还有以下这些,它们除了编码格式可能不同之外,内容也有差别,但大多数都能相互转换编码格式.

    • CRT - CRT应该是certificate的三个字母,其实还是证书的意思,常见于*NIX系统,有可能是PEM编码,也有可能是DER编码,大多数应该是PEM编码,相信你已经知道怎么辨别.

    • CER - 还是certificate,还是证书,常见于Windows系统,同样的,可能是PEM编码,也可能是DER编码,大多数应该是DER编码.

    • KEY - 通常用来存放一个公钥或者私钥,并非X.509证书,编码同样的,可能是PEM,也可能是DER.
      查看KEY的办法:openssl rsa -in mykey.key -text -noout
      如果是DER格式的话,同理应该这样了:openssl rsa -in mykey.key -text -noout -inform der

    • CSR - Certificate Signing Request,即证书签名请求,这个并不是证书,而是向权威证书颁发机构获得签名证书的申请,其核心内容是一个公钥(当然还附带了一些别的信息),在生成这个申请的时候,同时也会生成一个私钥,私钥要自己保管好。
      查看的办法:openssl req -noout -text -in my.csr

    • PFX/P12 - predecessor of PKCS#12,对*nix服务器来说,一般CRT和KEY是分开存放在不同文件中的,但Windows的IIS则将它们存在一个PFX文件中,(因此这个文件包含了证书及私钥),PFX通常会有一个"提取密码",你想把里面的东西读取出来的话,它就要求你提供提取密码,PFX使用的时DER编码,如何把PFX转换为PEM编码?
      openssl pkcs12 -in for-iis.pfx -out for-iis.pem -nodes
      这个时候会提示你输入提取代码. for-iis.pem就是可读的文本.
      生成pfx的命令类似这样:openssl pkcs12 -export -in certificate.crt -inkey privateKey.key -out certificate.pfx -certfile CACert.crt其中CACert.crt是CA(权威证书颁发机构)的根证书,有的话也通过-certfile参数一起带进去.这么看来,PFX其实是个证书密钥库.

    • JKS - 即Java Key Storage,这是Java的专利,跟OpenSSL关系不大,利用Java的一个叫"keytool"的工具,可以将PFX转为JKS

    5.4 证书编码的转换

    • PEM转为DER openssl x509 -in cert.crt -outform der -out cert.der

    • DER转为PEM openssl x509 -in cert.crt -inform der -outform pem -out cert.pem

    6. 自建证书

    OpenSSL 是一个免费开源的库,它提供了构建数字证书的命令行工具。一般都是三级证书,为了简单我就只做2级了,大家可以自己签发三级。

    6.1 创建 Root CA

    a) 生成根证书私钥

    $ openssl genrsa -aes256 -out cakey.pem 2048 (生成私钥)

    $ openssl pkcs8 -topk8 -in cakey.pem -out ca.key -nocrypt (grpc格式

    b)生成根证书签发申请文件

    $openssl req -new -key ca.key -out ca.csr -subj "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=wwc" (/CN代表的就是域名)

    c)自签发根证书(cer文件)

    $ openssl x509 -req -days 365 -sha1 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.cer

    6.2 签发自建证书

    a) 生成证书私钥

    $ openssl genrsa -aes256 -out server.pem 2048 (生成私钥)

    $ openssl pkcs8 -topk8 -in server.pem -out server.key -nocrypt

    b)生成证书签发申请文件

    $openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=wancai"

    c)使用根证书签发服务端证书

    $ openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ca.cer -CAkey ca.key -CAserial ca.srl -CAcreateserial -in server.csr -out server.cer

    7. gRPC使用自建证书

    将server.cer(证书)和server.key(私钥)拷贝到工作目录

    gRPC的通信组件是netty、okhttp,netty带了ssl实现,有动态和静态两种方式来提供TLS的实现库,为了开发方便,我这里使用了boringssl实现库。okhttp也实现了TLS,但okhttp使用在移动端,故在此不表。

    <dependency>

        <groupId>io.grpc</groupId>
    <artifactId>grpc-all</artifactId>
    <version>${grpc.version}</version>
    </dependency>
    <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-tcnative-boringssl-static</artifactId>
    <version>1.1.33.Fork23</version>
    </dependency>

    gRPC Server端

    使用TLS,添加证书和私钥

    /* The port on which the server should run */
    int port = 8443;
    server = ServerBuilder.forPort(port)
    .addService(new GreeterImpl())
    .useTransportSecurity(loadCert("server.cer"),loadCert("server.key"))
    .build()
    .start();
    logger.info("Server started, listening on " + port);
    Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
     public void run() {
    // Use stderr here since the logger may have been reset by its JVM shutdown hook.
       System.err.println("*** shutting down gRPC server since JVM is shutting down");
    HelloWorldServer.this.stop();
    System.err.println("*** server shut down");
    }
    });

    gRPC Client端

    添加信任的证书,同时注意刚才我们建立证书的时候,域名是wancai,所以在这里需要添加域名,否则链接失败。

    SslContext sslContext = null;
    try {
    sslContext = GrpcSslContexts.forClient().trustManager(
    loadCert("server.cer")).build();
    } catch (Exception ex) {
    throw new RuntimeException(ex);
    }
    InetAddress address;
    try {
    address = InetAddress.getByName(host);
    address = InetAddress.getByAddress("wancai", address.getAddress());
    } catch (UnknownHostException ex) {
    throw new RuntimeException(ex);
    }
    channel = NettyChannelBuilder.forAddress(new InetSocketAddress(address, port))
    .flowControlWindow(65 * 1024)
    .negotiationType(NegotiationType.TLS)
    .sslContext(sslContext)
    .build();
    blockingStub = GreeterGrpc.newBlockingStub(channel);

    最后,我们通过 wireshark,抓包看看使用TLS加密和不加密通信的信息。

    当没有加密时,通信如下

    参考资料

    1. https://blog.helong.info/blog/2015/09/07/tls-protocol-analysis-and-crypto-protocol-design/

    2. http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html

    3. http://www.barretlee.com/blog/2016/04/24/detail-about-ca-and-certs/

    4. http://www.cnblogs.com/guogangj/p/4118605.html

    5. https://my.oschina.net/itblog/blog/651434

    6. http://blog.csdn.net/clementad/article/details/50620067

    7. https://imququ.com/post/protocol-negotiation-in-http2.html

  • 相关阅读:
    windows下pip安装python module失败
    设置jenkins的邮件通知功能
    jenkins 中 violation使用pylint
    pylint & jenkins
    dracut 基本介绍
    etcdctl 命令介绍
    python
    django --------------------- [必要操作]
    vim
    ssl选购
  • 原文地址:https://www.cnblogs.com/parse-code/p/6194934.html
Copyright © 2020-2023  润新知