Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 740 – Index support for digital attestations

Author:
William Woodruff <william at yossarian.net>, Facundo Tuesca <facundo.tuesca at trailofbits.com>, Dustin Ingram <di at python.org>
Sponsor:
Donald Stufft <donald at stufft.io>
PEP-Delegate:
Donald Stufft <donald at stufft.io>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Informational
Topic:
Packaging
Created:
08-Jan-2024
Post-History:
02-Jan-2024, 29-Jan-2024

Table of Contents

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:

This PEP does not make a policy recommendation around mandatory digital attestations on release uploads or their subsequent verification by installing clients like pip.

Rationale and Motivation

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.

Additionally, this proposal identifies the following motivations:

  • Verifiable provenance for Python package distributions: many Python packages currently contain unauthenticated provenance metadata, such as URLs for source hosts. A cryptographic attestation format could enable strong authenticated links between these packages and their source hosts, allowing both the index and downstream users to cryptographically verify that a package originates from its claimed source repository.
  • Raising attacker requirements: an attacker who seeks to take over a Python package can be described along sophistication (unsophisticated to sophisticated) and targeting dimensions (opportunistic to targeted).

    Digital attestations impose additional sophistication requirements: the attacker must be sufficiently sophisticated to access private signing material (or signing identities).

  • Index verifiability: in the status quo, the only attestation provided by the index is an optional PGP signature per release file (see PGP signatures). These signatures are not (and cannot be) checked by the index either for well-formedness or for validity, since the index has no mechanism for identifying the right public key for the signature. This PEP overcomes this limitation by ensuring that provenance objects contain all of the metadata needed by the index to verify an attestation’s validity.

This PEP proposes a generic attestation format, containing an attestation payload for signature generation, with the expectation that index providers adopt the format with a suitable source of identity for signature verification, such as Trusted Publishing.

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, this PEP proposes that digital attestations be performed over HASH(name || HASH(contents)) rather than just HASH(contents).

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, after an investigation 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).

Wheel signatures

PEP 427 (and its living PyPA counterpart) specify the wheel format.

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 deprecated signature generation and verification support in 0.32.0, 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 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 array.
  • The attestations array SHALL have one or more items, each a JSON object representing an individual attestation.
  • Each attestation object MUST be verifiable by the index. If the index fails to verify any attestation in attestations, it MUST reject the upload. The format of attestation objects is defined under Attestation objects and the process for verifying attestations is defined under Attestation verification.

Index changes

Simple Index

  • When an uploaded file has one or more attestations, the index MAY provide a .provenance file adjacent to the hosted distribution. The format of the .provenance file SHALL be a JSON-encoded provenance object, which SHALL contain the file’s attestations.

    For example, if an uploaded file is hosted at the URL https://example.com/sampleproject-1.2.3.tar.gz, the provenance URL would be https://example.com/sampleproject-1.2.3.tar.gz.provenance.

  • When a .provenance file is present, the index MAY include a data-provenance attribute on its file link. The value of the data-provenance attribute SHALL be the SHA256 digest of the associated .provenance file.
  • The index MAY choose to modify the .provenance file. For example, the index MAY permit adding additional attestations and verification materials, such as attestations from third-party auditors or other services. When the index modifies the .provenance file, it MUST also update the data-provenance attribute’s value to the new SHA256 digest.

    See Changes to provenance objects for an additional discussion of reasons why a file’s provenance may change.

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. The format of the provenance object SHALL be a JSON-encoded provenance object, which SHALL contain the file’s attestations.
  • The index MAY choose to modify the provenance object, under the same conditions as the .provenance file specified above.

    See Changes to provenance objects for an additional discussion of reasons why a file’s provenance may change.

These changes require a version change to the JSON API:

  • The api-version SHALL specify version 1.2 or later.

Attestation objects

An attestation object is a JSON object with several required keys; applications or signers may include additional keys so long as all explicitly listed keys are provided. The required layout of an attestation object is provided as pseudocode below.

@dataclass
class Attestation:
    version: Literal[1]
    """
    The attestation object's version, which is always 1.
    """

    verification_material: VerificationMaterial
    """
    Cryptographic materials used to verify `message_signature`.
    """

    message_signature: str
    """
    The attestation's signature, as `base64(raw-sig)`, where `raw-sig`
    is the raw bytes of the signing operation over the attestation payload.
    """

@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.
    """

A full data model for each object in transparency_entries is provided in Appendix 2: Data models for Transparency Log Entries. Attestation objects SHOULD include one or more transparency log entries, and MAY include additional keys for other sources of signed time (such as an RFC 3161 Time Stamping Authority or a Roughtime server).

Attestation objects are versioned; this PEP specifies version 1. Each version is tied to a single cryptographic suite to minimize unnecessary cryptographic agility. In version 1, the suite is as follows:

  • Certificates are specified as X.509 certificates, and comply with the profile in RFC 5280.
  • The message signature algorithm is ECDSA, with the P-256 curve for public keys and SHA-256 as the cryptographic digest function.

Future PEPs may change this suite (and the overall shape of the attestation object) by selecting a new version number.

Attestation payload and signature generation

The attestation payload is the actual claim that is cryptographically signed over within the attestation object (as the message_signature).

The attestation payload is encoded as an RFC 8785 canonicalized JSON object, with the following pseudocode layout:

@dataclass
class AttestationPayload:
    distribution: str
    """
    The file name of the Python package distribution.
    """

    digest: str
    """
    The SHA-256 digest of the distribution's contents, as a hexadecimal string.
    """

The value of distribution is the same distribution filename that appears in the PEP 503 and PEP 691 APIs. For example, distribution would be sampleproject-1.2.0-py2.py3-none-any.whl for the following simple index entry:

<a href="https://example.com/...">sampleproject-1.2.0-py2.py3-none-any.whl</a><br/>

In practice, this means that distribution is defined by the PyPA’s living specifications for binary distributions and source distributions, although non-conforming distributions may be hosted by the index.

The following pseudocode demonstrates the construction of an attestation payload and its signature:

def build_payload(dist: Path) -> AttestationPayload:
    return AttestationPayload(
      distribution=dist.name,
      digest=sha256(dist.read_bytes()).hexdigest,
    )

attestation_payload = build_payload("sampleproject-1.2.0-py2.py3-none-any.whl")

# canonical_json is a fictitious module that performs RFC 8785 canonical
# JSON serialization.
encoded_payload = canonical_json.dumps(asdict(attestation_payload))

raw_signature = signing_key.sign(encoded_payload, ECDSA(SHA2_256()))
message_signature = b64encode(raw_signature)

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 above, and will have the following layout:

{
    "version": 1,
    "attestation_bundles": [
      {
        "publisher": {
          "kind": "important-ci-service",
          "claims": {},
          "vendor-property": "foo",
          "another-property": 123
        },
        "attestations": [
          { /* attestation 1 ... */ },
          { /* attestation 2 ... */ }
        ]
      }
    ]
}

or, as pseudocode:

@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 is 1. Like attestation objects, provenance objects are versioned, and this PEP only defines version 1.
  • attestation_bundles is a required JSON array, containing one or more “bundles” of attestations. Each bundle corresponds to a signing identity (such as a Trusted Publishing identity), and contains one or more attestation objects.

    As noted in the Publisher model, each AttestationBundle.publisher object is specific to its Trusted Publisher but must include at minimum:

    • A kind 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 Appendix 1: Example Trusted Publisher Representation.

    Each array of attestation objects is a superset of the attestations array supplied by the uploaded through the attestations field at upload time, as described in Upload endpoint changes and Changes to provenance objects.

Changes to provenance objects

Provenance objects are not immutable, and may change over time. Reasons for changes to the provenance object include but are not limited to:

  • Addition of new attestations for a pre-existing signing identity: the index MAY choose to allow additional attestations by pre-existing signing identities, such as newer attestation versions for already uploaded files.
  • Addition of new signing identities and associated attestations: the index MAY choose to support attestations from sources other than the file’s uploader, such as third-party auditors or the index itself. These attestations may be performed asynchronously, requiring the index to insert them into the provenance object post facto.

Attestation verification

Verifying an attestation object requires verification of each of the following:

  • version is 1. The verifier MUST reject any other version.
  • verification_material.certificate is a valid signing certificate, as issued by an a priori trusted authority (such as a root of trust already present within the verifying client).
  • verification_material.certificate identifies an appropriate signing subject, such as the machine identity of the Trusted Publisher that published the package.
  • message_signature can be verified by verification_material.certificate, using the reconstructed attestation payload as the cleartext input. The verifier MUST reconstruct the attestation payload itself.

In addition to the above required steps, a verifier MAY additionally verify verification_material.transparency_entries on a policy basis, e.g. requiring at least one transparency log entry or a threshold of entries. When verifying transparency entries, the verifier MUST confirm that the inclusion time for each entry lies within the signing certificate’s validity period.

Security Implications

This PEP is primarily “mechanical” in nature; it provides layouts for structuring and serving verifiable digital attestations without specifying higher level security “policies” around attestation validity, thresholds between attestations, and so forth.

Cryptographic agility in attestations

Algorithmic agility is a common source of exploitable vulnerabilities in cryptographic schemes. This PEP limits algorithmic agility in two ways:

  • All algorithms are specified in a single suite, rather than a geometric collection of parameters. This makes it impossible (for example) for an attacker to select a strong signature algorithm with a weak hash function, compromising the scheme as a whole.
  • Attestation objects are versioned, and may only contain the algorithmic suite specified for their version. If a specific suite is considered insecure in the future, clients may choose to blanket reject or qualify verifications of attestations that contain that suite.

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.

Flexible attestations

This PEP specifies a fixed attestation payload (defined in Attestation payload and signature generation), binding the contents of each uploaded file to its public name on the index. This payload format is fixed and inflexible to ease implementation, and to minimize additional mechanical changes to the index itself (e.g., needing to store and service detached attestation documents).

This PEP does not preclude or exclude future more flexible attestation payload formats, such as ones built on in-toto.

Recommendations

This PEP recommends, but does not mandate, that attestation objects contain one or more verifiable sources of signed time that corroborate the signing certificate’s claimed validity period. Indices that implement this PEP may choose to strictly enforce this requirement.

Appendix 1: Example Trusted Publisher Representation

This appendix provides a fictional example of a publisher key within a simple JSON API project.files[].provenance listing:

"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
}

Appendix 2: Data models for Transparency Log Entries

This appendix contains pseudocoded data models for transparency log entries in attestation objects. Each transparency log entry serves as a source of signed inclusion time, and can be verified either online or offline.

@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 the 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.
    """

Source: https://github.com/python/peps/blob/main/peps/pep-0740.rst

Last modified: 2024-04-24 17:54:44 GMT