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 及之前):
- 对明文消息计算 MAC 标签
- 将明文 + MAC 标签拼接
- 整体加密
TLS 1.3 改用了 AEAD 模式,将加密和认证合二为一,更简洁也更安全。但底层的认证思想依然是 MAC 的延续。
避坑指南:我曾经见过有人在自定义协议里直接用哈希(K || m) 当 MAC。这种构造存在长度扩展攻击,攻击者可以在不知道 K 的情况下伪造合法标签。切记:永远用标准的 HMAC,别自己发明 MAC 构造。
3.3 数字签名:身份的「电子印章」
MAC 解决了「谁发的」问题,但它依赖共享密钥。如果通信双方没有提前共享密钥呢?或者你需要向第三方证明这个消息确实是你发的?这时候就需要数字签名了。
数字签名的核心思想是非对称密钥:私钥签名,公钥验证。流程如下:
- 对消息做哈希,得到摘要
- 用私钥对摘要加密(签名)
- 验证方用公钥解密,比对摘要
你发现没有,这里又用到了哈希。所以说哈希是整个密码学的基础工具,一点也不夸张。
常见的签名算法:
| 算法 | 密钥类型 | 安全性 | 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 中扮演的角色。