密码学基础:TLS 握手的数学基石

如果你做过网络协议优化,肯定知道 TLS 握手不仅仅是密钥协商。背后的密码学算法才是真正的「地基」。说白了,握手过程就是在用密码学工具解决三个问题:你是谁、你发给我的东西有没有被篡改、我们之间能不能建立一个只有彼此知道的秘密。

这一节我会把对称加密、非对称加密、哈希函数、数字签名和密钥派生串起来讲。别怕数学,咱们只关心它们在握手里的实际角色。

对称加密:速度与安全的平衡点

对称加密用同一个密钥加密和解密。就像你给行李箱上了一把锁,自己用钥匙打开。TLS 握手里真正传输数据用的就是对称加密——原因很简单:它快。

常用的有 AES-GCM、ChaCha20-Poly1305。我自己在项目中偏好 AES-GCM,硬件加速下吞吐量能到几十 Gbps。但注意:对称加密的关键是密钥本身不能泄露。所以握手其实是在安全地交换这个对称密钥。

关键指标:TLS 1.3 中对称加密只允许 AEAD 算法,一次完成加密+认证,避免了 CBC 模式的历史漏洞。我见过不少老系统还在用 AES-CBC,迁移到 1.3 后性能反而提升了。

避坑指南:我曾经接手一个项目,他们的对称加密密钥是写死在配置文件里的。这不是加密,这是掩耳盗铃。密钥必须通过握手阶段的密钥派生得到,不能静态存储。

非对称加密:不共享秘密也能安全通信

非对称加密有一对钥匙:公钥公开,私钥保密。你用公钥加密的数据,只有私钥能解开。反过来,私钥签名的数据,任何持有公钥的人都能验证真伪。

RSA 和 ECDSA 是最常见的。我个人更推荐 ECDSA——相同的安全强度下密钥更短、性能更好。RSA 2048 位的签名计算量比 ECDSA P-256 大了不止一个数量级。

在 TLS 握手里,非对称加密主要用于两个地方:

  • 证书验证:服务器用私钥签名证书,客户端用公钥验签,证明服务器身份。
  • 密钥交换:TLS 1.2 里可以用 RSA 密钥传输(现在已经不推荐了),TLS 1.3 统一用 ECDHE 做临时密钥协商。

注意:千万别把非对称加密直接用来加密大数据块!它的加解密速度只有对称加密的千分之一。我在一次压力测试中见过有人用 RSA 加密 1MB 的文件……结果那个请求超时了。

哈希函数:数据的指纹

哈希函数把任意长度的输入变成固定长度的输出,比如 SHA-256 输出 256 位。好的哈希函数具有两个特性:

  1. 单向性:从哈希值反推原始数据几乎不可能。
  2. 抗碰撞性:几乎找不到两个不同输入拥有相同哈希值。

我在 TLS 握手里,哈希函数主要负责:

  • 生成消息摘要(用于数字签名)
  • 派生密钥(配合其他算法)
  • 计算握手消息的完整性校验(Finished 消息)

SHA-1 已经被淘汰了,现在最低要求 SHA-256。你想想看,2017 年 Google 和 CWI 就成功演示了 SHA-1 碰撞攻击,所以别再用老旧的哈希了。

常见哈希算法比较(截至 TLS 1.3)
算法输出长度安全状态我的建议
MD5128 bit已攻破不要用
SHA-1160 bit存在碰撞攻击迁移到 SHA-256
SHA-256256 bit安全标准配置
SHA-384384 bit安全更高安全度可选

数字签名:让消息不可抵赖

数字签名 = 哈希 + 非对称加密。签名方先对消息算哈希,然后用私钥加密这个哈希。验证方用公钥解密哈希,再自己算一遍消息的哈希,如果一致就说明消息没被篡改且确实是签名方发的。

实际项目里最常见的场景就是证书链验证。我记得有一次排查一个证书异常,发现根证书的签名算法是 SHA-1/RSA,但中间证书用了 SHA-256/ECDSA。操作系统和新浏览器都报了警告,最后只能重新颁发了整个证书链。

原则:签名算法必须和证书密钥类型匹配。EC 密钥不要用 RSA 签名,反之亦然。混合使用会导致握手失败。

经验分享:我曾经把签名验证的顺序搞反了——先验签名再检查证书链。结果被中间人攻击了。正确的做法是:先验证证书链的合法性(包括 CA、有效期、吊销状态),再用证书里的公钥验签。

密钥派生:从共享秘密到会话密钥

握手之后,客户端和服务器有了一个共享秘密(比如通过 ECDHE 交换)。但这个秘密不能直接用做对称加密密钥——它不够随机,长度也可能不够。所以需要密钥派生函数(KDF)来「拉伸」它。

TLS 1.3 使用 HKDF(HMAC-based Key Derivation Function)分两步:

  1. Extract:从原始的共享秘密中提取出一个伪随机的中间密钥。
  2. Expand:把这个中间密钥扩展出多个不同用途的子密钥(加密密钥、认证密钥、初始化向量等)。

我手写过一个简易的 HKDF 实现做测试,核心代码其实只有十几行:

def hkdf_extract(salt, ikm):
    return hmac_sha256(salt, ikm)

def hkdf_expand(prk, info, length):
    t = b""
    okm = b""
    for i in range(1, (length // 32) + 2):
        t = hmac_sha256(prk, t + info + bytes([i]))
        okm += t
    return okm[:length]

注意盐(salt)很重要。TLS 1.3 把共享秘密和 PSK(预共享密钥)一起作为输入,盐是固定的序列号——这就是为什么握手消息的顺序不能乱。

警告:不要自己写 KDF!我曾经看到一个团队直接拿 SHA-256 对共享秘密做哈希,然后截取前 128 位当密钥。这样生成的密钥缺乏随机性,容易被侧信道攻击。请用标准的 HKDF 或 PBKDF2。

知识体系速览

下面这张图把五个密码学概念在 TLS 握手中的角色串起来了。你可以对照着看:

TLS 握手密码学组件关系图 对称加密 AES-GCM / ChaCha20 非对称加密 RSA / ECDSA 哈希函数 SHA-256 / SHA-384 数字签名 哈希 + 非对称加密 密钥派生 (HKDF) 共享秘密 → 会话密钥 密钥交换(ECDHE) 依赖私钥 消息摘要 HMAC 作为 KDF 生成加密密钥 核心流程:非对称加密交换临时密钥 → 哈希函数参与派生 → 密钥派生生成对称密钥 → 用对称加密保护数据

这张图里每个组件都是 TLS 握手不可或缺的一环。你想想看,缺了哈希函数,数字签名就成了无根之木;缺了密钥派生,共享秘密就没法安全地变成加密密钥。下次遇到握手失败,按这个图逐层排查,大概率能找到问题。

好了,这就是密码学基础的核心。下一话我们正式进入 TLS 握手的具体流程。先消化一下这些概念,后面会频繁用到。