加密基础:对称 / 非对称 / 哈希与组合模式
想象这样一天:你注册了一个新网站,密码用 SHA-256 存进 DB——第二天被拖库,攻击者用 GPU 集群三分钟破译;你写了一个内网 API,用
SHA256(密钥 + 请求体)做签名——两周后被伪造调用,钱财转空;你上线 TLS,为了兼容老客户端保留了 RSA 密钥交换——三个月后服务器私钥泄露,去年一整年的历史流量被回放解密。这三种事故的根源都不是"忘了加密",而是把三大原语(对称/非对称/哈希)的组合方式搞错了。本篇是站点加密原理的单一事实源:一次讲通对称、非对称、哈希三块砖,再把 AEAD、HMAC、数字签名、密钥交换、KDF 这些组合方式搭起来,最后落到 TLS / JWT / mTLS 三个真实协议。
一句话结论
密码学面试的主线只有三条:对称快、非对称慢,所以工程上永远是"非对称谈钥匙 + 对称跑数据";哈希 ≠ MAC ≠ 签名——三者能防的攻击面不同(篡改 / 伪造 / 抵赖),选错就漏底;IV / Nonce 用错等同泄漏密钥。抓住这三条,AEAD / HMAC / ECDHE / JWT / mTLS 所有选型题都是同一道题的变形。
大白话入门
先用最土的话讲清楚这门学问在干什么、发展到今天为止解决了哪些问题。术语一律先翻译成中文,读到后面就不会被吓退。
密码学到底在解决什么?
抛开所有术语,密码学只有四个目标:
| 目标 | 一句话 | 生活比喻 | 靠什么原语 |
|---|---|---|---|
| 机密性(Confidentiality) | 让第三方看不懂 | 把信装进上锁的箱子 | 对称加密(AES)、非对称加密(RSA) |
| 完整性(Integrity) | 让第三方改不了(改了能发现) | 信封上贴防伪封条 | 哈希(SHA-256)、MAC(HMAC) |
| 认证(Authentication) | 确认对方是谁 | 看身份证、验签名 | 数字签名(RSA/ECDSA/Ed25519)、证书 |
| 不可抵赖(Non-repudiation) | 事后你没法说"这不是我发的" | 手写签名 + 公证 | 数字签名(HMAC 做不到) |
关键洞察:一个真实协议(TLS、JWT、mTLS)通常要同时满足前三个目标(TLS 还额外要"前向安全")。所以你会看到 TLS 里同时用了对称、非对称、哈希、签名、密钥交换、KDF——不是炫技,是因为这四个目标要用不同的工具。
一张图看懂发展史(1976 → 2024)
演进主线三句话:
- 对称 → 非对称(1970s):解决"陌生人怎么协商密钥"这个死结。
- 手拼加密+MAC → AEAD(2000s→2018):一步做完,杜绝 Padding Oracle 类实现坑。
- RSA → 椭圆曲线 → 抗量子(1977→2012→2024):性能提升 + 应对未来量子威胁。
术语速查表(先扫一遍,读正文时随时翻回来)
这一节读者可以直接跳过,遇到不认识的词再回来查。
分类 1:基础对象
| 术语 | 中文/含义 | 生活比喻 | 举例 |
|---|---|---|---|
| 明文(plaintext) | 原始可读的数据 | 信的内容 | "hello world" |
| 密文(ciphertext) | 加密后不可读的数据 | 锁进箱子的信 | 一堆看不懂的字节 |
| 密钥(key) | 加/解密用的秘密 | 钥匙 | 32 字节随机数 |
| 公钥(public key) | 可公开的钥匙(非对称) | 挂锁 | 挂网站上给人下载 |
| 私钥(private key) | 严格保密的钥匙(非对称) | 只有你有的钥匙 | 存本地文件+权限 600 |
| IV(Initialization Vector,初始向量) | 让相同明文加密出不同密文的随机数 | 加密时的"起点标记" | CBC 模式必需 |
| Nonce(Number used ONCE,一次性数) | 每次加密都必须不重复的数 | 一次性密码 | GCM/CTR 模式必需 |
| Salt(盐) | 让相同密码哈希出不同结果的随机数 | 密码前加的随机料 | Argon2/bcrypt 存密码时用 |
| Tag(认证标签) | AEAD 输出的完整性校验字节 | 一次性防伪封条 | GCM 的 16 字节 tag |
IV vs Nonce vs Salt 的差别:都是"随机数",但要求不同:
- IV:唯一即可,可以公开(CBC)
- Nonce:唯一即可,可以公开,但同密钥下绝不能重复(GCM/CTR)
- Salt:唯一即可,可以公开,用于密码存储
分类 2:算法类别
| 术语 | 全称/中文 | 一句话 |
|---|---|---|
| 对称加密 | Symmetric encryption | 加密解密同一把钥匙 |
| 非对称加密 | Asymmetric / Public-key encryption | 一对公私钥,方向相反 |
| 哈希 | Hash / Digest / 摘要 | 单向压缩成固定长度指纹 |
| MAC | Message Authentication Code / 消息认证码 | 带密钥的哈希,防伪造 |
| AEAD | Authenticated Encryption with Associated Data | 加密+完整性+认证三合一 |
| AAD | Associated Authenticated Data | AEAD 里"认证但不加密"的字段(如 HTTP 头) |
| KDF | Key Derivation Function / 密钥派生函数 | 从一个秘密派生出多把子密钥 |
| KEM | Key Encapsulation Mechanism / 密钥封装 | 非对称加密的现代叫法,专用于传对称密钥 |
| PFS | Perfect Forward Secrecy / 前向安全 | 长期私钥泄露也解不了历史流量 |
| PRF | Pseudo-Random Function / 伪随机函数 | 输出看起来像随机数的函数(HMAC 就是 PRF) |
| PRK | Pseudo-Random Key(HKDF Extract 的输出) | HKDF 第一步压出来的均匀密钥 |
| OKM | Output Keying Material(HKDF Expand 的输出) | HKDF 第二步展开出的最终子密钥 |
| IKM | Input Keying Material(HKDF Extract 的输入) | 原始的、熵不均匀的秘密(如 ECDH 共享秘密) |
分类 3:具体算法
| 术语 | 类别 | 一句话 |
|---|---|---|
| AES | 对称加密 | 分组密码,工业界默认,AES-128/192/256 |
| ChaCha20 | 对称加密 | 流密码,移动端首选(无 AES 硬件时) |
| RSA | 非对称加密 + 签名 | 老牌,密钥长(2048/3072 bit) |
| ECC | 椭圆曲线密码学(一类) | 短密钥、快,涵盖 ECDSA/ECDH/Ed25519 等 |
| ECDSA | 签名(椭圆曲线) | 需每次签名随机数 k,k 泄露 = 私钥泄露(PS3 案) |
| Ed25519 | 签名(椭圆曲线) | 确定性签名(无 k 依赖)、现代默认 |
| DH | 密钥交换 | 1976 年 Diffie-Hellman 首创,基于整数域 |
| ECDH | 密钥交换(椭圆曲线版) | 更短更快 |
| ECDHE | ECDH + Ephemeral(临时) | 每次会话用新密钥,实现前向安全 |
| X25519 | ECDH 的现代曲线实现 | TLS 1.3 默认,Bernstein 设计 |
| SHA-1 | 哈希 | 已弃用(2017 被破) |
| SHA-2(SHA-256/384/512) | 哈希 | 工业界主力 |
| SHA-3 | 哈希 | 海绵结构,抵御长度扩展,性能不如 SHA-2 |
| BLAKE2/BLAKE3 | 哈希 | 极快,非官方标准但工程流行 |
| HMAC | MAC | 哈希+密钥的标准做法(RFC 2104) |
| Poly1305 | 一次性 MAC | 组合 ChaCha20 → ChaCha20-Poly1305 AEAD |
| HKDF | KDF | HMAC 底子,TLS 1.3 官选(RFC 5869) |
| PBKDF2 | 密码存储 KDF | 老,只调迭代次数 |
| bcrypt | 密码存储 KDF | 曾是默认,输入 72 字节截断坑 |
| scrypt | 密码存储 KDF | 时间+内存双硬 |
| Argon2id | 密码存储 KDF | 当前推荐默认,PHC 冠军 |
分类 4:工作模式与结构
| 术语 | 含义 |
|---|---|
| ECB(Electronic Codebook) | 最原始的分组模式,每块独立加密,图案泄露,禁用 |
| CBC(Cipher Block Chaining) | 每块与前一块密文异或,需 IV,无认证需叠 HMAC |
| CTR(Counter Mode) | 把 AES 当流密码用,需 Nonce |
| GCM(Galois/Counter Mode) | CTR + GMAC = AEAD,现代默认 |
| SPN(Substitution-Permutation Network) | AES 底层的加密结构,"替换+置换"层层叠加,不用记细节 |
| Merkle-Damgård 结构 | SHA-1/2 的底层链式结构,长度扩展攻击的成因 |
| 海绵结构(Sponge) | SHA-3 的底层结构,抵御长度扩展 |
分类 5:协议与工程
| 术语 | 含义 |
|---|---|
| TLS(Transport Layer Security) | HTTPS 的底层,加密整个连接 |
| mTLS(mutual TLS) | 双向 TLS,客户端也提交证书 |
| JWT(JSON Web Token) | 自包含的签名令牌,HS256/RS256 两种签法 |
| JWKS(JSON Web Key Set) | 公钥公开分发的 URL 端点(配 RS256 用) |
| JWE(JSON Web Encryption) | 加密版 JWT(不只签名,还加密载荷) |
| CA(Certificate Authority) | 证书颁发机构(DigiCert / Let's Encrypt) |
| X.509 | 证书的国际标准格式 |
| CRL(Certificate Revocation List) | 证书吊销列表,CA 定期发布 |
| OCSP(Online Certificate Status Protocol) | 实时查证书吊销状态 |
| OCSP Stapling | 服务器代查 OCSP 并在握手时附上应答,事实标准 |
| SPIFFE(Secure Production Identity Framework for Everyone) | 服务网格的身份标准,每个服务一个 SPIFFE ID |
| PKI(Public Key Infrastructure) | 公钥基础设施,指 CA + 证书 + 吊销的整套体系 |
| PQC(Post-Quantum Cryptography) | 抗量子密码,如 NIST 已标准化的 Kyber/Dilithium |
| TOFU(Trust On First Use) | 首次连接时信任对方(SSH 的做法,无 CA 时用) |
一个新手最常混淆的对比
在读正文前,先把最容易搞混的三对说清楚:
混淆点 1:加密 vs 编码 vs 哈希
- 编码(Base64、URL encode):不是加密,任何人可逆。目的是"让二进制能在文本协议里传输"。
- 加密(AES):可逆(有密钥才能解开)。目的是保密。
- 哈希(SHA-256):不可逆。目的是产生指纹,不是保密。
混淆点 2:签名 vs 加密
- 都用非对称密钥对,但方向相反(前面 SVG 图讲过)。
- 加密:接收方公钥 → 私钥(保密)
- 签名:签名方私钥 → 公钥(认证)
- 签名不加密内容——原消息还是明文传,签名只是附加的一段可验证字节。
混淆点 3:HMAC vs 数字签名
- HMAC:双方共享密钥(对称的思路,只是用哈希实现)
- 数字签名:私钥签、公钥验(非对称)
- 区别的核心是"信任模型":HMAC 双方互信,签名单向。
场景问题
一个能秒懂的类比:三种"锁"
假设你要给朋友寄东西,市面上有三种"锁",选错了就出事:
| 类比 | 密码学对应 | 一句话 |
|---|---|---|
| 一把钥匙开关同一把锁(你和朋友共用) | 对称加密 | 快,但要先想办法把钥匙安全递过去 |
| 公开的挂锁 + 只有自己的钥匙(谁都能锁上,只有你能开) | 非对称加密 | 慢,但解决了"怎么把钥匙递过去" |
| 一次性防伪封条(撕过就看得出来,但封条本身不锁东西) | 哈希 | 不加密,只用来"验证有没有被动过" |
工程上永远是这三者混着用——用挂锁把钥匙寄过去(非对称),用钥匙锁大件(对称),加封条防拆(哈希)。TLS / SSH / mTLS / JWT 都是这个套路的变种。
面试高频翻车题
| 题目 | 考点 | 典型错答 | 正解要点 |
|---|---|---|---|
| RSA 加密和 RSA 签名钥匙一样吗 | 公私钥语义 | "都用公钥加密" | 加密用接收方公钥、签名用自己私钥——方向相反 |
用 SHA256(K + m) 拼一个 MAC 行不行 | 长度扩展攻击 | "行,够简单" | 不行,SHA-2 可被扩展,必须用 HMAC |
| AES-GCM 的 Nonce 能不能重复 | Nonce 复用 | "随便什么都行,反正是随机数" | 同密钥下绝不能重复,重复 = 密钥泄漏 |
| ECDHE 里那个 E 是什么 | 前向安全 | "椭圆的意思" | Ephemeral,每次会话临时密钥 |
| JWT 用 HS256 还是 RS256 | 信任模型 | "都能用,看喜好" | 内网 HS256、对外 RS256/ES256 |
| 密码存 DB 用 SHA-256 够不够 | 慢哈希 | "够,反正不可逆" | 不够,需 Argon2/bcrypt + 加盐 |
| 签名能防篡改吗、能防抵赖吗 | 能力矩阵 | "都能防篡改就行" | 签名比 HMAC 多"不可抵赖"能力 |
实现方案
下面开始正式讲。先看整体地图,再逐块拆解——这样后面每讲一个原语你都知道它落在哪里:
读图窍门:紫色是砖(原语)、蓝色是灰浆(组合方式)、绿色是最终房子(真实协议)。任何"这个协议为什么这样设计"都能顺着箭头往回追。
一、对称加密:一把钥匙开关
一分钟大白话
- 是什么:加密和解密用同一把钥匙的算法。你和朋友事先约好一句暗号(钥匙),之后就用它加密聊天。
- 什么时候诞生:几千年前就有(凯撒密码),现代版 DES 1977 年、AES 2001 年。
- 解决什么问题:大批量数据的高速加密——CPU 一秒能处理 GB 级数据。
- 典型场景:TLS 握手完之后传数据、磁盘全盘加密(BitLocker/FileVault)、数据库字段加密。
- 最大死穴:"第一次怎么把钥匙告诉对方"——这个问题一直到 1976 年 DH 密钥交换才解决(见第七节)。
一分钟大白话
- 是什么:加密和解密用同一把钥匙的算法。你和朋友事先约好一句暗号(钥匙),之后就用它加密聊天。
- 什么时候诞生:几千年前就有(凯撒密码),现代版 DES 1977 年、AES 2001 年。
- 解决什么问题:大批量数据的高速加密——CPU 一秒能处理 GB 级数据。
- 典型场景:TLS 握手完之后传数据、磁盘全盘加密(BitLocker/FileVault)、数据库字段加密。
- 最大死穴:"第一次怎么把钥匙告诉对方"——这个问题一直到 1976 年 DH 密钥交换才解决(见第七节)。
故事场景
你和朋友想私下聊天,事先在见面时对好了一句暗号(对称密钥)。之后就用暗号把信息编码后发出去——谁截获都读不懂,除非他也知道暗号。
问题只有一个:第一次怎么把暗号安全地告诉朋友?(——这就是后面非对称加密要解决的问题。)
直觉画面
关键点:加密和解密用的是同一把钥匙——所以叫"对称"。
术语与家族
- 分组密码 vs 流密码:AES 属于分组密码——一次处理固定长度的一块(AES 是 16 字节 = 128 bit);ChaCha20 属于流密码——像一条无穷长的伪随机比特流,逐字节 XOR 你的明文。
- AES 家族:
AES-128 / AES-192 / AES-256(数字是密钥长度),底层结构叫 SPN(Substitution-Permutation Network),面试不用深挖细节,知道"AES-256 是当前工业界默认最强对称算法"就够。 - 工作模式:AES 一次只能加 16 字节。真实消息几 KB 几 MB,就需要"工作模式"告诉 AES 怎么把一块一块拼起来。这里就有大坑:
| 模式 | 是否需要 IV/Nonce | 是否带认证 | 常见坑 |
|---|---|---|---|
| ECB(最简单:每块独立加密) | 否 | ❌ | 相同明文块 → 相同密文块,图案会泄露出来(企鹅图梗),禁用 |
| CBC(每块与前一块的密文异或) | 需要 IV,唯一即可(可公开) | ❌ | 无完整性,需再叠 HMAC;必须先加密再 MAC,否则有 Padding Oracle 攻击 |
| CTR(把 AES 当伪随机流用) | 需要 Nonce,必须唯一 | ❌ | Nonce 复用 → 两条明文异或直接暴露 |
| GCM(CTR + 内置认证 = AEAD) | 需要 Nonce,必须唯一 | ✅ | Nonce 复用 = 灾难,比 CTR 复用还严重 |
深入:ECB 为什么禁用(一张图讲透)
看出问题了吗?明文块 1 和 2 相同 → 密文块也相同。这在加密位图时最直观:一张企鹅图片加密完还是能看出企鹅的轮廓(对应像素颜色相同的地方,密文也相同)。
CBC 怎么修:让每一块都异或前一块的密文,第一块异或一个随机 IV:
即使明文块 1 和 2 相同,因为前一块密文不同 → 异或结果不同 → 输入 AES 的东西不同 → 密文不同。图案泄露解决。
现代默认:AEAD(一步到位)
AEAD = Authenticated Encryption with Associated Data(带关联数据的认证加密)。一步做完三件事:机密性 + 完整性 + 认证。主流两个:
- AES-256-GCM:有 CPU 硬件加速(AES-NI)时首选,云端和现代 x86/ARM 服务器都有。
- ChaCha20-Poly1305:没硬件加速的场景(老手机、嵌入式)首选,纯软实现比 AES 快。
类比:AEAD = 快递箱 + 一次性防伪封条
AAD(Associated Data):参与认证但不加密的字段。典型用途:HTTP 头、连接 ID——你不想加密它(否则中间节点看不到路由信息),但改动它 tag 会失效。
GCM 的 tag 挂在密文哪里(Go 伪码,非可运行程序):
// 加密:Seal(dst, nonce, plaintext, additionalData) → dst = ciphertext || tag
block, _ := aes.NewCipher(key32) // 32 字节 = AES-256
aead, _ := cipher.NewGCM(block) // 默认 12-byte nonce, 16-byte tag
nonce := make([]byte, aead.NonceSize())
rand.Read(nonce) // MUST 每次唯一:随机 or 单调计数器
ct := aead.Seal(nil, nonce, plaintext, aad) // ct 末尾自带 16 字节 tag
// 传输:nonce || ct(含 tag)
// 解密:Open() 内部先验 tag,失败直接返回 error,不吐半字节明文
pt, err := aead.Open(nil, nonce, ct, aad)
要点:tag 拼在密文尾部、Open 先验后拆——tag 不对直接失败,绝不返回"半解密"的中间结果。这是 AEAD 相对"CBC + HMAC 手拼"的关键防御边界(后面 HMAC 章节会讲为什么)。
Nonce 复用 = 密钥泄漏
"同一密钥下 Nonce 重复"在 GCM 里是灾难:两条消息用同一段密钥流 XOR 加密 → 两条密文相 XOR 就消掉密钥流,还原明文异或;更糟的是 GHASH 的认证密钥也会泄露,攻击者可任意伪造 tag。所以 Nonce 要么用真随机(96-bit 空间足够)、要么用单调计数器,同一密钥下绝不复用。
二、非对称加密:公开挂锁 + 私钥
一分钟大白话
- 是什么:加密解密用一对钥匙(公钥+私钥),方向相反。公钥可以贴到全世界看,只有私钥能解开。
- 什么时候诞生:1976 年 Diffie-Hellman 论文提出概念,1977 年 RSA 算法给出首个实用实现。这是密码学历史上最重要的突破——之前根本没人相信"陌生人在公开信道能保密通信"。
- 解决什么问题:"和陌生人首次交换密钥"——终于不用先见面对暗号了。也顺便解决了"证明消息真的是你发的"(数字签名)。
- 典型场景:TLS 握手协商对称密钥、SSH 免密登录、比特币钱包地址、JWT RS256 签名。
- 最大死穴:慢。RSA-2048 加密单核每秒才几千次,AES-GCM 每秒 GB 级——所以现实中永远是"非对称谈钥匙 + 对称跑数据"。
一分钟大白话
- 是什么:加密解密用一对钥匙(公钥+私钥),方向相反。公钥可以贴到全世界看,只有私钥能解开。
- 什么时候诞生:1976 年 Diffie-Hellman 论文提出概念,1977 年 RSA 算法给出首个实用实现。这是密码学历史上最重要的突破——之前根本没人相信"陌生人在公开信道能保密通信"。
- 解决什么问题:"和陌生人首次交换密钥"——终于不用先见面对暗号了。也顺便解决了"证明消息真的是你发的"(数字签名)。
- 典型场景:TLS 握手协商对称密钥、SSH 免密登录、比特币钱包地址、JWT RS256 签名。
- 最大死穴:慢。RSA-2048 加密单核每秒才几千次,AES-GCM 每秒 GB 级——所以现实中永远是"非对称谈钥匙 + 对称跑数据"。
故事场景
回到刚才的坑:Alice 和 Bob 想用对称加密聊天,但他们没见过面——怎么把对称密钥安全地告诉对方?直接寄过去中间人一截获就完蛋。
Diffie 和 Hellman 1976 年提出了神奇的方案:Bob 造一把"任何人都能锁上、只有 Bob 能打开"的挂锁,把挂锁的图纸公开挂在网上。Alice 想聊天时:
- 下载 Bob 的挂锁图纸(公钥)
- 用挂锁把自己生成的对称密钥锁进一个箱子(加密)
- 把箱子寄给 Bob
中间人截获箱子——没用,因为只有 Bob 的钥匙(私钥)能开。
直觉画面:RSA 加密
最大的坑:加密和签名的"钥匙方向相反"
RSA 有两个用途,钥匙方向正好相反——这是面试最爱考的一点。
记忆口诀:
- 加密:"公锁私开" —— 谁想给我发密文,就用我的公钥(公开的锁)加密;我用自己的私钥(唯一的钥匙)打开。
- 签名:"私签公验" —— 我要证明这消息是我发的,就用我的私钥签名;任何人拿我的公钥都能验证。
想想这个对称性为什么合理:
- 加密目标是 "只有一人能读" → 用只有一人有的东西解密 → 用私钥解
- 签名目标是 "只有一人能写" → 用只有一人有的东西加密 → 用私钥签
术语与家族
| 算法 | 密钥长度 | 用途 | 特点 |
|---|---|---|---|
| RSA-2048/3072 | 长(256/384 字节) | 加密、签名 | 老牌,兼容性最好,签名慢 |
| ECDSA (P-256) | 短(32 字节) | 签名 | 快、短,但依赖每次签名的随机数 k——k 复用/可泄露 = 私钥泄漏(Sony PS3 就栽在这里) |
| Ed25519 | 短(32 字节) | 签名 | 快、短、确定性签名(无 k 随机数依赖)、无坏曲线陷阱——现代首选 |
| X25519 | 短(32 字节) | 密钥交换(ECDH) | TLS 1.3 默认 |
为什么非对称"只用于握手":性能差距太大。AES-GCM 单核吞吐 GB/s,RSA-2048 签名单核每秒几千次——差 5 个数量级。全程非对称直接不可用。所以工程上永远是:非对称谈钥匙 + 对称跑数据。
三、哈希:单向指纹
一分钟大白话
- 是什么:把任意长度的输入压成固定长度的"指纹"(比如 SHA-256 永远输出 32 字节),且不可逆。
- 什么时候诞生:MD5 1992 年、SHA-1 1993 年、SHA-2 家族 2001 年、SHA-3 2015 年。MD5 和 SHA-1 都已被攻破,现在不能用。
- 解决什么问题:"验证数据有没有被动过"——你我各算一遍,指纹一样就说明没变。注意:哈希不是加密——它是单向的,无法还原。
- 典型场景:文件完整性校验(下载 ISO 后核对哈希)、Git 提交寻址、区块链、密码存储(配慢哈希)、密钥派生(配 HKDF)。
- 最大陷阱:新手最容易把"用 SHA-256 加密"挂嘴上——哈希不加密任何东西,只是产生指纹。想加密请回第一或第二节。
一分钟大白话
- 是什么:把任意长度的输入压成固定长度的"指纹"(比如 SHA-256 永远输出 32 字节),且不可逆。
- 什么时候诞生:MD5 1992 年、SHA-1 1993 年、SHA-2 家族 2001 年、SHA-3 2015 年。MD5 和 SHA-1 都已被攻破,现在不能用。
- 解决什么问题:"验证数据有没有被动过"——你我各算一遍,指纹一样就说明没变。注意:哈希不是加密——它是单向的,无法还原。
- 典型场景:文件完整性校验(下载 ISO 后核对哈希)、Git 提交寻址、区块链、密码存储(配慢哈希)、密钥派生(配 HKDF)。
- 最大陷阱:新手最容易把"用 SHA-256 加密"挂嘴上——哈希不加密任何东西,只是产生指纹。想加密请回第一或第二节。
故事场景
你从网上下载了一个 5GB 的镜像文件。怎么确认下载过程中没有被中间人替换?镜像的官方页面上贴了一段 64 位十六进制字符串——那就是哈希值。你本地也算一遍:一样,说明下载完整;不一样,说明有变化(无论是无意损坏还是恶意替换)。
哈希函数满足两个特性:
- 单向:给你哈希值
h,反推原始数据m计算不可行(不可能用 SHA-256 值倒推出源镜像)。 - 雪崩:输入变一个 bit,输出完全变样(一半以上的 bit 翻转)。
直觉画面
无论输入 11 字节还是 5GB,输出都是固定 32 字节。输入哪怕改一个字符,输出完全不一样。
术语与家族
- SHA-2(SHA-256/384/512):2001 年至今主力,未被攻破。
- SHA-3(Keccak):2015 年标准化,用了完全不同的"海绵结构",抗性更强,但性能通常不如 SHA-2。
- BLAKE2 / BLAKE3:速度极快,工程界流行,非官方标准。
- MD5 / SHA-1:已弃用。MD5 (2004 年被攻破,秒级构造碰撞)、SHA-1 (2017 年 Google SHAttered 攻破)。仅限非安全场景(如 Git 对象寻址、CDN 缓存 key)。
三性质:能防什么、不能防什么
| 性质 | 攻击者能力 | 一句话 |
|---|---|---|
| 抗原像(preimage) | 给 h,找不到 m 使 H(m)=h | "反推明文" 做不到 |
| 抗第二原像(2nd preimage) | 给 m1,找不到 m2 ≠ m1 使 H(m1)=H(m2) | "改造已有消息使摘要不变" 做不到 |
| 抗碰撞(collision) | 找不到任何 m1 ≠ m2 使 H(m1)=H(m2) | "预制两份摘要相同的消息" 做不到 |
抗碰撞是最强要求,MD5/SHA-1 就栽在这里。
红线:哈希 ≠ MAC(下一节详细讲)
一个新手最容易犯的错:"用 SHA-256 加密"。哈希不是加密——它是单向的、无法还原。哈希只能做完整性校验(你我都算一遍看结果一样不一样),不能防主动攻击——攻击者拿到消息可以自己重算一份哈希,让你以为没被改。要防主动攻击,得让哈希"知道秘密"——那就是 HMAC 或数字签名。
四、HMAC:给哈希加一把密钥
一分钟大白话
- 是什么:"哈希 + 密钥" 的组合,得到一个只有知道密钥的人才能算出的"带密码的指纹"。
- 什么时候诞生:1996 年 Bellare-Canetti-Krawczyk 论文提出,1997 年成为 RFC 2104。
- 解决什么问题:"让接收方能验证消息真的是你发的、路上没被改过"。哈希只能验证"没被无意改坏"(谁都能重算),HMAC 加了密钥后,攻击者不知道密钥就伪造不出合法 HMAC。
- 典型场景:API 计费签名(腾讯米大师、AWS S3 请求签名)、JWT HS256、内网服务鉴权、Cookie 防篡改(Django SECRET_KEY)。
- 为什么不直接
SHA256(密钥+消息):SHA-2 有长度扩展攻击漏洞,攻击者不知道密钥也能伪造合法输出。HMAC 的双层结构就是为了封住这个洞。
一分钟大白话
- 是什么:"哈希 + 密钥" 的组合,得到一个只有知道密钥的人才能算出的"带密码的指纹"。
- 什么时候诞生:1996 年 Bellare-Canetti-Krawczyk 论文提出,1997 年成为 RFC 2104。
- 解决什么问题:"让接收方能验证消息真的是你发的、路上没被改过"。哈希只能验证"没被无意改坏"(谁都能重算),HMAC 加了密钥后,攻击者不知道密钥就伪造不出合法 HMAC。
- 典型场景:API 计费签名(腾讯米大师、AWS S3 请求签名)、JWT HS256、内网服务鉴权、Cookie 防篡改(Django SECRET_KEY)。
- 为什么不直接
SHA256(密钥+消息):SHA-2 有长度扩展攻击漏洞,攻击者不知道密钥也能伪造合法输出。HMAC 的双层结构就是为了封住这个洞。
故事场景
内网服务 A 调用服务 B 的计费 API,怎么让 B 确认"这个请求真的是 A 发的、路上没被改过"?
方案一(错):让请求带一个 signature = SHA256(secret + body)——A 和 B 共享 secret。B 收到后自己算一遍对得上就通过。这有致命漏洞:SHA-2 存在长度扩展攻击——攻击者拿到 SHA256(secret + body) 输出,即使不知道 secret,也能算出 SHA256(secret + body + padding + evil_data),让 B 以为是合法扩展。
方案二(对):用 HMAC(Hash-based Message Authentication Code),双层哈希封住这个漏洞。
直觉画面:HMAC 的双层结构
公式:HMAC(K, m) = SHA256( (K ⊕ opad) || SHA256( (K ⊕ ipad) || m ) )
- ipad = 0x36 重复到密钥长度("inner padding")
- opad = 0x5C 重复到密钥长度("outer padding")
⊕是异或
为什么这个双层结构能抵御长度扩展:攻击者拿到的是外层哈希的输出,看不到内层哈希的状态——就无法从"外层输出"扩展一个消息使 HMAC 依然合法。SHA-3 因用海绵结构天然免疫,但工程界仍统一走 HMAC 保持算法族无关。
术语与选型
| 算法 | 一句话 |
|---|---|
| HMAC-SHA256 | 事实标准,性能好,安全 |
| HMAC-SHA512 | 更慢但没什么额外收益,除非有特殊合规要求 |
| HMAC-SHA1 | 弃用(SHA-1 本身已破,虽然 HMAC-SHA1 目前仍算安全,但没人会为它买单) |
| Poly1305 | 一次性 MAC(每条消息需一次性密钥);组合 ChaCha20 得 ChaCha20-Poly1305 AEAD |
Go API 示例:
import "crypto/hmac"
import "crypto/sha256"
mac := hmac.New(sha256.New, sharedSecret)
mac.Write([]byte(message))
sig := mac.Sum(nil) // 32 字节的认证码
// 验证时(重要!用 hmac.Equal,防时序侧信道)
ok := hmac.Equal(receivedSig, sig)
验证 MAC 要用「常时间比较」
bytes.Equal(a, b) 会在第一个不同字节就返回,攻击者可以通过测量响应时间逐字节猜出签名。hmac.Equal 保证无论哪里不同都花同样时间。
五、AEAD 内部:为什么"先加密再 MAC"
一分钟大白话
- 是什么:AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密)= 对称加密 + 消息认证一体化。一个函数同时做完"加密、防篡改、抗伪造"三件事。
- 什么时候诞生:概念 2002 年学界提出,AES-GCM 2007 年 NIST 标准化,ChaCha20-Poly1305 2013 年 Google 部署,2018 年 TLS 1.3 只允许 AEAD 套件。
- 解决什么问题:"手拼加密+MAC 太容易写错"——TLS 1.2 时代 BEAST/Lucky13/POODLE 一堆漏洞都是"顺序拼错"导致的,AEAD 把顺序封装在一个函数里,用户不可能拼错。
- 典型场景:TLS 1.3 应用数据、QUIC、Signal 消息加密、SSH 现代套件、AWS S3 SSE-C 客户端加密。
- 一句话选型:有 AES-NI 硬件加速时选 AES-256-GCM,移动端/无硬件时选 ChaCha20-Poly1305。
上面讲了对称、HMAC,现在回头看 AEAD 为什么"一步到位"更安全。要 既加密又认证,历史上有三种拼接顺序,只有一种真正安全:
| 顺序 | 别名 | 是否安全 | 代表 |
|---|---|---|---|
| Encrypt-then-MAC | EtM | ✅ 通用安全 | TLS 1.3、SSH(部分)、AEAD 都是这种 |
| MAC-then-Encrypt | MtE | ⚠️ 历史多次翻车 | TLS 1.2 CBC 套件(BEAST / Lucky13 攻击) |
| Encrypt-and-MAC | E&M | ⚠️ MAC 会泄露明文信息 | 早期 SSH |
为什么 EtM 最优:解密方拿到消息后先验 MAC 再解密。MAC 不对直接丢弃——攻击者无法通过"送畸形密文观察解密行为"来构造侧信道。这是历史上 Padding Oracle 攻击(BEAST/Lucky13/POODLE)的成因:它们都发生在"手拼 MtE"上,因为解密方要先解密到 padding 再验证。
AEAD 就是把 EtM 一体化封装:AES-GCM 内部结构 = CTR 模式加密 + GMAC 认证 + 一起产 tag。你只调 Seal/Open,不可能拼错。
六、数字签名:只有一个人能写
一分钟大白话
- 是什么:签名方用私钥对消息生成一段"签名字节",任何拿到公钥的人都能验证"这消息真是他写的、没被改过"。
- 什么时候诞生:概念随 RSA 1977 年诞生,DSA 1994 年标准化,ECDSA 2005 年,Ed25519 2012 年。
- 解决什么问题:"证明消息来源 + 无法抵赖"——HMAC 双方共享密钥,收方也能签所以可抵赖;数字签名只有私钥持有者能签,事后签名方无法否认。
- 典型场景:TLS 证书(CA 用私钥签服务器证书)、代码签名(Windows 驱动、iOS 应用、Docker 镜像)、JWT RS256/ES256、区块链交易、Git commit 签名(
git commit -S)、软件更新包(APT/Homebrew)。 - 和 HMAC 的关键差别:HMAC 快但双方共享密钥;签名慢但公钥可公开,适合"一方签、多方验"场景。
一分钟大白话
- 是什么:签名方用私钥对消息生成一段"签名字节",任何拿到公钥的人都能验证"这消息真是他写的、没被改过"。
- 什么时候诞生:概念随 RSA 1977 年诞生,DSA 1994 年标准化,ECDSA 2005 年,Ed25519 2012 年。
- 解决什么问题:"证明消息来源 + 无法抵赖"——HMAC 双方共享密钥,收方也能签所以可抵赖;数字签名只有私钥持有者能签,事后签名方无法否认。
- 典型场景:TLS 证书(CA 用私钥签服务器证书)、代码签名(Windows 驱动、iOS 应用、Docker 镜像)、JWT RS256/ES256、区块链交易、Git commit 签名(
git commit -S)、软件更新包(APT/Homebrew)。 - 和 HMAC 的关键差别:HMAC 快但双方共享密钥;签名慢但公钥可公开,适合"一方签、多方验"场景。
故事场景
Alice 要发一个软件更新包给全世界的用户。用户怎么确认"这真的是 Alice 发的,不是黑客伪造"?
用 HMAC 不行——HMAC 需要双方共享密钥,Alice 不可能给每个用户都发一份共享密钥(发出去就等于所有用户都能伪造 Alice 的签名)。
用数字签名:
- Alice 私钥签一次
- 公钥挂在官网人人可下载
- 任何用户拿公钥验签
关键差别:签名做到了 HMAC 做不到的一件事——不可抵赖。因为签名需要私钥,只有 Alice 有;HMAC 双方共享密钥,收方也能签,所以事后 Alice 可以说"这不是我签的"。
直觉画面
签名≠加密
数字签名不加密内容——消息还是明文传输,签名只是附加的一段可验证字节。想要保密还得叠加密(如 AEAD + 签名分层,或直接走 TLS)。
术语与选型
| 算法 | 密钥长度 | 特点 |
|---|---|---|
| RSA-PSS | 2048/3072 bit | 兼容老系统;PSS padding 有安全证明(PKCS#1 v1.5 已不推荐) |
| ECDSA (P-256/384) | 256/384 bit | 短、快;依赖每次签名随机 k,k 泄露/复用 = 私钥泄漏(Sony PS3 案) |
| Ed25519 | 256 bit | 短、快、确定性签名(无 k 随机数依赖)、无坏曲线陷阱——现代默认 |
HMAC vs 数字签名:什么时候用哪个
| 维度 | HMAC | 数字签名 |
|---|---|---|
| 密钥形态 | 双方共享同一把对称密钥 | 私钥(保密)+ 公钥(公开) |
| 信任模型 | 双方互信(任一方泄露即失效) | 单向验证(验证方只需公钥) |
| 可抵赖性 | ✅ 可抵赖(收方也能签,事后可否认) | ❌ 不可抵赖(只有私钥持有者能签) |
| 密钥分发 | 需安全信道预共享 | 公钥可任意公开(配 CA 更好) |
| 性能 | 极快 | 慢 1-2 个数量级 |
| 典型场景 | 内网服务鉴权、API 计费签名 | JWT RS256/ES256、TLS 证书、代码签名 |
七、密钥交换:ECDHE 与前向安全
一分钟大白话
- 是什么:让两个陌生人在公开信道上协商出一个共享秘密,中间人虽然看得到所有消息,但算不出这个秘密。
- 什么时候诞生:1976 年 Diffie & Hellman 论文(DH),1985 年 Miller & Koblitz 提出椭圆曲线版(ECDH),1990s ECDHE 加入 Ephemeral 概念,2018 年 TLS 1.3 只保留 ECDHE。
- 解决什么问题:"陌生人怎么协商对称密钥"——这是密码学史上最重要的问题。DH 之前根本没人相信这能做到。
- 典型场景:TLS 握手(每次连接协商会话密钥)、Signal 端对端加密、WireGuard VPN、SSH 密钥协商。
- 前向安全(PFS):ECDHE 里的 E = Ephemeral(临时) —— 每次会话生成一次性密钥对,用完销毁。长期私钥泄露时,攻击者也解不了历史流量(因为临时密钥早已销毁)。RSA 密钥交换没有这个特性,是 TLS 1.3 直接砍掉它的原因。
一分钟大白话
- 是什么:让两个陌生人在公开信道上协商出一个共享秘密,中间人虽然看得到所有消息,但算不出这个秘密。
- 什么时候诞生:1976 年 Diffie & Hellman 论文(DH),1985 年 Miller & Koblitz 提出椭圆曲线版(ECDH),1990s ECDHE 加入 Ephemeral 概念,2018 年 TLS 1.3 只保留 ECDHE。
- 解决什么问题:"陌生人怎么协商对称密钥"——这是密码学史上最重要的问题。DH 之前根本没人相信这能做到。
- 典型场景:TLS 握手(每次连接协商会话密钥)、Signal 端对端加密、WireGuard VPN、SSH 密钥协商。
- 前向安全(PFS):ECDHE 里的 E = Ephemeral(临时) —— 每次会话生成一次性密钥对,用完销毁。长期私钥泄露时,攻击者也解不了历史流量(因为临时密钥早已销毁)。RSA 密钥交换没有这个特性,是 TLS 1.3 直接砍掉它的原因。
故事场景
回到最开头 Alice 和 Bob 的问题:他们没见过面,怎么协商出一个共享的对称密钥?
RSA 方式:Bob 挂公钥,Alice 用它加密对称密钥发过去。有个隐患:如果 Bob 的私钥明年被偷了,攻击者去年录下的整箱流量今年全部可解(因为对称密钥当时就是用 Bob 那把私钥的公钥加密的)。
Diffie-Hellman 密钥交换的神奇之处:让 Alice 和 Bob 通过公开信道算出一个共享秘密,中间人虽然全程能看到通信,但算不出这个秘密。
直觉画面:ECDHE 一次会话
为什么中间人算不出:给你 a·G,反推 a 需要解"椭圆曲线离散对数难题"——目前没有已知的多项式时间算法(除了量子计算机,见文末 PQC 章节)。
术语解释:ECDHE 里的每个字母
- DH = Diffie-Hellman,1976 年的原始算法
- EC = Elliptic Curve(椭圆曲线),把整数域换成椭圆曲线群,密钥更短更快
- E = Ephemeral(临时)——每次会话生成一次性密钥对,用完即弃
这个 E 就是前向安全的关键:
这就是 TLS 1.3 直接砍掉 RSA 密钥交换、只保留 ECDHE 的原因。
八、KDF:从秘密派生子密钥
一分钟大白话
- 是什么:Key Derivation Function(密钥派生函数)——把一个"秘密"变成一把或多把"密钥"。
- 两种截然不同的 KDF:
- 快 KDF(HKDF):追求快、追求安全域分离。从 ECDH 共享秘密派生 TLS 会话密钥就是它。
- 慢 KDF(Argon2id / bcrypt / scrypt / PBKDF2):追求故意慢。用于存密码——让攻击者拖库后爆破一个密码要花几十年。
- 什么时候诞生:PBKDF2 2000 年、bcrypt 1999 年、HKDF 2010 年 RFC 5869、scrypt 2009 年、Argon2 2015 年(PHC 冠军)。
- 典型场景:
- HKDF:TLS 1.3 会话密钥派生、Signal 协议、Noise 协议
- Argon2id:用户密码存 DB、加密卷密码派生密钥
- 红线:存密码永远用慢 KDF + 加盐——直接用 SHA-256 存密码 = 拖库后分钟级被破。
一分钟大白话
- 是什么:Key Derivation Function(密钥派生函数)——把一个"秘密"变成一把或多把"密钥"。
- 两种截然不同的 KDF:
- 快 KDF(HKDF):追求快、追求安全域分离。从 ECDH 共享秘密派生 TLS 会话密钥就是它。
- 慢 KDF(Argon2id / bcrypt / scrypt / PBKDF2):追求故意慢。用于存密码——让攻击者拖库后爆破一个密码要花几十年。
- 什么时候诞生:PBKDF2 2000 年、bcrypt 1999 年、HKDF 2010 年 RFC 5869、scrypt 2009 年、Argon2 2015 年(PHC 冠军)。
- 典型场景:
- HKDF:TLS 1.3 会话密钥派生、Signal 协议、Noise 协议
- Argon2id:用户密码存 DB、加密卷密码派生密钥
- 红线:存密码永远用慢 KDF + 加盐——直接用 SHA-256 存密码 = 拖库后分钟级被破。
故事场景
Alice 和 Bob 通过 ECDHE 算出了一段共享秘密 S(比如 32 字节)。他们需要:一把加密密钥、一把 MAC 密钥、一个初始 IV……能不能直接把 S 切成几段用?
不能。原因:
S是椭圆曲线上一个点的 x 坐标,分布不均匀(不是均匀随机的),直接切片会让某些子密钥有偏。- 不同用途密钥必须"域分离"——否则会有跨用途的攻击(拿加密密钥当 MAC 密钥用能出事)。
HKDF(HMAC-based Key Derivation Function,RFC 5869)就是干这个的:
直觉画面:HKDF 两步
- Extract:
PRK = HMAC(salt, S)——把熵不均匀的输入压成均匀伪随机 - Expand:
OKM = HMAC(PRK, info || counter)——按需展开任意长子密钥,info做域分离
TLS 1.3、Noise 协议、Signal 协议全部用 HKDF。为什么不直接 SHA-256 派生:SHA-256 不是 PRF,直接切片没有形式化安全证明;HKDF 有 IETF 标准证明。
密码存储:为什么要"故意慢"
普通 KDF 追求快,密码存储的 KDF 反着来——故意慢,才能抗离线爆破。
场景:DB 被拖了,攻击者拿到一堆 (用户名, 密码哈希)。他想还原你的密码,只能一个一个试:
- 如果你的密码哈希是
SHA-256(password):GPU 集群每秒能试数百亿个密码,一个 8 位密码几分钟就爆掉 - 如果是
Argon2id(password, salt, mem=64MB, iter=3):GPU 每次尝试要花 100ms + 64MB 显存,同样 8 位密码要几十年
| 算法 | 内存硬 | 状态 |
|---|---|---|
| PBKDF2 | ❌ | 老,只能调迭代次数;FIPS 白名单 |
| bcrypt | ❌ | 72 字节输入截断坑;曾是默认 |
| scrypt | ✅ | 时间 + 内存 |
| Argon2id | ✅ | 2015 PHC 冠军,当前推荐默认 |
必配加盐:每个密码一份随机 salt,与哈希一起存 DB 明文即可。salt 不是"秘密",它的作用是让并行爆破无法共享预计算表——每个密码都得单独跑一遍完整的 Argon2。
九、TLS 1.3 握手:把上面所有东西串起来
一分钟大白话
- 是什么:TLS(Transport Layer Security)是 HTTPS 的底层加密协议。你打开一个
https://页面,浏览器和服务器就在你不知情时做完了 TLS 握手。 - 演进史:SSL 2.0 (1995) → SSL 3.0 (1996) → TLS 1.0 (1999) → TLS 1.1 (2006) → TLS 1.2 (2008) → TLS 1.3 (2018)。SSL 早年漏洞百出,已被彻底淘汰;TLS 1.0/1.1 也在 2020 年被主流浏览器禁用。现在只用 TLS 1.2/1.3。
- 一次握手做了三件事:
- 协商密钥(ECDHE):算出一个只有你俩知道的对称密钥。
- 认证服务器(证书 + 签名):确认对方真的是
google.com,不是黑客冒充。 - 派生密钥(HKDF):把共享秘密变成加密密钥 + MAC 密钥。
- 握手完之后:所有应用数据走 AEAD(AES-GCM 或 ChaCha20-Poly1305),高速对称加密。
- 1.3 相对 1.2 的四大改进:只留 5 个套件(全 AEAD + PFS)、1-RTT 握手(比 1.2 少一轮)、握手全加密、签名与密钥交换解耦。
一分钟大白话
- 是什么:TLS(Transport Layer Security)是 HTTPS 的底层加密协议。你打开一个
https://页面,浏览器和服务器就在你不知情时做完了 TLS 握手。 - 演进史:SSL 2.0 (1995) → SSL 3.0 (1996) → TLS 1.0 (1999) → TLS 1.1 (2006) → TLS 1.2 (2008) → TLS 1.3 (2018)。SSL 早年漏洞百出,已被彻底淘汰;TLS 1.0/1.1 也在 2020 年被主流浏览器禁用。现在只用 TLS 1.2/1.3。
- 一次握手做了三件事:
- 协商密钥(ECDHE):算出一个只有你俩知道的对称密钥。
- 认证服务器(证书 + 签名):确认对方真的是
google.com,不是黑客冒充。 - 派生密钥(HKDF):把共享秘密变成加密密钥 + MAC 密钥。
- 握手完之后:所有应用数据走 AEAD(AES-GCM 或 ChaCha20-Poly1305),高速对称加密。
- 1.3 相对 1.2 的四大改进:只留 5 个套件(全 AEAD + PFS)、1-RTT 握手(比 1.2 少一轮)、握手全加密、签名与密钥交换解耦。
这是最能看清"原语如何组合成协议"的地方。看完这张时序图,前面每一节都会瞬间连起来:
每个原语在做什么(对照前面各节):
- ECDHE(第七节):交换密钥份额,双方各自算共享秘密
g^(ab)= 会话根秘密 - HKDF(第八节):从根秘密派生一堆子密钥
- 服务器证书 + CA 签名链(第六节 + 下面证书章):证明"这个公钥属于该域名"
- CertificateVerify(第六节):服务器用证书私钥签整个握手记录 → 防中间人拿别人证书冒充
- Finished(第四节):用 HKDF 派生的 MAC 密钥做 HMAC → 校验握手过程未被篡改
- Application Data(第一节):AEAD 加密应用数据
TLS 1.2 → 1.3 的四条大改动:
- 只留 5 个套件:全部 AEAD + PFS,砍掉 RSA 交换、CBC、RC4、MD5、SHA-1、导出级弱套件。
- 1-RTT 握手(1.2 是 2-RTT):ClientHello 直接带上密钥份额;支持 0-RTT 恢复(但有重放风险,不能用于非幂等)。
- 握手全加密:ServerHello 之后所有帧(含证书)都在派生密钥下加密。
- 签名与密钥交换解耦:1.2 密码套件同时选中三者组合爆炸,1.3 拆开分别协商。
十、JWT / mTLS / 证书链
一分钟大白话
- JWT(JSON Web Token):自包含的签名令牌——一段编码后的 JSON + 签名字节,携带用户身份和权限信息。服务端不用查 DB,验签就知道是不是合法用户。典型场景:SSO 单点登录、微服务鉴权、API 访问令牌。注意:JWT payload 不加密,只签名——想保密请走 HTTPS。
- mTLS(mutual TLS):双向 TLS——普通 TLS 只验证服务器,mTLS 客户端也提交证书,服务端也验证。典型场景:服务网格内部通信(Istio 用 SPIFFE ID 给每个服务发短期证书)、B2B API、金融专线。是"零信任内网"的技术底座。
- 证书链:一张
example.com的证书是中间 CA 签的,中间 CA 是根 CA 签的,根 CA 预装在你的操作系统里。这套逐级验签就叫证书链(也叫 PKI 公钥基础设施)。CA 可以吊销证书,通过 CRL / OCSP / OCSP Stapling 三种机制通告吊销状态。 - HS256 vs RS256 一句话:内网选 HS256(HMAC,快),对外选 RS256/ES256(数字签名,公钥可公开)。永远拒绝
alg: none。
JWT:一张自包含的签名令牌
Base64URL(header) . Base64URL(payload) . Base64URL(signature)
alg=RS256 {sub:123,...} RSA/HMAC 签名
payload 不加密——只是签名保完整性。需要保密请套 JWE 或走 HTTPS。
HS256 vs RS256 选型:
| 场景 | 选 | 理由 |
|---|---|---|
| 内网服务间 / 单发单验 | HS256(HMAC-SHA256) | 一把共享密钥,简单快;但验签方能签,不适合多方 |
| 对外 API / SSO / 多方验签 | RS256 或 ES256 | 私钥只在签发方;公钥可通过 JWKS 公开分发 |
JWT 两大历史坑
alg: none攻击:早期库若信任 header 里的alg字段,攻击者把alg改成none就能提交无签名 token。修复:服务端 hard-code 期望算法。- 算法降级:把
alg: RS256改成alg: HS256,用服务器公钥当 HMAC 密钥重签——某些库会把公钥误当共享密钥验签成功。修复:验证前显式绑定算法族。
mTLS:双向 TLS
普通 TLS:只验证服务器证书(防冒充服务器),客户端身份靠应用层认证(用户名密码 / API Key)。
mTLS:客户端也提交证书 → 服务端也验证——双向证书身份。典型场景:
- 服务网格内部身份(Istio / Linkerd 用 SPIFFE ID)——每个服务发一张短期证书,服务间调用全量 mTLS,是"零信任内网"的技术底座
- B2B API
- 金融专线
证书链:谁给谁背书
客户端验证:从叶子证书往上一层层验签,一直验到"内置的可信根证书"停。任何一层验签失败 = 证书不可信。
证书吊销机制(如果 CA 想中途取消一张证书):
| 机制 | 谁维护 | 谁去查 | 代价 |
|---|---|---|---|
| CRL(吊销列表) | CA 定期发布全量列表 | 客户端下载 | 列表大、更新滞后 |
| OCSP | CA 维护实时状态 | 客户端 → OCSP 服务器实时查询 | 增加 RTT + 隐私泄露(CA 知道你访问了谁) |
| OCSP Stapling | CA 签发 OCSP 应答 | 服务器代查、握手时把应答附上 | 无隐私泄露、无额外 RTT,事实标准 |
SSH host key 的 TOFU(Trust-On-First-Use)反例:SSH 没有 CA,客户端"第一次连接时把 host key 记下来",后续变了就报警。适用于无中心信任锚的场景,代价是"首次连接必须可信信道"。
为什么这么做
- 为什么工程上永远是"非对称谈钥匙 + 对称跑数据":性能差 5 个数量级,全程非对称不可用;纯对称又无法在不安全信道首次交换密钥。混合方案取两者之长,是唯一工程可行的组合。
- 为什么 AEAD 淘汰"CBC + HMAC 手拼":AEAD 把 EtM 一体化封装,杜绝"实现顺序错 / 侧信道 / 中间态泄露"三类历史事故(Padding Oracle、BEAST、Lucky13 都发生在"手拼"上)。TLS 1.3 只留 AEAD 是必然。
- 为什么 HMAC 不是"哈希 + 密钥拼接":MD5/SHA-1/SHA-256 的 Merkle-Damgård 结构存在长度扩展攻击。HMAC 的双层结构让内外哈希独立、抵御扩展,且有形式化证明。
- 为什么现代默认 ECDHE 而非 RSA 密钥交换:ECDHE 提供前向安全——今天服务器被攻破偷走私钥,无法反解历史流量;RSA 密钥交换则历史抓包整箱可解。
- 为什么密码存储要慢哈希 + 加盐:SHA-256 单核每秒数亿次,拖库后 GPU 集群离线爆破仅需分钟级;Argon2id 故意消耗 100ms + 数十 MB 内存,把爆破成本抬到 GPU 都跑不动。加盐让每条哈希不能共享预计算表。
- 为什么 JWT 用 RS256 更适合对外 SSO:私钥只在身份提供方(IdP),所有依赖方(Relying Party)用公钥验签——不需要每加一个业务方就派发密钥,运维大幅简化。
- 为什么 TLS 1.3 只留 5 个密码套件:过去"配置一堆套件、按需协商"给了降级攻击(POODLE / FREAK / Logjam)可乘之机。TLS 1.3 直接把不安全套件从标准里删干净,实现无从"协商到弱套件"。
为什么别的选择不行
- 为什么不干脆全程用非对称加密(省得混合):性能差 5 个数量级——AES-GCM 单核 GB/s,RSA-2048 单核每秒几千次;1MB 数据 RSA 加密数百 ms,全程非对称 = TLS 卡死。且 RSA 一次最多加密(密钥长度 / 8)字节,本身不适合传大数据。
- 为什么不用"哈希 + 密钥拼接"自制 MAC:对 SHA-1/2 存在长度扩展攻击——攻击者不知道 secret 也能扩展你的消息并算出合法 MAC。历史上出过 Flickr API 签名漏洞(2009)。正解是 HMAC。
- 为什么内部服务用 HS256、不图省事全上 RS256:RS256 验签比 HMAC 慢一个数量级——内网每秒万级 QPS 的鉴权累积可观 CPU;且内部服务共享密钥可控,HMAC 的"共享密钥缺陷"在信任域内是可接受权衡。
- 为什么密码不用 AES 加密存 DB、要用慢哈希:一是"加密"意味着可解密——DB 泄露 = 密码全泄;二是密码本身不需要还原,只需"验证一致",单向哈希才是对的语义。可逆加密只用于确实需要还原的字段(如银行卡号)。
- 为什么不继续用 MD5 做安全场景:签名/证书/密码存储零容忍已破算法;非对抗性寻址场景(Git 内部指针、CDN 缓存 key)碰撞代价可控,暂未强制升级。
- 为什么不给 JWT 加密(JWE)替代签名:签名的目标是可公开验证,加密的目标是保密——两者正交。签名 + HTTPS 已同时提供完整性与传输保密性;JWE 只在"传输后仍需保密载荷"(如票据经不可信中转)时才用。且 JWE 复杂度高、库实现质量参差。
沉淀结论
三张辨析对照表(面试速答模板)
表 1 · RSA 加密 vs RSA 签名
| 维度 | RSA 加密 | RSA 签名 |
|---|---|---|
| 发送方(做动作) | 用接收方公钥 | 用自己私钥 |
| 接收方(验/解) | 用自己私钥 | 用发送方公钥 |
| 目的 | 保密(只有私钥拥有者能读) | 认证 + 不可抵赖 |
| 典型 API(Go) | rsa.EncryptOAEP / rsa.DecryptOAEP | rsa.SignPSS / rsa.VerifyPSS |
| 记忆 | 公锁私开 | 私签公验 |
表 2 · HMAC vs 数字签名
| 维度 | HMAC | 数字签名 |
|---|---|---|
| 密钥形态 | 双方共享同一把对称密钥 | 私钥 + 公钥 |
| 信任模型 | 双方互信(谁都能签、谁都能验) | 单向(只有签名方能签,验证方只需公钥) |
| 可抵赖 | ✅ 可抵赖(接收方也能伪造) | ❌ 不可抵赖(除私钥持有者外无人能签) |
| 密钥分发 | 需安全信道预共享 | 公钥可任意公开(配 PKI 更好) |
| 性能 | 极快 | 慢 1-2 个数量级 |
| 典型场景 | 内网服务鉴权、API 计费签名(见 business-proxy) | JWT RS256/ES256、TLS 证书、代码签名 |
表 3 · 哈希 vs MAC vs 签名 能力矩阵
| 能力 | 哈希(SHA-256) | MAC(HMAC / AEAD tag) | 数字签名 |
|---|---|---|---|
| 抗篡改(无意/传输错误) | ✅ | ✅ | ✅ |
| 抗伪造(攻击者无密钥不能改) | ❌(谁都能重算) | ✅(需密钥) | ✅(需私钥) |
| 不可抵赖(签名方无法否认) | ❌ | ❌(收方也能签) | ✅(只有私钥持有者能签) |
面试快速反应表(场景 → 原语选择)
| 场景 | 首选 | 备选 | 一句话理由 |
|---|---|---|---|
| 内网服务鉴权 / API 计费签名 | HMAC-SHA256 | HMAC-SHA512 | 对称高性能、双方互信可接受 |
| 对外公开 API 令牌 / SSO 断言 | RS256 或 ES256 | Ed25519(EdDSA) | 一方签、多方验、公钥可公开 |
| 客户端密码存 DB | Argon2id | bcrypt / scrypt | 慢 + 加盐 + 抗 GPU |
| 传输机密 + 完整性一体 | AES-256-GCM | ChaCha20-Poly1305(移动/无硬件加速) | AEAD 一步到位 |
| 文件完整性校验 / Git-style 寻址 | SHA-256 | BLAKE3(追求速度时) | 只需检测无意损坏 |
| 密钥派生(从共享秘密派生多子密钥) | HKDF-SHA256 | HKDF-SHA512 | 有形式化证明,TLS 1.3 官选 |
| 长期归档 / 不可抵赖法律证据 | Ed25519 签名 + 时间戳服务 | RSA-PSS | 短、快、无坏 k 坑 |
面试速答清单
- 三大原语:对称快 / 非对称慢 / 哈希不可逆——工程组合永远是"非对称谈钥匙 + 对称跑数据 + 哈希做指纹"
- RSA 加密与签名钥匙方向相反:加密公钥锁、私钥开;签名私钥签、公钥验
- AEAD(GCM / ChaCha20-Poly1305)= 加密 + 完整性 + 认证一体,tag 拼密文尾,Open() 先验后拆
- Nonce 复用等同密钥泄漏(GCM 场景)
- HMAC ≠ "哈希 + 密钥拼接":双层结构防长度扩展
- 签名 ≠ HMAC:信任模型差在"可抵赖 / 不可抵赖"、"共享 / 私有"
- ECDHE 的 E 是 Ephemeral:每次会话临时密钥 → 前向安全
- HKDF 两步 Extract / Expand,
info做域分离 - 密码存储 Argon2id + 加盐;SHA-256 单调直存 = 被 GPU 秒破
- JWT 用 HS256(内网单验)或 RS256/ES256(对外多验),永远拒绝
alg: none - TLS 1.3 只留 5 个套件,全 AEAD + PFS,砍 RSA 交换和 CBC
关于 PQC / 同态 / 零知识证明
抗量子密码(NIST 已标准化的 Kyber KEM、Dilithium 签名)、全同态加密(BFV/CKKS)、零知识证明(zk-SNARK/STARK)是独立且深的方向,本篇作为面试基础不深挖;工程侧 2024 起 TLS 已开始试点 X25519+Kyber 混合密钥交换(Chrome / Cloudflare),面试遇到只需知道"存在混合过渡方案、保守不激进"即可。
记忆口诀
- 三大原语:对称快跑数据 / 非对称慢谈钥匙 / 哈希单向做指纹
- RSA 钥匙方向:加密"公锁私开",签名"私签公验",方向反了就废
- AEAD 一步到位:GCM/ChaCha20-Poly1305 = 加密 + MAC 一体,tag 拼密文尾
- Nonce 复用 = 泄漏:GCM 同密钥下 Nonce 重复即等同密钥公开
- 哈希 ≠ MAC ≠ 签名:能防篡改/防伪造/防抵赖,三项能力逐级递增
- ECDHE 的 E:Ephemeral 临时密钥,用完即弃 → 前向安全
- HKDF 两步:Extract 压熵 + Expand 分域
- 密码存储:Argon2id + 加盐;MD5/SHA-256 直存等于裸奔
- JWT 三坑:
alg: none/ 算法降级 / HS256 对外滥用 - TLS 1.3 收敛:只留 AEAD + PFS,砍掉 RSA 交换与 CBC
内容来源
综合整理自:RFC 5246 (TLS 1.2) / RFC 8446 (TLS 1.3) / RFC 2104 (HMAC) / RFC 5869 (HKDF) / RFC 7519 (JWT) / RFC 8017 (RSA-PSS) / RFC 8032 (EdDSA) / RFC 8439 (ChaCha20-Poly1305)、NIST SP 800-38A/D (AES 模式与 GCM)、NIST SP 800-131A (弃用清单)、PHC (Password Hashing Competition, Argon2)、《Cryptography Engineering》(Ferguson / Schneier / Kohno)、Cloudflare / Google 公开博客关于 PQC 混合过渡的部署报告。加密原语跨专题引用点:TLS 协议层视角见本站 http-tls-rpc,接入网关加密落地见 access-gateway,计费签名工程实践见 business-proxy。请以各 RFC 与 NIST 标准原文为准。
自测:合上资料能说清楚吗?
- RSA 加密和 RSA 签名分别用哪把钥匙?为什么"钥匙方向相反"?
参考答案
加密:发送方用接收方公钥加密,接收方用自己私钥解密——公钥人人可取,只有私钥拥有者能读。签名:签名方用自己私钥签名,验证方用签名方公钥验证——只有私钥拥有者能签、公钥可公开验证。两个操作用的"公私钥语义"正好相反:加密需要"只有一个人能读"→ 用接收方钥匙对;签名需要"只有一个人能写"→ 用签名方钥匙对。
- 为什么 AES-GCM 的 Nonce 绝对不能重复?重复了会发生什么?
参考答案
GCM 内部是 CTR 模式 + GMAC。CTR 用 AES(Key, Nonce || counter) 生成密钥流,再与明文 XOR。同密钥下 Nonce 复用意味着两条消息用同一密钥流 XOR 加密,攻击者拿到两条密文相 XOR 就直接消掉密钥流、得到两条明文的 XOR——只要知道其中一条(或部分),另一条就恢复。更糟的是 GHASH 的认证密钥 H 也随之泄露,可任意伪造 tag。所以 GCM 的 Nonce 要么随机(96-bit 空间足够)要么单调计数器,但同一密钥下绝不复用。
- 为什么"哈希 + 密钥拼接"(
SHA256(K || m))不是合法的 MAC?HMAC 怎么修的?
参考答案
SHA-2 家族用 Merkle-Damgård 结构,存在长度扩展攻击:攻击者拿到 SHA256(K || m) 输出,即使不知道 K,也能计算 SHA256(K || m || padding || m') ——所以能伪造合法 MAC。HMAC 用双层结构 H( (K ⊕ opad) || H( (K ⊕ ipad) || m ) ) ——外层再哈希一次,让攻击者拿到的最终输出不再是"内部哈希状态",长度扩展无从下手。SHA-3 因用海绵结构天然免疫,但工程界仍统一走 HMAC 以保持算法族无关。
- 什么是前向安全(PFS)?ECDHE 相对 RSA 密钥交换的核心差异是什么?
参考答案
前向安全指长期私钥泄露也无法解开历史流量。RSA 密钥交换:客户端用服务器公钥加密 pre-master 发过去——服务器长期私钥若泄,历史抓包全部可解,无 PFS。ECDHE:双方每次会话各自生成一次性 ECDH 密钥对(Ephemeral 就是这个 E),协商完销毁,历史会话根本不依赖长期私钥来解密——长期私钥只用于签名 ECDHE 参数证明服务器身份,泄露后未来能被冒充,但过去的会话安全。这就是 TLS 1.3 直接砍掉 RSA 交换的原因。
- JWT 用 HS256 还是 RS256?
alg: none和算法降级攻击是怎么回事?
参考答案
HS256(HMAC-SHA256)适合内网单发单验——双方共享一把密钥,快但共享密钥泄露即失效、验证方能签。RS256(或 ES256/Ed25519)适合对外多方验签 / SSO——签发方持私钥、依赖方拿公钥(JWKS 分发)验签,公钥可任意公开。alg: none 攻击:早期库信任 header 里的 alg,攻击者改成 none 提交无签名 token 就蒙混过关;算法降级:alg: RS256 改成 alg: HS256,用服务器公钥作 HMAC 密钥重算——某些库会用公钥当"共享密钥"验签成功。修复:服务端 hard-code 期望算法、绝不信任 header 里的 alg。
- 密码存 DB 为什么必须用 Argon2id / bcrypt 这类慢哈希,而不是 SHA-256?
参考答案
SHA-256 单核每秒数亿次,GPU 集群每秒数百亿次——攻击者拖库后离线爆破 8 位密码只需分钟级。慢哈希(Argon2id / bcrypt / scrypt)故意消耗时间 + 内存(Argon2id 单次可配 100ms + 数十 MB),把 GPU/ASIC 的规模优势打掉,爆破成本抬到经济不可行。加盐是另一半:每密码一份随机 salt 与哈希一起存,让并行爆破无法共享预计算表——每个密码都得单独跑一遍。目标不是"存不可逆的哈希"(SHA-256 也不可逆),而是"存对离线爆破足够昂贵的哈希"。
- AEAD 的 tag 到底藏在哪里?Open() 失败时能拿到部分明文吗?
参考答案
以 AES-GCM 为例:Seal(nonce, plaintext, aad) → ciphertext || tag(tag 一般 16 字节,拼在密文末尾)。传输时 nonce || ciphertext || tag 一起送。解密时 Open() 先用 GHASH 重算 tag、与收到的 tag 比对,不合直接返回 error,绝不吐半个字节明文——这是 AEAD 相对"CBC + HMAC 手拼"的关键防御边界:杜绝 Padding Oracle 类侧信道(攻击者无法通过观察"解密到一半失败"的行为差异来构造攻击)。这就是"先验后拆(Encrypt-then-MAC)"的一体化实现。
- mTLS 和普通 TLS 的差别是什么?为什么服务网格喜欢用它?
参考答案
普通 TLS:客户端验证服务器证书(防冒充服务器),客户端身份靠应用层认证(用户名密码 / API Key / JWT)。mTLS:客户端也提交证书,服务端也验证——双向证书身份,把身份认证下沉到传输层。服务网格(Istio / Linkerd)用 SPIFFE ID 给每个服务发一张短期证书,服务间调用全量 mTLS——一是自动获得零信任内网身份(不再靠 IP 或 header 声明"我是谁")、二是加密与身份统一在 sidecar / eBPF 数据面完成、三是证书轮转自动化。这就是"零信任内网"的技术底座。
- TLS 1.3 相对 1.2 到底改了哪些密码学决定?
参考答案
四条大改动:一,砍套件——只留 5 个 AEAD + PFS 套件,删除 RSA 密钥交换、CBC 模式、RC4、MD5、SHA-1、导出级弱套件。二,握手 1-RTT(1.2 是 2-RTT),ClientHello 就带上密钥份额,避免"先协商再交换"的额外一轮;且支持 0-RTT 会话恢复(但 0-RTT 数据有重放风险,不能用于非幂等请求)。三,握手全加密——ServerHello 之后所有帧(含证书)都在派生密钥下加密,防握手中间人窥探证书内容。四,签名与密钥交换解耦——1.2 密码套件同时选中签名算法 + 密钥交换 + AEAD,组合爆炸;1.3 拆开分别协商,简化实现。
- 抗量子密码(PQC)是不是需要现在立刻切?工程上怎么过渡?
参考答案
不需要立刻全切。量子计算机对RSA / ECDH / ECDSA 有致命威胁(Shor 算法一次性搞定大整数分解与离散对数),但对对称密钥 + 哈希 只是安全强度减半(Grover 加速),AES-256、SHA-256 层面仍安全。过渡策略是"混合密钥交换":TLS 1.3 已试点 X25519+Kyber 组合密钥交换(Chrome、Cloudflare、AWS 2024 起大规模灰度),共享秘密由两者拼接派生 → 只要其中一个没被破就仍安全,兼顾"实用性能"与"抗量子过渡"。签名侧 NIST 已标准化 Dilithium,但证书体系迁移周期以年计。工程侧当前只需知道"有过渡方案、走混合、不激进推 PQC-only"即可。