trailofbits / trailofbits/coop
Pin the signer identity when verifying release attestations
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.rs—attestation_verify_argsbuilds["attestation", "verify", <tarball>, "--repo", REPO]plus an optional--bundle.install.sh:150andinstall.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-identitywould therefore have to be constructed per release, coupling the verifier to our tag naming.--signer-workflowignores the ref, which is why it is the more practical option. - The
#421bundle asset carries only attestation 2, since it comes from the attest step'sbundle-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 onlycontents: write. The--bundlepath 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
--signer-workflow trailofbits/coop/.github/workflows/release.yml— pins the workflow, ignores the ref. Simplest, but see the anchoring question below.--cert-identity-regexwith an explicit$— tight control over both workflow and ref shape, at the cost of a regex we maintain.--cert-identity— exact SAN including@refs/tags/vX.Y.Z, constructed per release. Strictest, most coupled.- 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-workflowlanded 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.ymlalso prefix-matches…/release.yml.evil. This came from readingpkg/cmd/attestation/verify/policy.goand 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=. Ifrelease.ymlwas 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.ymlever 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.mditem 4 — currently documents the gap rather than closing it
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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