PEP 740 – 数字认证的索引支持
- 作者:
- William Woodruff <william at yossarian.net>,Facundo Tuesca <facundo.tuesca at trailofbits.com>,Dustin Ingram <di at python.org>
- 赞助者:
- Donald Stufft <donald at stufft.io>
- PEP 代表:
- Donald Stufft <donald at stufft.io>
- 讨论邮件列表:
- Discourse 帖子
- 状态:
- 草案
- 类型:
- 标准轨迹
- 主题:
- 打包
- 创建日期:
- 2024 年 1 月 8 日
- 历史记录:
- 2024 年 1 月 2 日,2024 年 1 月 29 日
- 决议:
- Discourse 消息
摘要
本 PEP 提出了一系列与上传和分发数字签名的认证以及用于在 Python 包存储库(如 PyPI)上验证它们的元数据相关的更改。
这些更改有两个子组件
- 更改当前未标准化的 PyPI 上传 API,允许客户端将数字认证作为认证对象上传;
- 更改HTML 和 JSON “简单” API,允许客户端检索各个发行版文件的数字认证和受信任发布元数据作为来源对象。
本 PEP 未对发行版上传中的强制数字认证或 pip
等安装客户端随后对其进行的验证提出政策建议。
基本原理和动机
包维护者和下游用户都反复表达了对 Python 包数字签名的渴望
- 维护者希望证明其包上传的完整性和真实性;
- 单个下游用户希望验证包的完整性和真实性,而无需额外信任其索引的诚实性;
- “批量”下游用户(如操作系统发行版)希望执行类似的验证,并可能为其自己的下游打包生态系统重新公开或进行反签名。
本提案旨在满足上述每个用例。
此外,本提案确定了以下动机
- Python 包分发的可验证来源:许多 Python 包当前包含未经身份验证的来源元数据,例如源主机 URL。加密认证格式可以启用这些包与其源主机之间的强大经过身份验证的链接,使索引和下游用户都能以加密方式验证包是否来自其声称的源存储库。
- 提高攻击者要求:寻求接管 Python 包的攻击者可以根据复杂性(从不复杂到复杂)和目标(从机会主义到有针对性)维度进行描述。
数字认证增加了额外的复杂性要求:攻击者必须足够复杂才能访问私有签名材料(或签名身份)。
- 索引可验证性:在现状下,索引提供的唯一认证是每个发行版文件的可选 PGP 签名(请参阅PGP 签名)。这些签名既不会(也无法)由索引检查其格式是否正确,也不会检查其有效性,因为索引没有机制来识别签名的正确公钥。本 PEP 通过确保来源对象包含索引验证认证有效性所需的所有元数据来克服此限制。
本 PEP 提出了一种通用的认证格式,其中包含一个用于签名生成的认证声明,并期望索引提供者采用该格式,并使用合适的身份源进行签名验证,例如受信任发布。
设计考虑
在评估其自身提出的更改以及 Python 打包相同或相邻领域的先前工作时,本 PEP 确定了以下设计考虑因素
- 索引可访问性:Python 包的数字认证理想情况下可以直接从索引本身作为“分离的”资源检索。
这既简化了一些兼容性问题(通过避免修改分发格式本身的需要),也简化了潜在安装客户端的行为(通过允许它们在相应包之前检索每个认证,而无需进行流式解压缩)。
- 索引本身的验证:除了启用安装客户端的验证之外,每个数字认证理想情况下都可以以某种形式由索引本身进行验证。
这既提高了上传到索引的认证的整体质量(例如,防止用户意外上传不正确或无效的认证),也使索引本身上的 UI 和 UX 增强成为可能(例如,每个上传包的“来源”视图)。
- 通用适用性:数字认证应适用于上传到索引的任何和所有包,无论其格式(sdist 或 wheel)或内部内容如何。
- 元数据支持:本 PEP 引用“数字认证”而不是仅仅“数字签名”,以强调加密信封内理想存在其他元数据。
例如,为了防止分发名称与其内容之间存在域分离,本 PEP 使用来自in-toto 项目的“Statements”将分发的内容(通过 SHA-256 摘要)绑定到其文件名。
先前的工作
PGP 签名
PyPI 和其他索引历史上一直支持上传分发文件的 PGP 签名。这些签名可以在上传过程中提供,并且可以通过 PEP 503 API 中的 data-gpg-sig
属性、PEP 691 API 中的 gpg-sig
密钥或通过相邻的 .asc
后缀 URL 由安装客户端检索。
自2023 年 5 月以来,PyPI 上已禁用 PGP 签名上传,此前一项调查确定大多数签名(签名本身仅占总体上传的极小一部分)无法与公钥关联或以其他有意义的方式进行验证。
在 PyPI 上以前支持的形式中,PGP 签名满足上述考虑因素 (1) 和 (3),但不满足 (2)(由于需要外部密钥服务器和密钥分发)或 (4)(由于 PGP 签名通常仅在输入文件上构建,没有任何关联的签名元数据)。
Wheel 签名
PEP 427(及其现存 PyPA 对等物)指定了wheel 格式。
此格式包含对直接嵌入到 wheel 中的数字签名的适应,格式为 JWS 或 S/MIME。这些签名是在 PEP 376 RECORD 上指定的,该 RECORD 已修改为包含 wheel 中每个记录文件的加密摘要。
虽然 wheel 签名已完全指定,但似乎并没有被广泛使用;官方的wheel 工具已弃用签名生成和验证支持0.32.0 版本,该版本于 2018 年发布。
此外,wheel 签名不满足上述任何考虑因素(由于签名的“附加”性质、索引本身无法验证以及仅支持 wheel)。
规范
上传端点更改
当前的上传 API 未标准化。但是,我们建议对其进行以下更改
索引更改
简单索引
对简单存储库 API进行了以下更改
- 当上传的文件有一个或多个认证时,索引**可以**在托管分发文件旁边提供一个
.provenance
文件。.provenance
文件的格式**必须**是 JSON 编码的来源对象,该对象**必须**包含文件的认证。例如,如果一个上传的文件托管在 URL
https://example.com/sampleproject-1.2.3.tar.gz
,则其来源 URL 将为https://example.com/sampleproject-1.2.3.tar.gz.provenance
。 - 当存在
.provenance
文件时,索引**可以**在其文件链接上包含一个data-provenance
属性。data-provenance
属性的值**必须**是关联的.provenance
文件的 SHA-256 散列值。 - 索引**可以**选择修改
.provenance
文件。例如,索引**可以**允许添加额外的证明和验证材料,例如来自第三方审计员或其他服务的证明。当索引修改.provenance
文件时,它**必须**也将data-provenance
属性的值更新为新的 SHA-256 散列值。有关文件来源可能发生更改的其他原因,请参阅来源对象的变化。
基于 JSON 的简单 API
以下更改对JSON 简单 API进行了修改
- 当上传的文件有一个或多个证明时,索引**可以**在该文件的
file
字典中包含一个provenance
键。provenance
键的值**必须**是 JSON 字符串或null
。如果provenance
不是null
,则它**必须**是关联的.provenance
文件的 SHA-256 散列值,与简单索引中的一样。有关将 SHA-256 散列值嵌入 JSON API 而不是完整来源对象的技术决策的解释,请参阅附录 3:简单 JSON API 大小注意事项。
这些更改需要对 JSON API 进行版本更改
api-version
**必须**指定版本 1.2 或更高版本。
认证对象
证明对象是一个 JSON 对象,包含几个必需的键;应用程序或签名者可以包含额外的键,只要提供所有明确列出的键即可。下面以伪代码形式提供了证明对象的必需布局。
@dataclass
class Attestation:
version: Literal[1]
"""
The attestation object's version, which is always 1.
"""
verification_material: VerificationMaterial
"""
Cryptographic materials used to verify `envelope`.
"""
envelope: Envelope
"""
The enveloped attestation statement and signature.
"""
@dataclass
class Envelope:
statement: bytes
"""
The attestation statement.
This is represented as opaque bytes on the wire (encoded as base64),
but it MUST be an JSON in-toto v1 Statement.
"""
signature: bytes
"""
A signature for the above statement, encoded as base64.
"""
@dataclass
class VerificationMaterial:
certificate: str
"""
The signing certificate, as `base64(DER(cert))`.
"""
transparency_entries: list[object]
"""
One or more transparency log entries for this attestation's signature
and certificate.
"""
transparency_entries
中每个对象的完整数据模型在附录 2:透明日志条目的数据模型中提供。证明对象**应该**包含一个或多个透明日志条目,并且**可以**包含其他签名时间源的额外键(例如RFC 3161时间戳颁发机构或Roughtime服务器)。
证明对象是版本化的;本 PEP 指定版本 1。每个版本都与单个加密套件绑定,以最大程度地减少不必要的加密灵活性。在版本 1 中,套件如下所示
- 证书指定为 X.509 证书,并符合RFC 5280中的配置文件。
- 消息签名算法为 ECDSA,使用 P-256 曲线作为公钥,并使用 SHA-256 作为加密摘要函数。
未来的 PEP 可以通过选择新的版本号来更改此套件(以及证明对象的整体形状)。
认证声明和签名生成
证明语句是在证明对象内进行加密签名的实际声明(即 envelope.statement
)。
证明语句被编码为v1 in-toto 语句对象,以 JSON 格式表示。序列化时,该语句被视为不透明的二进制 Blob,避免了规范化的需要。在附录 4:证明语句示例中提供了 JSON 编码语句的示例。
除了是 v1 in-toto 语句外,证明语句还受到以下限制
- in-toto
subject
**必须**仅包含一个主题。 subject[0].name
是发行版的文件名,它**必须**是有效的源代码发行版或wheel 发行版文件名。subject[0].digest
**必须**包含 SHA-256 散列值。其他散列值**可以**存在。散列值**必须**以十六进制字符串表示。- 支持以下
predicateType
值
此语句上的签名使用v1 DSSE 签名协议构建,其中 PAYLOAD_TYPE
为 application/vnd.in-toto+json
,PAYLOAD_BODY
为上述 JSON 编码语句。不允许使用其他 PAYLOAD_TYPE
。
来源对象
索引将与可以帮助验证它们的元数据一起提供上传的证明,这些元数据采用 JSON 序列化对象的格式。
这些来源对象将通过简单索引和基于 JSON 的简单 API 提供,如上所述,并将具有以下布局
{
"version": 1,
"attestation_bundles": [
{
"publisher": {
"kind": "important-ci-service",
"claims": {},
"vendor-property": "foo",
"another-property": 123
},
"attestations": [
{ /* attestation 1 ... */ },
{ /* attestation 2 ... */ }
]
}
]
}
或者,作为伪代码
@dataclass
class Publisher:
kind: string
"""
The kind of Trusted Publisher.
"""
claims: object | None
"""
Any context-specific claims retained by the index during Trusted Publisher
authentication.
"""
_rest: object
"""
Each publisher object is open-ended, meaning that it MAY contain additional
fields beyond the ones specified explicitly above. This field signals that,
but is not itself present.
"""
@dataclass
class AttestationBundle:
publisher: Publisher
"""
The publisher associated with this set of attestations.
"""
attestations: list[Attestation]
"""
The set of attestations included in this bundle.
"""
@dataclass
class Provenance:
version: Literal[1]
"""
The provenance object's version, which is always 1.
"""
attestation_bundles: list[AttestationBundle]
"""
One or more attestation "bundles".
"""
version
为1
。与证明对象一样,来源对象也是版本化的,本 PEP 仅定义版本1
。attestation_bundles
是一个**必需的** JSON 数组,包含一个或多个证明“捆绑包”。每个捆绑包对应一个签名身份(例如受信任的发布者身份),并包含一个或多个证明对象。如“发布者”模型中所述,每个
AttestationBundle.publisher
对象特定于其受信任的发布者,但必须至少包含以下内容- 一个
kind
键,它**必须**是一个唯一标识受信任发布者类型的 JSON 字符串。 - 一个
claims
键,它**必须**是一个 JSON 对象,包含索引在受信任发布者身份验证期间保留的任何特定于上下文的声明。
发布者对象中的所有其他键都是特定于发布者的。在附录 1:受信任的发布者表示示例中提供了发布者对象的完整说明性示例。
证明对象的每个数组都是上传时通过
attestations
字段提供的attestations
数组的超集,如上传端点更改和来源对象的变化中所述。- 一个
来源对象更改
来源对象不是不可变的,可能会随着时间的推移而发生变化。来源对象更改的原因包括但不限于
- 为预先存在的签名身份添加新的证明:索引**可以**选择允许预先存在的签名身份添加额外的证明,例如已上传文件的较新证明版本。
- 添加新的签名身份和关联的证明:索引**可以**选择支持来自文件上传者以外的来源的证明,例如第三方审计员或索引本身。这些证明可以异步执行,要求索引事后将它们插入到来源对象中。
认证验证
验证证明对象相对于发行版文件的操作需要验证以下各项
version
为1
。验证程序**必须**拒绝任何其他版本。verification_material.certificate
是一个有效的签名证书,由先验受信任的机构(例如验证客户端中已存在的信任根)颁发。verification_material.certificate
标识合适的签名主体,例如发布软件包的受信任发布者的机器身份。envelope.statement
是一个有效的 in-toto v1 语句,其主题和散列值**必须**与发行版的文件名和内容匹配。对于发行版的文件名,匹配**必须**通过使用适当的源代码发行版或 wheel 文件名格式进行解析,因为语句的主题可能等效但已规范化。envelope.signature
是envelope.statement
的有效签名,对应于verification_material.certificate
,通过v1 DSSE 签名协议重新构建。
除了上述必需步骤外,验证程序还可以根据策略验证 verification_material.transparency_entries
,例如,要求至少一个透明日志条目或达到条目阈值。验证透明日志条目时,验证程序**必须**确认每个条目的包含时间位于签名证书的有效期内。
安全影响
本 PEP 主要在本质上是“机械的”;它提供了结构和提供可验证数字证明的布局,而没有指定围绕证明有效性、证明之间的阈值等更高级别的安全“策略”。
认证中的加密灵活性
算法灵活性是加密方案中可利用漏洞的常见来源。本 PEP 通过两种方式限制算法灵活性
- 所有算法都指定在一个套件中,而不是一组几何参数。这使得攻击者无法(例如)选择具有弱散列函数的强签名算法,从而危及整个方案。
- 证明对象是版本化的,并且只能包含其版本指定的算法套件。如果将来某个特定套件被认为是不安全的,客户端可以选择全面拒绝或限定包含该套件的证明的验证。
索引信任
本 PEP **不**增加(或减少)对索引本身的信任:索引仍然被有效地信任,可以诚实地交付未修改的软件包分发版,因为一个能够修改软件包内容的不诚实的索引也可能不诚实地修改或省略软件包证明。因此,本 PEP 对索引信任的假设等同于早期机制(如 PGP 和轮签名)中未明确说明的假设。
建议
本 PEP 建议,但不强制要求,证明对象包含一个或多个可验证的签名时间来源,以证实签名证书声明的有效期。实现本 PEP 的索引可以选择严格执行此要求。
附录 1:受信任发布者表示示例
本附录提供了一个简单的 JSON API project.files[].provenance
列表中 publisher
密钥的虚构示例。
"publisher": {
"kind": "GitHub",
"claims": {
"ref": "refs/tags/v1.0.0",
"sha": "da39a3ee5e6b4b0d3255bfef95601890afd80709"
},
"repository_name": "HolyGrail",
"repository_owner": "octocat",
"repository_owner_id": "1",
"workflow_filename": "publish.yml",
"environment": null
}
附录 2:透明度日志条目数据模型
本附录包含证明对象中透明度日志条目的伪代码数据模型。每个透明度日志条目都作为签名包含时间的来源,并且可以在线或离线验证。
@dataclass
class TransparencyLogEntry:
log_index: int
"""
The global index of the log entry, used when querying the log.
"""
log_id: str
"""
An opaque, unique identifier for the log.
"""
entry_kind: str
"""
The kind (type) of log entry.
"""
entry_version: str
"""
The version of the log entry's submitted format.
"""
integrated_time: int
"""
The UNIX timestamp from the log from when the entry was persisted.
"""
inclusion_proof: InclusionProof
"""
The actual inclusion proof of the log entry.
"""
@dataclass
class InclusionProof:
log_index: int
"""
The index of the entry in the tree it was written to.
"""
root_hash: str
"""
The digest stored at the root of the Merkle tree at the time of proof
generation.
"""
tree_size: int
"""
The size of the Merkle tree at the time of proof generation.
"""
hashes: list[str]
"""
A list of hashes required to complete the inclusion proof, sorted
in order from leaf to root. The leaf and root hashes are not themselves
included in this list; the root is supplied via `root_hash` and the client
must calculate the leaf hash.
"""
checkpoint: str
"""
The signed tree head's signature, at the time of proof generation.
"""
cosigned_checkpoints: list[str]
"""
Cosigned checkpoints from zero or more log witnesses.
"""
附录 3:简单 JSON API 大小考虑
本 PEP 的早期草案要求将每个 来源对象 直接嵌入到 JSON 简单 API 的相应部分中。
本 PEP 的当前版本改为嵌入来源对象的 SHA-256 散列。这样做是为了考虑大小和网络带宽。
- 我们估计证明对象的典型大小约为 5.3 KB 的 JSON。
- 我们保守估计,索引最终会为每个发布文件托管大约 3 个证明,或者每个组合来源对象大约 15.9 KB 的 JSON。
- 截至 2024 年 5 月,PyPI 上的平均项目大约有 21 个发布文件。我们保守地预计这个平均值会随着时间的推移而增加。
- 综合这些数字,这意味着一个典型的项目可能需要在其“项目详细信息”端点托管 60 到 70 个证明,或者大约 339 KB 的额外 JSON。
在“病态”情况下,这些数字会明显更糟糕,例如项目有数百或数千个发布版和/或每个发布版有数十个文件。
附录 4:认证声明示例
给定一个源代码分发版 sampleproject-1.2.3.tar.gz
,其 SHA-256 散列为 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
,以下是作为 JSON 对象的适当 in-toto 语句。
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "sampleproject-1.2.3.tar.gz",
"digest": {"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
}
],
"predicateType": "https://some-arbitrary-predicate.example.com/v1",
"predicate": {
"something-else": "foo"
}
}
版权
本文档置于公共领域或根据 CC0-1.0-Universal 许可证,以较宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0740.rst
上次修改时间:2024-08-22 06:00:24 GMT