地址栏那把锁,背后只谈了 6 句话。
我在浏览器里把 TLS 1.3 握手拆成 6 步,一步一步跑完:50ms 延迟下总耗时 63.6ms,密码学计算只占 2.6ms,双方最终拿到同一把会话密钥。
把其中一条消息里的公钥换掉,第 5 步立刻报 OperationError,握手停住。

在线案例与源码:gitcode.com/OPCTeam/dem… 成品只有一个 index.html,双击就能打开,不连网、不装依赖。
一、先看结果:6 步、1004 字节、63.6 毫秒
这 6 步不是画出来的流程图,每一步都真的生成了随机的密钥和报文。
| 步骤 | 消息 | 实测耗时 | 字节数 |
|---|---|---|---|
| 1 | ClientHello | 0.2 ms | 254 B |
| 2 | ServerHello | 0.3 ms | 254 B |
| 3 | EncryptedExtensions + Certificate | 1.3 ms | 182 B |
| 4 | CertificateVerify + Finished | 0.5 ms | 140 B |
| 5 | 客户端 Finished | 0.7 ms | 76 B |
| 6 | 应用数据 | 0.4 ms | 98 B |
6 步加起来 1004 字节。
页面顶部的 RTT 滑块放在 50ms,总耗时读数 63.6ms。
50ms 是模拟的一次往返,2.6ms 是 ECDH、HKDF、AES-GCM、ECDSA 四种运算加起来的真实耗时,剩下约 11ms 是 setTimeout 和页面渲染的固定开销。
这三个数对不上才奇怪,因为浏览器里的计时器本来就有几毫秒的粒度。
二、为什么值得看:能跑的文字教程太少
搜“TLS 1.3 握手”,出来的大多是时序图和一大段名词解释。
图能看懂,但没人告诉你 ClientHello 到底装了多少字节,也没人让你亲手把 key_share 换掉看它怎么失败。
我把这件事做成了一个单页应用,规则很简单:页面上出现的每一个十六进制字符串,都必须由 WebCrypto 当场算出来。
不是预埋的假数据,刷新一次,公钥和共享密钥全变。
这就是它和普通演示页的区别。
三、密钥到底怎么换:ECDH 只负责一半
第一步,客户端和服务端各生成一对 ECDH P-256 密钥。
双方交换公钥,各自用私钥做一次 deriveBits,得到 32 字节的共享秘密。
页面里两行十六进制必须一模一样,旁边会亮一个“一致”的绿标。
但共享秘密不能直接拿去当 AES 密钥。
TLS 1.3 中间还夹了一层 HKDF-SHA256:把共享秘密、双方随机数和固定的 info 字符串喂进去,派生出四条方向的密钥。
- 客户端→服务端 Handshake Key
- 服务端→客户端 Handshake Key
- 客户端→服务端 Application Key
- 服务端→客户端 Application Key
每条 16 字节,对应 AES-128-GCM。
为什么分方向?因为两端用的密钥必须错开,否则一方加密、另一方拿同一把钥匙解密,重放和反射攻击就有了空间。

第三步是证书链。
页面现场生成 Root CA、Intermediate CA 和站点证书三对 ECDSA P-256 密钥。
Root 自签名,Intermediate 由 Root 签,站点证书由 Intermediate 签。
验签也是真的:用上一级公钥去 verify 下一级的签名,三条全过,面板显示 3/3。

四、验证与踩坑:两个真 bug
第一版跑起来时,前四步都绿,第 5 步直接红。
日志只有一行:服务端 Finished 解密失败,OperationError。
原因不复杂:客户端拿自己的 Handshake Key 去解服务端加密的数据。
TLS 1.3 的双向密钥是分开的,客户端加密用 c2s,解密要用 s2c。
第一版只派生了两把钥匙,方向一错,AES-GCM 的认证标签必然对不上。
修法是把四条密钥都派生出来,加密用自己方向的那条,解密用对方方向的那条。
第二版改完,握手跑通。
第二个坑更隐蔽。
自检里有一条“篡改签名后验签失败”,结果它连原签名都判失败。
排查后发现,代码里写的是 new Uint8Array(sig)。
这不是复制,是视图。
后面的 tampered[flipPos] ^= 0xFF 改的是同一块内存,原签名跟着一起坏了。
改成 sig.slice(0) 先复制一份,再翻转字节,原签名通过、篡改签名失败,断言才真正成立。
这两个 bug 都不是排版问题,是密码学代码里最容易骗过眼睛的那种:界面看起来正常,逻辑已经反了。
改完后页面内 9 条自检全过:共享密钥一致、HKDF 两端一致、证书链 3/3、篡改验签失败、每次 ECDH 密钥不同、AES-GCM 往返恢复原文、篡改密文认证失败、正常签名通过、不同共享密钥派生结果不同。
五、三种模式的账:1 RTT、2 RTT、0 RTT
页面右上角可以切三种模式,RTT 滑块从 10ms 到 200ms。
同一个页面、同一套密码学运算,只改往返次数。
| 模式 | RTT 次数 | 50ms 下总耗时 | 150ms 下总耗时 | 计算耗时(50ms 档) |
|---|---|---|---|---|
| TLS 1.3 | 1 | 63.6 ms | 160.8 ms | 2.6 ms |
| TLS 1.2 | 2 | 113.4 ms | 313.1 ms | 2.8 ms |
| 会话复用 | 0 | 10.1 ms | 10.4 ms | 2.2 ms |
50ms 这一档,TLS 1.3 比 TLS 1.2 少 49.8ms,差不多正好省下一次往返。
150ms 这一档差距拉到 152.3ms,因为往返越贵,少一次往返越值钱。
0-RTT 那栏看着最夸张,10ms 出头,但它量的是复用连接后的数据阶段,不是第一次访问。
这里有个边界要说清楚:三种模式共用同一套密钥交换和证书链演示,所以“握手总字节”都是 1004 字节。
真实的 0-RTT 不会重做证书链,字节数会小得多;页面里的 1004 是演示实际构造的报文总量,不是线上 0-RTT 的握手大小。

六、失败路径:换掉公钥,握手就断
点“中间人攻击”,再点“开始握手”,页面会模拟攻击者替换 ServerHello 里的 key_share。
客户端用自己的私钥和攻击者的公钥算,服务端用另一套,两个共享秘密当场对不上。
第 5 步解密服务端 Finished 时抛 OperationError,时间轴变红,状态栏写“握手失败 — 中间人攻击”,总耗时停在 61.4ms。
再点“篡改密文”,页面会翻转应用数据密文中间的一个字节。
这次是第 28 字节,0x16 改成 0xe9。
AES-GCM 解密直接报认证失败,第 6 步标红,页面提示“篡改检测成功 — AES-GCM 认证失败”。
一个断在密钥协商,一个断在数据完整性,两条失败路径都能复现。

七、准备环境:进入码道 Web 与本地复现
下载仓库里的 index.html,双击打开即可。
本地复跑 9/9 通过,9 条断言全绿。
也可以在当前目录起一个静态服务:python -m http.server 8000,再打开 http://localhost:8000/index.html。
不需要 npm,也不会发出任何外部网络请求。
按这个顺序点,能复现本文所有数字:
- RTT 滑块保持 50ms,点“开始握手”,看 6 步耗时和总耗时 63.6ms。
- 切到“指标对比”,依次跑 TLS 1.2 和会话复用,看三行对比。
- 点“运行自检”,9 条断言应全绿。
- 点“中间人攻击”再点“开始握手”,第 5 步应报 OperationError。
- 点“篡改密文”再点“开始握手”,第 6 步应报 AES-GCM 认证失败。
浏览器用 Chrome 或 Edge 90 以上都行,WebCrypto 的 ECDH、HKDF、AES-GCM、ECDSA 都是原生支持。
想改需求重新生成,可以打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source…
八、它不是什么
这不是一个 TLS 实现,只是一间把协议拆开看的实验室。
页面没有真实网络、没有 TCP、没有抓包,也没有解析真实的 X.509 证书;证书对象是页面按字段构造的,签名和验签用的是真实 ECDSA。
RTT 是 setTimeout 模拟的,不是线上测量值。
CPU 调度和垃圾回收也会影响读数,所以同一档位多跑几次,总耗时会有几毫秒浮动。
0-RTT 只做了时间账,没有实现会话票据和 PSK 复用。
把它当成理解握手顺序和密钥方向的教具可以,别拿它当安全评估工具。
九、使用码道体会
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
需求里我把“必须用 WebCrypto 真算”写死了,生成出来的第一版确实用了真的 ECDH 和 AES-GCM,但密钥方向错了。
有意思的是,它自己跑测试时发现了这个错,日志里写着“客户端用 clientHandshakeKey 解密服务端数据,key 不匹配必然失败”,然后整份重写。
第二个视图和副本的坑,也是它自己定位到 new Uint8Array(sig) 这一行。
我要做的不是替它写代码,而是把验收条件写清楚:哪些数必须一致,哪条路径必须失败,失败时页面要显示什么。
条件写清楚了,返工就少。
十、下一步
这份页面还缺三块:TLS 1.2 的真实报文结构、会话票据的完整复用流程、以及从 pcap 文件导入一次真实握手做对照。
把这三块补上,它就从“教具”更接近“观察工具”。
如果你也在做类似的协议可视化,欢迎拿这份单文件改,仓库地址在文章开头。