【免费下载链接】publications
Publications from Trail of Bits
本文基于 Trail of Bits 首席安全工程师 Paul Kehrer 在 PyCon AU 2019 的演讲《Best Practices for Cryptography in Python》整理成文。文章以演讲的完整脉络为骨架——从"密码学到底是什么"的问题定义,到 Python 实现加密算法的语言级限制,再到 RSA 反例、威胁模型与 FFI 突围方案——并结合本仓库 publications 中同主题的系列演讲与源码级资源进行补充,帮助读者建立一套"什么时候 Python 适合做密码学、什么时候不适合、不适合时如何补救"的完整决策框架。
演讲背景:谁在什么场合讲了什么
本演讲出自 2019 年 8 月 2 日的 PyCon AU 大会,演讲者是 Paul Kehrer,时任 Trail of Bits 首席安全工程师,同时是 Python Cryptographic Authority(PyCA,Python 加密权威组织)的核心开发者,长期维护cryptography、PyNaCl、bcrypt、pyOpenSSL等 Python 生态最主流的加密库。完整的演讲元数据与幻灯片分别见 README.md 和 presentation.pdf。
演讲的第一张技术内容页给出了全篇的核心结论,只有一句话,且自带限定条件:
Secure cryptography is possible in Python(restrictions apply)—— 在 Python 中做出安全的密码学是可能的(但有限制)。
这句"先给结论、再讲限制"的开场,奠定了整场演讲的基调:不神化 Python,也不一棍子打死,而是精确地划出 Python 的适用边界,并为边界之外的场景给出工程化出路。
密码学到底是什么:先定义问题,再讨论语言
在讨论"Python 能不能做密码学"之前,演讲先回到了第一性原理——密码学要解决的问题是什么:
- 在存在对手的前提下,安全地通信与传递信息;
- 对手可能是被动的(只能窃听,无法篡改)或主动的(可以注入、重放、篡改消息);
- 通信可能是同步的(双方同时在线的握手式通信)或异步的(如签名、证书这类离线验证场景);
- 需要保护的"信息"不一定是文件,也可以是密钥、口令、协议消息等任意数据形态。
演讲随后点出了密码学软件工程中一个常被低估的事实:要写出安全的密码软件,算法本身和具体实现必须同时正确。算法层面的安全(如 RSA 的数学性质)与实现层面的安全(如常数时间、内存清零)是两条独立的失败路径,任意一条被攻破,整个系统就不可信了。
本次演讲的范围:聚焦实现层面的语言限制
演讲明确界定了自己的讨论范围:Implementation-based issues caused by language-level restrictions——即"由语言层面的限制所引发的实现问题"。
这一定位很重要,它把讨论从"协议设计是否合理""密钥管理是否妥当"等更宏大的话题中剥离出来,聚焦到一个具体、可回答的问题:当你想用 Python 这一门语言把某个加密算法写出来并跑在生产环境时,Python 的语言特性会在哪些环节、以何种方式损害安全性?
实现加密算法需要什么:四项硬性条件
演讲用一页幻灯片列出了"实现加密算法"这个任务的真实需求清单,每一项都是硬约束:
- 低层流程控制,防止数据相关的分支:加密代码中不能出现依赖秘密数据的条件跳转,否则攻击者可通过计时观测恢复密钥——这就是时序侧信道(timing side channel)的根源;
- 对缓存、SIMD 指令等的精细控制:现代 CPU 的缓存行为、向量指令的选择都会在物理层泄漏信息,算法实现者需要有能力控制到这一层;
- 精确的内存分配与擦除:密钥等敏感数据需要可预测的分配时机,且用完后必须可靠地清零,而不是交给 GC 或引用计数在不可控时刻回收;
- 尽可能高的执行速度:大数模幂、椭圆曲线点乘等运算是计算密集型的,性能本身就是安全性的组成部分(慢到不可用的加密实现会迫使开发者绕开它、走向不安全的自定义捷径)。
演讲用一个略带自嘲的句子总结了这份需求清单的推论:"Unfortunately this means it is mostly written in C"——遗憾的是,这意味着密码学实现大多是 C 语言写的。原因被归纳为三点:Ubiquity(普遍性)、Speed(速度),以及一条黑色幽默式的第三条:The memory unsafety industrial complex needs CVEs——"内存不安全的工业综合体需要(源源不断的)CVE"。这句玩笑的背后是一个严肃事实:加密库长期以 C 等不安全的系统语言实现,一方面是因为没有更好的选择,另一方面也为攻击面埋下了大量内存安全漏洞。
用 Python 实现加密算法的警示案例:RSA 与 pow()
为了具体展示"Python 实现加密算法"到底难在哪里,演讲选择了 RSA 作为例子——并且每翻一页,幻灯片上劝阻语气都更重一分:从 "do not use RSA" 到 "I beg of you, do not use RSA",再到 "No! Stop!" 与 "please do not use RSA, it is so bad"。
RSA 的数学核心只有两对公式:
- 签名/验签:
S = M^d (mod N),M = S^e (mod N) - 加密/解密:
C = M^e (mod N),M = C^d (mod N)
看起来极其简洁,而 Python 恰好提供了一个"一步到位"的内建函数pow(base, exp, mod),于是签名可以写成:
sig = pow(msg, d, n) # 签名 msg = pow(sig, e, n) # 验签演讲反复强调:do not use RSA and also do not do this——既不要用 RSA,也不要用这种方式实现它。原因在于,RSA 的安全性从来不在这一行数学表达式里,而在于大量教科书不会写的工程细节:
- 大数运算必须常数时间执行,模幂的迭代次数、每轮是否执行乘法的分支都不能泄漏指数
d的比特位; - 需要精确的内存管理,中间值(尤其是私钥指数
d)用后必须清零; - 需要标准的填充方案(如 PSS/OAEP),而填充校验的失败路径必须统一,否则就会引入 padding oracle 攻击。
而 Python 的pow(msg, d, n)在 CPython 解释器层面走的是通用大整数实现,其分支与运算时长对数据敏感,既无法满足常数时间要求,也无法对中间状态做可控擦除。演讲专门展示了一个真实的 2048 位模数N长什么样(约 617 位十进制数字),直观说明:当你把这些位数的模幂运算是交给一个逐比特分支解释执行的运行时时,侧信道攻击面是灾难性的。
需要强调的是,"不要用 RSA"并非 Python 特有的话题,而是整个行业在 20 多年攻击史中形成的共识。本仓库中同属 Cryptography 分类的另一场演讲 Seriously, stop using RSA 对此有完整的论证:RSA 是"内在脆弱的密码系统",弱参数难以检查、性能压力迫使开发者走捷径、padding oracle 攻击在其被发现二十年后依然肆虐——"理论上可以实现正确的 RSA,但实践已经证明这几乎不可达成"。
确立什么才是重要的:威胁模型驱动一切
在展示了 RSA 这一令人沮丧的反例之后,演讲紧接着给出了最重要的方法论转折——Establish what matters(确立什么才是重要的),具体包含三层意思:
- 明确定义你考虑防护的威胁集合:威胁模型(threat model)必须先行,而不是默认"所有攻击都必须防住";
- 很多语言层面的限制,在你的具体场景中可能根本无关紧要:例如一个只在本机、单进程、非交互式环境中处理密钥的脚本,与一个面对网络级主动攻击者的 TLS 终端,面临的风险完全不同;
- 存在强大的变通方案(workarounds)可用:语言短板不是死局,下一节要讲的 FFI 就是最核心的变通手段。
这层"威胁模型优先"的思想正是演讲标题中"Best Practices"的精髓:先问自己保护什么、防谁,再决定用什么工具、在哪里实现。这也是安全工程区别于"套模板"的关键——离开威胁模型的"最佳实践"往往是伪实践。
FFI:Python 的加密超能力
演讲用整整一节来介绍 Python 在密码学领域真正的优势所在:FFI(Foreign Function Interface,外部函数接口)。
Python 对讲 C ABI 的语言拥有丰富的 FFI 能力,通过
cffi与ctypes,Python 代码可以直接调用原生(native)代码。
这条"桥"的价值体现在两个方向:
- 获取安全加密所需的一切特性:把真正敏感的运算下沉到经过审计的、常数时间的、能精确控制内存的原生加密库中执行,Python 侧只负责传递数据、编排流程;
- 在难用的加密库之上构建 Pythonic API:原生加密库(如 OpenSSL、libsodium)的 C API 往往原始且反直觉,FFI 层可以封装出一套符合 Python 习惯的、类型安全的高层接口,让普通开发者无需直接面对 C API 的复杂度。
这恰恰是演讲者本人长期维护的 PyCA 生态的实践路径。以演讲中提到的 PyNaCl(演讲者维护的项目之一)为例,其典型用法是绑定到 libsodium 这类经受考验的原生实现上,为开发者提供高层的、难以误用的 API。同样的思路也延续到了本仓库中收录的后继演讲:在 Building a Rusty path validation library for PyCA Cryptography 与 Implementing X.509 path validation for Python 中,PyCA 将 X.509 路径验证这样高风险、高复杂度的逻辑从零用内存安全语言 Rust重新实现并集成进cryptography库——这正是"把敏感实现移出 Python、用 FFI 桥接原生代码"这一策略在 2019 年之后的新发展:被调用的原生代码本身也应当走向内存安全。
用演讲的原话概括 FFI 的地位:"FFI, Python's cryptographic super power"——FFI 是 Python 的加密超能力。它不是 Python 的妥协,而是 Python 在密码学领域的正确定位:当"胶水",而不是"引擎"。
额外的缓解措施:内存层面的精细控制
对于无法完全下沉到原生代码、必须留在 Python 侧处理的敏感数据(如密钥材料),演讲给出了内存层面的补充缓解措施:
While byte strings are immutable, bytearrays are not. You can also construct buffer protocol objects from native code to gain even more control.
- bytes 是不可变的:一个密钥如果以
bytes存储,你将无法就地覆写它——它的内存内容只能等垃圾回收/引用计数在不可控的时机释放,期间它可能被交换、被复制、被留在堆的多个副本中; - bytearray 是可变的:需要时可以用它承载敏感数据,并在用完后手动将内容覆写为随机或零值,从而降低密钥残留的风险;
- buffer protocol 对象:更进一步,可以从原生代码构造实现了 buffer protocol 的对象,从而获得更细粒度的内存控制权(例如把密钥固定在不被交换出去的页面上、由原生代码负责清零)。
这一节虽然只有一页幻灯片,却点出了一个在 Python 中极易被忽略的事实:即便你的加密运算已经交给原生库,密钥在进入/离开原生库边界时经过的那段 Python 内存,依然是需要管理的攻击面。
结论:Python 密码学的正确姿势
演讲的收尾页回到开头的结论句,并给出了完整的展开:
- Python 中做安全密码学是可能的,但通常意味着 Python 是调用底层原生代码的那一层,而不是加密算法本身的宿主——"Secure cryptography is possible in Python" 的真实含义是 "Python is what you use to call some underlying native code";
- 威胁模型是生死攸关的:构建安全软件必然伴随取舍(tradeoffs),你必须清醒地界定"防什么"与"不防什么",而不是追求一个不存在的绝对安全;
- 限制是诚实的:任何"Python 密码学最佳实践"都应以承认语言边界为前提,再给出跨过边界的工程手段。
这套方法论可以沉淀为一份可直接落地的检查清单:
| 决策环节 | 应遵循的做法 |
|---|---|
| 选型 | 优先使用cryptography、PyNaCl等基于成熟原生库(OpenSSL、libsodium)的 PyCA 生态库,而不是自行实现算法 |
| 算法选择 | 默认选择现代算法(如 X25519、Ed25519、AES-GCM),并明确避开 RSA 这类"教科书易写、工程难安全"的方案 |
| 威胁模型 | 先书面界定保护对象、对手能力、通信同步性,再决定敏感运算放在哪一层执行 |
| 敏感数据内存 | 用bytearray而非bytes承载可覆写的密钥材料,需要更精细控制时使用原生 buffer protocol 对象 |
| 侧信道 | 任何依赖秘密数据的运算都应下沉到常数时间实现的原生代码中,不要依赖 Python 解释器的行为 |
延伸阅读:仓库内的同主题资源
本文涉及的演讲属于本仓库 README.md 中 Cryptography 分类下的系列内容,以下资源可帮助你进一步深入:
- Seriously, stop using RSA:从攻击史出发系统论证 RSA 为何难以安全实现,是本文 RSA 反例的完整展开;
- Building a Rusty path validation library for PyCA Cryptography:PyCA 用内存安全语言重写 X.509 路径验证的工程实践,是"原生代码 + 内存安全"路线的最新演进;
- Implementing X.509 path validation for Python:同一工作的配套演讲,覆盖实现细节与测试策略;
- Constant-Time Coding Support in LLVM:从编译器层面保证常数时间实现不被破坏,与演讲中"数据相关分支"威胁直接呼应。
这些演讲共同勾勒出一条清晰的演进线索:2019 年,PyCA 的核心开发者告诉社区"别用 Python 实现加密算法,用 FFI 调原生库";此后数年,同一批人又在把被调用的原生库本身改写成内存安全的 Rust——"Python 密码学最佳实践"的终点,是让每一层都不再需要开发者去赌自己的运气。
【免费下载链接】publications
Publications from Trail of Bits
相关推荐
掌握Carbon语言:避开陷阱的终极指南与高级用法技巧
掌握Carbon语言:避开陷阱的终极指南与高级用法技巧 Carbon语言作为C++的实验性继任者,旨在解决现代软件开发中的关键挑战。本文将深入探讨Carbon语
编程语言编译器标准库彻底解决Xmake中XMAKE_GLOBALDIR路径配置的5大陷阱与最佳实践
彻底解决Xmake中XMAKE_GLOBALDIR路径配置的5大陷阱与最佳实践 你是否遇到过这些诡异问题? 在使用Xmake(一个基于Lua的轻量级跨平台构建工
构建工具开发工具包管理器告别多语言陷阱:Django-ModelTranslation 全场景避坑指南与最佳实践
告别多语言陷阱:Django ModelTranslation 全场景避坑指南与最佳实践 引言:多语言开发的隐形雷区 你是否曾遭遇过这些令人抓狂的多语言开发场景
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考