Skip to content
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 261 additions & 0 deletions cep-xxxx-serving-attestations.md
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 &lt;wolf@prefix.dev&gt;</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).

Copy link
Copy Markdown

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.

Copy link
Copy Markdown
Contributor

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?

These are not in contradiction, right?

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.

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.


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.

@facutuesca facutuesca Apr 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 .sigs endpoint as "this package does not exist", when channels that don't implement this CEP would return 404 on the .sigs endpoint for every package.

I think we should return 404 when the package does not exist, but define the meaning of 404 as:

  • For channels implementing this CEP, 404 means the package does not exist
  • For channels not implementing this CEP, .sigs always returns 404

and make it clear that clients should not use the .sigs endpoint to determine if a package exists or not


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
"attestations": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570"
}
```
"attestations": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570"
}
\```
The signatures hash enables mirrors to detect and re-fetch signature bundles as they are added to the repo.

(GitHub suggestions can't include the close-block, so I had to escape it. 😢 )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be subject[0].name? I'm imagining that I might have a CI process which produces an attestation for all the artifacts produced in a single attestation, so I have N+1 outputs, rather than 2N outputs. Should 1. and 2. be changed to "One of the attestation subjects MUST match as follows: (a) subject[N].name MUST match the artifact filename AND (b) `subject[N].digest.sha256 MUST match the SHA256 hash of the artifact"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Comment thread
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a default value for require (or the other settings)? In the example, require is omitted for the "foobar" repository.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
Comment thread
wolfv marked this conversation as resolved.
Outdated

Note: clients MUST periodically update the sigstore trust root to ensure no keys were revoked.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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