Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/tinfoil-leg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Tinfoil leg E2E

# Runs the TinfoilAdapter side of the bridge: real tinfoil-go SEV-SNP
# verification of three targets (vendored vector, atc.tinfoil.sh managed
# inference, and a Tinfoil-Containers third-party deploy), then exercises
# the full TEEBridge membership + ECIES onboarding flow.
#
# Target C is enabled when TINFOIL_CONTAINER_HOST is set in the workflow env
# (or as a workflow_dispatch input). Without it, the test runs A and B only.

on:
workflow_dispatch:
inputs:
container_host:
description: "Tinfoil-Containers host to verify (e.g. <name>.<org>.containers.tinfoil.dev). Empty = skip target C."
required: false
default: ""
schedule:
- cron: '23 14 * * *' # daily, after the awesome-private-inference probe
push:
branches: [main, wip/tinfoil-tee-proof]
paths:
- 'contracts/TinfoilAdapter.sol'
- 'tools/tinfoil-verify-helper/**'
- 'lib/tinfoil-go/**'
- 'test_e2e_bridge_tinfoil_proof.py'
- '.github/workflows/tinfoil-leg.yml'

permissions:
contents: read

jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 12
env:
TINFOIL_CONTAINER_HOST: ${{ inputs.container_host || '' }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-go@v5
with:
go-version: '1.22'

- uses: foundry-rs/foundry-toolchain@v1
with:
version: stable

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Python deps
run: pip install -r requirements.txt

- name: Build tinfoil-verify helper
working-directory: tools/tinfoil-verify-helper
run: go build -o tinfoil-verify ./...

- name: forge build
run: forge build

- name: Smoke-check target C reachability
if: env.TINFOIL_CONTAINER_HOST != ''
run: |
set -e
curl -sSk --max-time 10 "https://${TINFOIL_CONTAINER_HOST}/.well-known/tinfoil-attestation" \
| head -c 200
echo
./tools/tinfoil-verify-helper/tinfoil-verify \
--source host --host "$TINFOIL_CONTAINER_HOST"

- name: Run E2E
run: python3 test_e2e_bridge_tinfoil_proof.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ cache/
lib/
.env
__pycache__/
tools/tinfoil-verify-helper/tinfoil-verify
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/tinfoil-go"]
path = lib/tinfoil-go
url = https://github.com/tinfoilsh/tinfoil-go
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ CVM-A (dstack, Phala) ──onboard(encrypted)──▶ TEEBridge ──▶
| **Marlin Oyster** | **Reference exists** | [Oyster contracts](https://github.com/marlinprotocol/oyster-contracts) | AWS Nitro (PCR0/1/2) | `AttestationVerifier.sol` on Arbitrum Sepolia. Clean pattern: verify once, whitelist enclave pubkey | ~TBD |
| **Lit Protocol** | **Not started** | — | AMD SEV-SNP | Own attestation service on Chronicle L2. No reusable EVM verifier | N/A |
| **Google Confidential Space** | **Not started** | — | SEV-SNP or TDX (via vTPM) | Centralized Google Cloud Attestation API only. Could extract raw reports and use Automata verifiers | N/A |
| **Tinfoil** | **Adapter written + verifier exercised** | `TinfoilAdapter.sol` ([tinfoil-go](https://github.com/tinfoilsh/tinfoil-go) vendored under `lib/tinfoil-go`); test invokes `tinfoil-go` directly for real SEV-SNP verification of vendored vector + live `atc.tinfoil.sh` fetch | AMD SEV-SNP + Intel TDX (CPU), NVIDIA H100/H200/B200 (GPU). Code identity bound to GitHub release via Sigstore + dm-verity "attested disk" (Modelwrap). No sim mode upstream | **TEE Proof pattern (ERC-733 §C)**: heavy verification offloaded to an off-chain `tinfoil-go-verifier` CVM, itself attested by `DstackVerifier` and registered as a bridge member. Adapter checks signer is a registered member with codeId == canonical-verifier image. No admin signer allowlist | ~150K (ecrecover + 1 SLOAD) |
| **Oasis ROFL** | **Not started** | — | Runtime-verified, `bytes21` app ID | Sapphire precompile only. Cross-chain needs attestation bridging | N/A |
| **ARM CCA** | **Not started** | — | CCA token (COSE, EAT) | No known on-chain verifier. Similar to Nitro — P-256/P-384 + CBOR | TBD |

Expand Down
113 changes: 113 additions & 0 deletions contracts/TinfoilAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

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

interface ITEEBridgeView {
function getMember(bytes32 memberId) external view returns (
bytes32 codeId, address verifier, bytes memory pubkey, bytes memory userData, uint256 registeredAt
);
}

/// @title TinfoilAdapter (TEE Proof)
/// @notice Implements the "TEE Proof" pattern from ERC-733 §C: heavy attestation
/// verification (SEV-SNP / TDX cert chains, Sigstore bundle, dm-verity
/// root) is offloaded to an off-chain TEE running tinfoil-go. That TEE
/// signs the verified envelope with its encumbered secp256k1 key.
///
/// The signer's trustworthiness is rooted in TEEBridge itself: the
/// signer must already be a registered member whose codeId matches the
/// canonical tinfoil-go-verifier image. This composes the bridge with
/// itself — DstackVerifier (or any IVerifier) bootstraps the verifier
/// enclave; that enclave then bootstraps Tinfoil-attested targets.
///
/// Trust assumption: blockchain + TEE (per ERC-733 §C). Cost:
/// single ecrecover + one storage read (~150K gas).
contract TinfoilAdapter is IVerifier {
ITEEBridgeView public immutable bridge;
bytes32 public immutable verifierCodeId;

/// @notice Tinfoil attestation envelope, signed by an off-chain TEE running tinfoil-go.
struct TinfoilProof {
bytes32 codeId;
bytes32 sigstoreDigest;
bytes32 dmVerityRoot;
bytes derivedCompressedPubkey;
bytes userData;
string domain;
bytes signerCompressedPubkey;
bytes signerSig;
}

error VerifierNotRegistered();
error VerifierWrongCode();
error InvalidTinfoilSignature();

constructor(address _bridge, bytes32 _verifierCodeId) {
bridge = ITEEBridgeView(_bridge);
verifierCodeId = _verifierCodeId;
}

function verify(bytes calldata proof) external view override returns (bytes32 codeId, bytes memory pubkey, bytes memory userData) {
TinfoilProof memory p = abi.decode(proof, (TinfoilProof));

// 1. Reconstruct the signed envelope and recover the signer address
bytes32 envelope = keccak256(abi.encodePacked(
"tinfoil-release:",
p.codeId, p.sigstoreDigest, p.dmVerityRoot,
p.derivedCompressedPubkey, p.userData, bytes(p.domain)
));
bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", envelope));
address recovered = _recoverSigner(ethHash, p.signerSig);

// 2. Bind sig to claimed signer compressed pubkey
if (recovered != _compressedPubkeyToAddress(p.signerCompressedPubkey)) revert InvalidTinfoilSignature();

// 3. Look up signer as a bridge member and require canonical verifier codeId
bytes32 signerMemberId = keccak256(p.signerCompressedPubkey);
(bytes32 signerCodeId, , , , uint256 registeredAt) = bridge.getMember(signerMemberId);
if (registeredAt == 0) revert VerifierNotRegistered();
if (signerCodeId != verifierCodeId) revert VerifierWrongCode();

return (p.codeId, p.derivedCompressedPubkey, p.userData);
}

function verifyAndCache(bytes calldata proof) external override returns (bytes32 codeId, bytes memory pubkey, bytes memory userData) {
return this.verify(proof);
}

function _recoverSigner(bytes32 hash, bytes memory sig) internal pure returns (address) {
require(sig.length == 65, "bad sig len");
bytes32 r; bytes32 s; uint8 v;
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
if (v < 27) v += 27;
return ecrecover(hash, v, r, s);
}

function _compressedPubkeyToAddress(bytes memory pk) internal view returns (address) {
require(pk.length == 33, "need compressed pubkey");
uint8 prefix = uint8(pk[0]);
require(prefix == 0x02 || prefix == 0x03, "invalid prefix");

uint256 x;
assembly { x := mload(add(pk, 33)) }

uint256 p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F;
uint256 y2 = addmod(mulmod(mulmod(x, x, p), x, p), 7, p);
uint256 y = _modExp(y2, (p + 1) / 4, p);
if ((prefix == 0x02 && y % 2 != 0) || (prefix == 0x03 && y % 2 == 0)) y = p - y;

return address(uint160(uint256(keccak256(abi.encodePacked(x, y)))));
}

function _modExp(uint256 base, uint256 exp, uint256 mod) internal view returns (uint256) {
bytes memory input = abi.encodePacked(uint256(32), uint256(32), uint256(32), base, exp, mod);
bytes memory output = new bytes(32);
assembly { if iszero(staticcall(gas(), 0x05, add(input, 32), 192, add(output, 32), 32)) { revert(0, 0) } }
return abi.decode(output, (uint256));
}
}
1 change: 1 addition & 0 deletions lib/tinfoil-go
Submodule tinfoil-go added at 4652fd
138 changes: 138 additions & 0 deletions notes/TINFOIL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Tinfoil interop

[Tinfoil](https://tinfoil.sh) runs containers in TEEs (AMD SEV-SNP, Intel TDX,
NVIDIA H100/H200/B200 confidential compute) and binds the running code to a
GitHub release via Sigstore + dm-verity ("Modelwrap" attested disk). The
canonical client-side verifier is
[`tinfoilsh/tinfoil-go`](https://github.com/tinfoilsh/tinfoil-go), vendored
under `lib/tinfoil-go`. There is **no simulator mode** — verification expects
real SEV-SNP / TDX quotes.

ERC-733 Appendix C defines three on-chain verification optimization patterns
for TEE attestations:

| | On-chain certificate caching | ZK Proof | **TEE Proof** |
|---|---|---|---|
| Trust assumption | Blockchain | Blockchain + ZKVM | **Blockchain + TEE** |
| Security | Most secure | Secure | Least secure |
| Cost | Several M gas | ~250K | **Negligible** |
| Delay | None | ~20s proving | Negligible |

`TinfoilAdapter.sol` implements **TEE Proof** by composing the bridge with
itself.

## Trust chain

```
AMD/Intel root
└─ DstackVerifier verifies a dstack CVM running tinfoil-go-verifier
└─ that CVM's encumbered secp256k1 key is registered as bridge member M
with codeId == verifierCodeId (the canonical tinfoil-go-verifier compose hash)
└─ M off-chain verifies a target Tinfoil enclave's SEV-SNP / TDX quote,
Sigstore bundle, and dm-verity root using tinfoil-go
└─ M signs the verified envelope (codeId, sigstoreDigest, dmVerityRoot,
targetPubkey, userData, domain) with its encumbered key
└─ TinfoilAdapter.verify():
- ecrecover(sig) → addr
- addr == compressedPubkeyToAddress(p.signerCompressedPubkey)
- bridge.getMember(keccak256(p.signerCompressedPubkey)).codeId
== verifierCodeId
- return (target codeId, target pubkey, target userData)
```

No admin signer allowlist. Compromise of the deployer EOA cannot inject
trusted signers — the trust root is whatever `DstackVerifier` (or another
`IVerifier` if you wire one up) accepts as a CVM running the canonical
tinfoil-go-verifier image.

## Trust assumption boundaries

- **AMD/Intel root + dstack KMS root.** Same baseline as `DstackVerifier`.
- **The tinfoil-go-verifier image's correctness.** Whoever builds and publishes
the canonical `verifierCodeId` is implicitly trusted to faithfully implement
the SEV-SNP / TDX / Sigstore / dm-verity checks. This is the "TEE vendor
trustworthy" assumption from ERC-733 §C.
- **Side-channel resistance of the verifier CVM.** Inherent to TEE Proof.

The pattern is structurally weaker than Path B (ZK Proof) and Path C
(certificate caching) — both of which would verify the SEV-SNP report on-chain
directly without trusting any TEE-resident verifier. Those are open work; see
[Roadmap](#roadmap).

## Files

- `contracts/TinfoilAdapter.sol` — TEE Proof adapter (reads `TEEBridge.getMember`)
- `contracts/IVerifier.sol` — ERC-733 verifier interface
- `lib/tinfoil-go/` — vendored Go verifier (the canonical reference for what
the off-chain CVM should compute). Embeds Genoa cert chain
(`verifier/attestation/genoa_cert_chain.pem`), SGX/TDX root
(`sgx_root_ca.pem`), and Sigstore trusted root
(`verifier/client/trusted_root.json`). Real SEV-SNP and TDX test vectors
inline in `verifier/attestation/attestation_test.go`.
- `tools/tinfoil-verify-helper/` — small Go binary that links against the
vendored `tinfoil-go` and invokes `attestation.VerifyAttestationJSON()`.
Sources: `--source vendored-sev` (offline, deterministic),
`--source vendored-tdx` (currently broken — base64 corruption in const,
fixable), `--source live` (fetches `https://atc.tinfoil.sh/attestation`,
the managed-inference bundle path), `--source host --host <hostname>`
(fetches `/.well-known/tinfoil-attestation` directly — the only path that
works for Tinfoil-Containers third-party deploys, which do not publish to
ATC). Build: `cd tools/tinfoil-verify-helper && go build -o tinfoil-verify ./...`
- `test_e2e_bridge_tinfoil_proof.py` — anvil e2e. Invokes the Go helper for
each target (real cryptographic SEV-SNP verification — AMD-signed report
walked back through the Genoa cert chain via `google/go-sev-guest`).
Verification failure aborts before any contract call. The verified
measurement becomes the registered `codeId`; the verified HPKE/TLS
fingerprints are recorded in `userData`. Then registers a synthetic
dstack-attested verifier CVM, registers two-or-three Tinfoil targets
through it (target C is the third-party container leg, gated on the
`TINFOIL_CONTAINER_HOST` env var so the test stays runnable without
admin-key access), exchanges ECIES-encrypted secrets across legs,
and asserts three meaningful negatives (signer not registered, signer
wrong codeId, sig/pubkey binding mismatch).
- `.github/workflows/tinfoil-leg.yml` — CI runner that executes the same e2e
daily and on-push, with target C pointed at a live
`*.containers.tinfoil.dev` deploy.
- `test_decode_tinfoil_vectors.py` — confirms the vendored vectors decode to
standard SEV-SNP report layout (REPORT_DATA at 0x50 = TLS FP || HPKE
pubkey, MEASUREMENT at 0x90). Useful as a starting point for Path B/C work.

## What the test actually exercises

| Layer | Real | Mocked |
|---|---|---|
| SEV-SNP cryptographic verification (AMD signature, Genoa cert chain) | ✓ via `tinfoil-go` | |
| Live production Tinfoil attestation (`atc.tinfoil.sh` managed inference) | ✓ fetched + verified | |
| Live Tinfoil-Containers third-party deploy (`*.containers.tinfoil.dev`) | ✓ fetched + verified (when `TINFOIL_CONTAINER_HOST` set) | |
| Sigstore bundle check | | (would need `--source live` with full bundle path) |
| dm-verity root binding | | (set to zeros in test envelopes) |
| Verifier CVM identity (encumbered key in real dstack TEE) | | synthetic `Account.create()` |
| dstack KMS root signature | | synthetic test KMS key |

Going from "synthetic verifier CVM" to "real verifier CVM" means deploying
`tinfoil-go-verifier` as an actual dstack CVM and registering its
`/proof`-endpoint output via `register.py`. The contract code does not change.

## Roadmap

### Path B (ZK Proof)
Wrap Automata's SEV-SNP SP1 → Groth16 verifier
([automata-network/amd-sev-snp-attestation-sdk](https://github.com/automata-network/amd-sev-snp-attestation-sdk)).
~315K gas verifier; ~20s proving. Drives a real on-chain check of the SEV-SNP
report; `TinfoilAdapter` would then assert the verified `MEASUREMENT` matches
the canonical Sigstore-attested release measurement, with the dm-verity root
read from `REPORT_DATA[32:64]`. Replaces the bridge-member-lookup step with a
real cryptographic check.

### Path C (cert caching)
Direct on-chain SEV-SNP P-384 verification + Genoa cert chain caching. Several
M gas. Most expensive but no extra trust assumptions beyond AMD's root CA.
Lowest priority.

### TDX
Same shape, but the report is multi-register (MRTD + RTMR0–3) and the cert
chain is Intel's. Automata's
[automata-dcap-attestation](https://github.com/automata-network/automata-dcap-attestation)
covers it. Would slot in alongside the SEV-SNP path as a second
quote-verification backend, gated by the TDX `Document.Format` predicate type
in the Tinfoil envelope.
47 changes: 47 additions & 0 deletions test_decode_tinfoil_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Decode Tinfoil's vendored test vectors to confirm they're usable as inputs
to a future on-chain SEV-SNP / TDX verifier with parity to tinfoil-go.

Vectors are inline in lib/tinfoil-go/verifier/attestation/attestation_test.go.
Body format: base64(gzip(raw_quote_bytes)).
"""
import base64, gzip

# From TestGuestVerify, SEV-SNP guest v2 case
SEV_BODY_B64 = ("H4sIAAAAAAAA/2JmgAEEixBgZGBg4AKzxEPU0eQETrU6V/UVB3t6X/nzPHnDqkuB7Ge7tj5ZEHio29Wfkc1uX"
"9Sclq9brfxurj5f8/1vsLnEKWGd+VvbrZlW1uopNP7g1X277qF1y53Evj/F31o35j7JULPg0r0S+zF28d3utXt"
"mKJ26X/2ndOpEHVfxXfmrpYMOEO1oGgGNBec2/VR6lX2Gl0OiQHRZX6rfLIn+iuYbKf+jFB4bqZ34TwDAwlFSk"
"BGr+VIfV+XIhzFXsbbMitzRGPOTM8J+9sr3+qxGEkfMP1svbH7yRHSD5eb6JlZVrovx3R0LFq+9+eVA44HyWR5"
"vlUTM+1xg5muYMzKAMIxPxyCiCHQ6e7XWK8xY82mR/JozTx04Vy5l8FSb5PHojvm2wD2bL32f4PhFweCczqKfE"
"gb9gr/XG+Iy57HDxR1FBzhUzT5FZUW/TOHzX/fB7uei0kcHzO5v62TjbzG4Zxh1YsrdgwmpTrsN8vatoq8vRwE"
"uAAgAAP//tiY3daAEAAA=")
EXPECTED_TLS_FP = "10ca85437a8e7353494bd4fce763b0aad25107cd8ab5e4a051c28b454f01063e"
EXPECTED_HPKE = "be5a9c84f5b53a4ed9abcf7cf7fd533718ca132c9fb5873b02a97d2e2081f80d"
EXPECTED_REG = "2dedaee13b84dc618efc73f685b16de46826380a2dd45df15da3dd8badbc9822cadf7bfc7595912c4517ba6fab1b52c0"

raw_gz = base64.b64decode(SEV_BODY_B64)
raw = gzip.decompress(raw_gz)
print(f"SEV-SNP body: {len(raw_gz)}B gzipped -> {len(raw)}B raw quote")
print(f" first 32 bytes: {raw[:32].hex()}")
# SEV-SNP attestation_report struct is 1184 bytes; the v2 wrapper from tinfoil
# embeds the 1184B report + signature + extra context (TLS FP / HPKE pubkey).
# The TLS FP and HPKE pubkey are stored in REPORT_DATA (64 bytes) of the raw report.
# REPORT_DATA is at offset 0x50 in the SEV-SNP report.
# tinfoil-go: 32B TLS pubkey FP || 32B HPKE pubkey
report_data = raw[0x50:0x50+64]
tls_fp = report_data[:32].hex()
hpke = report_data[32:].hex()
print(f" REPORT_DATA[0:32] (TLS FP): {tls_fp}")
print(f" REPORT_DATA[32:64] (HPKE): {hpke}")
assert tls_fp == EXPECTED_TLS_FP, f"TLS FP mismatch: {tls_fp} != {EXPECTED_TLS_FP}"
assert hpke == EXPECTED_HPKE, f"HPKE mismatch: {hpke} != {EXPECTED_HPKE}"
print(" ✓ TLS FP and HPKE match expected from tinfoil-go test")

# SEV-SNP report MEASUREMENT field is at offset 0x90, 48 bytes (SHA-384)
measurement = raw[0x90:0x90+48].hex()
print(f" MEASUREMENT (offset 0x90, 48B): {measurement}")
assert measurement == EXPECTED_REG, f"MEASUREMENT mismatch: {measurement} != {EXPECTED_REG}"
print(" ✓ MEASUREMENT matches the SEV register from tinfoil-go test")

print("\nVectors are usable: raw SEV-SNP attestation_report struct, standard layout.")
print("An on-chain verifier (e.g. Automata SEV-SNP) takes this raw byte blob directly.")
Loading
Loading