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日
- 决议:
- 2024年7月17日
摘要
本 PEP 提出了一系列与 Python 包存储库(如 PyPI)上数字签名证明及其验证所需元数据的上传和分发相关的变更。
这些变更包含两个子组件:
- 对当前未标准化的 PyPI 上传 API 的变更,允许客户端将数字证明作为证明对象上传;
- 对 HTML 和 JSON“简单”API 的变更,允许客户端为单个发布文件检索数字证明和 可信发布 元数据,作为来源对象。
本 PEP 不对发布上传强制数字证明或安装客户端(如 pip)的后续验证提出政策建议。
原理与动机
包维护者和下游用户都反复表达了对 Python 包数字签名的需求:
- 维护者希望证明其包上传的完整性和真实性;
- 个人下游用户希望验证包的完整性和真实性,而无需对其索引的诚实性施加额外的信任;
- “批量”下游用户(例如操作系统发行版)希望执行类似的验证,并可能为其自己的下游打包生态系统重新公开或副签名。
本提案旨在满足上述所有用例。
此外,本提案确定了以下动机:
- Python 包分发的可验证来源:许多 Python 包目前包含 未认证 的来源元数据,例如源主机的 URL。加密证明格式可以实现这些包与其源主机之间的强 认证 链接,允许索引和下游用户以加密方式验证包是否源自其声称的源存储库。
- 提高攻击者要求:寻求接管 Python 包的攻击者可以根据 复杂性 (不复杂到复杂)和 目标定位 (机会主义到有目标)维度进行描述。
数字证明增加了额外的复杂性要求:攻击者必须足够复杂才能访问私有签名材料(或签名身份)。
- 索引可验证性:在现状中,索引提供的唯一证明是每个发布文件可选的 PGP 签名(参见PGP 签名)。这些签名无法(也无法)由索引检查其格式是否正确或有效,因为索引没有机制来识别签名的正确公钥。本 PEP 通过确保来源对象包含索引验证证明有效性所需的所有元数据来克服这一限制。
本 PEP 提出了一种通用的证明格式,包含一个用于签名生成的证明声明,并期望索引提供者采用该格式,并使用合适的身份来源进行签名验证,例如可信发布。
设计考虑
本 PEP 在评估其自身提出的变更以及 Python 打包相同或相邻领域的先前工作时,确定了以下设计考虑:
- 索引可访问性:Python 包的数字证明理想情况下可以直接从索引本身作为“分离”资源检索。
这既简化了一些兼容性问题(通过避免修改分发格式本身的需求),也简化了潜在安装客户端的行为(通过允许它们在相应的包之前检索每个证明,而无需进行流式解压)。
- 索引本身的验证:除了启用安装客户端的验证外,每个数字证明 理想情况下 可以由索引本身以某种形式进行验证。
这既提高了上传到索引的证明的整体质量(例如,防止用户意外上传不正确或无效的证明),也使索引本身能够进行 UI 和 UX 改进(例如,每个上传包的“来源”视图)。
- 普遍适用性:数字证明应适用于上传到索引的 任何 包,无论其格式(sdist 或 wheel)或内部内容如何。
- 元数据支持:本 PEP 提到“数字证明”而不是仅仅“数字签名”,以强调加密信封中理想情况下存在的附加元数据。
例如,为了防止分发名称与其内容之间的域分离,本 PEP 使用 in-toto 项目的“声明”将分发内容(通过 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 工具在 2018 年发布的 0.32.0 版本中弃用了签名生成和验证支持。
此外,wheel 签名不满足上述任何考虑(由于签名的“附加”性质、索引本身不可验证以及仅支持 wheels)。
规范
上传端点变更
当前的上传 API 未标准化。但是,我们建议对其进行以下更改:
索引变更
简单索引
对 简单存储库 API 进行了以下更改:
- 当上传的文件包含一个或多个证明时,索引 可以 提供一个包含与给定分发相关联的证明的来源文件。来源文件的格式 应 为 JSON 编码的来源对象,其中 应 包含该文件的证明。
来源文件的位置由索引通过
data-provenance属性发出信号。 - 当存在来源文件时,索引 可以 在其文件链接上包含
data-provenance属性。data-provenance属性的值 应 为一个完全限定的 URL,表示该文件的来源可以在该 URL 处找到。此 URL 必须 表示一个安全源。下表提供了发布文件 URL、
data-provenance值及其生成的来源文件 URL 示例。文件 URL data-provenance来源 URL https://example.com/sampleproject-1.2.3.tar.gz https://example.com/sampleproject-1.2.3.tar.gz.provenancehttps://example.com/sampleproject-1.2.3.tar.gz.provenance https://example.com/sampleproject-1.2.3.tar.gz https://other.example.com/sampleproject-1.2.3.tar.gz/provenancehttps://other.example.com/sampleproject-1.2.3.tar.gz/provenance https://example.com/sampleproject-1.2.3.tar.gz ../relative(无效:不是完全限定的 URL) https://example.com/sampleproject-1.2.3.tar.gz http://unencrypted.example.com/provenance(无效:不是安全源) - 索引 可以 选择修改来源文件。例如,索引 可以 允许添加额外的证明和验证材料,例如来自第三方审计员或其他服务的证明。
有关文件来源可能发生更改的其他讨论,请参见来源对象的变更。
基于 JSON 的简单 API
对 JSON 简单 API 进行了以下更改:
- 当上传的文件有一个或多个证明时,索引 可以 在该文件的
file字典中包含一个provenance键。provenance键的值 应 为 JSON 字符串或null。如果provenance不为null,则它 应 是指向关联来源文件的 URL。有关在 JSON API 中嵌入 SHA-256 摘要而非完整的来源对象的技术决策的解释,请参见附录3:简单 JSON API 大小考虑。
这些更改需要对 JSON API 进行版本更改:
api-version应 指定版本 1.3 或更高。
证明对象
证明对象是一个具有多个必需键的 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)。
证明声明以 JSON 形式编码为 v1 in-toto Statement 对象。序列化时,声明被视为不透明的二进制 blob,避免了规范化的需要。一个 JSON 编码的声明示例在附录4:证明声明示例中提供。
除了作为 v1 in-toto Statement 之外,证明声明还受到以下限制:
- 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 数组,包含一个或多个证明“包”。每个包对应一个签名身份(例如可信发布身份),并包含一个或多个证明对象。如
Publisher模型中所述,每个AttestationBundle.publisher对象都特定于其可信发布者,但必须至少包含:- 一个
kind键,它 必须 是一个唯一标识可信发布者类型的 JSON 字符串。 - 一个
claims键,它 必须 是一个 JSON 对象,包含在可信发布者身份验证期间由索引保留的任何特定上下文的声明。
发布者对象中的所有其他键都是发布者特定的。发布者对象的完整示例在附录1:可信发布者表示示例中提供。
证明对象数组是上传时通过
attestations字段提供的attestations数组的超集,如上传端点变更和来源对象的变更中所述。- 一个
来源对象的变更
来源对象 并非 不可变,并且可能会随时间变化。来源对象发生变化的原因包括但不限于:
- 为现有签名身份添加新证明:索引 可以 选择允许现有签名身份添加额外证明,例如已上传文件的更新证明版本。
- 添加新的签名身份和相关证明:索引 可以 选择支持来自文件上传者以外的来源的证明,例如第三方审计员或索引本身。这些证明可以异步执行,要求索引 事后 将它们插入到来源对象中。
证明验证
针对分发文件验证证明对象需要验证以下各项:
version为1。验证器 必须 拒绝任何其他版本。verification_material.certificate是一个有效的签名证书,由 预先 信任的机构(例如验证客户端中已存在的信任根)颁发。verification_material.certificate标识了适当的签名主题,例如发布该包的可信发布者的机器身份。envelope.statement是一个有效的 in-toto v1 Statement,其主题和摘要 必须 与分发的文件名和内容匹配。对于分发的文件名,匹配 必须 通过使用适当的源分发或 wheel 文件名格式进行解析来执行,因为声明的主题可能等效但已规范化。envelope.signature是envelope.statement的有效签名,对应于verification_material.certificate,通过 v1 DSSE 签名协议重新构建。
除了上述必需步骤外,验证器 可以 根据策略额外验证 verification_material.transparency_entries,例如要求至少一个透明日志条目或达到某个条目阈值。在验证透明条目时,验证器 必须 确认每个条目的包含时间落在签名证书的有效期内。
安全隐患
本 PEP 本质上主要是“机械性”的;它提供了构建和提供可验证数字证明的布局,而没有指定关于证明有效性、证明之间的阈值等更高层次的安全“策略”。
证明中的加密灵活性
算法灵活性是密码方案中可利用漏洞的常见来源。本 PEP 从两个方面限制了算法灵活性:
- 所有算法都在一个套件中指定,而不是一组几何参数。这使得攻击者无法(例如)选择一个具有弱哈希函数的强签名算法,从而损害整个方案。
- 证明对象是版本化的,并且只能包含为其版本指定的算法套件。如果将来某个特定套件被认为不安全,客户端可以选择全面拒绝或限定包含该套件的证明的验证。
索引信任
本 PEP 不会 增加(或减少)对索引本身的信任:索引仍然被有效地信任以诚实地交付未经修改的包分发,因为一个能够修改包内容的不诚实索引也可能不诚实地修改或省略包证明。因此,本 PEP 对索引信任的假设等同于早期机制(如 PGP 和 wheel 签名)中未说明的假设。
建议
本 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 Simple 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,以下是一个适当的 in-toto Statement,以 JSON 对象形式表示:
{
"_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