Skip to content

attestation!: report the DCAP collateral a verification consumed - #4

Open
samlaf wants to merge 3 commits into
mainfrom
sei-208/pr1-report-verified-collateral
Open

attestation!: report the DCAP collateral a verification consumed#4
samlaf wants to merge 3 commits into
mainfrom
sei-208/pr1-report-verified-collateral

Conversation

@samlaf

@samlaf samlaf commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

The crate is usable as a relying party for a one-time event rather than a live handshake: evidence is verified once against a measurement policy and archived as permanent provenance. Nothing reported which collateral bundle a verification used, so there was nothing to archive alongside the evidence it verified.

Fetching a second copy next to the verification is the obvious workaround, and it is subtly wrong. A PCCS cache refresh between the two fetches makes the archived bundle a bundle rather than the bundle the verification consumed, and for provenance that distinction is the whole point.

Verification now returns VerifiedAttestation: the measurements, the collateral it consumed, and the instant every freshness check was evaluated at. The public entry points return Option

  • None when the evidence carried no attestation and none was expected, the one case with no bundle and no instant to report. Nesting the absence in one Option keeps the three fields from ever disagreeing.

One type serves every platform. A GCP TDX quote is a DCAP quote, and Azure wraps one in an HCL report and a vTPM attestation, so a verification always consumes exactly one collateral bundle, whichever platform produced the evidence. Azure holds its vTPM leg to the instant its DCAP leg reported, so verified_at is the single instant behind every freshness check rather than one per leg.

That instant is the other half of what archiving buys: with the bundle, the same evidence and the same instant give the same answer forever. QuoteCollateralV3 is re-exported so callers can keep the bundle without taking a direct dependency on dcap-qvl.

The return type of verify_attestation and verify_attestation_sync changes from Option to Option. Both in-tree callers discard the value, so neither needed a change.

Addresses the reporting half of flashbots#84. Verifying archived evidence against a pinned bundle at an explicit instant, the other half, follows separately.

@linear-code

linear-code Bot commented Aug 25, 2026

Copy link
Copy Markdown

SEI-208

samlaf added 2 commits August 31, 2026 13:49
The crate is usable as a relying party for a one-time event rather than
a live handshake: evidence is verified once against a measurement
policy and archived as permanent provenance. Nothing reported which
collateral bundle a verification used, so there was nothing to archive
alongside the evidence it verified.

Fetching a second copy next to the verification is the obvious
workaround, and it is subtly wrong. A PCCS cache refresh between the
two fetches makes the archived bundle *a* bundle rather than *the*
bundle the verification consumed, and for provenance that distinction
is the whole point.

Verification now returns VerifiedAttestation: the measurements, the
collateral it consumed, and the instant every freshness check was
evaluated at. The public entry points return Option<VerifiedAttestation>
- None when the evidence carried no attestation and none was expected,
the one case with no bundle and no instant to report. Nesting the
absence in one Option keeps the three fields from ever disagreeing.

One type serves every platform. A GCP TDX quote is a DCAP quote, and
Azure wraps one in an HCL report and a vTPM attestation, so a
verification always consumes exactly one collateral bundle, whichever
platform produced the evidence. Azure holds its vTPM leg to the instant
its DCAP leg reported, so verified_at is the single instant behind every
freshness check rather than one per leg.

That instant is the other half of what archiving buys: with the bundle,
the same evidence and the same instant give the same answer forever.
QuoteCollateralV3 is re-exported so callers can keep the bundle without
taking a direct dependency on dcap-qvl.

The return type of verify_attestation and verify_attestation_sync
changes from Option<MultiMeasurements> to Option<VerifiedAttestation>.
Both in-tree callers discard the value, so neither needed a change.

Addresses the reporting half of flashbots#84. Verifying archived evidence
against a pinned bundle at an explicit instant, the other half,
follows separately.
The bundle and the instant were two independent public fields, so the
pair a later replay needs arrived pre-split. Taking one without the
other is not a mistake a caller has to work at: it is the shape of
least resistance, and nothing objects. The input side already refuses
that - VerifyMode::Archived carries both or neither - so the output
was the one place the pair could come apart.

CollateralSnapshot binds them. It is one value coming out and, in the
change that follows, the same value going back in, so archiving is
"keep the snapshot" and re-verifying is "hand it back".

VerifiedAttestation becomes AttestationResult. RFC 9334 calls what an
attester produces Evidence and what a verifier produces from it an
Attestation Result. The crate already takes AttestationEvidence in, so
this names the far end of one appraisal; "attestation" on its own named
no field of the struct.

The measurements docs now say where the values come from, which the
Azure path made worth stating. They are read out of the quote on DCAP
and GCP. On Azure they are the vTPM PCRs, measuring the guest boot
rather than the launched TD - chained to the quote, whose report data
commits to the HCL var data carrying the AK public key that signs the
vTPM quote, but no field of it. The fixture test now asserts the whole
snapshot, so the pairing itself is covered.
@samlaf
samlaf force-pushed the sei-208/pr1-report-verified-collateral branch from baca4a1 to 9b7c1ca Compare August 31, 2026 17:54
AttestationResult sat at the boundary that is meant to hide which
platform produced the evidence, and two of its three fields were Intel
types: a dcap-qvl Quote and a QuoteCollateralV3. The AWS Nitro work in
flashbots#45 has neither, so the struct would break a second time the moment a
non-TDX platform lands. flashbots#65 breaks it with no new platform at all - it
moves the collateral into the evidence, so the verifier fetches none.

The parsed quote leaves the shared type. It had exactly one consumer:
AttestationVerifier binds it only to hand to the GCP provenance check,
which reads the PPID out of the PCK leaf. gcp/firmware.rs discards it,
and so does our own downstream. dcap.rs still hands it back, now as the
second element of a tuple, so nothing re-parses the same bytes
(34346c6) and the module gains no public type of its own - flashbots#40 may
replace that module with attest-verify, which already owns a
ValidatedDcapQuote one letter away.

CollateralSnapshot becomes EndorsementSnapshot, and the bundle becomes
an optional field on it. What a verifier fetches varies; the instant it
holds that material to does not. So the instant stays a plain field and
the fetched material hangs off it, with #[non_exhaustive] and a
::dcap() constructor keeping the pairing structural and later fields
additive. A struct rather than an enum keyed by platform, because a
verification consumes a set of fetched material rather than one of
several alternatives, and because the axis it varies along is not the
platform: whether endorsements ride in the evidence or get fetched is a
transport choice of the protocol. flashbots#49 already made that choice one way
for the Azure AK chain, and flashbots#65 proposes the opposite for DCAP
collateral.

"Collateral" is Intel's DCAP word, and would read as a category error
the moment the snapshot holds Azure vTPM roots or whatever Nitro needs.
"Endorsements" is the loose umbrella rather than RFC 9334's strict term
- a DCAP bundle spans Endorsements and Reference Values both - and the
doc comment says so, so the imprecision is deliberate rather than
sloppy.

The type is called VerifiedAttestation again, which reverses the rename
in the commit before this one. That rename argued the value is the far
end of one appraisal. It is not: the appraisal policy runs after the
value is built, and a caller can configure it to check nothing, which
is exactly what our enclave does before appraising in its own admission
predicate. So no Reference Value comparison is implied, and what comes
back is verified evidence rather than an Attestation Result. Leaving the
RFC's name unspent keeps it for the type that would earn it if appraisal
is ever separated from verification. Result is also taken in Rust, with
AttestationError in this same crate.

The docs on both types are shorter for it. The cache-refresh caveat is
stated once instead of three times, and where the value sits in the RATS
pipeline is stated once, linked to RFC 9334.

Addresses review feedback on flashbots#85.

BREAKING CHANGE: AttestationResult is renamed VerifiedAttestation and
loses its quote field. CollateralSnapshot is renamed
EndorsementSnapshot, is #[non_exhaustive], and its collateral field
becomes dcap: Option<QuoteCollateralV3>, built through
EndorsementSnapshot::dcap.
The dcap::verify_* functions return (VerifiedAttestation, Quote), the
azure::verify_* functions return VerifiedAttestation, and
AttestationVerifier::verify_attestation{,_sync} return
Option<VerifiedAttestation>.
@samlaf
samlaf force-pushed the sei-208/pr1-report-verified-collateral branch from 21b8888 to f828787 Compare August 31, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant