计算机网络 25:少一次握手付出了什么,会话恢复、0-RTT 与重放边界
第二次 TLS 连接显示 Reused,是否意味着没有握手?客户端指定了早期数据文件,是否意味着服务器已经接受它?恢复成功、早期数据被接受、应用处理完成,是三个需要分别观察的结果。
第 24 篇建立了证书与服务身份验证。本篇保留专用测试 CA,在新建连接之间传递会话材料,比较完整握手、普通恢复和带早期数据的恢复。实验只运行本机 OpenSSL,不访问真实业务接口。
恢复的是安全上下文,不是旧 socket
RFC 8446 §2.2允许从先前连接建立恢复所用的预共享密钥关系。后续连接可以提出相应 PSK 身份,服务器接受后,新连接的安全上下文与先前认证建立的上下文相联系。
客户端仍然新建连接、发送 ClientHello 并处理服务器响应。它不是继续使用旧 TCP 序号空间,也不是把旧的加密字节直接接到新连接上。普通恢复不能只凭“第二次访问”或“耗时较小”判断,必须检查实现实际选择了何种握手路径。
PSK 可以结合新的临时密钥交换,也可以按协议允许的其他模式使用。是否包含新的共享秘密会影响前向保密性质,不能把所有恢复统称为“安全属性与完整证书握手完全一致”。本篇只报告所选实现的结果,不从 Reused 一个词推导全部密钥交换细节。
票据在握手后到达,保存成功也不保证可恢复
RFC 8446 §4.6.1定义 NewSessionTicket。服务器可以在收到客户端 Finished 后发送这类消息,因此客户端在主握手完成瞬间退出,可能还没有处理可供后续使用的票据。
会话文件存在,只能说明本地保存了某些状态。服务器还可能因为票据寿命、密钥变化、策略或兼容条件拒绝恢复。反过来,即使客户端请求恢复,连接最终也可能成功完成完整握手;“连接成功”与“恢复成功”不能互相替代。
本篇使用 OpenSSL 的 -sess_out 保存会话,再通过 -sess_in 提供给后续连接,并读取日志中的 New 或 Reused。会话文件含秘密材料,不能作为普通调试附件公开。脚本将它们与测试私钥一起放入临时目录,公开结果仅保留允许的状态和合成文本。
0-RTT 提前的是部分应用数据
RFC 8446 §2.3中的 0-RTT 允许在具有合适 PSK 的前提下,把部分应用数据放进首次发送的数据中。对于本篇的恢复路径,需要先前连接留下的会话材料及早期数据许可,不能把没有这些前提的首次普通访问写成“直接 0-RTT”。协议也允许外部配置 PSK,本篇没有使用该模式。
0-RTT 不是零耗时,也不是整个连接没有握手。网络传播、服务器处理与响应返回仍然存在;只是客户端可以在收到本次服务器握手消息前发送被允许的应用数据。也不是所有恢复都发送早期数据,普通恢复与早期数据接受必须分列。
1 | |
上图是机制示意,不是耗时测量。是否节省一次网络等待,还取决于比较对象、应用发送时机和是否发生重试;本机三次连接不足以给出公网延迟收益。
加密不能消除跨连接重放
早期数据的密钥来自 PSK,尚未包含本次 ServerHello 提供的新鲜输入。RFC 8446 §2.3 与 §8明确区分它与普通 1-RTT 数据的性质:早期数据不具备相同的前向保密与跨连接不可重放保证。
攻击者不一定需要解密数据才能再次发送已经捕获的数据。如果同一业务操作在另一连接中被再次接受,可能重复产生副作用。重复读取一份固定测试文本与重复扣款的后果不同,选择可提前发送的操作必须考虑实际业务语义,而非只看数据已经加密。
服务端可以采用单次票据或记录 ClientHello 等措施限制重放,但多实例之间的状态一致性、重启窗口和客户端重试仍会影响结果。一个本地进程拒绝某张重复票据,不能证明整个分布式系统没有重复执行;本篇甚至没有执行原始密文重放,只演示早期数据接受。
实验服务显式开启 OpenSSL 的 anti-replay。它的具体票据处理属于该实现的行为,不替应用提供通用的恰好一次执行保证。合成载荷没有业务副作用,服务端打印它也不能证明真实业务事务完成。
TLS 拒绝早期数据与 HTTP 425 是不同结果
TLS 层拒绝早期数据后,连接仍可能走后续握手流程。是否重发、何时重发以及重发是否安全,需要应用的明确策略,不能把“最后握手成功”当作早期数据已经执行。
HTTP 另外定义了风险传递与拒绝语义。RFC 8470 §5.1中的 Early-Data: 1 表示请求曾经通过早期数据传递。中间代理不能通过等待当前连接握手结束,消除前一跳已经产生的重放风险。
§5.2的 425 Too Early 表示服务器不愿承担处理可重放请求的风险;对应重试不得继续通过早期数据发送。手工加一个 Early-Data 请求头不等于实际使用过 TLS 0-RTT,本篇传输的也不是 HTTP 请求,没有验收 425 或代理重试路径。
本机三个连接的证据
2026-09-20 的复跑使用 macOS 27.0 arm64、OpenSSL 3.6.2。服务器仅监听 127.0.0.1 的临时端口,客户端固定同一端点;证书使用专用临时 CA 与 server.lab.test 的 SAN。-verify_return_error 保留校验失败中止行为,-no-CApath -no-CAstore 关闭默认信任目录和证书存储。
| 连接 | 输入的会话材料 | 客户端实际摘要 | 早期数据 |
|---|---|---|---|
| 0 | 无 | New, TLSv1.3 | 未发送 |
| 1 | 连接 0 保存的 session0.pem | Reused, TLSv1.3 | 未发送 |
| 2 | 连接 1 新保存的 session1.pem | Reused, TLSv1.3 | accepted |
三次均报告 TLS_AES_256_GCM_SHA384。第三次输入的文本是 LAB25-EARLY-SAFE\n,共 16 字节;服务端摘要实际包含该文本。连接 2 使用连接 1 新发的票据,未把连接 0 已消费的票据重复用于早期数据。
服务端配置 -early_data -max_early_data 1024 -recv_max_early_data 1024 -anti_replay,允许并限制本实验早期数据。启用参数只是接受条件的一部分;客户端的 accepted 和服务端接收文本共同构成此次验收。OpenSSL 手册注明 -early_data 不能与 -www、-WWW、-HTTP 或 -rev 合用,因此这里使用普通测试服务模式。
客户端等待会话文件生成后关闭标准输入,以 -no_ign_eof 正常结束连接。三个客户端与服务器退出码均为 0,临时目录和本次子进程完成清理。早期试运行曾在取得票据后直接终止客户端,导致缓冲的摘要未输出、断言失败;附件记录这一实验驱动问题,成功结果取自修正后的完整运行。
复现与练习
1 | |
这些命令在下载附件后的目录执行,要求 Python 3.13 或更新版本,以及脚本常量 OPENSSL 指向的已安装 OpenSSL。其他机器可将该常量改成已有可执行文件的绝对路径,并先核对对应版本支持这些选项;脚本不会安装依赖。--self-check 只验证摘要白名单排除合成秘密,真实 TLS 证据来自不带该选项的运行。
每次运行重新生成短期测试证书。所有票据、私钥和未过滤的会话日志都留在临时目录;对外保存的是状态白名单和公开证书信息。脚本有启动、票据获取与进程等待超时,失败时终止本次拥有的进程。临时端口从选择到服务绑定存在间隔,发生冲突应作为本轮失败处理。
复现验收应同时找到 New → Reused → Reused、第三次 Early data was accepted、服务端合成文本与清理记录。缺少任何一项,都不能只凭命令执行过或票据存在宣称完成。本实验没有重复票据或密文重放场景,也没有测量节省了多少毫秒。
一次连接显示 Reused,但显示 early data was not sent,是否恢复失败?不是。这可以是普通恢复成功,早期数据根本没有发送。应分别检查两个状态。
若票据文件写出后下一次仍显示 New,是否应该直接关掉证书校验?不应该。票据接受与证书校验不是同一个开关,应先核对票据获取、连接关闭和服务端策略。
若本地服务端打印了早期文本,能否声称已经验证跨机房防重放?不能。该证据只覆盖单个受控进程收到这份文本,没有跨节点复制、故障重试或原始密文重放验证。
一手资料与验证边界
本篇当次核查 RFC 8446、RFC 8470,以及OpenSSL s_client和s_server手册。当前Python ssl 的 TLS 1.3 文档仍注明 session 接口的兼容限制及不支持早期数据,因此实验直接使用已有 OpenSSL CLI,不因属性存在而宣称支持。
实验只证明该版本、该配置、本机三连接中的握手选择与文本接收。未测公网耗时、HTTP 425、分布式防重放、票据轮换故障或业务幂等,也未抓取原始网络报文独立重建全部握手。




