2. TLS握手协议基础:TLS 1.2与1.3握手流程对比、关键数据结构

聊到受限设备上的TLS优化,咱们得先搞清楚握手的底子。说白了,握手就是客户端和服务器互相确认身份、协商密钥的过程。我见过不少工程师一上来就调内存池、改加密库,结果发现握手本身的数据结构就占了大量堆空间——方向错了,白费功夫。

这一节,我带你过一遍TLS 1.2和1.3的核心握手流程,再拆开Certificate和ClientHello这两个最占内存的数据结构。嗯,这些都是后面做内存优化的基础。

2.1 TLS 1.2 握手流程:两轮往返的代价

TLS 1.2的握手,需要完整的两次网络往返(2-RTT)。在受限设备上,这不仅仅是延迟问题——每次往返都要维持状态缓冲区,内存开销不小。

基本流程是这样的:

  1. ClientHello:客户端发密码套件列表、随机数等。
  2. ServerHello + Certificate + ServerHelloDone:服务器选套件、发证书链、结束招呼。
  3. ClientKeyExchange + ChangeCipherSpec + Finished:客户端发密钥交换参数,切换加密,发验证消息。
  4. ChangeCipherSpec + Finished:服务器切换加密,发验证消息。

我早期在一个Zigbee网关项目里,就是用TLS 1.2。那个设备只有256KB RAM,每次握手都要吃掉将近40KB的临时缓冲区。最头疼的是证书链——服务器发下来一个三级CA链,光解析就得额外分配好几KB。

关键点:TLS 1.2的握手是显式的。每一步的消息都是明文(除了Finished之后),而且证书是完整发送的。这对受限设备来说,意味着每次握手都要为证书解析预留大量堆空间。

2.2 TLS 1.3 握手流程:1-RTT 与 0-RTT

TLS 1.3把握手压缩到了1-RTT(甚至0-RTT)。怎么做到的?核心思路是:合并消息、减少冗余。

1-RTT流程:

  1. ClientHello:客户端直接带上密钥交换参数(Key Share)。
  2. ServerHello + EncryptedExtensions + Certificate + CertificateVerify + Finished:服务器一次性回复所有东西,而且证书是加密传输的。
  3. Client Finished:客户端确认。

你看,从2-RTT变成1-RTT,省掉了一次往返。但对我这种搞嵌入式的人来说,更香的是内存占用的减少。因为TLS 1.3的证书是加密后发送的,客户端可以在解密前先分配一个固定大小的缓冲区,而不是像1.2那样先解析明文证书链再处理。

我记得有一次帮客户优化一个基于ESP32的工业采集器。原来用TLS 1.2,握手峰值内存到过48KB。切到1.3后,峰值降到了22KB。客户当场就愣了——他们本来打算换芯片的。

我的习惯:如果设备RAM小于512KB,我建议直接上TLS 1.3。0-RTT虽然快,但重放攻击风险需要应用层处理,这个后面章节会细讲。

2.3 关键数据结构:ClientHello

ClientHello是握手的第一条消息。它长什么样?我直接贴一个简化后的结构:

struct ClientHello {
    uint16_t protocol_version;      // 比如 0x0303 表示 TLS 1.2
    uint8_t random[32];             // 32字节随机数
    uint8_t session_id_length;      // 会话ID长度
    uint8_t session_id[32];         // 会话ID(最大32字节)
    uint16_t cipher_suites_length;  // 密码套件列表长度
    uint16_t cipher_suites[];       // 密码套件列表
    uint8_t compression_methods_length;
    uint8_t compression_methods[];
    uint16_t extensions_length;     // 扩展字段总长度
    uint8_t extensions[];           // 扩展字段
};

在受限设备上,扩展字段是内存大头。TLS 1.3的ClientHello里,扩展字段可能包含:

  • key_share:携带客户端公钥(比如X25519,32字节)。
  • supported_versions:声明支持TLS 1.3。
  • signature_algorithms:签名算法列表。
  • supported_groups:支持的椭圆曲线组。

我见过一个坑:某款RTOS的TLS栈,默认把ClientHello的扩展缓冲区设成了2KB。但实际上,大部分场景只需要512字节。后来我改成动态计算扩展长度,每次握手省了1.5KB。

避坑指南:我曾经在一个项目里,因为ClientHello的扩展字段里塞了太多不必要的密码套件(比如把DSA、RSA-PSS全列上了),导致消息体膨胀到1.8KB。在受限设备上,精简密码套件列表是最直接的内存优化手段。一般保留2-3个就够了。

2.4 关键数据结构:Certificate

Certificate消息是内存消耗的“大户”。它包含整个证书链,每个证书都是DER编码的X.509结构。

一个典型的证书链:

  • 叶子证书:服务器自己的证书,通常1-2KB。
  • 中间CA证书:1-2KB。
  • 根证书:通常不在链里发送(客户端本地有),但有些服务器会发。

加起来,一个三级链可能达到4-6KB。在只有几十KB RAM的设备上,这简直是灾难。

我优化过的一个方案是:证书压缩。TLS 1.3支持Certificate compression扩展(RFC 8879),可以用zlib或brotli把证书链压缩到原来的30%-50%。不过要注意,解压缩也需要临时缓冲区,得算好峰值。

证书类型 典型大小(DER) 压缩后(zlib) 说明
叶子证书(RSA 2048) ~1.5 KB ~0.8 KB 公钥和签名占大头
中间CA证书 ~1.2 KB ~0.6 KB 通常比叶子小一点
三级链合计 ~4.0 KB ~2.0 KB 压缩率约50%

嗯,这里要注意:不是所有服务端都支持证书压缩。如果设备内存实在吃紧,我建议在客户端做证书固定(Certificate Pinning),直接跳过证书链验证,只校验叶子证书的指纹。这个后面有专门章节讲。

2.5 TLS 1.2 vs 1.3 握手流程对比

我把核心差异整理成了一张表,方便你对照:

对比维度 TLS 1.2 TLS 1.3
握手往返次数 2-RTT 1-RTT(0-RTT可选)
证书传输 明文传输 加密传输
密钥交换 单独ClientKeyExchange消息 集成在ClientHello的key_share扩展
密码套件协商 显式列表,支持多种 固定几个AEAD套件
会话恢复 Session ID / Session Ticket PSK + 0-RTT
内存峰值(典型) 30-50 KB 15-25 KB

你想想看,TLS 1.3把证书加密后传输,意味着客户端不需要在握手阶段就分配一个能装下完整明文证书链的缓冲区。它可以先分配一个较小的加密帧缓冲区,解密后再逐段处理。这个设计,说白了就是为受限设备量身定做的。

2.6 握手流程对比图

下面这张SVG图,把TLS 1.2和1.3的握手流程放在一起对比。左边是1.2的两轮往返,右边是1.3的一轮搞定。我特意把证书消息标红了——因为它是内存优化的主战场。

TLS 1.2 vs 1.3 握手流程对比 TLS 1.2 vs 1.3 握手流程对比 TLS 1.2 (2-RTT) 客户端 服务端 ClientHello ServerHello Certificate ServerHelloDone ClientKeyExchange CCS + Finished CCS + Finished RTT 1 RTT 2 TLS 1.3 (1-RTT) 客户端 服务端 ClientHello + KeyShare ServerHello EncryptedExtensions Certificate CertVerify + Finished Finished RTT 1 说明: TLS 1.3 将证书加密传输(红色标记),客户端无需预分配大缓冲区解析明文证书链。 同时合并多条消息,减少握手状态机复杂度,内存峰值可降低 40%-60%。

从图上你能直观看到:TLS 1.3把证书消息放到了加密层之后,而且合并了ServerHello和后续消息。这意味着客户端的握手状态机可以更简单——不需要为每个中间状态维护独立的缓冲区。

2.7 小结

这一节我们拆了TLS 1.2和1.3的握手流程,重点看了ClientHello和Certificate这两个数据结构。说白了,TLS 1.3在内存优化上的优势,主要来自三个设计:

  • 减少往返:1-RTT vs 2-RTT,状态缓冲区更少。
  • 证书加密:避免预分配大缓冲区解析明文证书链。
  • 消息合并:减少握手状态机复杂度。