Skip to content

therealwiki/github-zktls

 
 

Repository files navigation

GitHub as a Trusted Execution Environment

Permissionless credential bridging. Turn any authentication you already have — GitHub account, email, browser session — into a verifiable on-chain claim. No oracles, no MPC, no cooperation from the original service.

GitHub Actions runs your code in an isolated VM, and Sigstore signs an attestation binding the output to the exact commit SHA. A ZK proof compresses this for on-chain verification at ~3M gas. Because attestations bind to commit SHA (not repo ownership), anyone can fork a workflow and produce valid proofs — deployment is permissionless in the same sense as smart contracts.


Try It: Base Sepolia Faucet

Claims Users ETH

Faucet contract: 0x72cd70d28284dD215257f73e1C5aD8e28847215B

Claim Base Sepolia testnet ETH by proving you have a GitHub account.

Option A: GitHub Web UI (easiest)

  1. Fork this repo — Click "Fork" at the top of this page
    • Important: Uncheck "Copy the master branch only" to include tags
  2. Switch to the release tag — In your fork, click the branch dropdown → "Tags" → select v1.0.3
  3. Go to Actions — Click the "Actions" tab
  4. Run the workflow — Click "GitHub Identity" → "Run workflow"
    • Important: Select v1.0.3 tag from the "Use workflow from" dropdown
    • Enter your ETH address (see below if you need one)
    • Leave "Generate ZK proof" checked (default)
    • Click "Run workflow" — takes ~1 min
  5. Download the artifact — Click the completed run → download identity-proof
  6. Claim your ETH:
    • No gas? Open an issue titled [CLAIM] and paste the contents of claim.json in a ```json code block. We'll relay it for you.
    • Have gas? Submit directly with cast send (see below)

Why the tag? The faucet contract verifies the exact commit SHA that produced your proof. Running from v1.0.3 ensures your proof matches the expected commit.

Option B: Command Line

# Fork and clone
gh repo fork amiller/github-zktls --clone
cd github-zktls

# Switch to the release tag
git checkout v1.0.3

# Run the workflow from the tag (proof generated in Actions)
gh workflow run github-identity.yml --ref v1.0.3 -f recipient_address=0xYOUR_ADDRESS

# Wait for completion, then download
gh run watch
gh run download -n identity-proof

Submit your claim (pick one):

cd identity-proof

# Option 1: Submit directly with cast (no relay needed)
cast send 0x72cd70d28284dD215257f73e1C5aD8e28847215B \
  "claim(bytes,bytes32[],bytes,string,address)" \
  "$(jq -r .proof claim.json)" \
  "[$(jq -r '.inputs | join(",")' claim.json)]" \
  "$(jq -r .certificateHex claim.json)" \
  "$(jq -r .username claim.json)" \
  "$(jq -r .recipient claim.json)" \
  --rpc-url https://sepolia.base.org --gas-limit 3500000 --private-key $YOUR_KEY

# Option 2: Gasless relay — open an issue titled [CLAIM] and paste claim.json

Prefer local proof generation? Uncheck "Generate ZK proof" when running the workflow, then:

gh run download -n identity-proof
cd identity-proof
gh attestation download certificate.json -o .
mv *.jsonl bundle.json
docker run --rm -v $(pwd):/work ghcr.io/amiller/zkproof generate /work/bundle.json /work
# claim.json now has everything — submit with cast send as above

Need an ETH address?

Install Rabby or Rainbow wallet. They'll generate an address for you and keep your keys safe. For testnet, any address works—you just need somewhere to receive the ETH.


How It Works

GitHub Actions provides isolation (fresh VM per run), transparency (workflow code visible at commit SHA), and attestation (Sigstore signs what ran via OIDC). The attestation binds three things:

  • Commit — exact code version that ran (primary — pins auditable, immutable code)
  • Artifact — hash of the workflow output
  • Repository — which repo triggered the workflow (informational — prover controls their repo)

Fetching the workflow at that commit SHA tells you exactly what executed. No councils, no signing keys, no approval processes — the trust anchor is GitHub itself. The security value is not "trust nobody" but "don't trust the application developer": once deployed, the author has no privileged role. Anyone can fork the repo, run the same workflow, and produce valid attestations.

Trust vs convenience: The TEE property comes from runner isolation + Sigstore attestations. Some examples also use GitHub issues, comments, and labels — those are application UX, not part of the trust model. See trust model for details.

Workflow Templates

Template Proves Uses Browser
github-identity.yml GitHub account ownership No
email-challenge.yml + email-verify.yml Email ownership No
tweet-capture.yml Tweet authorship Yes
file-hash.yml File contents at commit No
sealed-box.yml Multi-attestation sealed box No

See workflow-templates/ for ready-to-use templates and examples/workflows/ for more examples including Twitter profile, GitHub contributions, and PayPal balance proofs.


GitHub as zkTLS

Proving what a website showed you typically requires MPC-TLS (DECO, TLSNotary — interactive coordination during the TLS session), a trusted proxy (Reclaim Protocol), or a hardware TEE oracle (Town Crier). GitHub gives you a simpler path: single-party trust (GitHub), no coordination, no specialized hardware. The tradeoff is weaker isolation (software sandboxing vs. hardware memory encryption), but for applications where the threat is developer misbehavior rather than platform compromise, this is the right trust model.

Step What Happens
1. Store credentials Add session cookies as GitHub Secrets
2. Run browser container Headless Chromium starts in the runner
3. Inject session Cookies injected via Chrome extension bridge
4. Capture proof Screenshot + page data extracted
5. Sigstore attests Proof that this workflow produced this output

The trust model: GitHub sees your session, but only runs the code you committed. Anyone can audit the workflow. The attestation binds the result to the exact code version.

Browser Container

The browser-container/ runs headless Chromium with a bridge API:

# Inject session cookies
curl -X POST http://localhost:3000/session \
  -d '{"cookies": [{"name": "auth_token", "value": "...", "domain": ".twitter.com"}]}'

# Navigate and capture
curl -X POST http://localhost:3000/navigate -d '{"url": "https://twitter.com/home"}'
curl -X POST http://localhost:3000/capture

# Get artifacts (screenshot + page data)
curl http://localhost:3000/artifacts

A Chrome extension inside the container handles cookie injection and page capture. The bridge API lets workflows orchestrate the browser without touching credentials directly.

Example: Prove Twitter Identity

# .github/workflows/twitter-proof.yml
- name: Start browser container
  run: docker compose up -d browser

- name: Inject session and capture profile
  env:
    TWITTER_SESSION: ${{ secrets.TWITTER_SESSION }}
  run: |
    # Inject cookies (parsed from session string)
    curl -X POST http://localhost:3000/session -d "$TWITTER_SESSION"

    # Get logged-in username
    USERNAME=$(curl -s http://localhost:3000/twitter/me | jq -r .username)

    # Generate certificate
    echo '{"twitter_username": "'$USERNAME'"}' > certificate.json

The artifact contains the tweet content + proof you authored it. No one else can see your session cookies.


Gas-Efficient On-Chain Verification

Raw Sigstore attestations are ~4KB of JSON + certificates. Verifying on-chain would cost millions of gas. The ZK circuit compresses this:

The circuit verifies:

  1. Certificate chain — Sigstore intermediate CA signed the leaf cert (P-384 ECDSA)
  2. Attestation signature — Leaf cert signed the DSSE envelope (P-256 ECDSA)
  3. Claim extraction — Extracts repo hash, commit SHA, artifact hash
Metric Value
Proof generation 28s (ubuntu-latest runner)
Peak RAM 1.5 GB
Proof size 10,560 bytes
On-chain verification ~3M gas

Verification is a single contract call.

ISigstoreVerifier.Attestation memory att = verifier.verifyAndDecode(proof, inputs);
// att.commitSha    — 20-byte git commit (primary: pins auditable code)
// att.artifactHash — SHA-256 of workflow output
// att.repoHash     — SHA-256 of "owner/repo" (informational: prover controls their repo)

On-Chain Verifier

Base Sepolia: 0xbD08fd15E893094Ad3191fdA0276Ac880d0FA3e1

import {ISigstoreVerifier} from "./ISigstoreVerifier.sol";

contract MyApp {
    ISigstoreVerifier verifier = ISigstoreVerifier(0xbD08fd15E893094Ad3191fdA0276Ac880d0FA3e1);

    function claimReward(bytes calldata proof, bytes32[] calldata inputs) external {
        ISigstoreVerifier.Attestation memory att = verifier.verifyAndDecode(proof, inputs);
        require(att.commitSha == EXPECTED_COMMIT, "Wrong commit");
        // ... your logic
    }
}

Try It: Email Identity NFT

Prove you own an email address — no GitHub account, no wallet signing, no Docker needed. A notary sends you a challenge code by email; you share it back; an NFT gets minted.

EmailNFT: 0x720000d8999423e3c47d9dd0c22f33ab3a93534b (Base Sepolia) — view all minted NFTs

How to claim

  1. Open an issue with title [EMAIL] claim NFT and body:
    email: your@email.com
    recipient: 0xYourEthAddress
    
  2. Check your email for a 64-character hex code
  3. Comment the code on the issue (anyone can do this — the claimer doesn't need a GitHub account)

The workflow verifies the code, generates a ZK proof, and mints an ERC-721 with an on-chain SVG showing the verified email. One NFT per email, fully on-chain.

Anyone can run their own notary — just fork this repo and add AWS SES credentials. See docs/email-login.md for the full walkthrough and trust model.


Quick Start

Generate a Proof

# Run workflow in your fork (proof generated automatically)
gh workflow run github-identity.yml -f recipient_address=0xYOUR_ADDRESS

# Wait and download
gh run watch && gh run download -n identity-proof

The artifact contains claim.json ready for submission.

Verify On-Chain

cast call 0xbD08fd15E893094Ad3191fdA0276Ac880d0FA3e1 \
  "verifyAndDecode(bytes,bytes32[])" \
  "$(cat identity-proof/proof.hex)" "$(cat identity-proof/inputs.json)" \
  --rpc-url https://sepolia.base.org

Build Your Own App

ISigstoreVerifier.Attestation memory att = verifier.verifyAndDecode(proof, inputs);
// Use att.commitSha (primary), att.artifactHash, att.repoHash (optional)

For Agents

Quick Start: Claim Testnet ETH

# One command — note the --ref flag is critical
gh workflow run github-identity.yml --ref v1.0.3 \
  -f recipient_address=0xYOUR_ADDRESS \
  -f faucet_address=0x72cd70d28284dD215257f73e1C5aD8e28847215B

# Wait, download, submit via issue relay (gasless)
gh run watch && gh run download -n identity-proof
gh issue create --title "[CLAIM]" --body '```json'"
$(cat identity-proof/claim.json)"'
```'

Common errors and fixes:

Error Cause Fix
WrongCommit Workflow ran from wrong ref Add --ref v1.0.3
AlreadyClaimedToday 24h cooldown per username Wait and retry tomorrow
FaucetEmpty No ETH left Deposit to 0x72cd70d...
CertificateMismatch Certificate bytes were modified Use claim.json as-is from the artifact

Trustless Escrow

Post a bounty, get verifiable work, pay automatically.

The pattern: One agent posts a bounty with a prompt. Another agent forks the repo, does the work, runs a self-judging workflow where Claude evaluates the diff, and claims the bounty with a ZK proof.

# Worker claims bounty after Claude approves their diff
cast send $ESCROW "claim(uint256,bytes,bytes32[],bytes)" ...

No external judge needed. Claude runs inside GitHub Actions—the worker triggers it but can't fake the response.

See ESCROW.md for the full skill file, or examples/self-judging-bounty/ for a worked example.


Advanced Patterns

Sealed Box

GitHub Actions workflows are ephemeral — secrets generated during a run are lost when it terminates. The sealed-box pattern shows how to build persistent services from ephemeral runners: the runner generates an RSA keypair, attests the public key mid-execution, accepts encrypted submissions, decrypts them, and attests the results. Both attestations share the same run_id, proving the entire lifecycle happened in one execution context.

# One command: dispatch, encrypt, submit, verify
./examples/sealed-box/sealed-box.sh "my secret message"

No external binaries — uses openssl for RSA-OAEP encryption. See docs/sealed-box.md for the pattern and trust model.

Cross-Attestation GroupAuth

GitHub runners and Dstack TEEs as equal peers in a shared group. Different attestation systems — Sigstore (GitHub) and KMS signature chains (Dstack) — registering on the same contract and onboarding each other.

GroupAuth (Base mainnet): 0xdd29de730b99b876f21f3ab5dafba6711ff2c6ac

A group of agents needs to share a secret (an API key, a decryption key, a signing credential). New members prove their identity through whatever attestation system they have — GitHub proves via ZK-verified Sigstore attestations, Dstack TEEs prove via KMS signature chains — and existing members onboard them.

GitHub Runner A                    Dstack TEE (Phala Cloud)
  │                                  │
  │ registerGitHub(proof, inputs)    │ registerDstack(codeId, kmsProof)
  │──────────────┐  ┌────────────────│
  │              ▼  ▼                │
  │      ┌────────────────┐          │
  │      │  GroupAuth.sol  │          │
  │      │                 │          │
  │      │  allowedCode[]  │◄─ owner adds commit SHAs + app IDs
  │      │  members[]      │          │
  │      │  onboarding[]   │          │
  │      └────────────────┘          │
  │              │                   │
  │    MemberRegistered event        │
  │              │                   │
  │              └──────────────────►│ detects event
  │                                  │ onboard(myId, newId, encryptedSecret)
  │                                  │
  │◄─── getOnboarding(myId) ────────│
  │  "groupauth-demo-secret-v1"      │

The TEE agent runs 24/7 on Phala Cloud, watching for new members and automatically posting the group secret. GitHub runners are ephemeral — they register, receive their onboarding message, and exit.

A Dstack TEE agent is running now:

Both registration paths end at the same _register(codeId, pubkey):

Path Proof type codeId
registerGitHub ZK proof of Sigstore attestation bytes32(bytes20(commitSha))
registerDstack KMS signature chain (app→KMS→root) bytes32(bytes20(appId))

The faucet and email NFT are oracle-style — one-shot attestations that trigger on-chain state changes. GroupAuth is a coprocessor — on-chain logic mediates off-chain execution, with the blockchain as the coordination layer and ground truth for rollback protection.

GroupAuth treats attestation as a pluggable interface — if you can prove you ran approved code, you're in. The same on-chain registry accepts Sigstore ZK proofs (GitHub), KMS signature chains (Dstack), and could accept hardware TEE attestations (SGX, Nitro). This enables:

  • Hybrid networks — TEEs for always-on services, GitHub runners for batch jobs
  • Cross-cloud groups — members from different TEE vendors joining the same group
  • Incremental trust — start with GitHub Actions (free, auditable), graduate members to hardware TEEs

See contracts/examples/GroupAuth.sol for the contract and docs/groupauth-deployment.md for deployment details.


Trust Model

What the proof guarantees:

  • ✓ Valid Sigstore certificate chain (hardcoded intermediate CA)
  • ✓ Correct signature verification (P-256 + P-384 ECDSA in-circuit)
  • ✓ Immutable binding: repo × commit × artifact

What contracts can verify:

  • commitSha — pin to the exact workflow version (primary check — auditable, immutable)
  • sha256(certificate) == artifactHash — the certificate wasn't tampered with
  • Certificate contents — extract claims like github_actor for per-user logic
  • repoHash — optional repo filter (informational — prover controls their repo)

The pattern: The workflow outputs a structured certificate. The contract verifies the certificate matches the attested artifact hash, then parses it to extract claims. No circuit changes needed for new claim types.

See docs/trust-model.md for details.


Repository Structure

├── zk-proof/                 # ZK proving system
│   ├── circuits/             # Noir circuit (P-256 + P-384 verification)
│   ├── js/                   # Witness generator
│   └── Dockerfile            # One-command proof generation
│
├── browser-container/        # Headless browser for authenticated proofs
│   ├── bridge.js             # HTTP API for browser control
│   ├── proof-extension/      # Chrome extension for capture
│   └── docker-compose.yml    # Container orchestration
│
├── contracts/                # On-chain verification
│   ├── src/
│   │   ├── ISigstoreVerifier.sol   # Interface
│   │   ├── SigstoreVerifier.sol    # Implementation
│   │   └── HonkVerifier.sol        # Generated verifier
│   └── examples/
│       ├── GitHubFaucet.sol        # Faucet demo
│       ├── EmailNFT.sol            # Email identity NFT
│       ├── GroupAuth.sol           # Cross-attestation group membership
│       ├── SimpleEscrow.sol        # Basic bounty
│       └── SelfJudgingEscrow.sol   # AI-judged bounty
│
├── examples/sealed-box/         # Multi-attestation sealed box
│   ├── sealed-box.sh            # Full CLI orchestration
│   ├── submit.sh                # Standalone submit helper
│   └── verify-linkage.sh        # Verify attestation linkage
│
├── workflow-templates/       # Ready-to-fork workflows
│   ├── github-identity.yml   # Prove GitHub account
│   ├── tweet-capture.yml     # Prove tweet authorship
│   └── file-hash.yml         # Prove file contents
│
└── docs/
    ├── faucet.md             # Faucet demo walkthrough
    ├── email-login.md        # Email NFT walkthrough
    ├── trust-model.md        # What the proof guarantees
    └── auditing-workflows.md # Guide for verifiers

Links

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Solidity 63.6%
  • Python 10.0%
  • TypeScript 10.0%
  • JavaScript 8.4%
  • Noir 3.2%
  • Dockerfile 2.6%
  • Other 2.2%