PEP 506 – 为标准库添加一个secrets模块
- 作者:
- Steven D’Aprano <steve at pearwood.info>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2015年9月19日
- Python 版本:
- 3.6
- 发布历史:
摘要
本PEP提议将一个用于生成令牌等常见安全相关功能的模块添加到Python标准库中。
定义
本提案中使用的一些常见缩写
- PRNG
伪随机数生成器。一种确定性算法,用于生成具有某些理想统计特性的随机数。
- CSPRNG
密码学强伪随机数生成器。一种用于生成难以预测的随机数的算法。
- MT
梅森旋转算法。一种经过广泛研究的PRNG,目前被
random模块用作默认算法。
基本原理
这项提案的动机是担心Python的标准库让开发者过于容易地无意中犯下严重的安全错误。OpenBSD的创始人Theo de Raadt联系了Guido van Rossum,并对使用MT生成密码、安全令牌、会话密钥等敏感信息表示了一些担忧[1]。
尽管random模块的文档明确指出默认设置不适用于安全目的[2],但人们强烈认为许多Python开发者可能会忽略、忽视或误解此警告。特别是
- 开发者可能没有阅读文档,因此没有看到警告;
- 他们可能没有意识到他们对模块的特定使用具有安全隐患;或者
- 没有意识到可能存在问题,他们从不提供最佳实践的网站复制了代码(或学习了技术)。
在Google上搜索“python如何生成密码”时,第一个[3]结果是一个使用random模块默认功能的教程[4]。尽管它不打算用于web应用程序,但类似的技术很可能被用于这种情况下。第二个结果是StackOverflow上关于生成密码的问题[5]。给出的大多数答案,包括被接受的答案,都使用了默认功能。当一位用户警告说默认功能很容易被破解时,他们被告知“我认为你担心太多了。”[6]
这强烈表明现有的random模块在生成(例如)密码或安全令牌时是一个具有吸引力的麻烦。
其他动机(更具哲学倾向)可以在首次提出这个想法的帖子中找到[7]。
提案
替代方案侧重于random模块中的默认PRNG,旨在提供“默认安全”的密码学强原语,开发者无需考虑安全性即可在此基础上进行构建。(请参阅下面的替代方案。)本提案提出了一种不同的方法
- 标准库已经提供了密码学强原语,但许多用户不知道它们的存在或何时使用它们。
- 与其要求对加密不熟悉的用户编写安全代码,标准库应该包含一套现成的“电池”,用于最常见的需求,例如生成安全令牌。这些代码既可以直接满足需求(“我如何生成一个密码重置令牌?”),也可以作为开发者可以学习的良好实践的示例[8]。
为此,本PEP提议在标准库中添加一个新模块,建议名称为secrets。此模块将包含一套现成的函数,用于具有安全隐患的常见活动,以及一些较低级别的原语。
建议是secrets成为处理任何应保密事项(密码、令牌等)的首选模块,而random模块保持向后兼容。
API和实现
本PEP为secrets模块提出了以下函数
- 用于生成适合在(例如)密码恢复、会话密钥等中使用令牌的函数,格式如下:
- 作为字节,
secrets.token_bytes; - 作为文本,使用十六进制数字,
secrets.token_hex; - 作为文本,使用URL安全base-64编码,
secrets.token_urlsafe。
- 作为字节,
- 系统CSPRNG的有限接口,直接使用
os.urandom或random.SystemRandom。与random模块不同,这不需要提供用于播种、获取或设置状态,或任何非均匀分布的方法。它应该提供以下功能:- 一个从序列中选择项目的函数,
secrets.choice。 - 一个生成给定数量的随机位和/或字节作为整数的函数,
secrets.randbits。 - 一个返回半开区间0到给定上限的随机整数的函数,
secrets.randbelow[9]。
- 一个从序列中选择项目的函数,
- 一个比较文本或字节摘要是否相等且能抵抗计时攻击的函数,
secrets.compare_digest。
共识似乎是,为了支持这些用途,无需向random模块添加新的CSPRNG,SystemRandom就足够了。
Alyssa (Nick) Coghlan[10]给出了一些说明性的实现,Tim Peters[11]给出了一份极简API。这个想法也曾在“cryptography”模块的issue tracker上讨论过[12]。以下伪代码应作为实际实现的起点
from random import SystemRandom
from hmac import compare_digest
_sysrand = SystemRandom()
randbits = _sysrand.getrandbits
choice = _sysrand.choice
def randbelow(exclusive_upper_bound):
return _sysrand._randbelow(exclusive_upper_bound)
DEFAULT_ENTROPY = 32 # bytes
def token_bytes(nbytes=None):
if nbytes is None:
nbytes = DEFAULT_ENTROPY
return os.urandom(nbytes)
def token_hex(nbytes=None):
return binascii.hexlify(token_bytes(nbytes)).decode('ascii')
def token_urlsafe(nbytes=None):
tok = token_bytes(nbytes)
return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii')
secrets模块本身将是纯Python的,其他Python实现可以很容易地不加修改地使用它,或者根据需要进行调整。可以在BitBucket上找到一个实现[13]。
默认参数
一个难题是“我的令牌应该有多少字节?”。我们可以通过为“token_*”函数提供默认的熵量来帮助解决这个问题。如果nbytes参数为None或未给出,则将使用默认熵。此默认值应足够大,以期望在中等安全用途中是安全的,但预计将来会发生变化,甚至可能在维护版本中发生变化[14]。
命名约定
一个问题是模块中使用的命名约定[15],是使用C风格的命名约定(如“randrange”)还是更Python化的名称(如“random_range”)。
那些只是私有SystemRandom实例的绑定方法(例如randrange),或其薄包装的函数,应保留熟悉的名称。那些是新东西的函数(例如各种token_*函数)将使用更Python化的名称。
备选方案
一个替代方案是更改random模块提供的默认PRNG [16]。这受到了相当多的质疑和直接反对
- 人们担心CSPRNG可能比当前的PRNG(对于MT来说已经相当慢了)更慢。
- 某些应用程序(如科学模拟和重播游戏)需要能够将PRNG播种到已知状态,而CSPRNG在设计上缺乏此功能。
random模块的另一个主要用途是初学者编写的简单“猜数字”游戏,许多人不愿意对random模块进行任何可能使其变得更困难的更改。- 尽管没有提议从
random模块中移除MT,但对于不得不选择非CSPRNG或任何向后不兼容的更改的想法存在相当大的敌意。 - 针对MT的已证明攻击通常是针对PHP应用程序。据信,由于播种技术不佳[17],PHP版本的MT比Python版本更容易受到攻击。因此,在没有针对Python应用程序的已证明攻击的情况下,许多人反对向后不兼容的更改。
Alyssa Coghlan早先提出了全局可配置的PRNG,默认使用系统CSPRNG的建议,但后来已撤回,转而支持本提案。
与其他语言的比较
- PHP
PHP包含一个函数
uniqid[18],默认返回一个基于当前微秒时间的十三字符字符串。转换为Python语法,它具有以下签名def uniqid(prefix='', more_entropy=False)->str
PHP文档警告此函数不适用于安全目的。尽管如此,各种成熟的、知名的PHP应用程序仍将其用于此目的(需要引用)。
PHP 5.3及更高版本还包含一个函数
openssl_random_pseudo_bytes[19]。转换为Python语法,它大致具有以下签名def openssl_random_pseudo_bytes(length:int)->Tuple[str, bool]
此函数返回给定长度的伪随机字节字符串,以及一个布尔标志,指示该字符串是否被认为是加密强度高的。PHP手册建议,除了旧平台或有缺陷的平台,返回非True的情况应该很少见。
- JavaScript
根据相当粗略的搜索[20],JavaScript中似乎没有任何知名的标准函数可以生成强大的随机值。
Math.random经常被使用,尽管其严重弱点使其不适合加密目的[21]。近年来,大多数浏览器都开始支持window.crypto.getRandomValues[22]。Node.js 提供了一个丰富的密码模块,
crypto[23],其中大部分超出了本 PEP 的范围。它确实包含一个用于生成随机字节的函数,crypto.randomBytes。 - Ruby
Ruby 标准库包含一个模块
SecureRandom[24],其中包含以下方法- base64 - 返回Base64编码的随机字符串。
- hex - 返回一个随机的十六进制字符串。
- random_bytes - 返回一个随机的字节字符串。
- random_number - 根据参数,返回范围(0, n)内的随机整数,或0.0到1.0之间的随机浮点数。
- urlsafe_base64 - 返回一个随机的URL安全Base64编码字符串。
- uuid - 返回版本4的随机通用唯一标识符。
模块应该叫什么名字?
曾有一个提议添加一个“random.safe”子模块,引用Python之禅中的“命名空间是一个了不起的主意”这句格言。然而,Python之禅的作者Tim Peters已经表示反对这个想法[25],并建议使用顶级模块。
到目前为止,在python-ideas邮件列表上的讨论中,“secrets”这个名字已经获得了一些认可,没有遭到强烈反对。
已经有一个同名的第三方模块[26],但它似乎未被使用且已废弃。
常见问题
- 问:这是一个真正的问题吗?MT足够随机,没人能预测其输出,是吗?
答:安全专业人士的共识是,MT在安全环境中是不安全的。重建MT的内部状态并不困难[27] [28],因此可以预测所有过去和未来的值。有许多已知的、针对使用MT进行随机性的系统的实际攻击[29]。
- 问:对PHP的攻击是一回事,但有没有针对Python软件的已知攻击?
答:是的。Zope和Plone至少存在漏洞。Hanno Schlichting评论道[30]
"In the context of Plone and Zope a practical attack was demonstrated, but I can't find any good non-broken links about this anymore. IIRC Plone generated a random number and exposed this on each error page along the lines of 'Sorry, you encountered an error, your problem has been filed as <random number>, please include this when you contact us'. This allowed anyone to do large numbers of requests to this page and get enough random values to reconstruct the MT state. A couple of security related modules used random instead of system random (cookie session ids, password reset links, auth token), so the attacker could break all of those."
Christian Heimes 于2012年向 Zope 安全团队报告了此问题[31],至少存在两个相关的 CVE 漏洞[32],以及 Django 中至少有一个针对此问题的变通方案[33]。
- 问:这能替代SSL等专业密码软件吗?
答:不能。这是一个“开箱即用”的解决方案,而不是一个功能齐全的“核反应堆”。它旨在缓解一些基本的安全错误,而不是解决所有与安全相关的问题。引用 Alyssa Coghlan 谈到她早先的提案时的话[34]
"...folks really are better off learning to use things like cryptography.io for security sensitive software, so this change is just about harm mitigation given that it's inevitable that a non-trivial proportion of the millions of current and future Python developers won't do that."
- 问:密码生成器呢?
答:共识是密码生成器的要求变化太大,不适合作为标准库的一部分[35]。模块的初始版本不会包含密码生成器,而是会作为食谱(类似于
itertools模块中的食谱)在文档中提供[36]。 - 问:在 Linux 上,
secrets会使用 /dev/random(阻塞)还是 /dev/urandom(不阻塞)?其他平台呢?答:
secrets将基于os.urandom和random.SystemRandom,它们是操作系统最佳加密随机性源的接口。在Linux上,这可能是/dev/urandom[37],在Windows上可能是CryptGenRandom(),但详细实现细节请参阅文档和/或源代码。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0506.rst