Skip to content

Latest commit

 

History

History
121 lines (88 loc) · 5.32 KB

File metadata and controls

121 lines (88 loc) · 5.32 KB

Sealed Box: Mid-Workflow Multi-Attestation

What this demonstrates

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.

The pattern

┌──────────────── 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      │
└───────────────────────────────────────────────────────────────┘

Quick start

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_ID

How issues are used (minimally)

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

Trust model

  1. Verify attestation 1 (pubkey.json) — confirms this pubkey was generated by a specific workflow at a specific commit
  2. Audit the workflow code at that commit — confirm the private key never leaves runner memory
  3. Encrypt with the pubkey — only the runner can decrypt
  4. Verify attestation 2 (result.json) shares the same run_id — proves decryption happened in the same execution
  5. Both attestations are Sigstore-signed — tamper-evident, publicly verifiable

The disposable issue is an implementation detail. Trust comes from the attestations, not the issue.

OIDC fields that link attestations

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

What this enables

  • 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

Limits

  • 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_id match in a ZK circuit) is a follow-up

GitHub workflow dispatch gotcha

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=5

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