You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_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.4x86_64-unknown-linux-musl tarball (digest 91c211f9…). The API returns two attestations for the digest:
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.
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
--signer-workflow trailofbits/coop/.github/workflows/release.yml — pins the workflow, ignores the ref. Simplest, but see the anchoring question below.
--cert-identity-regex with 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-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.
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
--repoactually pinsBoth clients verify with
--repo trailofbits/coopand 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 hitsdocs/trust-model.mdonly, never a code path.With no identity flag,
--repo X/Ysets the certificate-identity policy toSANRegex = (?i)^https://github\.com/X/Y/. So it does constrain the signer — but only to any workflow, on any ref, in that repository holdingid-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.4x86_64-unknown-linux-musltarball (digest91c211f9…). The API returns two attestations for the digest:https://dotcom.releases.github.life-white.ukSHA256SUMShttps://github.com/trailofbits/coop/.github/workflows/release.yml@refs/tags/v0.5.4Attestation 2 is ours, from
actions/attest-build-provenance(Fulcio issuer OID1.3.6.1.4.1.57264.1.1, build-config refrefs/tags/v0.5.4). Attestation 1 is GitHub's own immutable-releases signer.Two things follow that matter for any pin:
--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.#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:
attestations: write. Placing a bundle in a release needs onlycontents: write. The--bundlepath accepts a strictly larger set of bundles.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.docs/trust-model.md. A legitimate outcome given the write-access precondition.Open questions — the research part
--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.--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.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.release.ymlever calls a reusable workflow the SAN changes to the callee, silently breaking a workflow pin. Worth knowing before we pin.References
docs/trust-model.mditem 4 — currently documents the gap rather than closing it