浅谈网络协议:HTTPS 篇

378 阅读11分钟

这是我参与更文挑战的第 10 天,活动详情查看: 更文挑战

HTTPS 安全性的基础

比起明文的 HTTP,HTTPS 要更加安全。所谓的安全,体现在机密性、完整性和身份验证。

机密性 —— 对称加密/非对称加密/混合加密

为了确保信息传输的安全,必须使用加密算法对信息进行加密。加密和解密都需要依靠密钥,因此问题的关键就在于如何在一开始安全地传递密钥,也就是所谓的“密钥交换”。

对称加密:

加密和解密使用的是同一把钥匙(密钥)。但如何在最开始去传递钥匙成了一个问题,如果黑客截获了钥匙,那么就可以劫持篡改信息,无法确保信息安全

非对称加密:

用公钥加密,用私钥解密。假设客户端是发送方,服务端是接收方,那么服务端首先生成一对公钥和私钥,私钥自己保存,公钥发送给客户端,客户端每次都通过公钥对信息进行加密,然后把信息发给服务端,服务端再使用私钥进行解密。即使黑客截获了公钥,意义也不大,因为信息必须依靠私钥才能解密

混合加密:

非对称加密虽然解决了密钥交换的安全问题,但是每次通信都需要进行耗时的加密和解密,这会进一步拖慢双方的通信速度。而混合加密则将对称加密和非对称加密做了一个结合:

  • 服务端这边基于非对称加密算法生成一对公钥和私钥,然后把公钥传给客户端
  • 客户端这边基于对称加密算法生成一个后续通信使用的会话密钥(session key),用会话密钥加密要发送的信息,用公钥加密会话密钥本身,也就是说,客户端发给服务器的有两个东西:
    • 一个是用公钥加密的会话密钥
    • 一个是用会话密钥加密的信息
  • 服务器拿到后,首先用私钥解密,拿到会话密钥,然后再用会话密钥解密,拿到实际的信息。

在这之后,实际上已经解决了安全传递密钥(会话密钥)的问题。之后的每次通信中,双端只需要用同样的会话密钥加密和解密即可,这样就确保了安全 + 性能。

完整性和身份验证 —— 摘要算法、数字签名、数字证书

目前为止只保证了一定的机密性,但是还有两个问题没有解决:第一个是身份验证问题,服务端的公钥是自由分发的,意味着黑客可以拿着公钥冒充客户端;反过来,黑客也可以冒充服务端分发公钥。不管是客户端还是服务端,确认对方的身份成了一个问题;第二个是数据完整性问题,既然黑客可以冒充客户端或者服务端,那么也就可以劫持并篡改数据,如何确保数据的完整成了一个问题。

数字签名:

数字签名和非对称加密是反过来的,用私钥进行加密,而用公钥进行解密。服务端这边基于摘要算法,对要传输的数据进行 hash 计算,从而得到一个与数据对应的摘要(digest),然后用私钥对摘要进行加密,形成签名,接着把 签名+数据 发送给客户端。客户端收到签名和数据后:

  • 解决身份验证问题:首先,用此前拿到的公钥进行验签(解密签名),拿到摘要。公钥和私钥是一对的,客户端认为,既然自己可以用服务端当时发过来的公钥对此时收到的签名进行解密,那么就可以确保这个签名确实是服务端自己加密然后发送过来的,身份验证这一关就 ok 了;
  • 解决数据完整性问题:接着,客户端对数据进行同样的 hash 计算,得到另一个摘要,将这个摘要与签名中的摘要进行对比,如果一致,说明数据是完整没有被篡改的 —— 因为如果被篡改(哪怕只是细微的变动),那么客户端基于被篡改的数据进行 hash 得到的摘要会和签名中的摘要完全不同。

数字证书:

但是这一切建立在一个大前提下,那就是客户端持有的公钥确实是服务端的公钥。要知道,这个公钥最开始也是服务端自由分发的,因此可能被黑客动过手脚,然后黑客再将自己的公钥发给客户端,客户端不知道,可能自己一直在和黑客而不是服务端通信,毕竟它坚信自己确实收到了服务端的公钥,且自己持有的公钥可以解密对方用私钥加密的东西,因此不会产生怀疑。

为了解决这种情况,我们需要找一个受信任的第三方机构(认证机构 CA)参与其中,通过它(而不是服务端)来进行服务端公钥的分发,从而确保客户端得到的公钥确实是来自于服务端的。具体地说,服务端首先向 CA 提交相关信息,申请一个证书,证书的内容是:

  • 基本信息:CA 信息、服务端信息、服务端公钥、有效期等
  • 签名:针对上述信息生成一个摘要,用 CA 的私钥去加密这个摘要,得到一个签名。

现在,这个证书就是被签过名的了。CA 把生成的证书给服务端,服务端再把证书传给客户端,客户端通过从 CA 那里获得的公钥进行验签,一旦通过即可认为证书确实是来自于服务端的,自然也就可以使用证书中携带的服务端的公钥。自此,已经解决了“确保客户端持有的公钥确实是服务端的公钥”的问题,之后的流程就和前面说的一样了。那么如何确保 CA 是可以信任的呢?小的 CA 由大的 CA 来签名认证,形成一条信任链/证书链,链条的最后是一个可以自签名的根证书,我们必须无条件信任它。

上面说了,服务端需要把证书发送给客户端,那么黑客是否可能在这个过程中对证书做手脚呢?

  • 比如说,黑客可能会篡改证书的基本信息?不可能,只要篡改了基本信息,后续客户端拿 对信息进行 hash 计算得到的摘要 与 公钥解密签名得到的摘要 进行对比的时候,就会发现两者是不一致的,因此就会认定这个证书以及它携带的公钥是有问题的。
  • 那么,黑客是否可以先篡改证书的基本信息,然后再生成对应的摘要,再去加密这个摘要形成签名呢?这个也是不可能的,因为黑客没有 CA 的私钥,无法自己形成签名。

HTTPS 安全性的实现 —— TLS 连接

TLS 1.2 四次握手

HTTPS 的安全性是通过 SSL/TLS 实现的 —— https = http over SSL/TLS,即在原先的应用层和传输层中间再加一个 TLS 层。除了之前的 TCP 三次握手之外,客户端还需要和服务端通过四次握手建立 TLS 连接。

首先还是通过三次握手建立 TCP 连接:

接着通过四次握手建立 TLS 连接:

  1. 客户端发送 client hello 消息,主要内容是:
  • 自己支持的 TLS 协议版本(version)
  • 支持的密码套件列表(cipher suites)
  • 一个随机数(client random)
  1. 服务端收到 client hello 消息后:
  • 发送 server hello 消息进行回应,主要内容是:
    • 自己和客户端协商好的 TLS 协议版本(version)
    • 自己从密码套件列表中选用的一个密码套件(cipher suite)
    • 一个随机数(server random)发送 server certificate 消息,传递服务端证书
  • 发送 server key exchange 消息,传递密钥交换算法的参数(server params)
  • 发送 server hello done 消息,表示 server hello 完成
  1. 客户端验证证书,确认服务端身份无误后:
  • 发送 client key exchange,传递密钥交换算法的参数(client params)
  • 客户端和服务端拿着共有的 client params 和 server params,利用 ECDHE 算法计算出 预备主密钥(pre-master secret)
  • 客户端和服务端拿着共有的 client random、server random、pre-master secret,生成主密钥(master secret)。(这里使用了三个随机数,是为了在两端可能不可靠的情况下提高随机程度)
  • 主密钥派生出会话密钥
  • 客户端发送 change cipher spec 消息,通知服务端用会话密钥来加密双方通信
  • 客户端发送 finished,结束自己的握手
  1. 服务端收到消息后:
  • 发送 change sipher spec,表示同意用会话密钥加密双方的通信
  • 发送 finished,表示结束自己的握手

这样,TLS 四次握手就结束了,此后双方收发加密的 http 请求和响应。

PS:以上过程使用了 ECDHE 来交换会话密钥,这与使用 RSA 有两个不同点:

  • 使用 ECDHE,双端需要互换密钥交换算法的参数,利用两个参数生成 pre-master secret;使用 RSA,只需要由客户端这边直接生成 pre-master secret,并共享给服务端。当然,最终都需要使用 pre-master secret + client random + server random 生成 master secret,这点是一样的
  • 使用 ECDHE 可以实现 TLS False Start,即抢跑,客户端可以在服务端尚未响应 finished 的时候就开始发送 https 请求;使用 RSA,只能在双方都 finished 之后才能收发请求响应。

TLS 1.3 三次握手

TLS 1.3 中,客户端一开始发送 client hello 消息的时候,就携带上了密钥交换算法的参数(client params),而不是像 TLS 1.2 那样在第二次发送消息的时候再携带,因此 1.3 只需要三次握手,1次往返(1-rtt);而 1.2 需要四次握手,2次往返(2-rtt)

HTTPS 性能优化

相比 http,https 需要通过握手创建 tls 连接,最多需要消耗两个 rtt,因此 https 一般会比 http 慢。如何优化 https 的连接速度呢?可以从以下几个方面入手:

  • 硬件优化:更快更好的 CPU,加速握手和传输SSL 加速卡,加速加解密

  • 软件优化:Linux 内核版本升级,Nginx 版本升级,OpenSSL 版本升级等

  • 协议优化:使用 TLS 1.3,大幅度简化握手过程,只需要 1-rtt使用 ECDHE 作为密钥交换算法,支持 TLS False Start 抢跑

  • 证书优化:

    • 使用 ECDSA 证书优化传输过程,使用 OCSP 优化证书验证
  • 会话复用:

    TLS 连接的重点在于创建共有的主密钥(或者说会话密钥)。若是每次创建 TLS 连接都需要重新创建主密钥,未免太大费周章了 —— 假如这个主密钥可以缓存下来,那么以后同一个客户端和服务端再次连接的时候就可以直接拿来继续使用了。这就是会话复用,而实现会话复用有 session id 和 session ticket 两种方式。

    • session id:客户端和服务端首次握手成功后,双方会缓存一个 key-value,key 是 此次会话独有的 session-id,value 是会话密钥。下次同一个客户端再和服务端创建 TLS 连接的时候,会在 client hello 中携带上之前的 session-id,服务端通过 session-id 到内存中查找相关的 value,一旦找到就直接利用会话密钥恢复会话状态,在一个 rtt 内直接建立通信。
      • 缺点1:服务端需要保存大量的会话状态(key-value),压力比较大
      • 缺点2:若通过多台服务器进行负载均衡,客户端不一定会命中此前访问的服务器,那么还是得走完整的 TLS 连接流程
    • session ticket:服务端不再保留会话状态,而是把压力分散到客户端上 —— 即将此次会话的会话密钥进行加密,形成 ticket,发送给客户端。客户端保存 ticket,以后再次和服务端连接的时候,就发送 ticket 给服务端,服务端进行解密以获取会话密钥,双方再进行加密通信。
    • pre-shared key:不管是 session id 还是 session ticket,至少也都需要 1-rtt 才能恢复会话状态。而 TLS 1.3 提出的 pre-shared key 则只需要 0-rtt,它的原理和 session ticket 类似,但是客户端在再次连接时,会把 ticket 和 https 请求一并发送过去,这样,服务端若验证会话密钥没问题,就可以恢复会话状态,并在这第一次返回中响应数据给客户端。

    为了安全起见,这几种方式都需要验证会话密钥的有效期。