# Sigstore attestations

Sigstore is a way to cryptographically sign packages (or any binary artifacts). It was pioneered in the container space, but has been adopted by many packaging ecosystems: PyPI, Python releases, Rust crates, Homebrew, and Rubygems.

Rattler-Build supports creating Sigstore attestations for conda packages, allowing you to cryptographically sign your packages and provide verifiable provenance information.

## What does a Sigstore attestation provide?

A Sigstore attestation ties the producer of a package (for example, a GitHub Actions workflow) to the package artifact. When the attestation is created, metadata about the artifact is registered in a "Transparency Log" which can be inspected at any time. The metadata contains information such as:

- The name and SHA256 hash of the package
- The CI workflow that built the package
- The repository Git hash that contained the CI workflow
- The organization and username that built the package

Using this information, you can attest that a given package was created and uploaded by a certain GitHub organization, and this is transparently logged on both the server and the Sigstore public good instance.

The attestation follows [CEP-27](https://conda.org/learn/ceps/cep-0027), which standardizes the in-toto attestation format with a Conda-specific predicate containing:

- The SHA256 hash of the package (guaranteed to be unique)
- The full filename (`{name}-{version}-{buildstring}.conda`)
- Optionally, the target channel URL (e.g., `https://prefix.dev/my-channel`)

!!! warning "One attestation per package"
    Per [CEP-27](https://conda.org/learn/ceps/cep-0027), a Conda publish
    attestation describes exactly **one** `.conda` artifact — its `subject`
    contains a single name and digest. If a build produces multiple packages
    (for example a multi-output recipe, or a matrix build across platforms),
    each package needs its own attestation bundle. Tools that accept a glob
    like `**/*.conda` and emit a single bundle covering all matches do **not**
    produce a CEP-27 compliant attestation, and uploads using such a bundle
    will fail to verify.

## Automatic attestation generation

The easiest way to create Sigstore attestations is using the
`--generate-attestation` flag. The same flag is available on both
`rattler-build publish` (which builds and uploads in one step) and on
`rattler-build upload prefix` (which uploads pre-built packages):

```bash
# Build and publish in one go
rattler-build publish ./my-recipe.yaml --to https://prefix.dev/my-channel --generate-attestation

# Or upload one or more pre-built packages
rattler-build upload prefix -c my-channel ./output/**/*.conda --generate-attestation
```

In both cases rattler-build will:

1. Take each `.conda` file individually,
2. Mint a fresh OIDC token from the CI runner and create one CEP-27 compliant
   Sigstore attestation **per package**,
3. Upload the package and its bundle to prefix.dev together.

You do **not** need `actions/attest`, `cosign`, or any other external tool
when you use this flag — rattler-build handles the per-package signing and
the one-attestation-per-package requirement automatically. This is the
recommended path for multi-output recipes and matrix builds.

Pass `--store-github-attestation` (on `upload prefix`) to additionally push
the generated bundle to GitHub's attestation API so it shows up in your
repository's attestation tab.

!!! note "Requirements"
    The `--generate-attestation` flag only works when:

    - Uploading to prefix.dev channels
    - Using Trusted Publishing (OIDC authentication) — it cannot be combined
      with an API key
    - Running in a supported CI environment (e.g., GitHub Actions)

### GitHub Actions example

Here's a complete example workflow using automatic attestation generation:

```yaml title=".github/workflows/build.yml"
name: Build and publish with attestation

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    # These permissions are needed for OIDC authentication
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Set up rattler-build
        uses: prefix-dev/rattler-build-action@v0.2.34

      - name: Build and publish with attestation
        run: |
          rattler-build publish ./recipe.yaml \
            --to https://prefix.dev/my-channel \
            --generate-attestation
```

## Manual attestation with GitHub Actions

!!! tip "Prefer `--generate-attestation` when possible"
    For most users — including multi-package and matrix builds — the
    `--generate-attestation` flag described above already does everything
    in this section automatically and correctly produces one bundle per
    package. Reach for the manual flow only when you need to customize the
    predicate, sign with an external trust root, or otherwise can't use
    rattler-build's built-in signing.

If you need more control over the attestation process, you can use GitHub's
official attest action to create the attestation separately. Because each
attestation must cover exactly one package (see the warning above), the
attestation and upload steps need to run **once per `.conda` artifact**.

The example below builds a single recipe and signs the resulting package:

```yaml title=".github/workflows/build.yml"
name: Package and sign

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    # These permissions are needed to create a sigstore certificate
    permissions:
      id-token: write
      contents: read
      attestations: write

    steps:
      - uses: actions/checkout@v4

      - name: Build conda package
        uses: prefix-dev/rattler-build-action@v0.2.34

      # Use GitHub's official attest action with the Conda predicate.
      # `subject-path` MUST point at a single `.conda` file — globs that
      # resolve to more than one file produce a non-compliant bundle.
      - uses: actions/attest@v1
        id: attest
        with:
          subject-path: ./output/linux-64/my-package-0.1.0-h123_0.conda
          predicate-type: "https://schemas.conda.org/attestations-publish-1.schema.json"
          predicate: '{"targetChannel": "https://prefix.dev/my-channel"}'

      # Upload the package together with its attestation bundle
      - name: Upload the package
        run: |
          rattler-build upload prefix -c my-channel \
            ./output/linux-64/my-package-0.1.0-h123_0.conda \
            --attestation ${{ steps.attest.outputs.bundle-path }}
```

### Multiple packages

If your build produces more than one `.conda` file (for example a multi-output
recipe, or several subdirs from a matrix build), `actions/attest` must be
invoked **once per package**, with `subject-path` pointing at exactly that
single file. There is no built-in way to produce one bundle for several
packages — that would not match the CEP-27 schema.

The most common pattern is a [matrix strategy](https://docs.github.com/actions/using-jobs/using-a-matrix-for-your-jobs)
that fans out one job per package, calls `actions/attest` once per job, and
then uploads the resulting `(package, bundle)` pair.

This approach gives you full control over the attestation creation and allows
you to customize the predicate or add additional attestation metadata.

## Source attestation verification

!!! note "Experimental"
    This feature requires the `--experimental` flag: `rattler-build build --experimental -r recipe.yaml`

In addition to signing built packages, Rattler-Build can also **verify** the attestations of source archives during the build. This lets you ensure that the source code you're building from was produced by a trusted publisher (e.g., a specific GitHub Actions workflow).

### How it works

Add an `attestation` block to a URL source in your recipe:

```yaml title="recipe.yaml"
source:
  url: https://files.pythonhosted.org/packages/.../flask-3.1.1.tar.gz
  sha256: "6489f1..."
  attestation:
    publishers:
      - github:pallets/flask
```

When Rattler-Build downloads the source, it will:

1. Fetch the Sigstore attestation bundle (automatically derived for PyPI packages, or from `bundle_url`)
2. Verify the bundle signature against the Sigstore transparency log
3. Check that the attestation identity matches one of the listed publishers

If verification fails, the build is aborted.

### PyPI sources

For packages hosted on PyPI, the attestation bundle URL is automatically derived from the [PyPI attestation API](https://docs.pypi.org/api/integrity/). You only need to specify the publisher:

```yaml
source:
  url: https://files.pythonhosted.org/packages/.../flask-3.1.1.tar.gz
  sha256: "6489f1..."
  attestation:
    publishers:
      - github:pallets/flask
```

### GitHub release sources

For source archives from GitHub releases, specify the `bundle_url` pointing to the `.sigstore.json` bundle:

```yaml
source:
  url: https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz
  sha256: "eb33e5..."
  attestation:
    bundle_url: https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz.sigstore.json
    publishers:
      - github:facebook/zstd
```

### Publisher format

Publishers are specified in `github:owner/repo` format. The identity is matched against the Sigstore certificate's Subject Alternative Name (SAN), which for GitHub Actions is the workflow identity.

## Verifying attestations

Once packages are published with attestations, they can be verified using several tools:

### Using the GitHub CLI

```bash
gh attestation verify my-package-0.1.0-h123_0.conda \
  --owner my-org \
  --predicate-type "https://schemas.conda.org/attestations-publish-1.schema.json"
```

### Using cosign

```bash
cosign verify-blob \
  --certificate-identity-regexp "https://github.com/my-org/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  my-package-0.1.0-h123_0.conda
```

### Using sigstore-python

```bash
pip install sigstore
sigstore verify identity \
  --cert-identity "https://github.com/my-org/my-repo/.github/workflows/build.yml@refs/heads/main" \
  --cert-oidc-issuer "https://token.actions.githubusercontent.com" \
  my-package-0.1.0-h123_0.conda
```

## Viewing attestations

You can find attestations for packages published to prefix.dev in several places:

- **GitHub**: View attestations in your repository's attestations tab (e.g., `https://github.com/my-org/my-repo/attestations`)
- **Sigstore public goods instance**: Search by package hash at [search.sigstore.dev](https://search.sigstore.dev)
- **prefix.dev**: View attestations on the package page

## Security benefits

Sigstore attestations provide several security benefits:

1. **Unforgeability**: Attestations cryptographically bind a package to its producer, preventing forgery of provenance metadata.

2. **Transparency**: All attestations are logged in a public, append-only transparency log. This means attackers cannot perform targeted attacks without leaving a public trace.

3. **No long-lived keys**: Sigstore uses ephemeral keys bound to identities (like GitHub Actions workflows), eliminating the risk of key compromise.

4. **Build provenance**: Machine identities in the attestation provide verifiable information about exactly which workflow, repository, and commit produced the package.

## Further reading

- [CEP-27: Standardizing a publish attestation for the conda ecosystem](https://conda.org/learn/ceps/cep-0027)
- [Securing the Conda Package Supply Chain with Sigstore](https://prefix.dev/blog/securing-the-conda-package-supply-chain-with-sigstore) (prefix.dev blog)
- [Sigstore documentation](https://docs.sigstore.dev/)
- [Example repository with full source code](https://github.com/prefix-dev/sigstore-example)
