GitHub Actions can call actions/attest-build-provenance@v2 multiple times in a single workflow run. All attestations share the same run_id, sha, and job_workflow_ref — proving they came from the same execution context.
This enables interactive attested computation: generate secrets, attest public parts, wait for external input, process it, attest results — all within one trusted execution.
┌──────────────── GitHub Runner (ephemeral VM) ────────────────┐
│ │
│ 1. Generate keypair privkey stays in memory │
│ 2. ATTEST pubkey.json ───► Sigstore signs it │
│ 3. Upload artifact + issue local script can download │
│ 4. Sleep (wait for input) │
│ 5. Collect encrypted msgs from issue comments │
│ 6. Decrypt with privkey only this runner can do this │
│ 7. ATTEST result.json ───► Sigstore signs it │
│ 8. Destroy privkey gone forever │
│ │
│ Both attestations share run_id ──► same execution proof │
└───────────────────────────────────────────────────────────────┘
One command does everything — dispatch, wait, encrypt, submit, verify:
./examples/sealed-box/sealed-box.sh "my secret message"Or step by step:
# 1. Dispatch
gh workflow run sealed-box.yml --ref sealed-box -f window_minutes=5
# 2. Get the run ID
RUN_ID=$(gh run list -w "Sealed Box" -L1 --json databaseId -q '.[0].databaseId')
# 3. Submit (downloads pubkey, verifies attestation, encrypts, posts)
./examples/sealed-box/submit.sh $RUN_ID "my secret message"
# 4. Wait for completion
gh run watch $RUN_ID
# 5. Verify both attestations share the same run_id
./examples/sealed-box/sealed-box.sh --verify $RUN_IDThe workflow auto-creates a disposable issue labeled sealed-box as a message bus for encrypted submissions. It's just data transport — all UX happens in your terminal.
The issue is necessary because a running GitHub Actions job has no other way to receive data from outside. Issue comments provide a public, timestamped, authenticated channel.
After the workflow completes, the issue is closed automatically.
- Verify attestation 1 (pubkey.json) — confirms this pubkey was generated by a specific workflow at a specific commit
- Audit the workflow code at that commit — confirm the private key never leaves runner memory
- Encrypt with the pubkey — only the runner can decrypt
- Verify attestation 2 (result.json) shares the same
run_id— proves decryption happened in the same execution - Both attestations are Sigstore-signed — tamper-evident, publicly verifiable
The disposable issue is an implementation detail. Trust comes from the attestations, not the issue.
| OID | Field | Purpose |
|---|---|---|
| .3 | Commit SHA | Same code |
| .5 | Repository | Same repo |
| .21 | Run URL (contains run_id + attempt) | Same execution |
| .9 | Source repository URI (workflow@ref) | Same workflow file |
- Sealed-bid auctions — bids encrypted to attested pubkey, opened in one execution
- Private input computation — submit encrypted data, get attested results
- Key ceremonies — ephemeral keys with provable lifecycle
- Agent-to-agent channels — one agent posts attested pubkey, another submits encrypted work
- 6 hour max on standard runners (self-hosted: unlimited)
- Sleep-based polling — runner idles during the submission window
- GitHub trust assumption — you trust GitHub's runner isolation
- Off-chain only — on-chain verification of linked attestations (proving
run_idmatch in a ZK circuit) is a follow-up
workflow_dispatch workflows must exist on the default branch (usually master) to be discoverable by the GitHub API and UI. A workflow that only exists on a feature branch returns 404 when dispatched.
The workaround: put a stub on master that registers the trigger, then dispatch with --ref <branch> to run the real version:
# On master: stub that just registers the workflow
name: Sealed Box
on:
workflow_dispatch:
inputs:
window_minutes:
default: '5'
jobs:
stub:
runs-on: ubuntu-latest
steps:
- run: echo "Dispatch from sealed-box branch instead" && exit 1# Dispatches from master (discovery) but runs the sealed-box branch version
gh workflow run "Sealed Box" --ref sealed-box -f window_minutes=5When --ref is specified, GitHub executes the workflow file at that ref, not the master version. The stub is just for registration. This also applies to forks: the faucet example works with --ref v1.0.1 because the workflow exists on master (registered) and at the tag (executed).