3、TLS握手协议详解:四次握手流程、ClientHello/ServerHello、密钥交换与协商、会话票据
TLS握手协议,说白了就是客户端和服务器在正式通信前,互相确认身份、商量加密方式、交换密钥材料的过程。我刚开始接触嵌入式TLS时,总觉得握手就是“打个招呼、交换个证书”这么简单。直到有一次在资源受限的MCU上调试,握手过程反复超时,我才意识到——这四次握手,每一步都有坑。
今天我们就把这四次握手掰开揉碎,看看每一轮到底在干什么。
3.1 四次握手流程概览
标准的TLS 1.2握手,一共四个阶段。我习惯把它们叫做“打招呼、换证书、算密钥、确认收工”。
核心要点:四次握手的本质是“先认证、再协商、后加密”。认证靠证书,协商靠Hello消息,加密靠密钥交换算法。
先看一张流程图,把整体脉络理清楚:
嗯,这张图基本把骨架搭出来了。下面我们逐轮拆解。
3.2 第一轮:ClientHello
握手的第一步,客户端先开口。ClientHello消息里包含什么?我列一下关键字段:
- TLS版本:客户端支持的最高版本,比如TLS 1.2
- 随机数(Client Random):32字节,其中4字节是时间戳,28字节是随机值。这个随机数后面要参与密钥派生
- 密码套件列表:客户端支持的加密算法组合,按优先级排序
- 扩展字段:比如SNI(服务器名称指示)、ALPN(应用层协议协商)
实战经验:我在嵌入式设备上调试时,经常遇到ClientHello发出去后服务端不响应。后来发现是密码套件列表太长,超过了MTU限制。建议嵌入式设备只保留3-5个最常用的套件,比如TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。
你想想看,客户端发这个Hello,其实就是在说:“我支持这些版本和算法,你选一个吧。”
3.3 第二轮:ServerHello + 证书 + ServerHelloDone
服务端收到ClientHello后,会做三件事:
- ServerHello:从客户端列表里选一个密码套件,同时生成自己的随机数(Server Random)
- 证书:把服务器证书链发给客户端。如果是双向认证,还会请求客户端证书
- ServerHelloDone:告诉客户端“我说完了,轮到你了”
这里有个细节——服务端选择的密码套件,决定了后续的密钥交换方式。比如选的是ECDHE,那服务端还会在Certificate消息之后,发一个ServerKeyExchange消息,带上ECDHE的临时公钥和签名。
注意:如果服务端选的是RSA密钥交换(不推荐),则不需要ServerKeyExchange。但RSA交换不支持前向安全性,我个人建议在嵌入式设备上优先用ECDHE。
我曾经在一个IoT项目中,服务端配置了RSA套件,结果设备上线后所有历史通信都能被破解——因为私钥一旦泄露,之前的会话全完蛋。后来全部切到ECDHE,才解决这个问题。
3.4 第三轮:ClientKeyExchange + ChangeCipherSpec + Finished
这一轮是客户端的主场。客户端收到服务端的证书后,先验证证书链的有效性。验证通过后,根据协商的密钥交换算法生成预主密钥(Pre-Master Secret)。
预主密钥的生成方式取决于算法:
| 密钥交换算法 | 预主密钥生成方式 | 前向安全性 |
|---|---|---|
| RSA | 客户端生成48字节随机数,用服务端公钥加密后发送 | 否 |
| DHE | 双方通过Diffie-Hellman参数计算共享密钥 | 是 |
| ECDHE | 双方通过椭圆曲线DH参数计算共享密钥 | 是 |
拿到预主密钥后,客户端用PRF(伪随机函数)派生出主密钥(Master Secret)。主密钥再进一步派生出会话密钥——包括加密密钥、MAC密钥和初始化向量。
然后客户端发送ChangeCipherSpec消息,告诉服务端:“我要切换加密套件了,后面的消息都是加密的。”紧接着发送Finished消息,这条消息是对整个握手过程的哈希校验,用来防止握手被篡改。
关键点:Finished消息是第一条加密消息。如果服务端能正确解密并验证哈希,说明密钥协商成功。否则握手失败。
3.5 第四轮:ChangeCipherSpec + Finished(服务端)
服务端收到客户端的Finished后,做同样的操作:
- 用相同的PRF和参数派生出主密钥和会话密钥
- 发送ChangeCipherSpec
- 发送Finished消息
客户端验证服务端的Finished后,握手正式完成。之后的所有应用数据,都用协商好的对称密钥加密传输。
说白了,第四轮就是服务端对客户端说:“我也准备好了,咱们开始加密通信吧。”
3.6 密钥交换与协商的底层逻辑
密钥交换是整个握手最核心的部分。我习惯把它拆成三个层次:
- 身份认证层:通过证书确认对方身份,防止中间人攻击
- 密钥材料交换层:交换DH参数或RSA加密的预主密钥
- 密钥派生层:用PRF从预主密钥、客户端随机数、服务端随机数派生出会话密钥
为什么需要两个随机数?你想想看,如果只用预主密钥,那每次会话的密钥都一样。加上客户端和服务端的随机数,就能保证每次会话的密钥都不同——这就是所谓的“会话唯一性”。
避坑指南:我曾经在移植mbedTLS时,发现随机数生成器(RNG)的熵源不够。结果每次握手生成的随机数都差不多,导致密钥空间严重缩小。后来改用硬件TRNG才解决。嵌入式设备上,RNG的质量直接影响TLS安全性,千万别用伪随机数糊弄。
3.7 会话票据:让握手更快
完整的四次握手开销不小——证书验证、密钥交换、多次网络往返。对于嵌入式设备来说,每次重连都做一次完整握手,功耗和延迟都受不了。
TLS提供了会话恢复机制,有两种方式:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Session ID | 服务端保存会话状态,客户端在ClientHello中带上Session ID | 实现简单 | 服务端需要维护会话缓存,扩展性差 |
| Session Ticket | 服务端将会话状态加密后发给客户端,客户端下次重连时带上 | 服务端无状态,扩展性好 | 需要服务端管理票据密钥 |
Session Ticket是RFC 5077定义的。服务端用只有自己知道的密钥,把会话状态(主密钥、密码套件等)加密成一个Ticket,发给客户端。客户端下次握手时,在ClientHello里带上这个Ticket。服务端解密后直接恢复会话,跳过证书交换和密钥计算。
实际效果:使用Session Ticket后,握手从4轮减少到2轮(ClientHello + ServerHello + Finished),延迟降低50%以上。对于电池供电的IoT设备,这个优化非常关键。
嗯,这里要注意——Session Ticket的加密密钥要定期轮换。我曾经见过一个项目,票据密钥一年没换,结果密钥泄露后所有历史会话都能被解密。建议每个月换一次,或者用HSM保护。
3.8 嵌入式环境下的握手优化建议
最后,结合我的经验,给嵌入式开发者几个实操建议:
- 精简密码套件:只保留TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256和TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,够用且安全
- 启用Session Ticket:减少握手次数,降低功耗
- 证书预置:把CA证书烧录到固件里,避免每次握手都下载证书链
- 调整超时时间:嵌入式网络不稳定,握手超时建议设为5-10秒,不要太短
- 使用硬件加速:如果MCU支持硬件加密引擎,优先用硬件做AES和ECC运算
我踩过最大的坑,就是没开硬件加速。一个ECDHE握手在软件实现下要跑2秒,开了硬件加速后降到50毫秒——差距就是这么大。