Livy Documentation

Nonces

Two distinct nonces protect against two distinct replay attacks at different layers of the stack.

Every attestation contains two independent nonces. They protect against different attacks and are verified by different parties.

ITA verifier nonce — quote-level replay

Issued by: Intel Trust Authority Prevents: capturing a valid DCAP quote from any TDX machine and replaying it to ITA on behalf of a different session Location in proof: verifier_nonce_val / verifier_nonce_iat

Without this nonce, an attacker could save a valid quote from a genuine TDX machine and re-submit it to ITA from a different request. ITA would accept it because the PCK certificate chain is valid regardless of who submitted it.

The high-level API fetches this nonce automatically before generating the quote:

REPORTDATA = SHA-512(nonce.val ‖ nonce.iat ‖ runtime_data)

ITA verifies server-side that the REPORTDATA matches. A replayed quote from a different ITA session will have a different REPORTDATA and be rejected.

The nonce is included in every proof so offline verifiers can also check the binding:

use livy_tee::verify_quote;

// This checks SHA-512(nonce_val ‖ nonce_iat ‖ runtime_data) == quote REPORTDATA.
let ok = verify_quote(
    &raw_quote,
    &runtime_data,
    &verifier_nonce_val,  // from ITA GET /nonce
    &verifier_nonce_iat,  // from ITA GET /nonce
    &expected_payload_hash,
)?;

Application nonce — request-level replay

Issued by: your application Prevents: saving a valid proof for request #1 and presenting it as the response to request #100 Location in proof: report_data.nonce (bytes [48..56] of runtime_data, u64 big-endian)

Without this nonce, an attacker could capture a proof from any request and replay it for any other request with matching inputs. Both would have a valid ITA JWT and a correct payload hash.

// Maintain a monotonically increasing counter — persist it across restarts.
let counter: u64 = db.next_nonce().await?;

let attestation = livy.attest()
    .commit(&input)
    .commit(&output)
    .nonce(counter)  // embed counter in REPORTDATA[48..56]
    .finalize()
    .await?;

// Store the nonce alongside the attestation.
db.store(attestation, counter).await?;

Verifier check:

// The verifier reads the nonce from the attestation and checks it matches
// the value stored for this specific request slot.
let nonce_in_proof = attestation.report_data.nonce;
let expected_nonce = db.nonce_for_request(request_id).await?;
assert_eq!(nonce_in_proof, expected_nonce, "nonce mismatch — possible replay");

Defaults to 0 if .nonce() is not called. This is acceptable for single-request or batch workloads where replay is not a concern.

Nonce persistence across restarts

The application nonce counter must survive server restarts. If the counter resets, old proofs could be replayed as new ones.

// On startup: seed from the last stored nonce.
let initial_nonce: u64 = db.last_nonce().await?.unwrap_or(0);
let counter = AtomicU64::new(initial_nonce + 1);

// On each request: fetch-and-increment atomically.
let nonce = counter.fetch_add(1, Ordering::SeqCst);

Comparison

ITA verifier nonceApplication nonce
Issued byIntel Trust AuthorityYour application
PreventsQuote theft across machines/sessionsProof reuse across requests
Verified byITA server-side + verify_quote locallyYour application logic
Locationverifier_nonce_val / verifier_nonce_iatreport_data.nonce (bytes [48..56])
Fetched byAttestBuilder::finalize automaticallyDeveloper via .nonce(counter)
RequiredYes — always fetched by the high-level APIRecommended — defaults to 0

The two nonces are complementary:

  • The ITA nonce pins the quote to a specific ITA session — no quote from any other session is accepted.
  • The application nonce pins the proof to a specific request slot — no proof from any other request is accepted.

Both must pass for a proof to be considered valid.