From 6f4b35e05a055e80bb4a56fbbd247c193f98e8a9 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 10 Dec 2025 17:11:36 +0100 Subject: [PATCH 01/11] add CEP about serving attestations --- cep-xxxx-serving-attestations.md | 294 +++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 cep-xxxx-serving-attestations.md diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md new file mode 100644 index 00000000..f8710bf6 --- /dev/null +++ b/cep-xxxx-serving-attestations.md @@ -0,0 +1,294 @@ +# CEP XXXX - Distribution of Sigstore Attestations for Conda Packages + + + + + + + + + + +
Title Distribution of Sigstore Attestations for Conda Packages
Status Draft
Author(s) Wolf Vollprecht <wolf@prefix.dev>
Created Dec 02, 2025
Updated Dec 02, 2025
Discussion NA
Implementation https://prefix.dev (preview implementation)
Requires CEP 27 (Publish Attestation)
+ +> 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. + +## Specification + +### Endpoint Definition + +For any conda package artifact at URL: + +``` +// +``` + +Attestations MUST be available at: + +``` +//.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 +[ + , + , + ... +] +``` + +Each element in the array MUST be a valid [Sigstore Bundle] as defined by the Sigstore specification. The bundle format supports multiple versions; implementations SHOULD support at least bundle versions v0.2 and v0.3. + +#### 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": "", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "", + "sig": "" + } + ] + }, + "verificationMaterial": { + "certificate": { + "rawBytes": "" + }, + "tlogEntries": [ + { + "logIndex": "168604147", + "logId": { + "keyId": "" + }, + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "integratedTime": "1738678814", + "inclusionPromise": { + "signedEntryTimestamp": "" + }, + "inclusionProof": { + "logIndex": "46699885", + "rootHash": "", + "treeSize": "46699887", + "hashes": [""], + "checkpoint": { + "envelope": "" + } + }, + "canonicalizedBody": "" + } + ] + } + } +] +``` + +### HTTP Status Codes + +| Status Code | Meaning | +|-------------|---------| +| `200 OK` | Attestations returned successfully (may be empty array) | +| `404 Not Found` | The package does not exist (distinct from "no attestations") | + +Channels MUST return `200 OK` with an empty array `[]` when a package exists but has no attestations. Channels MUST return `404 Not Found` only when the underlying package does not exist. + +This distinction allows clients to differentiate between: +- "This package has no attestations" (expected during transition period) +- "This package does not exist" (client error or tampering) + +### 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. + +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 +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 (or use the SHA256 sum from the repodata.json) +2. **Fetch attestations** from `.sigs` +3. **Verify each attestation** according to the client's trust policy: + - Verify the Sigstore bundle signature + - Verify the certificate chain to Fulcio root + - Verify the transparency log inclusion proof + - Verify the in-toto subject matches the downloaded package +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) | +| `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. + +## Security Considerations + +### Trust Model + +The security of this scheme depends on: + +1. **Sigstore infrastructure**: Fulcio CA, Rekor transparency log, and their availability +2. **Identity binding**: OIDC providers correctly authenticating signing identities +3. **Client trust policy**: Correctly configured trusted identities +4. **TLS security**: Secure transport when fetching attestations + +### Threat Mitigations + +| Threat | Mitigation | +|--------|------------| +| Forged attestations | Sigstore signatures are cryptographically verified against Fulcio certificates | +| Tampered attestations | Rekor transparency log provides tamper evidence | +| Replay attacks | In-toto subject binds attestation to specific artifact hash | +| Removed attestations | Rekor log entries are permanent; monitors can detect removal | +| Compromised signing identity | Transparency log enables detection; trust policy limits blast radius | + +### Limitations + +This scheme does NOT protect against: + +1. Legitimate signers publishing malicious packages +2. Compromise of the Sigstore infrastructure itself +3. Incorrect client trust policies +4. Attacks before attestation was added to the package + +## Backwards Compatibility + +This proposal is fully backwards compatible: + +1. **Existing channels**: No changes required; clients will receive `404` for `.sigs` endpoints +2. **Existing clients**: Will not request `.sigs` endpoints; behavior unchanged + +The `.sigs` extension was chosen to avoid conflicts with existing URL patterns and file extensions in the conda ecosystem. + +## 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/ \ No newline at end of file From ecc6b318f9eb95aaa9835b49d8d61e074da77a55 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 10 Dec 2025 17:19:02 +0100 Subject: [PATCH 02/11] fix lint --- cep-xxxx-serving-attestations.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md index f8710bf6..d3f60311 100644 --- a/cep-xxxx-serving-attestations.md +++ b/cep-xxxx-serving-attestations.md @@ -150,6 +150,7 @@ The following is an abbreviated example of a `.sigs` response containing a singl Channels MUST return `200 OK` with an empty array `[]` when a package exists but has no attestations. Channels MUST return `404 Not Found` only when the underlying package does not exist. This distinction allows clients to differentiate between: + - "This package has no attestations" (expected during transition period) - "This package does not exist" (client error or tampering) @@ -291,4 +292,4 @@ All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdo [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/ \ No newline at end of file +[PEP 740]: https://peps.python.org/pep-0740/ From b360b431858e63190d595421a97005005fc0b9f2 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Tue, 17 Feb 2026 16:06:58 +0100 Subject: [PATCH 03/11] update CEP --- cep-xxxx-serving-attestations.md | 95 ++++++++++++-------------------- 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md index d3f60311..911f33cf 100644 --- a/cep-xxxx-serving-attestations.md +++ b/cep-xxxx-serving-attestations.md @@ -33,7 +33,7 @@ Without a standardized distribution mechanism, clients cannot reliably discover 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. +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 @@ -53,11 +53,11 @@ Attestations MUST be available at: #### Examples -| Package URL | Attestation URL | -|-------------|-----------------| +| 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` | +| `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 @@ -77,7 +77,7 @@ The response MUST have `Content-Type: application/json`. ] ``` -Each element in the array MUST be a valid [Sigstore Bundle] as defined by the Sigstore specification. The bundle format supports multiple versions; implementations SHOULD support at least bundle versions v0.2 and v0.3. +Each element in the array MUST be a valid [Sigstore Bundle] as defined by the Sigstore specification. #### Empty Response @@ -142,17 +142,23 @@ The following is an abbreviated example of a `.sigs` response containing a singl ### HTTP Status Codes -| Status Code | Meaning | -|-------------|---------| -| `200 OK` | Attestations returned successfully (may be empty array) | -| `404 Not Found` | The package does not exist (distinct from "no attestations") | +| Status Code | Meaning | +| --------------- | ------------------------------------------------------------ | +| `200 OK` | Attestations returned successfully (may be empty array) | -Channels MUST return `200 OK` with an empty array `[]` when a package exists but has no attestations. Channels MUST return `404 Not Found` only when the underlying package does not exist. +Channels that support attestations MUST always return `200 OK` with an empty array `[]`, even when the package does not exist. -This distinction allows clients to differentiate between: +### Repodata changes -- "This package has no attestations" (expected during transition period) -- "This package does not exist" (client error or tampering) +The repodata index is changed to include a new `attestations` field that MUST contain the SHA256 hash of the signatures file. + +``` +{ + "name": "foobar", + "version": "1.2.3", + "attestations": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" +} +``` ### Attestation Requirements @@ -168,11 +174,11 @@ Each attestation in the response MUST comply with [CEP 27]. Specifically: A package MAY have multiple attestations from different sources. Common scenarios include: -| Source | Purpose | -|--------|---------| +| 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 | +| 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. @@ -192,13 +198,9 @@ TODO: specify further the desired behavior of mirrors. Clients implementing attestation verification SHOULD follow this workflow: -1. **Download package** from the channel (or use the SHA256 sum from the repodata.json) +1. **Download package** from the channel 2. **Fetch attestations** from `.sigs` -3. **Verify each attestation** according to the client's trust policy: - - Verify the Sigstore bundle signature - - Verify the certificate chain to Fulcio root - - Verify the transparency log inclusion proof - - Verify the in-toto subject matches the downloaded package +3. **Verify each attestation** against the configuration. 4. **Accept or reject** the package based on verification results ### Configuration @@ -220,48 +222,20 @@ attestations: - "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) | -| `trusted_identities` | List of patterns | Only accept attestations from matching Sigstore identities | +| 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) | +| `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. -## Security Considerations - -### Trust Model - -The security of this scheme depends on: - -1. **Sigstore infrastructure**: Fulcio CA, Rekor transparency log, and their availability -2. **Identity binding**: OIDC providers correctly authenticating signing identities -3. **Client trust policy**: Correctly configured trusted identities -4. **TLS security**: Secure transport when fetching attestations - -### Threat Mitigations - -| Threat | Mitigation | -|--------|------------| -| Forged attestations | Sigstore signatures are cryptographically verified against Fulcio certificates | -| Tampered attestations | Rekor transparency log provides tamper evidence | -| Replay attacks | In-toto subject binds attestation to specific artifact hash | -| Removed attestations | Rekor log entries are permanent; monitors can detect removal | -| Compromised signing identity | Transparency log enables detection; trust policy limits blast radius | - -### Limitations - -This scheme does NOT protect against: - -1. Legitimate signers publishing malicious packages -2. Compromise of the Sigstore infrastructure itself -3. Incorrect client trust policies -4. Attacks before attestation was added to the package +Note: clients MUST periodically update the sigstore trust root to guarantee security. ## Backwards Compatibility @@ -293,3 +267,4 @@ All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdo [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 \ No newline at end of file From 6b0904f7e29270350326c279f0415e6f8fedef64 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Tue, 17 Feb 2026 16:25:24 +0100 Subject: [PATCH 04/11] add json --- cep-xxxx-serving-attestations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md index 911f33cf..b5f383be 100644 --- a/cep-xxxx-serving-attestations.md +++ b/cep-xxxx-serving-attestations.md @@ -152,7 +152,7 @@ Channels that support attestations MUST always return `200 OK` with an empty arr 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", From b93d8f50fb320a0ba17a7e3795c13b756d0b065a Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 25 Mar 2026 15:03:39 +0100 Subject: [PATCH 05/11] fix --- cep-xxxx-serving-attestations.md | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md index b5f383be..a3693edc 100644 --- a/cep-xxxx-serving-attestations.md +++ b/cep-xxxx-serving-attestations.md @@ -232,19 +232,10 @@ attestations: ### Offline and Air-gapped Environments -For offline verification, clients MAY cache `.sigs` files alongside packages in local repositories +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. -Note: clients MUST periodically update the sigstore trust root to guarantee security. - -## Backwards Compatibility - -This proposal is fully backwards compatible: - -1. **Existing channels**: No changes required; clients will receive `404` for `.sigs` endpoints -2. **Existing clients**: Will not request `.sigs` endpoints; behavior unchanged - -The `.sigs` extension was chosen to avoid conflicts with existing URL patterns and file extensions in the conda ecosystem. +Note: clients MUST periodically update the sigstore trust root to ensure no keys were revoked. ## References From 889062f589cf0d2cc14150c58ec759411529fc21 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 13 May 2026 11:20:51 +0200 Subject: [PATCH 06/11] improvements --- cep-xxxx-serving-attestations.md | 54 +++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md index a3693edc..99732249 100644 --- a/cep-xxxx-serving-attestations.md +++ b/cep-xxxx-serving-attestations.md @@ -25,11 +25,11 @@ This CEP defines a standard endpoint for distributing [Sigstore] attestations al > "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: +Without a standardized distribution mechanism, clients cannot reliably discover and retrieve attestations. This CEP addresses that gap by defining a simple, read-only HTTP sidecar 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). +2. **Supports multiple attestations**: A single package may have multiple attestations, such as attestations produced during build, during channel upload, or by other review processes. 3. **Works with existing infrastructure**: The sidecar file approach integrates naturally with static file hosting, CDNs, and mirrors. @@ -45,7 +45,7 @@ For any conda package artifact at URL: // ``` -Attestations MUST be available at: +For channels implementing this CEP, attestations MUST be available at: ``` //.sigs @@ -61,7 +61,7 @@ Attestations MUST be available at: ### 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. +For channels implementing this CEP, 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 @@ -142,11 +142,14 @@ The following is an abbreviated example of a `.sigs` response containing a singl ### HTTP Status Codes -| Status Code | Meaning | -| --------------- | ------------------------------------------------------------ | -| `200 OK` | Attestations returned successfully (may be empty array) | +| Status Code | Meaning | +| --------------- | ----------------------------------------------------------------------------------------------- | +| `200 OK` | The package exists and attestations were returned successfully (the array may be empty) | +| `404 Not Found` | For channels implementing this CEP, the package does not exist | -Channels that support attestations MUST always return `200 OK` with an empty array `[]`, even when the package does not exist. +Channels that support attestations MUST return `200 OK` with an empty array `[]` when the package exists but no attestations are available for it. + +For backwards compatibility, clients MUST NOT use a `404 Not Found` response from the `.sigs` endpoint alone to determine whether a package exists. Channels that do not implement this CEP may return `404 Not Found` for every `.sigs` URL, even when the underlying package exists. Clients that need to determine package existence MUST use the channel's package metadata or the package artifact URL itself. ### Repodata changes @@ -160,27 +163,35 @@ The repodata index is changed to include a new `attestations` field that MUST co } ``` +This hash allows mirrors and clients to detect changes to the `.sigs` sidecar, including attestations added after the package was first published, and re-fetch the sidecar when it changes. + ### Attestation Requirements -Each attestation in the response MUST comply with [CEP 27]. Specifically: +This CEP defines discovery and distribution of attestations. Verification of publish attestations MUST follow [CEP 27]. + +Each element in the response MUST be a valid [Sigstore Bundle]. If a bundle contains a [CEP 27] publish attestation, then: 1. The in-toto statement's `subject[0].name` MUST match the artifact filename. 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. +3. The `predicateType` MUST be `https://schemas.conda.org/attestations-publish-1.schema.json`. + +CEP 27 publish attestations intentionally describe a single package artifact. Other predicate types MAY appear in the same `.sigs` response, but this CEP does not define their verification rules. Clients MUST verify each recognized predicate type according to its own specification and MAY ignore or reject unrecognized predicate types according to local policy. ### Multiple Attestations -A package MAY have multiple attestations from different sources. Common scenarios include: +A package MAY have multiple attestations from different sources. Examples of attestation producers that this distribution format can support include: | Source | Purpose | | ----------------------------------- | ------------------------------------------------------ | -| Build system (e.g., GitHub Actions) | Proves the package was built from specific source code | +| 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. +When multiple attestations are present, each attestation intended to apply to the package artifact MUST identify that artifact according to the verification rules for its predicate type. Clients MAY choose which attestations to verify based on their trust policy. + +This CEP does not define upload authorization, channel admission policy, or access control for adding attestations. For example, a third party may produce an attestation, but the channel decides whether and how that attestation is accepted for distribution. ### Mirror Behavior @@ -188,9 +199,11 @@ Mirrors and proxies SHOULD: 1. Fetch and cache `.sigs` files alongside packages 2. Serve cached attestations without modification -3. Return `404` if the upstream `.sigs` endpoint returns `404` +3. Use the repodata `attestations` hash to detect changed `.sigs` sidecars +4. Re-fetch a cached `.sigs` sidecar when the repodata `attestations` hash changes +5. Preserve upstream `404 Not Found` responses for `.sigs` URLs when the upstream channel does not provide a sidecar -TODO: specify further the desired behavior of mirrors. +Mirrors MUST NOT infer that a package does not exist solely because the upstream `.sigs` endpoint returns `404 Not Found`. ## Client Behavior @@ -200,7 +213,7 @@ Clients implementing attestation verification SHOULD follow this workflow: 1. **Download package** from the channel 2. **Fetch attestations** from `.sigs` -3. **Verify each attestation** against the configuration. +3. **Verify each attestation** using the verification process defined by [CEP 27] or by the attestation's registered predicate type. 4. **Accept or reject** the package based on verification results ### Configuration @@ -218,13 +231,16 @@ attestations: - "https://github.com/my-org/*" https://prefix.dev/foobar: enabled: true + require: warn trusted_identities: - "https://github.com/foobar" ``` +Each channel entry MUST specify `enabled`, `require`, and `trusted_identities`; clients MUST NOT infer default values for omitted fields. + | Setting | Values | Behavior | | -------------------- | ---------------- | --------------------------------------------------------------- | -| `enabled` | `true`/`false` | Enable or disable attestation fetching and verification | +| `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) | @@ -235,7 +251,7 @@ attestations: 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. -Note: clients MUST periodically update the sigstore trust root to ensure no keys were revoked. +Clients MUST periodically update the Sigstore trust root so they do not miss trust-root changes, including key revocations and newly trusted keys. ## References @@ -258,4 +274,4 @@ All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdo [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 \ No newline at end of file +[rubygems]: https://github.com/rubygems/release-gem From 613e1d3855cb10dc631ff8c5fecf71db231bfb12 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 13 May 2026 13:07:21 +0200 Subject: [PATCH 07/11] shrink content to relevant bits only --- cep-xxxx-serving-attestations.md | 63 +------------------------------- 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md index 99732249..bfdc5b49 100644 --- a/cep-xxxx-serving-attestations.md +++ b/cep-xxxx-serving-attestations.md @@ -87,59 +87,6 @@ If no attestations exist for a package, the endpoint MUST return an empty JSON a [] ``` -#### 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": "", - "payloadType": "application/vnd.in-toto+json", - "signatures": [ - { - "keyid": "", - "sig": "" - } - ] - }, - "verificationMaterial": { - "certificate": { - "rawBytes": "" - }, - "tlogEntries": [ - { - "logIndex": "168604147", - "logId": { - "keyId": "" - }, - "kindVersion": { - "kind": "dsse", - "version": "0.0.1" - }, - "integratedTime": "1738678814", - "inclusionPromise": { - "signedEntryTimestamp": "" - }, - "inclusionProof": { - "logIndex": "46699885", - "rootHash": "", - "treeSize": "46699887", - "hashes": [""], - "checkpoint": { - "envelope": "" - } - }, - "canonicalizedBody": "" - } - ] - } - } -] -``` - ### HTTP Status Codes | Status Code | Meaning | @@ -181,15 +128,7 @@ CEP 27 publish attestations intentionally describe a single package artifact. Ot ### Multiple Attestations -A package MAY have multiple attestations from different sources. Examples of attestation producers that this distribution format can support 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, each attestation intended to apply to the package artifact MUST identify that artifact according to the verification rules for its predicate type. Clients MAY choose which attestations to verify based on their trust policy. +A package MAY have multiple attestations, provided each attestation intended to apply to the package artifact identifies that artifact according to the verification rules for its predicate type. Clients MAY choose which attestations to verify based on their trust policy. This CEP does not define upload authorization, channel admission policy, or access control for adding attestations. For example, a third party may produce an attestation, but the channel decides whether and how that attestation is accepted for distribution. From 1a16ffd684d68d586823ff53ec0e1724e96b0fd6 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 13 May 2026 13:43:47 +0200 Subject: [PATCH 08/11] fix precommit --- cep-xxxx-serving-attestations.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md index bfdc5b49..11204a6d 100644 --- a/cep-xxxx-serving-attestations.md +++ b/cep-xxxx-serving-attestations.md @@ -33,7 +33,7 @@ Without a standardized distribution mechanism, clients cannot reliably discover 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]). +4. **Follows ecosystem conventions**: Similar approaches are used by PyPI ([Integrity API][PyPI Integrity]), npm ([provenance attestations][npm provenance]), and ([RubyGems][rubygems]). ## Specification @@ -41,13 +41,13 @@ Without a standardized distribution mechanism, clients cannot reliably discover For any conda package artifact at URL: -``` +```text // ``` For channels implementing this CEP, attestations MUST be available at: -``` +```text //.sigs ``` @@ -56,7 +56,7 @@ For channels implementing this CEP, attestations MUST be available at: | 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://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 @@ -91,7 +91,7 @@ If no attestations exist for a package, the endpoint MUST return an empty JSON a | Status Code | Meaning | | --------------- | ----------------------------------------------------------------------------------------------- | -| `200 OK` | The package exists and attestations were returned successfully (the array may be empty) | +| `200 OK` | The package exists and attestations were returned successfully (the array may be empty) | | `404 Not Found` | For channels implementing this CEP, the package does not exist | Channels that support attestations MUST return `200 OK` with an empty array `[]` when the package exists but no attestations are available for it. @@ -179,7 +179,7 @@ Each channel entry MUST specify `enabled`, `require`, and `trusted_identities`; | Setting | Values | Behavior | | -------------------- | ---------------- | --------------------------------------------------------------- | -| `enabled` | `true`/`false` | Enable or disable attestation fetching and verification | +| `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) | From 851d0506b0b0b237fc0a928a437ce5a151f4ec69 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Mon, 6 Jul 2026 10:30:39 +0200 Subject: [PATCH 09/11] tie down attestations through repodata attestations key with sha + size --- cep-XXXX.md | 262 +++++++++++++++++++++++++++++++ cep-xxxx-serving-attestations.md | 216 ------------------------- 2 files changed, 262 insertions(+), 216 deletions(-) create mode 100644 cep-XXXX.md delete mode 100644 cep-xxxx-serving-attestations.md diff --git a/cep-XXXX.md b/cep-XXXX.md new file mode 100644 index 00000000..11ad921d --- /dev/null +++ b/cep-XXXX.md @@ -0,0 +1,262 @@ +# CEP XXXX - Distribution of Sigstore Attestations for Conda Packages + + + + + + + + + + +
Title Distribution of Sigstore Attestations for Conda Packages
Status Draft
Author(s) Wolf Vollprecht <wolf@prefix.dev>
Created Dec 02, 2025
Updated Jul 05, 2026
Discussion https://github.com/conda/ceps/pull/142
Implementation https://prefix.dev (preview implementation)
Requires CEP 27 (Publish Attestation)
+ +> 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. +> +> More specifically, violations of a MUST or MUST NOT rule MUST result in an error. Violations of the + rules specified by any of the other all-capital terms MAY result in a warning, at discretion of the + implementation. + +## 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, read-only HTTP sidecar 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. For example, future workflows could attach attestations produced during build, during channel upload, or by third-party review processes. + +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], standardized in [PEP 740]), npm ([provenance attestations][npm provenance]), and RubyGems ([release-gem][rubygems]). + +## Specification + +### Endpoint Definition + +For any conda package artifact at URL: + +```text +// +``` + +if the package has one or more attestations, they MUST be available at: + +```text +//.sigs +``` + +Whether a package has attestations is advertised in the repodata index (see [Repodata changes](#repodata-changes)). Clients discover sidecars through repodata rather than by probing `.sigs` URLs; packages without attestations need not have a sidecar file at all. + +#### 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` file MUST contain a JSON array of one or more [Sigstore bundles][Sigstore Bundle]. Each bundle represents one attestation for the package. + +The sidecar is a static artifact: channels MUST serve it byte-for-byte identical to the file whose SHA256 hash is published in the repodata `attestations` field (see [Repodata changes](#repodata-changes)). Channels MUST NOT re-serialize the JSON when serving it. This makes the sidecar safe to host on static infrastructure (e.g. object storage or CDNs) and safe to cache and mirror by content hash. + +#### Content-Type + +The response SHOULD have `Content-Type: application/json`. Clients MUST NOT reject a sidecar based on the `Content-Type` header, since static file hosts do not always allow configuring it. + +#### Schema + +```json +[ + , + , + ... +] +``` + +Each element in the array MUST be a valid [Sigstore Bundle] as defined by the Sigstore specification. + +### HTTP Status Codes + +| Status Code | Meaning | +| --------------- | ---------------------------------------- | +| `200 OK` | The sidecar file exists and was returned | +| `404 Not Found` | No sidecar file exists at this URL | + +Attestation discovery goes through repodata, so clients MUST NOT infer anything about the existence of the package itself from a `.sigs` response. In particular, channels that do not implement this CEP may return `404 Not Found` for every `.sigs` URL, even when the underlying package exists. + +For a package whose repodata record carries an `attestations` field, any failure to retrieve a sidecar matching the advertised hash — including a `404 Not Found` — is a retrieval failure and MUST be handled according to the client's `require` policy (see [Configuration](#configuration)). + +### Repodata changes + +A package record in the repodata index (in `packages` or `packages.conda`, or the equivalent per-package record in sharded repodata per [CEP 16]) gains a new OPTIONAL `attestations` field: + +```json +{ + "name": "foobar", + "version": "1.2.3", + "attestations": { + "sha256": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570", + "size": 4842 + } +} +``` + +- `sha256` (REQUIRED): the SHA256 hash of the exact bytes of the `.sigs` file as served. +- `size` (REQUIRED): the size of the `.sigs` file in bytes. + +The field MUST be present if and only if at least one attestation is available for the package. Its absence means the package has no attestations; clients MUST NOT treat an absent field as an error and SHOULD NOT request the `.sigs` URL in that case. + +The field is the single discovery and integrity mechanism for attestation sidecars: + +1. **Discovery**: Clients learn from repodata alone whether a `.sigs` file exists, avoiding a network round-trip for packages without attestations. +2. **Integrity**: Clients MUST verify that the fetched `.sigs` bytes hash to `sha256` before using the sidecar (see [Verification Workflow](#verification-workflow)). This prevents a mirror or intermediary from stripping or replacing attestations without detection. +3. **Change detection**: When attestations are added after a package was first published, the channel publishes an updated `.sigs` file and updates the field. Mirrors and clients re-fetch the sidecar when the hash changes. +4. **Resource bounds**: `size` allows clients to enforce a download limit before fetching. Clients MAY refuse to download sidecars larger than a locally configured limit; such a refusal MUST be handled like a retrieval failure (see [Configuration](#configuration)). + +Tools that post-process repodata (e.g. hotfixing and patching pipelines) MUST preserve the `attestations` field. Channels using sharded repodata ([CEP 16]) update only the affected shard when attestations change, so clients pick up new attestations incrementally; consumers of monolithic `repodata.json` receive the update on the next regeneration. + +### Attestation Requirements + +This CEP defines discovery and distribution of attestations. Verification of publish attestations MUST follow [CEP 27]. + +If a bundle contains a [CEP 27] publish attestation, then: + +1. The in-toto statement's `subject[0].name` MUST match the artifact filename. + +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`. + +CEP 27 publish attestations intentionally describe a single package artifact. Other predicate types MAY appear in the same `.sigs` response, but this CEP does not define their verification rules. When a client verifies a bundle with a recognized predicate type, it MUST apply the verification rules of that predicate type's specification. Clients MAY ignore or reject unrecognized predicate types according to local policy. + +### Multiple Attestations + +A package MAY have multiple attestations, provided each attestation intended to apply to the package artifact identifies that artifact according to the verification rules for its predicate type. Clients MAY choose which attestations to verify based on their trust policy. + +This CEP does not define upload authorization, channel admission policy, or access control for adding attestations. For example, a third party may produce an attestation, but the channel decides whether and how that attestation is accepted for distribution. + +### Mirror Behavior + +Mirrors and proxies treat `.sigs` sidecars like any other channel artifact. They SHOULD: + +1. Mirror the `.sigs` file for every package whose repodata record carries an `attestations` field +2. Serve the file byte-for-byte, without modification +3. Re-fetch a mirrored sidecar when the `attestations.sha256` value in the mirrored repodata changes + +A mirror that modifies sidecar bytes breaks the hash binding to repodata, and clients will treat its responses as retrieval failures. Mirrors MUST NOT infer that a package does not exist solely because the upstream `.sigs` endpoint returns `404 Not Found`. + +## Client Behavior + +### Verification Workflow + +Clients implementing attestation verification SHOULD follow this workflow: + +1. **Check repodata**: If the package's repodata record has no `attestations` field, the package has no attestations. Handle this per the `require` policy for missing attestations and skip the remaining attestation steps. +2. **Download the package** from the channel. +3. **Fetch the sidecar** from `.sigs` and verify that its bytes hash to the `attestations.sha256` value from repodata. On a mismatch or fetch failure, the client SHOULD retry once with refreshed repodata, since the sidecar may have been updated concurrently. A persistent mismatch is a retrieval failure and MUST be handled per the `require` policy. +4. **Verify each attestation** using the verification process defined by [CEP 27] or by the attestation's predicate type (see [Attestation Requirements](#attestation-requirements)). +5. **Accept or reject** the package based on the verification results and the `require` policy. + +### 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 + require: warn + trusted_identities: + - "https://github.com/foobar" +``` + +Each channel entry MUST specify `enabled`, `require`, and `trusted_identities`; clients MUST NOT infer default values for omitted fields. + +| Setting | Values | Behavior | +| -------------------- | ---------------------------- | ---------------------------------------------------------- | +| `enabled` | `true`/`false` | Enable or disable attestation fetching and verification | +| `require` | `error`/`warn`/`ignore` | How to respond to attestation problems (see below) | +| `trusted_identities` | List of patterns | Only accept attestations from matching Sigstore identities | + +#### `require` semantics + +Three classes of attestation problems exist: + +- **Missing**: the package's repodata record has no `attestations` field. +- **Retrieval failure**: the sidecar cannot be fetched, exceeds the client's size limit, or its bytes do not match the repodata `sha256`. +- **Verification failure**: a bundle fails verification for its predicate type, or no attestation matches `trusted_identities`. + +| `require` | Missing | Retrieval failure | Verification failure | +| --------- | ------- | ----------------- | -------------------- | +| `error` | fail | fail | fail | +| `warn` | warn | warn | warn | +| `ignore` | silent | warn | warn | + +"Fail" means the package MUST NOT be installed; "warn" means the client MUST log a warning and MAY continue. `ignore` accepts packages without attestations silently, but retrieval and verification failures are potential tampering signals and MUST NOT be silently swallowed: they are handled as under `warn`. + +#### `trusted_identities` matching + +Each entry is a pattern matched against the certificate identity (the SubjectAlternativeName of the Sigstore signing certificate). Matching is case-sensitive and literal, except that `*` matches any sequence of characters, including `/`. +For example, `https://github.com/conda-forge/*` matches `https://github.com/conda-forge/numpy-feedstock/.github/workflows/build.yml@refs/heads/main` but not `https://github.com/conda-forge-evil/...`, because the literal prefix includes the trailing slash. +A future revision of this CEP MAY extend entries to additionally pin the OIDC issuer. + +### 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. + +Clients SHOULD refresh the Sigstore trust root regularly — for example, on the update cadence of the Sigstore TUF repository — so they do not miss trust-root changes, including key revocations and newly trusted keys. Clients SHOULD warn when verifying against a trust root older than a configurable threshold. In air-gapped environments, deployments SHOULD establish an out-of-band process for updating the trust root alongside the mirrored packages. + +## Security Considerations + +The `.sigs` sidecar is served by the same infrastructure as the package it describes: an attacker who can modify the package artifact can equally modify or remove the sidecar. The mechanisms in this CEP layer as follows: + +- **Sigstore verification** binds each attestation to a signing identity. An attacker cannot forge attestations for identities they do not control, but an attacker who controls the distribution path can substitute attestations signed by an identity they *do* control. The `trusted_identities` policy is what turns bundle verification into a guarantee about who produced the package. +- **The repodata `attestations` hash** binds the sidecar bytes to the repodata. An intermediary (mirror, CDN, proxy) that strips or rewrites a sidecar is detected by the client's hash check, provided the client obtained repodata from a trusted source. +- The strength of the hash binding is bounded by the integrity of repodata itself, which is currently protected by transport security to the channel origin. A compromised channel origin can consistently rewrite the package, the sidecar, and the repodata. This is the same trust model that applies to packages today. A future repodata signing mechanism would automatically extend to sidecar integrity, since the `attestations` field is part of the signed content. +- `require: warn` and `require: ignore` do not block installation on failure. Deployments that rely on attestations as a security control MUST use `require: error` together with a restrictive `trusted_identities` list. + +## References + +- [CEP 16 - Sharded Repodata][CEP 16] +- [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] +- [RubyGems release-gem][rubygems] +- [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 16]: ./cep-0016.md +[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 diff --git a/cep-xxxx-serving-attestations.md b/cep-xxxx-serving-attestations.md deleted file mode 100644 index 11204a6d..00000000 --- a/cep-xxxx-serving-attestations.md +++ /dev/null @@ -1,216 +0,0 @@ -# CEP XXXX - Distribution of Sigstore Attestations for Conda Packages - - - - - - - - - - -
Title Distribution of Sigstore Attestations for Conda Packages
Status Draft
Author(s) Wolf Vollprecht <wolf@prefix.dev>
Created Dec 02, 2025
Updated Dec 02, 2025
Discussion NA
Implementation https://prefix.dev (preview implementation)
Requires CEP 27 (Publish Attestation)
- -> 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, read-only HTTP sidecar 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, such as attestations produced during build, during channel upload, or by other review processes. - -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]). - -## Specification - -### Endpoint Definition - -For any conda package artifact at URL: - -```text -// -``` - -For channels implementing this CEP, attestations MUST be available at: - -```text -//.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 - -For channels implementing this CEP, 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 -[ - , - , - ... -] -``` - -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 -[] -``` - -### HTTP Status Codes - -| Status Code | Meaning | -| --------------- | ----------------------------------------------------------------------------------------------- | -| `200 OK` | The package exists and attestations were returned successfully (the array may be empty) | -| `404 Not Found` | For channels implementing this CEP, the package does not exist | - -Channels that support attestations MUST return `200 OK` with an empty array `[]` when the package exists but no attestations are available for it. - -For backwards compatibility, clients MUST NOT use a `404 Not Found` response from the `.sigs` endpoint alone to determine whether a package exists. Channels that do not implement this CEP may return `404 Not Found` for every `.sigs` URL, even when the underlying package exists. Clients that need to determine package existence MUST use the channel's package metadata or the package artifact URL itself. - -### 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" -} -``` - -This hash allows mirrors and clients to detect changes to the `.sigs` sidecar, including attestations added after the package was first published, and re-fetch the sidecar when it changes. - -### Attestation Requirements - -This CEP defines discovery and distribution of attestations. Verification of publish attestations MUST follow [CEP 27]. - -Each element in the response MUST be a valid [Sigstore Bundle]. If a bundle contains a [CEP 27] publish attestation, then: - -1. The in-toto statement's `subject[0].name` MUST match the artifact filename. - -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`. - -CEP 27 publish attestations intentionally describe a single package artifact. Other predicate types MAY appear in the same `.sigs` response, but this CEP does not define their verification rules. Clients MUST verify each recognized predicate type according to its own specification and MAY ignore or reject unrecognized predicate types according to local policy. - -### Multiple Attestations - -A package MAY have multiple attestations, provided each attestation intended to apply to the package artifact identifies that artifact according to the verification rules for its predicate type. Clients MAY choose which attestations to verify based on their trust policy. - -This CEP does not define upload authorization, channel admission policy, or access control for adding attestations. For example, a third party may produce an attestation, but the channel decides whether and how that attestation is accepted for distribution. - -### Mirror Behavior - -Mirrors and proxies SHOULD: - -1. Fetch and cache `.sigs` files alongside packages -2. Serve cached attestations without modification -3. Use the repodata `attestations` hash to detect changed `.sigs` sidecars -4. Re-fetch a cached `.sigs` sidecar when the repodata `attestations` hash changes -5. Preserve upstream `404 Not Found` responses for `.sigs` URLs when the upstream channel does not provide a sidecar - -Mirrors MUST NOT infer that a package does not exist solely because the upstream `.sigs` endpoint returns `404 Not Found`. - -## Client Behavior - -### Verification Workflow - -Clients implementing attestation verification SHOULD follow this workflow: - -1. **Download package** from the channel -2. **Fetch attestations** from `.sigs` -3. **Verify each attestation** using the verification process defined by [CEP 27] or by the attestation's registered predicate type. -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 - require: warn - trusted_identities: - - "https://github.com/foobar" -``` - -Each channel entry MUST specify `enabled`, `require`, and `trusted_identities`; clients MUST NOT infer default values for omitted fields. - -| 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) | -| `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. - -Clients MUST periodically update the Sigstore trust root so they do not miss trust-root changes, including key revocations and newly trusted keys. - -## 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 From 13a83f8dd4ad801c3fbc1b81b77dd572e1bede1c Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Wed, 22 Jul 2026 10:50:52 +0200 Subject: [PATCH 10/11] Update cep-XXXX.md Co-authored-by: Travis Hathaway --- cep-XXXX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cep-XXXX.md b/cep-XXXX.md index 11ad921d..62b4d2d2 100644 --- a/cep-XXXX.md +++ b/cep-XXXX.md @@ -174,7 +174,7 @@ Clients implementing attestation verification SHOULD follow this workflow: Clients SHOULD support the following configuration options: ```yaml -# Example ~/.condarc configuration +# Abstract example of Sigstore configuration: attestations: conda-forge: enabled: true From d32943e56a4d0c772108eaa2b3ad4bf52c908461 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Mon, 27 Jul 2026 12:23:44 +0200 Subject: [PATCH 11/11] cep updates --- cep-XXXX.md | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/cep-XXXX.md b/cep-XXXX.md index 62b4d2d2..f34a2780 100644 --- a/cep-XXXX.md +++ b/cep-XXXX.md @@ -5,7 +5,7 @@ Status Draft Author(s) Wolf Vollprecht <wolf@prefix.dev> Created Dec 02, 2025 - Updated Jul 05, 2026 + Updated Jul 23, 2026 Discussion https://github.com/conda/ceps/pull/142 Implementation https://prefix.dev (preview implementation) Requires CEP 27 (Publish Attestation) @@ -21,7 +21,8 @@ ## 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. +This CEP defines how [Sigstore] attestations are distributed alongside conda packages and how clients consume them. +Building upon [CEP 27], which standardizes the attestation format, this proposal specifies how channels serve attestations via a `.sigs` sidecar endpoint, how the repodata index advertises and integrity-protects sidecars through a new `attestations` field, and the client-side configuration for discovering, verifying, and enforcing policy on attestations. Together, these enable verification of package integrity and provenance. ## Motivation @@ -123,7 +124,7 @@ The field is the single discovery and integrity mechanism for attestation sideca 1. **Discovery**: Clients learn from repodata alone whether a `.sigs` file exists, avoiding a network round-trip for packages without attestations. 2. **Integrity**: Clients MUST verify that the fetched `.sigs` bytes hash to `sha256` before using the sidecar (see [Verification Workflow](#verification-workflow)). This prevents a mirror or intermediary from stripping or replacing attestations without detection. 3. **Change detection**: When attestations are added after a package was first published, the channel publishes an updated `.sigs` file and updates the field. Mirrors and clients re-fetch the sidecar when the hash changes. -4. **Resource bounds**: `size` allows clients to enforce a download limit before fetching. Clients MAY refuse to download sidecars larger than a locally configured limit; such a refusal MUST be handled like a retrieval failure (see [Configuration](#configuration)). +4. **Resource bounds**: `size` allows clients to enforce a download limit before fetching, protecting against accidentally downloading oversized sidecars. It is a pre-flight hint rather than a security mechanism: clients MUST also enforce their limit on the actual bytes received. Clients MAY refuse to download sidecars larger than a locally configured limit; such a refusal MUST be handled like a retrieval failure (see [Configuration](#configuration)). Tools that post-process repodata (e.g. hotfixing and patching pipelines) MUST preserve the `attestations` field. Channels using sharded repodata ([CEP 16]) update only the affected shard when attestations change, so clients pick up new attestations incrementally; consumers of monolithic `repodata.json` receive the update on the next regeneration. @@ -180,22 +181,25 @@ attestations: enabled: true require: warn # "error", "warn", or "ignore" trusted_identities: - - "https://github.com/conda-forge/*" - - "https://github.com/my-org/*" + - identity: "https://github.com/conda-forge/*" + issuer: "https://token.actions.githubusercontent.com" + - identity: "https://github.com/my-org/*" + issuer: "https://token.actions.githubusercontent.com" https://prefix.dev/foobar: enabled: true require: warn trusted_identities: - - "https://github.com/foobar" + - identity: "https://github.com/foobar" + issuer: "https://token.actions.githubusercontent.com" ``` Each channel entry MUST specify `enabled`, `require`, and `trusted_identities`; clients MUST NOT infer default values for omitted fields. -| Setting | Values | Behavior | -| -------------------- | ---------------------------- | ---------------------------------------------------------- | -| `enabled` | `true`/`false` | Enable or disable attestation fetching and verification | -| `require` | `error`/`warn`/`ignore` | How to respond to attestation problems (see below) | -| `trusted_identities` | List of patterns | Only accept attestations from matching Sigstore identities | +| Setting | Values | Behavior | +| -------------------- | ------------------------------------ | ---------------------------------------------------------- | +| `enabled` | `true`/`false` | Enable or disable attestation fetching and verification | +| `require` | `error`/`warn`/`ignore` | How to respond to attestation problems (see below) | +| `trusted_identities` | List of `(identity, issuer)` entries | Only accept attestations from matching Sigstore identities | #### `require` semantics @@ -215,9 +219,15 @@ Three classes of attestation problems exist: #### `trusted_identities` matching -Each entry is a pattern matched against the certificate identity (the SubjectAlternativeName of the Sigstore signing certificate). Matching is case-sensitive and literal, except that `*` matches any sequence of characters, including `/`. -For example, `https://github.com/conda-forge/*` matches `https://github.com/conda-forge/numpy-feedstock/.github/workflows/build.yml@refs/heads/main` but not `https://github.com/conda-forge-evil/...`, because the literal prefix includes the trailing slash. -A future revision of this CEP MAY extend entries to additionally pin the OIDC issuer. +A Sigstore signing identity is the *pair* of the certificate identity (the SubjectAlternativeName of the signing certificate) and the OIDC issuer that authenticated it. The same SubjectAlternativeName authenticated by two different issuers constitutes two different identities, so each `trusted_identities` entry MUST specify both fields: + +- `identity`: a pattern matched against the SubjectAlternativeName. Matching is case-sensitive and literal, except that `*` matches any sequence of characters, including `/`. + For example, `https://github.com/conda-forge/*` matches `https://github.com/conda-forge/numpy-feedstock/.github/workflows/build.yml@refs/heads/main` but not `https://github.com/conda-forge-evil/...`, because the literal prefix includes the trailing slash. +- `issuer`: the OIDC issuer URL (e.g. `https://token.actions.githubusercontent.com` for GitHub Actions). Issuers are compared literally; patterns are not supported. + +An attestation matches an entry only when the SubjectAlternativeName matches `identity` **and** the certificate's OIDC issuer equals `issuer`. Clients MUST NOT accept attestations based on the identity pattern alone. + +Clients MAY offer shorthand notations that expand deterministically to `(identity, issuer)` entries (for example, deriving the GitHub Actions issuer from a `github:owner/repo` form), provided the expansion is documented and the underlying policy always contains both fields. ### Offline and Air-gapped Environments @@ -230,7 +240,7 @@ Clients SHOULD refresh the Sigstore trust root regularly — for example, on the The `.sigs` sidecar is served by the same infrastructure as the package it describes: an attacker who can modify the package artifact can equally modify or remove the sidecar. The mechanisms in this CEP layer as follows: -- **Sigstore verification** binds each attestation to a signing identity. An attacker cannot forge attestations for identities they do not control, but an attacker who controls the distribution path can substitute attestations signed by an identity they *do* control. The `trusted_identities` policy is what turns bundle verification into a guarantee about who produced the package. +- **Sigstore verification** binds each attestation to a signing identity, that is, the pair of certificate identity and OIDC issuer. An attacker cannot forge attestations for identities they do not control, but an attacker who controls the distribution path can substitute attestations signed by an identity they *do* control. The `trusted_identities` policy, because it pins both the identity pattern and the issuer, is what turns bundle verification into a guarantee about who produced the package. - **The repodata `attestations` hash** binds the sidecar bytes to the repodata. An intermediary (mirror, CDN, proxy) that strips or rewrites a sidecar is detected by the client's hash check, provided the client obtained repodata from a trusted source. - The strength of the hash binding is bounded by the integrity of repodata itself, which is currently protected by transport security to the channel origin. A compromised channel origin can consistently rewrite the package, the sidecar, and the repodata. This is the same trust model that applies to packages today. A future repodata signing mechanism would automatically extend to sidecar integrity, since the `attestations` field is part of the signed content. - `require: warn` and `require: ignore` do not block installation on failure. Deployments that rely on attestations as a security control MUST use `require: error` together with a restrictive `trusted_identities` list.