透视 HTTP 协议(三)

HTTPS 和 SSL/TLS

HTTP 协议有两个缺点:明文不安全,仅凭 HTTP 自身是无力解决的,需要引入新的 HTTPS 协议。

HTTPS

HTTPS 其实是一个 “非常简单” 的协议,除了在 RFC 文档中规定了 新的协议名 HTTPS,默认端口号 443,其他的部分都完全沿用 HTTP,没有任何新的东西。

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

SSL/TLS

SSL 即 安全套接层(Secure Sockets Layer),在 OSI 模型中处于第 5 层(会话层),由网景公司于 1994 年发明,有 v2 和 v3 两个版本,而 v1 因为有严重的缺陷从未公开过。

SSL 发展到 v3 时已经证明了它自身是一个非常好的安全通信协议,于是互联网工程组 IETF 在 1999 年把它正式标准化,改名为 TLS,即 传输层安全(Transport Layer Security),版本号从 1.0 重新算起,所以 TLSv1.0 实际上就是 SSLv3.1。

到今天 TLS 已经发展出了三个版本,分别是 2006 年的 v1.1、2008 年的 v1.2 和 2018 年的 v1.3,每个新版本都紧跟密码学的发展和互联网的现状,持续强化安全和性能,已经成为了信息安全领域中的权威标准,目前应用的最广泛的 TLSv1.2

TLS 由记录协议(Record Protocol)、警告协议(Alert Protocol)、握手协议(Handshake Protocol)、变更密码规范协议(Change Cipher Spec Protocol)、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。

浏览器和服务器在使用 TLS 建立连接时需要选择一组恰当的加密算法来实现安全通信,这些算法的组合被称为 密码套件(cipher suite)。TLSv1.2 的密码套件命名格式为:密钥交换算法 - 签名算法 - 对称加密算法 - 摘要算法,比如:ECDHE-RSA-AES256-GCM-SHA384

除了 HTTP => HTTPS,SSL/TLS 也可以承载其他应用协议,例如 FTP => FTPS,LDAP => LDAPS。

OpenSSL

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

由于 OpenSSL 是开源的,所以它还有一些代码分支,比如 Google 的 BoringSSL、OpenBSD 的 LibreSSL,这些分支在 OpenSSL 的基础上删除了一些老旧代码,也增加了一些新特性,虽然背后有 “大金主”,但离取代 OpenSSL 还差得很远。

OpenSSL 是从另一个开源库 SSLeay 发展出来的,曾经考虑命名为 OpenTLS,但当时(1998 年)TLS 还未正式确立,而 SSL 早已广为人知,所以最终使用了 OpenSSL 的名字。

TLSv1.2 连接过程

在 HTTP 协议中,通过三次握手与网站建立 TCP 连接后,浏览器会立即发送请求报文。但使用 HTTPS 协议,会先与服务器执行 TCP 握手,然后 再执行 TLS 握手建立安全连接,之后才是数据通信。这个握手过程与 TCP 有些类似,是 HTTPS 和 TLS 协议里最重要、最核心的部分。

下面的这张图简要地描述了 TLS 的握手过程,其中每一个 “框” 都是一个记录,多个记录组合成一个 TCP 包发送。所以,需要经过 2 个 RTT 完成握手,然后就可以在安全的通信环境里发送 HTTP 报文,实现 HTTPS 协议。

ECDHE 握手过程

目前主流的 TLS 握手过程使用的是 ECDHE 密钥交换算法,流程如下:

使用 ECDHE 实现密钥交换有两个特点,一是服务器端会发出 Server Key Exchange 消息,二是客户端可以不用等到服务器返回 Finished 确认握手完毕,立即就发出 HTTP 报文,即 TLS False Start

在上述过程中,Change Cipher Spec 之前传输的都是明文,之后都是用对称密钥加密的密文,握手的目的就是安全交换通信过程中使用的对称密钥(会话密钥)。

RSA 握手过程

传统的 RSA 密钥交换中没有 Server Key Exchange 和 False Start,流程如下:

大体的流程没有变,只是 Pre-Master 不再需要用算法生成,而是客户端直接生成随机数,然后用服务器的公钥加密,通过 Client Key Exchange 消息发给服务器。服务器再用私钥解密,这样双方也实现了共享三个随机数,就可以生成主密钥。

TLSv1.2 协议细节

  • 在 TLSv1.2 中,客户端和服务端的随机数长度都是 28 字节,前面的 4 个字节是 unix 时间戳。
  • TLS 协议中原本定义有压缩方式,但后来发现存在安全漏洞(CRIME 攻击),所以现在这个字段总是 NULL,即不压缩。
  • 对 TLSv1.2 已知的攻击有:BEAST、BREACH、CRIME、FREAK、LUCKY13、POODLE、ROBOT 等。

TLSv1.3 特性解析

虽然 TLSv1.2 久经考验,但已经是 10 多年前的协议了,在安全、性能等方面已经跟不上如今的互联网了。于是经过四年、近 30 个草案的反复打磨,TLSv1.3 终于在 2018 年正式登场,再次确立了信息安全领域的新标准。

TLSv1.3 的主要改进目标有三个:兼容安全性能

最大化兼容性

由于 v1.1、v1.2 等协议已经出现了很多年,很多应用软件、中间代理只认老的记录协议格式,更新改造很困难,甚至是不可行。

在早期的试验中发现,一旦变更了记录头字段里的版本号,也就是由 0x303(TLS1.2)改为 0x304(TLS1.3)的话,大量的代理服务器、网关都无法正确处理,最终导致 TLS 握手失败。

为了保证这些被广泛部署的老设备能够继续使用,避免新协议带来的冲击,TLSv1.3 不得不做出妥协,保持现有的记录格式不变,通过伪装来实现兼容,使得 TLSv1.3 看上去像是 TLSv1.2。

TLSv1.3 通过 扩展字段 来增加新的功能。在记录头的 Version 字段被兼容性固定的情况下,TLS1.3 协议会在握手的 “Hello“ 消息后面添加 supported_versions 扩展,其中标记了 TLS 的版本号,使用它就能区分新旧协议。

强化安全

TLSv1.2 在十来年的应用中获得了许多宝贵的经验,发现了很多的漏洞和加密算法的弱点,所以 TLSv1.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 种。

算法精简后的好处是:原来众多的算法、参数组合导致密码套件非常复杂,难以选择,而现在的 TLS1.3 里只有下面 5 个套件:

TLSv1.3 里的密码套件没有指定密钥交换算法和签名算法,可以通过扩展字段判断。

这里还要特别说一下为什么废除了 RSA 和 DH 密钥交换算法,因为它不具备 前向安全(Forward Secrecy)

假设有一个很有耐心的黑客,一直在长期收集混合加密系统收发的所有报文。如果加密系统使用服务器证书里的 RSA 做密钥交换,一旦私钥泄露或被破解(使用社会工程学或者超级计算机),那么黑客就能够使用私钥解密出之前所有报文的 Pre-Master,再算出会话密钥,破解所有密文。这就是所谓的 “今日截获,明日破解”

而 ECDHE 算法在每次握手时都会生成一对临时的公钥和私钥,每次通信的密钥对都是不同的,也就是 一次一密,即使黑客花大力气破解了这一次的会话密钥,也只是这次通信被攻击,之前的历史消息不会受到影响,仍然是安全的。

ECDHE(Elliptic Curve Diffie-Hellman Ephemeral),即 短暂-椭圆曲线-迪菲-赫尔曼算法,使用椭圆曲线增强了 DH 算法的安全性和性能,公钥和私钥都是临时生成的。

提升性能

HTTPS 建立连接时不仅要做 TCP 握手,还要做 TLS 握手,在 v1.2 中会多花 2-RTT,可能导致几十毫秒甚至上百毫秒的延迟,在移动网络中延迟还会更严重。

现在因为密码套件大幅度简化,也就没有必要再像以前那样走复杂的协商流程了。TLS1.3 压缩了以前的 “Hello” 协商过程,删除了 Key Exchange 消息,把握手时间减少到了 1-RTT,效率提高了一倍。

在具体的实现上还是利用了扩展。客户端在 “Client Hello” 消息里直接用 supported_groups 带上支持的曲线,用 key_share 带上曲线对应的客户端公钥参数,用 signature_algorithms 带上签名算法。服务器收到消息后,在这些扩展里选定一个曲线和参数,再用 key_share 扩展返回服务器这边的公钥参数,就实现了双方的密钥交换,后面的流程就和 v1.2 基本一样了。

除了标准的 1-RTT 握手,TLS1.3 还引入了 0-RTT 握手,用 pre_shared_keyearly_data 扩展,在 TCP 连接后立即建立安全连接发送加密消息,这种方法被称为 预共享密钥(Pre-shared Key,PSK)

但 PSK 也不是完美的,它为了追求效率而牺牲了一点安全性,容易受到重放攻击。解决方式是在消息里加入时间戳、nonce 验证等。

握手过程

SNI 和 HSTS

服务器名称指示

在使用 HTTPS 服务前还有一个 虚拟主机 的问题需要解决。

由于在 HTTP 协议里,多个域名可以同时在一个 IP 地址上运行(即虚拟主机),Web 服务器会使用请求头里的 Host 字段来选择。

但在 HTTPS 里,因为请求头只有在 TLS 握手之后才能发送,在握手时就必须选择 虚拟主机对应的证书,此时 TLS 无法得知域名的信息,只能用 IP 地址来区分。所以,最早的时候每个 HTTPS 域名必须使用独立的 IP 地址,非常不方便。

这个问题后来通过 server_name 扩展得以解决,支持在 TLS 协议中添加 SNI(Server Name Indication,服务器名称指示)记录。SNI 的作用和 Host 字段差不多,允许客户端在 “Client Hello” 时带上域名信息,这样服务器就可以根据名字而不是 IP 地址来选择证书。

1
2
3
4
Extension: server_name (len=20)
Server Name Indication extension
Server Name Type: host_name (0)
Server Name: www.b.com

Nginx 很早就基于 SNI 特性支持了 HTTPS 的虚拟主机,在 OpenResty 里可还以编写 Lua 脚本,利用 Redis、MySQL 等数据库更快更灵活地加载证书。

重定向跳转

当 Web 站点迁移到 HTTPS 服务,原来的 HTTP 站点也不能马上弃用,因为还是会有很多人习惯在地址栏里直接输入域名,或者是旧的书签、超链接,默认使用 HTTP 协议访问。

解决方式很简单,使用 “重定向跳转” 技术把不安全的 HTTP 网址用 301 或 302 重定向到新的 HTTPS 网站就可以了,这在 Nginx 里也很容易做到:

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

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

使用 HSTS(HTTP Strict Transport Security,HTTP 严格传输安全)可以消除这种安全隐患。HTTPS 服务器需要在响应头中添加一个 Strict-Transport-Security 字段,再设定一个有效期,例如:Strict-Transport-Security: max-age=15768000; includeSubDomains。这相当于告诉浏览器:我这个网站必须严格使用 HTTPS 协议,在半年之内(182.5 天)都不允许用 HTTP。

有了 HSTS 的指示,以后浏览器再访问同样的域名时就会自动把 URI 里的 http:// 改成 https://,直接访问安全的 HTTPS 网站。这样避免了中间人攻击,而且对于客户端来说也免去了一次跳转,加快了连接速度。

HSTS 无法防止黑客对第一次访问的攻击,所以 chrome 等浏览器还内置了一个 HSTS preload 的列表(chrome://net-internals/#hsts),只要域名在这个列表里,无论何时都会强制使用 HSTS 访问。