This document explains what the ZK proof guarantees and what it doesn't.
Sigstore Root CA
│ (offline, 10-year validity)
▼
Fulcio Intermediate CA ← HARDCODED IN CIRCUIT (P-384)
│ (online, signs leaf certs)
▼
Leaf Certificate ← VERIFIED IN CIRCUIT (P-256)
│ (ephemeral, ~10 minute validity)
│ Contains: repo, commit, workflow info (OIDC claims)
▼
Attestation Signature ← VERIFIED IN CIRCUIT
│
▼
Artifact Hash ← EXTRACTED AS PUBLIC INPUT
-
Valid Sigstore Attestation
- The attestation was signed by a valid Fulcio leaf certificate
- The leaf certificate was signed by the Fulcio intermediate CA
- The intermediate CA's public key matches the hardcoded value
-
Correct Claim Extraction
commitShacomes from the certificate's OIDC extension (OID 1.3.6.1.4.1.57264.1.3) — primary: pins immutable, auditable codeartifactHashcomes from the DSSE envelope payloadrepoHashcomes from the certificate's OIDC extension (OID 1.3.6.1.4.1.57264.1.5) — informational: the prover controls their repo, so this is a convenience filter, not a security boundary
-
Immutable Binding
- These three values are cryptographically bound together
- You cannot mix-and-match claims from different attestations
-
Workflow Behavior
- The proof doesn't say what the workflow did
- It only says which workflow (repo + commit) ran
-
Artifact Interpretation
- The proof doesn't say what the artifact means
- Just its hash
-
Repo Integrity
- The proof doesn't guarantee the repo hasn't been compromised
- The prover controls their repo
There are two paths to a Sigstore attestation. They produce the same bundle format and the ZK circuit can't tell them apart.
The workflow calls the action with a file path. Under the hood:
- GitHub's OIDC provider issues a token proving "I am repo X at commit Y in run Z"
- Fulcio exchanges the token for a short-lived leaf certificate with those claims as X.509 extensions
- The action signs the file, logs the signature to Rekor (Sigstore's transparency log)
- Result: a Sigstore bundle (DSSE envelope + certificate + Rekor inclusion proof)
This only works inside a GitHub Actions run — that's where the OIDC token comes from.
Anyone can get a Sigstore certificate. Fulcio supports multiple OIDC providers (GitHub, Google, Microsoft, etc.):
- Authenticate with
cosign sign-blobor the Sigstore libraries - Fulcio issues a cert based on whatever OIDC identity you present
- You sign your artifact, it goes to Rekor
- Result: same format Sigstore bundle
The certificate contains different OIDC claims depending on the provider. A Google-authenticated cert has an email; a GitHub Actions cert has repo, commit, run_id.
Path 1 is just a convenience wrapper around Path 2. Same Sigstore infrastructure, same Fulcio CA, same bundle format.
GitHub serves two roles in this system. Conflating them makes the trust model look more GitHub-dependent than it is.
These are what the ZK proof actually depends on:
| Component | What it provides | Could be replaced? |
|---|---|---|
| OIDC tokens | Identity claims (repo, commit, run_id) baked into Fulcio certs | Only by another Sigstore OIDC provider |
| Runner isolation | Ephemeral VM — secrets stay in memory, can't be exfiltrated | By any TEE (SGX, Nitro, etc.) |
| Workflow code at commit | Auditable execution — verifier reads the YAML at that SHA | By any reproducible build system |
The ZK circuit only touches the Sigstore certificate chain. It verifies that Fulcio signed a cert containing certain claims, and that cert signed a DSSE envelope containing an artifact hash. Nothing else.
These are useful but invisible to the proof:
| Feature | How we use it | Trust role |
|---|---|---|
| Issues | Trigger workflows, collect user input, display results | None — just UX |
| Comments | Message passing (email codes, encrypted submissions) | Transport only — could be any channel |
| Labels | Track state (pending, claimed) | None — bookkeeping |
attest-build-provenance action |
One-line attestation | Convenience wrapper around Sigstore APIs |
| Artifacts | Download proofs and bundles | File hosting — could be any storage |
A verifier checking a ZK proof on-chain never sees issues, comments, or labels. They see: commitSha (primary — pins auditable code), artifactHash, and repoHash (informational). Everything else is scaffolding.
If you're auditing a workflow's trust model, focus on:
- What OIDC claims end up in the Sigstore certificate?
- What does the workflow code at that commit actually do?
- What artifact gets attested?
Ignore: issue formatting, comment templates, label management, notification UX. Those are application code, not trust code.
The circuit hardcodes the Fulcio intermediate CA public key:
- Key: P-384 ECDSA
- Validity: April 2022 - October 2031
- Source: Sigstore TUF root
If Sigstore is compromised, proofs become meaningless. This is the root of trust.
Fulcio issues certificates based on GitHub's OIDC tokens. You trust:
- GitHub correctly identifies the repo/commit in OIDC tokens
- GitHub Actions runs the workflow at that commit
The circuit code extracts claims correctly. This is auditable:
circuits/src/main.nr- Main circuit logic- Uses well-audited libraries (zkpassport's ecdsa, bignum)
This is the verifier's responsibility:
- Does the workflow do what the prover claims?
- Could the prover manipulate the artifact?
- Are dependencies pinned?
A prover could:
- Write a workflow that produces fake artifacts ✓ (verifier must audit)
- Modify their workflow after getting a proof ✗ (commit is pinned)
- Claim someone else's repo ✗ (they can't get Sigstore to sign it)
- Create a proof with fake claims ✗ (cryptographic verification)
If a prover's repo is compromised:
- Attacker could run a malicious workflow
- Proof would be valid for the malicious commit
- Verifier should check commit history / timing
If Fulcio is compromised:
- Attacker could get certs for any repo/commit
- All proofs become untrustworthy
- This is the weakest link (but Sigstore has strong security practices)
commitSha- Always revealed (primary: pins the exact code that ran)artifactHash- Always revealedrepoHash- Hash is revealed, but not the repo name directly (informational only)
- Actual repo name (unless prover discloses)
- Full certificate contents
- Attestation payload details
A prover can:
- Keep their repo private (verifier sees only the hash)
- Disclose repo name only to verifiers who need to audit
- Use a dedicated repo for sensitive proofs
The Fulcio intermediate CA expires in 2031. Before then:
- New circuit with updated key
- New verifier contract deployment
- Migration path for existing integrations
Future versions could allow:
- Proving only specific claims
- Multiple workflows in one proof
- Recursive proofs for workflow composition