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 討論串
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2024年1月8日
- 公告歷史:
- 2024年1月2日, 2024年1月29日
- 決議:
- 2024年7月17日
摘要
本 PEP 提議針對 Python 套件儲存庫(如 PyPI)中數位簽署認證的上傳、分發及其驗證所使用的中繼資料,進行一系列的變更。
這些變更包含兩個子項目:
- 修改目前未標準化的 PyPI 上傳 API,允許用戶端將數位認證作為認證物件上傳;
- 修改 HTML 與 JSON “Simple” API,允許用戶端檢索個別發佈檔案的數位認證與受信任發佈 (Trusted Publishing) 中繼資料,並以來源證明物件的形式呈現。
本 PEP 不會針對發佈上傳時是否強制執行數位認證,或由安裝程式(如 pip)進行後續驗證提出任何政策建議。
基本原理與動機
套件維護者與下游使用者多次表達了對 Python 套件數位簽名的需求:
- 維護者希望證明其套件上傳的完整性與真實性;
- 個別下游使用者希望在不完全依賴索引本身的誠信前提下,驗證套件的完整性與真實性;
- 「批量」下游使用者(如作業系統發行版)希望進行類似的驗證,並可能為其下游封裝生態系統進行重新公開或連署。
本提案旨在滿足上述所有使用情境。
此外,本提案確認了以下動機:
- Python 套件分發的可驗證來源:許多 Python 套件目前包含「未經身分驗證」的來源中繼資料(例如原始碼主機的 URL)。加密認證格式可實現套件與其原始碼主機之間強大的「經身分驗證」連結,使索引與下游使用者均能以密碼學方式驗證套件確實源自其所宣稱的原始碼儲存庫。
- 提高攻擊者的門檻:試圖劫持 Python 套件的攻擊者可依據「複雜度」(從不複雜到複雜)與「目標性」(從機會主義到針對性)進行描述。
數位認證增加了複雜度要求:攻擊者必須具備足夠的能力存取私有簽署材料(或簽署身分)。
- 索引可驗證性:在現狀下,索引唯一提供的認證是每個發佈檔案的可選 PGP 簽名(參見 PGP 簽名)。這些簽名並未(且無法)被索引檢查格式正確性或有效性,因為索引沒有識別簽名正確公鑰的機制。本 PEP 透過確保來源證明物件包含索引驗證認證有效性所需的所有中繼資料,克服了這一限制。
本 PEP 提議了一種通用的認證格式,包含一個用於簽名生成的認證聲明,期望索引提供者採用該格式,並配合適合的簽名驗證身分來源(如受信任發佈)。
設計考量
本 PEP 在評估自身提案與 Python 封裝領域中相關先前工作時,確立了以下設計考量:
- 索引的可存取性: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 檢索。
由於一項調查確定大多數簽名(本身僅佔總上傳量的極小比例)無法與公鑰相關聯或以其他方式進行有意義的驗證,PyPI 自 2023 年 5 月起禁用了 PGP 簽名上傳。
在 PyPI 先前支援的形式中,PGP 簽名滿足了上述考量 (1) 與 (3),但無法滿足 (2)(由於需要外部金鑰伺服器與金鑰分發)或 (4)(因為 PGP 簽名通常僅針對輸入檔案進行建構,沒有任何相關聯的簽署中繼資料)。
Wheel 簽名
PEP 427(及其現行的 PyPA 對應規範)規範了 wheel 格式。
該格式包含直接嵌入 wheel 中的數位簽名調整,格式為 JWS 或 S/MIME。這些簽名是針對 PEP 376 的 RECORD 進行規範的,該記錄被修改以包含 wheel 中每個已記錄檔案的加密摘要。
儘管 wheel 簽名已有完整規範,但似乎未被廣泛使用;官方 wheel 工具在 2018 年發佈的 0.32.0 版本中廢棄了對簽名生成與驗證的支援。
此外,wheel 簽名無法滿足上述任何考量(由於簽名是「附加」性質、在索引本身不可驗證,且僅支援 wheel)。
規範
上傳端點變更
目前的上傳 API 尚未標準化。然而,我們對其提出以下變更:
- 除了目前頂層的
content與gpg_signature欄位外,索引**應 (SHALL)** 接受attestations作為額外的多部分 (multipart) 表單欄位。 - 新的
attestations欄位**應 (SHALL)** 為 JSON 陣列。 attestations陣列**應 (SHALL)** 包含一或多個項目,每個項目為代表個別認證的 JSON 物件。- 每個認證物件**必須 (MUST)** 能由索引進行驗證。若索引無法驗證
attestations中的任何認證,則**必須 (MUST)** 拒絕該次上傳。認證物件的格式定義於認證物件,而認證驗證流程定義於認證驗證。
索引變更
簡單索引 (Simple Index)
針對 Simple Repository API 進行以下變更:
- 當上傳的檔案具有一或多個認證時,索引**可 (MAY)** 提供包含與該分發相關聯認證的來源證明檔案。來源證明檔案的格式**應 (SHALL)** 為 JSON 編碼的來源證明物件,其中**應 (SHALL)** 包含該檔案的認證。
來源證明檔案的位置由索引透過
data-provenance屬性標示。 - 當來源證明檔案存在時,索引**可 (MAY)** 在其檔案連結中包含
data-provenance屬性。data-provenance屬性的值**應 (SHALL)** 為完全限定的 URL,標示該檔案的來源證明可在該 URL 找到。此 URL **必須 (MUST)** 代表一個安全來源 (secure origin)。下表提供發佈檔案 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(無效:非安全來源) - 索引**可 (MAY)** 選擇修改來源證明檔案。例如,索引**可 (MAY)** 允許新增額外的認證與驗證材料,例如來自第三方稽核人員或其他服務的認證。
參見對來源證明物件的變更,以進一步討論檔案來源證明可能變更的原因。
基於 JSON 的 Simple API
針對 JSON Simple API 進行以下變更:
- 當上傳的檔案具有一或多個認證時,索引**可 (MAY)** 在該檔案的
file字典中包含一個provenance鍵。provenance鍵的值**應 (SHALL)** 為 JSON 字串或null。若provenance非null,則其**應 (SHALL)** 為指向相關來源證明檔案的 URL。參見附錄 3:Simple JSON API 大小考量,了解在 JSON API 中嵌入 SHA-256 摘要而非完整來源證明物件的技術決策說明。
這些變更要求 JSON API 的版本必須變更:
api-version**應 (SHALL)** 指定為 1.3 或更高版本。
認證物件 (Attestation objects)
認證物件是一個具有多個必要鍵的 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:透明度記錄條目的資料模型。認證物件**應 (SHOULD)** 包含一或多個透明度記錄條目,並**可 (MAY)** 為其他已簽署時間來源包含額外鍵(例如 RFC 3161 時間戳記授權單位或 Roughtime 伺服器)。
認證物件具有版本編號;本 PEP 指定版本為 1。每個版本均綁定至單一加密套件,以最大限度地減少不必要的加密靈活性。在第 1 版中,套件如下:
- 憑證指定為 X.509 憑證,並符合 RFC 5280 中的設定檔。
- 訊息簽名演算法為 ECDSA,公鑰使用 P-256 曲線,加密摘要函數為 SHA-256。
未來的 PEP 可能會透過選擇新的版本編號來變更此套件(以及認證物件的整體形狀)。
認證聲明與簽名生成
「認證聲明 (attestation statement)」是認證物件中實際進行加密簽署的聲明(即 envelope.statement)。
認證聲明以 JSON 形式編碼為 v1 in-toto 聲明物件。序列化時,該聲明被視為不透明的二進位資料塊 (blob),無需標準化。JSON 編碼的聲明範例提供於附錄 4:認證聲明範例。
除了作為 v1 in-toto 聲明外,認證聲明還受到以下限制:
- in-toto
subject**必須 (MUST)** 僅包含單一主體。 subject[0].name為分發的檔名,其**必須 (MUST)** 為有效的原始碼分發或wheel 分發檔名。subject[0].digest**必須 (MUST)** 包含 SHA-256 摘要。可同時存在其他摘要。摘要**必須 (MUST)** 以十六進位字串表示。- 支援以下
predicateType值:- SLSA Provenance:
https://slsa.dev/provenance/v1 - PyPI 發佈認證:
https://docs.pypi.org/attestations/publish/v1
- SLSA Provenance:
針對此聲明的簽名使用 v1 DSSE 簽名協定建構,PAYLOAD_TYPE 為 application/vnd.in-toto+json,PAYLOAD_BODY 為上述 JSON 編碼的聲明。不允許使用其他 PAYLOAD_TYPE。
來源證明物件 (Provenance objects)
索引將提供已上傳的認證以及以 JSON 序列化物件形式提供的中繼資料,協助進行驗證。
這些「來源證明物件」將透過上述 Simple Index 與 JSON-based Simple 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 陣列,包含一或多個認證「套件 (bundles)」。每個套件對應一個簽署身分(如受信任發佈身分),並包含一或多個認證物件。如
Publisher模型中所述,每個AttestationBundle.publisher物件皆為其受信任發佈者所特有,但必須至少包含:- 一個
kind鍵,其**必須 (MUST)** 為唯一識別受信任發佈者類型的 JSON 字串。 - 一個
claims鍵,其**必須 (MUST)** 為包含索引在驗證受信任發佈者期間所保留任何情境特定聲明的 JSON 物件。
發佈者物件中的所有其他鍵均為發佈者專用。發佈者物件的完整說明範例提供於附錄 1:受信任發佈者表示範例。
認證物件的每個陣列皆是上傳時透過
attestations欄位所提供之attestations陣列的超集,詳見上傳端點變更與對來源證明物件的變更。- 一個
對來源證明物件的變更
來源證明物件「非」不可變,並可能隨時間變更。來源證明物件變更的原因包括但不限於:
- 為預先存在的簽署身分新增新認證:索引**可 (MAY)** 選擇允許預先存在的簽署身分進行額外認證,例如針對已上傳檔案的較新認證版本。
- 新增簽署身分與相關認證:索引**可 (MAY)** 選擇支援來自檔案上傳者以外來源的認證,例如第三方稽核人員或索引本身。這些認證可非同步執行,要求索引「事後」將其插入來源證明物件中。
認證驗證
針對分發檔案驗證認證物件需要驗證以下各項:
version為1。驗證程式**必須 (MUST)** 拒絕任何其他版本。verification_material.certificate為有效的簽署憑證,由「事先」信任的權威機構簽發(例如驗證用戶端中已存在的信任根)。verification_material.certificate識別出適當的簽署主體,例如發佈該套件的受信任發佈者機器身分。envelope.statement為有效的 in-toto v1 聲明,其主體與摘要**必須 (MUST)** 與分發檔案的檔名與內容相符。針對分發檔案的檔名,匹配**必須 (MUST)** 透過使用適當的原始碼分發或 wheel 檔名格式進行解析,因為聲明的主體可能是等效但經過正規化的。envelope.signature為對應至verification_material.certificate之envelope.statement的有效簽名,透過 v1 DSSE 簽名協定重組。
除了上述必要步驟外,驗證程式**可 (MAY)** 額外基於政策驗證 verification_material.transparency_entries,例如要求至少一個透明度記錄條目或滿足一定閾值的條目。驗證透明度記錄時,驗證程式**必須 (MUST)** 確認每個條目的包含時間皆位於簽署憑證的有效期限內。
安全性影響
本 PEP 本質上主要是「機械式」的;它提供了建構與服務可驗證數位認證的佈局,而不指定關於認證有效性、認證間閾值等更高層級的安全「政策」。
認證中的加密靈活性
演算法靈活性是加密方案中常見的可利用漏洞來源。本 PEP 透過以下兩種方式限制演算法靈活性:
- 所有演算法均指定在單一套件中,而非參數的幾何集合。這使得攻擊者不可能(例如)選擇強大的簽名演算法搭配脆弱的雜湊函數,從而損害整體方案。
- 認證物件具有版本控制,且僅能包含該版本指定的加密套件。若未來某個特定套件被認為不安全,用戶端可選擇全面拒絕或限定驗證包含該套件的認證。
索引信任
本 PEP **不會**增加(或減少)對索引本身的信任:索引依然被有效地信任以誠實地傳遞未經修改的套件分發,因為有能力修改套件內容的不誠實索引同樣可以不誠實地修改或省略套件認證。因此,本 PEP 對索引信任的預設與早期機制(如 PGP 與 wheel 簽名)所隱含的預設相同。
建議
本 PEP 建議(但不強制)認證物件包含一或多個可驗證的已簽署時間來源,以佐證簽署憑證宣稱的有效期限。實作本 PEP 的索引可選擇嚴格執行此要求。
附錄 1:受信任發佈者 (Trusted Publisher) 表示範例
本附錄提供 Simple 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:透明度記錄條目 (Transparency Log Entries) 的資料模型
本附錄包含認證物件中透明度記錄條目的虛擬資料模型。每個透明度記錄條目作為已簽署包含時間的來源,且可透過線上或離線方式進行驗證。
@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:Simple 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 聲明(以 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