Skip to content

Getting Started

This page gets you from a fresh checkout to a running local KMS and the attestation-verifier web app, then walks one challenge → get-key cycle against your local instance. It is the fastest way to see the moving parts before you read the flows in depth.

The production KMS only releases keys to nodes running inside a real Intel TDX confidential VM, with quotes signed by the hardware and keys derived from a cluster root that exists only inside TDX replicas. None of that exists on a laptop, so the KMS ships a dev-only mock path (a Cargo feature, off by default) that lets you run a single replica and exercise the HTTP surface and most of the verification pipeline without TDX hardware.

  • Rust 1.88+ (rustup, cargo) — the KMS is a Cargo workspace.
  • Node.js 18+ and npm — for the attestation-verifier web app.
  • curl and (optionally) jq — to drive the endpoints by hand.

The workspace has a single member, mero-kms. From the repository root:

Terminal window
# Debug build of the whole workspace
cargo build
# Or an optimized build
cargo build --release

This produces the production binary — it contains no mock-attestation code. To run locally you want the dev build described next.

Two independent things stand between a fresh start and a working /get-key locally, and you have to satisfy both:

  1. Mock attestation — accept mock TDX quotes instead of requiring real hardware-signed ones. This is gated behind the default-off mock-attestation Cargo feature, and only then is the ACCEPT_MOCK_ATTESTATION env var read.
  2. A cluster root — /challenge and /get-key are fail-closed. Until the replica holds a root it returns 503 policy_not_ready. Locally, make your single replica the bootstrap one with MERO_KMS_BOOTSTRAP=true, so it generates a root at startup instead of trying to join peers.

Build with the feature and set both knobs:

Terminal window
cd mero-kms
ACCEPT_MOCK_ATTESTATION=true \
MERO_KMS_BOOTSTRAP=true \
cargo run --features mock-attestation

In mock mode the replica’s own quote is a mock too, so the debug-TD and RTMR3 startup checks that a real replica enforces are skipped.

On startup the service logs its resolved configuration. Expect to see the mock warning, the bootstrap flag, the locked-read-only profile, and the new root:

INFO Starting mero-kms
INFO Listen address: 0.0.0.0:8080
INFO Cluster: bootstrap=true peers=[]
INFO KMS profile cohort: locked-read-only
WARN WARNING: Mock attestation acceptance is enabled. This should NEVER be used in production!
INFO Generated a new cluster root; this replica bootstraps the cluster
INFO Server listening on 0.0.0.0:8080

The HTTP listener defaults to 0.0.0.0:8080. Override with LISTEN_ADDR if 8080 is taken:

Terminal window
LISTEN_ADDR=127.0.0.1:9090 \
ACCEPT_MOCK_ATTESTATION=true \
MERO_KMS_BOOTSTRAP=true \
cargo run --features mock-attestation

/health is a liveness probe — it answers 200 whether or not the replica holds a root, and says which:

Terminal window
curl -s http://localhost:8080/health
# {"status":"alive","service":"mero-kms","clusterRootReady":true}

Key release is a two-round challenge-response. Round 1 (/challenge) is fully self-contained and works locally; round 2 (/get-key) runs the whole verification pipeline and is where the local/hardware boundary shows up. Both routes are documented in full in Key Release.

Send any base58btc peer ID. The KMS returns a stateless challenge ID (112 hex characters: 16 random bytes, the expiry and a tag) plus the base64 nonce, an HMAC of the ID and the peer ID under a root-derived key, and its expiry:

Terminal window
curl -s -X POST http://localhost:8080/challenge \
-H 'content-type: application/json' \
-d '{"peerId":"12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp"}'
{
"challengeId": "<112 hex chars>",
"nonceB64": "aGVsbG8td29ybGQtZXhhbXBsZS0zMi1ieXRlcy1ub25jZQ==",
"expiresAt": 1753104000
}

The challenge is valid for CHALLENGE_TTL_SECS (default 60) and a replica redeems it at most once.

/get-key takes the challenge ID plus a TDX quote, the peer’s public key, and a signature binding all of it to the peer identity:

Terminal window
curl -s -X POST http://localhost:8080/get-key \
-H 'content-type: application/json' \
-d '{
"challengeId": "<112 hex chars from round 1>",
"quoteB64": "<base64 raw TDX quote with the nonce in report_data>",
"peerId": "12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp",
"peerPublicKeyB64": "<base64 protobuf libp2p public key>",
"signatureB64": "<base64 signature over the canonical payload>",
"sealToB64": "<base64 one-time X25519 public key>"
}'

The KMS then, in order: validates the shapes, redeems the challenge (so a replay to the same replica always fails on the second try), verifies the peer signature and public-key ownership, verifies the quote (mock quotes are accepted here in dev), enforces the measurement policy (skipped for accepted mocks), and finally derives the key by HKDF from the root and seals it to sealToB64. On success it returns:

{ "sealedKeyB64": "<sealed storage key>", "sealNonceB64": "<12-byte nonce>" }

You can still observe the fail-closed behavior directly. Send a /get-key request with placeholder credentials and watch the gates reject it, and watch the challenge get redeemed even on failure:

Terminal window
# Second identical request returns invalid_challenge — the first attempt
# already redeemed the single-use challenge, regardless of why it failed.

Every error is a machine-readable tag with an HTTP status; the full catalog is in Error handling.

The verifier is a separate React/Vite app with a serverless /api/verify backend. Run it locally:

Terminal window
cd attestation-verifier
npm install
npm run dev

Vite serves it at http://localhost:5173. The two routes are /kms (verify a pasted mero-kms /attest response; KMS replicas listen only inside their VPC) and /mero-tee (verify a merod node by URL); the landing page redirects to /kms.