-
Notifications
You must be signed in to change notification settings - Fork 38
CEP about serving sigstore attestations in Conda repositories #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
6f4b35e
ecc6b31
b360b43
6b0904f
b93d8f5
889062f
613e1d3
1a16ffd
851d050
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,261 @@ | ||||||||||||||||||
| # CEP XXXX - Distribution of Sigstore Attestations for Conda Packages | ||||||||||||||||||
|
|
||||||||||||||||||
| <table> | ||||||||||||||||||
| <tr><td> Title </td><td> Distribution of Sigstore Attestations for Conda Packages </td> | ||||||||||||||||||
| <tr><td> Status </td><td> Draft </td></tr> | ||||||||||||||||||
| <tr><td> Author(s) </td><td> Wolf Vollprecht <wolf@prefix.dev></td></tr> | ||||||||||||||||||
| <tr><td> Created </td><td> Dec 02, 2025</td></tr> | ||||||||||||||||||
| <tr><td> Updated </td><td> Dec 02, 2025</td></tr> | ||||||||||||||||||
| <tr><td> Discussion </td><td> NA </td></tr> | ||||||||||||||||||
| <tr><td> Implementation </td><td> https://prefix.dev (preview implementation) </td></tr> | ||||||||||||||||||
| <tr><td> Requires </td><td> CEP 27 (Publish Attestation) </td></tr> | ||||||||||||||||||
| </table> | ||||||||||||||||||
|
|
||||||||||||||||||
| > The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", | ||||||||||||||||||
| "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as | ||||||||||||||||||
| described in [RFC2119][RFC2119] when, and only when, they appear in all capitals, as shown here. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Abstract | ||||||||||||||||||
|
|
||||||||||||||||||
| This CEP defines a standard endpoint for distributing [Sigstore] attestations alongside conda packages. Building upon [CEP 27], which standardizes the attestation format, this proposal specifies how channels serve attestations to clients via a `.sigs` sidecar endpoint, enabling verification of package integrity and provenance. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Motivation | ||||||||||||||||||
|
|
||||||||||||||||||
| [CEP 27] defines a standard attestation format for the conda ecosystem using [in-toto] statements and [Sigstore] bundles. However, it explicitly leaves the distribution mechanism as future work: | ||||||||||||||||||
|
|
||||||||||||||||||
| > "This CEP does not specify a distribution mechanism for attestations (i.e., Sigstore bundles containing attestations)." | ||||||||||||||||||
|
|
||||||||||||||||||
| Without a standardized distribution mechanism, clients cannot reliably discover and retrieve attestations. This CEP addresses that gap by defining a simple, RESTful endpoint that: | ||||||||||||||||||
|
|
||||||||||||||||||
| 1. **Enables client verification**: Clients can fetch attestations alongside packages and verify them before installation. | ||||||||||||||||||
|
|
||||||||||||||||||
| 2. **Supports multiple attestations**: A single package may have multiple attestations (e.g., from the build system, from the channel on upload, from third-party auditors). | ||||||||||||||||||
|
|
||||||||||||||||||
| 3. **Works with existing infrastructure**: The sidecar file approach integrates naturally with static file hosting, CDNs, and mirrors. | ||||||||||||||||||
|
|
||||||||||||||||||
| 4. **Follows ecosystem conventions**: Similar approaches are used by PyPI ([Integrity API][PyPI Integrity]), npm ([provenance attestations][npm provenance]), and ([RubyGems][rubygems release-gem]). | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Specification | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Endpoint Definition | ||||||||||||||||||
|
|
||||||||||||||||||
| For any conda package artifact at URL: | ||||||||||||||||||
|
|
||||||||||||||||||
| ``` | ||||||||||||||||||
| <channel_url>/<subdir>/<artifact_filename> | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| Attestations MUST be available at: | ||||||||||||||||||
|
|
||||||||||||||||||
| ``` | ||||||||||||||||||
| <channel_url>/<subdir>/<artifact_filename>.sigs | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Examples | ||||||||||||||||||
|
|
||||||||||||||||||
| | Package URL | Attestation URL | | ||||||||||||||||||
| | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | ||||||||||||||||||
| | `https://conda.anaconda.org/conda-forge/linux-64/numpy-2.0.0-py312h1234567_0.conda` | `https://conda.anaconda.org/conda-forge/linux-64/numpy-2.0.0-py312h1234567_0.conda.sigs` | | ||||||||||||||||||
| | `https://prefix.dev/my-channel/noarch/my-package-1.0.0-pyhd8ed1ab_0.conda` | `https://prefix.dev/my-channel/noarch/my-package-1.0.0-pyhd8ed1ab_0.conda.sigs` | | ||||||||||||||||||
| | `https://example.com/channel/win-64/pkg-1.0-0.tar.bz2` | `https://example.com/channel/win-64/pkg-1.0-0.tar.bz2.sigs` | | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Response Format | ||||||||||||||||||
|
|
||||||||||||||||||
| The `.sigs` endpoint MUST return a JSON array containing zero or more [Sigstore bundles][Sigstore Bundle]. Each bundle represents one attestation for the package. | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Content-Type | ||||||||||||||||||
|
|
||||||||||||||||||
| The response MUST have `Content-Type: application/json`. | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Schema | ||||||||||||||||||
|
|
||||||||||||||||||
| ```json | ||||||||||||||||||
| [ | ||||||||||||||||||
| <Sigstore Bundle>, | ||||||||||||||||||
| <Sigstore Bundle>, | ||||||||||||||||||
| ... | ||||||||||||||||||
| ] | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| Each element in the array MUST be a valid [Sigstore Bundle] as defined by the Sigstore specification. | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Empty Response | ||||||||||||||||||
|
|
||||||||||||||||||
| If no attestations exist for a package, the endpoint MUST return an empty JSON array: | ||||||||||||||||||
|
|
||||||||||||||||||
| ```json | ||||||||||||||||||
| [] | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Example Response | ||||||||||||||||||
|
|
||||||||||||||||||
| The following is an abbreviated example of a `.sigs` response containing a single attestation: | ||||||||||||||||||
|
|
||||||||||||||||||
| ```json | ||||||||||||||||||
| [ | ||||||||||||||||||
| { | ||||||||||||||||||
| "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", | ||||||||||||||||||
| "dsseEnvelope": { | ||||||||||||||||||
| "payload": "<base64-encoded in-toto statement>", | ||||||||||||||||||
| "payloadType": "application/vnd.in-toto+json", | ||||||||||||||||||
| "signatures": [ | ||||||||||||||||||
| { | ||||||||||||||||||
| "keyid": "", | ||||||||||||||||||
| "sig": "<base64-encoded signature>" | ||||||||||||||||||
| } | ||||||||||||||||||
| ] | ||||||||||||||||||
| }, | ||||||||||||||||||
| "verificationMaterial": { | ||||||||||||||||||
| "certificate": { | ||||||||||||||||||
| "rawBytes": "<base64-encoded Fulcio certificate>" | ||||||||||||||||||
| }, | ||||||||||||||||||
| "tlogEntries": [ | ||||||||||||||||||
| { | ||||||||||||||||||
| "logIndex": "168604147", | ||||||||||||||||||
| "logId": { | ||||||||||||||||||
| "keyId": "<base64-encoded log ID>" | ||||||||||||||||||
| }, | ||||||||||||||||||
| "kindVersion": { | ||||||||||||||||||
| "kind": "dsse", | ||||||||||||||||||
| "version": "0.0.1" | ||||||||||||||||||
| }, | ||||||||||||||||||
| "integratedTime": "1738678814", | ||||||||||||||||||
| "inclusionPromise": { | ||||||||||||||||||
| "signedEntryTimestamp": "<base64-encoded SET>" | ||||||||||||||||||
| }, | ||||||||||||||||||
| "inclusionProof": { | ||||||||||||||||||
| "logIndex": "46699885", | ||||||||||||||||||
| "rootHash": "<base64-encoded root hash>", | ||||||||||||||||||
| "treeSize": "46699887", | ||||||||||||||||||
| "hashes": ["<base64-encoded hashes>"], | ||||||||||||||||||
| "checkpoint": { | ||||||||||||||||||
| "envelope": "<signed checkpoint>" | ||||||||||||||||||
| } | ||||||||||||||||||
| }, | ||||||||||||||||||
| "canonicalizedBody": "<base64-encoded canonical body>" | ||||||||||||||||||
| } | ||||||||||||||||||
| ] | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| ] | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| ### HTTP Status Codes | ||||||||||||||||||
|
|
||||||||||||||||||
| | Status Code | Meaning | | ||||||||||||||||||
| | --------------- | ------------------------------------------------------------ | | ||||||||||||||||||
| | `200 OK` | Attestations returned successfully (may be empty array) | | ||||||||||||||||||
|
|
||||||||||||||||||
| Channels that support attestations MUST always return `200 OK` with an empty array `[]`, even when the package does not exist. | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My previous comment was about the ambiguity of defining a 404 error on the I think we should return 404 when the package does not exist, but define the meaning of 404 as:
and make it clear that clients should not use the |
||||||||||||||||||
|
|
||||||||||||||||||
| ### Repodata changes | ||||||||||||||||||
|
|
||||||||||||||||||
| The repodata index is changed to include a new `attestations` field that MUST contain the SHA256 hash of the signatures file. | ||||||||||||||||||
|
|
||||||||||||||||||
| ```json | ||||||||||||||||||
| { | ||||||||||||||||||
| "name": "foobar", | ||||||||||||||||||
| "version": "1.2.3", | ||||||||||||||||||
| "attestations": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" | ||||||||||||||||||
| } | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming this is because of the discussion here: https://github.com/conda/ceps/pull/142/changes#r2816839339 It might be worth a sentence here to suggest the rationale:
Suggested change
(GitHub suggestions can't include the close-block, so I had to escape it. 😢 )
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||||||||||||||||||
|
|
||||||||||||||||||
| ### Attestation Requirements | ||||||||||||||||||
|
|
||||||||||||||||||
| Each attestation in the response MUST comply with [CEP 27]. Specifically: | ||||||||||||||||||
|
|
||||||||||||||||||
| 1. The in-toto statement's `subject[0].name` MUST match the artifact filename. | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for this is that having one attestation per artifact makes things simpler from a verification POV, and also prevents certain footguns (more context here) |
||||||||||||||||||
|
|
||||||||||||||||||
| 2. The in-toto statement's `subject[0].digest.sha256` MUST match the SHA256 hash of the artifact. | ||||||||||||||||||
|
|
||||||||||||||||||
| 3. The `predicateType` MUST be `https://schemas.conda.org/attestations-publish-1.schema.json` or another registered predicate type. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Multiple Attestations | ||||||||||||||||||
|
|
||||||||||||||||||
| A package MAY have multiple attestations from different sources. Common scenarios include: | ||||||||||||||||||
|
|
||||||||||||||||||
| | Source | Purpose | | ||||||||||||||||||
| | ----------------------------------- | ------------------------------------------------------ | | ||||||||||||||||||
| | Build system (e.g., GitHub Actions) | Proves the package was built from specific source code | | ||||||||||||||||||
| | Channel operator | Proves the channel accepted and published the package | | ||||||||||||||||||
| | Third-party auditor | Proves the package passed security review | | ||||||||||||||||||
|
|
||||||||||||||||||
| When multiple attestations are present, they MUST all refer to the same artifact (same filename and SHA256 hash). Clients MAY choose which attestations to verify based on their trust policy. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Mirror Behavior | ||||||||||||||||||
|
|
||||||||||||||||||
| Mirrors and proxies SHOULD: | ||||||||||||||||||
|
|
||||||||||||||||||
| 1. Fetch and cache `.sigs` files alongside packages | ||||||||||||||||||
| 2. Serve cached attestations without modification | ||||||||||||||||||
|
wolfv marked this conversation as resolved.
Outdated
|
||||||||||||||||||
| 3. Return `404` if the upstream `.sigs` endpoint returns `404` | ||||||||||||||||||
|
|
||||||||||||||||||
| TODO: specify further the desired behavior of mirrors. | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Client Behavior | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Verification Workflow | ||||||||||||||||||
|
|
||||||||||||||||||
| Clients implementing attestation verification SHOULD follow this workflow: | ||||||||||||||||||
|
|
||||||||||||||||||
| 1. **Download package** from the channel | ||||||||||||||||||
| 2. **Fetch attestations** from `<package_url>.sigs` | ||||||||||||||||||
| 3. **Verify each attestation** against the configuration. | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make explicit that the verification process is defined in CEP-27 |
||||||||||||||||||
| 4. **Accept or reject** the package based on verification results | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Configuration | ||||||||||||||||||
|
|
||||||||||||||||||
| Clients SHOULD support the following configuration options: | ||||||||||||||||||
|
|
||||||||||||||||||
| ```yaml | ||||||||||||||||||
| # Example ~/.condarc configuration | ||||||||||||||||||
| attestations: | ||||||||||||||||||
| conda-forge: | ||||||||||||||||||
| enabled: true | ||||||||||||||||||
| require: warn # "error", "warn", or "ignore" | ||||||||||||||||||
| trusted_identities: | ||||||||||||||||||
| - "https://github.com/conda-forge/*" | ||||||||||||||||||
| - "https://github.com/my-org/*" | ||||||||||||||||||
| https://prefix.dev/foobar: | ||||||||||||||||||
| enabled: true | ||||||||||||||||||
| trusted_identities: | ||||||||||||||||||
| - "https://github.com/foobar" | ||||||||||||||||||
| ``` | ||||||||||||||||||
|
|
||||||||||||||||||
| | Setting | Values | Behavior | | ||||||||||||||||||
| | -------------------- | ---------------- | --------------------------------------------------------------- | | ||||||||||||||||||
| | `enabled` | `true`/`false` | Enable or disable attestation fetching and verification | | ||||||||||||||||||
| | `require` | `error` | Fail if attestations are missing or invalid | | ||||||||||||||||||
| | | `warn` | Log warning but continue if attestations are missing or invalid | | ||||||||||||||||||
| | | `ignore` | Silently continue (still verify if attestations exist) | | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a default value for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I vote for making the 3 fields mandatory, with no default values |
||||||||||||||||||
| | `trusted_identities` | List of patterns | Only accept attestations from matching Sigstore identities | | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Offline and Air-gapped Environments | ||||||||||||||||||
|
|
||||||||||||||||||
| For offline verification, clients MAY cache `.sigs` files alongside packages in local repositories. | ||||||||||||||||||
| The Sigstore bundle format is self-contained and supports offline verification once the Sigstore trust root is available locally. | ||||||||||||||||||
|
wolfv marked this conversation as resolved.
Outdated
|
||||||||||||||||||
|
|
||||||||||||||||||
| Note: clients MUST periodically update the sigstore trust root to ensure no keys were revoked. | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the reason is more general, it's to ensure the client doesn't miss any trust root changes (which could be key revocation, but also new keys added) |
||||||||||||||||||
|
|
||||||||||||||||||
| ## References | ||||||||||||||||||
|
|
||||||||||||||||||
| - [CEP 27 - Standardizing a publish attestation for the conda ecosystem][CEP 27] | ||||||||||||||||||
| - [Sigstore Bundle Specification][Sigstore Bundle] | ||||||||||||||||||
| - [in-toto Attestation Framework][in-toto] | ||||||||||||||||||
| - [PyPI Integrity API][PyPI Integrity] | ||||||||||||||||||
| - [npm Provenance Statements][npm provenance] | ||||||||||||||||||
| - [PEP 740 - Index support for digital attestations][PEP 740] | ||||||||||||||||||
|
|
||||||||||||||||||
| ## Copyright | ||||||||||||||||||
|
|
||||||||||||||||||
| All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). | ||||||||||||||||||
|
|
||||||||||||||||||
| [RFC2119]: https://www.ietf.org/rfc/rfc2119.txt | ||||||||||||||||||
| [Sigstore]: https://sigstore.dev | ||||||||||||||||||
| [Sigstore Bundle]: https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto | ||||||||||||||||||
| [in-toto]: https://in-toto.io | ||||||||||||||||||
| [CEP 27]: ./cep-0027.md | ||||||||||||||||||
| [PyPI Integrity]: https://docs.pypi.org/api/integrity/ | ||||||||||||||||||
| [npm provenance]: https://docs.npmjs.com/generating-provenance-statements | ||||||||||||||||||
| [PEP 740]: https://peps.python.org/pep-0740/ | ||||||||||||||||||
| [rubygems]: https://github.com/rubygems/release-gem | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this CEP describes the endpoint as "RESTful", it seems like it may be a read-only endpoint?
If you want to support additional attestations from third-parties, you'll need to figure out access control for users "vouching" for other packages. That seems like a diversion from the main goal of this CEP, so it might be worth calling out third-party auditors as a future capability that you don't want to prevent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are not in contradiction, right?
Not necessarily? A third party could generate attestations, and a channel admin could upload them to their respective packages, without giving access to the third party. This section is characterizing the distribution mechanism, the "attestations from third-party auditors" is just an example of a type of attestation that may exist.