一次合成订单访问结束,浏览器很快又连同一服务。TLS 1.3 可以利用旧连接留下的恢复材料,避免每次都完整重复此前的证书握手;更激进的做法是在服务端回应本次 ClientHello 前发送应用字节。查询商品和创建付款订单不能因为都叫“HTTPS 请求”就无差别提前。恢复、PSK-only、PSK-DHE、0-RTT指向不同决策:旧秘密是否被复用,本次是否加入新的临时 DH,以及握手确认之前是否让业务处理早期消息。

本篇实测了一次 TLS 1.3 普通握手,再借内存里的票据成功恢复一次 PSK-DHE 握手;环境没有真实运行 0-RTT 或 PSK-only。没跑过的分支只据 RFC 9846 讨论规则,仍留作 NOT_RUN。

票据把上一轮带到了下一轮

第一次证书认证路径已把预期 orders.test 的身份与本次握手绑定。握手后,服务端可发 NewSessionTicket;客户端保存与票据相关的恢复材料。下一次 ClientHello 提供相应的 pre_shared_key 身份,服务端若接受它,双方才可用先前握手派生的 PSK 引导新的密钥协商。这里的预共享密钥并非把原会话应用流量密钥直接再次用于所有新消息,也不能让浏览器无条件相信任何自报的票据来源。

1
2
3
4
5
6
首次证书认证握手 --> 会话得到恢复材料 --> NewSessionTicket
|
后续 ClientHello:PSK 身份 + 可选 key_share -------> 服务端
| 按票据状态/模式选择
ServerHello:选择 PSK [+ 新 key_share] <-----------+
两边以被选择的输入继续派生密钥

本章的 SSLSession 包含敏感恢复材料,仅在同一次实验进程中传给第二次连接,不把票据原文、PSK 或会话秘密提交 Git 或展示在博文里。观察回来的布尔字段 session_reused 要与 ClientHello、ServerHello 的实际扩展选择一同解读:只看客户端“愿意恢复”不能证明服务器真的选用了 PSK。

新 DHE 是否参与,改变了前向保密边界

在 PSK-only 路径,后续会话没有新临时 DH 输入;若用于恢复的秘密在相关条件下泄漏,就缺少本轮独立 DH 为该会话提供的额外前向保密。PSK-DHE 在原有 PSK 之外还选中新的 key share,为正常握手后传输的数据带来这一轮临时交换的保护条件。两条路径都要先信任旧安全上下文并管理票据生命周期;“带 DHE”也不等于凭空给错误身份、公用弱 PSK 或失陷端点补救。

1
2
3
PSK-only:旧恢复秘密 -------------------> 派生这次会话状态
PSK-DHE:旧恢复秘密 + 本次临时交换 ------> 派生这次会话状态
↑ 必须检查服务端确实选了 key_share

前向保密讨论的也是在什么时间、什么秘密泄漏后,对以往传输的影响,而不是万能的“泄漏了也安全”承诺。实际握手仍需要正确的协议参数、TLS 端点和业务信任;16 的记录层认证加密只有在那些条件满足时才对实际记录发挥作用。

0-RTT 不是 PSK-DHE 正常流量的同义词

0-RTT 早期数据在服务端完成当次握手确认之前发送。RFC 9846 明确警告:早期数据不具备正常 1-RTT 数据可能具有的前向保密保证,也不保证跨连接不可重放。随后一次恢复握手若选择了 PSK-DHE,不能把“后来有了新 DHE”反向套在此前发出的早期订单上。对于有副作用的支付、下单等接口,应由协议配置、服务端反重放措施与应用业务状态共同决定是否允许提前处理,不能看到 TLS 图标就默认安全。

这同 16 对复制旧记录的实测并不矛盾,攻击对象不同:

1
2
3
旧 1-RTT 密文在同一连接重复投递 -> 本方向序号/nonce 已变化,记录拒绝
0-RTT 数据在新的恢复连接被重放 -> 跨连接早期数据风险,需单独处理
合法用户用新记录再发一次订单 -> 通道正确,业务仍要做消费检查

即使服务端按 RFC 提供单次票据、记录 ClientHello 或检测新鲜度,集群间状态是否共享、应用是否幂等仍要验证;不能把某个单实例的否定结果外推成跨连接零风险。本篇没有一个真实端点发送/重放早期订单,因此不能把这部分写成已运行的防重放实验。

实测的分支到底是什么

examples/cryptography/17_resumption.py 沿用 14 的临时合成 CA 与证书,仅在 MemoryBIO 中进行两次 TLS 1.3 握手。第一次拿到票据,第二次带着仅存于内存的 SSLSession。脚本只解析未加密 ClientHello 和 ServerHello 中的扩展类型号:pre_shared_key=0x0029、key_share=0x0033、early_data=0x002a。不会把恢复秘密或票据值写入公开证据;内部真实协议组件的计算交给 OpenSSL。

1
2
python3 examples/cryptography/17_resumption.py
python3 -m unittest discover -s examples/cryptography -p 'test_17*.py' -v

2026-10-06 UTC,Python 3.12.3 / OpenSSL 3.0.13 上两条命令退出码都为 0,1 个测试通过。第一次 session_reused=false,第二次 true;第二次客户端提供 PSK 扩展,服务端同时选择 PSK 与 key share——因此实测的是 PSK-DHE 恢复。没有 early_data 扩展,故没有任何 0-RTT 数据进入这次实验;也没有实际协商 PSK-only。即便带着旧票据,若服务端没有选择 key share,也不能把那一分支改名为 PSK-DHE。

小型 MemoryBIO 测试没有公网 RTT、服务扩容状态或浏览器退化路径,不能据此杜撰“恢复快了多少毫秒”或“0-RTT 零重放”。测试里的临时 CA 只给指定客户端,执行后清理;它属于实验隔离,不是生产根存储配置。

按五问卡检查安全目标

如果网络攻击者能捕获早期数据后跨连接重放,它想要的不是反推出密钥,而是让服务端重复执行一笔本来有效的操作。PSK-only 不加入新 DH 时,恢复秘密生命周期尤其重要;PSK-DHE 虽加入临时 share,也没有自动给0-RTT增加它之前不存在的 DHE 输入。若删除应用订单唯一值与原子消费状态,合法用户主动重发新连接订单仍可能成功;若删除 TLS 证书或原身份检查,旧 PSK 的身份归属亦可能被错误使用。删掉每一步都产生不同可行攻击。

注意“0-RTT 是否可用”不是“服务端握手缩短了一点”的同义词。要在本仓库未来把它标记为实验通过,必须找到真实支持早期数据的端点,保存合成早期请求、网络和服务器输入配置、返回及服务端处理证据、跨连接失败对照,并明确重放防护的状态范围。没有这些证据就保留 NOT_RUN。

两道带答案的练习

画图题。 对 PSK-only、PSK-DHE 的 1-RTT 和 PSK-DHE 握手中的 0-RTT,分别画出派生输入。哪一条早期数据不能因后续加入 DHE 就“事后得到前向保密”?

可核对答案: PSK-only 只有旧 PSK;PSK-DHE 后续正常流量有旧 PSK 加本次 DHE。0-RTT 数据发送时服务端尚未提供这次 DHE 的对应输入,仍以 PSK 为依赖,因此无法反向获得 DHE 的保证,并需单独处理跨连接重放。不能把 16 的同连接密文复制实验当成早期数据测试。

变更题。 把 17_resumption.py 第二次 connected 调用的 session 删除,再跑脚本与测试。哪两个关键断言会失败?直接将输出字段 early_data_extension_offered 改为真,能否宣称做过 0-RTT?

可核对答案: 第二次不再复用旧票据,second_connection_reused_ticket 和 resumed_server_selects_psk 应失败;没有实际早期数据发送、处理和重放输入,仅改布尔值是伪证据,不会让 0-RTT 的 NOT_RUN 消失。

资料与导航