TLS 1.2 握手

本页目录

服务器发来证书和临时 ECDHE 公钥,客户端为什么不能只验证证书就开始加密?证书绑定长期身份,握手签名把临时参数绑定到该身份,Finished 再确认双方对协商历史和密钥的理解一致。这三个检查互相配合,才构成可认证的安全通道。

想确认什么对应机制
对端是否是预期服务证书链、服务身份与用途检查
临时密钥参数是否被冒充ECDHE ServerKeyExchange 签名
双方是否得到一致秘密密钥派生及 Finished 验证
后续记录是否被改动AEAD 或套件规定的记录保护
为何重连可以减少往返恢复先前会话状态,仍验证新 Finished

本篇以 TLS 1.2、ECDHE 和 AEAD 的完整服务器认证握手为主,另解释历史 RSA 密钥交换和恢复差异。TLS 1.2 的全部历史选项不等于今天都适合启用;现代部署选择应结合安全使用规范与客户端兼容性。RFC 9325:TLS 安全使用

截至 2026 年 9 月,TLS 1.3 的更新规范 RFC 9846 已发布,并取代 RFC 8446 及若干旧版本相关文档,也为 TLS 1.2 实现规定新增要求。本篇引用 RFC 5246 等是为了准确解释 TLS 1.2 机制,不把历史可选项当作当前部署基线。RFC 9846

先区分 TCP、TLS record 与握手消息

TLS 通常运行在 TCP 之上,TCP 分段、TLS record 和 Handshake message 是三种边界。一条握手消息可以跨多个 record,一个 record 也可装多个握手消息;抓到一个 TCP 包不等于看到完整 Certificate。

TLS 1.2 record 的固定头为 5 B,表示内容类型、版本和长度。Handshake message 另有 1 B 类型和 3 B 长度。握手 transcript 按协议握手消息编码累计,不是直接把以太网帧或 TCP 分段字节全部 hash。RFC 5246:记录层与握手

四个 flight:约两次 TLS 往返

TLS 1.2 完整 ECDHE 握手:无客户端证书的常见路径 客户端 服务器 ClientHello:版本、random、套件与扩展 ServerHello、Certificate、ServerKeyExchange、ServerHelloDone ClientKeyExchange、CCS、Finished CCS、Finished 客户端验证证书身份与服务器临时参数签名 双方独立计算 ECDH 共享秘密,按相同规则派生密钥 CCS 是独立协议消息,切换记录保护;Finished 验证握手 transcript。图不包含 TCP 建连和可选恢复。

客户端先发 ClientHello,服务端返回选择结果、证书和带签名的临时参数;客户端验证后发自己的临时公钥,再切换保护并发 Finished;服务端验证后切换并返回 Finished。常规完整握手约两 RTT,⁠不含 TCP 建连、DNS、重试与证书处理⁠,也不是无论何种扩展都固定耗时。

服务器在 ECDHE 路径中计算共享秘密,不是“用 RSA 私钥解密客户端发来的 pre_master_secret”。后者属于另一种历史密钥交换。可选客户端证书认证还会增加 CertificateRequest、客户端 Certificate/CertificateVerify 等消息,后续 mTLS 篇单独展开。

ClientHello/ServerHello:选择必须有共同上下文

ClientHello 带版本、32 B random、套件列表以及扩展;ServerHello 选定可用版本、套件及相应扩展结果。历史 Random 格式的时间字段不能作为可信时钟或客户端系统指纹;现代实现也不应靠它承担业务时间语义。

SNI 帮助服务器选择虚拟主机和证书,但不能替客户端的服务身份验证;ALPN 协商应用协议,例如 h2 或 http/1.1。即使 TLS 成功,应用层仍要遵守选定协议,而不是继续发送与 ALPN 不符的内容。RFC 6066:SNI、RFC 7301:ALPN

supported_groups、签名算法与套件是不同约束:支持一条曲线不等于接受任意证书算法。TLS 1.2 历史上可协商压缩,现代安全配置应禁用记录压缩;不能将现代要求说成 RFC 5246 从未定义过非 null 压缩。

套件名拆解:RSA 可以只负责签名

以 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 为例:

部分本例作用不应误读为
ECDHE临时椭圆曲线 DH 密钥交换证书必须也使用同一条 EC 曲线
RSA服务器认证所用签名类别RSA 加密所有应用数据或 PMS
AES_128_GCM128 bit AES 的 AEAD 记录保护另加一个 SHA256 记录 MAC
SHA256套件规定的 TLS 1.2 PRF hash全部 TLS 1.2 套件只允许 SHA256

例如 AES-256-GCM-SHA384 套件使用 SHA384 PRF。套件是注册的整体组合,不能任意拼四段字符串就产生一个有效套件;握手签名使用的 hash 还受签名算法协商约束,不必等于套件名结尾。RFC 5289:ECC AEAD 套件

ECDHE:签名临时参数,再计算共同秘密

客户端临时私钥为 a、公钥为 aG,服务端临时私钥为 b、公钥为 bG。经过正确的参数检查与认证后,两者分别计算 a(bG) 和 b(aG),得到相同共享秘密,而线上不直接传输 a、b 或共享秘密本身。

服务端用证书对应私钥签 ClientHello.random、ServerHello.random 和规定的 ECDHE 参数编码。客户端既要检查证书链与预期名称,也要验这份签名;只接受一个任意 ECDHE 公钥,会给中间人替换参数留下空间。RFC 8422:TLS ECC 握手

前向保密来自临时密钥交换及秘密的正确管理:未来长期签名密钥泄漏,不应直接从过去抓包恢复已销毁的临时 DH 秘密。它不保证正在运行端点失陷时仍能保护内存中的会话,也不自动覆盖会话票据保护密钥泄漏。

历史 RSA key exchange 则由客户端生成 PMS,用证书 RSA 公钥加密,服务端解密;记录流量被保存后,长期解密私钥泄漏会影响历史秘密。它在这里用于理解协议差异,不作为新的部署推荐。RFC 5246:RSA 与 DH 计算

master_secret:传统派生与 EMS

为便于对照旧抓包资料,本节保留 RFC 5246 的变量名。RFC 9846 将 master secret 改称 main secret、premaster secret 改称 preliminary secret,EMS 改称 Extended Main Secret;线上 PRF 标签保持兼容,不应把代码中的字面标签跟着改名。RFC 9846 附录 D

传统 TLS 1.2 派生式为:

master_secret = PRF(PMS, "master secret", client_random || server_random)[0:48]

EMS 协商成功后,改为使用 "extended master secret" 标签与规定的 session_hash,把主秘密绑定到本次握手上下文。不能讲了 EMS 防护之后仍只列旧公式并声称它适用于所有握手。RFC 7627:Extended Master Secret

接着生成记录保护所需材料:

key_block = PRF(master_secret, "key expansion", server_random || client_random)

这里两个 random 的顺序与传统 master_secret 式相反。PRF 标签、输入次序、hash 和长度都参与结果,不能只要双方有相同 PMS 就任意拼接。客户端写密钥用于客户端发送方向,服务器写密钥用于反方向。

AES-128-GCM 示例:分方向派生 40 B key_block master_secret:48 B 由 PMS 与协商的 PRF/EMS 规则派生 client_write_key 16 B server_write_key 16 B client IV 4 B server IV 4 B 此 GCM 套件没有独立记录 MAC key;固定 IV 与每条记录显式 nonce 共同构造 nonce,不能重复。

AES-128-GCM 示例需要两份 16 B write key、两份 4 B 固定 IV,共 40 B;独立 write_MAC_key 长度为零。AEAD 自身提供记录认证,因此这里不再分配独立的记录 HMAC 密钥。GCM nonce 还含每条记录的 8 B 显式部分,固定 IV 不等于每条记录可重用完全相同 nonce。RFC 5288:TLS GCM

CCS 与 Finished:切换保护和确认历史

TLS 1.2 ChangeCipherSpec 是独立协议消息,通知对端随后使用已协商的保护状态。Finished 是握手消息,使用新记录保护发送,其 verify_data 为基于 master_secret、方向标签和 transcript hash 的 PRF 输出;本例为 12 B。RFC 5246:Finished

Finished 不是服务器再用证书私钥做一次数字签名。验证它需要共享秘密,也把协商历史绑定进来;客户端 Finished 的 transcript 不包含自身,之后服务器 Finished 的 transcript 则包含已经发送的客户端 Finished。CCS 不作为 Handshake message 纳入该 transcript。

验完服务器 Finished 才能按常规完整握手流程确信这一阶段完成。某些优化会允许特定条件下提前发送,不能用它们把基线流程任意改写。TLS alert 与 TCP 断开也不同,协议关闭语义和应用是否完成自己的消息需要分别处理。

改变一处输入,哪些秘密会跟着改变?

先在传统路径中只改变其他握手消息:主秘密与记录密钥保持不变,Finished改变。再切到EMS,同样的修改沿session_hash传到主秘密及后续输出。颜色来自实际字节比较,不是预设的动画。

正在呈现知识画面
改变一处输入,哪些秘密会跟着改变?

派生是有方向的依赖图。传统主秘密不绑定完整transcript,EMS绑定session_hash;Finished还会绑定自己所覆盖的握手历史。

记录开销:应用字节不等于线上字节

在本例 TLS 1.2 AES-GCM record 中,1000 B 应用明文,另有 8 B 显式 nonce、16 B tag 和 5 B record 头,TLS 层共 1029 B;尚未计 TCP/IP 和链路头。不同 AEAD/CBC 套件有不同开销,不能把 29 B 套用到 TLS 1.3。

记录序号等信息参与认证,接收端按连接状态处理,不能任意交换或删改记录后仍得到相同有效应用流。若应用自己需要消息级去重和事务确认,TLS 也不替它完成这些语义。

恢复:复用会话秘密,派生新的连接状态

Session ID 方式由服务端用 ID 查找缓存会话;票据方式可将会话状态以服务端可恢复、带机密性和完整性保护的形式交给客户端。RFC 5077 通过扩展协商,并由 NewSessionTicket 握手消息发出票据,不能说首次票据只是塞进 ServerHello extension 的 blob。RFC 5077:会话票据

恢复成功时使用简化握手,通常服务端先发送 ServerHello、CCS、Finished,客户端再返回 CCS、Finished,约一 RTT 的 TLS 交互。双方使用新 random 派生连接密钥,而不是把上一次所有记录 key/nonce/序号原样拿来继续。

恢复可能被拒绝并转完整握手,取决于票据/缓存寿命、密钥、参数和策略。票据机制减少逐会话服务器存储,不代表服务端完全无状态:票据保护密钥、轮换、策略及资源仍需管理。

公开可观察到的加密票据本身与能解密票据的服务端保护密钥泄漏不是同一事件。后者可能暴露被封装的会话秘密及相关历史流量;客户端保存的恢复秘密失窃也有影响。保密与生命周期应按整个恢复机制分析,不能用“ticket 泄露就一定解密所有历史会话”概括。

本地 PRF 与编码预算

import hashlib
import hmac

def prf(secret,label,seed,length):
    seed = label+seed
    A = seed
    out = b''
    while len(out)<length:
        A = hmac.digest(secret,A,'sha256')
        out += hmac.digest(secret,A+seed,'sha256')
    return out[:length]

# 固定公开教学材料,不是生产密钥。
pms = bytes(range(32))
client_random,server_random = bytes(range(32)),bytes(range(32,64))
master = prf(pms,b'master secret',client_random+server_random,48)
assert len(master)==48
assert master != prf(pms,b'master secret',server_random+client_random,48)
transcript_hash = hashlib.sha256(b'teaching handshake transcript').digest()
ems = prf(pms,b'extended master secret',transcript_hash,48)
assert ems != master
block = prf(master,b'key expansion',server_random+client_random,40)
client_key,server_key,civ,siv = block[:16],block[16:32],block[32:36],block[36:40]
assert tuple(map(len,(client_key,server_key,civ,siv)))==(16,16,4,4)
assert client_key != server_key
finished = prf(master,b'client finished',transcript_hash,12)
changed = hashlib.sha256(b'changed handshake transcript').digest()
assert finished != prf(master,b'client finished',changed,12)
assert finished != prf(master,b'server finished',transcript_hash,12)
assert 1000+8+16+5==1029
print('TLS 1.2 PRF 输入顺序、EMS 分离、方向密钥、Finished 与 GCM 开销通过')

这段代码演示 SHA256 P_hash 组合及依赖关系,transcript 是教学占位字节,不是完整 TLS 握手;未生成真实证书、与外部服务握手或更改 TLS 配置。

继续查阅