大家好我是毛衣哥。上一期我们把 HTTP 扒光了这一期来看看加了锁的 HTTPS 又是怎么被一步步拧开的。准备好瓜子七步拆完。上一期我们把 TLS 1.2 的七步握手拆成了零件。这一期我们看看 TLS 1.3 改了什么——以及这些改动对中间人意味着什么。如果你已经读懂了 TLS 1.2 的七步握手那理解 TLS 1.3 就太容易了。因为 1.3 要做的事情只有一件什么都加密能少跑一趟就少跑一趟。先问一个问题TLS 1.2 有什么不好TLS 1.2 从 2008 年用到 2018 年十年间没出过大问题。但它有几个设计上的历史包袱问题一两轮往返2-RTT太慢了TLS 1.2 握手的完整流程客户端 → 服务器ClientHello 服务器 → 客户端ServerHello Certificate ServerKeyExchange ServerHelloDone 客户端 → 服务器ClientKeyExchange ChangeCipherSpec Finished 服务器 → 客户端ChangeCipherSpec Finished ↑ 到这里才能发第一个 HTTP 请求数一下从客户端发出 ClientHello到服务器发出 ChangeCipherSpec经历了两次往返2-RTT。对于远距离网络比如中国到美国RTT 可能 200ms两次往返就是 400ms 的额外延迟——这还没算上服务器的处理时间。问题二太多遗骸TLS 1.2 支持 37 种密码套件。但实际上常用的就那么三四种。剩下的 30 多种要么是安全强度不够的旧算法RC4、3DES要么是没必要的变体。多一个选项就多一个攻击面。历史上 TLS 的漏洞有相当一部分是由于向后兼容旧版导致的。问题三握手消息大部分是明文TLS 1.2 里从 ClientHello 到 ServerHello 到 Certificate全是明文。中间人可以读到你访问的域名SNI你用的浏览器/操作系统特征通过密码套件列表和 TLS 扩展的排列组合可以识别这叫 TLS 指纹你的证书内容你的服务器配置TLS 1.3 做了什么手术改动一砍密码套件从 37 个砍到 5 个TLS 1.3 的密码套件只有 5 种——准确地说实际生效的只有 3 种TLS_AES_128_GCM_SHA256 0x1301 TLS_AES_256_GCM_SHA384 0x1302 TLS_CHACHA20_POLY1305_SHA256 0x1303名字变短了。因为 TLS 1.3 把密钥交换算法和签名算法从密码套件里拆了出去——密码套件只包含对称加密算法AEADAuthenticated Encryption with Associated Data同时提供机密性和完整性HMAC 算法用于密钥派生密钥交换算法在扩展中协商比如supported_groups扩展里说你支持 x25519、secp256r1 等。为什么砍这么多因为实践证明在 TLS 1.2 的 37 个套件里每年总有那么一两个被发现漏洞然后推出一堆补丁。与其继续打补丁不如直接把不安全的算法砍掉。改动二握手脚程减半从 2-RTT 降到 1-RTTTLS 1.3 的握手简化成 4 个 TCP 包// TLS 1.3 握手的伪代码对比 1.2 的七步 function tls_13_handshake(client_conn, server_conn): // 客户端第一次发包就同时做两件事 client_hello ClientHello { version: 0x0304, // TLS 1.3 random: generate_32_bytes(), cipher_suites: [0x1301, 0x1302, 0x1303], // 只有 3 个 extensions: [ key_share: { // 一步到位DH 公钥夹带了 group: x25519, key_exchange: generate_dh_public() }, supported_versions: [0x0304, 0x0303] // 支持 1.3 和 1.2 ] } send(client_conn, client_hello) // 服务器计算 → 加密 → 回复 server_hello receive(client_conn) // 服务器收到了客户端的 KeyShare已算出握手密钥 handshake_secret derive_handshake_secret( client_hello.key_share, server_hello.key_share ) // Certificate 是加密的不像 1.2 是明文 encrypted_cert receive(client_conn) certificate decrypt(encrypted_cert, handshake_secret) // 客户端完成 finish receive(client_conn) verify(finish, handshake_secret) // 验证握手完整性 // 客户端可以发 HTTP 请求了 http_request GET / HTTP/1.1\r\nHost: example.com\r\n\r\n send_encrypted(client_conn, http_request, traffic_secret) // 总共4 个往返消息 vs TLS 1.2 的 7 个简化后的流程客户端 → 服务器ClientHello KeyShare客户端第一次发包就带上了自己的 DH 公钥 服务器 → 客户端ServerHello KeyShare Certificate Finished都是加密的 客户端 → 服务器Finished 第一条 HTTP 请求可以跟 Finished 一起发 服务器 → 客户端HTTP 响应看到了吗握手和数据传输只隔了一个 RTT。客户端在第三次发包时已经开始发 HTTP 请求了。而在 TLS 1.2 里这个点还在发 Finished 确认密钥。具体变化是TLS 1.3 的 ClientHello 多了 KeyShare 扩展Extension: key_share (type0x0033) Key Share Entry: Group: x25519 Key Exchange Type: x25519 Key Exchange Length: 32 Key Exchange: 32 字节的公钥值——客户端第一次发包就带来了客户端第一次说话时就已经生成了自己的 DH 密钥对把公钥放进了 ClientHello 里。服务器收到后如果它支持这个曲线直接用自己的 DH 私钥 客户端的 DH 公钥计算出握手密钥立刻回复加密后的 ServerHello Certificate。这个跟 TLS 1.2 的本质区别是在 1.2 里ClientKeyExchange 是第五步在 Certificate 之后。服务器必须先发 Certificate 让客户端知道自己的公钥客户端才能用它加密 Pre-Master Secret。但在 1.3 里客户端提前发自己的 KeyShare服务器在发了自己的 KeyShare 后双方立即就能计算出握手密钥然后 Certificate 消息是加密的。改动三更多握手消息被加密了在 TLS 1.2 里ClientHello → 明文ServerHello → 明文Certificate → 明文ServerKeyExchange → 明文ClientKeyExchange → RSA 模式下加密DH 模式下明文Finished → 加密在 TLS 1.3 里ClientHello → 明文但 ECH 正在推进把它也加密ServerHello → 明文Certificate → 加密了用握手密钥加密所有后续消息 → 加密这意味着什么在 TLS 1.2 里中间人不需要解密任何内容就能看到证书。它能看到你用的是 Let’s Encrypt 的证书还是 DigiCert 的证书你的证书什么时候过期。在 TLS 1.3 里Certificate 被加密了。中间人需要持有握手密钥才能看到证书内容。而握手密钥的计算需要双方的 DH 私钥——中间人没有所以看不到。这是一个设计上的大进步1.3 在设计时就把什么时候加密这个问题的答案从握手完成之后提前到了可能被攻击的敏感信息产生时。改动四0-RTT——“第一次包就带数据”如果客户端之前连接过同一个服务器这就更夸张了。TLS 1.3 支持 0-RTTZero Round Trip Time Resumption客户端 → 服务器ClientHello KeyShare 0-RTT Data加密的 HTTP 请求 服务器 → 客户端ServerHello KeyShare Certificate Finished HTTP 响应客户端在第一个 TCP 包里就带了 HTTP 请求。这怎么做到的因为第一次连接时服务器给了客户端一个会话票证Session Ticket里面包含了一个预共享密钥PSK。下次连接时客户端直接用这个 PSK 加密数据在第一个包里就发送。但 0-RTT 有个安全风险——重放攻击。假设客户端用 0-RTT 发了请求POST /transfer HTTP/1.1 Content-Type: application/json {to: alice, amount: 1000}中间人把这个包复制了一份再发一遍。服务器收到两个同样的请求因为它还保有 PSK可以解密这两个请求如果服务端没有做幂等处理——钱被转了两次。所以 0-RTT 只能用于幂等操作GET 查询不能用于 POST 转账。TLS 1.3 的降级保护这是一个很多人没注意到但非常重要的设计。假设客户端支持 TLS 1.3它发了 ClientHello版本 1.3。如果服务器只支持 1.2它回复 ServerHello版本 1.2。如果中间人截获了客户端的 ClientHello把它改成了 1.2假装客户端只支持 1.2那么服务器就会回复 1.2 的握手——双方就被降级了。这叫降级攻击中间人把安全的 1.3 降到了可能含有漏洞的 1.2。TLS 1.3 怎么防这个服务器在处理 ClientHello 时如果在它支持的 1.3 扩展中但客户端说我只用 1.2服务器会在 ServerHello 的随机数末尾写入一个特殊的降级标记TLS 1.2 降级标记44 4F 57 4E 47 52 44 01DOWNGRD 固定字节 TLS 1.1 降级标记44 4F 57 4E 47 52 44 00如果客户端发的是 1.3它实际支持但收到 ServerHello 里包含了降级标记——说明服务器被强制降了级。客户端直接终止连接不跟降级后的服务器握手。但如果客户端真的只支持 1.2它不会检查降级标记——它本来就只支持 1.2。所以这个机制既不误伤老客户端又能防住降级攻击。对中间人的实际影响TLS 1.3 让中间人的日子更难了但没到完全没法过的程度。更难之处Certificate 被加密了中间人没法从握手阶段直接读取证书信息密码套件选择少了攻击面缩小了Forward Secrecy 变成标配RSA 密钥交换被移除降级保护让你没法强行降到 1.2但中间人还是能做这些事看到 ClientHello 的 SNI域名还是明文的除非 ECH 普及看到 IP 地址总能知道你在跟谁通信抓到握手完成后如果拿到了会话密钥装好证书 SSLKEYLOGFILE看到的解密后的内容量是一样的不过——对一般用户来说TLS 1.3 的最大意义不是安全是快。打开一个 HTTPS 网站第一次连接只多花一个 RTT200ms 内之后的连接0-RTT 会话恢复几乎是瞬间完成。你感觉不到加密的代价了。下期预告CA 信任链——一张证书怎么做到让全世界都信它。我们会从根证书一直挖到网站证书看看这 60 年的信任体系到底是怎么运作的。DumpAny 怎么做TLS 1.3 的握手速度比 1.2 快了但对中间人来说反而更复杂了——Certificate 消息是加密的、密码套件少了但更统一了。DumpAny 在实现对 1.3 的支持时最大的改动是密钥派生逻辑因为 1.3 的密钥体系跟 1.2 完全不同。不过对用户来说这些差异是不可见的——你看到的还是解密后的 HTTP 请求。服务器客户端服务器客户端TLS 1.22-RTT↑ 这里才能开始发 HTTPClientHelloServerHello Certificate KeyExchangeClientKeyExchange CCS FinishedCCS Finished服务器客户端服务器客户端TLS 1.31-RTT↑ 一步到位ClientHello KeyShare把DH公钥夹带了ServerHello KeyShare Certificate加密的 FinishedFinished HTTP 请求
03:TLS 1.3 vs 1.2——少了四步,中间人更难了
大家好我是毛衣哥。上一期我们把 HTTP 扒光了这一期来看看加了锁的 HTTPS 又是怎么被一步步拧开的。准备好瓜子七步拆完。上一期我们把 TLS 1.2 的七步握手拆成了零件。这一期我们看看 TLS 1.3 改了什么——以及这些改动对中间人意味着什么。如果你已经读懂了 TLS 1.2 的七步握手那理解 TLS 1.3 就太容易了。因为 1.3 要做的事情只有一件什么都加密能少跑一趟就少跑一趟。先问一个问题TLS 1.2 有什么不好TLS 1.2 从 2008 年用到 2018 年十年间没出过大问题。但它有几个设计上的历史包袱问题一两轮往返2-RTT太慢了TLS 1.2 握手的完整流程客户端 → 服务器ClientHello 服务器 → 客户端ServerHello Certificate ServerKeyExchange ServerHelloDone 客户端 → 服务器ClientKeyExchange ChangeCipherSpec Finished 服务器 → 客户端ChangeCipherSpec Finished ↑ 到这里才能发第一个 HTTP 请求数一下从客户端发出 ClientHello到服务器发出 ChangeCipherSpec经历了两次往返2-RTT。对于远距离网络比如中国到美国RTT 可能 200ms两次往返就是 400ms 的额外延迟——这还没算上服务器的处理时间。问题二太多遗骸TLS 1.2 支持 37 种密码套件。但实际上常用的就那么三四种。剩下的 30 多种要么是安全强度不够的旧算法RC4、3DES要么是没必要的变体。多一个选项就多一个攻击面。历史上 TLS 的漏洞有相当一部分是由于向后兼容旧版导致的。问题三握手消息大部分是明文TLS 1.2 里从 ClientHello 到 ServerHello 到 Certificate全是明文。中间人可以读到你访问的域名SNI你用的浏览器/操作系统特征通过密码套件列表和 TLS 扩展的排列组合可以识别这叫 TLS 指纹你的证书内容你的服务器配置TLS 1.3 做了什么手术改动一砍密码套件从 37 个砍到 5 个TLS 1.3 的密码套件只有 5 种——准确地说实际生效的只有 3 种TLS_AES_128_GCM_SHA256 0x1301 TLS_AES_256_GCM_SHA384 0x1302 TLS_CHACHA20_POLY1305_SHA256 0x1303名字变短了。因为 TLS 1.3 把密钥交换算法和签名算法从密码套件里拆了出去——密码套件只包含对称加密算法AEADAuthenticated Encryption with Associated Data同时提供机密性和完整性HMAC 算法用于密钥派生密钥交换算法在扩展中协商比如supported_groups扩展里说你支持 x25519、secp256r1 等。为什么砍这么多因为实践证明在 TLS 1.2 的 37 个套件里每年总有那么一两个被发现漏洞然后推出一堆补丁。与其继续打补丁不如直接把不安全的算法砍掉。改动二握手脚程减半从 2-RTT 降到 1-RTTTLS 1.3 的握手简化成 4 个 TCP 包// TLS 1.3 握手的伪代码对比 1.2 的七步 function tls_13_handshake(client_conn, server_conn): // 客户端第一次发包就同时做两件事 client_hello ClientHello { version: 0x0304, // TLS 1.3 random: generate_32_bytes(), cipher_suites: [0x1301, 0x1302, 0x1303], // 只有 3 个 extensions: [ key_share: { // 一步到位DH 公钥夹带了 group: x25519, key_exchange: generate_dh_public() }, supported_versions: [0x0304, 0x0303] // 支持 1.3 和 1.2 ] } send(client_conn, client_hello) // 服务器计算 → 加密 → 回复 server_hello receive(client_conn) // 服务器收到了客户端的 KeyShare已算出握手密钥 handshake_secret derive_handshake_secret( client_hello.key_share, server_hello.key_share ) // Certificate 是加密的不像 1.2 是明文 encrypted_cert receive(client_conn) certificate decrypt(encrypted_cert, handshake_secret) // 客户端完成 finish receive(client_conn) verify(finish, handshake_secret) // 验证握手完整性 // 客户端可以发 HTTP 请求了 http_request GET / HTTP/1.1\r\nHost: example.com\r\n\r\n send_encrypted(client_conn, http_request, traffic_secret) // 总共4 个往返消息 vs TLS 1.2 的 7 个简化后的流程客户端 → 服务器ClientHello KeyShare客户端第一次发包就带上了自己的 DH 公钥 服务器 → 客户端ServerHello KeyShare Certificate Finished都是加密的 客户端 → 服务器Finished 第一条 HTTP 请求可以跟 Finished 一起发 服务器 → 客户端HTTP 响应看到了吗握手和数据传输只隔了一个 RTT。客户端在第三次发包时已经开始发 HTTP 请求了。而在 TLS 1.2 里这个点还在发 Finished 确认密钥。具体变化是TLS 1.3 的 ClientHello 多了 KeyShare 扩展Extension: key_share (type0x0033) Key Share Entry: Group: x25519 Key Exchange Type: x25519 Key Exchange Length: 32 Key Exchange: 32 字节的公钥值——客户端第一次发包就带来了客户端第一次说话时就已经生成了自己的 DH 密钥对把公钥放进了 ClientHello 里。服务器收到后如果它支持这个曲线直接用自己的 DH 私钥 客户端的 DH 公钥计算出握手密钥立刻回复加密后的 ServerHello Certificate。这个跟 TLS 1.2 的本质区别是在 1.2 里ClientKeyExchange 是第五步在 Certificate 之后。服务器必须先发 Certificate 让客户端知道自己的公钥客户端才能用它加密 Pre-Master Secret。但在 1.3 里客户端提前发自己的 KeyShare服务器在发了自己的 KeyShare 后双方立即就能计算出握手密钥然后 Certificate 消息是加密的。改动三更多握手消息被加密了在 TLS 1.2 里ClientHello → 明文ServerHello → 明文Certificate → 明文ServerKeyExchange → 明文ClientKeyExchange → RSA 模式下加密DH 模式下明文Finished → 加密在 TLS 1.3 里ClientHello → 明文但 ECH 正在推进把它也加密ServerHello → 明文Certificate → 加密了用握手密钥加密所有后续消息 → 加密这意味着什么在 TLS 1.2 里中间人不需要解密任何内容就能看到证书。它能看到你用的是 Let’s Encrypt 的证书还是 DigiCert 的证书你的证书什么时候过期。在 TLS 1.3 里Certificate 被加密了。中间人需要持有握手密钥才能看到证书内容。而握手密钥的计算需要双方的 DH 私钥——中间人没有所以看不到。这是一个设计上的大进步1.3 在设计时就把什么时候加密这个问题的答案从握手完成之后提前到了可能被攻击的敏感信息产生时。改动四0-RTT——“第一次包就带数据”如果客户端之前连接过同一个服务器这就更夸张了。TLS 1.3 支持 0-RTTZero Round Trip Time Resumption客户端 → 服务器ClientHello KeyShare 0-RTT Data加密的 HTTP 请求 服务器 → 客户端ServerHello KeyShare Certificate Finished HTTP 响应客户端在第一个 TCP 包里就带了 HTTP 请求。这怎么做到的因为第一次连接时服务器给了客户端一个会话票证Session Ticket里面包含了一个预共享密钥PSK。下次连接时客户端直接用这个 PSK 加密数据在第一个包里就发送。但 0-RTT 有个安全风险——重放攻击。假设客户端用 0-RTT 发了请求POST /transfer HTTP/1.1 Content-Type: application/json {to: alice, amount: 1000}中间人把这个包复制了一份再发一遍。服务器收到两个同样的请求因为它还保有 PSK可以解密这两个请求如果服务端没有做幂等处理——钱被转了两次。所以 0-RTT 只能用于幂等操作GET 查询不能用于 POST 转账。TLS 1.3 的降级保护这是一个很多人没注意到但非常重要的设计。假设客户端支持 TLS 1.3它发了 ClientHello版本 1.3。如果服务器只支持 1.2它回复 ServerHello版本 1.2。如果中间人截获了客户端的 ClientHello把它改成了 1.2假装客户端只支持 1.2那么服务器就会回复 1.2 的握手——双方就被降级了。这叫降级攻击中间人把安全的 1.3 降到了可能含有漏洞的 1.2。TLS 1.3 怎么防这个服务器在处理 ClientHello 时如果在它支持的 1.3 扩展中但客户端说我只用 1.2服务器会在 ServerHello 的随机数末尾写入一个特殊的降级标记TLS 1.2 降级标记44 4F 57 4E 47 52 44 01DOWNGRD 固定字节 TLS 1.1 降级标记44 4F 57 4E 47 52 44 00如果客户端发的是 1.3它实际支持但收到 ServerHello 里包含了降级标记——说明服务器被强制降了级。客户端直接终止连接不跟降级后的服务器握手。但如果客户端真的只支持 1.2它不会检查降级标记——它本来就只支持 1.2。所以这个机制既不误伤老客户端又能防住降级攻击。对中间人的实际影响TLS 1.3 让中间人的日子更难了但没到完全没法过的程度。更难之处Certificate 被加密了中间人没法从握手阶段直接读取证书信息密码套件选择少了攻击面缩小了Forward Secrecy 变成标配RSA 密钥交换被移除降级保护让你没法强行降到 1.2但中间人还是能做这些事看到 ClientHello 的 SNI域名还是明文的除非 ECH 普及看到 IP 地址总能知道你在跟谁通信抓到握手完成后如果拿到了会话密钥装好证书 SSLKEYLOGFILE看到的解密后的内容量是一样的不过——对一般用户来说TLS 1.3 的最大意义不是安全是快。打开一个 HTTPS 网站第一次连接只多花一个 RTT200ms 内之后的连接0-RTT 会话恢复几乎是瞬间完成。你感觉不到加密的代价了。下期预告CA 信任链——一张证书怎么做到让全世界都信它。我们会从根证书一直挖到网站证书看看这 60 年的信任体系到底是怎么运作的。DumpAny 怎么做TLS 1.3 的握手速度比 1.2 快了但对中间人来说反而更复杂了——Certificate 消息是加密的、密码套件少了但更统一了。DumpAny 在实现对 1.3 的支持时最大的改动是密钥派生逻辑因为 1.3 的密钥体系跟 1.2 完全不同。不过对用户来说这些差异是不可见的——你看到的还是解密后的 HTTP 请求。服务器客户端服务器客户端TLS 1.22-RTT↑ 这里才能开始发 HTTPClientHelloServerHello Certificate KeyExchangeClientKeyExchange CCS FinishedCCS Finished服务器客户端服务器客户端TLS 1.31-RTT↑ 一步到位ClientHello KeyShare把DH公钥夹带了ServerHello KeyShare Certificate加密的 FinishedFinished HTTP 请求