先说个我自己的事。
2023 年做支付对账模块的代码审查,我在一个跑了三年的 Java 服务里看到这么一行:String iv = String.valueOf(new Random().nextInt(1000000));,后面这个 iv 被拿去当 AES-GCM 的随机数用。我盯着屏幕大概十秒,最后问了一句「这段谁写的」,答案是三年前离职的实习生。这个 bug 在线上躺了三年,扫描器没报,因为从语法上看它完全不违规。
密码学出事故的方式,九成跟数学没关系。 这篇文章不聊 AES 和 RSA 哪个更难破,聊一条我在实际项目里反复验证的规律:决定一个密码学方案能活多久的,不是它的安全归约证明多漂亮,而是它的失败有多容易被发现。
一、两次碎在数学之外的著名事故
2006 年 5 月,一位叫 Luciano Bello 的 Debian 打包者在跑 Valgrind 时,看到 OpenSSL 的 md_rand.c 里有两处「使用了未初始化内存」的警告。他的处理非常符合工程师直觉:既然读到的是垃圾数据,那删掉不就行了。于是他注释掉了那几行。
两年无人察觉。直到 2008 年 5 月,CVE-2008-0166 公布,大家才发现 Debian 和 Ubuntu 生成的 SSH 密钥、SSL 证书、OpenVPN 密钥,熵源只剩进程 PID,总共 32768 种可能。顺序枚举一遍,五分钟跑完。那两年里用 Debian 生成的密钥,本质上等于明文。
另一个案子更贴近普通人。2013 年 8 月,Android 上的比特币钱包被批量盗币,损失不小。当时的报道标题是「ECDSA 被破解」,但 ECDSA 一点事没有——碎的是 Java 的 SecureRandom。早期 Android 系统里,应用启动时熵池还没攒够,多个进程可能拿到相同的随机数。比特币签名用的 k 一旦重复,两条签名方程一联立,私钥 d 直接解出来:先算 k = (z1-z2)/(s1-s2),再算 d = (s1·k - z1)/r。同样的 bug 在 2010 年索尼 PS3 上出现过一次——他们干脆把 k 写成了常数。
这两个案子的共同点是:AES 没错,RSA 没错,椭圆曲线也没错。塌掉的是熵源、是默认参数、是「这段代码看起来不重要所以我删了」。
二、教科书指标和线上指标,经常是反着的
| 维度 | 学术界评价方式 | 工程上实际发生的事 |
|---|---|---|
| 核心指标 | 安全归约是否紧致、假设是否标准 | 失败模式是否可观测、能否回滚 |
| 攻击模型 | IND-CCA2 这类形式化定义 | 运维脚本、日志采样、超时、内存分配 |
| 演进速度 | 每届会议都有更优方案 | 换一个签名算法要动 200 个下游服务 |
| 代价衡量 | 证明的优雅程度 | 密钥字节数、握手往返、CPU 周期 |
这张表不是用来贬低学术界的,恰恰相反。我想说的是:安全证明保证的是「在模型内不出错」,工程要解决的是「在模型外出错后你多快能知道」。 这两件事的目标经常是冲突的。
举个具体冲突。抗侧信道的实现要求代码常时间执行、不做数据相关的分支和内存访问。但常时间往往意味着放弃表驱动优化,性能掉 30% 到 50%。你在 paper 里写「本方案可抵抗 cache-timing 攻击」很漂亮,线上一压测发现 QPS 掉一半,业务方直接把你打回来。
三、把参数摊开,差距比大多数人想的大
下面这些数字都是可以直接去 FIPS 203/204 文档里核对的:
| 算法 | 公钥 | 私钥 | 签名或密文 | 备注 |
|---|---|---|---|---|
| RSA-2048 | 256 B | 约 1190 B | 签名 256 B | 生成慢,验签比签快 |
| ECDSA P-256 | 64 B | 32 B | 签名约 72 B | 每次签名依赖新随机数 k |
| Ed25519 | 32 B | 32 B | 64 B | 确定性签名,不用随机数 |
| X25519 | 32 B | 32 B | 32 B(共享密钥) | 密钥交换,无签名能力 |
| ML-KEM-768 | 1184 B | 2400 B | 密文 1088 B | FIPS 203,2024-08-13 发布 |
| ML-DSA-65 | 1952 B | 4032 B | 签名 3309 B | FIPS 204,签名是 Ed25519 的 50 倍 |
| SLH-DSA-128s | 32 B | 64 B | 签名 7856 B | FIPS 205,签名极小但慢得离谱 |
先挑一个容易被忽略的点说:Ed25519 的签名是确定性的,同一个私钥签同一段消息,永远得到同一串字节。ECDSA 不是,它每次抽一个新的 k,k 一旦重复或者可预测,私钥当场就没了——这就是上面那个安卓钱包事故。
所以 Ed25519 相对 P-256 的工程优势压根不在数学上,而在于它把「你必须有一个高质量随机数源」这个依赖项删掉了。 少一个依赖,就少一整类线上事故。这个判断和「哪种曲线更难破」完全无关,但它在真实项目里的权重高得多。
再看对称加密。很多团队有个执念,非 AES-256 不用。实测数据是:在支持 AES-NI 的 x86 上,AES-256-GCM 比 AES-128-GCM 慢 20% 左右(轮数 14 对 10);如果跑在纯软件实现上,差距能到 40%。安全强度上,Grover 算法把 128 位密钥的暴力搜索降到 2^64 次查询,这个量级今天依然不可行。在对称加密这一侧,AES-128 在很多场景下是比 AES-256 更合理的选择,因为它更快、密钥调度更简单、出错面更小。 这话说出来容易挨骂,但你去看 Chrome 和各大 CDN 的实际配置,AES-128-GCM 一直是首选套件。
四、SIKE 之死:一个漂亮的反面教材
2022 年 7 月,Wouter Castryck 和 Thomas Decru 发了一篇论文,把 SIKE 打死了。
SIKE 是那几年 NIST 后量子标准化第四轮的候选方案,基于超奇异同源问题。理论性质非常漂亮:密钥极短,量子攻击也走不通。结果被破的代价是一台普通笔记本电脑,跑了一小时上下。
但真正值得琢磨的不是「它被破了」。密码学方案被破太正常了。值得琢磨的是它被破的方式:从头到尾没有任何中间状态提示你「系统正在被攻击」。 攻击者算完就拿到私钥,服务器日志干干净净,监控曲线平得像没事发生。
对比一下 RSA。这四十年 RSA 被打得千疮百孔——Bleichenbacher 1998、Lucky13(2013)、ROBOT(2017,把 F5 和 Citrix 一堆设备打回原形)、各种 padding oracle。可每次被破,症状都摆在明面上:握手失败率上升、错误码集中出现、响应时间出现统计偏差。运维看监控就知道不对,补丁打上去,第二天接着跑。
所以我给出的观点是:密码学的护城河不是数学难度,是失败可见性。 一个方案哪怕安全性只有 128 位,只要它的失败信号足够强、修复路径足够短,它在真实系统里的生命力往往超过一个理论安全性更强但失败静默的方案。这个判据在选型时比「谁的归约更紧」好用得多,可惜没什么 paper 会写它。
五、SHA-1 为什么到现在还没死透
2017 年 2 月,CWI 和 Google 宣布了 SHAttered,第一个 SHA-1 碰撞。代价是 6500 CPU 年加 110 GPU 年,折算成云算力大概 11 万美元。这个数字当年被很多人当成「SHA-1 还安全」的证据——11 万美元一次碰撞,谁会花这个钱?
但 2019 年 Leurent 和 Peyrin 把选择性前缀碰撞的成本压下来了一个量级。而现实是,到 2026 年,还有一堆老系统的代码签名、PDF 签名、内网 CA 在用 SHA-1。原因跟数学无关,是这些东西换不动:某个 2011 年写的固件签名工具只能跑在 32 位系统上,那台机器还在机房里转。
密码学的达尔文主义是按部署时间走的,不是按数学强度走的。 3DES 在支付行业活到 2020 年代,不是因为没人知道它有 Sweet32 问题,是因为整个行业的 HSM 和 POS 终端要一起换。
六、PQC 迁移:时间表已经定了,别再观望
几个确定的时间节点,都可以查到:
- 2024 年 8 月 13 日,NIST 发布 FIPS 203(ML-KEM)、FIPS 204(ML-DSA)、FIPS 205(SLH-DSA)。这是最终版,不是草案。
- 2024 年 11 月,NIST 发布 IR 8547 草案,建议 2030 年前淘汰 112 位安全强度(就是 RSA-2048、ECDSA P-256、DH-2048),2035 年前全面禁用。
- NSA 的 CNSA 2.0 时间表:2025 到 2030 年完成软件和固件签名迁移,2030 到 2033 年完成浏览器和服务器,2035 年收尾。
- 部署进度上,OpenSSH 9.0(2022 年 4 月)默认启用 sntrup761x25519 混合密钥交换;Chrome 从 124 版本起默认开启 X25519Kyber768 混合握手。
注意所有这些动作的共性:先做密钥交换的混合,再做签名的替换。 原因很实际——密钥交换是短暂的,双方谈完就扔,出问题大不了重连一次;签名是有长期效力的,证书一签就是一年,替换涉及 CA、涉及客户端信任链,动一发牵全身。先动简单的。
我建议的落地顺序是这样的:
- 先跑一遍资产清点。
openssl x509 -in cert.pem -noout -text | grep "Signature Algorithm"把所有证书的签名算法扒出来,ssh -Q kex列出所有 SSH 端支持的密钥交换,做一张表。八成团队做完这一步会发现,自己以为的「已经全面 ECC」其实是三套体系并存。 - 对称加密统一到 AES-128-GCM 或 ChaCha20-Poly1305。别纠结 256。选型标准看硬件:有 AES-NI 用 AES-GCM,没有(比如部分 ARM 和低端 IoT)优先 ChaCha20-Poly1305。
- 签名侧把 ECDSA 换成 Ed25519,前提是不需要和只支持 NIST 曲线的老设备互通。收益是删掉对随机数质量的依赖,以及签名长度从约 72 字节降到 64 字节。
- PQC 先上混合模式,TLS 用 X25519MLKEM768,SSH 用 sntrup761x25519。别一步跳到纯 PQC,客户端兼容性会让你哭。
- 密钥管理里最容易忽略的一环:ML-KEM-768 的密文是 1088 字节,ML-DSA-65 的签名是 3309 字节。这个尺寸会直接影响 TLS 握手包是否超过 MTU、是否触发分片。上线前一定在真实网络里压测,别只在 localhost 上测通。
最后回到开头那个实习生。他写的 new Random() 那行代码,语法没问题、编译能过、单测能绿、CI 不会拦。密码学真正的难处就藏在这里:它的错误不是崩溃,而是安静地、持续地、在生产环境里替你把秘密念给所有人听。
所以与其纠结用哪条曲线,不如先去做一件事——把系统里所有生成随机数的地方列出来,一个个打开看。这件事花不了两天,但它拦下来的事故,可能比换十次算法都多。