A self-verifying remote attestation implementation using wasm verification components + Groth16 zero-knowledge proofs, decoupling the verifier from TEE platforms.
unified-attestation solves the coupling problem in remote attestation where "the verifier must embed platform-specific code for each TEE." Platform-specific evidence verification logic is encapsulated into wasm components and uploaded by the attester alongside evidence. The verifier only performs sha256 whitelist validation, wasmtime sandbox invocation, policy comparison, and EAR issuance. TEE platform upgrades only require replacing the component and updating the whitelist — the verifier binary needs no re-release.
Supports six TEE platform paths: mock / CCA / CSV / TDX / iTrustee / VirtCCA, with five hydra zero-knowledge proof stacking paths (cca-hydra / csv-hydra / tdx-hydra / itrustee-hydra / virtcca-hydra) for a total of 11 tee_type values. The hydra layer proves device identity within a whitelist without revealing the specific index.
Four core mechanisms (detailed discussion in docs/en/design.md):
- Verifier decoupled from TEE platforms: Each TEE's evidence verification logic is encapsulated into a wasm component. Platform upgrades only require component replacement + sha256 whitelist updates.
- Challenge-proof cryptographic binding: The challenge nonce directly enters Groth16 public inputs or TEE report_data. The proof and nonce are cryptographically coupled — replay attacks have no window.
- Zero-knowledge device identity proofs: Shrubs accumulator + Merkle path prove a device is in the whitelist without exposing the specific index.
- Three independent trust anchors: Component whitelist + nonce binding + trusted root list. The three layers are independent of each other.
- EAR self-containment: EAR is an ES256 JWT that can be independently verified by any third party holding the public key. The verifier is just the issuer.
Dependencies:
- Rust 1.90.0 (see
rust-toolchain.toml) cargo install cargo-component --locked(for building wasm appraisers)rustup target add wasm32-wasip1openssl(for generating ES256 key pairs)- CCA / CSV / TDX / iTrustee / VirtCCA evidence collection depends on guest-components (
attestation-agent+api-server-rest)
Build:
# 1. Generate EAR signing key pair into config/keys/
bash scripts/gen-keys.sh
# 2. Build all wasm appraisers
bash scripts/build-appraisers.sh
# 3. Build host binaries
cargo build --release -p verifier -p attester -p relying-partyconfig/keys/ is generated by scripts and gitignored.
Single-machine end-to-end smoke test — verifier / attester / relying-party run the full flow locally:
./scripts/run-mvp.shThe mock path does not perform real hardware verification; it only tests the host ↔ wasm pipeline.
sequenceDiagram
autonumber
participant RP as relying-party
participant At as attester
participant V as verifier
participant W as wasm appraiser<br/>(mock)
RP->>RP: generate 32B random nonce
RP->>At: GetEvidence { tee_type=mock, nonce }
Note over At: evidence = { nonce } (no TEE call)
At-->>RP: { evidence, wasm_component }
RP->>V: Verify { tee_type=mock, nonce, evidence, wasm_component }
V->>V: verify sha256 whitelist
V->>W: evaluate(evidence, expected_report_data=nonce)
W-->>V: claims { verification: passed }
V-->>RP: EAR JWT (ES256)
RP->>RP: verify EAR signature locally + compare eat_nonce
The above is the background-check flow. In passport mode (--mode passport), the attester generates its own nonce and calls the verifier internally; the RP receives the EAR directly. See docs/en/operations.md.
End-to-end steps for each TEE require corresponding hardware. Command references and configuration in per-path docs:
| Path | Doc | Hardware / Dependency |
|---|---|---|
| CCA | docs/en/cca.md | ARM CCA + guest-components |
| CSV | docs/en/csv.md | Hygon CSV + AA + HSK/CEK cache or KDS |
| TDX | docs/en/tdx.md | Intel TDX + AA + PCS / PCCS reachable |
| iTrustee | docs/en/itrustee.md | iTrustee TEE + libteeverifier.so |
| VirtCCA | docs/en/virtcca.md | VirtCCA TEE + libvccaattestation.so |
Passport mode with tee_type = <path>-hydra enables hydra stacking — see below.
Hydra uses a long-lived TCP channel independent of gRPC, separate from wasm evidence verification. All three peers must stay running with persistent connections; the verifier batches for 120 seconds, updates the shrubs tree, and broadcasts a PublicContext (containing the latest root + verifier public key) to the attester and relying-party.
sequenceDiagram
autonumber
participant A as Attester<br/>(TCP client task)
participant V as Verifier<br/>(TCP daemon :7001)
participant R as Relying Party<br/>(TCP listener :7002)
A->>V: SignedDeviceClientInfor (long-lived)
Note over V: batch 120s → update shrubs tree
V-->>A: encrypted ResponseDeviceInfor + PublicContext
V-->>R: PublicContext (plaintext)
Note over A: save dev_config.bin / public_context.bin locally
A->>R: EvidenceReply + attester signature
R-->>A: verification result (success / failed)
The hydra flow can run as a single shot or split into two steps:
# Step 1: long-lived daemon — bootstraps the hydra session
# (auto-generates session dir + dev_config.bin + public_context.bin)
attester --config config/attester.toml
# Step 2: build an EvidenceReply from the latest session and ship it
attester --config config/attester.toml hydra-evidence --rp 127.0.0.1:7002
# With an explicit session directory
attester --config config/attester.toml hydra-evidence \
--session workspace-data/attester/attester-runs/attester-xxx \
--rp 127.0.0.1:7002
# Omitting --rp falls back to [hydra] relying_party_addrsTo disable hydra, omit the [hydra] section from the attester config and set [hydra] relying_party_addrs = [] in the verifier config.
See docs/en/hydra.md for details.
| Service | Method | Caller | Description |
|---|---|---|---|
AttesterService |
GetEvidence |
RP → attester | Push nonce, receive evidence (EAR directly in passport mode) |
VerifierService |
Verify |
RP / attester → verifier | Submit evidence, receive EAR |
Full message field definitions in protos/attestation.proto. Call flow and EAR claims: docs/en/protocol.md.
- verifier listens on
[hydra].listen(default127.0.0.1:7001) - relying-party listens on
--hydra-listen(recommended127.0.0.1:7002) - attester connects to verifier via
[hydra].verifier_addr
Frame format and message types: docs/en/hydra.md.
All verifier / attester / relying-party configuration keys: docs/en/config.md.
The RP generates a nonce, calls the attester and verifier separately, then verifies the EAR locally and compares eat_nonce.
flowchart TB
RP["Relying Party<br/>generates nonce / verifies EAR locally"]
subgraph Attester["Attester Host"]
A_Svc[attester gRPC]
A_AA[attestation-agent<br/>guest-components]
A_TEE{{TEE Hardware<br/>CCA / CSV / TDX<br/>iTrustee / VirtCCA}}
A_Svc --> A_AA --> A_TEE
end
subgraph Verifier["Verifier Host (platform-agnostic)"]
V_Svc[gRPC handler]
V_Reg[(Component Whitelist<br/>sha256)]
V_Host[Host verification<br/>CCA / CSV<br/>+ TDX collateral fetch]
V_WT[wasmtime sandbox]
V_Pol[Policy matching + EAR issuance]
V_Svc --> V_Reg
V_Svc --> V_Host
V_Svc --> V_WT --> V_Pol
end
PCS[(Intel PCS / PCCS)]
KDS[(Hygon KDS)]
RP -- "1. GetEvidence(nonce)" --> A_Svc
A_Svc -- "2. evidence + wasm" --> RP
RP -- "3. Verify(nonce, evidence, wasm)" --> V_Svc
V_Host -. "TDX collateral" .-> PCS
V_Host -. "CSV HSK/CEK" .-> KDS
V_Pol -- "4. EAR JWT" --> RP
flowchart TB
RP["Relying Party<br/>verifies EAR locally (iat freshness)"]
RP_HL["Relying Party<br/>TCP :7002<br/>receives PublicContext + EvidenceReply"]
subgraph Attester["Attester Host"]
A_Svc[attester gRPC]
A_AA[attestation-agent]
A_TEE{{TEE Hardware}}
A_HC[hydra TCP client task]
A_Svc --> A_AA --> A_TEE
A_Svc --> A_HC
end
subgraph Verifier["Verifier Host"]
V_Svc[gRPC handler + wasm sandbox]
V_HD[hydra TCP daemon<br/>:7001<br/>batch 120s]
end
RP -- "1. GetEvidence(mode=PASSPORT)" --> A_Svc
A_Svc -- "2. Verify(nonce, evidence, wasm)" --> V_Svc
V_Svc -- "3. EAR JWT" --> A_Svc
A_Svc -- "4. EAR" --> RP
A_HC -- "SignedDeviceClientInfor" --> V_HD
V_HD -- "encrypted ResponseDeviceInfor + PublicContext" --> A_HC
V_HD -- "PublicContext (plaintext)" --> RP_HL
A_HC -- "EvidenceReply + sig" --> RP_HL
unified-attestation/
├── protos/ gRPC services and messages (attestation.proto, tonic-build)
├── verifier/ Platform-agnostic verifier host (wasmtime runtime + CCA/CSV host verification + hydra TCP daemon)
├── attester/ Evidence collection + wasm component delivery + hydra TCP client task
├── relying-party/ RP client + EAR JWT verification + hydra TCP listener
├── hydra-toolkit/ Shared hydra core: Groth16 circuit, shrubs accumulator, wire codecs, AES-GCM encryption, TCP framing
├── appraisers/ Wasm verification components (built with cargo-component)
│ ├── wit/verifier.wit WIT interface definition
│ ├── mock/ Skeleton appraiser
│ ├── cca/ + cca-hydra/ ARM CCA (host verification)
│ ├── csv/ + csv-hydra/ Hygon CSV (host verification)
│ ├── tdx/ + tdx-hydra/ Intel TDX (full-chain verification in wasm)
│ ├── itrustee/ + itrustee-hydra/ iTrustee (host extraction)
│ └── virtcca/ + virtcca-hydra/ VirtCCA (host extraction)
├── config/ Configuration templates
├── scripts/ Build and one-shot scripts
└── docs/ Detailed documentation (by TEE / topic)
├── en/ English documentation
└── zh/ Chinese documentation
*-hydra appraisers are equivalent to their non-hydra counterparts; only the tee_type claim differs. Hydra zero-knowledge proof verification is handled by the verifier's separate hydra TCP daemon, independent of wasm.
| Topic | Doc |
|---|---|
| Design rationale (four core mechanisms) | docs/en/design.md |
| Protocol layer (gRPC services, messages, EAR format, Hydra frames) | docs/en/protocol.md |
| Configuration reference (all verifier / attester keys) | docs/en/config.md |
| Operations manual (build, scripts, tool commands) | docs/en/operations.md |
| CCA / CCA + hydra path | docs/en/cca.md |
| Hygon CSV / CSV + hydra path | docs/en/csv.md |
| TDX / TDX + hydra path | docs/en/tdx.md |
| iTrustee / iTrustee + hydra path | docs/en/itrustee.md |
| VirtCCA / VirtCCA + hydra path | docs/en/virtcca.md |
| hydra sub-module (circuit, shrubs, TCP channel, two-step commands) | docs/en/hydra.md |