本文转载至https://flyhigher.top/develop/1093.html

这是一种能保证你花掉工资/零花钱时的愉悦感的技术

从咕果谈起

就在不久之前,咕果宣布 Chrome 将在不久之后提前移除对于 HTTPS 网站的“安全”标识,同时对于 HTTP 网站强制显示“不安全”。虽然咕果表示目前 HTTPS 的普及已经达到他们认为足够的地步了,但是我看着国内的进展…啧。

为什么要使用 HTTPSHTTPS 提供的不只是加密功能。事实上,许多 Web 新技术,如 HTTP/2Service Worker 等都需要依托 HTTPS 才能实现,相信这也会成为未来的趋势。

什么是 SSL?

从网络模型来看,SSL 在 HTTP 应用层之下,TCP 传输层之上提供了一层传输安全层,确保了经过传输层的数据都是经过加密的。

SSL 作为 HTTPS 的基础最早由网景于 1994 年提出。虽然 HTTP 协议本身提供了基本认证和摘要认证(后来提出)两种客户端认证方案,但是毕竟除了用户名和密码外的所有数据仍然是明文传输的(甚至基本认证只对密码信息做了 Base64 编码),要保证传输的安全性还得依靠 SSL。SSL 本身是一种二进制协议,避免了随随便便一个代理就能读取/修改数据的问题。

随着技术发展,SSL 本身也在不断改进。从 SSL 1.0、SSL 2.0 到 SSL 3.0,再到标准化后的 TLS(目前 TLS 1.3 刚刚定稿),这个协议本身也在完善当中。在本文中,我们按照一般惯例使用 SSL 一词同时指代 SSL 和 TLS 两种协议。

翻开历史书

加密方法离不开数学基础,为了更好地解释加密原理,我们先来了解一下加密的基础知识。当然,如果已经了解了就直接跳过吧。

人类使用加密已经有几千年的历史了。最初,人们使用特定的方法处理文本使其在被截获之后也不会泄露信息。我们通常把这种特定的方法叫做密码。传说凯撒曾使用过一种三位位移密码,也就是将文本中的所有字母替换为这个字母在字母表中后三位的字母。这种方法是一种密码。也就是说,密码 C 接受明文 R 一个参数,输出密文 E。

$$E=C(R)$$


然而,使用单一密码的加密非常容易被破译,而且一旦被破译,以往和未来的所有密文也不再安全。为了解决这个问题,人们提出了新的方法也就是密钥。用同一种密码加密,输入不同的密钥就会产生不同的结果。比如上文中的三位位移密码,如果把 3 当做密钥,那么不同的密钥(1,2,3,4,5…)就会产生不同的输出。这种时候,就算密码被破译,只要密钥的可能值足够多,要得到明文还是相当困难的。也就是,密码 C 现在接受明文 R 和密钥 K 两个参数,产生密文 E。不同的 R 和 K 组合会产生不同的 E。

$$E=C(R,K)$$

然而这种方法仍然有问题。密文的接收方要解密得到明文,必须要得知密码和密钥两个信息。通常密码和密钥组合是变化的(不然就和没有密钥一样了),要正确解密,发送方和接收方每次都必须要交换密码和密钥信息,通常是只交换密钥而密码不变。但密钥存在被截获的可能,要确保安全必须对密钥进行加密,然后加密密钥也需要交换密钥,然后第二个密钥也需要加密…很快这就变成了一个鸡生蛋问题,很明显是不可能实现的。


到目前为止,我们讨论的都是对称加密,也就是加密和解密用的是相同的密码和密钥。对于无限加密的问题,使用对称加密是无解了。此时,我们的救星非对称加密就登场了。

非对称加密可以使用不同的密钥来分别加密和解密文本,它们通常是配对的,我们叫它们公钥和私钥。使用相同的密码时,接受方只需生成一对公钥和私钥,然后保管好自己的私钥并公开公钥。发送方只需要用商定的密码和接收方的公钥作为密钥来加密文本即可。加密后的文本只有使用私钥才能解开,公钥本身是无法解开的。也就是说,密码 C 对于明文 R 和公钥 PK 产生的输出只能由与密码 C 对应的解码 D 和公钥 PK 对应的私钥 SK 才能解密回明文。

$$D(C(R,PK),SK)=R$$

这里就不详述数学上的证明了,有兴趣的可以自行了解。

通过这种方式,接收方可以保证密文只能由接受方进行解码,交换公钥并不会破坏加密的安全性。更棒的是,私钥和公钥是可以互换的,即是,使用公钥 PK 加密并使用私钥 SK 解密得到的结果和使用私钥 SK 加密并使用公钥 PK 解密得到的结果是一致的,都是原始明文

$$D(C(R,PK),SK)=D(C(R,SK),PK)=R$$

这样的话,我们甚至可以使用非对称加密验证接收方的身份。毕竟只有接收方拥有私钥,可以要求接收方用私钥加密明文并试图用其公钥解密来确保对方就是拥有正确私钥的那一方。如此看来,有了非对称加密,似乎一切都完美了。

等等!

你可能会发现一些问题。如果有一个聪明的中间人(或者说网络中的某一台代理),他可能可以轻松破解非对称加密

对的!试想以下的场景: A 和 B 作为两位优秀的魔法少女常常需要互通信件,然而不巧邪恶的 E 同学总是试图偷看 A 和 B 的对话,于是 A 和 B 决定使用非对称加密来加密信件以解决问题。

然而 E 同学想出了一个办法。A 向 B 发送信件时,B 会先向 A 发送她的公钥以便 A 加密文本,这时 E 将其截获,记下 B 的公钥然后自己生成一套公钥和私钥并将自己的公钥伪装成 B 的信件重新发送给 A。不知情的 A 会用 E 的公钥加密文本并发回给 B。E 可以再次截获信件,此时他利用自己的私钥便可以轻松读取信中的内容了。然后 E 将信件重新使用 B 的公钥加密并发送给 B,这样 B 也不会发现异常,认为自己成功与 A 完成了一次沟通,殊不知 E 已经读取到了信的内容。当 B 要向 A 寄信时,E 可以如法炮制读取 A 和 B 的对话。

怎么办才能避免 E 读取内容呢?幸好,我们有一种方法可以在 E 偷偷摸摸更换密钥时就发现问题并及时停止。要使用这种方法,我们需要有请我们的下一位选手登场。

欢迎,公正的第三方

现在,作为第三方的 C 登场了。他会作为一个验证人帮助信件的接收方验证收到的信件有没有经过篡改以及是不是真的由 B 发出的。

整个过程这样进行。C 先生成一对公私钥,然后将公钥亲自告诉 A 和 B。A 发信给 B 时,C 会提前把 B 的公钥和一些 B 和 C 的基本信息放在一起,然后用自己的私钥加密这段信息并将密文交给 B。B 随后只需将这段密文交给 A 即可。A 随后会试着使用 C 的公钥解开密文,如果成功了,那就说明信件没有经过篡改,并且 A 也安全地获得了 B 的公钥。如果失败了,就说明有人篡改了这段密文,信件即作废。当 B 给 A 寄信时将整个过程反过来即可。看,这样我们就可以安全的交换密钥而不用担心内容被窃听了。可怜的 E 现在束手无策。

说好的 HTTPS 去哪了?

别激动,其实 A、B、C、E 的故事就是一次典型的 HTTPS 连接。

慢慢来,实际情况会比上面的故事稍稍复杂一些。我们把 A 看作客户端,通常情况下也就是浏览器,B 看作服务器,E 看作网络链路上某台邪恶的代理,C 是一位公正的第三方,顺便,我们把 C 交给 B 的那段含有一些基本信息和 B 的公钥的密文叫做数字证书。现在,我们可以把 C 叫做证书颁发机构。

在真实情况下,数字证书会包含一些加密的基本信息。毕竟数字证书并没有统一的标准,这些加密信息会用来告诉 A 如何解密数字证书。当然这些信息不会被加密,不然 A 完全无法解密数字证书。因此现实情况下,C 会对数字证书包含的所有内容建立一个摘要(你可以把它当做简介),然后只对这个摘要用自己的私钥进行加密。这个过程叫做签名。当 A 收到证书后,会解密这段摘要,然后自己通过相同的算法独立算出证书的摘要——如果是一样的,那就没问题了。


那么现在开始连接。A 向 B 发出了一个请求。在建立连接后,B 会将数字证书发送给 A。A 会验证数字证书(甚至会询问 C 确保这张证书没有作废)。如果成功,A 会利用从证书中取得的 B 的公钥在加密环境下协商出一个一致的临时密钥,接下来双方会用这个密钥进行对称加密来互相交流。

等等!在这种情况下,B 没有 A 的公钥,他是怎么回答 A 来完成临时密钥的协商的呢?啊哈!实际上 B 并不需要回复 A 来完成协商。在整个过程中,只需要 A 向 B 发送一条加密信息,双方即可计算出相同的临时密钥。至于技术细节,我们稍后就谈。

那为什么需要临时密钥呢?那是因为非对称加密需要的计算量远大于对称加密。只需要保证密钥不泄露,对称加密是安全的。利用非对称加密来交换对称加密密钥完美解决了密钥交换问题,两种加密方法的混合使用也保证了安全和性能的平衡。

这就完成了!现在 A 和 B 可以使用相同的密钥进行对称加密通信在这个基础上,A 和 B 可以使用标准的 HTTP 协议进行交流。同时由于没能截获密钥,可怜的 E 同学再次扑了个空。我们成功完成了一次加密的 HTTPS 通信。

接下来就是技术细节!

在加密建立的过程中,算法不同会导致实际实现的不同——尽管并没有太大差别。

简单来看,我们可以将其分为 RSA 算法和 DH 算法。两种算法建立加密的过程我们都会介绍。


开始时两种加密算法的实现是一致的。浏览器准备发出一个请求到一台服务器。首先浏览器打开了到该服务器 443 端口的 TCP 连接,准备开始 SSL 握手。

随后,浏览器生成一串随机数并附上自己支持的协议版本和加密方法等一并发送给服务器。协议版本即指 SSL/TLS 协议版本,而加密方式是指实现加密使用的具体算法,也就是密码。这个过程被称为 ClientHello

接着,服务器会发回确定下来的协议版本和加密方式,以及一串由服务器生成的随机数及数字证书。此时,如果双方没有共同支持的协议版本或加密方式,连接就会断开,浏览器显示错误信息。这个过程被称为 SeverHello

浏览器会检查服务器证书。数字证书目前没有一个统一的标准,不过使用最广泛的标准是 X.509,版本 3。浏览器会解析证书,检查其签发者是否是可信的,并会试图使用该签发者的公钥来检查证书是否经过篡改。浏览器还会检查证书是否到期、证书包含的主机名和当前主机名是否匹配,甚至会向证书颁发机构的在线服务询问证书是否被注销。一旦有任何一条检查出现问题,浏览器就会中断连接并显示错误信息。

浏览器是如何检查证书颁发机构是否可信的?基本上,在每一台联网设备里,都有一套可信机构列表。这些机构和它们的公钥被直接存储于每一台计算机中,因此不用担心被篡改(还记得上面的故事里 C 是亲自告诉 A 和 B 他的公钥的么?)。当然,用户可以添加自己的证书来让浏览器信任自己签发的证书,但这仅限于单台计算机。通常,这些在列表中的机构还会为其他机构签发证书。此时,这些其他机构签发的证书也会被系统所信任,这便构成了证书链。

通常情况下,浏览器还会检查一些其他信息,如证书透明度信息等,就不再概述了。


不同算法的不同实现在这里开始出现分歧。我们先来介绍 RSA 算法的实现。

在这时,浏览器产生整个过程中的最后一个随机数,并将它使用服务器的公钥加密,发送给服务器,即 Client Key Exchange

这时,双方会通过已有的 3 个随机数各自计算出临时密钥。如果没有差错,双方的计算结果应该是相同的。随后,双方会互发 Change Cipher Spec 消息确认接下来的数据传输已经可以切换到加密方式。

为什么需要随机数?因为数字证书本身是静态的,要保障安全性,我们需要每次连接时都有不同的密钥,这个密钥便是由双方提出的随机数计算而得的,这种方式增强了连接的安全性。


对于 DH 算法,此时服务器会利用私钥将浏览器生成的随机数、服务器随机数和服务器 DH 参数签名,生成服务器签名,并随后发送服务器 DH 参数和服务器签名,这是 Server Key Exchange

紧接着浏览器也会发送客户端 DH 参数(Client Key Exchange),随后双方即可独立计算出临时密钥,切换至加密协议(Change Cipher Spec)。


最后,两种加密算法的实现又变得一致。双方会互换这一轮握手过程中发送/接受到的信息的散列值以确保在握手过程中没有数据被恶意替换,这是 Finished

通过抓包可以看出两种实现的不同。(图源网络)
RSA:

DH:

到这里,浏览器和服务器已经成功建立了安全的 SSL 连接。接下来,标准的 HTTP 协议就可以由 SSL 传输安全层负载进行传输了。从 TCP 传输层来看,这些数据是原始的二进制数据,即使有人截获也无法将其解密。

还有一些小问题…

通常情况下,你要访问一个网站,浏览器会在发给服务器的请求中附带一个 host 字段来告诉服务器要访问哪个网站。这对于一个服务器上托管了多个网站的情况(很常见)很有用,服务器可以立刻明白你要访问哪个网站并把对应的数据发回浏览器。

但是,在访问使用 HTTPS 的网站时,SSL 握手的过程中浏览器并不会告诉服务器要访问哪个网站(这是在 HTTP 协议下发出的,而 HTTP 通信会在 SSL 连接建立之后才开始)。这会导致服务器不知道你需要哪张证书且往往会将错误的证书发送给你。这通常会导致证书验证失败。

为了解决这一问题,人们提出了 SNI,一个 TLS 拓展协议。SNI 会在 SSL 握手时就向服务器发送要访问的主机名,以便服务器发回正确的证书。目前绝大部分服务器软件和客户端都支持这一拓展协议。


在 SSL 加密开始后,在 TCP 传输层上传输的就是完完全全的二进制数据。对于代理来说,这不是什么好事。对于这样的数据,它们完全不知道应该向哪里转发。

当然,盲转发代理是不可取的。盲转发是指对数据包不做处理直接转发的行为。这会彻底地破坏 HTTP/1.1 中引入的长连接,在拖慢速度的同时还大大加重的服务器的负担。因此通常情况下代理会解析 HTTP 头以获得一些连接的基本信息。

一种解决方案是使用 HTTPS SSL 隧道协议,这通常是使用 HTTP CONNECT 方法实现的,具体细节就不再详述了。除此之外也有很多方法,有兴趣可以自行了解。