|
| 1 | +PEP: 740 |
| 2 | +Title: Index support for digital attestations |
| 3 | +Author: William Woodruff <william@yossarian.net>, |
| 4 | + Facundo Tuesca <facundo.tuesca@trailofbits.com> |
| 5 | +Sponsor: Donald Stufft <donald@stufft.io> |
| 6 | +PEP-Delegate: Donald Stufft <donald@stufft.io> |
| 7 | +Discussions-To: https://discuss.python.org/t/pre-pep-exposing-trusted-publisher-provenance-on-pypi/42337 |
| 8 | +Status: Draft |
| 9 | +Type: Informational |
| 10 | +Topic: Packaging |
| 11 | +Created: 08-Jan-2024 |
| 12 | + |
| 13 | +Abstract |
| 14 | +======== |
| 15 | + |
| 16 | +This PEP proposes a collection of changes related to the upload and distribution |
| 17 | +of digitally signed attestations and metadata used to verify them on a Python |
| 18 | +package repository, such as PyPI. |
| 19 | + |
| 20 | +These changes have two subcomponents: |
| 21 | + |
| 22 | +* Changes to the currently unstandardized PyPI upload API, allowing clients |
| 23 | + to upload digital attestations; |
| 24 | +* Changes to the :pep:`503` and :pep:`691` "simple" APIs, allowing clients |
| 25 | + to retrieve both digital attestations and |
| 26 | + `Trusted Publishing <https://docs.pypi.org/trusted-publishers/>`_ metadata |
| 27 | + for individual release files. |
| 28 | + |
| 29 | +This PEP does not recommend a specific digital attestation format, nor does |
| 30 | +it make a policy recommendation around mandatory digital attestations on |
| 31 | +release uploads or their subsequent verification by installing clients like |
| 32 | +``pip``. |
| 33 | + |
| 34 | +Rationale |
| 35 | +========= |
| 36 | + |
| 37 | +Desire for digital signatures on Python packages has been repeatedly |
| 38 | +expressed by both package maintainers and downstream users: |
| 39 | + |
| 40 | +* Maintainers wish to demonstrate the integrity and authenticity of their |
| 41 | + package uploads; |
| 42 | +* Individual downstream users wish to verify package integrity and authenticity |
| 43 | + without placing additional trust in their index's honesty; |
| 44 | +* "Bulk" downstream users (such as Operating System distributions) wish to |
| 45 | + perform similar verifications and potentially re-expose or countersign |
| 46 | + for their own downstream packaging ecosystems. |
| 47 | + |
| 48 | +This proposal seeks to accommodate each of the above use cases. |
| 49 | + |
| 50 | +While this PEP does not recommend a specific digital attestation format, |
| 51 | +it does recognize the utility of Trusted Publishing as a pre-existing, |
| 52 | +"zero-configuration" source of strong provenance for Python packages. |
| 53 | +Consequently this PEP includes a proposed scheme for exposing each release |
| 54 | +file's Trusted Publisher metadata, with the expectation that a future digital |
| 55 | +attestation format will likely make use of it. |
| 56 | + |
| 57 | +Design Considerations |
| 58 | +--------------------- |
| 59 | + |
| 60 | +This PEP identifies the following design considerations when evaluating |
| 61 | +both its own proposed changes and previous work in the same or adjacent |
| 62 | +areas of Python packaging: |
| 63 | + |
| 64 | +1. Index accessibility: digital attestations for Python packages |
| 65 | + are ideally retrievable directly from the index itself, as "detached" |
| 66 | + resources. |
| 67 | + |
| 68 | + This both simplifies some compatibility concerns (by avoiding |
| 69 | + the need to modify the distribution formats themselves) and also simplifies |
| 70 | + the behavior of potential installing clients (by allowing them to |
| 71 | + retrieve each attestation before its corresponding package without needing |
| 72 | + to do streaming decompression). |
| 73 | + |
| 74 | +2. Verification by the index itself: in addition to enabling verification |
| 75 | + by installing clients, each digital attestation is *ideally* verifiable |
| 76 | + in some form by the index itself. |
| 77 | + |
| 78 | + This both increases the overall quality |
| 79 | + of attestations uploaded to the index (preventing, for example, users |
| 80 | + from accidentally uploading incorrect or invalid attestations) and also |
| 81 | + enables UI and UX refinements on the index itself (such as a "provenance" |
| 82 | + view for each uploaded package). |
| 83 | + |
| 84 | +3. General applicability: digital attestations should be applicable to |
| 85 | + *any and every* package uploaded to the index, regardless of its format |
| 86 | + (sdist or wheel) or interior contents. |
| 87 | + |
| 88 | +4. Metadata support: this PEP refers to "digital attestations" rather than |
| 89 | + just "digital signatures" to emphasize the ideal presence of additional |
| 90 | + metadata within the cryptographic envelope. |
| 91 | + |
| 92 | + For example, to prevent domain separation between a distribution's name and |
| 93 | + its contents, the digital attestation could be performed over |
| 94 | + ``HASH(name || HASH(contents))`` rather than just ``HASH(contents)``. |
| 95 | + |
| 96 | +5. Consistent release attestations: if a file belonging to a release has a |
| 97 | + set of digital attestations, then all of the other files belonging to that |
| 98 | + release should also have the same types of attestations. |
| 99 | + |
| 100 | + This simplifies the downstream use story for digital attestations, and |
| 101 | + prevents potentially vulnerable "swiss cheese" release patterns (where |
| 102 | + a verifier checks for a valid attestation on ``HolyGrail-1.0.tar.gz`` |
| 103 | + but their installing client actually resolves an attacker-controlled, |
| 104 | + platform-specific ``.whl`` instead). |
| 105 | + |
| 106 | + |
| 107 | +Previous Work |
| 108 | +------------- |
| 109 | + |
| 110 | +PGP signatures |
| 111 | +^^^^^^^^^^^^^^ |
| 112 | + |
| 113 | +PyPI and other indices have historically supported PGP signatures on uploaded |
| 114 | +distributions. These could be supplied during upload, and could be retrieved |
| 115 | +by installing clients via the ``data-gpg-sig`` attribute in the :pep:`503` |
| 116 | +API, the ``gpg-sig`` key on the :pep:`691` API, or via an adjacent |
| 117 | +``.asc``-suffixed URL. |
| 118 | + |
| 119 | +PGP signature uploads have been disabled on PyPI since |
| 120 | +`May 2023 <https://blog.pypi.org/posts/2023-05-23-removing-pgp/>`_, after |
| 121 | +`an investigation <https://blog.yossarian.net/2023/05/21/PGP-signatures-on-PyPI-worse-than-useless>`_ |
| 122 | +determined that the majority of signatures (which, themselves, constituted a |
| 123 | +tiny percentage of overall uploads) could not be associated with a public key or |
| 124 | +otherwise meaningfully verified. |
| 125 | + |
| 126 | +In their previously supported form on PyPI, PGP signatures satisfied |
| 127 | +considerations (1) and (3) above but not (2) (owing to the need for external |
| 128 | +keyservers and key distribution) or (4) (due to PGP signatures typically being |
| 129 | +constructed over just an input file, without any associated signed metadata). |
| 130 | +Similarly, PyPI's historical implementation of PGP did not satisfy consideration |
| 131 | +(5), due to a lack of consistency checks between different release files |
| 132 | +(and an inability to perform those checks due to no access to the signer's |
| 133 | +public key). |
| 134 | + |
| 135 | +Wheel signatures |
| 136 | +^^^^^^^^^^^^^^^^ |
| 137 | + |
| 138 | +:pep:`427` (and its :ref:`living PyPA counterpart <packaging:binary-distribution-format>`) |
| 139 | +specify the :term:`wheel format <packaging:Wheel>`. |
| 140 | + |
| 141 | +This format includes accommodations for digital signatures embedded directly |
| 142 | +into the wheel, in either JWS or S/MIME format. These signatures are specified |
| 143 | +over a :pep:`376` RECORD, which is modified to include a cryptographic digest |
| 144 | +for each recorded file in the wheel. |
| 145 | + |
| 146 | +While wheel signatures are fully specified, they do not appear to be broadly |
| 147 | +used; the official `wheel tooling <https://github.com/pypa/wheel>`_ deprecated |
| 148 | +signature generation and verification support |
| 149 | +`in 0.32.0 <https://wheel.readthedocs.io/en/stable/news.html>`_, which was |
| 150 | +released in 2018. |
| 151 | + |
| 152 | +Additionally, wheel signatures do not satisfy any of |
| 153 | +the above considerations (due to the "attached" nature of the signatures, |
| 154 | +non-verifiability on the index itself, and support for wheels only). |
| 155 | + |
| 156 | +Specification |
| 157 | +============= |
| 158 | + |
| 159 | +.. _upload-endpoint: |
| 160 | + |
| 161 | +Upload endpoint changes |
| 162 | +----------------------- |
| 163 | + |
| 164 | +The current upload API is not standardized. However, we propose the following |
| 165 | +changes to it: |
| 166 | + |
| 167 | +* In addition to the current top-level ``content`` and ``gpg_signature`` fields, |
| 168 | + the index **SHALL** accept ``attestations`` as an additional multipart form |
| 169 | + field. |
| 170 | +* The new ``attestations`` field **SHALL** be a JSON object. |
| 171 | +* The JSON object **SHALL** have one or more keys, each identifying an |
| 172 | + attestation format known to the index. If any key does not identify an |
| 173 | + attestation format known to the index, the index **MUST** reject the upload. |
| 174 | +* The value associated with each well-known key **SHALL** be a JSON object. |
| 175 | +* Each attestation value **MUST** be verifiable by the index. If the index fails |
| 176 | + to verify any attestation in ``attestations``, it **MUST** reject the upload. |
| 177 | + |
| 178 | +In addition to the above, the index **SHALL** enforce a consistency |
| 179 | +policy for release attestations via the following: |
| 180 | + |
| 181 | +* If the first file under a new release is supplied with ``attestations``, |
| 182 | + then all subsequently uploaded files under the same release **MUST** also |
| 183 | + have ``attestations``. Conversely, if the first file under a new release |
| 184 | + does not have any ``attestations``, then all subsequent uploads under the |
| 185 | + same release **MUST NOT** have ``attestations``. |
| 186 | +* All files under the same release **MUST** have the same set of well-known |
| 187 | + attestation format keys. |
| 188 | + |
| 189 | +The index **MUST** reject any file upload that does not satisfy these |
| 190 | +consistency properties. |
| 191 | + |
| 192 | +Index changes |
| 193 | +------------- |
| 194 | + |
| 195 | +.. _provenance-object: |
| 196 | + |
| 197 | +Provenance objects |
| 198 | +^^^^^^^^^^^^^^^^^^ |
| 199 | + |
| 200 | +The index will serve uploaded attestations along with metadata that can assist |
| 201 | +in verifying them in the form of JSON serialized objects. |
| 202 | + |
| 203 | +These "provenance objects" will be available via both the :pep:`503` Simple Index |
| 204 | +and :pep:`691` JSON-based Simple API as described below, and will have the |
| 205 | +following structure: |
| 206 | + |
| 207 | +.. code-block:: json |
| 208 | +
|
| 209 | + { |
| 210 | + "publisher": { |
| 211 | + "type": "important-ci-service", |
| 212 | + "claims": {}, |
| 213 | + "vendor-property": "foo", |
| 214 | + "another-property": 123 |
| 215 | + }, |
| 216 | + "attestations": { |
| 217 | + "some-attestation": {/* ... */}, |
| 218 | + "another-attestation": {/* ... */} |
| 219 | + } |
| 220 | + } |
| 221 | +
|
| 222 | +* ``publisher`` is an **optional** JSON object, containing a |
| 223 | + representation of the file's Trusted Publisher configuration at the time |
| 224 | + the file was uploaded to the package index. The keys within the ``publisher`` |
| 225 | + object are specific to each Trusted Publisher but include, at minimum: |
| 226 | + |
| 227 | + * A ``type`` key, which **MUST** be a JSON string that uniquely identifies the |
| 228 | + kind of Trusted Publisher. |
| 229 | + * A ``claims`` key, which **MUST** be a JSON object containing any context-specific |
| 230 | + claims retained by the index during Trusted Publisher authentication. |
| 231 | + |
| 232 | + All other keys in the ``publisher`` object are publisher-specific. A full |
| 233 | + illustrative example of a ``publisher`` object is provided in :ref:`appendix-2`. |
| 234 | +* ``attestations`` is a **required** JSON object, containing one or |
| 235 | + more attestation objects as identified by their keys. This object is |
| 236 | + a superset of ``attestations`` object supplied by the uploader through the |
| 237 | + ``attestations`` field, as described in :ref:`upload-endpoint`. |
| 238 | + |
| 239 | + Because ``attestations`` is a superset of the file's original uploaded attestations, |
| 240 | + the index **MAY** chose to embed additional attestations of its own. |
| 241 | + |
| 242 | +Simple Index |
| 243 | +^^^^^^^^^^^^ |
| 244 | + |
| 245 | +* When an uploaded file has one or more attestations, the index **MAY** include a |
| 246 | + ``data-provenance`` attribute on its file link, with a value of either |
| 247 | + ``true`` or ``false``. |
| 248 | +* When ``data-provenance`` is ``true``, the index **MUST** serve a |
| 249 | + :ref:`provenance object <provenance-object>` at the same URL, but with |
| 250 | + ``.provenance`` appended to it. For example, if ``HolyGrail-1.0.tar.gz`` |
| 251 | + exists and has associated attestations, those attestations would be located |
| 252 | + within the provenance object hosted at ``HolyGrail-1.0.tar.gz.provenance``. |
| 253 | + |
| 254 | +JSON-based Simple API |
| 255 | +^^^^^^^^^^^^^^^^^^^^^ |
| 256 | + |
| 257 | +* When an uploaded file has one or more attestations, the index **MAY** include a |
| 258 | + ``provenance`` object in the ``file`` dictionary for that file. |
| 259 | +* ``provenance``, when present, **MUST** be a :ref:`provenance object <provenance-object>`. |
| 260 | + |
| 261 | +Security Implications |
| 262 | +===================== |
| 263 | + |
| 264 | +This PEP is "mechanical" in nature; it provides only the plumbing for future |
| 265 | +digital attestations on package indices, without specifying their concrete |
| 266 | +cryptographic details. |
| 267 | + |
| 268 | +As such, we do not identify any positive or negative security implications |
| 269 | +for this PEP. |
| 270 | + |
| 271 | +Index trust |
| 272 | +----------- |
| 273 | + |
| 274 | +This PEP does **not** increase (or decrease) trust in the index itself: |
| 275 | +the index is still effectively trusted to honestly deliver unmodified package |
| 276 | +distributions, since a dishonest index capable of modifying package |
| 277 | +contents could also dishonestly modify or omit package attestations. |
| 278 | +As a result, this PEP's presumption of index trust is equivalent to the |
| 279 | +unstated presumption with earlier mechanisms, like PGP and Wheel signatures. |
| 280 | + |
| 281 | +This PEP does not preclude or exclude future index trust mechanisms, such |
| 282 | +as :pep:`458` and/or :pep:`480`. |
| 283 | + |
| 284 | +Recommendations |
| 285 | +=============== |
| 286 | + |
| 287 | +This PEP does not recommend specific attestation formats. It does, |
| 288 | +however, make the following recommendations to package indices seeking |
| 289 | +to create new or implement pre-existing attestation formats: |
| 290 | + |
| 291 | +1. Consult the :ref:`living PyPA specifications <packaging:packaging-specifications>` |
| 292 | + first, and determine if any currently defined attestation formats suit |
| 293 | + your purpose. |
| 294 | +2. If no suitable attestation format is defined under the PyPA specifications, |
| 295 | + consider submitting it to the PyPA specifications for longevity and reuse |
| 296 | + purposes. |
| 297 | + |
| 298 | +When designing a new attestation format, we make the following recommendations: |
| 299 | + |
| 300 | +1. Pick a short, but unique name for your attestation format; this name will |
| 301 | + serve as the attestation's identifier in the upload and index APIs. |
| 302 | + |
| 303 | + When appropriate for an attestation format, we recommend using ``:`` as a |
| 304 | + domain separator. For example, an attestation format that provides publish |
| 305 | + provenance using `Sigstore <https://www.sigstore.dev/>`_ might have the |
| 306 | + name ``sigstore:publish``. |
| 307 | +2. Prefer parsimony in your format: avoid optional fields and functionality, |
| 308 | + avoid unnecessary cryptographic agility and message malleability, and ensure |
| 309 | + that verifying the attestation communicates something meaningful beyond a |
| 310 | + basic integrity check (since the index itself already supplies cryptographic |
| 311 | + digests for this purpose). |
| 312 | + |
| 313 | +.. _appendix-1: |
| 314 | + |
| 315 | +Appendix 1: Example Uploaded Attestations |
| 316 | +========================================= |
| 317 | + |
| 318 | +This appendix provides a fictional example of the ``attestations`` field |
| 319 | +submitted on file upload, with two fictional attestations (``publish`` and |
| 320 | +``timestamp``): |
| 321 | + |
| 322 | +.. code-block:: json |
| 323 | +
|
| 324 | + { |
| 325 | + "publish": { |
| 326 | + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", |
| 327 | + "verificationMaterial": { /* omitted for brevity */ }, |
| 328 | + "messageSignature": { |
| 329 | + "messageDigest": { |
| 330 | + "algorithm": "some-hash-algo", |
| 331 | + "digest": "digest-here" |
| 332 | + }, |
| 333 | + "signature": "signature-here" |
| 334 | + } |
| 335 | + }, |
| 336 | + "timestamp": { |
| 337 | + "cms": "some-long-blob-here" |
| 338 | + } |
| 339 | + } |
| 340 | +
|
| 341 | +The payloads of these fictional attestations are purely illustrative. |
| 342 | + |
| 343 | +.. _appendix-2: |
| 344 | + |
| 345 | +Appendix 2: Example Trusted Publisher Representation |
| 346 | +==================================================== |
| 347 | + |
| 348 | +This appendix provides a fictional example of a ``publisher`` key within |
| 349 | +a :pep:`691` ``project.files[].provenance`` listing: |
| 350 | + |
| 351 | +.. code-block:: json |
| 352 | +
|
| 353 | + "publisher": { |
| 354 | + "type": "GitHub", |
| 355 | + "claims": { |
| 356 | + "ref": "refs/tags/v1.0.0", |
| 357 | + "sha": "da39a3ee5e6b4b0d3255bfef95601890afd80709" |
| 358 | + }, |
| 359 | + "repository_name": "HolyGrail", |
| 360 | + "repository_owner": "octocat", |
| 361 | + "repository_owner_id": "1", |
| 362 | + "workflow_filename": "publish.yml", |
| 363 | + "environment": null |
| 364 | + } |
| 365 | +
|
| 366 | +Copyright |
| 367 | +========= |
| 368 | + |
| 369 | +This document is placed in the public domain or under the |
| 370 | +CC0-1.0-Universal license, whichever is more permissive. |
0 commit comments