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).