feat!(attestation): report the DCAP collateral a verification consumed - #85
Conversation
a055859 to
6ab5e69
Compare
…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 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. |
| /// verification always consumes exactly one collateral bundle, whichever | ||
| /// platform produced the evidence. | ||
| #[derive(Clone, Debug)] | ||
| pub struct AttestationResult { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
| /// verification always consumes exactly one collateral bundle, whichever | ||
| /// platform produced the evidence. | ||
| #[derive(Clone, Debug)] | ||
| pub struct AttestationResult { |
There was a problem hiding this comment.
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?
| /// 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 { |
There was a problem hiding this comment.
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.
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.
baca4a1 to
9b7c1ca
Compare
…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
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>.
21b8888 to
f828787
Compare
…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
left a comment
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
+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, |
There was a problem hiding this comment.
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.
…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
…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.
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
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.