PEP 493 – Python 2.7 的 HTTPS 验证迁移工具
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>,Robert Kuska <rkuska at redhat.com>,Marc-André Lemburg <mal at lemburg.com>
- BDFL 代表:
- Barry Warsaw
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建:
- 2015 年 5 月 10 日
- Python 版本:
- 2.7.12
- 历史记录:
- 2015 年 7 月 6 日,2015 年 11 月 11 日,2015 年 11 月 24 日,2016 年 2 月 24 日
- 决议:
- Python-Dev 消息
摘要
PEP 476 更新了 Python 在客户端模块中处理 HTTPS 证书的默认方式,使其与 Web 浏览器中的证书处理方式保持一致,通过验证收到的证书是否属于客户端尝试联系的服务器。Python 2.7 长期维护系列被认为在此更改的范围内,新的行为在 Python 2.7.9 维护版本中引入。
这为受影响的 Python 2.7 维护版本带来了一个非平凡的采用障碍,因此本 PEP 提出了一些额外的 Python 2.7 特定功能,允许系统管理员和其他用户更轻松地将决定在 HTTPS 客户端模块中验证服务器证书与决定更新到更新的 Python 2.7 维护版本的决定分离开来。
基本原理
PEP 476 更改了 Python 的默认行为,使其与 Web 浏览器在 HTTPS URL 语义方面确立的期望保持一致:从 Python 2.7.9 和 3.4.3 开始,标准库中的 HTTPS 客户端默认验证服务器证书。
但是,此更改也确实会导致操作依赖自签名证书的私有内联网或遇到新的默认证书验证设置问题的基础架构管理员出现问题。
为了管理这些情况,Web 浏览器为用户提供“点击确认”警告,允许用户将服务器的证书添加到浏览器的证书存储中。像 curl
和 wget
这样的网络客户端工具提供了完全关闭证书检查的选项(分别通过 curl --insecure
和 wget --no-check-certificate
)。
在技术栈的不同层面上,像 SELinux
和 AppArmor
这样的 Linux 安全模块,尽管默认情况下由发行版供应商启用,但提供了相对简单的关闭机制。
目前,尚不存在此类方便的机制来禁用 Python 对整个进程的默认证书检查。
PEP 476 确实尝试解决此问题,方法是介绍如何在通过修补 ssl
模块以恢复旧行为来恢复整个进程的旧设置。不幸的是,已确定为允许系统管理员在其标准操作环境定义中默认禁用该功能而提出的基于 sitecustomize.py
的技术在某些情况下不足。导致最初创建此 PEP 的具体案例是 Linux 发行版旨在为其用户提供比直接使用上游 CPython 2.7 版本提供的标准版本更 平滑的迁移路径 的案例,但也指出了更新嵌入式 Python 运行时和其他用户级 Python 安装的其他潜在挑战。
为了避免大量相互不兼容的迁移技术的出现,本 PEP 提出在 Python 2.7.12 中添加一项附加功能,以便更容易地将进程恢复到过去在 HTTPS 客户端模块中跳过证书验证的行为。它还为将这些功能反向移植到 Python 2.7.9 之前的版本的重新分发者提供了其他建议。
备选方案
在没有明确的上游指导和建议的情况下,商业重新分发者仍将出于客户利益而做出自己的设计决策。可用的主要方法是
- 继续基于新的 Python 2.7.x 版本进行重新构建,同时除了在 PEP 476 中定义的机制之外,不提供任何其他帮助,以从标准库 HTTPS 客户端中未经检查的主机名迁移到已检查的主机名
- 将默认处理 HTTPS 连接的更改的可用性与从 Python 2 升级到 Python 3 关联起来
- 对于 Linux 发行版供应商,将默认处理 HTTPS 连接的更改的可用性与升级到新的操作系统版本关联起来
- 实施本 PEP 中描述的一个或两个反向移植建议,而不管 PEP 的正式状态如何
范围限制
这些更改纯粹是为了作为帮助管理在 Python 2.7 上下文中过渡到新的默认证书处理行为的工具而提出的。它们不是作为 Python 3 的新功能提出的,因为预计绝大多数受此问题影响且无法更新应用程序本身的客户端应用程序将是 Python 2 应用程序。
将来版本的 Python 3 允许在每个协议的基础上配置安全协议的默认证书处理可能也是可取的,但这个问题超出了本 PEP 的范围。
功能检测需求
由于本 PEP 中的提案旨在促进反向移植到早期 Python 版本,因此 Python 版本号不能用作检测它们的可靠方法。相反,它们旨在允许使用以下技术来确定功能的存在与否
python -c "import ssl; ssl.<_relevant_attribute>"
如果相关功能不可用,则这将失败并出现 AttributeError
(因此返回代码不为零)。
本 PEP 定义的功能检测属性为
ssl._https_verify_certificates
:运行时配置 APIssl._https_verify_envvar
:基于环境的配置ssl._cert_verification_config
:基于文件的配置(PEP 476 选择加入)
标记属性以下划线为前缀,以指示这些功能的实现依赖性和安全敏感性。
功能:配置 API
此更改建议包含在 CPython 2.7.12 及更高版本的 CPython 2.7.x 版本中。它包含一个新的 ssl._https_verify_certificates()
函数,用于指定标准库客户端库中 HTTPS 证书的默认处理方式。
不建议将此更改向前移植到 Python 3,因此需要支持跳过证书验证的 Python 3 应用程序仍需要定义自己的合适安全上下文。
功能检测
与该功能相关的 ssl
模块上的标记属性是 ssl._https_verify_certificates
函数本身。
规范
ssl._https_verify_certificates
函数的工作原理如下
def _https_verify_certificates(enable=True):
"""Verify server HTTPS certificates by default?"""
global _create_default_https_context
if enable:
_create_default_https_context = create_default_context
else:
_create_default_https_context = _create_unverified_context
如果在没有参数的情况下调用,或者将 enable
设置为真值,则标准库客户端模块随后将默认验证 HTTPS 证书,否则它们将跳过验证。
如果调用时将 enable
设置为假值,则标准库客户端模块随后将默认跳过验证 HTTPS 证书。
安全注意事项
包含此功能将允许安全敏感的应用程序在其代码中包含以下向前兼容的代码段
if hasattr(ssl, "_https_verify_certificates"):
ssl._https_verify_certificates()
一些开发人员还可以选择使用 ssl._https_verify_certificates(enable=False)
选择退出证书检查。这不会引入任何重大的新安全问题,因为修补受影响的内部 API 已经是可能的。
功能:基于环境的配置
此更改建议包含在 CPython 2.7.12 及更高版本的 CPython 2.7.x 版本中。它包含一个新的 PYTHONHTTPSVERIFY
环境变量,可以将其设置为 '0'
以禁用默认验证,而无需修改应用程序源代码(在仅限字节码的应用程序分发的情况下,甚至可能无法获得应用程序源代码)。
不建议将此更改向前移植到 Python 3,因此需要支持跳过证书验证的 Python 3 应用程序仍需要定义自己的合适安全上下文。
功能检测
与该功能相关的 ssl
模块上的标记属性为
ssl._https_verify_envvar
属性,给出影响默认行为的环境变量名称。
这不仅使检测功能的存在(或不存在)变得简单,还使程序化确定相关环境变量名称成为可能。
规范
而不是始终默认使用 ssl.create_default_context
,ssl
模块将被修改为
- 在模块首次导入到 Python 进程时读取
PYTHONHTTPSVERIFY
环境变量 - 如果此环境变量存在且设置为
'0'
,则将ssl._create_default_https_context
函数设置为ssl._create_unverified_context
的别名 - 否则,像往常一样将
ssl._create_default_https_context
函数设置为ssl.create_default_context
的别名
示例实现
_https_verify_envvar = 'PYTHONHTTPSVERIFY'
def _get_https_context_factory():
if not sys.flags.ignore_environment:
config_setting = os.environ.get(_https_verify_envvar)
if config_setting == '0':
return _create_unverified_context
return create_default_context
_create_default_https_context = _get_https_context_factory()
安全注意事项
相对于 Python 3.4.3+ 和 Python 2.7.9->2.7.11 中的行为,此方法确实引入了针对默认安全设置的新降级攻击,可能允许决心足够强的攻击者将 Python 恢复为 CPython 2.7.8 及更早版本中使用的默认行为。
可用攻击面略有增加是以下原因的关键所在
- 安全敏感的应用程序仍应定义自己的 SSL 上下文
- 本 PEP 中描述的迁移功能未添加到 Python 3 中
然而,也值得注意的是,执行此类攻击需要能够在导入ssl
模块之前修改 Python 进程的执行环境。结合写入文件系统任何部分(例如/tmp
)的能力,任何具有此类访问权限的攻击者都能够修改底层 OpenSSL 实现、动态库加载器和其他可能的安全敏感组件的行为。
与 Python 虚拟环境的交互
默认设置直接从进程环境中读取,因此无论解释器是否在激活的 Python 虚拟环境中运行,其工作方式都相同。
参考实现
针对 Python 2.7 实现上述两个功能的补丁已附加到相关的跟踪问题。
将此 PEP 反向移植到早期 Python 版本
如果接受此 PEP,则商业 Python 发行商可以选择将此 PEP 中定义的每个进程配置机制反向移植到早于 Python 2.7.9 的基本版本,而无需同时反向移植PEP 476对整个 Python 安装的默认行为的更改。
这种反向移植与本 PEP 中提出的机制的区别仅在于当PYTHONHTTPSVERIFY
完全未设置时的默认行为:它将继续默认为跳过证书验证。
在这种情况下,如果定义了PYTHONHTTPSVERIFY
环境变量,并且将其设置为'0'
以外的任何值,则应启用 HTTPS 证书验证。
功能检测
没有特定属性指示这种情况适用。相反,它由ssl._https_verify_certificates
和ssl._https_verify_envvar
属性在名义上早于 Python 2.7.12 的 Python 版本中存在来指示。
规范
实现此反向移植涉及反向移植PEP 466、476 和此 PEP 中的更改,并对ssl
模块中PYTHONHTTPSVERIFY
环境变量的处理进行以下更改
- 在模块首次导入到 Python 进程时读取
PYTHONHTTPSVERIFY
环境变量 - 如果此环境变量存在并设置为
'0'
以外的任何值,则将ssl._create_default_https_context
函数设置为ssl.create_default_context
的别名 - 否则,将
ssl._create_default_https_context
函数设置为ssl._create_unverified_context
的别名
示例实现
_https_verify_envvar = 'PYTHONHTTPSVERIFY'
def _get_https_context_factory():
if not sys.flags.ignore_environment:
config_setting = os.environ.get(_https_verify_envvar)
if config_setting != '0':
return create_default_context
return _create_unverified_context
_create_default_https_context = _get_https_context_factory()
def _disable_https_default_verification():
"""Skip verification of HTTPS certificates by default"""
global _create_default_https_context
_create_default_https_context = _create_unverified_context
安全注意事项
此更改将严格提升任何当前默认跳过标准库 HTTPS 客户端中证书验证的 Python 版本的安全性。需要考虑的技术权衡主要与PEP 466反向移植的规模有关,而不是与任何安全相关的方面有关。
与 Python 虚拟环境的交互
默认设置直接从进程环境中读取,因此无论解释器是否在激活的 Python 虚拟环境中运行,其工作方式都相同。
将 PEP 476 反向移植到早期 Python 版本
上述反向移植方法使 Python 2.7 安装的默认 HTTPS 证书验证行为保持不变:仍然需要在每个连接或每个进程的基础上选择验证证书。
为了允许修改整个安装的默认行为而不破坏向后兼容性,Red Hat 为 Red Hat Enterprise Linux 7.2+ 中的系统 Python 2.7 安装设计了一种配置机制,该机制提供
- 一种选择加入模型,允许独立于升级到首次反向移植该功能的操作系统版本的决定来做出启用 HTTPS 证书验证的决定
- 系统管理员设置在系统 Python 安装中直接运行的 Python 应用程序和脚本的默认行为的能力
- 重新分发者将来在某个时间点考虑更改新安装的默认行为的能力,而不会影响已明确配置为默认跳过验证 HTTPS 证书的现有安装
由于它仅影响对早期 Python 2.7 版本的反向移植,因此此更改不建议包含在 CPython 上游中,而是作为对选择为其用户提供类似功能的其他重新分发者的建议提供。
此 PEP 并没有对这种特定更改是否是一个好主意发表立场——相反,它建议如果重新分发者选择在早于 Python 2.7.9 的 Python 版本中使默认行为可配置,那么在重新分发者之间保持一致的方法将对用户有利。
但是,对于任何将自身宣传为提供 Python 2.7.9 或更高版本的 Python 安装,都不应使用此方法,因为大多数 Python 用户会合理地期望所有此类环境都默认验证 HTTPS 证书。
功能检测
与该功能相关的 ssl
模块上的标记属性为
_cert_verification_config = '<path to configuration file>'
这不仅使检测功能的存在(或不存在)变得简单,还使程序化确定相关配置文件名称成为可能。
Python 标准库的推荐修改
将PEP 476修改反向移植到更早的点版本的推荐方法是,相对于 Python 2.7.9+ 中实现的默认PEP 476行为,实现以下更改
- 修改
ssl
模块,以便在模块首次导入到 Python 进程时读取系统范围的配置文件 - 定义一个平台默认行为(验证或不验证 HTTPS 证书),如果此配置文件不存在则使用该行为
- 支持在以下三种操作模式之间进行选择
- 确保启用 HTTPS 证书验证
- 确保禁用 HTTPS 证书验证
- 将决策委托给提供此 Python 版本的重新分发者
- 根据给定的配置设置,将
ssl._create_default_https_context
函数设置为ssl.create_default_context
或ssl._create_unverified_context
的别名。
推荐的文件位置
由于 PEP 作者不知道有任何供应商提供针对 Windows、Mac OS X 或 *BSD 系统的长期支持版本,因此此方法目前仅针对 Linux 系统 Python 安装明确定义。
Linux 系统上的推荐配置文件名为/etc/python/cert-verification.cfg
。
.cfg
文件名扩展名建议与 Python 3 标准库中venv
模块使用的pyvenv.cfg
保持一致。
推荐的文件格式
配置文件应使用 ConfigParser ini 样式格式,其中包含一个名为[https]
的部分,其中包含一个必需的设置verify
。
建议的部分名称取自传递给受影响的客户端 API 的“https”URL 模式。
允许的verify
值为
enable
:确保默认启用 HTTPS 证书验证disable
:确保默认禁用 HTTPS 证书验证platform_default
:将决策委托给提供此特定 Python 版本的重新分发者
如果[https]
部分或verify
设置丢失,或者verify
设置为未知值,则应将其视为配置文件不存在。
示例实现
_cert_verification_config = '/etc/python/cert-verification.cfg'
def _get_https_context_factory():
# Check for a system-wide override of the default behaviour
context_factories = {
'enable': create_default_context,
'disable': _create_unverified_context,
'platform_default': _create_unverified_context, # For now :)
}
import ConfigParser
config = ConfigParser.RawConfigParser()
config.read(_cert_verification_config)
try:
verify_mode = config.get('https', 'verify')
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
verify_mode = 'platform_default'
default_factory = context_factories.get('platform_default')
return context_factories.get(verify_mode, default_factory)
_create_default_https_context = _get_https_context_factory()
安全注意事项
此反向移植案例的具体建议旨在适用于特权的安全敏感进程,即使是在以下锁定配置中运行的进程
- 从锁定管理员控制的目录而不是普通用户目录运行(防止基于
sys.path[0]
的特权提升攻击) - 使用
-E
开关运行(防止基于PYTHON*
环境变量的特权提升攻击) - 使用
-s
开关运行(防止基于用户站点目录的特权提升攻击) - 使用
-S
开关运行(防止基于sitecustomize
的特权提升攻击)
目的是,当使用此方法时,唯一导致 HTTPS 验证在安装范围内关闭的原因是
- 最终用户正在运行重新分发者提供的 CPython 版本,而不是直接运行上游 CPython
- 该重新分发者已决定提供比上游项目提供的更平滑的默认验证 HTTPS 证书的迁移路径
- 重新分发者或本地基础设施管理员已确定保留默认的 2.7.9 之前的行为(至少暂时如此)是合适的
使用管理员控制的配置文件而不是环境变量具有提供更平滑的迁移路径的基本功能,即使对于使用-E
开关运行的应用程序也是如此。
与 Python 虚拟环境的交互
此设置由解释器安装限定,并影响使用该解释器的所有 Python 进程,无论解释器是否在激活的 Python 虚拟环境中运行。
此建议的来源
此建议基于 Red Hat Enterprise Linux 7.2 采用的反向移植方法,如本 PEP 的 2015 年 7 月原始草案中发布并在此知识库文章中详细描述。Red Hat 为 Python 2.7.5 实现此反向移植的补丁可以在CentOS git 存储库中找到。
组合功能反向移植的建议
如果重新分发者选择将此 PEP 中基于环境变量的配置设置反向移植到也实现基于配置文件的PEP 476反向移植的修改后的 Python 版本,则环境变量应优先于系统范围的配置设置。这允许更改给定用户或应用程序的设置,而不管安装范围的默认行为。
示例实现
_https_verify_envvar = 'PYTHONHTTPSVERIFY'
_cert_verification_config = '/etc/python/cert-verification.cfg'
def _get_https_context_factory():
# Check for an environmental override of the default behaviour
if not sys.flags.ignore_environment:
config_setting = os.environ.get(_https_verify_envvar)
if config_setting is not None:
if config_setting == '0':
return _create_unverified_context
return create_default_context
# Check for a system-wide override of the default behaviour
context_factories = {
'enable': create_default_context,
'disable': _create_unverified_context,
'platform_default': _create_unverified_context, # For now :)
}
import ConfigParser
config = ConfigParser.RawConfigParser()
config.read(_cert_verification_config)
try:
verify_mode = config.get('https', 'verify')
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
verify_mode = 'platform_default'
default_factory = context_factories.get('platform_default')
return context_factories.get(verify_mode, default_factory)
_create_default_https_context = _get_https_context_factory()
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0493.rst
上次修改时间:2023-10-11 12:05:51 GMT