Skip to content

fix(evidence): verify private Cloud attestations on GitHub's instance - #293

Draft
abrichr wants to merge 2 commits into
mainfrom
fix/private-cloud-attestation-trust-path
Draft

fix(evidence): verify private Cloud attestations on GitHub's instance#293
abrichr wants to merge 2 commits into
mainfrom
fix/private-cloud-attestation-trust-path

Conversation

@abrichr

@abrichr abrichr commented Aug 20, 2026

Copy link
Copy Markdown
Member

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-cloud is 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:

public-good root GitHub internal root
tlogs https://rekor.sigstore.dev, https://log2025-1.rekor.sigstore.dev none
TSA https://timestamp.sigstore.dev/api/v1/timestamp timestamp.githubapp.com
Fulcio https://fulcio.sigstore.dev fulcio.githubapp.com
$ gh attestation trusted-root   # gh 2.67.0
... GitHub root: "tlogs": [], TSA uri "timestamp.githubapp.com", CA uri "fulcio.githubapp.com"

The GitHub instance has no transparency log at all, and the gh CLI verifies it with verify.WithSignedTimestamps(1) rather than WithTransparencyLog(1). So _validate_verified_provenance requiring exactly one "Tlog" entry at https://rekor.sigstore.dev could never be satisfied by a genuine openadapt-cloud certificate — 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. verifiedTimestamps must now contain exactly one "TimestampAuthority" entry whose uri is timestamp.githubapp.com (new constant GITHUB_TIMESTAMP_AUTHORITY), still inside the fifteen minutes after the certificate's acceptance_verified_at. The surrounding structure is unchanged, so a Rekor time, a public-good TSA time, a CurrentTime entry, or a second authority time is refused.

Type and URI are not guessed. TimestampVerificationResult in sigstore-go is built as {Type: "TimestampAuthority", URI: vts.URI, ...}, root.Timestamp is returned as &Timestamp{Time: timestamp.Time, URI: tsa.URI}, and tsa.URI is the trusted root's timestampAuthorities[].uritimestamp.githubapp.com in the dump above.

Refuse the public-good trust root outright. gh attestation verify now 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. certificateIssuer no longer claims CN=sigstore-intermediate,O=sigstore.dev for 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.md records 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-cloud main carries the matching contract in tests/unit/execute-live-acceptance.test.mjs:

assert.equal(record.identities.producer.repository, 'OpenAdaptAI/openadapt-cloud');
assert.equal(record.identities.producer.source_ref, 'refs/heads/main');
assert.equal(
  record.identities.verifier.certificate_identity,
  'https://github.com/OpenAdaptAI/openadapt-cloud/.github/workflows/execute-live-acceptance.yml@refs/heads/main',
);

That is character-for-character CLOUD_REPOSITORY and CLOUD_CERTIFICATE_IDENTITY. The acceptance workflow is meant to stay in the private repository, so private visibility and the GitHub-internal trust path are the correct targets.

Verification

  • 233 tests pass (229 before; +4 new timestamp cases).
  • Mutation checks, so the tests guard the gates rather than restate them:
    • Removing --no-public-good fails test_verifier_uses_exact_repository_workflow_ref_and_hosted_runner_policy.
    • Accepting any timestamp authority fails 3 of the 4 new cases (the duplicate-entry case still refuses on count).
  • ruff check . passes.
  • CI: test passes. evidence-freshness fails for a pre-existing reason unrelated to this PR — the published evidence set is pinned to openadapt-flow 1.31.0 while 1.32.0 is now on PyPI. The same check fails on main (b7d4fe8, run 32342633608).

Two prerequisites before this path can ever run — neither belongs in this PR

  1. The Cloud repository deliberately forbids the signing step today. tests/unit/execute-live-acceptance.test.mjs on Cloud main contains test('the workflow cannot publish evidence before private retention is approved and verified'), which asserts the workflow does not match id-token: write, attestations: write, actions/attest, or actions/upload-artifact. The absent attestation step is a guarded decision that mirrors the import_files() refusal on this side, not an oversight. It must stay absent until the private retention contract is approved.
  2. Private-repository attestations need GitHub Enterprise Cloud. On GitHub Free, Pro, and Team, artifact attestations are available for public repositories only. OpenAdaptAI is on the free plan (gh api orgs/OpenAdaptAI --jq .plan.name), so openadapt-cloud cannot 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

abrichr and others added 2 commits August 20, 2026 07:00
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
abrichr marked this pull request as draft August 20, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant