fix(evidence): verify private Cloud attestations on GitHub's instance - #293
Draft
abrichr wants to merge 2 commits into
Draft
fix(evidence): verify private Cloud attestations on GitHub's instance#293abrichr wants to merge 2 commits into
abrichr wants to merge 2 commits into
Conversation
A private repository does not use the Sigstore public-good instance. GitHub signs those attestations with its own instance: the internal Fulcio at fulcio.githubapp.com and the timestamp authority at timestamp.githubapp.com. `gh attestation trusted-root` shows that instance carries no tlogs at all, and the gh CLI verifies it with WithSignedTimestamps(1) rather than a transparency log. So the previous requirement of exactly one "Tlog" entry at https://rekor.sigstore.dev could never be met by a genuine certificate from OpenAdaptAI/openadapt-cloud, exactly like the visibility field fixed alongside it. Require exactly one verified "TimestampAuthority" entry from timestamp.githubapp.com instead, still inside the fifteen minutes after the certificate acceptance_verified_at value. A Rekor time, a public-good timestamp-authority time, a current-clock time, and a second authority time are all refused. Also pass --no-public-good to `gh attestation verify`. A certificate signed by the public-good instance is now refused by the verifier itself, not only by the sourceRepositoryVisibilityAtSigning field, so the trust root and the claimed source visibility must agree. The fixture no longer claims a public-good Sigstore intermediate for a signature that can only come from GitHub's internal Fulcio. Both new gates are mutation-checked: removing --no-public-good fails the command test, and accepting any timestamp authority fails three of the four new cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abrichr
marked this pull request as draft
August 20, 2026 17:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #292. That PR fixed one field. This one fixes the rest of the same class: every remaining place where the verifier assumed the certificate came from a public repository signed by the Sigstore public-good instance.
The defect
OpenAdaptAI/openadapt-cloudis private, so GitHub does not sign its attestations with the Sigstore public-good instance. It signs them with its own instance. The local GitHub CLI trust roots show the difference exactly:https://rekor.sigstore.dev,https://log2025-1.rekor.sigstore.devhttps://timestamp.sigstore.dev/api/v1/timestamptimestamp.githubapp.comhttps://fulcio.sigstore.devfulcio.githubapp.comThe GitHub instance has no transparency log at all, and the gh CLI verifies it with
verify.WithSignedTimestamps(1)rather thanWithTransparencyLog(1). So_validate_verified_provenancerequiring exactly one"Tlog"entry athttps://rekor.sigstore.devcould never be satisfied by a genuineopenadapt-cloudcertificate — the same unreachable-gate defect as the visibility field in #292.Still unreachable today:
import_files()refuses every import until an approved private-export contract exists, and it fails closed. No released behaviour changes.Changes
Bind the substitute time source.
verifiedTimestampsmust now contain exactly one"TimestampAuthority"entry whoseuriistimestamp.githubapp.com(new constantGITHUB_TIMESTAMP_AUTHORITY), still inside the fifteen minutes after the certificate'sacceptance_verified_at. The surrounding structure is unchanged, so a Rekor time, a public-good TSA time, aCurrentTimeentry, or a second authority time is refused.Type and URI are not guessed.
TimestampVerificationResultin sigstore-go is built as{Type: "TimestampAuthority", URI: vts.URI, ...},root.Timestampis returned as&Timestamp{Time: timestamp.Time, URI: tsa.URI}, andtsa.URIis the trusted root'stimestampAuthorities[].uri—timestamp.githubapp.comin the dump above.Refuse the public-good trust root outright.
gh attestation verifynow gets--no-public-good. gh does not construct the public-good verifier under that flag and rejects an attestation it detects as public-good-signed. The trust root and the claimed source visibility must now agree; the visibility field alone is no longer the only thing standing between a public-good signature and acceptance.Fixture honesty.
certificateIssuerno longer claimsCN=sigstore-intermediate,O=sigstore.devfor a signature that can only come from GitHub's internal Fulcio. The field is not asserted on, so this is not a gate — it stops the fixture from teaching the next reader the wrong trust path.Docs.
PRODUCTION_READINESS.mdrecords the instance split, names both new bindings, and states plainly that this is a weaker public-audit property than a public repository gives, forced by the private control-plane boundary.Why "private" is the right target, confirmed from the Cloud side
openadapt-cloudmaincarries the matching contract intests/unit/execute-live-acceptance.test.mjs:That is character-for-character
CLOUD_REPOSITORYandCLOUD_CERTIFICATE_IDENTITY. The acceptance workflow is meant to stay in the private repository, soprivatevisibility and the GitHub-internal trust path are the correct targets.Verification
--no-public-goodfailstest_verifier_uses_exact_repository_workflow_ref_and_hosted_runner_policy.ruff check .passes.testpasses.evidence-freshnessfails for a pre-existing reason unrelated to this PR — the published evidence set is pinned toopenadapt-flow1.31.0 while 1.32.0 is now on PyPI. The same check fails onmain(b7d4fe8, run 32342633608).Two prerequisites before this path can ever run — neither belongs in this PR
tests/unit/execute-live-acceptance.test.mjson Cloudmaincontainstest('the workflow cannot publish evidence before private retention is approved and verified'), which asserts the workflow does not matchid-token: write,attestations: write,actions/attest, oractions/upload-artifact. The absent attestation step is a guarded decision that mirrors theimport_files()refusal on this side, not an oversight. It must stay absent until the private retention contract is approved.OpenAdaptAIis on thefreeplan (gh api orgs/OpenAdaptAI --jq .plan.name), soopenadapt-cloudcannot produce an artifact attestation at all until the org moves to Enterprise Cloud. If that upgrade is declined, the design has to change — either the acceptance workflow moves to a public repository (which reverts fix(evidence): expect private Cloud source visibility at signing #292 and this PR), or the certificate is bound by the existing Ed25519 envelope machinery instead of a GitHub attestation.Note for whoever eventually runs the importer: the host needs a GitHub CLI that supports
--no-public-good(verified on 2.67.0), authenticated with read access to the private Cloud repository. A missing flag or missing auth surfaces as a refusal, not a pass.🤖 Generated with Claude Code