# [HTTP 系列] 第 4 篇 —— HTTPS

> 这里是《写给前端工程师的 HTTP 系列》, 记得有位大佬曾经说过: 大厂前端面试对 HTTP 的要求比 CSS 还要高, 由此可见 HTTP 的重要程度不可小视. 本篇是该系列的第 4 篇 —— HTTPS.

* [[HTTP 系列] 第 1 篇 —— 从 TCP/UDP 到 DNS 解析](https://www.yanceyleo.com/post/3a9d3d47-9977-4579-a756-4bf5af4a3fd4)
* [[HTTP 系列] 第 2 篇 —— HTTP 协议那些事](https://www.yanceyleo.com/post/efd566e9-4ee3-4ee2-8448-628414659480)
* [[HTTP 系列] 第 3 篇 —— HTTP 缓存那些事](https://www.yanceyleo.com/post/89731d8e-5510-4094-8962-462b127ed5d0)
* [[HTTP 系列] 第 4 篇 —— HTTPS](https://www.yanceyleo.com/post/f976717f-9e40-4849-a73f-86c63c0c45e1)
* [[HTTP 系列] 第 5 篇 —— 网络安全](https://www.yanceyleo.com/post/953ccf20-7f3b-4d3f-816e-b6e98b26a4da)
* [[HTTP 系列] 第 6 篇 —— 从输入 URL 回车到页面呈现](https://www.yanceyleo.com/post/05daeef2-2caf-4ebe-89f0-2ad9cae286c4)

## HTTPS 安全四要素

由于 HTTP 天生**明文传输**的特点, 整个传输过程完全透明, 任何人都能够在链路中截获, 修改或者伪造请求或者响应报文, 数据不具有可信性. 通常认为, 如果通信过程具备了四个特性, 就可以认为是**安全**的, 这四个特性是: 机密性, 完整性, 身份认证和不可否认.

* 机密性(Secrecy/Confidentiality)是指对数据的**保密**, 只能由可信的人访问, 对其他人是不可见的**秘密**, 简单来说就是不能让不相关的人看到不该看的东西.
* 完整性(Integrity, 也叫一致性)是指数据在传输过程中没有被篡改, 不多也不少, **完完整整**地保持着原状.
* 身份认证(Authentication)是指确认对方的真实身份, 也就是**证明你真的是你**, 保证消息只能发送给可信的人.
* 第四个特性是不可否认(Non-repudiation/Undeniable), 也叫不可抵赖, 意思是不能否认已经发生过的行为, 不能**说话不算数**, **耍赖皮**.

对于 HTTPS, 机密性由对称加密保证, 完整性由摘要算法保证, 身份认证和不可否认由非对称加密保证.

## 什么是 HTTPS

HTTPS 的本质是把 HTTP 下层的传输协议由 TCP/IP 换成了 SSL/TLS, 由**HTTP over TCP/IP**变成了**HTTP over SSL/TLS**, 让 HTTP 运行在了安全的 SSL/TLS 协议上, 收发报文不再使用 Socket API, 而是调用专门的安全接口.

![HTTPS 和 HTTP 对比图](https://static.yancey.app/mxn6p11p-1647791764263.webp)

SSL 即安全套接层(Secure Sockets Layer), 在 OSI 模型中处于第 5 层(会话层), 由网景公司于 1994 年发明, 有 v2 和 v3 两个版本, 而 v1 因为有严重的缺陷从未公开过. SSL 发展到 v3 时已经证明了它自身是一个非常好的安全通信协议, 于是互联网工程组 IETF 在 1999 年把它改名为 TLS(传输层安全, Transport Layer Security), 正式标准化, 目前应用的最广泛的 TLS 是 1.2.

TLS 由记录协议, 握手协议, 警告协议, 变更密码规范协议, 扩展协议等几个子协议组成, 综合使用了对称加密, 非对称加密, 身份认证等许多密码学前沿技术. 浏览器和服务器在使用 TLS 建立连接时需要选择一组恰当的加密算法来实现安全通信, 这些算法的组合被称为**密码套件**(cipher suite, 也叫加密套件).

下面这个例子中可以看出, 使用的 TLS 是 1.2, 客户端和服务器都支持非常多的密码套件, 密码套件的格式为 **密钥交换算法-签名算法-对称加密算法-分组模式-摘要算法**, 下面这个例子协商选定的是 **ECDHE-RSA-AES256-GCM-SHA384**. 即握手时使用 ECDHE 算法进行密钥交换, 用 RSA 签名和身份认证, 握手后的通信使用 AES 对称算法, 密钥长度 256 位, 分组模式是 GCM, 摘要算法 SHA384 用于消息认证和产生随机数. 

![密码套件](https://static.yancey.app/inp3i46p-1647792347196.webp)

OpenSSL 是一个著名的开源密码学程序库和工具包, 几乎支持所有公开的加密算法和协议, 已经成为了事实上的标准, 许多应用软件都会使用它作为底层库来实现 TLS 功能, 包括常用的 Web 服务器 Apache, Nginx 等.

## 加密

实现机密性最常用的手段是**加密**(encrypt), 就是把消息用某种方式转换成谁也看不懂的乱码, 只有掌握特殊**钥匙**的人才能再转换出原始文本.

这里的**钥匙**就叫做**密钥**(key), 加密前的消息叫**明文**(plain text/clear text), 加密后的乱码叫**密文**(cipher text), 使用密钥还原明文的过程叫**解密**(decrypt), 是加密的反操作, 加密解密的操作过程就是**加密算法**. 所有的加密算法都是公开的, 任何人都可以去分析研究, 而算法使用的**密钥**则必须保密.

**密钥**就是一长串的数字, 但约定俗成的度量单位是**位**(bit), 而不是**字节**(byte). 比如, 说密钥长度是 128, 就是 16 字节的二进制串, 密钥长度 1024, 就是 128 字节的二进制串.

按照密钥的使用方式, 加密可以分为两大类: **对称加密**和**非对称加密**.

### 对称加密

**对称加密**就是指加密和解密时使用的密钥都是同一个, 是**对称**的. 只要保证了密钥的安全, 明文通过该密钥加密, 也通过该密钥解密, 外人拿到的只是一段被加密的乱码, 那整个通信过程就可以说具有了机密性.

![对称加密](https://static.yancey.app/g95l573a-1647798875927.webp)

TLS 里有非常多的对称加密算法可供选择, 比如 RC4, DES, 3DES, AES, ChaCha20 等, 但前三种算法都被认为是不安全的, 通常都禁止使用, 目前常用的只有 AES 和 ChaCha20. AES 的意思是**高级加密标准**(Advanced Encryption Standard), 密钥长度可以是 128, 192 或 256. 它是 DES 算法的替代者, 安全强度很高, 性能也很好, 而且有的硬件还会做特殊优化, 所以非常流行, 是应用最广泛的对称加密算法. ChaCha20 是 Google 设计的另一种加密算法, 密钥长度固定为 256 位, 纯软件运行性能要超过 AES, 曾经在移动客户端上比较流行, 但 ARMv8 之后也加入了 AES 硬件优化, 所以现在不再具有明显的优势, 但仍然算得上是一个不错的算法.

对称算法还有一个**分组模式(Block cipher, 又称分块加密或块密码)**的概念, 它将明文分成多个等长的模块(block), 使用确定的算法和对称密钥对每组分别加密解密. 这是因为在实际加密中, 一般加密的数据不会只有几百 bit, 而是几 mb, 甚至几 gb. 这样, 加密过程就是每加密 128bit 接着再加密 128bit, 直至将全部数据加密完. 最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data), 在加密的同时增加了认证的功能, 常用的是 GCM, CCM 和 Poly1305.

把上面这些组合起来, 就可以得到 TLS 密码套件中定义的对称加密算法. 比如, AES128-GCM, 意思是密钥长度为 128 位的 AES 算法, 使用的分组模式是 GCM; ChaCha20-Poly1305 的意思是 ChaCha20 算法, 使用的分组模式是 Poly1305.

### 非对称加密

对称加密看上去好像完美地实现了机密性, 但其中有一个很大的问题: 如何把密钥安全地传递给对方, 术语叫**密钥交换**. 因为在对称加密算法中只要持有密钥就可以解密. 如果你和网站约定的密钥在传递途中被黑客窃取, 那他就可以在之后随意解密收发的数据, 通信过程也就没有机密性可言了.

因此就出现了非对称加密, 它有两个密钥, 一个叫**公钥**(public key), 一个叫**私钥**(private key). 两个密钥是不同的, **不对称**, 公钥可以公开给任何人使用, 而私钥必须严格保密. 公钥和私钥有个特别的**单向**性, 虽然都可以用来加密解密, 但公钥加密后只能用私钥解密, 反过来, 私钥加密后也只能用公钥解密. 非对称加密可以解决**密钥交换**的问题. 网站秘密保管私钥, 在网上任意分发公钥, 你想要登录网站只要用公钥加密就行了, 密文只能由私钥持有者才能解密. 而黑客因为没有私钥, 所以就无法破解密文.

具体来讲:

* 客户端向服务器发起连接请求，获取服务器公钥和证书。
* 客户端将该证书发送到本地信任中心进行验证，确认证书的合法性。
* 客户端使用服务器公钥，对产生的随机会话密钥进行加密，发送给服务器。
* 服务器收到客户端发送的信息后，使用自己的私钥对信息进行解密，得到会话密钥。
* 服务器使用该会话密钥对数据进行加密和解密。

![非对称加密](https://static.yancey.app/u35cioie-1647801298144.webp)

非对称加密算法要比对称加密算法复杂的多, 在 TLS 里只有很少的几种, 比如 DH, DSA, RSA, ECC 等.

#### RSA 算法

RSA 可能是其中最著名的一个, 几乎可以说是非对称加密的代名词, 它的安全性基于**整数分解**的数学难题, 使用两个超大素数的乘积作为生成密钥的材料, 想要从公钥推算出私钥是非常困难的. 以前 RSA 密钥推荐使用 1024 位, 但随着计算机运算能力的提高, 现在 1024 已经不安全, 普遍认为至少要 2048 位.

第一步选择两个大质数 p 和 q, p 不等于 q, 计算 N=p * q;

第二步是根据欧拉函数获取 r, 即 `r = φ(N) = φ(p)φ(q) = (p-1)(q-1)`. 欧拉函数 φ(n) 的定义是**小于或等于 n 的正整数中与 n 互质(如果两个或两个以上的整数的最大公约数是 1, 则称它们为互质)的数的数目**.

举个例子, φ(3 * 5) = 15, 其互质数有 1, 2, 4, 7, 8, 11, 13, 14. 关于欧拉公式的推导可以看[这篇文章](https://zh.wikipedia.org/wiki/%E6%AC%A7%E6%8B%89%E5%87%BD%E6%95%B0).

#### ECC 算法

ECC(Elliptic Curve Cryptography)是非对称加密里的**后起之秀**, 它基于**椭圆曲线离散对数**的数学难题, 使用特定的曲线方程和基点生成公钥和私钥, 子算法 ECDHE 用于密钥交换, ECDSA 用于数字签名. 目前比较常用的两个曲线是 P-256(secp256r1, 在 OpenSSL 称为 prime256v1)和 x25519. P-256 是 NIST(美国国家标准技术研究所)和 NSA(美国国家安全局)推荐使用的曲线, 而 x25519 被认为是最安全, 最快速的曲线.

比起 RSA, ECC 在安全强度和性能上都有明显的优势. 160 位的 ECC 相当于 1024 位的 RSA, 而 224 位的 ECC 则相当于 2048 位的 RSA. 因为密钥短, 所以相应的计算量, 消耗的内存和带宽也就少, 加密解密的性能就上去了, 对于现在的移动互联网非常有吸引力.

如下图是两个椭圆曲线 `y^2=x^3+7`, `y^2=x^3-x`.

![ECC 算法](https://static.yancey.app/cx3u6ed8-1647805889570.jpg)

#### ECDHE 算法

先从 ECDHE 算法的名字说起. ECDHE 就是**短暂 - 椭圆曲线 - 迪菲 - 赫尔曼**算法(ephemeral Elliptic Curve Diffie–Hellman), 里面的关键字是**短暂**, **椭圆曲线**和**迪菲 - 赫尔曼**, 我先来讲**迪菲 - 赫尔曼**, 也就是 DH 算法.

DH 算法是一种非对称加密算法, 只能用于密钥交换, 它的数学基础是**离散对数**(Discrete logarithm). 我们知道指数就是幂运算, 对数是指数的逆运算, 是已知底数和真数(幂结果), 反推出指数. 例如, 如果以 10 作为底数, 那么指数运算是 **y=10**x, 对数运算是 **y=logx**, 100 的对数是 2, 2 的对数是 0.301. 对数运算的域是实数, 取值是连续的, 而**离散对数**顾名思义, 取值是不连续的, 数值都是整数. 既然要取整, 那必然要涉及到模运算, 也就是取余数.

假设有模数 17, 底数 5, 而对数是 3, 那么有 **5**3** % 17 = 6**, 因此反过来, 以 5 为底, 17 为模数, 6 的离散对数就是 3. 这里的(17, 5)是离散对数的公共参数, 6 是真数, 3 是对数. 知道了对数, 就可以用幂运算很容易地得到真数, 但反过来, 知道真数却很难推断出对数, 于是就形成了一个**单向函数**.

知道了离散对数, 我们来看 DH 算法, 假设 Alice 和 Bob 约定使用 DH 算法来交换密钥.

Alice 和 Bob 需要首先确定模数和底数作为算法的参数, 这两个参数是公开的, 用 P 和 G 来代称, 简单起见我们还是用 17 和 5(P=17, G=5). 然后 Alice 和 Bob 各自选择一个随机整数作为私钥(必须在 1 和 P - 2 之间), 严格保密. 比如 Alice 选择 a = 10, Bob 选择 b = 5.

有了 DH 的私钥, Alice 和 Bob 再计算幂作为公钥, 也就是 **A = (G**a** % P) = 9, B = (G**b** % P) = 14**, 这里的 A 和 B 完全可以公开, 因为根据离散对数的原理, 从真数反向计算对数 a 和 b 是非常困难的.

交换 DH 公钥之后, Alice 手里有五个数：P=17, G=5, a=10, A=9, B=14, 然后执行一个运算：**(B**a** % P)= 8**.

因为离散对数的幂运算有交换律, **B**a** = (G**b** )**a** = (G**a**)**b** = A**b, 所以 Bob 计算 **A**b** % P** 也会得到同样的结果 8, 这个就是 Alice 和 Bob 之间的共享秘密, 可以作为会话密钥使用, 也就是 TLS 里的 Pre-Master.

![DH](https://static.yancey.app/lsjqrrp5-1649089070342.webp)

整个通信过程中, Alice 和 Bob 公开了 4 个信息：P, G, A, B, 其中 P, G 是算法的参数, A 和 B 是公钥, 而 a, b 是各自秘密保管的私钥, 无法获取, 所以黑客只能从已知的 P, G, A, B 下手, 计算 9 或 14 的离散对数. 由离散对数的性质就可以知道, 如果 P 非常大, 那么他很难在短时间里破解出私钥 a, b, 所以 Alice 和 Bob 的通信是安全的.

DH 算法有两种实现形式, 一种是已经被废弃的 DH 算法, 也叫 static DH 算法, 另一种是现在常用的 DHE 算法(有时候也叫 EDH). static DH 交换密钥时就只有客户端的公钥会变, 而服务器公钥不变, 在长期通信时就增加了被破解的风险. 而 DHE 算法的关键在于**E**表示的临时性上(ephemeral), 每次交换密钥时双方的私钥都是随机选择, 临时生成的, 用完就扔掉, 下次通信不会再使用, 相当于**一次一密**, 具有**前向安全**.

ECDHE 算法, 就是把 DHE 算法里整数域的离散对数, 替换成了椭圆曲线上的离散对数. 也就是上面 ECC 算法那张图, 因为椭圆曲线离散对数的计算难度比普通的离散对数更大, 那破解起来就更加困难了.

### 混合加密

虽然非对称加密能解决密钥交换的安全问题, 但由于它们都是基于复杂的数学难题, 运算速度很慢, 而对称加密使用的是位运算, 相对就很快. 即使是 ECC 也要比 AES 差上好几个数量级. 这样虽然保证了安全, 但无法保证速度. 因此, 在实际应用中, TLS 里使用的混合加密方式来保障机密性.

即在通信刚开始的时候使用非对称算法, 比如 RSA, ECDHE, 首先解决密钥交换的问题.

然后用随机数产生对称算法使用的**会话密钥**(session key), 再用公钥加密. 因为会话密钥很短, 通常只有 16 字节或 32 字节, 所以慢一点也无所谓.

对方拿到密文后用私钥解密, 取出会话密钥. 这样, 双方就实现了对称密钥的安全交换, 后续就不再使用非对称加密, 全都使用对称加密. 这样混合加密就解决了对称加密算法的密钥交换问题, 而且安全和性能兼顾, 完美地实现了机密性.

![混合加密示意图](https://static.yancey.app/w7drrjqk-1647806315377.webp)

## 摘要算法

混合加密保障了数据的机密性, 但仍然无法保障完整性, 因为虽然黑客无法破解机密数据, 但如果给加密后的数据添油加醋, 甚至篡改, 那么最终接收到的数据仍是残缺的. 因此需要一种机制来保障数据的完整性.

实现完整性的手段主要是摘要算法(Digest Algorithm), 也就是常说的散列函数, 哈希函数(Hash Function). 它能够把任意长度的数据**压缩**成固定长度, 而且独一无二的**摘要**字符串, 就好像是给这段数据生成了一个数字**指纹**. 摘要算法实际上是把数据从一个**大空间**映射到了**小空间**, 所以就存在**冲突**(collision, 也叫碰撞)的可能性, 即有两份或多份不同的原文对应相同的摘要. 好的摘要算法必须能够**抵抗冲突**, 让这种可能性尽量地小.

常见的摘要算法有 MD5(Message-Digest 5), SHA-1(Secure Hash Algorithm 1), 能够生成 16 字节和 20 字节长度的数字摘要. 但这两个算法的安全强度比较低, 不够安全, 在 TLS 里已经被禁止使用了. 目前 TLS 推荐使用的是 SHA-2. SHA-2 实际上是一系列摘要算法的统称, 总共有 6 种, 常用的有 SHA224, SHA256, SHA384, 分别能够生成 28 字节, 32 字节, 48 字节的摘要.

因为摘要算法对输入具有**单向性**和**雪崩效应**, 输入的微小不同会导致输出的剧烈变化, 所以也被 TLS 用来生成伪随机数(PRF, pseudo random function).

摘要算法保证了**数字摘要**和原文是完全等价的. 所以, 我们只要在原文后附上它的摘要, 就能够保证数据的完整性. 比如, 你发了条消息: **转账 1000 元**, 然后再加上一个 SHA-2 的摘要. 网站收到后也计算一下消息的摘要, 把这两份**指纹**做个对比, 如果一致, 就说明消息是完整可信的, 没有被修改.

![摘要算法](https://static.yancey.app/ywqao5x7-1647883741961.webp)

通过使用摘要算法来保障数据的完整性, 但如果是明文传输, 数据仍然是会被泄露的, 因此完整性必须要建立在机密性之上, 在混合加密系统里用会话密钥加密消息和摘要, 这样黑客无法得知明文, 也就没有办法动手脚了. 这里有个术语, 叫哈希消息认证码(HMAC). 它通过特别计算方式之后产生的消息认证码(MAC), 使用密码散列函数, 同时结合一个加密密钥. 它可以用来保证资料的完整性, 同时可以用来作某个消息的身份验证. 具体的算法可参考 [HMAC](https://zh.wikipedia.org/wiki/HMAC).

![哈希消息认证码](https://static.yancey.app/7eflrll0-1647884511734.webp)

## 数字签名

似乎加密算法结合摘要算法已经保障了数据的机密性和完整性, 但这里有个漏洞. 上面说的都是黑客可以伪装成网站来窃取你的信息, 但反过来, 黑客也可以冒充你向网站发送支付, 转账等消息, 网站没有办法确认你的身份, 钱可能就这么被偷走了. 现实生活中我们通过签名或印章, 来证明**我是我**. 这种只能由本人持有, 而其他任何人都不会有的便是**私钥**.

数字签名的原理其实很简单, 就是把公钥私钥的用法反过来, 之前是公钥加密, 私钥解密, 现在是私钥加密, 公钥解密. 但又因为非对称加密效率太低, 所以私钥只加密原文的摘要, 这样运算量就小的多, 而且得到的数字签名也很小, 方便保管和传输. 签名和公钥一样完全公开, 任何人都可以获取. 但这个签名只有用私钥对应的公钥才能解开, 拿到摘要后, 再比对原文验证完整性, 就可以像签署文件一样证明消息确实是你发的(这个工程的专业术语叫做**验签**). 这种使用私钥加密摘要的策略, 就能够实现**数字签名**, 同时实现**身份认证**和**不可否认**.

![数字签名过程](https://static.yancey.app/xrm7ssui-1647948635651.webp)

只要你和网站互相交换公钥, 就可以用**签名**和**验签**来确认消息的真实性, 因为私钥保密, 黑客不能伪造签名, 就能够保证通信双方的身份. 比如, 你用自己的私钥签名一个消息**我是小明**. 网站收到后用你的公钥验签, 确认身份没问题, 于是也用它的私钥签名消息**我是某宝**. 你收到后再用它的公钥验一下, 也没问题, 这样你和网站就都知道对方不是假冒的, 后面就可以用混合加密进行安全通信了.

## 数字证书和 CA

上面说道**只要你和网站互相交换公钥, 就可以用**签名**和**验签**来确认消息的真实性**. 不知道你是否有疑惑, 因为公钥是公开的, 谁都可以发布公钥, 我们还缺少防止黑客伪造公钥的手段, 也就是说, 怎么来判断这个公钥就是你或者某宝的公钥呢? 这就需要 CA(Certificate Authority, 证书认证机构)来帮助我们. 它具有极高的可信度, 由它来给各个公钥签名, 用自身的信誉来保证公钥无法伪造, 是可信的.

CA 对公钥的签名认证也是有格式的, 不是简单地把公钥绑定在持有者身份上就完事了, 还要包含序列号, 用途, 颁发者, 有效时间等等, 把这些打成一个包再签名, 完整地证明公钥关联的各种信息, 形成**数字证书**(Certificate).

![CA 证书字段](https://static.yancey.app/fzhzzodh-1648388697871.jpg)

证书根据等级分为 DV, OV, EV 三种:

| 类型/区别      | 审核内容                 | 颁发周期       | 使用年限       | 浏览器显示形式                    | 适用对象                            | 价格             |
| ---------- | -------------------- | ---------- | ---------- | -------------------------- | ------------------------------- | -------------- |
| DV         | 域名所有权                | 几分钟-几小时    | 1-2 年      | https + 小锁标志               | 中小型企业网站, 电子商务网站, 电子邮局服务器, 个人网站等 | 免费             |
| OV         | 域名所有权; 企业信息;         | 2-3 个工作日   | 1-2 年      | https + 小锁标志               | 企业网站, 电子商务网站, 证券, 金融机构等         | 2,982.40 元/年 * |
| EV         | 域名所有权; 企业信息; 第三方数据核查 | 5-7 个工作日   | 一 1-2 年年   | https + 小锁标志 + 绿色网址 + 企业名称 | 银行, 保险, 金融机构, 电子商务网站, 大型企业等     | 6,400 元/年 *    |

>  上述价格节选自阿里云单域名每年的价格, 时间为 2022/03/23, 具体价格以实际购买时的价格为准. 

不过, CA 怎么证明自己呢? 这还是信任链的问题. 小一点的 CA 可以让大 CA 签名认证, 但链条的最后, 也就是 Root CA, 就只能自己证明自己了, 这个就叫**自签名证书**(Self-Signed Certificate)或者**根证书**(Root Certificate). 你必须相信, 否则整个证书信任链就走不下去了.

![证书自证](https://static.yancey.app/4azofok4-1647997260175.webp)

有了这个证书体系, 操作系统和浏览器都内置了各大 CA 的根证书, 上网的时候只要服务器发过来它的证书, 就可以验证证书里的签名, 顺着证书链(Certificate Chain)一层层地验证, 直到找到根证书, 就能够确定证书是可信的, 从而里面的公钥也是可信的.

### 证书体系的弱点

证书体系(PKI, Public Key Infrastructure)虽然是目前整个网络世界的安全基础设施, 但绝对的安全是不存在的, 它也有弱点, 还是关键的**信任**二字. 如果 CA 失误或者被欺骗, 签发了错误的证书, 虽然证书是真的, 可它代表的网站却是假的. 还有一种更危险的情况, CA 被黑客攻陷, 或者 CA 有恶意, 因为它(即根证书)是信任的源头, 整个信任链里的所有证书也就都不可信了.

针对第一种, 开发出了 CRL(证书吊销列表, Certificate revocation list)和 OCSP(在线证书状态协议, Online Certificate Status Protocol), 及时废止有问题的证书. 对于第二种, 因为涉及的证书太多, 就只能操作系统或者浏览器从根上**下狠手**了, 撤销对 CA 的信任, 列入**黑名单**, 这样它颁发的所有证书就都会被认为是不安全的.

## ECDHE 详细握手过程

在 HTTP 协议里, 通过三次握手建立连接后, 浏览器会立即发送请求报文. 但现在是 HTTPS 协议, 它需要再用另外一个**握手**过程, 在 TCP 上建立安全连接, 之后才是收发 HTTP 报文. 在讲 TLS 握手之前, 先简单介绍一下 TLS 协议的组成.

TLS 协议有多个模块组成, 比较常用的有记录协议, 警报协议, 握手协议, 变更密码规范协议等.

**记录协议**(Record Protocol)规定了 TLS 收发数据的基本单位: 记录(record). 它有点像是 TCP 里的 segment, 所有的其他子协议都需要通过记录协议发出. 但多个记录数据可以在一个 TCP 包里一次性发出, 也并不需要像 TCP 那样返回 ACK.

**警报协议**(Alert Protocol)的职责是向对方发出警报信息, 有点像是 HTTP 协议里的状态码. 比如, protocol_version 就是不支持旧版本, bad_certificate 就是证书有问题, 收到警报后另一方可以选择继续, 也可以立即终止连接.

**握手协议**(Handshake Protocol)是 TLS 里最复杂的子协议, 要比 TCP 的 SYN/ACK 复杂的多, 浏览器和服务器会在握手过程中协商 TLS 版本号, 随机数, 密码套件等信息, 然后交换证书和密钥参数, 最终双方协商得到会话密钥, 用于后续的混合加密系统.

**变更密码规范协议**(Change Cipher Spec Protocol), 它非常简单, 就是一个**通知**, 告诉对方, 后续的数据都将使用加密保护. 那么反过来, 在它之前, 数据都是明文的.

下面的这张图简要地描述了 TLS 的握手过程, 其中每一个**框**都是一个记录, 多个记录组合成一个 TCP 包发送. 所以, 最多经过两次消息往返(4 个消息)就可以完成握手, 然后就可以在安全的通信环境里发送 HTTP 报文, 实现 HTTPS 协议.

![TLS 协议](https://static.yancey.app/2gloicuc-1647999700009.webp)

短暂椭圆曲线迪菲 - 赫尔曼密钥交换(Elliptic Curve Diffie - Hellman key exchange, 缩写为 ECDH), 是一种匿名的密钥合意协议(Key-agreement protocol), 这是迪菲 - 赫尔曼密钥交换的变种, 采用椭圆曲线密码学来加强性能与安全性. 在这个协定下, 双方利用由椭圆曲线密码学建立的公钥与私钥对, 在一个不安全的通道中, 建立起安全的共有加密资料. 临时 ECDH(ECDH Ephemeral, ECDHE)能够提供前向安全性.

![ECDHE 详细握手过程](https://static.yancey.app/jbf7vavm-1648374811554.webp)

### 阶段 1: Client Hello

在 TCP 建立连接之后, 浏览器会首先发一个 **Client Hello** 消息, 也就是跟服务器**打招呼**. 里面有客户端的版本号, 支持的密码套件, 还有一个**随机数**(Client Random), 用于后续生成会话密钥, 具体所有参数在下面. **Client Hello** 的目的就是: **我这边有这些这些信息, 你看看哪些是能用的, 关键的随机数可得留着.**

* 支持的 SSL 版本
* 客户端生成的一个用于生成主密钥(master key)的 32 字节的随机数(主密钥由客户端和服务端的随机数共同生成)
* 会话 ID
* 加密套件
  * 加密算法
  * 密钥交换算法
  * MAC 算法
  * 加密方式(流, 分组)
* 压缩算法(由于压缩会带来安全漏洞(CRIME 攻击, Compression Ratio Info-leak Made Easy), 所以压缩算法这里一般写死成 null, 后面 TLS 1.3 明令禁止使用压缩. 简单说一下攻击原理: 它依赖于攻击者能观察浏览器发送的密文的大小, 并在同时诱导浏览器发起多个精心设计的到目标网站的连接. 攻击者会观察已压缩请求载荷的大小, 其中包括两个浏览器只发送到目标网站的私密 Cookie, 以及攻击者创建的变量内容. 当压缩内容的大小降低时, 攻击者可以推断注入内容的某些部分与源内容的某些部分匹配, 其中包括攻击者想要发掘的私密内容. 使用分治法技术可以用较小的尝试次数解读真正秘密的内容, 需要恢复的字节数会大幅降低)

![Client Hello](https://static.yancey.app/to2ed2xt-1648319923179.jpg)

下面是客户端支持的所有加密套件, 后面服务端可以选择其中的一个作为此次通信使用的加密套件算法.

![加密套件](https://static.yancey.app/6ezavr4h-1648320849344.jpg)

### 阶段 2: Server Hello

服务器收到 **Client Hello** 后, 会返回一个 **Server Hello** 消息. 把版本号确认一下, 也给出一个**随机数**(Server Random), 需要客户端也留着, 然后从客户端的列表里选一个作为本次通信使用的密码套件.

* 服务端采纳的本次通讯的 SSL 版本
* 服务端生成的一个用于生成主密钥(master key)的 32 字节的随机数(主密钥由客户端和服务端的随机数共同生成), 它是由随机种子 gmt_unix_time 使用伪随机数函数(PRF)生成的 32 字节随机数.
* 会话 ID: 如果没有建立过连接则对应值为空, 不为空则说明之前建立过对应的连接并缓存.
* 服务端采纳的用于本次通讯的加密套件(从客户端发送的加密套件列表中选出了一个, 下面的例子选出的加密组合是 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ECDHE_RSA 作为密钥交换算法)
  * 加密算法
  * 密钥交换算法
  * MAC 算法
  * 加密方式(流, 分组)
* 压缩算法(如果支持压缩的话)

![Server Hello](https://static.yancey.app/rtnrb58y-1648375017814.jpg)

### 阶段 2: Server Certificate

SSL 服务器将**携带自己公钥信息的数字证书**和到根 CA 整个链发给客户端通过 Certificate 消息发送给 SSL 客户端(整个公钥文件都发送过去), 客户端使用这个公钥完成以下任务:

* 客户端可以使用该公钥来验证服务端的身份, 因为只有服务端有对应的私钥能解密它的公钥加密的数据;
* 用于对 **premaster secret** 进行加密, 这个 **premaster secret** 就是用客户端和服务端生成的 Random 随机数来生成的, 客户端用服务端的公钥对其进行了加密后发送给服务端.

下面这个例子, 我们访问的是百度, 它会给我们从百度的证书到根 CA 证书.

![Server Certificate Chain](https://static.yancey.app/zzats6wk-1648385767041.jpg)

![CA Chain](https://static.yancey.app/ityhtt6n-1648388509073.jpg)

### 阶段 2: Server Key Exchange

下一步则是密钥交换阶段, Server Key Exchange 消息中包含有密钥交换算法所需要的额外参数, 它是一个可选步骤, 之所以说是可选步骤, 是因为只有在下列场景下这个步骤才会发生:

* 协商采用了 RSA 加密, 但是服务端的证书没有提供 RSA 公钥
* 协商采用了 DH 加密, 但是服务端的证书没有提供 DH 参数
* 协商采用了 fortezza_kea 加密, 但是服务端的证书没有提供参数

总结来说, **Server Key Exchange**这个步骤是对上一步 **Certificate** 的一个补充, 为了让整个 SSL 握手过程能正常进行.

![Server Key Exchange](https://static.yancey.app/4nq5zant-1648392843770.png)

### 阶段 2: Server Hello Done

SSL 服务器发送 Server Hello Done 消息, 通知 SSL 客户端版本和加密套件协商结束. 这样第一个消息往返就结束了(两个 TCP 包), 结果是客户端和服务器通过明文共享了三个信息: Client Random, Server Random 和 Server Params.

![Server Hello Done](https://static.yancey.app/4x6gg567-1648392989481.png)

### 阶段 3: Client Key Exchange

客户端这时也拿到了服务器的证书, 开始走证书链逐级验证, 确认证书的真实性, 再用证书公钥验证签名, 就确认了服务器的身份. 利用证书中的公钥加密 SSL 客户端随机生成的 **premaster secret**(通过之前客户端, 服务端分别生成的随机数生成的), 并通过 Client Key Exchange 消息发送给 SSL 服务器. 注意, 这一步完成后, 客户端和服务端都已经保存了**主密钥**(之所以这里叫预备主密钥, 是因为还没有投入使用). 这个**主密钥**会用于之后的 SSL 通信数据的加密. master secret 的伪代码算法如下:

```ts
master_secret = PRF(
  pre_master_secret,
  "master secret",
  ClientHello.random + ServerHello.random
);
```

![Client Key Exchange](https://static.yancey.app/32ujnxab-1648393270773.png)

### 阶段 3: Change Cipher Spec

客户端发送 **Change Cipher Spec**(密钥改变协议), 通知 SSL 服务器后续报文将采用协商好的**主密钥**和加密套件进行加密和 MAC 计算, 即以后咱们都用这个密钥进行通信数据的加密吧. 然后客户端会发送一个 Finished, 表示结束了.

![Change Cipher Spec](https://static.yancey.app/rc7r05kr-1648393413671.png)

### 阶段 3: Finished

SSL 客户端计算已交互的握手消息(除 Change Cipher Spec 消息外所有已交互的消息)的 Hash 值, 利用协商好的密钥和加密套件处理 Hash 值(计算并添加 MAC 值, 加密等), 并通过 Finished 消息发送给 SSL 服务器. SSL 服务器利用同样的方法计算已交互的握手消息的 Hash 值, 并与 Finished 消息的解密结果比较, 如果二者相同, 且 MAC 值验证成功, 则证明密钥和加密套件协商成功. 意思就是告诉服务器: 后面都改用对称算法加密通信了, 用的就是打招呼时说的 AES, 加密对不对还得你测一下.

### 阶段 4: 客户端发送请求

客户端使用主密钥加密数据, 发送给服务端.

![加密后的数据](https://static.yancey.app/8blqnzab-1648393278877.png)

### 阶段 4: Change Cipher Spec

同样地, SSL 服务器发送 Change Cipher Spec 消息, 通知 SSL 客户端后续报文将采用协商好的密钥和加密套件进行加密和 MAC 计算.

### 阶段 4: Finished

SSL 服务器计算已交互的握手消息的 Hash 值, 利用协商好的密钥和加密套件处理 Hash 值(计算并添加 MAC 值, 加密等), 并通过 Finished 消息发送给 SSL 客户端. SSL 客户端利用同样的方法计算已交互的握手消息的 Hash 值, 并与 Finished 消息的解密结果比较, 如果二者相同, 且 MAC 值验证成功, 则证明密钥和加密套件协商成功.

SSL 客户端接收到 SSL 服务器发送的 Finished 消息后, 如果解密成功, 则可以判断 SSL 服务器是数字证书的拥有者, 即 SSL 服务器身份验证成功, 因为只有拥有私钥的 SSL 服务器才能从 Client Key Exchange 消息中解密得到 premaster secret, 从而间接地实现了 SSL 客户端对 SSL 服务器的身份验证.

### 阶段 4: 服务端发送响应

最后就是服务端使用协商好的加密算法加密响应数据, 返回给客户端.

### 双向认证

上面说的是**单向认证**握手过程, 只认证了服务器的身份, 而没有认证客户端的身份. 这是因为通常单向认证通过后已经建立了安全通信, 用账号, 密码等简单的手段就能够确认用户的真实身份. 但为了防止账号, 密码被盗, 有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书, 实现**双向认证**, 这样会更加安全. 双向认证的流程也没有太多变化, 只是在**Server Hello Done**之后, **Client Key Exchange**之前, 客户端要发送**Client Certificate**消息, 服务器收到后也把证书链走一遍, 验证客户端的身份.

我记录了一份 [gist](https://gist.github.com/YanceyOfficial/6b0bd5579f7fb244044541b7e38e6acc), 可以看到双向认证的流程.

## 谈一谈 TLS 1.3

上面说的是都是基于 TLS 1.2, 它发布于 2008 年, 从现在来看在很多方面已经力不从心了. 因此经过近 30 个草案的反复打磨, TLS 1.3 于 2018 年发布, 再次确立了信息安全领域的新标准. TLS 1.3 的三个目标是**兼容**, **安全**与**性能**.

![TLS 1.3](https://static.yancey.app/3znd82t2-1648887969787.jpg)

### 兼容 1.1 和 1.2

由于 1.1, 1.2 等协议已经出现了很多年, 屎山已经改不动了, 因此必须要求 TLS 1.3 进行**兼容**. 它采用了**扩展协议(Extension Protocol)的手段, 通过在记录末尾添加一系列的扩展字段**来增加新的功能, 老版本的 TLS 不认识它可以直接忽略, 这就实现了**后向兼容**.

在记录头的 Version 字段被兼容性**固定**的情况下, 只要是 TLS1.3 协议, 握手的 **Hello** 消息后面就必须有 **supported_versions** 扩展, 它标记了 TLS 的版本号, 使用它就能区分新旧协议.

```shell
Handshake Protocol: Client Hello
    Version: TLS 1.2 (0x0303)
    Extension: supported_versions (len=11)
        Supported Version: TLS 1.3 (0x0304)
        Supported Version: TLS 1.2 (0x0303)
```

### 更高的安全性

TLS 1.2 在十来年的应用中获得了许多宝贵的经验, 陆续发现了很多的漏洞和加密算法的弱点, 所以 TLS 1.3 就在协议里修补了这些不安全因素. 比如:

* 伪随机数函数由 PRF 升级为 HKDF(HMAC-based Extract-and-Expand Key Derivation Function);
* 明确禁止在记录协议里使用压缩;
* 废除了 RC4, DES 对称加密算法;
* 废除了 ECB, CBC 等传统分组模式;
* 废除了 MD5, SHA1, SHA-224 摘要算法;
* 废除了 RSA, DH 密钥交换算法和许多命名曲线.

这样 TLS1.3 里只保留了 AES, ChaCha20 对称加密算法, 分组模式只能用 AEAD 的 GCM, CCM 和 Poly1305, 摘要算法只能用 SHA256, SHA384, 密钥交换算法只有 ECDHE 和 DHE, 椭圆曲线也被**砍**到只剩 P-256 和 x25519 等 5 种. 基于此, TLS 1.3 的密码套件数量大幅减少:

![TLS 1.3 的密码套件](https://static.yancey.app/wgscs3uv-1648885988928.webp)

这里还要特别说一下废除 RSA 和 DH 密钥交换算法的原因. 上面我们介绍到 ECDHE, 其实还有一种使用 RSA 来做密钥交换, 但是浏览器默认会使用 ECDHE 而不是 RSA 做密钥交换, 这是因为它不具有**前向安全**(Forward Secrecy). 这是因为 RSA 服务端的私钥是**固定**的, 一旦私钥被破解了, 加密就不安全了. 而 ECDHE 算法在每次握手时都会生成一对临时的公钥和私钥, 即便是某次被破解了, 也只会影响本次通信, 而不会影响后续的. 所以现在主流的服务器和浏览器在握手阶段都已经不再使用 RSA, 改用 ECDHE, 而 TLS1.3 在协议里明确废除 RSA 和 DH 则在标准层面保证了**前向安全**.

此外, TLS 1.3 还做了防恶意降级机制, 如果发现中间人恶意将版本降级到 1.2, 服务器的最后八个字节会被设置为 44 4F 57 4E 47 52 44 01, 即 DOWNGRD01, 支持 TLS 1.3 的客户端就会识别到, 然后发出报警.

### 性能的提升

HTTPS 建立连接时除了要做 TCP 握手, 还要做 TLS 握手, 在 TLS 1.2 中会多花两个消息往返(2-RTT), 可能导致几十毫秒甚至上百毫秒的延迟, 在移动网络中延迟还会更严重. 而 TLS 1.3 因为密码套件大幅度简化(只有 5 个), 也就没有必要再像以前那样走复杂的协商流程了.

此外, TLS1.3 压缩了以前的 **Hello** 协商过程, 删除了 **Key Exchange** 消息, 把握手时间减少到了 **1-RTT**, 效率提高了一倍. 具体的做法还是利用了**扩展**. 客户端在 **Client Hello** 消息里直接用 **supported_groups** 带上支持的曲线, 比如 P-256, x25519, 用**key_share**带上曲线对应的客户端公钥参数, 用 **signature_algorithms** 带上签名算法. 服务器收到后在这些扩展里选定一个曲线和参数, 再用 **key_share** 扩展返回服务器这边的公钥参数, 就实现了双方的密钥交换, 后面的流程就和 TLS 1.2 基本一样了. 如下是 TLS 1.3 握手的概略图.

![TLS 1.3 握手的概略图](https://static.yancey.app/rgclesir-1648887706093.webp)

除了标准的 **1-RTT** 握手, TLS1.3 还引入了 **0-RTT** 握手, 用 **pre_shared_key** 和 **early_data** 扩展, 在 TCP 连接后立即就建立安全连接发送加密消息.

### TLS 1.3 握手分析

![TLS 1.3 握手分析](https://static.yancey.app/gitkz6uh-1648887627101.webp)

在 Client Hello 阶段, 除了跟 TLS 1.2 相同的部分, 还增加了 **supported_versions**(支持的版本), **supported_groups**(支持的曲线), **key_share**(曲线对应的参数) 等字段. 具体可以看 

```shell
Handshake Protocol: Client Hello
    Version: TLS 1.2 (0x0303)
    Random: cebeb6c05403654d66c2329...
    Cipher Suites (18 suites)
        Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
        Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
        Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
    Extension: supported_versions (len=9)
        Supported Version: TLS 1.3 (0x0304)
        Supported Version: TLS 1.2 (0x0303)
    Extension: supported_groups (len=14)
        Supported Groups (6 groups)
            Supported Group: x25519 (0x001d)
            Supported Group: secp256r1 (0x0017)
    Extension: key_share (len=107)
        Key Share extension
            Client Key Share Length: 105
            Key Share Entry: Group: x25519
            Key Share Entry: Group: secp256r1
```

Server Hello 阶段, 如果确认使用 TLS 1.3, **supported_versions** 会标明使用的是 TLS 1.3, 然后在 **key_share** 扩展带上曲线和对应的公钥参数.

```shell
Handshake Protocol: Server Hello
    Version: TLS 1.2 (0x0303)
    Random: 12d2bce6568b063d3dee2...
    Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
    Extension: supported_versions (len=2)
        Supported Version: TLS 1.3 (0x0304)
    Extension: key_share (len=36)
        Key Share extension
            Key Share Entry: Group: x25519, Key Exchange length: 32
```

这时只交换了两条消息, 客户端和服务器就拿到了四个共享信息: **Client Random** 和 **Server Random**, **Client Params** 和 **Server Params**, 两边就可以各自用 ECDHE 算出 **Pre-Master**, 再用 HKDF 生成主密钥 **Master Secret**, 效率比 TLS 1.2 提高了一大截, 因为不需要在 cipher suite 协商了.

在算出主密钥后, 服务器立刻发出 **Change Cipher Spec** 消息, 比 TLS 1.2 提早进入加密通信, 后面的证书等就都是加密的了, 减少了握手时的明文信息泄露.

这里 TLS1.3 还有一个安全强化措施, 多了个 **Certificate Verify** 消息, 用服务器的私钥把前面的曲线, 套件, 参数等握手数据加了签名, 作用和 **Finished** 消息差不多. 但由于是私钥签名, 所以强化了身份认证和和防窜改.

这两个 **Hello** 消息之后, 客户端验证服务器证书, 再发 **Finished** 消息, 就正式完成了握手, 开始收发 HTTP 报文.

## HTTPS 的优化

我们知道, HTTPS 连接大致上可以划分为两个部分, 第一个是建立连接时的非对称加密握手, 第二个是握手后的对称加密报文传输. 由于目前流行的 AES, ChaCha20 性能都很好, 还有硬件优化, 报文传输的性能损耗可以说是非常地小, 小到几乎可以忽略不计了. 所以, 通常所说的**HTTPS 连接慢**指的就是**刚开始建立连接的那段时间**.

在 TCP 建连之后, 正式数据传输之前, HTTPS 比 HTTP 增加了一个 TLS 握手的步骤, 这个步骤最长可以花费两个消息往返, 也就是 2-RTT. 而且在握手消息的网络耗时之外, 还会有其他的一些**隐形**消耗, 比如:

* 产生用于密钥交换的临时公私钥对(ECDHE);
* 验证证书时访问 CA 获取 CRL 或者 OCSP;
* 非对称加密解密处理**Pre-Master**.

![TLS 握手的耗时之处](https://static.yancey.app/f3ze23e9-1648991828388.webp)

在最差的情况下, 也就是不做任何的优化措施, HTTPS 建立连接可能会比 HTTP 慢上几百毫秒甚至几秒, 这其中既有网络耗时, 也有计算耗时. 现在已经有了很多行之有效的 HTTPS 优化手段, 运用得好可以把连接的额外耗时降低到几十毫秒甚至是**零**.

### 硬件加速

HTTPS 连接是计算密集型, 首先, 你可以选择更快的 CPU, 最好还内建 AES 优化, 这样即可以加速握手, 也可以加速传输.

其次, 你可以选择 **SSL 加速卡**, 加解密时调用它的 API, 让专门的硬件来做非对称加解密, 分担 CPU 的计算压力. 不过 **SSL 加速卡**也有一些缺点, 毕竟是固件, 就无法得到实时的升级.

所以, 就出现了第三种硬件加速方式: **SSL 加速服务器**, 用专门的服务器集群来彻底**卸载** TLS 握手时的加密解密计算, 性能自然要比单纯的**加速卡**要强大的多.

### 软件加速

软件升级不必多说, 升级下 Nginx, OpenSSL 的版本, 一般都会有提升. 当然 TLS 协议最好就是升级成 TLS 1.3, 如有历史遗留问题暂时只能用 TLS 1.2, 那么握手时使用的密钥交换协议应当尽量选用椭圆曲线的 ECDHE 算法. 它不仅运算速度快, 安全性高, 还支持 **False Start(在 TLS 协商第二阶段, 浏览器发送 ChangeCipherSpec 和 Finished 后, 立即发送加密的应用层数据, 而无需等待服务器端的确认)**, 能够把握手的消息往返由 2-RTT 减少到 1-RTT, 达到与 TLS 1.3 类似的效果. 另外, 椭圆曲线也要选择高性能的曲线, 最好是 x25519, 次优选择是 P-256. 对称加密算法方面, 也可以选用 **AES_128_GCM**, 它能比 **AES_256_GCM** 略快一点点.

在 Nginx 里可以用 **ssl_ciphers**, **ssl_ecdh_curve** 等指令配置服务器使用的密码套件和椭圆曲线, 把优先使用的放在前面, 例如:

```shell
ssl_ciphers     TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:EECDH+CHACHA20;
ssl_ecdh_curve  X25519:P-256;
```

### 证书优化

除了密钥交换, 握手过程中的证书验证也是一个比较耗时的操作, 服务器需要把自己的证书链全发给客户端, 然后客户端接收后再逐一验证. 这里就有两个优化点, 一个是**证书传输**, 一个是**证书验证**.

首先, 服务器的证书应该选择椭圆曲线(ECDSA)证书而不是 RSA 证书, 因为 224 位的 ECC 相当于 2048 位的 RSA, 这即能够节约带宽也能减少客户端的运算量.

客户端的证书验证其实是个很复杂的操作, 除了要公钥解密验证多个证书签名外, 因为证书还有可能会被撤销失效, 客户端有时还会再去访问 CA, 下载 CRL(Certificate revocation list, 证书吊销列表, 由 CA 定期发布, 里面是所有被撤销信任的证书序号, 查询这个列表就可以知道证书是否有效) 或者 OCSP(在线证书状态协议, Online Certificate Status Protocol), 这又会产生 DNS 查询, 建立连接, 收发数据等一系列网络通信, 增加好几个 RTT.

由于 CRL 因为是**定期**发布, 就有**时间窗口**的安全隐患, 而且随着吊销证书的增多, 列表会越来越大, 一个 CRL 经常会上 MB. 所以每次下载这么一个大的 map 实在性能低下. 因此现在已经不使用 CRL 了, 都使用 OCSP, CA 发送查询请求, 让 CA 返回证书的有效状态. 但 OCSP 也要多出一次网络请求的消耗, 而且还依赖于 CA 服务器, 如果 CA 服务器很忙, 那响应延迟也是等不起的.

于是就有了 **OCSP Stapling**, 它可以让服务器预先访问 CA 获取 OCSP 响应, 然后在握手时随着证书一起发给客户端, 免去了客户端连接 CA 服务器查询的时间, 有种 prefetch 內味了.

### 会话复用与会话票证

TLS 握手的重点是算出主密钥 **Master Secret** , 而主密钥每次连接都要重新计算, 未免有点太浪费了, 因此, 复用主密钥缓存的做法就叫**会话复用**(TLS session resumption), 和 HTTP Cache 一样, 也是提高 HTTPS 性能的**大杀器**, 被浏览器和服务器广泛应用.

会话复用分两种, 第一种叫 **Session ID**, 就是客户端和服务器首次连接后各自保存一个会话的 ID 号, 内存里存储主密钥和其他相关的信息. 当客户端再次连接时发一个 ID 过来, 服务器就在内存里找, 找到就直接用主密钥恢复会话状态, 跳过证书验证和密钥交换, 只用一个消息往返就可以建立安全通信.

**Session ID** 是最早出现的会话复用技术, 也是应用最广的, 但它也有缺点, 服务器必须保存每一个客户端的会话数据, 对于拥有百万, 千万级别用户的网站来说存储量就成了大问题, 加重了服务器的负担. 于是, 又出现了第二种 **Session Ticket** 方案. 它有点类似 HTTP 的 Cookie, 存储的责任由服务器转移到了客户端, 服务器加密会话信息, 用 **New Session Ticket** 消息发给客户端, 让客户端保存. 重连的时候, 客户端使用扩展 **session_ticket** 发送 **Ticket** 而不是 **Session ID**, 服务器解密后验证有效期, 就可以恢复会话, 开始加密通信. 不过 **Session Ticket** 方案需要使用一个固定的密钥文件(ticket_key)来加密 Ticket, 为了防止密钥被破解, 保证**前向安全**, 密钥文件需要定期轮换, 比如设置为一小时或者一天.

### 预共享密钥

预共享密钥原理和 **Session Ticket** 差不多, 但在发送 Ticket 的同时会带上应用数据(Early Data), 免去了 1.2 里的服务器确认步骤, 这种方式叫 **Pre-shared Key**, 简称为 **PSK**. 但 **PSK** 也不是完美的, 它为了追求效率而牺牲了一点安全性, 容易受到**重放攻击**(Replay attack)的威胁. 黑客可以截获 **PSK** 的数据, 像复读机那样反复向服务器发送. 解决的办法是只允许安全的 GET/HEAD 方法, 在消息里加入时间戳, **nonce** 验证, 或者**一次性票证**限制重放. 注意, 在 TLS 1.3 废除了 **Session ID** 和 **Session Ticket** 两种方案, 只保留了 **Pre-shared Key**.

![预共享密钥](https://static.yancey.app/ig3drhos-1649001188389.webp)

## 附录: 配置 HTTPS

以 nginx 为例, 监听 443 端口, 并提供证书的路径.

```shell
listen                443 ssl;

ssl_certificate       xxx_rsa.crt;  # rsa2048 cert
ssl_certificate_key   xxx_rsa.key;  # rsa2048 private key

ssl_certificate       xxx_ecc.crt;  # ecdsa cert
ssl_certificate_key   xxx_ecc.key;  # ecdsa private ke
```

为了提高 HTTPS 的安全系数和性能, 还可以强制 Nginx 只支持 TLS1.2 以上的协议, 打开 **Session Ticket** 会话复用:

```shell
ssl_protocols               TLSv1.2 TLSv1.3;

ssl_session_timeout         5m;
ssl_session_tickets         on;
ssl_session_ticket_key      ticket.key;
```

密码套件的选择方面, 建议是以服务器的套件优先. 这样可以避免恶意客户端故意选择较弱的套件, 降低安全等级, 然后密码套件向 TLS1.3**看齐**, 只使用 ECDHE, AES 和 ChaCha20, 支持 **False Start**. 如果客户端硬件没有 AES 优化, 服务器就会顺着客户端的意思, 优先选择与 AES **等价**的 ChaCha20 算法, 让客户端能够快一点. 可以通过 [SSL Server Test](https://www.ssllabs.com/ssltest/analyze.html) 来测试你的服务器 SSL 的安全性.

```shell
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE+AES128:!MD5:!SHA1;

# 如果你的服务器上使用了 OpenSSL 的分支 BorringSSL,
# 那么还可以使用一个特殊的**等价密码组**(Equal preference cipher groups)特性,
# 它可以让服务器配置一组**等价**的密码套件, 在这些套件里允许客户端优先选择
ssl_ciphers
[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305];
```

### SNI

在 HTTP 协议里, 多个域名可以同时在一个 IP 地址上运行, 这就是**虚拟主机**, Web 服务器会使用请求头里的 Host 字段来选择. 但在 HTTPS 里, 因为请求头只有在 TLS 握手之后才能发送, 在握手时就必须选择**虚拟主机**对应的证书, TLS 无法得知域名的信息, 就只能用 IP 地址来区分. 所以, 最早的时候每个 HTTPS 域名必须使用独立的 IP 地址, 非常不方便.

这需要用到 TLS 的**扩展**, 给协议加个 SNI(Server Name Indication)的**补充条款**. 它的作用和 Host 字段差不多, 客户端会在 **Client Hello** 时带上域名信息, 这样服务器就可以根据名字而不是 IP 地址来选择证书.

### 重定向跳转

通过**重定向跳转**技术, 把不安全的 HTTP 网址用 301 或 302 **重定向**到新的 HTTPS 网站, 这在 Nginx 里也很容易做到, 使用 **return** 或 **rewrite** 都可以.

```shell
return 301 https://$host$request_uri; #永久重定向
rewrite ^ https://$host$request_uri permanent; #永久重定向
```

但这种方式有两个问题. 一个是重定向增加了网络成本, 多出了一次请求；另一个是存在安全隐患, 重定向的响应可能会被**中间人**窜改, 实现**会话劫持**, 跳转到恶意网站.

不过有一种叫**HSTS**(HTTP 严格传输安全, HTTP Strict Transport Security)的技术可以消除这种安全隐患. HTTPS 服务器需要在发出的响应头里添加一个**Strict-Transport-Security**的字段, 再设定一个有效期, 例如:

```shell
add_header Strict-Transport-Security max-age=15768000; #182.5days
```

这相当于告诉浏览器: 我这个网站必须严格使用 HTTPS 协议, 在半年之内(182.5 天)都不允许用 HTTP, 你以后就自己做转换吧, 不要再来麻烦我了. 有了**HSTS**的指示, 以后浏览器再访问同样的域名的时候就会自动把 URI 里的**http**改成**https**, 直接访问安全的 HTTPS 网站. 这样**中间人**就失去了攻击的机会, 而且对于客户端来说也免去了一次跳转, 加快了连接速度.

## 小结

TLS 通过以下几点来保证机密性, 完整性, 身份认证和不可否认.

* 摘要算法用来实现完整性, 能够为数据生成独一无二的**指纹**, 常用的算法是 SHA-2;
* 数字签名是私钥对摘要的加密, 可以由公钥解密后验证, 实现身份认证和不可否认;
* 公钥的分发需要使用数字证书, 必须由 CA 的信任链来验证, 否则就是不可信的;
* 作为信任链的源头 CA 有时也会不可信, 解决办法有 CRL, OCSP, 还有终止信任.

TLS 握手小结:

1. HTTPS 协议会先与服务器执行 TCP 握手, 然后执行 TLS 握手, 才能建立安全连接;
2. 握手的目标是安全地交换对称密钥, 需要三个随机数, 第三个随机数**Pre-Master**必须加密传输, 绝对不能让黑客破解;
3. **Hello**消息交换随机数, **Key Exchange**消息交换**Pre-Master**;
4. **Change Cipher Spec**之前传输的都是明文, 之后都是对称密钥加密的密文.

对 TLS 1.2 已知的攻击有 BEAST, BREACH, CRIME, TREAK, LUCKY13, POODLE, ROBOT.


---

Source: https://yanceyleo.com/post/f976717f-9e40-4849-a73f-86c63c0c45e1
Published: 2022-04-04
Tags: HTTP