TEE Auction Coprocessor: Replay-Safe Attested Auction Receipt with Gramine SGX — Tutorial

TEE Auction Coprocessor: Replay-Safe Attested Auction Receipt with Gramine SGX — Tutorial

12/20/202518 min • cryptography
TEESGXGramineRustAttestationAuctions

TL;DR — This is a tutorial for building a tiny “TEE coprocessor” for a sealed-bid, second-price (Vickrey) auction:

  • auction-core: the trusted computation (auction + canonical bid commitment + statement hashing).
  • enclave-auction: runs inside Gramine; emits a single JSON “receipt” (EnclaveResponse).
  • attestation-verifier: the security referee; recomputes deterministically, enforces replay policy, and (when SGX evidence exists) parses an SGX quote and checks that report_data binds the receipt to the quote bytes (quote authenticity via DCAP/QVL is deferred)..
  • Left for the follow-up: full DCAP collateral/TCB verification (PCS chain, revocation, freshness rules), hardened production manifests, and a secure bid ingress channel (RA-TLS / encrypted bids / sealed files).

Repo: https://github.com/reymom/tee-auction-lab

What this tutorial verifies (and what it deliberately does not)

This tutorial focuses on integrity + replay safety. Confidentiality requires a secure ingress path (e.g., RA-TLS) and is intentionally deferred.

Attestation scope note: In this tutorial, “attested mode” means “a quote is present and parsed, and we check application binding (report_data) and policy allowlists against fields extracted from that quote.” We do not verify quote authenticity (QVL) or DCAP collateral / TCB status here; that end-to-end verification is deferred to a follow-up.

What we verify end-to-end (deterministically, in all modes)

  • Deterministic semantics: the result is exactly run_second_price_auction(bids) for the provided bids.json.
  • Canonical commitment: the verifier recomputes bids_commitment_hex from canonicalized bids and rejects tampering.
  • Binding at the application layer: the receipt includes statement_hash_hex binding (auction_id, nonce, bids_commitment_hex, result).

What we verify in “attested mode”

  • The receipt's statement_hash_hex is bound into the SGX quote via report_data (64B), and the verifier recomputes and checks this binding (binding to quote bytes; quote authenticity / collateral verification is deferred).
  • The verifier enforces an explicit trust policy by allowlisting MRENCLAVE extracted from the quote (full DCAP collateral / TCB verification is deferred).
  • Replay protection is enforced via replay_db.json (reject reused auction_id + nonce per policy).

What we do NOT claim yet (intentionally deferred)

  • Full DCAP chain / TCB validation: we do not validate Intel PCS collateral end-to-end (certificate chain, revocation, TCB status/freshness). This is required for production and is deferred to a follow-up.
  • Production hardening: debug enclaves (sgx.debug = true), hardened manifests, minimal trusted files, and operational hygiene are not the focus here.
  • Confidential bid ingress: bids are provided as a host-side plaintext file (bids.json). A TEE does not magically make plaintext inputs confidential from the host that supplies them.

1. Motivation: why this is a “TEE coprocessor” pattern

A private auction is the smallest non-trivial program where TEEs actually earn their keep:

  • Privacy: bids should remain hidden from the host OS / cloud provider.
  • Integrity: the result should come from the intended binary/config, not a modified one.
  • Statefulness pitfalls: even valid attestations can be replayed unless you design replay protection from day one.

The key design principle is the coprocessor pattern:

The host is a dumb orchestrator. The verifier is the decider.
The enclave produces a small receipt (output + evidence), and the verifier decides what to accept.

2. Repository layout (one trust boundary line)

Current workspace:

tee-auction-lab/
  auction-core/           # pure function + commitments + statement hashing
  enclave-auction/        # emits EnclaveResponse, optionally collects quote
  host-cli/               # generates bids, runs enclave, writes artifacts
  attestation-verifier/   # canonical verifier + replay DB + policy enforcement
  gramine/                # manifest template + build/sign glue
  policy.dev.toml         # dev policy (no evidence required)
  policy.sgx.toml         # SGX policy (evidence required + MRENCLAVE allowlist)
  replay_db.json          # verifier state (replay protection)

One-liner pipeline:

(bidders) -> bids.json --(untrusted host)--> enclave-auction (Gramine/SGX) -> enclave_output.json
                                       |                                         |
                                       +--------------> attestation-verifier <---+
                                                (canonical acceptance)

3. Prerequisites

3.1 Local development (no SGX required)

  • Rust toolchain (stable via rustup)
  • A working Linux environment for Gramine direct mode (optional, but recommended for parity)

3.2 SGX VM deployment (to reproduce the attested-mode run)

  • An SGX-enabled environment with driver support (e.g. Azure Confidential VMs)
  • Gramine built with SGX + DCAP support
  • Presence of SGX devices (/dev/sgx_enclave, /dev/sgx_provision) and Gramine attestation files (/dev/attestation/* inside the enclave)

Deployment references (for later):

3.3 Azure SGX VM: exact machine + sanity checks (DCsv3 / West Europe)

For the “attested mode” run (Gramine in SGX mode plus DCAP quote emission via Gramine's attestation interface), I used an Azure Confidential Computing VM from the DCsv3 family.

VM sizing

  • VM size: Standard DC2s v3
  • vCPU / RAM: 2 vCPU, 16 GiB RAM
  • Region: West Europe
  • Pricing note: on pay-as-you-go this was roughly “tens of cents per hour” at the time of the demo; always verify current pricing in the Azure portal because it varies by region and offer.

OS image

  • Distro: Ubuntu Server 22.04 LTS (Gen2)
  • Publisher / offer / plan: Canonical / 0001-com-ubuntu-server-jammy / 22_04-lts-gen2
  • Kernel (example): 6.8.0-1044-azure (kernel revisions can change over time as Azure images update)

Security profile (host-side)

  • “Trusted launch” security type (Secure Boot + vTPM enabled). This is orthogonal to SGX itself, but it is a reasonable default hardening baseline for cloud hosts.

SGX/Gramine readiness checks on the VM

The minimum host-side checks I ran before attempting Gramine SGX execution:

# OS / kernel sanity
uname -a
uname -r
 
# SGX device exposure on the host (required for in-VM SGX enclaves)
ls -la /dev/sgx* || true
 
# CPU feature bits should include sgx (+ typically sgx_lc on modern platforms)
egrep -m1 -i 'sgx|sgx_lc' /proc/cpuinfo || true
 
# Gramine toolchain presence
gramine-sgx --version
 
# Gramine’s own end-to-end environment check
is-sgx-available

Expected outcome:

  • /dev/sgx_enclave and /dev/sgx_provision are present on the host.
  • is-sgx-available succeeds (exit code 0) and reports SGX as available.

On my VM, the host exposed /dev/sgx_enclave and /dev/sgx_provision (modern in-kernel SGX driver path; legacy /dev/isgx was not present).

On this VM, the check succeeded (exit_code=0) and confirmed:

  • SGX1 + SGX2 supported
  • Flexible Launch Control supported
  • SGX driver loaded
  • AESM daemon available (service aesmd)
  • SGX PSW / libsgx installed
  • EPC present (SGX enclave page cache)

This is the fastest way to establish “the platform is SGX-capable and the software stack is present”.

Note: You may see warnings about the Quote Provider Library (e.g., libdcap_quoteprov.so). These are typically resolved by installing the default DCAP Quote Provider Library package for your distro and ensuring the expected .so name is present via ldconfig/symlinks. This matters because quote generation (and full verification in a follow-up) relies on the right collateral.

On Ubuntu, the quick fix is typically installing libsgx-dcap-default-qpl and ensuring libdcap_quoteprov.so resolves via ldconfig.

Gramine SGX smoke test: CI-Examples/helloworld

Next, I ran Gramine's canonical SGX sample to confirm that:

  • manifest generation works,
  • SGX signing works, and
  • the enclave actually executes under gramine-sgx.
# Gramine version sanity
gramine-sgx --version
 
# Build + sign the enclave-wrapped sample
cd ~/gramine/CI-Examples/helloworld
make clean
make SGX=1
 
# Run inside an SGX enclave via Gramine
gramine-sgx helloworld

Successful run (abridged):

Gramine 1.9 (...)
 
gramine-sgx-sign ... --output helloworld.manifest.sgx
Measurement:
    b81f2f7b...
 
gramine-sgx helloworld
Gramine detected the following insecure configurations:
  - sgx.debug = true (this is a debug enclave)
Hello, world

Two important takeaways:

  • Measurement is printed at signing time. This is the enclave measurement you would pin in a policy allowlist for production-like deployments (exact field depends on the attestation format/policy).
  • The warning about sgx.debug = true is expected for a tutorial/sample. Debug enclaves are convenient for development but are not appropriate for production security claims.

With this smoke test passing, I consider the SGX VM “known good” for running the auction enclave under Gramine SGX. From here, the remaining work is application-specific (manifest hardening, trusted file set, and—when enabled—DCAP quote emission and report_data binding checks).

Reproducing the exact SGX run for this repo

# From repo root on the SGX VM:
cd ~/tee-auction-lab
chmod +x scripts/run_enclave_sgx.sh
 
# 1) Build + sign + run enclave under SGX (writes enclave_output.json + gramine_sgx.log)
AUCTION_ID="auction-123" AUCTION_NONCE="nonce-abc" ./scripts/run_enclave_sgx.sh
 
# scripts/run_enclave_sgx.sh runs the enclave under SGX and writes enclave_output.json. 
# It also runs verify-local as a quick determinism check; the policy gate is demonstrated below with verify-policy.
 
# 2) Confirm receipt is valid JSON and includes SGX evidence
jq . enclave_output.json >/dev/null && echo "JSON OK"
jq -r '.evidence.quote_base64 | length' enclave_output.json
# Example: 6312
 
# 3) Pin enclave identity (MRENCLAVE) in policy
# Copy the "Measurement:" hex printed by gramine-sgx-sign during the script run.
# Example measurement:
#   7343cf214e94e3efe8486fc0e323e16f242e493fe1e66d2e4524fd3d90cdca85
# Put it into policy.sgx.toml under allowed_mrenclaves.
 
# 4) Verify in policy mode (attested) + record replay state
rm -f replay_db.json  # optional: reset for a clean demo
cargo run -p attestation-verifier -- verify-policy \
  --policy policy.sgx.toml \
  --replay-db replay_db.json \
  --bids bids.json \
  --enclave-output enclave_output.json
 
# 5) Replay protection demo: run the same verification again -> should fail
cargo run -p attestation-verifier -- verify-policy \
  --policy policy.sgx.toml \
  --replay-db replay_db.json \
  --bids bids.json \
  --enclave-output enclave_output.json
# Expected: Error: REPLAY DETECTED: already used auction_id+nonce

Notes:

  • verify-local is determinism-only (no evidence requirement).
  • verify-policy is the evidence-required flow: requires a quote, checks report_data binding, and enforces MRENCLAVE allowlisting (full DCAP/QVL verification deferred).

Identity pinning note

MRENCLAVE is the enclave measurement over the enclave's initial contents (as reflected in the quote). Any change to the enclave binary, manifest, or trusted file set changes the measurement. After rebuild/re-sign, copy the new Measurement: printed by gramine-sgx-sign into policy.sgx.toml.

Otherwise we get:

Error: MRENCLAVE not allowed by policy: <measurement>

Example:

[enclave_identity]
mode = "mrenclave"
allowed_mrenclaves = [
  "7343cf214e94e3efe8486fc0e323e16f242e493fe1e66d2e4524fd3d90cdca85"
]

4. The receipt format: EnclaveResponse (the contract)

Everything revolves around a single struct emitted by the enclave and consumed by the verifier.

// auction-core/src/lib.rs (excerpt)
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuctionResult {
    pub winner_id: String,
    pub clearing_price: u64,
    pub highest_bid: u64,
    pub second_highest_bid: u64,
}
 
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SgxDcapEvidence {
    /// Base64-encoded raw quote bytes read from `/dev/attestation/quote`.
    pub quote_base64: String,
}
 
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EnclaveResponse {
    pub auction_id: String,
    pub nonce: String,
 
    pub result: AuctionResult,
    pub bids_commitment_hex: String,
 
    /// Hash of a domain-separated statement binding inputs + output.
    pub statement_hash_hex: String,
 
    /// Present only when running under SGX with quote generation enabled.
    pub evidence: Option<SgxDcapEvidence>,
}

Interpretation:

  • bids_commitment_hex is the public commitment to the canonical bid set.
  • statement_hash_hex binds (auction_id, nonce, bids_commitment_hex, result) into one hash.
  • In dev/local mode, evidence is typically null. On SGX+DCAP, the receipt includes evidence.quote_base64. Just confirm it via:
jq -r '.evidence.quote_base64 | length' enclave_output.json

5. Determinism: canonical bid commitment

If bids arrive in different orders, you still want the same commitment.

We canonicalize bids, then commit to the JSON serialization:

// auction-core/src/lib.rs (excerpt)
use sha2::{Digest, Sha256};
 
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Bid {
    pub bidder_id: String,
    pub amount: u64,
}
 
fn canonicalize_bids(bids: &[Bid]) -> Vec<Bid> {
    let mut v = bids.to_vec();
    v.sort_by(|a, b| a.bidder_id.cmp(&b.bidder_id).then_with(|| a.amount.cmp(&b.amount)));
    v
}
 
pub fn bids_commitment(bids: &[Bid]) -> [u8; 32] {
    let canonical = canonicalize_bids(bids);
    let bytes = serde_json::to_vec(&canonical).expect("bids must serialize");
    let mut hasher = Sha256::new();
    hasher.update(&bytes);
    hasher.finalize().into()
}

This is the bridge between:

  • an untrusted host that shuffles files and processes, and
  • a verifier that can recompute and reject tampering.

6. The statement we bind into SGX report_data

SGX gives you exactly 64 bytes of report_data. The coprocessor pattern needs you to bind your application statement into this field.

We define a domain-separated statement and hash it:

// auction-core/src/lib.rs (excerpt)
#[derive(Debug, Clone, Serialize)]
struct AuctionStatement<'a> {
    domain: &'a str,
    auction_id: &'a str,
    nonce: &'a str,
    bids_commitment_hex: &'a str,
    result: &'a AuctionResult,
}
 
pub fn statement_hash_hex(
    auction_id: &str,
    nonce: &str,
    bids_commitment_hex: &str,
    result: &AuctionResult,
) -> String {
    let st = AuctionStatement {
        domain: "tee-auction-v1",
        auction_id,
        nonce,
        bids_commitment_hex,
        result,
    };
 
    let bytes = serde_json::to_vec(&st).expect("statement JSON");
    let mut hasher = Sha256::new();
    hasher.update(&bytes);
    hex::encode(hasher.finalize())
}
 
/// SGX `report_data` is 64 bytes; we use `hash || hash` (32B + 32B).
pub fn report_data_64_from_statement_hash(statement_hash_hex: &str) -> [u8; 64] {
    let mut out = [0u8; 64];
    let h = hex::decode(statement_hash_hex).expect("hex");
    assert_eq!(h.len(), 32, "SHA-256 is 32 bytes");
    out[..32].copy_from_slice(&h);
    out[32..].copy_from_slice(&h);
    out
}

Why hash || hash?

  • It fills 64 bytes without inventing a second independent value.
  • The security property is the binding: quote report_data must match this 64-byte value.

7. Enclave logic: compute receipt, optionally collect SGX quote

Enclave flow:

  1. Read bids from ENCLAVE_BIDS_PATH (or default path)
  2. Require AUCTION_ID and AUCTION_NONCE
  3. Compute result, bids_commitment, and statement_hash
  4. Compute report_data_64 from statement_hash
  5. If running under SGX + Gramine DCAP attestation, write report_data and read quote
  6. Emit EnclaveResponse JSON to stdout

Key excerpt:

// enclave-auction/src/main.rs (excerpt)
let result = run_second_price_auction(&bids);
let bids_commitment_hex = hex::encode(auction_core::bids_commitment(&bids));
 
let statement_hash_hex = statement_hash_hex(&auction_id, &nonce, &bids_commitment_hex, &result);
let report_data_64 = report_data_64_from_statement_hash(&statement_hash_hex);
 
let evidence = try_collect_dcap_quote(&report_data_64)
    .context("failed to collect DCAP quote")?;
 
let response = EnclaveResponse {
    auction_id,
    nonce,
    result,
    bids_commitment_hex,
    statement_hash_hex,
    evidence,
};
 
println!("{}", serde_json::to_string_pretty(&response)?);

Note: The enclave prints the JSON receipt to stdout (so the host can capture it as a file). Any debugging should go to stderr so it doesn't corrupt the receipt.

Quote collection is explicitly gated on the presence of Gramine's attestation files:

// enclave-auction/src/main.rs (excerpt)
fn try_collect_dcap_quote(report_data_64: &[u8; 64]) -> Result<Option<SgxDcapEvidence>> {
    let quote_path = Path::new("/dev/attestation/quote");
    let user_report_data_path = Path::new("/dev/attestation/user_report_data");
 
    if !quote_path.exists() || !user_report_data_path.exists() {
        return Ok(None); // dev / non-SGX mode
    }
 
    // 1) Bind our statement hash into SGX report_data (must be exactly 64 bytes).
    {
        let mut f = File::create(user_report_data_path)?;
        f.write_all(report_data_64)?;
        f.flush().ok();
    }
 
    // 2) Read the DCAP quote bytes.
    let mut quote_file = File::open(quote_path)?;
    let mut quote_bytes = Vec::new();
    quote_file.read_to_end(&mut quote_bytes)?;
 
    Ok(Some(SgxDcapEvidence {
        quote_base64: base64::engine::general_purpose::STANDARD.encode(&quote_bytes),
    }))
}

This avoids pretending to have SGX evidence in local mode: locally evidence is null; on an SGX VM it becomes populated.

8. The verifier: recompute, enforce replay policy, and bind evidence

8.1 The verifier is canonical by design

The host is not a verifier; it just runs the enclave and stores artifacts.

The verifier:

  • reads bids.json and enclave_output.json
  • recomputes commitment, result, statement hash
  • enforces replay rules
  • if evidence exists: parses quote and checks report_data binding
  • enforces identity policy (allowlist) when evidence exists

8.2 The “undeniable” check: quote report_data must match expected

This is the core coprocessor evidence check:

// attestation-verifier/src/main.rs (excerpt)
let expected_commitment_hex = hex::encode(auction_core::bids_commitment(&bids));
let expected_result = run_second_price_auction(&bids);
 
let expected_statement_hash = statement_hash_hex(
    &resp.auction_id,
    &resp.nonce,
    &expected_commitment_hex,
    &expected_result,
);
 
let expected_report_data = report_data_64_from_statement_hash(&expected_statement_hash);
 
// Evidence required for attested mode
let evidence = resp.evidence.as_ref().context("missing evidence")?;
let quote_bytes = base64::engine::general_purpose::STANDARD
    .decode(&evidence.quote_base64)?;
 
// Parse quote and read report_data
let quote = sgx_quote::Quote::parse(&quote_bytes)
    .map_err(|e| anyhow::anyhow!("quote parse error: {e:?}"))?;
 
let report_data = quote.isv_report.report_data;
if report_data != expected_report_data {
    anyhow::bail!("quote report_data mismatch (statement not bound to evidence)");
}

If this passes, you have:

  • a receipt (statement) that matches deterministic recomputation, and
  • evidence that is cryptographically bound to that receipt via report_data.

That is the essence of “TEE as coprocessor”.

9. Replay protection: boring state, correct behavior

Attested results can be replayed unless you store state. We implement this explicitly with a persistent DB (replay_db.json).

Two useful replay policies:

  • strict: unique auction_id
  • relaxed: unique (auction_id, nonce)

The policy is versioned and lives in policy.toml.

Example policy.toml:

version = 1
 
[replay]
# If true, reject any second receipt with same auction_id (strongest, simplest).
# If false, replay key can be auction_id::nonce.
auction_id_unique = true
 
[enclave_identity]
# When evidence exists, enforce identity allowlist.
mode = "mrenclave"
allowed_mrenclaves = [
  # Filled after SGX VM run (real measurement hex)
  # "532a135d...f9def6b"
]

The verifier only records replay state after all checks pass:

// attestation-verifier (conceptual excerpt)
let replay_key = if policy.replay.auction_id_unique {
    resp.auction_id.clone()
} else {
    format!("{}::{}", resp.auction_id, resp.nonce)
};
 
if used.contains(&replay_key) {
    anyhow::bail!("REPLAY DETECTED: {}", replay_key);
}
 
// only after deterministic + (optional) evidence checks succeed:
used.insert(replay_key);
save_replay_db(...)?;

This is the discipline we want in a “rollup node” style verifier.

10. Host orchestration: produce artifacts, do not become the verifier

The host CLI's job is:

  • produce bids.json
  • run enclave
  • store enclave stdout as enclave_output.json

Minimal tutorial flow:

# 1) Create bids
cargo run -p host-cli -- gen-sample-bids
 
# 2) Run enclave locally (plain process runner)
cargo run -p host-cli -- run-local --bids bids.json --out enclave_output.json
 
# 3) Verify deterministically (no evidence required)
cargo run -p attestation-verifier -- verify-local \
  --bids bids.json \
  --enclave-output enclave_output.json
 
# Optional: exercise replay DB + policy logic locally (dev policy)
cargo run -p attestation-verifier -- verify-policy \
  --policy policy.dev.toml \
  --replay-db replay_db.json \
  --bids bids.json \
  --enclave-output enclave_output.json

In local mode we should see:

  • evidence: null in enclave_output.json
  • verifier passes deterministic checks and records replay state

11. Gramine / SGX integration: what we already have (and what the SGX-enabled VM run adds, see above)

We already have the correct manifest building blocks:

  • passthrough env vars (AUCTION_ID, AUCTION_NONCE, ENCLAVE_BIDS_PATH)
  • sgx.remote_attestation = "dcap"
  • trusted file declarations (for integrity + measurement)
  • the signed manifest (*.manifest.sgx) and measurement printed by gramine-sgx-sign

When we ran:

make build copy-bids manifest sign

and got a printed Measurement, we proved:

  • the enclave configuration is “measurable” and reproducible
  • we have a concrete value to pin in a policy allowlist

The next step that fails on my laptop (missing driver) is exactly the step that will succeed in an SGX-ready VM:

AUCTION_ID="auction-123" AUCTION_NONCE="nonce-abc" ENCLAVE_BIDS_PATH="/bids.json"   gramine-sgx enclave-auction > ../enclave_output.json

On an SGX VM with drivers:

  • /dev/sgx_enclave exists on the host
  • Gramine can launch the enclave
  • /dev/attestation/* exists in the enclave
  • enclave_output.json should now contain non-null evidence.quote_base64

At that point, we flip the policy:

  • add the real MRENCLAVE to allowed_mrenclaves
  • run verifier again, now enforcing identity based on evidence

Confidentiality: what SGX does not magically give you

A crucial limitation of the current tutorial is that bids are supplied as a plaintext host file (bids.json) and mounted into the enclave.

This means:

  • SGX protects in-enclave memory and prevents the host OS from reading enclave RAM directly.
  • But the host still sees plaintext inputs before they enter the enclave (because it created or stored bids.json).

So the current tutorial demonstrates integrity + replay safety, but not bid confidentiality.

What “confidential bids” would require (follow-up options)

Any of the following patterns is acceptable; the right choice depends on your product constraints:

  1. RA-TLS bid submission (recommended)

    • The enclave runs a small TLS server.
    • Bidders establish an RA-TLS connection and only send bids after verifying enclave identity (measurement/policy).
    • The host sees ciphertext only.
  2. Encrypted bids + in-enclave decryption

    • Bidders encrypt their bid to an enclave public key.
    • The enclave holds the private key (sealed to disk if needed).
    • The host stores ciphertext blobs, not plaintext bids.
  3. Sealed file input

    • If bids must be file-based, encrypt and seal them to the enclave.
    • This still needs a secure way to provision the key (which typically means remote attestation anyway).

Until one of these is implemented, you should not describe the system as a “private auction” end-to-end; it is currently an integrity-focused coprocessor receipt with replay protection.

12. VM-ready next steps (fast checklist)

This is the minimum “SGX-ready” checklist to finish the tutorial and make the follow-up post mostly copy/paste:

  1. Add a host runner mode for SGX

    • Option A: host-cli run-sgx ... that shells out to gramine-sgx enclave-auction
    • Option B: a small script that runs make run-sgx AUCTION_ID=... AUCTION_NONCE=...
  2. Run on an SGX VM and obtain a quote

    • confirm /dev/sgx_enclave on host
    • confirm /dev/attestation/quote inside enclave
    • verify evidence.quote_base64 becomes non-null
  3. Pin measurement

    • extract MRENCLAVE from quote (verifier already prints it in attested mode)
    • put it into policy.toml allowlist
    • demonstrate:
      • wrong measurement fails
      • replay attempt fails
  4. Only then (follow-up): full DCAP chain verification

    • validate quote signature chain and TCB status
    • document vendor roots + freshness rules

13. Resources

Primary references used while building this lab:

Appendix: “professional claim” summary (for a DM)

This repo is not “production-grade SGX attestation.” It is:

  • A minimal coprocessor interface (receipt struct) with deterministic recomputation.
  • A clean binding story: statement_hash -> report_data -> quote.
  • A verifier that behaves like a policy-driven rollup node: replay-aware, explicit trust policy, upgrade-ready allowlists.
  • A clear scope boundary: full DCAP chain/TCB verification is acknowledged and intentionally postponed for a follow-up.
  • A clearly scoped tutorial: SGX execution + deterministic receipt verification now; quote emission, report_data binding, and full DCAP chain verification next.

Stay Updated

Get notified when I publish new articles about Web3 development, hackathon experiences, and cryptography insights.

You might also like

Crescent Bench Lab: Measuring ZK Presentations for Real Credentials (JWT + mDL)

A small Rust lab that vendors microsoft/crescent-credentials, generates Crescent test vectors, and benchmarks zksetup/prove/show/verify across several parameters — including proof sizes and selective disclosure variants.

Baby-Ligero: Three Tiny Tests for a Tiny Circuit — ZK Hack S3M5

A mini Rust lab that implements a baby version of Ligero's three tests — proximity, multiplication, and linear — for a tiny arithmetic circuit, and uses them to see soundness amplification in action.

Norm Blowup in Lattice Folding (LatticeFold Lab) — ZK Hack S3M4

A hands-on Rust experiment exploring why folding causes norm blowup in lattice commitments, and how decomposition keeps the digits small — the core idea behind LatticeFold and LatticeFold+.