02:TLS 1.2 七步定乾坤——加密通道到底是怎么拧上的

📅 2026/8/10 12:05:39
02:TLS 1.2 七步定乾坤——加密通道到底是怎么拧上的
大家好,我是毛衣哥。上一期我们把 HTTP 扒光了,这一期来看看加了锁的 HTTPS 又是怎么被一步步拧开的。准备好瓜子,七步拆完。上一篇我们把 HTTP 裸奔的惨状全抖出来了。有人问:那 HTTPS 一加密,中间人是不是就彻底没辙了?答案是:要看它卡在哪一步。如果你不理解 TLS 握手每一步的精确内容,你就永远不会知道中间人到底在哪个环节做了手脚。这篇可能会有点长,但我保证这是你看过最详细的 TLS 握手拆解。TLS 的目标就两个,记死了所有加密通信要解决的根本问题永远只有两个:第一:对方是它自称的那个人吗?(身份认证)第二:怎么商量一把只有我俩知道的钥匙?(密钥协商)第一个问题靠CA 证书。第二个问题靠非对称加密。TLS 握手就是同时完成这两件事的过程。七步定乾坤:完整 TLS 1.2 握手拆解我在纸上画了无数遍。TLS 1.2 的完整握手,不算两端内部的密钥派生,正好七个 TCP 包。第一步:ClientHello(客户端发起通话)客户端发一条明文消息给服务器。里面写了什么?前 4 字节(时间戳):GID // 0x474944 就是 ASCII 的 "GID"开个玩笑。实际 TLS 的 ClientRandom 的前 4 字节是 Unix 时间戳:00 00 00 00 - 版本号(TLS 1.2 = 0x0303,这里只举例)不对,我认真说:ClientHello 的精确结构:1 字节:TLS 内容类型(0x16 = Handshake) 2 字节:协议版本(0x0301 = TLS 1.0,但实际内容看下面) 2 字节:长度(指整个 Handshake 消息的长度) Handshake 消息体: 1 字节:握手类型(0x01 = ClientHello) 3 字节:长度 2 字节:ClientHello 版本(0x0303 = TLS 1.2) 32 字节:ClientRandom(前 4 字节是 Unix 时间戳,后 28 字节纯随机) ……然后是一系列扩展:关键内容拆解:① 密码套件列表客户端列出一堆它支持的密码套件,每个套件 2 字节。你猜有多少个?通常是 15-30 个。Cipher Suites (16 suites): TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) ← 最强推荐 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f) ← 也很强 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c) TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c) ← 没有 Forward Secrecy ……注意:密码套件名字里每个段都有含义:密钥交换算法:ECDHE、RSA、DHE证书签名算法:ECDSA、RSA对称加密算法:AES_128_GCM、AES_256_GCM、CHACHA20_POLY1305HMAC 算法:SHA256、SHA384② 32 字节的 ClientRandom这 32 个字节很重要。它不是随便生成的随机数——它跟后面 ServerRandom、Pre-Master Secret 一起,经过一个伪随机函数(PRF)推导出最终的 Master Secret。整个过程输入的三个参数里,只要有一个变了,最终密钥就完全不同。③ SNI 扩展(这个最骚)Extension: server_name (type=0x0000, length=…) Server Name Indication extension Server Name list length: … Server Name Type: host_name (0x00) Server Name length: … Server Name: www.dumpany.cn看到了吗?你访问的域名在 ClientHello 里是明文传输的!即使使用 HTTPS,中间人也能从 ClientHello 的 SNI 扩展里知道你正在访问哪个网站。这就解释了为什么有些国家的防火墙能封掉特定网站——它不需要解密 TLS,只需要看 ClientHello 里的 SNI。域名是明文的。这也是为什么 ECH(Encrypted Client Hello,加密的 ClientHello)在最近的协议版本中被大力推进——它要把这最后一块明文的拼图也给加密。④ ALPN 扩展客户端说:“我支持这些应用层协议:http/1.1、h2(HTTP/2)”。服务器会挑一个双方都支持的。这在抓包里可以看到协商结果。第二步:ServerHello(服务器回礼)服务器回复,也是明文:1 字节:0x16(Handshake) 2 字节:0x0303(TLS 1.2) 2 字节:长度 Handshake 内容: 1 字节:0x02(ServerHello) 3 字节:长度 2 字节:服务器选定的 TLS 版本(一般也是 0x0303) 32 字节:ServerRandom 1 字节:Session ID 长度 可变:Session ID(如果有) 2 字节:选定的密码套件(比如 0xc02f) 1 字节:选定的压缩方法(0x00 = 不压缩) 可变:扩展关键:ServerRandom 也是一个 32 字节的随机数。它的作用是保证即使两个客户端在同一毫秒发了同样的 Pre-Master Secret(几乎不可能),最终的会话密钥也完全不同。第三步:Certificate(服务器亮出身份证)这一步信息量极大,是整个 TLS 握手最值得细细品味的消息。服务器发来自己的证书链——注意不是只有一个证书,是多个。正常情况下的证书链:证书 1(叶子证书 / 服务器证书) Version: 3 (0x02) Serial Number: 04:79:fb:8a:... Signature Algorithm: sha256WithRSAEncryption Issuer: C=US, O=Let's Encrypt, CN=R3 ← 谁签发了我?(中间 CA 名) Validity Not Before: Jul 20 01:00:00 2026 GMT Not After : Oct 18 01:00:00 2026 GMT Subject: CN=dumpany.cn ← 这个证书是发给谁的 Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) ... X509v3 extensions: X509v3 Subject Alternative Name: ← 这就是 SAN 列表 DNS:dumpany.cn, DNS:www.dumpany.cn ← 所有受保护的域名 CA:FALSE ← 这个证书不能做 CA ... 证书 2(中间 CA 证书) Version: 3 (0x02) Serial Number: ... Issuer: C=US, O=Internet Security Research Group, CN=ISRG Root X1 ← 根证书签发了我 Subject: C=US, O=Let's Encrypt, CN=R3 ← 我就是 Let's Encrypt 的中间 CA X509v3 extensions: CA:TRUE ← 这个证书是 CA(可以签发其他证书) ... 证书 3(根证书——一般不发送,因为浏览器已经有了)在这个消息里,中间人能读到什么?明文!在 TLS 1.2 里,Certificate 消息是明文的。中间人可以看到完整的证书内容——包括你用的哪个 CA、证书什么时候过期、你的公钥是多少。这也意味着:在 TLS 1.2 里,中间人在握手阶段就能判断你访问的是哪个网站——即使 SNI 被加密了(实际上没有),从证书的 CN 或 SAN 也能读到域名。这也是 TLS 1.3 要改善的地方之一——在 1.3 里,Certificate 消息是加密的。第四步:ServerKeyExchange(如果有的话)这一步只在使用了 DHE/ECDHE 密钥交换时才出现。如果是 RSA 密钥交换,没有这一步。服务器发了什么:1 字节:0x0c(ServerKeyExchange) 3 字节:长度 内容: 1 字节:椭圆曲线类型(named_curve = 0x03) 2 字节:曲线名称(x25519 = 0x001d,或 secp256r1 = 0x0017) 1 字节:公钥长度 可变:服务器 DH 公钥 2 字节:签名长度 可变:签名(用自己的证书私钥签的)这一步是 ECDHE 的核心所在。服务器说:“这是我的 DH 公钥,你要的曲线是 x25519,参数都在明文中。你验一下签名,确认是我发的,没人改过。”为什么这一步对中间人至关重要?如果中间人想冒充服务器,它必须在这一步之后,自己也生成一对 DH 公私钥,把自己的公钥发给客户端。但是——中间人用自己的私钥签名后,客户端用真正的服务器的公钥(从第三步的证书里拿到的)去验证签名时,会失败。因为签名用的是中间人的私钥,不是服务器的私钥。这就是 CA 信任链在 ECDHE 中的关键作用。第五步:ClientKeyExchange(客户端奉上钥匙)客户端发:1 字节:0x10(ClientKeyExchange) 3 字节:长度 内容:根据不同的密钥交换算法,内容完全不同RSA 模式(现在很少用了):客户端生成 48 字节的 Pre-Master Secret:Pre-Master Secret: 2 字节:客户端支持的 TLS 版本(比如 0x0303) 46 字节:纯随机数然后用服务器的公钥加密这 48 字节,发过去。服务器用自己的私钥解密,拿到 Pre-Master Secret。DH/ECDHE 模式(现代主流,TLS 1.3 强制用):客户端发送自己的 DH 公钥。然后双方各自用自己的私钥 + 对方的公钥,计算出相同的 Pre-Master Secret。关键区别:RSA 模式下,Pre-Master Secret 是"客户端生成 → 加密发送 → 服务器解密"——它在网络上传输过(虽然是加密的)ECDHE 模式下,Pre-Master Secret 是"双方各自用对方的公钥 + 自己的私钥在本地计算出"——它从未在网络上传输过第二个模式的最大优势:Forward Secrecy(前向安全性)。什么意思呢?如果用 RSA 模式,五年后攻击者拿到了服务器的私钥——它可以解密五年前保存的加密的 Pre-Master Secret,进而推算出五年前的会话密钥,解密五年前的通信内容。如果用 ECDHE 模式,五年前的通信的 DH 临时密钥早销毁了。即使拿到了服务器的主私钥,也计算不出过去的 Pre-Master Secret。也就解密不了过去的通信。这就是"前向安全"——确保即使今天被攻破,过去的通信仍然安全。第六步:Change Cipher Spec + Finished双方各自做了一大堆数学计算:ClientRandom + ServerRandom + Pre-Master Secret → PRF(伪随机函数) → Master Secret(48 字节) → 密钥导出函数 → 6 个会话密钥: - 客户端写密钥(AES-128-GCM 的 key) - 服务端写密钥 - 客户端 IV - 服务端 IV - 客户端 HMAC 密钥 - 服务端 HMAC 密钥这个过程在代码里的伪代码表示:// TLS 1.2 密钥派生伪代码 function derive_keys(client_random[32], server_random[32], premaster_secret[48]): // 第一步:用 PRF 从 Pre-Master Secret 派生出 Master Secret master_secret[48] = PRF( secret: premaster_secret, label: "master secret", seed: client_random + server_random ) // 第二步:用 PRF 从 Master Secret 派生出 6 个会话密钥 key_b