3. 密码学基础:哈希函数、消息认证码、数字签名、公钥基础设施 (PKI)

各位同学,欢迎来到第三章。这一章咱们把目光聚焦在 TLS 背后那四个最基础的密码学工具上。说它们是工具,其实更像是地基——哈希函数、消息认证码、数字签名、PKI,这四个东西搞不明白,后面学 TLS 握手、密钥派生、证书校验,基本就是空中楼阁。

我个人做安全开发的习惯是:先理解每个工具解决什么问题,再去看它怎么用。这一章我就按这个思路来带大家过一遍。

3.1 哈希函数:数据的「数字指纹」

哈希函数,说白了就是一个压缩映射。输入任意长度的数据,输出固定长度的摘要。好的哈希函数必须满足几个特性:确定性(同一输入永远同一输出)、快速计算、抗原像性(不能从摘要反推原文)、抗碰撞性(找不到两个不同输入产生相同摘要)。

你想想看,这些特性在 TLS 里多重要。握手过程中双方需要校验对方发来的参数有没有被篡改,用哈希打个指纹,一比对就知道。

核心要点:哈希不是加密。加密可以解密,哈希不可逆。这是很多新手容易混淆的地方。

常见哈希算法对比如下:

算法 输出长度 (bit) 安全性现状 TLS 中的用途
MD5 128 已攻破,禁止使用 无(仅历史兼容)
SHA-1 160 已攻破,逐步淘汰 无(TLS 1.3 已移除)
SHA-256 256 安全,广泛使用 证书指纹、PRF、Finished 消息
SHA-384 384 安全,高安全场景 TLS 1.3 密码套件
SHA-3 可变 安全,新兴 部分新实现支持

我在项目中遇到过一件事:有同事用 MD5 做服务端的响应校验,结果被轻松碰撞出两个不同 payload 产生了相同哈希。嗯,从那以后我定了个规矩——任何安全场景,哈希算法不得低于 SHA-256。

在 TLS 里,哈希的具体落点包括:

  • 证书指纹:CA 签发证书时,会对证书本体做哈希,再用 CA 私钥签名这个摘要。
  • PRF (伪随机函数):TLS 1.2 使用 HMAC-SHA256 作为 PRF,派生各种密钥。
  • Finished 消息:握手最后双方交换一个哈希值,确保整个握手过程未被篡改。

实用命令:用 OpenSSL 查看证书指纹:
openssl x509 -in cert.pem -noout -fingerprint -sha256

3.2 消息认证码 (MAC):防篡改的「封印」

哈希只能保证数据没变,但不能证明数据是谁发的。消息认证码解决了这个问题——它用共享密钥加上消息内容,计算出一个固定长度的标签。只有持有同一把密钥的人才能验证这个标签。

最常用的构造是 HMAC,它的核心公式长这样:

HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))

其中 K' 是密钥经过填充后的版本,opad 和 ipad 分别是外填充和内填充常量。你想想看,为什么要套两层哈希?因为单层哈希存在长度扩展攻击的风险,HMAC 这种双壳结构能有效防御。

我曾经帮一个团队排查过 TLS 连接偶发失败的问题。抓包发现记录层的 MAC 校验总是通不过。后来定位到是密钥更新时机没对齐——客户端已经用了新密钥计算 MAC,服务端还在用旧密钥验证。嗯,这种边界问题在复杂的异步系统中特别容易出。

TLS 记录层使用 MAC-then-Encrypt 的结构(TLS 1.2 及之前):

  1. 对明文消息计算 MAC 标签
  2. 将明文 + MAC 标签拼接
  3. 整体加密

TLS 1.3 改用了 AEAD 模式,将加密和认证合二为一,更简洁也更安全。但底层的认证思想依然是 MAC 的延续。

避坑指南:我曾经见过有人在自定义协议里直接用哈希(K || m) 当 MAC。这种构造存在长度扩展攻击,攻击者可以在不知道 K 的情况下伪造合法标签。切记:永远用标准的 HMAC,别自己发明 MAC 构造。

3.3 数字签名:身份的「电子印章」

MAC 解决了「谁发的」问题,但它依赖共享密钥。如果通信双方没有提前共享密钥呢?或者你需要向第三方证明这个消息确实是你发的?这时候就需要数字签名了。

数字签名的核心思想是非对称密钥:私钥签名,公钥验证。流程如下:

  1. 对消息做哈希,得到摘要
  2. 用私钥对摘要加密(签名)
  3. 验证方用公钥解密,比对摘要

你发现没有,这里又用到了哈希。所以说哈希是整个密码学的基础工具,一点也不夸张。

常见的签名算法:

算法 密钥类型 安全性 TLS 中的应用
RSA-PKCS#1 v1.5 RSA 有已知攻击,需使用 PSS 填充 TLS 1.2 证书签名
RSA-PSS RSA 安全,推荐使用 TLS 1.3 证书签名
ECDSA EC 安全,密钥短 TLS 1.2/1.3 广泛使用
EdDSA (Ed25519) Edwards 安全,性能高 TLS 1.3 支持

我记得有一次排查 TLS 握手失败,客户端报 "signature verification failed"。检查了半天才发现是服务端用了 RSA-PSS 签名,但客户端只支持 RSA-PKCS#1 v1.5。TLS 1.3 强制要求实现 PSS,但很多老旧系统还在用 v1.5,兼容性坑很多。

在 TLS 握手中,数字签名出现在两个关键位置:

  • CertificateVerify 消息:服务端(或客户端)用私钥签名握手摘要,证明自己持有证书对应的私钥。
  • 证书签名:CA 用其私钥对用户证书进行签名,形成证书链。

实用命令:验证 RSA-PSS 签名:
openssl pkeyutl -verify -pubin -inkey pub.pem -in data.bin -sigfile sig.bin -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss

3.4 公钥基础设施 (PKI):信任的「骨架」

数字签名解决了「这个公钥确实对应这个私钥」的问题,但你怎么知道这个公钥是属于「银行官网」而不是「钓鱼网站」的?这就是 PKI 要解决的核心问题——公钥的可信分发与身份绑定。

PKI 的核心组件包括:

  • CA (证书颁发机构):签发证书的信任根。CA 用自己的根证书自签名,然后签发下级证书。
  • RA (注册机构):负责验证申请者的身份信息,通过后将请求转发给 CA 签发。
  • 证书库:存储已签发证书和 CRL (证书吊销列表) 的公共仓库。
  • 终端实体:持有证书和私钥的服务端或客户端。

X.509 证书的结构如下:

字段 说明 示例
版本号 证书格式版本 (1/2/3) v3
序列号 CA 分配的唯一编号 0x12345678
签名算法 CA 签名使用的算法 sha256WithRSAEncryption
颁发者 CA 的 DN (区分名) C=CN, O=Example CA
有效期 起止时间 2024-01-01 ~ 2026-01-01
主体 证书持有者的 DN C=CN, O=My Server, CN=example.com
公钥 持有者的公钥信息 RSA 2048-bit
扩展 附加属性 (SAN, Key Usage 等) DNS:example.com
CA 签名 CA 对上述字段的签名值 0xABCD...

在 TLS 握手过程中,服务端发送证书链,客户端从信任锚(操作系统或浏览器内置的根证书)开始,逐级验证每个证书的签名,直到匹配到服务器证书。这就是证书链验证。

我之前在做一个网关项目时,需要内部自建 PKI。图省事用了自签名证书,结果客户端每次连接都要手工导入信任,运维同学叫苦连天。后来搭了一套小型内部 CA,用中间证书签发服务器证书,客户端只需要信任根证书一次,所有服务器证书自动可信。嗯,这件事让我明白了一个道理:PKI 的核心不是技术,是信任模型的合理设计。

避坑指南:我见过有人在生产环境直接使用 CA 根证书签发服务器证书,而不是通过中间 CA。一旦根证书私钥泄露,整个信任体系都要重建。正确的做法是:根证书离线保存,用中间 CA 签发终端证书,这样可以把风险隔离在中间层。

3.5 核心知识体系一图总览

下面这张图把四个核心概念的关系串起来了。从哈希函数出发,到消息认证码,再到数字签名,最终由 PKI 构建信任底座,支撑上层的 TLS 加密通信。每个节点都标注了它在 TLS 中扮演的角色。

密码学基础核心知识体系