引言
加密的世界里,最危险的不是「没有加密」,而是「加密了但用错了」——用了 ECB 模式、把哈希当加密、自己发明 MAC、把私钥提交进仓库、证书链少装中间证书。这些坑的共同点是:代码能跑、测试能过、上线后才在安全审计或事故里暴露。本文把对称加密、非对称加密、哈希与 HMAC、数字签名、证书链与 TLS 校验串成一条线,再配一份可直接复制的 openssl 速查,最后落到密钥管理与常见红线,让你既能动手操作,也知道每一步为什么安全。
前置:/others-hashing-guide/(哈希函数与碰撞)、/others-binary-encoding-tools/(Base64/Hex 与字节)。传输与证书运维见 network 专题。
目录
- 1. 对称加密:块与流、模式与填充
- 2. 非对称加密与密钥交换
- 3. 哈希与 HMAC:完整性与认证
- 4. 数字签名与证书链
- 5. TLS 握手与证书校验
- 6. openssl 命令速查
- 7. 密钥与证书管理实践
- 8. 常见误用与安全红线
- 9. 密钥轮换与存储
- 10. 速查表与一句话记忆
- 延伸阅读
1. 对称加密:块与流、模式与填充
对称加密用同一把密钥加解密,快,适合大数据量。核心是「算法 + 模式 + 填充」三件套。
算法:AES(128/192/256 位,首选)、ChaCha20(移动端/无 AES 加速时首选)
DES/3DES/RC4 已淘汰,禁用
模式(决定多个块怎么串):
ECB 每块独立加密 → 相同明文出相同密文 → 泄露结构,禁用
CBC 前一块密文参与下一块 → 需 IV,串行,易受填充预言攻击
CTR 计数器模式 → 可并行,变流加密,绝不能重用 nonce
GCM 带认证的 AEAD → 推荐,一次给出机密性 + 完整性
填充与 IV:
填充:块加密要求明文长度是块大小(AES=16 字节)的整数倍
PKCS#7 最常见(缺 n 字节就填 n 个 n);错误的填充校验导致 Padding Oracle
IV:CBC/CTR 都需要,必须"每次加密随机且唯一";IV 不需保密,但绝不能固定
GCM 的 nonce 重用 = 灾难(密钥流复用,明文可被还原)
AEAD 才是正解:同时提供机密性与完整性,别自己拼「加密 + MAC」。推荐 AES-256-GCM 或 ChaCha20-Poly1305:一次调用同时产出密文与认证标签(tag),解密时校验 tag 失败就直接拒绝(不返回任何明文)。
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
nonce = os.urandom(12) # 每次加密必须唯一
aes = AESGCM(key)
ct = aes.encrypt(nonce, b"secret data", b"associated-data")
pt = aes.decrypt(nonce, ct, b"associated-data") # tag 校验失败会抛异常
记忆:对称加密用「算法+模式+填充」——ECB 泄露结构禁用、CBC 要随机 IV 且防填充预言、CTR/GCM 的 nonce 绝不能重用;优先用 AEAD(AES-GCM/ChaCha20-Poly1305),别自己拼加密+MAC。
2. 非对称加密与密钥交换
非对称加密用公钥加密、私钥解密(或反过来签名),慢,但解决了「密钥分发」难题。
RSA:基于大整数分解,密钥 2048/3072/4096 位
加密用途已基本被"密钥交换 + 对称加密"取代,签名场景仍广泛
ECC:更短密钥达同等强度(256 位 ECC ≈ 3072 位 RSA),证书更小
P-256(secp256r1)通用;Ed25519 用于签名(快且安全)
强度对照:RSA-2048 ≈ ECC-224;RSA-3072 ≈ ECC-256;RSA-7680 ≈ ECC-384
非对称不直接加密大数据,而是用于密钥交换与签名:
密钥交换(TLS 握手里):双方协商出"会话密钥",之后用对称加密传数据
ECDHE(临时椭圆曲线 DH)→ 提供前向保密(Forward Secrecy)
签名:私钥签、公钥验 → 证明"消息来自私钥持有者且未被篡改"
算法:RSA-PSS / ECDSA / Ed25519
前向保密为什么重要:无 PFS(如纯 RSA 密钥传输)时,攻击者录下密文 + 日后拿到服务器私钥 → 解开历史全部流量;有 PFS(ECDHE)时每次会话密钥独立且用后即弃 → 私钥泄露也解不开历史。现代 TLS 配置强制 ECDHE。
记忆:非对称慢,只用于「密钥交换」与「签名」——ECC(P-256/Ed25519)比 RSA 更短更快更安全;TLS 用 ECDHE 做密钥交换提供前向保密,即使私钥泄露也解不开历史流量。
3. 哈希与 HMAC:完整性与认证
哈希保证完整性(没被改),HMAC 在哈希基础上加密钥,保证认证(确实是持钥者发的)。
哈希(SHA-256 等):任何人可计算 → 只能防误改,不能防篡改
攻击者改了内容后重新算哈希 → 校验通过 → "哈希 ≠ 认证"
HMAC:HMAC(K, M) = H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ M))
需要密钥才能算 → 攻击者不知道 K 就伪造不了
用途:API 签名、Webhook 校验、JWT HS256、下载链接防篡改
为什么不能「哈希(密钥 + 消息)」:朴素拼接易受长度扩展攻击(length extension),HMAC 的双重哈希结构专门防它。
import hmac, hashlib
mac = hmac.new(key, msg, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(mac, received_mac) # 恒定时间比较,防时序攻击
密码存储 ≠ 普通哈希:密码要用慢哈希 + 盐(BCrypt/Argon2/scrypt)——别用 SHA-256 存密码(太快,GPU 秒破)、每个用户独立随机盐(防彩虹表)、比较用恒定时间函数。
| 场景 | 推荐 |
|---|---|
| 消息完整性 + 认证 | HMAC-SHA256 |
| 加密 + 完整性一体 | AES-GCM / ChaCha20-Poly1305 |
| 密码存储 | Argon2id / BCrypt |
| 内容寻址/去重 | SHA-256(无需密钥) |
| 签名(不可否认) | Ed25519 / RSA-PSS |
记忆:哈希防误改、HMAC 防伪造(需密钥)——用 HMAC-SHA256 而非哈希拼接(防长度扩展),验证用恒定时间比较(防时序攻击);密码存储用慢哈希+盐(Argon2id/BCrypt),别拿 SHA-256 存密码。
4. 数字签名与证书链
数字签名 = 私钥加密摘要 + 公钥验签,提供完整性 + 认证 + 不可否认。
签名:1) 对消息算摘要 H(M) 2) 用私钥对摘要签名 3) 附上 signature 与公钥/证书
验签:1) 用公钥解出摘要 2) 自己算 H(M) 比对 → 一致则签名有效
签名 ≠ 加密:签名"私钥签、公钥验"(谁都能验,证明来源)
加密"公钥加、私钥解"(只有持有者能读)
证书 = 被 CA 签名的公钥 + 身份信息:
证书内容:Subject(身份/域名)、Subject Public Key、Issuer、Validity、
Extensions(SAN 域名、Key Usage、Basic Constraints)、Signature
信任链(chain of trust):
根 CA(自签,预置在系统/浏览器信任库)
└─ 中间 CA(被根 CA 签)
└─ 站点证书(被中间 CA 签)
验证时逐级向上验签,直到某个"受信任的根"
SAN 才是域名的正主:现代校验看 Subject Alternative Name,CN 已废弃。一张证书可含多个 SAN(DNS:example.com, DNS:api.example.com),通配符 DNS:*.example.com 只覆盖一级子域。为什么链必须完整:服务器要发送「站点证书 + 中间证书」,浏览器只预置了根证书;少发中间证书时浏览器能自动补全(AIA),但很多客户端/移动端不能 → 报「证书链不完整」。
记忆:签名是「私钥签、公钥验」证明来源且不可否认;证书是被 CA 签名的公钥,信任链从站点证书逐级验到预置的根;域名认 SAN 不认 CN,服务器必须发全「站点+中间」证书,否则移动端报链不完整。
5. TLS 握手与证书校验
TLS 握手的目标:认证服务器(可选认证客户端)+ 协商出会话密钥(前向保密)。
TLS 1.2(简化):ClientHello(套件/随机数/SNI)→ ServerHello + Certificate
→ ClientKeyExchange(ECDHE 公钥)→ ChangeCipherSpec + Finished
TLS 1.3(更快):1-RTT 甚至 0-RTT,砍掉冗余往返;仅保留 AEAD 套件;
默认强制前向保密(ECDHE)
证书校验的完整步骤(客户端侧):
1. 有效期:当前时间在 notBefore~notAfter 之间
2. 信任链:逐级验签直到受信任根
3. 域名匹配:请求域名命中 SAN(含通配符规则)
4. 用途:Key Usage/Extended Key Usage 允许 serverAuth
5. 吊销:CRL / OCSP(或 OCSP Stapling)
6. 签名算法强度:拒绝 SHA-1、弱密钥
排错时最常踩的四个坑:证书链不完整(服务器没发中间证书)、域名不匹配(SAN 没包含该域名或通配符层级错)、过期(忘记续期)、时间偏差(客户端系统时钟错,导致「尚未生效」)。
# 只看证书链与有效期
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# 用 curl 快速验证(-v 打印证书信息与链错误)
curl -vI https://example.com 2>&1 | grep -Ei 'SSL|certificate|expire'
记忆:TLS 握手 = 认证 + 协商会话密钥(TLS 1.3 默认前向保密);校验六步「有效期→信任链→域名(SAN)→用途→吊销→强度」;排错先查「链是否完整、域名是否匹配、是否过期、时钟是否准」。
6. openssl 命令速查
生成密钥:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out key.pem
openssl ecparam -name prime256v1 -genkey -noout -out ec.key # ECC P-256
openssl genpkey -algorithm ED25519 -out ed25519.key # 签名用
openssl pkey -in key.pem -pubout -out pub.pem # 导出公钥
生成 CSR 与自签证书:
# CSR(证书签名请求)
openssl req -new -key key.pem -out req.csr \
-subj "/C=CN/ST=Beijing/O=Example/CN=example.com"
# 自签证书(含 SAN,开发测试用)
openssl req -x509 -new -nodes -key key.pem -sha256 -days 365 -out cert.pem \
-subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:*.example.com"
# 用自建 CA 签发(模拟完整链)
openssl x509 -req -in req.csr -CA ca.pem -CAkey ca.key \
-CAcreateserial -out cert.pem -days 365 -sha256
查看与校验:
openssl x509 -in cert.pem -noout -text # 证书全文
openssl x509 -in cert.pem -noout -dates -ext subjectAltName # 到期与 SAN
openssl verify -CAfile ca.pem cert.pem # 验证链
# 私钥/CSR/证书是否匹配(三者公钥指纹应一致)
openssl pkey -in key.pem -pubout | openssl sha256
openssl x509 -in cert.pem -noout -pubkey | openssl sha256
加解密、摘要与签名:
openssl enc -aes-256-cbc -pbkdf2 -salt -in plain.txt -out plain.enc
openssl enc -d -aes-256-cbc -pbkdf2 -in plain.enc -out plain.txt
openssl dgst -sha256 file.bin
openssl dgst -sha256 -hmac "secret-key" file.bin
openssl pkeyutl -sign -inkey ed25519.key -rawin -in msg.txt -out msg.sig
openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in msg.txt -sigfile msg.sig
格式转换:
openssl x509 -in cert.pem -outform DER -out cert.der # PEM → DER
openssl x509 -inform DER -in cert.der -outform PEM -out cert.pem
openssl pkey -in key.pem -aes256 -out key.enc.pem # 加密私钥
openssl pkcs12 -export -out bundle.p12 -inkey key.pem -in cert.pem -certfile chain.pem
openssl pkcs12 -in bundle.p12 -nodes -out all.pem
记忆:openssl 速查记五组——genpkey/ecparam 生成密钥、req 出 CSR 与自签、x509/verify 查看校验、enc/dgst/pkeyutl 加解密与签名、pkcs12 打包;「三方公钥指纹一致」可确认私钥/CSR/证书配套。
7. 密钥与证书管理实践
证书生命周期:生成 → 签发 → 部署 → 监控到期 → 续期 → 吊销/替换。自动化是唯一可持续的方式(人工续期必然出事):ACME(Let’s Encrypt)自动签发与续期,内部用 CA + 自动签发(cert-manager/Vault PKI)。
私钥的存放红线:
绝不进 git(用 .gitignore + 提交前扫描 secret)
绝不硬编码进镜像/代码;绝不通过明文渠道分发(聊天工具/邮件)
生产私钥放 KMS/HSM/Vault,应用只拿"引用"或短期凭证
文件权限与自动化:
chmod 600 key.pem # 私钥仅属主可读
chmod 644 cert.pem # 证书可公开
certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
certbot renew --dry-run # 演练续期
certbot renew --deploy-hook "nginx -s reload" # 续期后重载
证书监控(批量检查到期天数):
for h in example.com api.example.com; do
exp=$(echo | openssl s_client -connect "$h":443 -servername "$h" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo "$h -> $exp"
done
记忆:证书生命周期必须自动化(ACME/cert-manager)——私钥绝不进 git/镜像/明文渠道,生产私钥放 KMS/HSM/Vault;私钥 chmod 600、证书 644;到期监控与 –dry-run 演练是续期不翻车的保险。
8. 常见误用与安全红线
八条红线:
1. ECB 模式加密(相同块出相同密文,泄露结构)→ 用 GCM/CBC+随机 IV
2. 自己发明"加密+MAC"拼接 → 用 AEAD(GCM/Poly1305)
3. 哈希当加密用(哈希不可逆,不是加密)→ 分清用途
4. 哈希拼接做 MAC(长度扩展攻击)→ 用 HMAC
5. 用 == 比较 MAC/口令(时序攻击)→ 用恒定时间比较
6. 固定 IV/nonce 或重用 nonce(GCM 重用 = 密钥流复用灾难)
7. 弱算法(MD5/SHA-1/RC4/DES/1024 位 RSA)→ 全部禁用
8. 私钥进仓库/日志/镜像 → 用 secret 扫描与 KMS
易被忽视的两点:
随机数必须是密码学安全的:
错误:Math.random() / rand() → 可预测
正确:crypto.randomBytes / os.urandom / getrandom()
错误信息别泄露细节:登录失败统一回"用户名或密码错误"
解密失败别回具体原因(防填充预言)
JWT 的三个典型坑:alg=none 攻击(客户端把 alg 改成 none,服务端不校验签名 → 必须白名单允许的 alg,绝不接受 none);HS256 与 RS256 混用(把 RS256 公钥当 HMAC 密钥用 → 固定算法,不按 token 里的 alg 动态选);不校验 exp/aud/iss(过期 token 与跨服务 token 被接受)。
记忆:八条红线——禁 ECB、禁自造 AEAD、哈希不当加密、HMAC 不用拼接、比较用恒定时间、nonce 不重用、禁弱算法、私钥不进仓库;随机数要密码学安全,JWT 必须固定算法白名单并校验 exp/aud/iss(防 alg=none)。
9. 密钥轮换与存储
为什么要轮换:密钥用久了,泄露概率随时间累积;轮换把「一次泄露的影响面」限制在一段时间内。
会话密钥 → 每次连接全新(ECDHE 天然如此)
数据加密密钥(DEK)→ 定期轮换或按数据量轮换
主密钥(KEK)→ 更少轮换,用 KMS 托管
签名密钥 → 按合规要求轮换,保留旧公钥验证历史签名
信封加密(Envelope Encryption):
数据 → 用 DEK(数据密钥)加密
DEK → 用 KEK(主密钥,存 KMS)加密后随数据一起存
解密 → 先用 KMS 解出 DEK,再解数据
好处:大数据不经过 KMS(只加密小 DEK)
轮换 KEK 只需重加密 DEK,不必重加密全部数据
密钥分层与存储选型:
| 存储 | 适用 | 说明 |
|---|---|---|
| 环境变量 | 低敏感配置 | 易泄露进日志/进程列表,慎用 |
| 配置文件 + 权限 | 单机服务 | chmod 600,配合 secret 扫描 |
| KMS(云托管) | 生产主流 | 密钥不出 KMS,API 加解密 |
| HSM | 高合规场景 | 硬件保护,密钥永不导出 |
| Vault | 动态凭证 | 短期凭证 + 自动轮换 |
密钥版本化:轮换时新旧并存,用版本号标记,让旧数据仍可解、旧签名仍可验,再逐步淘汰。
密钥环(keyring):keyring = { v1: oldKey, v2: currentKey }
加密用 currentKey 并写入版本号;解密按数据里的版本号选对应密钥
→ 轮换 = 加新版本 + 旧版本保留一段时间后删除
记忆:轮换限制「一次泄露的影响面」——会话密钥天然每次全新、DEK 定期轮换、KEK 交 KMS;信封加密让大数据不经过 KMS 且轮换 KEK 只需重加密 DEK;密钥版本化做到新旧并存、平滑淘汰。
10. 速查表与一句话记忆
| 主题 | 结论 |
|---|---|
| 对称加密 | AES-GCM/ChaCha20-Poly1305(AEAD) |
| 禁用模式 | ECB;CBC 需随机 IV |
| nonce | 绝不重用(GCM 重用即灾难) |
| 非对称 | ECC(P-256/Ed25519)优于 RSA |
| 密钥交换 | ECDHE,提供前向保密 |
| 完整性 | SHA-256(防误改) |
| 认证 | HMAC-SHA256(需密钥,防伪造) |
| 密码存储 | Argon2id/BCrypt + 盐 |
| 签名 | 私钥签、公钥验,不可否认 |
| 证书 | 域名认 SAN;链要发全中间证书 |
| openssl | genpkey/req/x509/enc/dgst/pkeyutl/pkcs12 |
| 私钥 | chmod 600,绝不进 git/镜像 |
| 轮换 | 信封加密 + 密钥版本化 |
一句话记忆:对称加密用 AEAD(AES-GCM/ChaCha20-Poly1305,禁 ECB、nonce 绝不重用)、非对称(ECC 优于 RSA)只做密钥交换与签名、TLS 用 ECDHE 提供前向保密;哈希防误改、HMAC 防伪造(禁拼接、用恒定时间比较)、密码存储用 Argon2id/BCrypt+盐;签名是私钥签公钥验,证书域名认 SAN、链要发全中间证书;openssl 五组命令搞定密钥/CSR/校验/加解密/打包;私钥 chmod 600 且绝不进仓库、生产放 KMS/HSM、用信封加密与版本化做轮换——加密的难点从来不是算法,而是「不误用」。
延伸阅读
- /others-hashing-guide/ — 哈希函数、碰撞与密码存储
- /others-binary-encoding-tools/ — Base64/Hex 与 PEM/DER 编码
- /others-data-compression-guide/ — 压缩与加密的顺序与信息论
- /others-cli-ecosystem/ — 命令行工具的退出码与流约定
- /others-log-parsing/ — 日志脱敏与敏感信息防泄露
- network 专题 — TLS 握手抓包与证书运维
- OpenSSL 文档
- Let’s Encrypt 证书自动化
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。