2. TLS协议基础:握手过程介绍
聊TLS握手之前,我想先问个问题:你平时用微信或者网银的时候,有没有想过那层层包裹的“小锁”到底干了啥?说白了,TLS握手的任务就三个——证明身份、商量密码、保证安全。咱们今天要讲的,正是这个握手过程的底层逻辑。我把这套逻辑画成了一张流程图,你先瞄一眼,接下来咱们逐段拆解。
2.1 握手到底在握什么?
我第一次接触TLS握手时,觉得这就是个“Hello → 证书 → 密钥 → 加密”的流水线。其实不是这么简单。**握手本质是在不安全信道上建立一个安全通道**。听起来像变魔术,对吧?核心靠三个机制:
- 身份认证——服务端必须证明自己就是那个证书持有者(通常靠CA签名链 + 私钥签名)
- 密钥协商——客户端和服务端通过公开信道算出只有双方才知道的会话密钥
- 安全参数确认——双方确认使用哪个协议版本、哪些密码套件、是否开启扩展
记住一点:握手过程的所有明文消息都是可被监听的,但协商出来的密钥对方拿不到。这是整个密码学的精妙之处。
2.2 经典 TLS 1.2 握手步骤
咱们先看最经典的 TLS 1.2,完整握手需要 2 个 RTT(往返时间)。流程大致是这样:
- ClientHello:客户端发一个随机数、支持的密码套件列表、TLS版本
- ServerHello + Certificate + ServerKeyExchange + ServerHelloDone:服务端选中密码套件、发证书、发密钥交换参数
- ClientKeyExchange + ChangeCipherSpec + Finished:客户端发送自己的密钥交换参数,切换密码,发Finished
- ChangeCipherSpec + Finished:服务端也切换密码,确认握手完成
嗯,这中间每个消息都有验证工作。之前我在一个项目里遇到过 握手延迟过高的问题——原因竟然是服务端每次握手都重新生成临时 DH 密钥对,导致加解密计算把 CPU 打满了。后来改成预计算 + 缓存,延迟降了一半。
| 消息 | 方向 | 内容 |
|---|---|---|
| ClientHello | Client → Server | 随机数、密码套件列表、扩展 |
| ServerHello | Server → Client | 选中的套件、服务端随机数 |
| Certificate | Server → Client | 服务端证书链(X.509) |
| ServerKeyExchange | Server → Client | DH/ECDH 参数或签名(必要时) |
| ClientKeyExchange | Client → Server | 客户端的 DH/ECDH 参数 |
| ChangeCipherSpec | 双向 | 通知对方开始用新密钥 |
| Finished | 双向 | 握手消息的 HMAC 校验 |
个人经验:实战中 Certificate 消息是握手体积的“大头”,尤其当证书链包含多个中间 CA 时。我用 Wireshark 看过一个 PKI 体系的握手,证书链 12KB……直接拖慢了首屏加载。
2.3 TLS 1.3 握手——更简洁
TLS 1.3 把握手压缩到 1 个 RTT。怎么做到的?秘诀是:密钥协商和证书验证并行。客户端在 ClientHello 里直接把自己的密钥交换参数发过去(key_share),服务端在 ServerHello 里直接把证书 + 签名 + 服务端密钥参数一块儿发回来。算下来省掉了一次往返。
当然,这么做的代价是 协议兼容性变差——如果服务端不支持客户端选的参数,就必须回退到 HelloRetryRequest。我曾经在对接一个第三方支付平台时碰到过这个坑:他们的老服务器不支持 TLS 1.3 的 x25519 曲线,我这边一发 key_share 就被弹回来重试,多了整整一个 RTT……
避坑提示:国密改造时如果直接套用 TLS 1.3 框架,注意 SM2 签名的 key_exchange 消息放什么位置。目前 GMTL 国密规范倾向于放在 ServerHello 之后的一个独立 Extension 里,而不是直接塞 key_share。
2.4 握手完成之后——密钥导出
握手不光是交换消息,更重要的是:双方必须从共享的秘密中派生出最终加密密钥。这个推导过程叫做密钥导出(Key Derivation)。
在 TLS 1.2 中,步骤大致是:
- 用 pre_master_secret 和客户端/服务端随机数,通过 PRF 算出 master_secret
- 再用 master_secret 算出 6 个密钥:客户端读密钥、客户端写密钥、服务端读密钥、服务端写密钥 + 两个 IV
换成国密后,PRF 换成 SM3 的 HMAC,pre_master_secret 通过 SM2 密钥交换协议算出。具体我们后面章节会讲,这里你先有个印象:流程框架不变,密码学原语替换。
// 伪代码示意:TLS 1.2 密钥导出(非国密版本)
pre_master_secret = DH_shared_secret(client_priv, server_pub)
master_secret = PRF(pre_master_secret, "master secret", client_random + server_random)
key_block = PRF(master_secret, "key expansion", server_random + client_random)
// 然后按长度分割 key_block 得到各个密钥
2.5 关于 ALPN 与 SNI——你可能忽略的扩展
握手里有两个扩展,对现代互联网至关重要:
- SNI(Server Name Indication):客户端告诉服务端它想访问哪个域名,服务端据此返回对应的证书。我在调试线上问题时发现,如果没有 SNI,Nginx 只能返回默认证书,导致浏览器报证书不匹配。
- ALPN(Application-Layer Protocol Negotiation):握手阶段直接约定上层用什么协议,比如 h2 表示 HTTP/2。我曾经在迁移国密 HTTPS 时忘开 ALPN,结果浏览器始终走 HTTP/1.1,白白浪费了 0-RTT 的好处。
总结一下:TLS 握手就是个“互相试探 + 谈妥条件 + 交换密钥”的过程。国密改造对握手流程的影响主要在密码套件、密钥交换方式和证书格式上,流程骨架几乎原样保留。
你想想看,咱们后面章节要讲的那些 SM2/SM3/SM4 替换,就是在图中的每一条消息里做“换芯手术”。