Skip to content

feat!(attestation): report the DCAP collateral a verification consumed - #85

Merged
ameba23 merged 3 commits into
flashbots:mainfrom
SeismicSystems:sei-208/pr1-report-verified-collateral
Sep 1, 2026
Merged

feat!(attestation): report the DCAP collateral a verification consumed#85
ameba23 merged 3 commits into
flashbots:mainfrom
SeismicSystems:sei-208/pr1-report-verified-collateral

Conversation

@samlaf

@samlaf samlaf commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes half of #84. Second half is SeismicSystems#3 (github doesn't allow stacked PRs on upstream repos so will make an upstream PR once this one lands).

TLDR is that we need to archive the collateral needed to reverify a quote at a later time (so we can reverify the quotes from the genesis machines on our chain). This PR is only the part that returns the archive. A separate PR needs to follow that allows passing the archived collateral as input. Kept separate because I found it hard to review both changes in one, and both introduce separate API breaking changes, so easier to keep them separate imo.

LLM Summary

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.

@samlaf samlaf changed the title attestation!: report the DCAP collateral a verification consumed feat!(attestation): report the DCAP collateral a verification consumed Aug 25, 2026
@samlaf
samlaf force-pushed the sei-208/pr1-report-verified-collateral branch from a055859 to 6ab5e69 Compare August 25, 2026 17:36
samlaf added a commit to SeismicSystems/attested-tls that referenced this pull request Aug 25, 2026
…fyMode

Second of two changes for flashbots#84, stacked on
flashbots#85.

Reporting the collateral a verification consumed is half of provenance.
The other half is running the same verification again later, against
that bundle, and getting the same answer. Nothing exposed that: the
public entry points always fetched, and the only way to supply a bundle
was through test-only variants that also took a bare timestamp.

Verification now takes a VerifyMode. Live fetches collateral and reads
the wall clock. Archived carries the bundle and the instant it was
collected, so the two can never be supplied apart - a pinned bundle
evaluated at the wrong instant is the mistake the old
(Option<collateral>, now) pair permitted. Azure holds its AK certificate
chain to the instant the DCAP leg reported, so both legs evaluate at one
time in either mode.

The Azure TCB override leaves the public DCAP entry points. Only the
Azure verifier has a reason to relax TCB checks, so it reaches the
override through a crate-private variant; verify_dcap_attestation always
verifies at full strictness. The mock verifier likewise becomes its own
function rather than a cfg switch inside the production one, so
verify_dcap_attestation means Intel-rooted in every build and the
fixture tests replay real captures against it. AttestationVerifier is
the one place that picks between them.

BREAKING CHANGE: verify_attestation and verify_attestation_sync take a
VerifyMode; the DCAP and Azure entry points take mode before pccs and
the DCAP ones drop override_azure_outdated_tcb; the
*_with_given_timestamp variants are gone, replaced by
VerifyMode::Archived.

GCP checks and Archived mode
----------------------------

The GCP host provenance check from flashbots#54 stays live
in either mode, as does the firmware fetch for the quote's MRTD. Neither
rests on signed material a replay could re-verify: the provenance
document is an unsigned JSON object whose trust is the TLS connection to
Google's bucket, so archiving it would not make a replay stronger. The
docs on VerifyMode::Archived and verify_attestation state the carve-out.
Whether Archived should skip the provenance lookup instead is left open.

Closes flashbots#84
@samlaf

samlaf commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@ameba23 could you take a look at this and its followup SeismicSystems#3 when you get a chance? Would like to know if this aligns with the various issues you've been battling and with your current direction for the crate.

Comment thread crates/attestation/src/lib.rs Outdated
/// verification always consumes exactly one collateral bundle, whichever
/// platform produced the evidence.
#[derive(Clone, Debug)]
pub struct AttestationResult {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having tdx quote and collateral is TDX specific. Which might make it tricky to later add support for other TEE platforms (eg: AWS Nitro which there is already a PR for).

Im not sure what the best way is to make this extendable later for other possible supported platforms. Probably we want an attestation evidence enum with TDX variant - but don't know if this should encompass the measurements as well or be a separate enum.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also small nit on naming: 'result' might make people think this can encompass both success and failure. i cant think of a better name. VerfiedAttestation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased PR and updated it to be more generic. Let me know what you think of this form... tried to follow RFC9334's nomenclature as much as possible. I think this is getting closer to a nicer platform-independent form, although there's still some more updates that will be needed (created separate issues for them). But Nitro would easily fit in this form since it only requires a timestamp, and the collateral is now optional.

On the naming RATS has no such thing as VerifiedAttestation since an attestation is a procedure/verb, not an object, so it would rather use something like VerifiedEvidence or AttestationResult (which is what I was using which is the exact rats term but actually doesnt match our repo structure so it was still off from its true meaning.. which is why I reverted back to your VerifiedAttestation for now. we can revisit this name later as the library structure progresses.)

@ameba23 ameba23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks great 🥇

See comment. We could kick this down the road until we actually add support for a non-TDX platform. But it would mean making another breaking change then

Comment thread crates/attestation/src/lib.rs Outdated
/// verification always consumes exactly one collateral bundle, whichever
/// platform produced the evidence.
#[derive(Clone, Debug)]
pub struct AttestationResult {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also small nit on naming: 'result' might make people think this can encompass both success and failure. i cant think of a better name. VerfiedAttestation?

Comment thread crates/attestation/src/lib.rs Outdated
/// together is what makes a verification reproducible: the same evidence,
/// the same bundle and the same instant give the same answer forever.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CollateralSnapshot {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 this is a good idea

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realized we probably should go even further than this and archive more things. See my comment in #84 (comment)

Would do this as a followup though, not in this PR.

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
samlaf added a commit to SeismicSystems/attested-tls that referenced this pull request Aug 31, 2026
…fyMode

Second of two changes for flashbots#84, stacked on
flashbots#85.

Reporting the collateral a verification consumed is half of provenance.
The other half is running the same verification again later, against
that bundle, and getting the same answer. Nothing exposed that: the
public entry points always fetched, and the only way to supply a bundle
was through test-only variants that also took a bare timestamp.

Verification now takes a VerifyMode. Live fetches collateral and reads
the wall clock. Archived carries the bundle and the instant it was
collected, so the two can never be supplied apart - a pinned bundle
evaluated at the wrong instant is the mistake the old
(Option<collateral>, now) pair permitted. Azure holds its AK certificate
chain to the instant the DCAP leg reported, so both legs evaluate at one
time in either mode.

The Azure TCB override leaves the public DCAP entry points. Only the
Azure verifier has a reason to relax TCB checks, so it reaches the
override through a crate-private variant; verify_dcap_attestation always
verifies at full strictness. The mock verifier likewise becomes its own
function rather than a cfg switch inside the production one, so
verify_dcap_attestation means Intel-rooted in every build and the
fixture tests replay real captures against it. AttestationVerifier is
the one place that picks between them.

BREAKING CHANGE: verify_attestation and verify_attestation_sync take a
VerifyMode; the DCAP and Azure entry points take mode before pccs and
the DCAP ones drop override_azure_outdated_tcb; the
*_with_given_timestamp variants are gone, replaced by
VerifyMode::Archived.

GCP checks and Archived mode
----------------------------

The GCP host provenance check from flashbots#54 stays live
in either mode, as does the firmware fetch for the quote's MRTD. Neither
rests on signed material a replay could re-verify: the provenance
document is an unsigned JSON object whose trust is the TLS connection to
Google's bucket, so archiving it would not make a replay stronger. The
docs on VerifyMode::Archived and verify_attestation state the carve-out.
Whether Archived should skip the provenance lookup instead is left open.

Closes flashbots#84
samlaf added a commit to SeismicSystems/attested-tls that referenced this pull request Aug 31, 2026
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>.
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
samlaf added a commit to SeismicSystems/attested-tls that referenced this pull request Aug 31, 2026
…fyMode

Second of two changes for flashbots#84, stacked on
flashbots#85.

Reporting the collateral a verification consumed is half of provenance.
The other half is running the same verification again later, against
that bundle, and getting the same answer. Nothing exposed that: the
public entry points always fetched, and the only way to supply a bundle
was through test-only variants that also took a bare timestamp.

Verification now takes a VerifyMode. Live fetches collateral and reads
the wall clock. Archived carries the bundle and the instant it was
collected, so the two can never be supplied apart - a pinned bundle
evaluated at the wrong instant is the mistake the old
(Option<collateral>, now) pair permitted. Azure holds its AK certificate
chain to the instant the DCAP leg reported, so both legs evaluate at one
time in either mode.

The Azure TCB override leaves the public DCAP entry points. Only the
Azure verifier has a reason to relax TCB checks, so it reaches the
override through a crate-private variant; verify_dcap_attestation always
verifies at full strictness. The mock verifier likewise becomes its own
function rather than a cfg switch inside the production one, so
verify_dcap_attestation means Intel-rooted in every build and the
fixture tests replay real captures against it. AttestationVerifier is
the one place that picks between them.

BREAKING CHANGE: verify_attestation and verify_attestation_sync take a
VerifyMode; the DCAP and Azure entry points take mode before pccs and
the DCAP ones drop override_azure_outdated_tcb; the
*_with_given_timestamp variants are gone, replaced by
VerifyMode::Archived.

GCP checks and Archived mode
----------------------------

The GCP host provenance check from flashbots#54 stays live
in either mode, as does the firmware fetch for the quote's MRTD. Neither
rests on signed material a replay could re-verify: the provenance
document is an unsigned JSON object whose trust is the TLS connection to
Google's bucket, so archiving it would not make a replay stronger. The
docs on VerifyMode::Archived and verify_attestation state the carve-out.
Whether Archived should skip the provenance lookup instead is left open.

Closes flashbots#84

@ameba23 ameba23 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great and +1 for using more RATS style terminology.

One thing to note - on GCP, the resulting VerifiedAttestation does not contain everything a this does not cover everything a future verifier would be interested in. Theres also potentially the signed firmware and result of the provenance check (which unfortunately is not signed). But i think thats not so important.

///
/// [RFC 9334]: https://www.rfc-editor.org/rfc/rfc9334.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for making this non exhaustive

/// PCRs, which measure the guest boot rather than the launched TD and
/// chain to the TD quote: its report data commits to the HCL var data
/// carrying the AK public key that signs the vTPM quote
pub measurements: MultiMeasurements,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was already mentioned elsewhere: In #79 this would be switched the the matched measurements from the policy (ExpectedMeasurements) rather than the actual measurements. Which in most cases is the same thing. But in case this does not play nice with what you are doing here, we could include both.

@ameba23
ameba23 merged commit 02276b8 into flashbots:main Sep 1, 2026
2 checks passed
@samlaf
samlaf deleted the sei-208/pr1-report-verified-collateral branch September 1, 2026 18:41
samlaf added a commit to SeismicSystems/attested-tls that referenced this pull request Sep 1, 2026
…fyMode

Second of two changes for flashbots#84, stacked on
flashbots#85.

Reporting the collateral a verification consumed is half of provenance.
The other half is running the same verification again later, against
that bundle, and getting the same answer. Nothing exposed that: the
public entry points always fetched, and the only way to supply a bundle
was through test-only variants that also took a bare timestamp.

Verification now takes a VerifyMode. Live fetches collateral and reads
the wall clock. Archived carries the bundle and the instant it was
collected, so the two can never be supplied apart - a pinned bundle
evaluated at the wrong instant is the mistake the old
(Option<collateral>, now) pair permitted. Azure holds its AK certificate
chain to the instant the DCAP leg reported, so both legs evaluate at one
time in either mode.

The Azure TCB override leaves the public DCAP entry points. Only the
Azure verifier has a reason to relax TCB checks, so it reaches the
override through a crate-private variant; verify_dcap_attestation always
verifies at full strictness. The mock verifier likewise becomes its own
function rather than a cfg switch inside the production one, so
verify_dcap_attestation means Intel-rooted in every build and the
fixture tests replay real captures against it. AttestationVerifier is
the one place that picks between them.

BREAKING CHANGE: verify_attestation and verify_attestation_sync take a
VerifyMode; the DCAP and Azure entry points take mode before pccs and
the DCAP ones drop override_azure_outdated_tcb; the
*_with_given_timestamp variants are gone, replaced by
VerifyMode::Archived.

GCP checks and Archived mode
----------------------------

The GCP host provenance check from flashbots#54 stays live
in either mode, as does the firmware fetch for the quote's MRTD. Neither
rests on signed material a replay could re-verify: the provenance
document is an unsigned JSON object whose trust is the TLS connection to
Google's bucket, so archiving it would not make a replay stronger. The
docs on VerifyMode::Archived and verify_attestation state the carve-out.
Whether Archived should skip the provenance lookup instead is left open.

Closes flashbots#84
samlaf added a commit to SeismicSystems/attested-tls that referenced this pull request Sep 1, 2026
…fyMode

Closes flashbots#84

Second of two changes for that issue, on top of flashbots#85.

Reporting the endorsements a verification consumed is half of provenance.
The other half is running the same verification again later, against
that snapshot, and getting the same answer. Nothing exposed that: the
public entry points always fetched and always read the wall clock, and
the only way to supply a bundle was through variants that also took a
bare timestamp.

API before and after
--------------------

    AttestationVerifier
      verify_attestation(msg, input)                -> verify_attestation(msg, input, mode)
      verify_attestation_sync(msg, input)           -> verify_attestation_sync(msg, input, mode)

    dcap
      verify_dcap_attestation(q, input, pccs)       -> verify_dcap_attestation(q, input, mode, pccs)
      verify_dcap_attestation_sync(q, input, pccs)  -> verify_dcap_attestation_sync(q, input, mode, pccs)
      verify_dcap_attestation_with_given_timestamp(
          q, input, pccs, Option<collateral>, now,
          override_azure_outdated_tcb)              -> (removed)
      verify_dcap_attestation_with_timestamp_sync(
          q, input, pccs, Option<collateral>, now,
          override_azure_outdated_tcb)              -> (removed)

    azure
      verify_azure_attestation(a, input, pccs, override)
                                                    -> verify_azure_attestation(a, input, mode, pccs, override)
      verify_azure_attestation_sync(a, input, pccs, override)
                                                    -> verify_azure_attestation_sync(a, input, mode, pccs, override)

    new
      enum VerifyMode { Live, Archived(EndorsementSnapshot) }
      DcapVerificationError::ArchivedWithoutDcapCollateral

Why this shape
--------------

One input instead of two. The removed variants took the collateral and
the instant as separate arguments, so a caller could pair a pinned bundle
with the wrong instant, or a live fetch with a pinned instant, and get a
verdict that reproduces nothing. VerifyMode::Archived takes the
EndorsementSnapshot that flashbots#85 hands back, so the bundle and the instant it
was held to travel together and the mistake has no spelling. The mixed
case is refused too: an archived snapshot with no bundle for the DCAP leg
fails with ArchivedWithoutDcapCollateral rather than being completed by a
fetch.

The mode reaches the verifier. The measurement-policy check lives on
AttestationVerifier, and a relying party re-checking archived evidence
needs both it and the pinned instant. The removed variants sat below the
verifier, so that combination did not exist. The mode is a parameter of
the call rather than the builder because it is a fact about one
verification, not about the verifier: the same instance serves a live
handshake and an archive replay.

One instant for both Azure legs. The DCAP leg reports the instant it
evaluated at, and the vTPM AK chain is checked at that same instant, in
either mode. The wall clock is read in exactly one place.

The Azure TCB override leaves the public surface. It rode along on the
removed variants only because the Azure verifier and the fixture tests
shared them. Both now call the crate-private body that the two public
entry points wrap, so the override is an argument of the Azure leg and
nothing else. Only Azure has a reason to relax TCB checks.

Live is behaviour-preserving. Every existing caller passes
VerifyMode::Live and gets what it got before: collateral from the PCCS or
Intel, freshness at the wall clock. The two in-tree callers, attested-tls
and attestation-provider-server, needed only that argument.

GCP checks and Archived mode
----------------------------

The GCP host provenance check from flashbots#54 stays live
in either mode, as does the firmware fetch for the quote's MRTD. Neither
rests on signed material a replay could re-verify: the provenance
document is an unsigned JSON object whose trust is the TLS connection to
Google's bucket, so archiving it would not make a replay stronger. The
docs on VerifyMode::Archived and verify_attestation state the carve-out.
Whether Archived should skip the provenance lookup instead is left open.

BREAKING CHANGE: verify_attestation and verify_attestation_sync take a
VerifyMode; the DCAP and Azure entry points take mode before pccs; the
*_with_given_timestamp variants are gone, replaced by
VerifyMode::Archived, which fails with
DcapVerificationError::ArchivedWithoutDcapCollateral when its snapshot
carries no DCAP bundle.
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.

2 participants