371 lines
15 KiB
ReStructuredText
371 lines
15 KiB
ReStructuredText
|
PEP: 740
|
||
|
Title: Index support for digital attestations
|
||
|
Author: William Woodruff <william@yossarian.net>,
|
||
|
Facundo Tuesca <facundo.tuesca@trailofbits.com>
|
||
|
Sponsor: Donald Stufft <donald@stufft.io>
|
||
|
PEP-Delegate: Donald Stufft <donald@stufft.io>
|
||
|
Discussions-To: https://discuss.python.org/t/pre-pep-exposing-trusted-publisher-provenance-on-pypi/42337
|
||
|
Status: Draft
|
||
|
Type: Informational
|
||
|
Topic: Packaging
|
||
|
Created: 08-Jan-2024
|
||
|
|
||
|
Abstract
|
||
|
========
|
||
|
|
||
|
This PEP proposes a collection of changes related to the upload and distribution
|
||
|
of digitally signed attestations and metadata used to verify them on a Python
|
||
|
package repository, such as PyPI.
|
||
|
|
||
|
These changes have two subcomponents:
|
||
|
|
||
|
* Changes to the currently unstandardized PyPI upload API, allowing clients
|
||
|
to upload digital attestations;
|
||
|
* Changes to the :pep:`503` and :pep:`691` "simple" APIs, allowing clients
|
||
|
to retrieve both digital attestations and
|
||
|
`Trusted Publishing <https://docs.pypi.org/trusted-publishers/>`_ metadata
|
||
|
for individual release files.
|
||
|
|
||
|
This PEP does not recommend a specific digital attestation format, nor does
|
||
|
it make a policy recommendation around mandatory digital attestations on
|
||
|
release uploads or their subsequent verification by installing clients like
|
||
|
``pip``.
|
||
|
|
||
|
Rationale
|
||
|
=========
|
||
|
|
||
|
Desire for digital signatures on Python packages has been repeatedly
|
||
|
expressed by both package maintainers and downstream users:
|
||
|
|
||
|
* Maintainers wish to demonstrate the integrity and authenticity of their
|
||
|
package uploads;
|
||
|
* Individual downstream users wish to verify package integrity and authenticity
|
||
|
without placing additional trust in their index's honesty;
|
||
|
* "Bulk" downstream users (such as Operating System distributions) wish to
|
||
|
perform similar verifications and potentially re-expose or countersign
|
||
|
for their own downstream packaging ecosystems.
|
||
|
|
||
|
This proposal seeks to accommodate each of the above use cases.
|
||
|
|
||
|
While this PEP does not recommend a specific digital attestation format,
|
||
|
it does recognize the utility of Trusted Publishing as a pre-existing,
|
||
|
"zero-configuration" source of strong provenance for Python packages.
|
||
|
Consequently this PEP includes a proposed scheme for exposing each release
|
||
|
file's Trusted Publisher metadata, with the expectation that a future digital
|
||
|
attestation format will likely make use of it.
|
||
|
|
||
|
Design Considerations
|
||
|
---------------------
|
||
|
|
||
|
This PEP identifies the following design considerations when evaluating
|
||
|
both its own proposed changes and previous work in the same or adjacent
|
||
|
areas of Python packaging:
|
||
|
|
||
|
1. Index accessibility: digital attestations for Python packages
|
||
|
are ideally retrievable directly from the index itself, as "detached"
|
||
|
resources.
|
||
|
|
||
|
This both simplifies some compatibility concerns (by avoiding
|
||
|
the need to modify the distribution formats themselves) and also simplifies
|
||
|
the behavior of potential installing clients (by allowing them to
|
||
|
retrieve each attestation before its corresponding package without needing
|
||
|
to do streaming decompression).
|
||
|
|
||
|
2. Verification by the index itself: in addition to enabling verification
|
||
|
by installing clients, each digital attestation is *ideally* verifiable
|
||
|
in some form by the index itself.
|
||
|
|
||
|
This both increases the overall quality
|
||
|
of attestations uploaded to the index (preventing, for example, users
|
||
|
from accidentally uploading incorrect or invalid attestations) and also
|
||
|
enables UI and UX refinements on the index itself (such as a "provenance"
|
||
|
view for each uploaded package).
|
||
|
|
||
|
3. General applicability: digital attestations should be applicable to
|
||
|
*any and every* package uploaded to the index, regardless of its format
|
||
|
(sdist or wheel) or interior contents.
|
||
|
|
||
|
4. Metadata support: this PEP refers to "digital attestations" rather than
|
||
|
just "digital signatures" to emphasize the ideal presence of additional
|
||
|
metadata within the cryptographic envelope.
|
||
|
|
||
|
For example, to prevent domain separation between a distribution's name and
|
||
|
its contents, the digital attestation could be performed over
|
||
|
``HASH(name || HASH(contents))`` rather than just ``HASH(contents)``.
|
||
|
|
||
|
5. Consistent release attestations: if a file belonging to a release has a
|
||
|
set of digital attestations, then all of the other files belonging to that
|
||
|
release should also have the same types of attestations.
|
||
|
|
||
|
This simplifies the downstream use story for digital attestations, and
|
||
|
prevents potentially vulnerable "swiss cheese" release patterns (where
|
||
|
a verifier checks for a valid attestation on ``HolyGrail-1.0.tar.gz``
|
||
|
but their installing client actually resolves an attacker-controlled,
|
||
|
platform-specific ``.whl`` instead).
|
||
|
|
||
|
|
||
|
Previous Work
|
||
|
-------------
|
||
|
|
||
|
PGP signatures
|
||
|
^^^^^^^^^^^^^^
|
||
|
|
||
|
PyPI and other indices have historically supported PGP signatures on uploaded
|
||
|
distributions. These could be supplied during upload, and could be retrieved
|
||
|
by installing clients via the ``data-gpg-sig`` attribute in the :pep:`503`
|
||
|
API, the ``gpg-sig`` key on the :pep:`691` API, or via an adjacent
|
||
|
``.asc``-suffixed URL.
|
||
|
|
||
|
PGP signature uploads have been disabled on PyPI since
|
||
|
`May 2023 <https://blog.pypi.org/posts/2023-05-23-removing-pgp/>`_, after
|
||
|
`an investigation <https://blog.yossarian.net/2023/05/21/PGP-signatures-on-PyPI-worse-than-useless>`_
|
||
|
determined that the majority of signatures (which, themselves, constituted a
|
||
|
tiny percentage of overall uploads) could not be associated with a public key or
|
||
|
otherwise meaningfully verified.
|
||
|
|
||
|
In their previously supported form on PyPI, PGP signatures satisfied
|
||
|
considerations (1) and (3) above but not (2) (owing to the need for external
|
||
|
keyservers and key distribution) or (4) (due to PGP signatures typically being
|
||
|
constructed over just an input file, without any associated signed metadata).
|
||
|
Similarly, PyPI's historical implementation of PGP did not satisfy consideration
|
||
|
(5), due to a lack of consistency checks between different release files
|
||
|
(and an inability to perform those checks due to no access to the signer's
|
||
|
public key).
|
||
|
|
||
|
Wheel signatures
|
||
|
^^^^^^^^^^^^^^^^
|
||
|
|
||
|
:pep:`427` (and its :ref:`living PyPA counterpart <packaging:binary-distribution-format>`)
|
||
|
specify the :term:`wheel format <packaging:Wheel>`.
|
||
|
|
||
|
This format includes accommodations for digital signatures embedded directly
|
||
|
into the wheel, in either JWS or S/MIME format. These signatures are specified
|
||
|
over a :pep:`376` RECORD, which is modified to include a cryptographic digest
|
||
|
for each recorded file in the wheel.
|
||
|
|
||
|
While wheel signatures are fully specified, they do not appear to be broadly
|
||
|
used; the official `wheel tooling <https://github.com/pypa/wheel>`_ deprecated
|
||
|
signature generation and verification support
|
||
|
`in 0.32.0 <https://wheel.readthedocs.io/en/stable/news.html>`_, which was
|
||
|
released in 2018.
|
||
|
|
||
|
Additionally, wheel signatures do not satisfy any of
|
||
|
the above considerations (due to the "attached" nature of the signatures,
|
||
|
non-verifiability on the index itself, and support for wheels only).
|
||
|
|
||
|
Specification
|
||
|
=============
|
||
|
|
||
|
.. _upload-endpoint:
|
||
|
|
||
|
Upload endpoint changes
|
||
|
-----------------------
|
||
|
|
||
|
The current upload API is not standardized. However, we propose the following
|
||
|
changes to it:
|
||
|
|
||
|
* In addition to the current top-level ``content`` and ``gpg_signature`` fields,
|
||
|
the index **SHALL** accept ``attestations`` as an additional multipart form
|
||
|
field.
|
||
|
* The new ``attestations`` field **SHALL** be a JSON object.
|
||
|
* The JSON object **SHALL** have one or more keys, each identifying an
|
||
|
attestation format known to the index. If any key does not identify an
|
||
|
attestation format known to the index, the index **MUST** reject the upload.
|
||
|
* The value associated with each well-known key **SHALL** be a JSON object.
|
||
|
* Each attestation value **MUST** be verifiable by the index. If the index fails
|
||
|
to verify any attestation in ``attestations``, it **MUST** reject the upload.
|
||
|
|
||
|
In addition to the above, the index **SHALL** enforce a consistency
|
||
|
policy for release attestations via the following:
|
||
|
|
||
|
* If the first file under a new release is supplied with ``attestations``,
|
||
|
then all subsequently uploaded files under the same release **MUST** also
|
||
|
have ``attestations``. Conversely, if the first file under a new release
|
||
|
does not have any ``attestations``, then all subsequent uploads under the
|
||
|
same release **MUST NOT** have ``attestations``.
|
||
|
* All files under the same release **MUST** have the same set of well-known
|
||
|
attestation format keys.
|
||
|
|
||
|
The index **MUST** reject any file upload that does not satisfy these
|
||
|
consistency properties.
|
||
|
|
||
|
Index changes
|
||
|
-------------
|
||
|
|
||
|
.. _provenance-object:
|
||
|
|
||
|
Provenance objects
|
||
|
^^^^^^^^^^^^^^^^^^
|
||
|
|
||
|
The index will serve uploaded attestations along with metadata that can assist
|
||
|
in verifying them in the form of JSON serialized objects.
|
||
|
|
||
|
These "provenance objects" will be available via both the :pep:`503` Simple Index
|
||
|
and :pep:`691` JSON-based Simple API as described below, and will have the
|
||
|
following structure:
|
||
|
|
||
|
.. code-block:: json
|
||
|
|
||
|
{
|
||
|
"publisher": {
|
||
|
"type": "important-ci-service",
|
||
|
"claims": {},
|
||
|
"vendor-property": "foo",
|
||
|
"another-property": 123
|
||
|
},
|
||
|
"attestations": {
|
||
|
"some-attestation": {/* ... */},
|
||
|
"another-attestation": {/* ... */}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
* ``publisher`` is an **optional** JSON object, containing a
|
||
|
representation of the file's Trusted Publisher configuration at the time
|
||
|
the file was uploaded to the package index. The keys within the ``publisher``
|
||
|
object are specific to each Trusted Publisher but include, at minimum:
|
||
|
|
||
|
* A ``type`` key, which **MUST** be a JSON string that uniquely identifies the
|
||
|
kind of Trusted Publisher.
|
||
|
* A ``claims`` key, which **MUST** be a JSON object containing any context-specific
|
||
|
claims retained by the index during Trusted Publisher authentication.
|
||
|
|
||
|
All other keys in the ``publisher`` object are publisher-specific. A full
|
||
|
illustrative example of a ``publisher`` object is provided in :ref:`appendix-2`.
|
||
|
* ``attestations`` is a **required** JSON object, containing one or
|
||
|
more attestation objects as identified by their keys. This object is
|
||
|
a superset of ``attestations`` object supplied by the uploader through the
|
||
|
``attestations`` field, as described in :ref:`upload-endpoint`.
|
||
|
|
||
|
Because ``attestations`` is a superset of the file's original uploaded attestations,
|
||
|
the index **MAY** chose to embed additional attestations of its own.
|
||
|
|
||
|
Simple Index
|
||
|
^^^^^^^^^^^^
|
||
|
|
||
|
* When an uploaded file has one or more attestations, the index **MAY** include a
|
||
|
``data-provenance`` attribute on its file link, with a value of either
|
||
|
``true`` or ``false``.
|
||
|
* When ``data-provenance`` is ``true``, the index **MUST** serve a
|
||
|
:ref:`provenance object <provenance-object>` at the same URL, but with
|
||
|
``.provenance`` appended to it. For example, if ``HolyGrail-1.0.tar.gz``
|
||
|
exists and has associated attestations, those attestations would be located
|
||
|
within the provenance object hosted at ``HolyGrail-1.0.tar.gz.provenance``.
|
||
|
|
||
|
JSON-based Simple API
|
||
|
^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
||
|
* When an uploaded file has one or more attestations, the index **MAY** include a
|
||
|
``provenance`` object in the ``file`` dictionary for that file.
|
||
|
* ``provenance``, when present, **MUST** be a :ref:`provenance object <provenance-object>`.
|
||
|
|
||
|
Security Implications
|
||
|
=====================
|
||
|
|
||
|
This PEP is "mechanical" in nature; it provides only the plumbing for future
|
||
|
digital attestations on package indices, without specifying their concrete
|
||
|
cryptographic details.
|
||
|
|
||
|
As such, we do not identify any positive or negative security implications
|
||
|
for this PEP.
|
||
|
|
||
|
Index trust
|
||
|
-----------
|
||
|
|
||
|
This PEP does **not** increase (or decrease) trust in the index itself:
|
||
|
the index is still effectively trusted to honestly deliver unmodified package
|
||
|
distributions, since a dishonest index capable of modifying package
|
||
|
contents could also dishonestly modify or omit package attestations.
|
||
|
As a result, this PEP's presumption of index trust is equivalent to the
|
||
|
unstated presumption with earlier mechanisms, like PGP and Wheel signatures.
|
||
|
|
||
|
This PEP does not preclude or exclude future index trust mechanisms, such
|
||
|
as :pep:`458` and/or :pep:`480`.
|
||
|
|
||
|
Recommendations
|
||
|
===============
|
||
|
|
||
|
This PEP does not recommend specific attestation formats. It does,
|
||
|
however, make the following recommendations to package indices seeking
|
||
|
to create new or implement pre-existing attestation formats:
|
||
|
|
||
|
1. Consult the :ref:`living PyPA specifications <packaging:packaging-specifications>`
|
||
|
first, and determine if any currently defined attestation formats suit
|
||
|
your purpose.
|
||
|
2. If no suitable attestation format is defined under the PyPA specifications,
|
||
|
consider submitting it to the PyPA specifications for longevity and reuse
|
||
|
purposes.
|
||
|
|
||
|
When designing a new attestation format, we make the following recommendations:
|
||
|
|
||
|
1. Pick a short, but unique name for your attestation format; this name will
|
||
|
serve as the attestation's identifier in the upload and index APIs.
|
||
|
|
||
|
When appropriate for an attestation format, we recommend using ``:`` as a
|
||
|
domain separator. For example, an attestation format that provides publish
|
||
|
provenance using `Sigstore <https://www.sigstore.dev/>`_ might have the
|
||
|
name ``sigstore:publish``.
|
||
|
2. Prefer parsimony in your format: avoid optional fields and functionality,
|
||
|
avoid unnecessary cryptographic agility and message malleability, and ensure
|
||
|
that verifying the attestation communicates something meaningful beyond a
|
||
|
basic integrity check (since the index itself already supplies cryptographic
|
||
|
digests for this purpose).
|
||
|
|
||
|
.. _appendix-1:
|
||
|
|
||
|
Appendix 1: Example Uploaded Attestations
|
||
|
=========================================
|
||
|
|
||
|
This appendix provides a fictional example of the ``attestations`` field
|
||
|
submitted on file upload, with two fictional attestations (``publish`` and
|
||
|
``timestamp``):
|
||
|
|
||
|
.. code-block:: json
|
||
|
|
||
|
{
|
||
|
"publish": {
|
||
|
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2",
|
||
|
"verificationMaterial": { /* omitted for brevity */ },
|
||
|
"messageSignature": {
|
||
|
"messageDigest": {
|
||
|
"algorithm": "some-hash-algo",
|
||
|
"digest": "digest-here"
|
||
|
},
|
||
|
"signature": "signature-here"
|
||
|
}
|
||
|
},
|
||
|
"timestamp": {
|
||
|
"cms": "some-long-blob-here"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
The payloads of these fictional attestations are purely illustrative.
|
||
|
|
||
|
.. _appendix-2:
|
||
|
|
||
|
Appendix 2: Example Trusted Publisher Representation
|
||
|
====================================================
|
||
|
|
||
|
This appendix provides a fictional example of a ``publisher`` key within
|
||
|
a :pep:`691` ``project.files[].provenance`` listing:
|
||
|
|
||
|
.. code-block:: json
|
||
|
|
||
|
"publisher": {
|
||
|
"type": "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
|
||
|
}
|
||
|
|
||
|
Copyright
|
||
|
=========
|
||
|
|
||
|
This document is placed in the public domain or under the
|
||
|
CC0-1.0-Universal license, whichever is more permissive.
|