2027-03-25 · cryptography
国密算法与国密 TLS 系列索引
从 SM3/SM4/SM2 的设计对比到国密 TLS 握手、生态落地、PQC 迁移——国密技术的完整知识图谱。























当浏览器地址栏亮起那把小锁时,背后正在运行的便是 TLS(Transport Layer Security)协议。从电子商务到即时通信,从 API 调用到物联网遥测,几乎所有需要在不可信网络上传输敏感数据的场景都依赖 TLS 提供机密性、完整性与身份认证。TLS 1.3 在 2018 年由 RFC 8446 正式发布,标志着互联网安全传输协议经历二十余年迭代后达到的最新里程碑。本文将从协议演进史讲起,逐层拆解 TLS 1.3 的握手消息、密钥调度、零往返时间(0-RTT)数据、证书验证、会话恢复等核心机制,并展望 ECH(Encrypted Client Hello)与后量子迁移等前沿议题。
先用一张图把本文的核心关系串起来。
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello + key_share
S->>C: ServerHello + key_share
S->>C: {EncryptedExtensions, Certificate, CertificateVerify, Finished}
C->>S: {Finished}
C->>S: [Application Data]
S->>C: [Application Data]
1995 年,Netscape 公司发布了 SSL 2.0(Secure Sockets Layer),这是业界首个被广泛部署的安全传输协议。然而 SSL 2.0 的设计存在严重缺陷:它使用 MD5 作为唯一的消息摘要算法,握手过程缺少对消息完整性的保护,攻击者可以在中间截断密码套件列表,迫使双方降级到最弱的算法——这便是最早的降级攻击(downgrade attack)。此外,SSL 2.0 的密钥派生过程过于简单,加密与认证使用同一把密钥,违反了密钥分离(key separation)原则。这些教训在后续版本中反复被引述:协议设计必须假设攻击者可以篡改握手消息中的任何明文字段。
1996 年发布的 SSL 3.0 对协议进行了全面重写。它引入了更完善的握手状态机、独立的 MAC 密钥与加密密钥、以及用于防止降级的 Finished 消息——该消息是对整个握手转录(transcript)的哈希校验,任何对握手消息的篡改都会导致 Finished 验证失败。SSL 3.0 的框架极具影响力,后续的 TLS 1.0 至 TLS 1.2 在结构上都是它的继承者。但 SSL 3.0 本身也未能幸免于密码学攻击:2014 年披露的 POODLE 攻击利用了 CBC 模式填充验证(padding oracle)的弱点,宣告了 SSL 3.0 的终结。
TLS 1.0(RFC 2246,1999 年)本质上是 SSL 3.0 的标准化版本,改动幅度有限。它将密钥派生中的自定义伪随机函数替换为基于 HMAC 的 PRF(pseudorandom function),但仍保留了 CBC 模式中可预测的初始化向量(IV),这一缺陷后来被 BEAST 攻击所利用。TLS 1.1(RFC 4346,2006 年)修复了 IV 预测问题,要求每条记录使用显式随机 IV,但在密码套件与握手流程方面未做根本性变革。
TLS 1.2(RFC 5246,2008 年)是一次重要升级。它引入了协商哈希算法的能力——此前版本强制使用 MD5 与 SHA-1 的组合——并为 AEAD(Authenticated Encryption with Associated Data)密码套件铺平了道路,其中 AES-GCM 成为最受推荐的选择。TLS 1.2 支持丰富的扩展机制,功能强大却也日趋臃肿:它允许 RSA 密钥传输(不提供前向保密)、静态 Diffie-Hellman 参数、CBC 密码套件、记录层压缩(为 CRIME 攻击提供了条件)、以及重协商(renegotiation)机制。这些可选特性的组合爆炸使得实现者难以全面覆盖测试,也让攻击面居高不下。
TLS 1.3 的设计哲学可以用一个词概括:做减法。它移除了所有不提供前向保密的密钥交换方式,移除了 CBC 密码套件,移除了压缩,移除了重协商,将握手往返次数从两次缩减为一次(1-RTT),并引入了零往返恢复(0-RTT)模式。同时,握手消息在 ServerHello 之后全部加密传输,极大地缩小了被动监听者能获取的元数据。TLS 1.3 的开发历程长达四年,经过了 28 个草案迭代,并受到形式化验证工具(如 Tamarin 和 ProVerif)的检验,是密码学协议工程的典范。
TLS 1.3 的完整握手(full handshake)仅需一个往返时间(1-RTT)即可完成密钥协商与身份认证。以下是消息流的逐步解析。
客户端 服务端
| |
| ClientHello |
| + supported_versions(1.3) |
| + key_share(g^x) |
| + signature_algorithms |
| + supported_groups |
| + psk_key_exchange_modes [可选] |
| + pre_shared_key [可选] |
| -------------------------------------------------> |
| |
| ServerHello |
| + supported_versions |
| + key_share(g^y) |
| {EncryptedExtensions} |
| {CertificateRequest*} |
| {Certificate} |
| {CertificateVerify} |
| {Finished} |
| <------------------------------------------------- |
| |
| {Certificate*} |
| {CertificateVerify*} |
| {Finished} |
| -------------------------------------------------> |
| |
| [Application Data] <==========> [Application Data]|
| |
() = 明文 {} = 握手密钥加密 [] = 应用密钥加密
* = 可选消息(取决于是否要求客户端证书)
客户端发送的第一条消息包含以下关键字段。supported_versions
扩展声明客户端支持 TLS
1.3;这一设计取代了旧版本中利用记录层版本号进行协商的方式,避免了中间设备因不认识新版本号而错误地丢弃连接的兼容性问题。key_share
扩展包含一个或多个临时(ephemeral)Diffie-Hellman
公钥,客户端在发送 ClientHello 时便主动提供密钥材料,这是
TLS 1.3 能够在一个往返内完成握手的关键——在 TLS 1.2
中,密钥交换参数要等到第二轮消息才会出现。signature_algorithms
告知服务端客户端支持哪些签名算法,用于验证服务端证书链。supported_groups
列出客户端支持的椭圆曲线或有限域群。如果客户端持有来自先前连接的预共享密钥(PSK),还可以携带
pre_shared_key 扩展实现会话恢复或 0-RTT。
服务端从客户端提供的选项中选定密码套件与密钥交换参数后回复
ServerHello。该消息包含服务端的临时 DH 公钥
key_share(g^y),客户端与服务端此时各自可以计算出共享密钥
g^{xy}。从 ServerHello
之后,所有后续握手消息都将使用从该共享密钥派生的握手流量密钥(handshake
traffic key)进行加密。这意味着
EncryptedExtensions、Certificate、CertificateVerify、Finished
等消息对被动窃听者完全不可见——在 TLS 1.2
中,这些消息全部以明文传输。
值得注意的是,如果服务端不支持客户端在
key_share 中提供的任何群,它会发送一条
HelloRetryRequest
消息,指定一个它支持的群,要求客户端重新发送
ClientHello,此时握手退化为
2-RTT,但这种情况在实践中并不常见。
EncryptedExtensions 消息承载那些不需要用于密码套件协商但又属于握手参数的扩展,例如服务器名称指示(SNI)的确认、应用层协议协商(ALPN)的结果等。它之所以单独成为一条消息而不合并到 ServerHello 中,正是因为这些扩展需要加密保护——ServerHello 本身必须保持明文以完成密钥交换。
Certificate 消息携带服务端的证书链。CertificateVerify 消息是 TLS 1.3 安全性的基石之一:服务端使用其证书私钥对整个握手转录的哈希值进行签名。这里的转录哈希(transcript hash)涵盖了从 ClientHello 到 Certificate 的所有消息。由于 ClientHello 中包含了客户端的随机数和密钥份额,而 ServerHello 中包含了服务端的随机数和密钥份额,因此该签名将服务端的身份与本次特定的密钥交换绑定在一起。任何中间人(MITM)如果试图替换密钥份额,就无法生成有效的 CertificateVerify 签名,因为它不持有服务端证书对应的私钥。
与 TLS 1.2 相比,TLS 1.3 的 CertificateVerify 签名对象是转录哈希,而非仅对密钥交换参数签名,这避免了 TLS 1.2 中存在的跨协议攻击(cross-protocol attack)风险。此外,签名前会加上一段固定的上下文字符串与 0x20 填充,以防止签名被重新用于其他目的。
Finished 消息是对整个握手转录的 HMAC 校验,使用从握手密钥派生的专用密钥(finished_key)计算。客户端和服务端各自发送一条 Finished 消息。服务端的 Finished 证明它确实参与了握手并持有正确的密钥材料;客户端的 Finished 完成握手确认。在双方都验证了对方的 Finished 之后,握手结束,后续通信切换到应用流量密钥(application traffic key)。
TLS 1.3 的密钥调度(key schedule)是协议中最精妙的设计之一,它基于 HKDF(HMAC-based Key Derivation Function,RFC 5869)构建了一条清晰的密钥派生链,实现了密钥分离与前向保密。
HKDF 包含两个阶段:提取(Extract)与扩展(Expand)。HKDF-Extract 接受一个盐(salt)和输入密钥材料(IKM),输出一个伪随机密钥(PRK)。HKDF-Expand-Label 在 TLS 1.3 中是 HKDF-Expand 的包装,接受 PRK、一个标签(label)、一个上下文(context)和输出长度,产生派生密钥。标签与上下文的引入确保了即使从同一个 PRK 出发,用于不同目的的密钥也彼此独立——这就是密钥分离原则在实践中的体现。
一个自然的疑问是:为什么不能直接将 Diffie-Hellman
共享密钥 g^{xy} 用作对称加密密钥?原因在于,DH
运算的原始输出并非均匀随机比特串。以椭圆曲线为例,ECDH
的共享秘密是曲线上一个点的 x
坐标,其比特分布受曲线代数结构约束——某些比特位的取值存在统计偏差。对于
AES
等对称算法而言,密钥的每一位都应当是不可预测的;如果密钥存在偏差,攻击者可以利用这些统计规律缩小暴力搜索空间。
考虑一个反例:假设某实现将 256 位 ECDH 共享秘密直接截取为 AES-256 密钥。
// 危险:直接使用原始 DH 输出作为密钥
shared_point = ECDH(my_private, peer_public) // 曲线上的点
raw_key = shared_point.x // 取 x 坐标的 256 位表示
aes_key = raw_key // 直接用作 AES-256 密钥 — 错误!
// 正确:通过 HKDF 提纯后再使用
prk = HKDF-Extract(salt, shared_point.x) // 熵集中,输出均匀分布
aes_key = HKDF-Expand(prk, "enc", 32) // 派生加密密钥
mac_key = HKDF-Expand(prk, "mac", 32) // 派生 MAC 密钥,与加密密钥独立
由于椭圆曲线点的 x 坐标并不均匀覆盖
{0,1}^256
空间,攻击者通过分析曲线结构可以推断出密钥的若干比特约束,从而将等效安全强度从
256
位降低。更严重的是,如果同一原始密钥被复用于多个目的(加密、MAC、IV
派生),一个用途的泄露可能连锁暴露其他用途的密钥。
HKDF 的 Extract 步骤正是为了解决这一问题:它通过 HMAC 将任意分布的输入密钥材料”压缩”为一个固定长度的伪随机密钥(PRK),将分散的熵集中到均匀分布的输出中。随后,Expand 步骤从这个高质量的 PRK 出发,利用不同的标签和上下文派生出多把相互独立的子密钥——即使攻击者获得了其中一把,也无法推导出其他子密钥。这就是 Extract-then-Expand 两步分离设计的安全直觉:先”提纯”熵源,再”分发”密钥。
TLS 1.3 的密钥调度将 HKDF 编织成一条三级派生链:Early Secret、Handshake Secret、Master Secret。每一级都通过 HKDF-Extract 混入新的密钥材料,再通过 Derive-Secret(基于 HKDF-Expand-Label)派生下一级的盐值,形成单向递进的密钥演化过程。
Early Secret = HKDF-Extract(salt=0, IKM=PSK)
|
+-> Derive-Secret(., "derived", "") --作为下一级的盐-->
|
Handshake Secret = HKDF-Extract(salt=上述派生值, IKM=DHE共享密钥)
|
+-> Derive-Secret(., "derived", "") --作为下一级的盐-->
|
Master Secret = HKDF-Extract(salt=上述派生值, IKM=0)
这条链的关键设计在于:每一级的 Extract 操作都将前一级的输出(通过 Derive-Secret 转化为盐)与新的密钥材料不可逆地混合。即使攻击者获知了 Master Secret,也无法逆向还原 Handshake Secret 或 Early Secret,因为 HMAC 的单向性阻断了回溯路径。同样,即使 PSK 泄露,只要 DHE 密钥交换使用了临时密钥对,Handshake Secret 和 Master Secret 仍然安全——这正是前向保密的密钥调度基础。
TLS 1.3 的密钥调度沿三级结构演进:
0(全零值)
|
v
PSK -------> HKDF-Extract = Early Secret
|
+--------+--------+
| |
v v
binder_key client_early_traffic_secret
(用于 0-RTT 数据)
|
v
(EC)DHE ----> HKDF-Extract = Handshake Secret
|
+--------+--------+
| |
v v
client_handshake server_handshake
_traffic_secret _traffic_secret
(加密客户端握手) (加密服务端握手)
|
v
0 --------> HKDF-Extract = Master Secret
|
+--------+--------+
| |
v v
client_app server_app
_traffic_secret _traffic_secret
(加密应用数据) (加密应用数据)
第一级是 Early Secret,由 PSK(如果存在)与全零盐经
HKDF-Extract 生成。如果没有 PSK,则使用全零值替代。Early
Secret 派生出用于 0-RTT 数据加密的
client_early_traffic_secret 以及用于 PSK 验证的
binder_key。
第二级是 Handshake Secret,将 Early Secret 的派生值作为盐,与 (EC)DHE 共享密钥一起输入 HKDF-Extract。它派生出客户端握手流量密钥和服务端握手流量密钥,分别用于加密双方在握手阶段发送的消息。
第三级是 Master Secret,以 Handshake Secret 的派生值为盐,输入全零值经 HKDF-Extract 生成。它派生出应用数据的读写密钥,以及用于会话恢复的 resumption_master_secret。
这一设计的核心优势在于密钥分离:即使攻击者获取了某一层的密钥,也无法推导出其他层的密钥。例如,泄露应用流量密钥不会危及握手消息的机密性,反之亦然。同时,由于每次握手都使用临时 DH 密钥对,即使长期私钥(证书私钥)被泄露,过去的会话记录也无法被解密——这便是前向保密(forward secrecy)的保障。在 TLS 1.2 中使用 RSA 密钥传输时,服务端的 RSA 私钥泄露将导致所有历史会话可被解密,TLS 1.3 从根本上消除了这一风险。
TLS 1.3 还支持在连接存续期间通过 KeyUpdate 消息触发密钥更新。新的应用流量密钥由旧密钥经 HKDF-Expand-Label 派生而来,无需重新握手。这一机制在长寿命连接中限制了单把密钥加密的数据量,降低了密钥磨损风险。
在现代互联网架构中,延迟是用户体验的关键指标。传统 TLS 握手需要至少一个往返时间(约数十到数百毫秒)才能开始传输应用数据。对于频繁重连的场景——例如移动设备在 Wi-Fi 与蜂窝网络之间切换、CDN 边缘节点的短连接——每次握手的延迟都意味着可感知的卡顿。0-RTT(zero round-trip time)模式允许客户端在发送 ClientHello 的同时附带早期数据(early data),服务端无需等待完整握手即可处理这些数据,从而将有效延迟降为零。
0-RTT 的前提是客户端持有来自先前连接的 PSK。客户端利用
PSK 派生 Early Secret,再派生
client_early_traffic_secret,用它加密早期数据(通常是
HTTP 请求的第一个报文)。这些数据随 ClientHello
一起发送。服务端收到后,如果能找到对应的
PSK,即可立刻解密并处理这些早期数据,同时并行完成握手。
客户端 服务端
| |
| ClientHello + key_share + pre_shared_key |
| (early_data 指示) |
| [0-RTT 应用数据 — 使用 early traffic key 加密] |
| -------------------------------------------------> |
| |
| ServerHello |
| {EncryptedExtensions} |
| (early_data 接受/拒绝) |
| {Finished} |
| <------------------------------------------------- |
| |
| {Finished} |
| -------------------------------------------------> |
| |
| [Application Data] <==========> [Application Data]|
0-RTT 数据面临一个根本性的安全限制:它不具备抗重放性(replay resistance)。在标准的 1-RTT 握手中,服务端的 ServerHello 提供了新鲜的随机值,确保每次握手唯一;但 0-RTT 数据在收到服务端回复之前就已发出,缺少服务端贡献的新鲜性。攻击者可以捕获并重放包含 0-RTT 数据的 ClientHello,如果服务端不加防范,就会重复处理这些请求。
以金融交易为例:用户通过手机银行向某账户转账 1000 元,客户端将这条 POST 请求作为 0-RTT 早期数据发出。攻击者在网络链路上(例如恶意 Wi-Fi 热点)录制了该 ClientHello 及其附带的 0-RTT 密文,随后向银行服务端重放 50 次。如果服务端不具备反重放能力,每一次重放都将被视为一笔独立的转账请求——用户账户被扣款 50000 元,攻击者实现了”双重支付”甚至”多重支付”。这并非理论推演:任何将 0-RTT 数据当作普通请求处理的后端系统都暴露在此风险之下。
与之形成对比的是幂等操作。HTTP GET 请求(如加载首页资源、查询账户余额)天然具备幂等性:无论重放多少次,结果相同且不产生副作用。因此 GET 请求是 0-RTT 的理想载荷。但 HTTP POST、PUT、DELETE 等变更操作(创建订单、修改密码、删除记录)每次执行都可能改变服务端状态,一旦被重放就会造成实际损害。对于实时媒体流场景,0-RTT 可以在视频通话建立时节省一个往返的连接延迟,而媒体帧本身的重放至多导致短暂的画面重复,不会产生业务级损害——这是 0-RTT 收益高而风险可控的另一类典型场景。
TLS 1.3 规范建议了两种反重放策略。第一种是一次性票据(single-use ticket):服务端为每张会话票据维护一个使用标记,一旦 PSK 被用于 0-RTT 就立即作废,后续使用同一票据的 0-RTT 请求一律拒绝。这种方案提供最强的反重放保证,但在分布式环境中实现成本较高——多台服务器需要通过共享存储(如 Redis 集群)同步票据使用状态,否则攻击者可以将重放请求发送到尚未标记该票据的另一台服务器。
第二种是打击寄存器(strike register):服务端维护一个滑动时间窗口内收到的所有 ClientHello 的哈希值集合,拒绝窗口内重复出现的请求。这种方案无需跨服务器共享票据状态,但需要足够的内存来存储哈希值,且时间窗口之外的重放无法检测。
无论采用哪种协议层防御,应用层的幂等性保护都是不可或缺的最后一道防线。最佳实践是:HTTP 服务器应仅允许 GET 和 HEAD 请求通过 0-RTT 发送,将所有变更操作(POST、PUT、DELETE)延迟到 1-RTT 握手完成后再处理;对于必须接受 0-RTT 变更请求的场景,应在应用层实施请求去重(如基于唯一事务 ID 的幂等键)。
0-RTT 带来的延迟收益是实实在在的:在典型的跨大陆连接中(如北京到法兰克福),一个 TLS 往返约需 150-200ms,省去这一往返意味着用户在页面加载或 API 调用时可以节省 50-100ms 的可感知延迟。对于 CDN 边缘节点的短连接、移动端频繁重连、实时媒体建连等场景,这一优化直接关系到用户体验。
但这一收益必须与重放风险权衡。总结而言,0-RTT 适合以下场景:请求本身是幂等的(GET、HEAD);服务端部署了有效的反重放机制(一次性票据或打击寄存器);延迟敏感度高于安全敏感度。对于金融交易、状态变更等操作,绝不应通过 0-RTT 发送。RFC 8446 明确指出,0-RTT 数据的安全属性弱于 1-RTT 数据,协议实现者和应用开发者都必须充分理解这一权衡。
笔者认为,0-RTT 的重放困境是整个安全协议设计领域一个更深层张力的微缩样本:每一个成熟的网络协议,最终都会面临”你愿意用多少安全性来换取一个往返时间”这一根本性问题。TCP Fast Open 面临过它,QUIC 面临过它,DNS over HTTPS 正在面临它。0-RTT 的独特价值在于,TLS 1.3 的设计者没有试图假装这个问题有完美解答——他们选择了透明地暴露权衡:0-RTT 数据明确标记为安全属性弱于 1-RTT 数据,抗重放责任被显式地上推到应用层(幂等性检查)。这种”承认不完美并明确界定边界”的设计哲学,比试图在协议层面隐藏所有复杂性要诚实得多,也安全得多。此外,TLS 1.3 长达七年、历经 28 个草案版本的标准化过程本身就值得深思。这一过程中捕获了三次握手攻击(Triple Handshake)、DROWN、以及多个降级攻击的变种——这些问题在较短的审查周期中几乎不可能被发现。从工程实践来看,密码协议标准化的”慢”恰恰是其最重要的安全特性:每一个额外的草案版本都意味着更多的密码分析者有时间从更多的角度审视协议。快速发布的”安全”协议几乎总是在部署后才暴露致命缺陷——WEP 用了不到两年就被彻底攻破。TLS 1.3 用七年的耐心换来了一个在部署数年后仍未出现重大安全缺陷的协议,这笔交易无论怎么算都是值得的。
TLS 1.3 相对于 TLS 1.2 的改进可以从”移除”与”新增”两个维度来理解。
RSA 密钥传输:在 TLS 1.2 中,客户端可以直接用服务端的 RSA 公钥加密预主密钥(pre-master secret),这种方式不提供前向保密——一旦 RSA 私钥泄露,所有使用该密钥协商的历史会话都可被解密。TLS 1.3 强制要求使用临时 (EC)DHE 密钥交换,从协议层面保证了前向保密。
静态 Diffie-Hellman:类似地,TLS 1.2 允许使用固定的 DH 参数,同样不提供前向保密。TLS 1.3 仅允许临时密钥。
CBC 密码套件:CBC 模式在 TLS 中的历史充满了填充预言攻击(padding oracle attack),从 POODLE 到 Lucky Thirteen,每一次攻击都源于 MAC-then-encrypt 架构下填充验证的时序泄露。TLS 1.3 彻底移除了 CBC 模式,仅保留 AEAD 算法。
压缩:TLS 记录层压缩被 CRIME 和 BREACH 攻击利用,通过观察压缩后密文长度的变化来推断明文内容。TLS 1.3 移除了协议层压缩。
重协商:TLS 1.2 的重协商机制曾被用于注入攻击(renegotiation attack),虽然后来通过 RFC 5746 打了补丁,但 TLS 1.3 选择从根本上移除这一机制,用 KeyUpdate 和 post-handshake 认证来替代其合法用途。
自定义 DHE 群:TLS 1.2 允许服务端发送任意 DH 参数,曾导致 Logjam 攻击——攻击者利用小群进行离散对数预计算,然后实时降级连接。TLS 1.3 仅允许使用标准化的命名群(named groups)。
握手消息加密、1-RTT 握手、0-RTT 恢复、HKDF 密钥调度、简化的密码套件协商——这些改进在前文已详细讨论。此外,TLS 1.3 将 ChangeCipherSpec 消息降格为兼容性占位符(仅在 middlebox compatibility mode 中发送),不再承担实际的密码状态切换功能,进一步简化了状态机。
从工程实践来看,TLS 1.3 最深远的贡献或许不是任何单一的安全改进,而是它对复杂性的激进削减。TLS 1.2 支持数百种密码套件组合这一事实本身就是一个安全漏洞——不是因为其中每个组合都不安全,而是因为组合爆炸使得穷举测试所有交互路径变得不可能。每增加一个密码套件选项,实现中的代码路径数量就可能翻倍,而安全漏洞往往藏在那些极少被执行、因此极少被测试的代码路径中。POODLE 攻击利用的是 CBC 回退路径,FREAK 利用的是出口级密码套件路径,Logjam 利用的是小群 DH 路径——这三个攻击都发生在”理论上不应该被使用”但”实际上必须被实现”的边缘代码中。TLS 1.3 将密码套件从数百个压缩到五个,这一决策消灭的不仅是弱算法,更是所有为支持弱算法而存在的代码路径。在安全工程中,最有效的防御往往不是加固现有代码,而是删除不需要存在的代码。
TLS 1.2 定义了数百种密码套件组合,涵盖密钥交换算法、认证算法、对称加密算法和哈希算法的笛卡尔积。这种灵活性在理论上允许细粒度的安全策略定制,但实践中带来了灾难性的复杂度:实现者难以正确支持所有组合,配置者难以选择安全的子集,攻击者则可以利用降级攻击迫使双方选择最弱的组合。
TLS 1.3 采取了截然不同的策略:密码套件仅指定 AEAD 算法与哈希算法,密钥交换与认证算法独立协商。RFC 8446 定义的密码套件仅有五个:
| 密码套件 | AEAD 算法 | 哈希算法 |
|---|---|---|
| TLS_AES_128_GCM_SHA256 | AES-128-GCM | SHA-256 |
| TLS_AES_256_GCM_SHA384 | AES-256-GCM | SHA-384 |
| TLS_CHACHA20_POLY1305_SHA256 | ChaCha20-Poly1305 | SHA-256 |
| TLS_AES_128_CCM_SHA256 | AES-128-CCM | SHA-256 |
| TLS_AES_128_CCM_8_SHA256 | AES-128-CCM-8 | SHA-256 |
这五个套件全部基于 AEAD,确保加密与认证一体化完成,消除了 MAC-then-encrypt 带来的各种攻击面。AES-GCM 与 ChaCha20-Poly1305 覆盖了主流硬件平台的性能需求:AES-GCM 在具备 AES-NI 指令集的处理器上性能优异,而 ChaCha20-Poly1305 在缺乏硬件加速的移动设备和嵌入式平台上表现更佳。CCM 模式主要面向物联网等资源受限环境。
从安全角度看,密码套件的大幅缩减意味着:每一种组合都经过了充分的密码学分析和实现审计;降级攻击的收益大大降低(所有可选项都是强算法);配置错误的概率显著下降。这是”少即是多”原则在安全协议设计中的经典案例。
在 TLS 握手中,服务端需要向客户端证明自己确实是证书所声称的实体。仅仅出示证书是不够的——证书是公开信息,任何人都可以获取。关键在于证明持有证书对应的私钥。CertificateVerify 消息正是实现这一证明的机制。
TLS 1.3 的 CertificateVerify 签名覆盖以下结构:一段由 64 个 0x20 字节组成的填充、一个上下文字符串(服务端为 “TLS 1.3, server CertificateVerify”,客户端为 “TLS 1.3, client CertificateVerify”)、一个 0x00 分隔符、以及当前握手转录的哈希值。转录哈希从 ClientHello 开始,一直到 CertificateVerify 之前的所有握手消息。
这一设计有几重安全考量。首先,转录哈希将签名与本次特定握手绑定,防止签名被跨会话重用。其次,上下文字符串区分了客户端与服务端的签名,防止一方的签名被伪装为另一方。再次,0x20 填充确保签名输入不会以任何 TLS 消息类型的字节值开头,防止与其他协议的签名方案产生混淆(cross-protocol confusion)。
CertificateVerify
签名覆盖了整个握手转录,这意味着如果中间人试图修改
ClientHello 中的 supported_versions
扩展来将连接降级到 TLS
1.2,转录哈希将不匹配,CertificateVerify
验证将失败。此外,TLS 1.3 在 ServerHello 的
random 字段末尾嵌入了特殊的哨兵值(sentinel
value):如果服务端支持 TLS 1.3 但被迫协商 TLS 1.2
或更低版本,它会在 ServerHello.random 的最后 8
字节填入预定义值,客户端据此检测降级攻击并中止连接。这种多层防御策略使得
TLS 1.3 对降级攻击具有极强的抵抗力。
中间人攻击的核心在于攻击者在客户端与服务端之间分别建立两个独立的 TLS 连接,伪装成对方。然而在 TLS 1.3 中,CertificateVerify 要求签名者对包含 DH 密钥份额的转录进行签名。攻击者无法同时控制两侧的 DH 密钥份额并生成有效签名——因为它不持有合法服务端的私钥。即使攻击者拥有一张由受信任 CA 签发的其他域名证书,转录哈希中包含的 SNI 扩展也会暴露域名不匹配。
TLS 1.3 的会话恢复不再使用 TLS 1.2 中的会话 ID(session
ID)或会话票据(session ticket)的旧机制,而是统一通过
PSK(Pre-Shared
Key)实现。在完成握手后,服务端可以向客户端发送一条或多条
NewSessionTicket
消息。每张票据包含一个不透明的票据值(ticket
value)、一个过期时间(ticket_lifetime)和一个随机数(ticket_nonce)。客户端使用
resumption_master_secret 和
ticket_nonce 经 HKDF-Expand-Label
派生出与该票据关联的 PSK,并在后续连接的 ClientHello 中通过
pre_shared_key 扩展发送票据标识。
客户端在 psk_key_exchange_modes
扩展中声明它支持的 PSK 使用方式:psk_ke(仅
PSK)或 psk_dhe_ke(PSK + (EC)DHE)。纯 PSK
模式不执行 Diffie-Hellman 密钥交换,因此不提供前向保密——如果
PSK 泄露,使用该 PSK 的所有会话都将暴露。PSK + (EC)DHE
模式则在 PSK 的基础上额外执行临时密钥交换,即使 PSK
日后泄露,会话密钥仍然安全。因此,RFC 8446
强烈建议实现者优先使用 psk_dhe_ke 模式。
服务端可以选择两种票据实现策略。第一种是自加密票据(self-encrypted ticket):服务端使用只有自己知道的密钥加密会话状态,将密文作为票据值发送给客户端。恢复时,服务端解密票据即可恢复状态,无需维护服务端存储。这种方案适合无状态的分布式部署。第二种是数据库查找(database lookup):票据值仅为一个索引,实际状态存储在服务端的分布式缓存中。这种方案对票据撤销更友好。
PSK 是 0-RTT 数据的前提。只有当客户端持有有效的 PSK
且服务端在 NewSessionTicket 中设置了
max_early_data_size 参数时,客户端才能发送
0-RTT 数据。PSK 的存在使得客户端在握手开始前就能派生 Early
Secret,进而加密早期数据。但如前所述,纯 PSK 模式的 0-RTT
数据不具备前向保密性,这进一步强调了使用
psk_dhe_ke 的重要性。
以下将 TLS 1.3 的三种握手模式并列对照——从完整的 1-RTT 全握手,到 PSK 恢复握手,再到 0-RTT 早期数据——可以直观地看出每种模式在往返次数、消息数量和安全属性上的递进权衡:
(1) 完整 1-RTT 握手 (2) PSK 恢复握手 (3) 0-RTT + PSK 握手
(首次连接,无 PSK) (持有 PSK,1-RTT) (持有 PSK,0-RTT 早期数据)
Client Server Client Server Client Server
| | | | | |
| ClientHello | | ClientHello | | ClientHello |
| + key_share | | + key_share | | + key_share |
| + sig_algs | | + psk_ke_modes | | + psk_ke_modes |
| -------------> | | + pre_shared_ | | + pre_shared_ |
| | | key | | key |
| | | + early_data | | + early_data |
| ServerHello | | -------------> | | -------------> |
| + key_share | | | | |
| {Encrypted | | ServerHello | | (早期数据) |
| Extensions} | | + key_share | | [App Data] |
| {Certificate | | + pre_shared_| | -------------> |
| Request*} | | key | | |
| {Certificate} | | {Encrypted | | ServerHello |
| {Certificate | | Extensions} | | + key_share |
| Verify} | | {Finished} | | + pre_shared_|
| {Finished} | | <------------- | | key |
| <------------- | | | | {Encrypted |
| | | {Finished} | | Extensions} |
| {Certificate*}| | -------------> | | {Finished} |
| {Certificate | | | | <------------- |
| Verify*} | | [App Data] <=> | | |
| {Finished} | | | | {End Of Early |
| -------------> | | | | Data} |
| | | | | {Finished} |
| [App] <=> [App]| | | | -------------> |
| | | | | |
| [App] <=> [App]|
往返次数: 1-RTT 往返次数: 1-RTT 往返次数: 0-RTT(早期数据)
消息数量: 最多 9 条 消息数量: 5 条 消息数量: 7 条
身份认证: 证书签名 身份认证: PSK 身份认证: PSK
前向保密: 完整(DHE) 前向保密: 完整(PSK+DHE) 前向保密: 仅 1-RTT 数据
重放保护: 完整 重放保护: 完整 重放保护: 需额外机制
() = 明文 {} = 握手密钥加密 [] = 应用密钥加密 * = 可选消息
从这三张并列的时序图中,我想分享一个常被忽视的观察:TLS 1.3 握手简化带来的安全增益,可能比其密码学改进本身还要深远。TLS 1.2 的全握手涉及至少十几种消息组合和复杂的状态机转换——ChangeCipherSpec 的时序、重协商的嵌套、多种密钥交换模式的笛卡尔积——每一个额外的状态都是一个潜在的实现漏洞。POODLE、Lucky Thirteen、FREAK、Logjam——这些攻击无一不是从协议状态机的复杂性缝隙中钻出来的。TLS 1.3 将状态数压缩到最小,不是因为极简主义的美学偏好,而是因为每一个被移除的状态,都是一个永远不会再被利用的攻击面。这或许是安全协议设计中最反直觉的教训:真正的安全提升,往往不是来自增加新的防御机制,而是来自移除不必要的复杂性。
TLS 1.3 加密了大部分握手消息,但 ClientHello 中的服务器名称指示(SNI)扩展仍以明文传输。SNI 告诉服务端客户端想要连接的域名,以便在同一 IP 地址托管多个 HTTPS 站点时选择正确的证书。然而,这也意味着网络路径上的任何观察者——ISP、企业防火墙、政府审查机构——都可以看到用户正在访问哪个网站,即使无法看到具体内容。
SNI 泄露带来的隐私威胁是具体而严重的。首先,它使得基于域名的流量审查成为可能:中国的防火长城(GFW)通过深度包检测(DPI)读取 TLS ClientHello 中的 SNI 字段,精确阻断对特定域名的访问,而无需解密任何加密流量;伊朗的网络审查系统同样利用 SNI 过滤来封锁社交媒体和新闻网站。其次,即使不实施封锁,ISP 和网络运营商也可以通过记录 SNI 构建用户的完整浏览画像——用户访问了哪些医疗网站、政治论坛、宗教团体——这些元数据的敏感程度不亚于通信内容本身。再者,企业网络中的中间设备可以基于 SNI 实施选择性拦截或降级,例如阻止员工访问特定云服务或强制将流量导入解密代理。
从协议设计的角度看,SNI 明文传输是 TLS 1.3 在隐私保护上最显著的遗留缺口:加密了握手的几乎所有内容,却将用户的目标域名暴露给了整条网络路径。这一缺口的讽刺之处在于,TLS 1.3 越是成功地加密了其他握手字段,SNI 就越成为唯一可用的监控锚点——攻击者的注意力被集中到了这个最后的明文字段上。
IETF 最初提出了加密 SNI(Encrypted
SNI,ESNI)方案,通过将 SNI 字段用服务端发布在 DNS
中的公钥加密来保护域名隐私。然而,仅加密 SNI 而留下其他
ClientHello 字段(如
ALPN、supported_versions、key_share
的群选择)明文传输,可能泄露足够的侧信道信息来推断目标域名——例如,一个特定的
ALPN 协议列表组合可能全网仅有少数几个站点使用。因此,ESNI
演进为 ECH(Encrypted Client Hello),将整个 ClientHello
分为外层(ClientHelloOuter)和内层(ClientHelloInner)。
ECH 的工作流程如下:客户端首先通过 DNS 查询目标域名的
HTTPS 资源记录(HTTPS RR),从中获取服务端发布的 ECH
公钥配置(ECHConfig)。随后,客户端构造两份
ClientHello:内层(ClientHelloInner)包含真实的
SNI、所有敏感扩展和完整的握手参数;外层(ClientHelloOuter)包含一个不敏感的”掩护”
SNI(通常是 CDN 提供商的域名,如
cloudflare-ech.com),并携带使用 HPKE(Hybrid
Public Key Encryption)加密的内层密文。服务端收到外层
ClientHello 后,使用对应的 ECH 私钥解密内层,按真实 SNI
选择证书并完成握手。对网络观察者而言,所有经过该 CDN
的连接看起来都在访问同一个掩护域名,真实目标域名被完全隐藏。
ECH 的安全性依赖一个关键前提:ECH 公钥必须通过安全通道分发。如果 DNS 查询本身以明文传输(传统 UDP DNS),攻击者可以篡改 HTTPS 资源记录、替换 ECH 公钥,从而解密内层 ClientHello——隐私保护形同虚设。因此,ECH 在实践中强依赖 DNS over HTTPS(DoH)或 DNS over TLS(DoT)来保护 DNS 查询的完整性和机密性。这意味着 ECH 的隐私保障建立在整个链路(DNS 查询 + TLS 握手)的安全性之上,任何环节的明文暴露都会削弱最终效果。
密钥轮换同样是部署中的关键环节。ECH 私钥需要定期更新以限制密钥泄露的影响范围,但 DNS 记录存在缓存延迟——客户端可能在密钥轮换后仍持有旧的 ECHConfig。为此,ECH 协议设计了优雅的回退机制:如果服务端无法解密内层 ClientHello(因密钥不匹配),它会在握手中返回最新的 ECHConfig,客户端据此重试连接。这一”先尝试、再更新”的策略在密钥轮换期间维持了连接的可用性。
此外,ECH 的大规模部署还面临若干工程挑战:它要求 CDN 提供商(如 Cloudflare、Fastly)在边缘节点统一支持 ECH 并管理密钥轮换;客户端操作系统和浏览器需要更新 TLS 栈以支持 ECH 协商;某些国家的网络审查系统可能通过直接封锁所有携带 ECH 扩展的 ClientHello 来对抗 ECH 部署,将隐私对抗从协议层面升级为可用性对抗。尽管如此,Cloudflare 已于 2023 年在其全球网络上默认启用 ECH 支持,Chrome 和 Firefox 也已实现客户端支持,ECH 正从实验走向实际部署。
TLS 1.3 是历史上受形式化验证检验最为深入的网络协议之一。在协议设计阶段,研究者使用 Tamarin Prover 和 ProVerif 等符号化验证工具对握手协议的安全属性进行了建模与证明,包括认证性(authentication)、机密性(secrecy)、前向保密(forward secrecy)和抗降级性(downgrade resistance)。这些验证覆盖了各种模式组合——全握手、PSK 恢复、0-RTT——并发现了若干早期草案中的微妙缺陷,推动了协议的修正。此外,密码学研究者还对 TLS 1.3 的密钥调度进行了计算安全性证明(computational proof),在标准密码学假设下证明了其满足多阶段密钥交换(multi-stage key exchange)的安全定义。
形式化方法的引入代表了协议工程的范式转变:从”设计→实现→发现攻击→打补丁”的被动循环,转向”设计→验证→实现”的主动防御。TLS 1.3 的成功经验正在影响其他协议的设计,例如 MLS(Messaging Layer Security)和 Noise 框架。
量子计算机对 TLS 的威胁主要体现在两个方面:Shor
算法可以攻破基于整数分解和离散对数的密钥交换与签名算法(RSA、ECDH、ECDSA);Grover
算法对对称密钥算法的安全强度减半(AES-128 降至 64
位安全)。TLS 1.3
的模块化设计为后量子迁移提供了良好的基础:密钥交换算法在
supported_groups 和 key_share
扩展中协商,签名算法在 signature_algorithms
扩展中协商,两者均可独立替换为后量子方案。
目前,IETF 正在标准化混合密钥交换(hybrid key exchange)方案,将传统的 ECDH 与后量子 KEM(如 ML-KEM,前称 Kyber)组合使用,确保即使其中一个算法被攻破,连接仍然安全。混合方案的 ClientHello 会携带更大的 key_share 扩展,可能面临 UDP 分片或 TCP 初始窗口不足等工程挑战。后量子签名算法(如 ML-DSA,前称 Dilithium)的签名和公钥尺寸也显著大于 ECDSA,对证书链传输和握手延迟构成压力。
TLS 的未来注定是一场在安全性、性能、兼容性和隐私之间持续博弈的演化过程。从 SSL 2.0 的草创到 TLS 1.3 的精炼,再到 ECH 的隐私增强与后量子的算法迁移,每一步都在回应同一个核心命题:如何在开放的互联网上建立可信赖的安全通道。协议设计者二十余年积累的教训——密钥分离、前向保密、最小化可选性、转录绑定、形式化验证——构成了这一领域最宝贵的工程智慧,也将继续指引下一代安全传输协议的演进方向。
密码学百科系列 · 第 22 篇
把当前热点继续串成多页阅读,而不是停在单篇消费。
2027-03-25 · cryptography
从 SM3/SM4/SM2 的设计对比到国密 TLS 握手、生态落地、PQC 迁移——国密技术的完整知识图谱。
2026-04-04 · cryptography
加密和认证必须同时进行——本文深入 AEAD 的形式化安全定义,逐步拆解 AES-GCM 的 GHASH 有限域乘法、ChaCha20-Poly1305 的组合构造,以及 nonce-misuse resistant 设计
2026-04-04 · cryptography
从共享秘密到可用密钥,从用户密码到安全存储——本文系统解析密钥派生与密码哈希的设计原理,深入 HKDF 的 Extract-Expand 范式与 Argon2 的内存困难设计
2026-04-04 · cryptography
PKI 是互联网信任的基础设施——本文从 X.509 证书结构深入到信任链验证,剖析 CA 失败案例、证书透明度机制,以及 Let's Encrypt 如何改变了证书生态
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。