Skip to content

Pin the signer identity when verifying release attestations #436

Description

@hbrodin

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 Make the attestation-API fallback credential-free for pre-bundle releases #423. If the API fallback becomes credential-free, both transports should carry the same policy so they cannot diverge.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions