trailofbits / trailofbits/coop

Pin the signer identity when verifying release attestations

Open
#436 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
243
Forks
13
Avg merge
1d 20h
Merged PRs (30d)
30

Description

Status: needs research

Raised while reviewing #421. This is a finding plus a direction, not a worked-out proposal — several of the questions below need answering before we could implement anything, and at least one of them (the historical-release audit) could rule the strict form out entirely. Filing it so it is not rediscovered later.

What --repo actually pins

Both clients verify with --repo trailofbits/coop and nothing else:

  • src/update.rsattestation_verify_args builds ["attestation", "verify", <tarball>, "--repo", REPO] plus an optional --bundle.
  • install.sh:150 and install.sh:162 — the same argument list.

Neither passes --signer-workflow, --cert-identity, or --cert-identity-regex. grep -rn 'signer-workflow|cert-identity' over the tree hits docs/trust-model.md only, never a code path.

With no identity flag, --repo X/Y sets the certificate-identity policy to SANRegex = (?i)^https://github\.com/X/Y/. So it does constrain the signer — but only to any workflow, on any ref, in that repository holding id-token: write + attestations: write. It is a repository pin, not a workflow pin.

Verified state of a real release

Decoded from the attestations API for the v0.5.4 x86_64-unknown-linux-musl tarball (digest 91c211f9…). The API returns two attestations for the digest:

# Certificate SAN Subjects
1 https://dotcom.releases.github.com 3 tarballs + SHA256SUMS
2 https://github.com/trailofbits/coop/.github/workflows/release.yml@refs/tags/v0.5.4 3 tarballs

Attestation 2 is ours, from actions/attest-build-provenance (Fulcio issuer OID 1.3.6.1.4.1.57264.1.1, build-config ref refs/tags/v0.5.4). Attestation 1 is GitHub's own immutable-releases signer.

Two things follow that matter for any pin:

  • The SAN embeds the tag ref. An exact --cert-identity would therefore have to be constructed per release, coupling the verifier to our tag naming. --signer-workflow ignores the ref, which is why it is the more practical option.
  • The #421 bundle asset carries only attestation 2, since it comes from the attest step's bundle-path, whereas the API path returns both. A pin has to be correct for both transports.

Why #421 raises the stakes

Documenting this gap was defensible while GitHub's attestation store was the source of truth. #421 moves the trust anchor into a client-supplied release asset, which changes two properties:

  • Write scope. Registering an attestation in the store needs attestations: write. Placing a bundle in a release needs only contents: write. The --bundle path accepts a strictly larger set of bundles.
  • Revocability. DELETE /orgs/{org}/attestations/digest/{digest} exists, but Fulcio certificates carry no CRL/OCSP and nothing in gh's verification path consults a revocation source. A bundle already published as an asset keeps verifying indefinitely after the store entry is deleted.

Neither is exploitable by anyone who does not already have write access to the repository. This is defence-in-depth against a compromised-workflow scenario, not a live weakness — consistent with the framing in #422.

Options

  1. --signer-workflow trailofbits/coop/.github/workflows/release.yml — pins the workflow, ignores the ref. Simplest, but see the anchoring question below.
  2. --cert-identity-regex with an explicit $ — tight control over both workflow and ref shape, at the cost of a regex we maintain.
  3. --cert-identity — exact SAN including @refs/tags/vX.Y.Z, constructed per release. Strictest, most coupled.
  4. Do nothing, and record it as an accepted trade-off in docs/trust-model.md. A legitimate outcome given the write-access precondition.

Open questions — the research part

  • gh version floor. --signer-workflow landed in gh 2.51.0. There is no gh version gate anywhere in the repo today. What happens on older gh — fail closed, warn and continue unpinned, or skip? Failing closed on an old gh would regress installs that work now.
  • Anchoring. --signer-workflow's value reportedly compiles to a start-anchored-only regex (^ + QuoteMeta, no $), which would mean …/release.yml also prefix-matches …/release.yml.evil. This came from reading pkg/cmd/attestation/verify/policy.go and has not been confirmed empirically — it should be, since it decides between options 1 and 2.
  • Historical releases. Any pin has to hold for every tag still installable via VERSION=. If release.yml was ever renamed or moved, or a release was ever cut from a different workflow or a branch rather than a tag, a strict pin breaks those installs permanently. This needs an audit of the cert identity of every published release before committing to a form. This is the question most likely to change the answer.
  • Reusable workflows. We do not use one today, but if release.yml ever calls a reusable workflow the SAN changes to the callee, silently breaking a workflow pin. Worth knowing before we pin.
  • Interaction with #423. If the API fallback becomes credential-free, both transports should carry the same policy so they cannot diverge.

References

  • #421 — publishes the bundle as a release asset (the change that motivates this)
  • #422 — broader verification-chain audit; this is a gap it does not cover
  • #423 — credential-free API fallback
  • docs/trust-model.md item 4 — currently documents the gap rather than closing it

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/update.rs and install.sh:150 and :162, then verify the signer-workflow anchoring behavior in gh's pkg/cmd/attestation/verify/policy.go. Audit certificate identities for every release still reachable through VERSION= and check the gh version floor, reusable-workflow behavior, and #423's transport interaction. Done means a supported verification policy is chosen or the trade-off is documented in docs/trust-model.md, consistently for both transports.

Written by the indexing model from the issue text.

Assessment

Tech stack
github-actions, rust, shell
Domain
cli, release, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.