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 nonce | Application nonce | |
|---|---|---|
| Issued by | Intel Trust Authority | Your application |
| Prevents | Quote theft across machines/sessions | Proof reuse across requests |
| Verified by | ITA server-side + verify_quote locally | Your application logic |
| Location | verifier_nonce_val / verifier_nonce_iat | report_data.nonce (bytes [48..56]) |
| Fetched by | AttestBuilder::finalize automatically | Developer via .nonce(counter) |
| Required | Yes — always fetched by the high-level API | Recommended — 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.