Skip to content

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

One attestation per package

Per CEP-27, 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):

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

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:

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

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:

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

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:

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. You only need to specify the publisher:

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:

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#

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#

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#

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
  • 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#