From 2617ef19f53bf706738a53bbf2f5bbb4cb79653e Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 10:07:07 +0100 Subject: [PATCH 01/51] Refactor measurements code --- src/attestation/measurements.rs | 127 ++++++++++++++++++++ src/{attestation.rs => attestation/mod.rs} | 128 +-------------------- src/lib.rs | 4 +- src/main.rs | 2 +- src/test_helpers.rs | 2 +- 5 files changed, 134 insertions(+), 129 deletions(-) create mode 100644 src/attestation/measurements.rs rename src/{attestation.rs => attestation/mod.rs} (71%) diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs new file mode 100644 index 0000000..f7b547b --- /dev/null +++ b/src/attestation/measurements.rs @@ -0,0 +1,127 @@ +use crate::attestation::AttestationError; +use dcap_qvl::quote::Report; +use http::{header::InvalidHeaderValue, HeaderValue}; +use std::collections::HashMap; +use thiserror::Error; + +/// Measurements determined by the CVM platform +#[derive(Clone, PartialEq, Debug)] +pub struct PlatformMeasurements { + pub mrtd: [u8; 48], + pub rtmr0: [u8; 48], +} + +impl PlatformMeasurements { + pub fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { + let report = match quote.report { + Report::TD10(report) => report, + Report::TD15(report) => report.base, + Report::SgxEnclave(_) => { + return Err(AttestationError::SgxNotSupported); + } + }; + Ok(Self { + mrtd: report.mr_td, + rtmr0: report.rt_mr0, + }) + } + + pub fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { + Self { + mrtd: quote.mrtd(), + rtmr0: quote.rtmr0(), + } + } +} + +/// Measurements determined by the CVM image +#[derive(Clone, PartialEq, Debug)] +pub struct CvmImageMeasurements { + pub rtmr1: [u8; 48], + pub rtmr2: [u8; 48], + pub rtmr3: [u8; 48], +} + +impl CvmImageMeasurements { + pub fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { + let report = match quote.report { + Report::TD10(report) => report, + Report::TD15(report) => report.base, + Report::SgxEnclave(_) => { + return Err(AttestationError::SgxNotSupported); + } + }; + Ok(Self { + rtmr1: report.rt_mr1, + rtmr2: report.rt_mr2, + rtmr3: report.rt_mr3, + }) + } + + pub fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { + Self { + rtmr1: quote.rtmr1(), + rtmr2: quote.rtmr2(), + rtmr3: quote.rtmr3(), + } + } +} +#[derive(Debug, Clone, PartialEq)] +pub struct Measurements { + pub platform: PlatformMeasurements, + pub cvm_image: CvmImageMeasurements, +} + +impl Measurements { + pub fn to_header_format(&self) -> Result { + let mut measurements_map = HashMap::new(); + measurements_map.insert(0, hex::encode(self.platform.mrtd)); + measurements_map.insert(1, hex::encode(self.platform.rtmr0)); + measurements_map.insert(2, hex::encode(self.cvm_image.rtmr1)); + measurements_map.insert(3, hex::encode(self.cvm_image.rtmr2)); + measurements_map.insert(4, hex::encode(self.cvm_image.rtmr3)); + Ok(HeaderValue::from_str(&serde_json::to_string( + &measurements_map, + )?)?) + } + + pub fn from_header_format(input: &str) -> Result { + let measurements_map: HashMap = serde_json::from_str(input)?; + let measurements_map: HashMap = measurements_map + .into_iter() + .map(|(k, v)| (k, hex::decode(v).unwrap().try_into().unwrap())) + .collect(); + + Ok(Self { + platform: PlatformMeasurements { + mrtd: *measurements_map + .get(&0) + .ok_or(MeasurementFormatError::MissingValue("MRTD".to_string()))?, + rtmr0: *measurements_map + .get(&1) + .ok_or(MeasurementFormatError::MissingValue("RTMR0".to_string()))?, + }, + cvm_image: CvmImageMeasurements { + rtmr1: *measurements_map + .get(&2) + .ok_or(MeasurementFormatError::MissingValue("RTMR1".to_string()))?, + rtmr2: *measurements_map + .get(&3) + .ok_or(MeasurementFormatError::MissingValue("RTMR2".to_string()))?, + rtmr3: *measurements_map + .get(&4) + .ok_or(MeasurementFormatError::MissingValue("RTMR3".to_string()))?, + }, + }) + } +} + +#[derive(Error, Debug)] +pub enum MeasurementFormatError { + #[error("JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("Missing value: {0}")] + MissingValue(String), + #[error("Invalid header value: {0}")] + BadHeaderValue(#[from] InvalidHeaderValue), +} diff --git a/src/attestation.rs b/src/attestation/mod.rs similarity index 71% rename from src/attestation.rs rename to src/attestation/mod.rs index c4bb207..8e38989 100644 --- a/src/attestation.rs +++ b/src/attestation/mod.rs @@ -1,5 +1,7 @@ +pub mod measurements; + +use measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}; use std::{ - collections::HashMap, fmt::{self, Display, Formatter}, time::SystemTimeError, }; @@ -9,7 +11,6 @@ use dcap_qvl::{ collateral::get_collateral_for_fmspc, quote::{Quote, Report}, }; -use http::{header::InvalidHeaderValue, HeaderValue}; use sha2::{Digest, Sha256}; use tdx_quote::QuoteParseError; use thiserror::Error; @@ -19,66 +20,6 @@ use x509_parser::prelude::*; /// For fetching collateral directly from intel, if no PCCS is specified const PCS_URL: &str = "https://api.trustedservices.intel.com"; -#[derive(Debug, Clone, PartialEq)] -pub struct Measurements { - pub platform: PlatformMeasurements, - pub cvm_image: CvmImageMeasurements, -} - -impl Measurements { - pub fn to_header_format(&self) -> Result { - let mut measurements_map = HashMap::new(); - measurements_map.insert(0, hex::encode(self.platform.mrtd)); - measurements_map.insert(1, hex::encode(self.platform.rtmr0)); - measurements_map.insert(2, hex::encode(self.cvm_image.rtmr1)); - measurements_map.insert(3, hex::encode(self.cvm_image.rtmr2)); - measurements_map.insert(4, hex::encode(self.cvm_image.rtmr3)); - Ok(HeaderValue::from_str(&serde_json::to_string( - &measurements_map, - )?)?) - } - - pub fn from_header_format(input: &str) -> Result { - let measurements_map: HashMap = serde_json::from_str(input)?; - let measurements_map: HashMap = measurements_map - .into_iter() - .map(|(k, v)| (k, hex::decode(v).unwrap().try_into().unwrap())) - .collect(); - - Ok(Self { - platform: PlatformMeasurements { - mrtd: *measurements_map - .get(&0) - .ok_or(MeasurementFormatError::MissingValue("MRTD".to_string()))?, - rtmr0: *measurements_map - .get(&1) - .ok_or(MeasurementFormatError::MissingValue("RTMR0".to_string()))?, - }, - cvm_image: CvmImageMeasurements { - rtmr1: *measurements_map - .get(&2) - .ok_or(MeasurementFormatError::MissingValue("RTMR1".to_string()))?, - rtmr2: *measurements_map - .get(&3) - .ok_or(MeasurementFormatError::MissingValue("RTMR2".to_string()))?, - rtmr3: *measurements_map - .get(&4) - .ok_or(MeasurementFormatError::MissingValue("RTMR3".to_string()))?, - }, - }) - } -} - -#[derive(Error, Debug)] -pub enum MeasurementFormatError { - #[error("JSON: {0}")] - Json(#[from] serde_json::Error), - #[error("Missing value: {0}")] - MissingValue(String), - #[error("Invalid header value: {0}")] - BadHeaderValue(#[from] InvalidHeaderValue), -} - /// Type of attestaion used /// Only supported (or soon-to-be supported) types are given #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -164,69 +105,6 @@ impl QuoteGenerator for DcapTdxQuoteGenerator { } } -/// Measurements determined by the CVM platform -#[derive(Clone, PartialEq, Debug)] -pub struct PlatformMeasurements { - pub mrtd: [u8; 48], - pub rtmr0: [u8; 48], -} - -impl PlatformMeasurements { - fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { - let report = match quote.report { - Report::TD10(report) => report, - Report::TD15(report) => report.base, - Report::SgxEnclave(_) => { - return Err(AttestationError::SgxNotSupported); - } - }; - Ok(Self { - mrtd: report.mr_td, - rtmr0: report.rt_mr0, - }) - } - - fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { - Self { - mrtd: quote.mrtd(), - rtmr0: quote.rtmr0(), - } - } -} - -/// Measurements determined by the CVM image -#[derive(Clone, PartialEq, Debug)] -pub struct CvmImageMeasurements { - pub rtmr1: [u8; 48], - pub rtmr2: [u8; 48], - pub rtmr3: [u8; 48], -} - -impl CvmImageMeasurements { - fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { - let report = match quote.report { - Report::TD10(report) => report, - Report::TD15(report) => report.base, - Report::SgxEnclave(_) => { - return Err(AttestationError::SgxNotSupported); - } - }; - Ok(Self { - rtmr1: report.rt_mr1, - rtmr2: report.rt_mr2, - rtmr3: report.rt_mr3, - }) - } - - fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { - Self { - rtmr1: quote.rtmr1(), - rtmr2: quote.rtmr2(), - rtmr3: quote.rtmr3(), - } - } -} - /// Verify DCAP TDX quotes, allowing them if they have one of a given set of platform-specific and /// OS image specific measurements #[derive(Clone)] diff --git a/src/lib.rs b/src/lib.rs index f5720e6..ad78ad4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ pub mod attestation; -use attestation::{AttestationError, AttestationType, Measurements}; +use attestation::{measurements::Measurements, AttestationError, AttestationType}; pub use attestation::{ DcapTdxQuoteGenerator, DcapTdxQuoteVerifier, NoQuoteGenerator, NoQuoteVerifier, QuoteGenerator, QuoteVerifier, @@ -690,7 +690,7 @@ fn server_name_from_host( #[cfg(test)] mod tests { - use crate::attestation::CvmImageMeasurements; + use crate::attestation::measurements::CvmImageMeasurements; use super::*; use test_helpers::{ diff --git a/src/main.rs b/src/main.rs index e3ab26d..c6ee9be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use std::{fs::File, net::SocketAddr, path::PathBuf}; use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; use attested_tls_proxy::{ - attestation::{AttestationType, CvmImageMeasurements}, + attestation::{measurements::CvmImageMeasurements, AttestationType}, get_tls_cert, DcapTdxQuoteGenerator, DcapTdxQuoteVerifier, NoQuoteGenerator, NoQuoteVerifier, ProxyClient, ProxyServer, TlsCertAndKey, }; diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 14d3c34..c48e1ee 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -12,7 +12,7 @@ use tokio_rustls::rustls::{ }; use crate::{ - attestation::{CvmImageMeasurements, Measurements, PlatformMeasurements}, + attestation::measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, MEASUREMENT_HEADER, }; From 4c8d97a388d420e537715ca9f4edf6e92a470033 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 10:23:16 +0100 Subject: [PATCH 02/51] Add WIP quote generation --- Cargo.lock | 608 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + shell.nix | 11 + src/attestation/azure.rs | 23 ++ src/attestation/dcap.rs | 117 ++++++++ src/attestation/mod.rs | 118 +------- src/lib.rs | 6 +- 7 files changed, 752 insertions(+), 132 deletions(-) create mode 100644 shell.nix create mode 100644 src/attestation/azure.rs create mode 100644 src/attestation/dcap.rs diff --git a/Cargo.lock b/Cargo.lock index 60a6dd8..ecf11a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.21" @@ -76,7 +85,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 2.0.17", "time", ] @@ -132,6 +141,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "az-tdx-vtpm", "bytes", "clap", "configfs-tsm", @@ -149,7 +159,7 @@ dependencies = [ "serde_json", "sha2", "tdx-quote", - "thiserror", + "thiserror 2.0.17", "tokio", "tokio-rustls", "webpki-roots", @@ -214,24 +224,142 @@ dependencies = [ "tracing", ] +[[package]] +name = "az-cvm-vtpm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3d0900c6757c9674b05b0479236458297026e25fb505186dc8d7735091a21c" +dependencies = [ + "bincode 1.3.3", + "jsonwebkey", + "memoffset", + "openssl", + "serde", + "serde-big-array", + "serde_json", + "sev", + "sha2", + "thiserror 2.0.17", + "tss-esapi", + "zerocopy", +] + +[[package]] +name = "az-tdx-vtpm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04849677b3c0704d4593d89940cde0dc0caad2202bf9fb29352e153782b91ff8" +dependencies = [ + "az-cvm-vtpm", + "base64-url", + "bincode 1.3.3", + "serde", + "serde_json", + "thiserror 2.0.17", + "ureq", + "zerocopy", +] + [[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-url" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5b428e9fb429c6fda7316e9b006f993e6b4c33005e4659339fb5214479dddec" +dependencies = [ + "base64 0.22.1", +] + [[package]] name = "base64ct" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitfield" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf79f42d21f18b5926a959280215903e659760da994835d27c3a0c5ff4f898f" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6115af052c7914c0cbb97195e5c72cb61c511527250074f5c041d1048b0d8b16" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -378,6 +506,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "codicon" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12170080f3533d6f09a19f81596f836854d0fa4867dc32c8172b8474b4e9de61" + [[package]] name = "colorchoice" version = "1.0.4" @@ -517,7 +651,7 @@ checksum = "435989ce7ba46ba3f837f9df3c8139469e72ae810e707893b19f8b6b370d14ef" dependencies = [ "anyhow", "asn1_der", - "base64", + "base64 0.22.1", "borsh", "byteorder", "chrono", @@ -631,6 +765,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -708,6 +863,26 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -748,6 +923,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -937,7 +1127,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -960,7 +1150,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -974,6 +1164,12 @@ dependencies = [ "digest", ] +[[package]] +name = "hostname-validator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" + [[package]] name = "http" version = "1.3.1" @@ -1065,7 +1261,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1206,6 +1402,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "iocuddle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8972d5be69940353d5347a1344cb375d9b457d6809b428b05bb1ca2fb9ce007" + [[package]] name = "ipconfig" version = "0.3.2" @@ -1256,6 +1458,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebkey" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57c852b14147e2bd58c14fde40398864453403ef632b1101db130282ee6e2cc" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "generic-array", + "num-bigint", + "serde", + "serde_json", + "thiserror 1.0.69", + "yasna 0.4.0", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -1289,6 +1508,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + [[package]] name = "litemap" version = "0.8.1" @@ -1322,12 +1551,31 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "mbox" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d142aeadbc4e8c679fc6d93fbe7efe1c021fa7d80629e615915b519e3bc6de" +dependencies = [ + "libc", + "stable_deref_trait", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1411,6 +1659,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1441,6 +1700,15 @@ dependencies = [ "libm", ] +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -1466,6 +1734,60 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "p256" version = "0.13.2" @@ -1560,7 +1882,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -1579,6 +1901,41 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "picky-asn1" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295eea0f33c16be21e2a98b908fdd4d73c04dd48c8480991b76dbcf0cb58b212" +dependencies = [ + "oid", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-der" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df7873a9e36d42dadb393bea5e211fe83d793c172afad5fb4ec846ec582793f" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5f20f71a68499ff32310f418a6fad8816eac1a2859ed3f0c5c741389dd6208" +dependencies = [ + "base64 0.21.7", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1612,6 +1969,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1683,7 +2046,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.1", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -1704,7 +2067,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -1813,7 +2176,16 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "yasna", + "yasna 0.5.2", +] + +[[package]] +name = "rdrand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" +dependencies = [ + "rand_core 0.6.4", ] [[package]] @@ -1822,16 +2194,56 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", ] +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "reqwest" version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -2069,6 +2481,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-human-bytes" version = "0.1.1" @@ -2079,6 +2500,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2136,6 +2567,32 @@ dependencies = [ "serde", ] +[[package]] +name = "sev" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420c6c161b5d6883d8195584a802b114af6c884ed56d937d994e30f7f81d54ec" +dependencies = [ + "base64 0.22.1", + "bincode 2.0.1", + "bitfield 0.19.3", + "bitflags 2.10.0", + "byteorder", + "codicon", + "dirs", + "hex", + "iocuddle", + "lazy_static", + "libc", + "openssl", + "rdrand", + "serde", + "serde-big-array", + "serde_bytes", + "static_assertions", + "uuid", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2226,6 +2683,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2281,6 +2744,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tdx-quote" version = "0.0.4" @@ -2295,13 +2764,33 @@ dependencies = [ "x509-verify", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2461,7 +2950,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2523,6 +3012,39 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tss-esapi" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ea9ccde878b029392ac97b5be1f470173d06ea41d18ad0bb3c92794c16a0f2" +dependencies = [ + "bitfield 0.14.0", + "enumflags2", + "getrandom 0.2.16", + "hostname-validator", + "log", + "mbox", + "num-derive", + "num-traits", + "oid", + "picky-asn1", + "picky-asn1-x509", + "regex", + "serde", + "tss-esapi-sys", + "zeroize", +] + +[[package]] +name = "tss-esapi-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535cd192581c2ec4d5f82e670b1d3fbba6a23ccce8c85de387642051d7cad5b5" +dependencies = [ + "pkg-config", + "target-lexicon", +] + [[package]] name = "typenum" version = "1.19.0" @@ -2547,6 +3069,26 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "log", + "once_cell", + "serde", + "serde_json", + "url", +] + [[package]] name = "url" version = "2.5.7" @@ -2585,15 +3127,28 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.4", "js-sys", + "serde", "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "want" version = "0.3.1" @@ -3015,7 +3570,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 2.0.17", "time", ] @@ -3041,6 +3596,15 @@ dependencies = [ "x509-ocsp", ] +[[package]] +name = "yasna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" +dependencies = [ + "num-bigint", +] + [[package]] name = "yasna" version = "0.5.2" @@ -3119,6 +3683,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 24c5994..64e1263 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ http-body-util = "0.1.3" bytes = "1.10.1" http = "1.3.1" serde_json = "1.0.145" +az-tdx-vtpm = "0.7.4" [dev-dependencies] rcgen = "0.14.5" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..f852fe5 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + + buildInputs = with pkgs;[ + tpm2-tss + ]; +} diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs new file mode 100644 index 0000000..66f1006 --- /dev/null +++ b/src/attestation/azure.rs @@ -0,0 +1,23 @@ +use tokio_rustls::rustls::pki_types::CertificateDer; + +use crate::attestation::{compute_report_input, AttestationError, AttestationType, QuoteGenerator}; + +#[derive(Clone)] +pub struct MaaQuoteGenerator {} + +impl QuoteGenerator for MaaQuoteGenerator { + /// Type of attestation used + fn attestation_type(&self) -> AttestationType { + AttestationType::AzureTdx + } + + fn create_attestation( + &self, + cert_chain: &[CertificateDer<'_>], + exporter: [u8; 32], + ) -> Result, AttestationError> { + let quote_input = compute_report_input(cert_chain, exporter)?; + + todo!() + } +} diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs new file mode 100644 index 0000000..9dd86be --- /dev/null +++ b/src/attestation/dcap.rs @@ -0,0 +1,117 @@ +use crate::attestation::{ + compute_report_input, generate_quote, get_quote_input_data, + measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, + AttestationError, AttestationType, QuoteGenerator, QuoteVerifier, PCS_URL, +}; + +use dcap_qvl::{collateral::get_collateral_for_fmspc, quote::Quote}; +use tokio_rustls::rustls::pki_types::CertificateDer; + +/// Quote generation using configfs_tsm +#[derive(Clone)] +pub struct DcapTdxQuoteGenerator { + pub attestation_type: AttestationType, +} + +impl QuoteGenerator for DcapTdxQuoteGenerator { + /// Type of attestation used + fn attestation_type(&self) -> AttestationType { + self.attestation_type + } + + fn create_attestation( + &self, + cert_chain: &[CertificateDer<'_>], + exporter: [u8; 32], + ) -> Result, AttestationError> { + let quote_input = compute_report_input(cert_chain, exporter)?; + + Ok(generate_quote(quote_input)?) + } +} + +/// Verify DCAP TDX quotes, allowing them if they have one of a given set of platform-specific and +/// OS image specific measurements +#[derive(Clone)] +pub struct DcapTdxQuoteVerifier { + pub attestation_type: AttestationType, + /// Platform specific allowed Measurements + /// Currently an option as this may be determined internally on a per-platform basis (Eg: GCP) + pub accepted_platform_measurements: Option>, + /// OS-image specific allows measurement - this is effectively a list of allowed OS images + pub accepted_cvm_image_measurements: Vec, + /// URL of a PCCS (defaults to Intel PCS) + pub pccs_url: Option, +} + +impl QuoteVerifier for DcapTdxQuoteVerifier { + /// Type of attestation used + fn attestation_type(&self) -> AttestationType { + self.attestation_type + } + + async fn verify_attestation( + &self, + input: Vec, + cert_chain: &[CertificateDer<'_>], + exporter: [u8; 32], + ) -> Result, AttestationError> { + let quote_input = compute_report_input(cert_chain, exporter)?; + let (platform_measurements, image_measurements) = if cfg!(not(test)) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let quote = Quote::parse(&input)?; + + let ca = quote.ca()?; + let fmspc = hex::encode_upper(quote.fmspc()?); + let collateral = get_collateral_for_fmspc( + &self.pccs_url.clone().unwrap_or(PCS_URL.to_string()), + fmspc, + ca, + false, + ) + .await?; + + let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?; + + let measurements = ( + PlatformMeasurements::from_dcap_qvl_quote("e)?, + CvmImageMeasurements::from_dcap_qvl_quote("e)?, + ); + if get_quote_input_data(quote.report) != quote_input { + return Err(AttestationError::InputMismatch); + } + measurements + } else { + // In tests we use mock quotes which will fail to verify + let quote = tdx_quote::Quote::from_bytes(&input)?; + if quote.report_input_data() != quote_input { + return Err(AttestationError::InputMismatch); + } + + ( + PlatformMeasurements::from_tdx_quote("e), + CvmImageMeasurements::from_tdx_quote("e), + ) + }; + + if let Some(accepted_platform_measurements) = &self.accepted_platform_measurements + && !accepted_platform_measurements.contains(&platform_measurements) + { + return Err(AttestationError::UnacceptablePlatformMeasurements); + } + + if !self + .accepted_cvm_image_measurements + .contains(&image_measurements) + { + return Err(AttestationError::UnacceptableOsImageMeasurements); + } + + Ok(Some(Measurements { + platform: platform_measurements, + cvm_image: image_measurements, + })) + } +} diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 8e38989..a018d8e 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -1,16 +1,15 @@ +pub mod azure; +pub mod dcap; pub mod measurements; -use measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}; +use measurements::Measurements; use std::{ fmt::{self, Display, Formatter}, time::SystemTimeError, }; use configfs_tsm::QuoteGenerationError; -use dcap_qvl::{ - collateral::get_collateral_for_fmspc, - quote::{Quote, Report}, -}; +use dcap_qvl::quote::Report; use sha2::{Digest, Sha256}; use tdx_quote::QuoteParseError; use thiserror::Error; @@ -82,115 +81,6 @@ pub trait QuoteVerifier: Clone + Send + 'static { ) -> impl Future, AttestationError>> + Send; } -/// Quote generation using configfs_tsm -#[derive(Clone)] -pub struct DcapTdxQuoteGenerator { - pub attestation_type: AttestationType, -} - -impl QuoteGenerator for DcapTdxQuoteGenerator { - /// Type of attestation used - fn attestation_type(&self) -> AttestationType { - self.attestation_type - } - - fn create_attestation( - &self, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], - ) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; - - Ok(generate_quote(quote_input)?) - } -} - -/// Verify DCAP TDX quotes, allowing them if they have one of a given set of platform-specific and -/// OS image specific measurements -#[derive(Clone)] -pub struct DcapTdxQuoteVerifier { - pub attestation_type: AttestationType, - /// Platform specific allowed Measurements - /// Currently an option as this may be determined internally on a per-platform basis (Eg: GCP) - pub accepted_platform_measurements: Option>, - /// OS-image specific allows measurement - this is effectively a list of allowed OS images - pub accepted_cvm_image_measurements: Vec, - /// URL of a PCCS (defaults to Intel PCS) - pub pccs_url: Option, -} - -impl QuoteVerifier for DcapTdxQuoteVerifier { - /// Type of attestation used - fn attestation_type(&self) -> AttestationType { - self.attestation_type - } - - async fn verify_attestation( - &self, - input: Vec, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], - ) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; - let (platform_measurements, image_measurements) = if cfg!(not(test)) { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_secs(); - let quote = Quote::parse(&input)?; - - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let collateral = get_collateral_for_fmspc( - &self.pccs_url.clone().unwrap_or(PCS_URL.to_string()), - fmspc, - ca, - false, - ) - .await?; - - let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?; - - let measurements = ( - PlatformMeasurements::from_dcap_qvl_quote("e)?, - CvmImageMeasurements::from_dcap_qvl_quote("e)?, - ); - if get_quote_input_data(quote.report) != quote_input { - return Err(AttestationError::InputMismatch); - } - measurements - } else { - // In tests we use mock quotes which will fail to verify - let quote = tdx_quote::Quote::from_bytes(&input)?; - if quote.report_input_data() != quote_input { - return Err(AttestationError::InputMismatch); - } - - ( - PlatformMeasurements::from_tdx_quote("e), - CvmImageMeasurements::from_tdx_quote("e), - ) - }; - - if let Some(accepted_platform_measurements) = &self.accepted_platform_measurements - && !accepted_platform_measurements.contains(&platform_measurements) - { - return Err(AttestationError::UnacceptablePlatformMeasurements); - } - - if !self - .accepted_cvm_image_measurements - .contains(&image_measurements) - { - return Err(AttestationError::UnacceptableOsImageMeasurements); - } - - Ok(Some(Measurements { - platform: platform_measurements, - cvm_image: image_measurements, - })) - } -} - /// Given a [Report] get the input data regardless of report type fn get_quote_input_data(report: Report) -> [u8; 64] { match report { diff --git a/src/lib.rs b/src/lib.rs index ad78ad4..8c13211 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ pub mod attestation; -use attestation::{measurements::Measurements, AttestationError, AttestationType}; pub use attestation::{ - DcapTdxQuoteGenerator, DcapTdxQuoteVerifier, NoQuoteGenerator, NoQuoteVerifier, QuoteGenerator, - QuoteVerifier, + dcap::{DcapTdxQuoteGenerator, DcapTdxQuoteVerifier}, + NoQuoteGenerator, NoQuoteVerifier, QuoteGenerator, QuoteVerifier, }; +use attestation::{measurements::Measurements, AttestationError, AttestationType}; use bytes::Bytes; use http::HeaderValue; use http_body_util::combinators::BoxBody; From ce3d47d68f17e211da44e0ddecc83433cc33b7e9 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 10:34:38 +0100 Subject: [PATCH 03/51] Update CI to include dependency for azure --- .github/workflows/test.yml | 5 +++++ src/attestation/azure.rs | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93e8fc1..82d0005 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,11 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libtss2-dev + - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 66f1006..b29c525 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -1,4 +1,6 @@ +use az_tdx_vtpm::{hcl, imds, report, tdx, vtpm}; use tokio_rustls::rustls::pki_types::CertificateDer; +// use openssl::pkey::{PKey, Public}; use crate::attestation::{compute_report_input, AttestationError, AttestationType, QuoteGenerator}; @@ -18,6 +20,23 @@ impl QuoteGenerator for MaaQuoteGenerator { ) -> Result, AttestationError> { let quote_input = compute_report_input(cert_chain, exporter)?; + let td_report = report::get_report().unwrap(); + let td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); + + let bytes = vtpm::get_report().unwrap(); + let hcl_report = hcl::HclReport::new(bytes).unwrap(); + let var_data_hash = hcl_report.var_data_sha256(); + let ak_pub = hcl_report.ak_pub().unwrap(); + + let td_report: tdx::TdReport = hcl_report.try_into().unwrap(); + assert!(var_data_hash == td_report.report_mac.reportdata[..32]); + + // let nonce = "a nonce".as_bytes(); + // + // let tpm_quote = vtpm::get_quote(nonce).unwrap(); + // let der = ak_pub.key.try_to_der().unwrap(); + // let pub_key = PKey::public_key_from_der(&der).unwrap(); + // tpm_quote.verify(&pub_key, nonce).unwrap(); todo!() } } From 1f688490f146f03c338fb66ea88a0f8dd0ee33d5 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 10:51:05 +0100 Subject: [PATCH 04/51] Comments --- src/attestation/azure.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index b29c525..060dcd8 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -21,6 +21,14 @@ impl QuoteGenerator for MaaQuoteGenerator { let quote_input = compute_report_input(cert_chain, exporter)?; let td_report = report::get_report().unwrap(); + + // let mrtd = td_report.tdinfo.mrtd; + // let rtmr0 = td_report.tdinfo.rtrm[0]; + // let rtmr1 = td_report.tdinfo.rtrm[1]; + // let rtmr2 = td_report.tdinfo.rtrm[2]; + // let rtmr3 = td_report.tdinfo.rtrm[3]; + + // This makes a request to Azure Instance metadata service and gives us a binary response let td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); let bytes = vtpm::get_report().unwrap(); From e9329575b5e080e383153babf658defe62dbe457 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 10:51:57 +0100 Subject: [PATCH 05/51] Clippy --- src/attestation/azure.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 060dcd8..c0016db 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -18,7 +18,7 @@ impl QuoteGenerator for MaaQuoteGenerator { cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], ) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; + let _quote_input = compute_report_input(cert_chain, exporter)?; let td_report = report::get_report().unwrap(); @@ -29,12 +29,12 @@ impl QuoteGenerator for MaaQuoteGenerator { // let rtmr3 = td_report.tdinfo.rtrm[3]; // This makes a request to Azure Instance metadata service and gives us a binary response - let td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); + let _td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); let bytes = vtpm::get_report().unwrap(); let hcl_report = hcl::HclReport::new(bytes).unwrap(); let var_data_hash = hcl_report.var_data_sha256(); - let ak_pub = hcl_report.ak_pub().unwrap(); + let _ak_pub = hcl_report.ak_pub().unwrap(); let td_report: tdx::TdReport = hcl_report.try_into().unwrap(); assert!(var_data_hash == td_report.report_mac.reportdata[..32]); From 739507fa5968b98b3a432569714d661c035c44fb Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 12:03:23 +0100 Subject: [PATCH 06/51] WIP - get JWT from azure api --- Cargo.lock | 2 + Cargo.toml | 4 +- src/attestation/azure.rs | 92 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecf11a5..f846d84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,7 @@ dependencies = [ "anyhow", "axum", "az-tdx-vtpm", + "base64 0.22.1", "bytes", "clap", "configfs-tsm", @@ -156,6 +157,7 @@ dependencies = [ "rcgen", "reqwest", "rustls-pemfile", + "serde", "serde_json", "sha2", "tdx-quote", diff --git a/Cargo.toml b/Cargo.toml index 64e1263..e37bf56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,10 @@ bytes = "1.10.1" http = "1.3.1" serde_json = "1.0.145" az-tdx-vtpm = "0.7.4" +serde = "1.0.228" +base64 = "0.22.1" +reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls-webpki-roots-no-provider"] } [dev-dependencies] rcgen = "0.14.5" axum = "0.8.6" -reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls-webpki-roots-no-provider"] } diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index c0016db..0dc3b03 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -1,11 +1,17 @@ use az_tdx_vtpm::{hcl, imds, report, tdx, vtpm}; use tokio_rustls::rustls::pki_types::CertificateDer; // use openssl::pkey::{PKey, Public}; +use base64::prelude::*; +use reqwest::Client; +use serde::Serialize; use crate::attestation::{compute_report_input, AttestationError, AttestationType, QuoteGenerator}; #[derive(Clone)] -pub struct MaaQuoteGenerator {} +pub struct MaaQuoteGenerator { + maa_endpoint: String, + aad_access_token: String, +} impl QuoteGenerator for MaaQuoteGenerator { /// Type of attestation used @@ -18,7 +24,7 @@ impl QuoteGenerator for MaaQuoteGenerator { cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], ) -> Result, AttestationError> { - let _quote_input = compute_report_input(cert_chain, exporter)?; + let quote_input = compute_report_input(cert_chain, exporter)?; let td_report = report::get_report().unwrap(); @@ -29,15 +35,19 @@ impl QuoteGenerator for MaaQuoteGenerator { // let rtmr3 = td_report.tdinfo.rtrm[3]; // This makes a request to Azure Instance metadata service and gives us a binary response - let _td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); + let td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); - let bytes = vtpm::get_report().unwrap(); - let hcl_report = hcl::HclReport::new(bytes).unwrap(); - let var_data_hash = hcl_report.var_data_sha256(); - let _ak_pub = hcl_report.ak_pub().unwrap(); + let hcl_report_bytes = vtpm::get_report_with_report_data("e_input).unwrap(); + let hcl_report = hcl::HclReport::new(hcl_report_bytes).unwrap(); + let hcl_var_data = hcl_report.var_data(); - let td_report: tdx::TdReport = hcl_report.try_into().unwrap(); - assert!(var_data_hash == td_report.report_mac.reportdata[..32]); + // let bytes = vtpm::get_report().unwrap(); + // let hcl_report = hcl::HclReport::new(bytes).unwrap(); + // let var_data_hash = hcl_report.var_data_sha256(); + // let _ak_pub = hcl_report.ak_pub().unwrap(); + // + // let td_report: tdx::TdReport = hcl_report.try_into().unwrap(); + // assert!(var_data_hash == td_report.report_mac.reportdata[..32]); // let nonce = "a nonce".as_bytes(); // @@ -45,6 +55,70 @@ impl QuoteGenerator for MaaQuoteGenerator { // let der = ak_pub.key.try_to_der().unwrap(); // let pub_key = PKey::public_key_from_der(&der).unwrap(); // tpm_quote.verify(&pub_key, nonce).unwrap(); + + let quote_b64 = BASE64_STANDARD.encode(&td_quote_bytes); + let runtime_b64 = BASE64_STANDARD.encode(hcl_var_data); + + let body = TdxVmRequest { + quote: quote_b64, + runtimeData: Some(RuntimeData { + data: runtime_b64, + data_type: "Binary", + }), + nonce: Some("my-app-nonce-or-session-id".to_string()), + }; + let body_bytes = serde_json::to_vec(&body).unwrap(); + let jwt_token = self.call_tdxvm_attestation(body_bytes).await; todo!() } } + +impl MaaQuoteGenerator { + /// Get a signed JWT from the azure API + async fn call_tdxvm_attestation( + &self, + body_bytes: Vec, + ) -> Result> { + let url = format!("{}/attest/TdxVm?api-version=2025-06-01", self.maa_endpoint); + + let client = Client::new(); + let res = client + .post(&url) + .bearer_auth(&self.aad_access_token) + .header("Content-Type", "application/json") + .body(body_bytes) + .send() + .await?; + + let status = res.status(); + let text = res.text().await?; + + if !status.is_success() { + return Err(format!("MAA attestation failed: {status} {text}").into()); + } + + #[derive(serde::Deserialize)] + struct AttestationResponse { + token: String, + } + + let parsed: AttestationResponse = serde_json::from_str(&text)?; + Ok(parsed.token) // Microsoft-signed JWT + } +} + +#[derive(Serialize)] +struct RuntimeData<'a> { + data: String, // base64url of VarData bytes + #[serde(rename = "dataType")] + data_type: &'a str, // "Binary" in our case +} + +#[derive(Serialize)] +struct TdxVmRequest<'a> { + quote: String, // base64url(TDX quote) + #[serde(skip_serializing_if = "Option::is_none")] + runtimeData: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + nonce: Option, +} From 33c733d3f4e0298749d0f95dd826fb2bd24734d5 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 13:29:12 +0100 Subject: [PATCH 07/51] Begin verification fn --- src/attestation/azure.rs | 47 +++++++++++++++++++++++++++++----------- src/attestation/dcap.rs | 2 +- src/attestation/mod.rs | 6 ++--- src/lib.rs | 7 ++++-- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 0dc3b03..9ec06f3 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -1,4 +1,4 @@ -use az_tdx_vtpm::{hcl, imds, report, tdx, vtpm}; +use az_tdx_vtpm::{hcl, imds, report, vtpm}; use tokio_rustls::rustls::pki_types::CertificateDer; // use openssl::pkey::{PKey, Public}; use base64::prelude::*; @@ -7,24 +7,26 @@ use serde::Serialize; use crate::attestation::{compute_report_input, AttestationError, AttestationType, QuoteGenerator}; +use super::QuoteVerifier; + #[derive(Clone)] -pub struct MaaQuoteGenerator { +pub struct MaaGenerator { maa_endpoint: String, aad_access_token: String, } -impl QuoteGenerator for MaaQuoteGenerator { +impl QuoteGenerator for MaaGenerator { /// Type of attestation used fn attestation_type(&self) -> AttestationType { AttestationType::AzureTdx } - fn create_attestation( + async fn create_attestation( &self, cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], ) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; + let input_data = compute_report_input(cert_chain, exporter)?; let td_report = report::get_report().unwrap(); @@ -37,7 +39,7 @@ impl QuoteGenerator for MaaQuoteGenerator { // This makes a request to Azure Instance metadata service and gives us a binary response let td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); - let hcl_report_bytes = vtpm::get_report_with_report_data("e_input).unwrap(); + let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data).unwrap(); let hcl_report = hcl::HclReport::new(hcl_report_bytes).unwrap(); let hcl_var_data = hcl_report.var_data(); @@ -61,19 +63,19 @@ impl QuoteGenerator for MaaQuoteGenerator { let body = TdxVmRequest { quote: quote_b64, - runtimeData: Some(RuntimeData { + runtime_data: Some(RuntimeData { data: runtime_b64, data_type: "Binary", }), nonce: Some("my-app-nonce-or-session-id".to_string()), }; let body_bytes = serde_json::to_vec(&body).unwrap(); - let jwt_token = self.call_tdxvm_attestation(body_bytes).await; - todo!() + let jwt_token = self.call_tdxvm_attestation(body_bytes).await.unwrap(); + Ok(jwt_token.as_bytes().to_vec()) } } -impl MaaQuoteGenerator { +impl MaaGenerator { /// Get a signed JWT from the azure API async fn call_tdxvm_attestation( &self, @@ -107,6 +109,25 @@ impl MaaQuoteGenerator { } } +#[derive(Clone)] +pub struct MaaVerifier; + +impl QuoteVerifier for MaaVerifier { + fn attestation_type(&self) -> AttestationType { + AttestationType::AzureTdx + } + + async fn verify_attestation( + &self, + input: Vec, + cert_chain: &[CertificateDer<'_>], + exporter: [u8; 32], + ) -> Result, AttestationError> { + let input_data = compute_report_input(cert_chain, exporter)?; + todo!() + } +} + #[derive(Serialize)] struct RuntimeData<'a> { data: String, // base64url of VarData bytes @@ -116,9 +137,9 @@ struct RuntimeData<'a> { #[derive(Serialize)] struct TdxVmRequest<'a> { - quote: String, // base64url(TDX quote) - #[serde(skip_serializing_if = "Option::is_none")] - runtimeData: Option>, + quote: String, // base64 (TDX quote) + #[serde(rename = "runtimeData", skip_serializing_if = "Option::is_none")] + runtime_data: Option>, #[serde(skip_serializing_if = "Option::is_none")] nonce: Option, } diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index 9dd86be..982586d 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -19,7 +19,7 @@ impl QuoteGenerator for DcapTdxQuoteGenerator { self.attestation_type } - fn create_attestation( + async fn create_attestation( &self, cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index a018d8e..8e378c8 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -54,7 +54,7 @@ impl Display for AttestationType { } } -/// Defines how to generate a quote +/// Defines how to generate an attestation pub trait QuoteGenerator: Clone + Send + 'static { /// Type of attestation used fn attestation_type(&self) -> AttestationType; @@ -64,7 +64,7 @@ pub trait QuoteGenerator: Clone + Send + 'static { &self, cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], - ) -> Result, AttestationError>; + ) -> impl Future, AttestationError>> + Send; } /// Defines how to verify a quote @@ -114,7 +114,7 @@ impl QuoteGenerator for NoQuoteGenerator { } /// Create an empty attestation - fn create_attestation( + async fn create_attestation( &self, _cert_chain: &[CertificateDer<'_>], _exporter: [u8; 32], diff --git a/src/lib.rs b/src/lib.rs index 8c13211..6bd62e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,7 +193,9 @@ impl ProxyServer { let remote_cert_chain = connection.peer_certificates().map(|c| c.to_owned()); let attestation = if local_quote_generator.attestation_type() != AttestationType::None { - local_quote_generator.create_attestation(&cert_chain, exporter)? + local_quote_generator + .create_attestation(&cert_chain, exporter) + .await? } else { Vec::new() }; @@ -508,7 +510,8 @@ impl ProxyClient { let attestation = if local_quote_generator.attestation_type() != AttestationType::None { local_quote_generator - .create_attestation(&cert_chain.ok_or(ProxyError::NoClientAuth)?, exporter)? + .create_attestation(&cert_chain.ok_or(ProxyError::NoClientAuth)?, exporter) + .await? } else { Vec::new() }; From 1220af510053b1b8e07baadeeae20a20634dad98 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 13 Nov 2025 10:25:46 +0100 Subject: [PATCH 08/51] Add test asset --- test-assets/hclreport.bin | Bin 0 -> 2600 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test-assets/hclreport.bin diff --git a/test-assets/hclreport.bin b/test-assets/hclreport.bin new file mode 100644 index 0000000000000000000000000000000000000000..ff9494a0f9fd9a89fd32f74429c5047b21dacdad GIT binary patch literal 2600 zcmeHHTZ|J`814dYg!o_(agi*+hV=%MwA1Na2rAR*^gh$+WxAa~tTWTucG|gL+MO;N zuknJ)1ICD$K;%VH5=k&Y2%NAwt3Tz{r5YE zzx!l{e)pSi_pF;9-Yx?%^5xH07(efTrYkRPm|6DOzF)`a2fD(_nIm_fAH8GYPsFa% ztCyS@GmoP$+;!o%5BA<8d~{;$nfKQn`uO11_Lj~go1NvrlFg4E-0gD$n?3gf_{J@Z z^CwS!x_*aqFDkzN(7`QN_VI7+){AeA5Wl#q7Oyy_ZhLUF`2FaC(J|!9^ScMTR-ZU< z>z$W|E+2K?(C}3y;Q0&A>*(PE2o^PiIrUTR8{%v#pQ3lGj&}zaU&VtCE0r(u}0&ak7j^zg=ja{XXUxq=67IY4*`PXtg8pbMk? zB#z^<#dZcnPnu})HXc=@wPvl3;jMNi4lrsgWtTYDFAY+WNgonzLCeHIx}7Krm3V_= zVYP}h6}W+CQHpG`J~PfTIlJrk%(&QdI};eHRUNPHr}-|cctDF}l zFyk`422E%ct!#NBpvM|oQNkc9l1%X&KMui31QNVjM(1tA(^3t7P|SkSL5bxQyMVfx zI)yN4RjE07KU(5-4CCNtUh|Ez3FkUg(d#&>7zf*iM8z-@EcjBMKva%``KVRH#)}x- z(z9*Arww$n0pM7!6rNu5m(GxX?~JTVwr{%s&>WRfqecnG2d&A9%huaMtpign#7L4# zrElP1&KHcV!fH)0lVTaHFQP6&fayL1xka_jwuC~RNvBCJTjVuGO!(uvnFG3AE#+3^ zB+&x8Nu!q#TXE6E2121(1KqmR<4W}|-Kj+zJ%MHm!xn0*wDRtE@_ z)(yH$SQV%mhx3r4W4fuEv0SMyAsQ_;d$nSQj@05M+v0q!+$^RGeYQU7XhNMb@`X5t zSQHq8>s3<03XU1cr3TixQ=}PIM=Y%?kO{42S-RNhgCs8k!3QOravNaRr7Mw&^M zEEn@gt<7a>zUY9KOG3ShuSeTR*R0f_DuyyLk_N#v3$et;eh5zsU}AoUQcE8Y*nYJx|30XNT4-GmONVq#30v|ysoLuEj3G-vh662 z`)B>0#_EdY$=O+2$=#19D^em2@^;h30_~=b7UK63a${u QME>Iuo}Tli{V%7#0~De6@&Et; literal 0 HcmV?d00001 From aae97c1e0a1eb3b47490e0c54b3a2b3ce81b17af Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 13 Nov 2025 10:28:44 +0100 Subject: [PATCH 09/51] Add test for user data in HCL report --- Cargo.lock | 59 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/attestation/azure.rs | 48 +++++++++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f846d84..26f3030 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -152,6 +158,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "josekit", "pem-rfc7468", "rand_core 0.6.4", "rcgen", @@ -561,6 +568,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -919,6 +935,16 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1450,6 +1476,23 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "josekit" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c" +dependencies = [ + "anyhow", + "base64 0.22.1", + "flate2", + "openssl", + "regex", + "serde", + "serde_json", + "thiserror 2.0.17", + "time", +] + [[package]] name = "js-sys" version = "0.3.82" @@ -1590,6 +1633,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.0" @@ -2631,6 +2684,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.11" diff --git a/Cargo.toml b/Cargo.toml index e37bf56..e057a01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ az-tdx-vtpm = "0.7.4" serde = "1.0.228" base64 = "0.22.1" reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls-webpki-roots-no-provider"] } +josekit = "0.10.3" +# jwt-simple = "0.12.13" [dev-dependencies] rcgen = "0.14.5" diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 9ec06f3..d4a280a 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -1,7 +1,7 @@ use az_tdx_vtpm::{hcl, imds, report, vtpm}; use tokio_rustls::rustls::pki_types::CertificateDer; // use openssl::pkey::{PKey, Public}; -use base64::prelude::*; +use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; use reqwest::Client; use serde::Serialize; @@ -58,8 +58,8 @@ impl QuoteGenerator for MaaGenerator { // let pub_key = PKey::public_key_from_der(&der).unwrap(); // tpm_quote.verify(&pub_key, nonce).unwrap(); - let quote_b64 = BASE64_STANDARD.encode(&td_quote_bytes); - let runtime_b64 = BASE64_STANDARD.encode(hcl_var_data); + let quote_b64 = BASE64_URL_SAFE.encode(&td_quote_bytes); + let runtime_b64 = BASE64_URL_SAFE.encode(hcl_var_data); let body = TdxVmRequest { quote: quote_b64, @@ -123,11 +123,28 @@ impl QuoteVerifier for MaaVerifier { cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], ) -> Result, AttestationError> { - let input_data = compute_report_input(cert_chain, exporter)?; + let _input_data = compute_report_input(cert_chain, exporter)?; + let token = String::from_utf8(input).unwrap(); + + self.decode_jwt(&token).await.unwrap(); + todo!() } } +impl MaaVerifier { + async fn decode_jwt(&self, token: &str) -> Result<(), AttestationError> { + // Parse payload (claims) without verification (TODO this will be swapped out once we have the + // key-getting logic) + let parts: Vec<&str> = token.split('.').collect(); + let claims_json = BASE64_URL_SAFE.decode(parts[1]).unwrap(); + + let claims: serde_json::Value = serde_json::from_slice(&claims_json).unwrap(); + println!("{claims}"); + Ok(()) + } +} + #[derive(Serialize)] struct RuntimeData<'a> { data: String, // base64url of VarData bytes @@ -143,3 +160,26 @@ struct TdxVmRequest<'a> { #[serde(skip_serializing_if = "Option::is_none")] nonce: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_decode_hcl() { + // from cvm-reverse-proxy/internal/attestation/azure/tdx/testdata/hclreport.bin + let hcl_bytes: &'static [u8] = include_bytes!("../../test-assets/hclreport.bin"); + + let hcl_report = hcl::HclReport::new(hcl_bytes.to_vec()).unwrap(); + let hcl_var_data = hcl_report.var_data(); + let var_data_values: serde_json::Value = serde_json::from_slice(&hcl_var_data).unwrap(); + + // Check that it contains 64 byte user data + assert_eq!( + hex::decode(var_data_values["user-data"].as_str().unwrap()) + .unwrap() + .len(), + 64 + ); + } +} From 654f23d605757d58c9978ff6b4624e90803b802d Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 24 Nov 2025 10:11:10 +0100 Subject: [PATCH 10/51] Refactor DCAP code into a module --- src/attestation/azure.rs | 1 + src/attestation/dcap.rs | 185 +++++++++++++++++++-------------------- src/attestation/mod.rs | 134 ++++------------------------ 3 files changed, 108 insertions(+), 212 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 162e13f..bd3dd31 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -1,3 +1,4 @@ +//! Microsoft Azure Attestation (MAA) evidence generation and verification use az_tdx_vtpm::{hcl, imds, report, vtpm}; use tokio_rustls::rustls::pki_types::CertificateDer; // use openssl::pkey::{PKey, Public}; diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index 982586d..8896698 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -1,117 +1,108 @@ +//! Data Center Attestation Primitives (DCAP) evidence generation and verification use crate::attestation::{ - compute_report_input, generate_quote, get_quote_input_data, + compute_report_input, measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, - AttestationError, AttestationType, QuoteGenerator, QuoteVerifier, PCS_URL, + AttestationError, }; -use dcap_qvl::{collateral::get_collateral_for_fmspc, quote::Quote}; +use configfs_tsm::QuoteGenerationError; +use dcap_qvl::{ + collateral::get_collateral_for_fmspc, + quote::{Quote, Report}, +}; use tokio_rustls::rustls::pki_types::CertificateDer; -/// Quote generation using configfs_tsm -#[derive(Clone)] -pub struct DcapTdxQuoteGenerator { - pub attestation_type: AttestationType, -} - -impl QuoteGenerator for DcapTdxQuoteGenerator { - /// Type of attestation used - fn attestation_type(&self) -> AttestationType { - self.attestation_type - } +/// For fetching collateral directly from Intel, if no PCCS is specified +const PCS_URL: &str = "https://api.trustedservices.intel.com"; - async fn create_attestation( - &self, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], - ) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; +/// Quote generation using configfs_tsm +pub async fn create_dcap_attestation( + cert_chain: &[CertificateDer<'_>], + exporter: [u8; 32], +) -> Result, AttestationError> { + let quote_input = compute_report_input(cert_chain, exporter)?; - Ok(generate_quote(quote_input)?) - } + Ok(generate_quote(quote_input)?) } -/// Verify DCAP TDX quotes, allowing them if they have one of a given set of platform-specific and -/// OS image specific measurements -#[derive(Clone)] -pub struct DcapTdxQuoteVerifier { - pub attestation_type: AttestationType, - /// Platform specific allowed Measurements - /// Currently an option as this may be determined internally on a per-platform basis (Eg: GCP) - pub accepted_platform_measurements: Option>, - /// OS-image specific allows measurement - this is effectively a list of allowed OS images - pub accepted_cvm_image_measurements: Vec, - /// URL of a PCCS (defaults to Intel PCS) - pub pccs_url: Option, -} +/// Verify a DCAP TDX quote, and return the measurement values +pub async fn verify_dcap_attestation( + input: Vec, + cert_chain: &[CertificateDer<'_>], + exporter: [u8; 32], + pccs_url: Option, +) -> Result { + let quote_input = compute_report_input(cert_chain, exporter)?; + let (platform_measurements, image_measurements) = if cfg!(not(test)) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let quote = Quote::parse(&input)?; -impl QuoteVerifier for DcapTdxQuoteVerifier { - /// Type of attestation used - fn attestation_type(&self) -> AttestationType { - self.attestation_type - } + let ca = quote.ca()?; + let fmspc = hex::encode_upper(quote.fmspc()?); + let collateral = get_collateral_for_fmspc( + &pccs_url.clone().unwrap_or(PCS_URL.to_string()), + fmspc, + ca, + false, // Indicates not SGX + ) + .await?; - async fn verify_attestation( - &self, - input: Vec, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], - ) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; - let (platform_measurements, image_measurements) = if cfg!(not(test)) { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_secs(); - let quote = Quote::parse(&input)?; + let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?; - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let collateral = get_collateral_for_fmspc( - &self.pccs_url.clone().unwrap_or(PCS_URL.to_string()), - fmspc, - ca, - false, - ) - .await?; - - let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?; + let measurements = ( + PlatformMeasurements::from_dcap_qvl_quote("e)?, + CvmImageMeasurements::from_dcap_qvl_quote("e)?, + ); + if get_quote_input_data(quote.report) != quote_input { + return Err(AttestationError::InputMismatch); + } + measurements + } else { + // In tests we use mock quotes which will fail to verify + let quote = tdx_quote::Quote::from_bytes(&input)?; + if quote.report_input_data() != quote_input { + return Err(AttestationError::InputMismatch); + } - let measurements = ( - PlatformMeasurements::from_dcap_qvl_quote("e)?, - CvmImageMeasurements::from_dcap_qvl_quote("e)?, - ); - if get_quote_input_data(quote.report) != quote_input { - return Err(AttestationError::InputMismatch); - } - measurements - } else { - // In tests we use mock quotes which will fail to verify - let quote = tdx_quote::Quote::from_bytes(&input)?; - if quote.report_input_data() != quote_input { - return Err(AttestationError::InputMismatch); - } + ( + PlatformMeasurements::from_tdx_quote("e), + CvmImageMeasurements::from_tdx_quote("e), + ) + }; - ( - PlatformMeasurements::from_tdx_quote("e), - CvmImageMeasurements::from_tdx_quote("e), - ) - }; + Ok(Measurements { + platform: platform_measurements, + cvm_image: image_measurements, + }) +} - if let Some(accepted_platform_measurements) = &self.accepted_platform_measurements - && !accepted_platform_measurements.contains(&platform_measurements) - { - return Err(AttestationError::UnacceptablePlatformMeasurements); - } +/// Create a mock quote for testing on non-confidential hardware +#[cfg(test)] +fn generate_quote(input: [u8; 64]) -> Result, QuoteGenerationError> { + let attestation_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng); + let provisioning_certification_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng); + Ok(tdx_quote::Quote::mock( + attestation_key.clone(), + provisioning_certification_key.clone(), + input, + b"Mock cert chain".to_vec(), + ) + .as_bytes()) +} - if !self - .accepted_cvm_image_measurements - .contains(&image_measurements) - { - return Err(AttestationError::UnacceptableOsImageMeasurements); - } +/// Create a quote +#[cfg(not(test))] +fn generate_quote(input: [u8; 64]) -> Result, QuoteGenerationError> { + configfs_tsm::create_quote(input) +} - Ok(Some(Measurements { - platform: platform_measurements, - cvm_image: image_measurements, - })) +/// Given a [Report] get the input data regardless of report type +fn get_quote_input_data(report: Report) -> [u8; 64] { + match report { + Report::TD10(r) => r.report_data, + Report::TD15(r) => r.base.report_data, + Report::SgxEnclave(r) => r.report_data, } } diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 617a6a1..70f896a 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -1,7 +1,8 @@ pub mod azure; +pub mod dcap; pub mod measurements; -use measurements::{CvmImageMeasurements, MeasurementRecord, Measurements, PlatformMeasurements}; +use measurements::{MeasurementRecord, Measurements}; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::{ @@ -9,20 +10,12 @@ use std::{ time::SystemTimeError, }; -use configfs_tsm::QuoteGenerationError; -use dcap_qvl::{ - collateral::get_collateral_for_fmspc, - quote::{Quote, Report}, -}; use sha2::{Digest, Sha256}; use tdx_quote::QuoteParseError; use thiserror::Error; use tokio_rustls::rustls::pki_types::CertificateDer; use x509_parser::prelude::*; -/// For fetching collateral directly from intel, if no PCCS is specified -const PCS_URL: &str = "https://api.trustedservices.intel.com"; - /// This is the type sent over the channel to provide an attestation #[derive(Debug, Serialize, Deserialize, Encode, Decode)] pub struct AttesationPayload { @@ -99,12 +92,14 @@ impl Display for AttestationType { } } +/// Can generate a local attestation based on attestation type #[derive(Clone)] pub struct AttestationGenerator { pub attestation_type: AttestationType, } impl AttestationGenerator { + /// Generate an attestation exchange message pub async fn generate_attestation( &self, cert_chain: &[CertificateDer<'_>], @@ -118,6 +113,7 @@ impl AttestationGenerator { }) } + /// Generate attestation evidence bytes based on attestation type async fn generate_attestation_bytes( &self, cert_chain: &[CertificateDer<'_>], @@ -128,7 +124,8 @@ impl AttestationGenerator { AttestationType::AzureTdx => { azure::create_azure_attestation(cert_chain, exporter).await } - _ => create_dcap_attestation(cert_chain, exporter).await, + AttestationType::Dummy => Err(AttestationError::AttestationTypeNotSupported), + _ => dcap::create_dcap_attestation(cert_chain, exporter).await, } } } @@ -162,11 +159,11 @@ impl AttestationVerifier { attestation_type: AttestationType::DcapTdx, measurement_id: "test".to_string(), measurements: Measurements { - platform: PlatformMeasurements { + platform: measurements::PlatformMeasurements { mrtd: [0; 48], rtmr0: [0; 48], }, - cvm_image: CvmImageMeasurements { + cvm_image: measurements::CvmImageMeasurements { rtmr1: [0; 48], rtmr2: [0; 48], rtmr3: [0; 48], @@ -187,15 +184,6 @@ impl AttestationVerifier { let attestation_type = attestation_payload.attestation_type; let measurements = match attestation_type { - AttestationType::DcapTdx => { - verify_dcap_attestation( - attestation_payload.attestation, - cert_chain, - exporter, - self.pccs_url.clone(), - ) - .await? - } AttestationType::None => { if self.has_remote_attestion() { return Err(AttestationError::AttestationTypeNotAccepted); @@ -214,9 +202,18 @@ impl AttestationVerifier { ) .await? } - _ => { + AttestationType::Dummy => { return Err(AttestationError::AttestationTypeNotSupported); } + _ => { + dcap::verify_dcap_attestation( + attestation_payload.attestation, + cert_chain, + exporter, + self.pccs_url.clone(), + ) + .await? + } }; // look through all our accepted measurements @@ -234,79 +231,6 @@ impl AttestationVerifier { } } -/// Quote generation using configfs_tsm -async fn create_dcap_attestation( - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], -) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; - - Ok(generate_quote(quote_input)?) -} - -/// Verify DCAP TDX quotes, allowing them if they have one of a given set of platform-specific and -/// OS image specific measurements -async fn verify_dcap_attestation( - input: Vec, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], - pccs_url: Option, -) -> Result { - let quote_input = compute_report_input(cert_chain, exporter)?; - let (platform_measurements, image_measurements) = if cfg!(not(test)) { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_secs(); - let quote = Quote::parse(&input)?; - - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let collateral = get_collateral_for_fmspc( - &pccs_url.clone().unwrap_or(PCS_URL.to_string()), - fmspc, - ca, - false, // Indicates not SGX - ) - .await?; - - let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?; - - let measurements = ( - PlatformMeasurements::from_dcap_qvl_quote("e)?, - CvmImageMeasurements::from_dcap_qvl_quote("e)?, - ); - if get_quote_input_data(quote.report) != quote_input { - return Err(AttestationError::InputMismatch); - } - measurements - } else { - // In tests we use mock quotes which will fail to verify - let quote = tdx_quote::Quote::from_bytes(&input)?; - if quote.report_input_data() != quote_input { - return Err(AttestationError::InputMismatch); - } - - ( - PlatformMeasurements::from_tdx_quote("e), - CvmImageMeasurements::from_tdx_quote("e), - ) - }; - - Ok(Measurements { - platform: platform_measurements, - cvm_image: image_measurements, - }) -} - -/// Given a [Report] get the input data regardless of report type -fn get_quote_input_data(report: Report) -> [u8; 64] { - match report { - Report::TD10(r) => r.report_data, - Report::TD15(r) => r.base.report_data, - Report::SgxEnclave(r) => r.report_data, - } -} - /// Given a certificate chain and an exporter (session key material), build the quote input value /// SHA256(pki) || exporter pub fn compute_report_input( @@ -320,26 +244,6 @@ pub fn compute_report_input( Ok(quote_input) } -/// Create a mock quote for testing on non-confidential hardware -#[cfg(test)] -fn generate_quote(input: [u8; 64]) -> Result, QuoteGenerationError> { - let attestation_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng); - let provisioning_certification_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng); - Ok(tdx_quote::Quote::mock( - attestation_key.clone(), - provisioning_certification_key.clone(), - input, - b"Mock cert chain".to_vec(), - ) - .as_bytes()) -} - -/// Create a quote -#[cfg(not(test))] -fn generate_quote(input: [u8; 64]) -> Result, QuoteGenerationError> { - configfs_tsm::create_quote(input) -} - /// Given a certificate chain, get the [Sha256] hash of the public key of the leaf certificate fn get_pki_hash_from_certificate_chain( cert_chain: &[CertificateDer<'_>], From e34befc8fe98a58656fae6377a978c0e8186d987 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 24 Nov 2025 13:00:20 +0100 Subject: [PATCH 11/51] Custom error type for MAA --- src/attestation/azure.rs | 55 +++++++++++++++++++++++++++++----------- src/attestation/mod.rs | 4 ++- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index bd3dd31..ee5287f 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -1,10 +1,13 @@ //! Microsoft Azure Attestation (MAA) evidence generation and verification +use std::string::FromUtf8Error; + use az_tdx_vtpm::{hcl, imds, report, vtpm}; use tokio_rustls::rustls::pki_types::CertificateDer; // use openssl::pkey::{PKey, Public}; use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; use reqwest::Client; use serde::Serialize; +use thiserror::Error; use crate::attestation::{compute_report_input, AttestationError}; @@ -15,12 +18,13 @@ use crate::attestation::{compute_report_input, AttestationError}; pub async fn create_azure_attestation( cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], -) -> Result, AttestationError> { +) -> Result, MaaError> { let maa_endpoint = "todo".to_string(); let aad_access_token = "todo".to_string(); - let input_data = compute_report_input(cert_chain, exporter)?; + let input_data = compute_report_input(cert_chain, exporter) + .map_err(|e| MaaError::InputData(e.to_string()))?; - let td_report = report::get_report().unwrap(); + let td_report = report::get_report()?; // let mrtd = td_report.tdinfo.mrtd; // let rtmr0 = td_report.tdinfo.rtrm[0]; @@ -29,10 +33,10 @@ pub async fn create_azure_attestation( // let rtmr3 = td_report.tdinfo.rtrm[3]; // This makes a request to Azure Instance metadata service and gives us a binary response - let td_quote_bytes = imds::get_td_quote(&td_report).unwrap(); + let td_quote_bytes = imds::get_td_quote(&td_report)?; - let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data).unwrap(); - let hcl_report = hcl::HclReport::new(hcl_report_bytes).unwrap(); + let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?; + let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; let hcl_var_data = hcl_report.var_data(); // let bytes = vtpm::get_report().unwrap(); @@ -61,10 +65,8 @@ pub async fn create_azure_attestation( }), nonce: Some("my-app-nonce-or-session-id".to_string()), }; - let body_bytes = serde_json::to_vec(&body).unwrap(); - let jwt_token = call_tdxvm_attestation(maa_endpoint, aad_access_token, body_bytes) - .await - .unwrap(); + let body_bytes = serde_json::to_vec(&body)?; + let jwt_token = call_tdxvm_attestation(maa_endpoint, aad_access_token, body_bytes).await?; Ok(jwt_token.as_bytes().to_vec()) } @@ -73,7 +75,7 @@ async fn call_tdxvm_attestation( maa_endpoint: String, aad_access_token: String, body_bytes: Vec, -) -> Result> { +) -> Result { let url = format!("{}/attest/TdxVm?api-version=2025-06-01", maa_endpoint); let client = Client::new(); @@ -89,7 +91,7 @@ async fn call_tdxvm_attestation( let text = res.text().await?; if !status.is_success() { - return Err(format!("MAA attestation failed: {status} {text}").into()); + return Err(MaaError::MaaProvider(status, text)); } #[derive(serde::Deserialize)] @@ -105,9 +107,10 @@ pub async fn verify_azure_attestation( input: Vec, cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], -) -> Result { - let _input_data = compute_report_input(cert_chain, exporter)?; - let token = String::from_utf8(input).unwrap(); +) -> Result { + let _input_data = compute_report_input(cert_chain, exporter) + .map_err(|e| MaaError::InputData(e.to_string()))?; + let token = String::from_utf8(input)?; decode_jwt(&token).await.unwrap(); @@ -141,6 +144,28 @@ struct TdxVmRequest<'a> { nonce: Option, } +#[derive(Error, Debug)] +pub enum MaaError { + #[error("Failed to build input data: {0}")] + InputData(String), + #[error("Report: {0}")] + Report(#[from] az_tdx_vtpm::report::ReportError), + #[error("IMDS: {0}")] + Imds(#[from] imds::ImdsError), + #[error("vTPM report: {0}")] + VtpmReport(#[from] az_tdx_vtpm::vtpm::ReportError), + #[error("HCL: {0}")] + Hcl(#[from] hcl::HclError), + #[error("JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("HTTP Client: {0}")] + HttpClient(#[from] reqwest::Error), + #[error("MAA provider response: {0} - {1}")] + MaaProvider(http::StatusCode, String), + #[error("Token is bad UTF8: {0}")] + BadUtf8(#[from] FromUtf8Error), +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 70f896a..7a64236 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -122,7 +122,7 @@ impl AttestationGenerator { match self.attestation_type { AttestationType::None => Ok(Vec::new()), AttestationType::AzureTdx => { - azure::create_azure_attestation(cert_chain, exporter).await + Ok(azure::create_azure_attestation(cert_chain, exporter).await?) } AttestationType::Dummy => Err(AttestationError::AttestationTypeNotSupported), _ => dcap::create_dcap_attestation(cert_chain, exporter).await, @@ -291,4 +291,6 @@ pub enum AttestationError { AttestationTypeNotAccepted, #[error("Measurements not accepted")] MeasurementsNotAccepted, + #[error("MAA: {0}")] + Maa(#[from] azure::MaaError), } From 3990753bcff8c5c985dd0d5c3640f17e804e11f0 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 24 Nov 2025 13:05:25 +0100 Subject: [PATCH 12/51] Tidy --- src/attestation/azure.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index ee5287f..f0241d9 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -57,16 +57,15 @@ pub async fn create_azure_attestation( let quote_b64 = BASE64_URL_SAFE.encode(&td_quote_bytes); let runtime_b64 = BASE64_URL_SAFE.encode(hcl_var_data); - let body = TdxVmRequest { + let tdx_vm_request = TdxVmRequest { quote: quote_b64, runtime_data: Some(RuntimeData { data: runtime_b64, data_type: "Binary", }), - nonce: Some("my-app-nonce-or-session-id".to_string()), + nonce: Some("my-app-nonce-or-session-id".to_string()), // TODO }; - let body_bytes = serde_json::to_vec(&body)?; - let jwt_token = call_tdxvm_attestation(maa_endpoint, aad_access_token, body_bytes).await?; + let jwt_token = call_tdxvm_attestation(maa_endpoint, aad_access_token, &tdx_vm_request).await?; Ok(jwt_token.as_bytes().to_vec()) } @@ -74,7 +73,7 @@ pub async fn create_azure_attestation( async fn call_tdxvm_attestation( maa_endpoint: String, aad_access_token: String, - body_bytes: Vec, + tdx_vm_request: &TdxVmRequest<'_>, ) -> Result { let url = format!("{}/attest/TdxVm?api-version=2025-06-01", maa_endpoint); @@ -83,7 +82,7 @@ async fn call_tdxvm_attestation( .post(&url) .bearer_auth(&aad_access_token) .header("Content-Type", "application/json") - .body(body_bytes) + .body(serde_json::to_vec(tdx_vm_request)?) .send() .await?; From cfbeab02cdd61c8b8c4e9d625865bc99a8635699 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 25 Nov 2025 10:21:20 +0100 Subject: [PATCH 13/51] Verify azure attestation locally - not using MAA API --- Cargo.lock | 1 + Cargo.toml | 1 + src/attestation/azure.rs | 204 ++++++++++++++++++++------------------- src/attestation/dcap.rs | 2 +- src/attestation/mod.rs | 1 + 5 files changed, 107 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35f2f9b..344170a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,7 @@ dependencies = [ "hyper", "hyper-util", "josekit", + "openssl", "parity-scale-codec", "pem-rfc7468", "rand_core 0.6.4", diff --git a/Cargo.toml b/Cargo.toml index d07d6f2..a8c7363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ josekit = "0.10.3" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } parity-scale-codec = "3.7.5" +openssl = "0.10.75" [dev-dependencies] rcgen = "0.14.5" diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index f0241d9..68ef73f 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -2,145 +2,141 @@ use std::string::FromUtf8Error; use az_tdx_vtpm::{hcl, imds, report, vtpm}; -use tokio_rustls::rustls::pki_types::CertificateDer; -// use openssl::pkey::{PKey, Public}; use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; -use reqwest::Client; -use serde::Serialize; +use openssl::pkey::PKey; +use serde::{Deserialize, Serialize}; use thiserror::Error; +use tokio_rustls::rustls::pki_types::CertificateDer; -use crate::attestation::{compute_report_input, AttestationError}; - -// #[derive(Clone)] -// pub struct MaaGenerator { -// } +use crate::attestation::{ + self, compute_report_input, + measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, +}; pub async fn create_azure_attestation( cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], ) -> Result, MaaError> { - let maa_endpoint = "todo".to_string(); - let aad_access_token = "todo".to_string(); let input_data = compute_report_input(cert_chain, exporter) .map_err(|e| MaaError::InputData(e.to_string()))?; let td_report = report::get_report()?; - // let mrtd = td_report.tdinfo.mrtd; - // let rtmr0 = td_report.tdinfo.rtrm[0]; - // let rtmr1 = td_report.tdinfo.rtrm[1]; - // let rtmr2 = td_report.tdinfo.rtrm[2]; - // let rtmr3 = td_report.tdinfo.rtrm[3]; - // This makes a request to Azure Instance metadata service and gives us a binary response let td_quote_bytes = imds::get_td_quote(&td_report)?; let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?; - let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; - let hcl_var_data = hcl_report.var_data(); - // let bytes = vtpm::get_report().unwrap(); - // let hcl_report = hcl::HclReport::new(bytes).unwrap(); - // let var_data_hash = hcl_report.var_data_sha256(); - // let _ak_pub = hcl_report.ak_pub().unwrap(); - // - // let td_report: tdx::TdReport = hcl_report.try_into().unwrap(); - // assert!(var_data_hash == td_report.report_mac.reportdata[..32]); + // let quote_b64 = ; + // let runtime_b64 = BASE64_URL_SAFE.encode(hcl_var_data); - // let nonce = "a nonce".as_bytes(); - // - // let tpm_quote = vtpm::get_quote(nonce).unwrap(); - // let der = ak_pub.key.try_to_der().unwrap(); - // let pub_key = PKey::public_key_from_der(&der).unwrap(); - // tpm_quote.verify(&pub_key, nonce).unwrap(); - - let quote_b64 = BASE64_URL_SAFE.encode(&td_quote_bytes); - let runtime_b64 = BASE64_URL_SAFE.encode(hcl_var_data); - - let tdx_vm_request = TdxVmRequest { - quote: quote_b64, - runtime_data: Some(RuntimeData { - data: runtime_b64, - data_type: "Binary", - }), - nonce: Some("my-app-nonce-or-session-id".to_string()), // TODO + let tpm_attestation = TpmAttest { + ak_pub: vtpm::get_ak_pub()?, + quote: vtpm::get_quote(&input_data)?, + event_log: Vec::new(), + instance_info: None, }; - let jwt_token = call_tdxvm_attestation(maa_endpoint, aad_access_token, &tdx_vm_request).await?; - Ok(jwt_token.as_bytes().to_vec()) -} -/// Get a signed JWT from the azure API -async fn call_tdxvm_attestation( - maa_endpoint: String, - aad_access_token: String, - tdx_vm_request: &TdxVmRequest<'_>, -) -> Result { - let url = format!("{}/attest/TdxVm?api-version=2025-06-01", maa_endpoint); - - let client = Client::new(); - let res = client - .post(&url) - .bearer_auth(&aad_access_token) - .header("Content-Type", "application/json") - .body(serde_json::to_vec(tdx_vm_request)?) - .send() - .await?; - - let status = res.status(); - let text = res.text().await?; - - if !status.is_success() { - return Err(MaaError::MaaProvider(status, text)); - } - - #[derive(serde::Deserialize)] - struct AttestationResponse { - token: String, - } + let attestation_document = AttestationDocument { + tdx_quote_base64: BASE64_URL_SAFE.encode(&td_quote_bytes), + hcl_report_base64: BASE64_URL_SAFE.encode(&hcl_report_bytes), + tpm_attestation, + }; - let parsed: AttestationResponse = serde_json::from_str(&text)?; - Ok(parsed.token) // Microsoft-signed JWT + Ok(serde_json::to_vec(&attestation_document)?) } pub async fn verify_azure_attestation( input: Vec, cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], + pccs_url: Option, ) -> Result { - let _input_data = compute_report_input(cert_chain, exporter) + let input_data = compute_report_input(cert_chain, exporter) .map_err(|e| MaaError::InputData(e.to_string()))?; - let token = String::from_utf8(input)?; - decode_jwt(&token).await.unwrap(); + let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; - todo!() -} + // Verify TDX quote (same as with DCAP) - TODO deduplicate this code + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let tdx_quote_bytes = BASE64_URL_SAFE + .decode(attestation_document.tdx_quote_base64) + .unwrap(); + + let quote = dcap_qvl::quote::Quote::parse(&tdx_quote_bytes).unwrap(); -async fn decode_jwt(token: &str) -> Result<(), AttestationError> { - // Parse payload (claims) without verification (TODO this will be swapped out once we have the - // key-getting logic) - let parts: Vec<&str> = token.split('.').collect(); - let claims_json = BASE64_URL_SAFE.decode(parts[1]).unwrap(); + let ca = quote.ca().unwrap(); + let fmspc = hex::encode_upper(quote.fmspc().unwrap()); + let collateral = dcap_qvl::collateral::get_collateral_for_fmspc( + &pccs_url + .clone() + .unwrap_or(attestation::dcap::PCS_URL.to_string()), + fmspc, + ca, + false, // Indicates not SGX + ) + .await + .unwrap(); - let claims: serde_json::Value = serde_json::from_slice(&claims_json).unwrap(); - println!("{claims}"); - Ok(()) + let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now).unwrap(); + + // Check that hcl_report_bytes (hashed?) matches TDX quote report data + // if get_quote_input_data(quote.report) != quote_input { + // return Err(AttestationError::InputMismatch); + // } + + let hcl_report_bytes = BASE64_URL_SAFE + .decode(attestation_document.hcl_report_base64) + .unwrap(); + + let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; + // + let var_data_hash = hcl_report.var_data_sha256(); + let hcl_ak_pub = hcl_report.ak_pub()?; + let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?; + assert!(var_data_hash == td_report.report_mac.reportdata[..32]); + + let vtpm_quote = attestation_document.tpm_attestation.quote; + let hcl_ak_pub_der = hcl_ak_pub.key.try_to_der().unwrap(); + let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der).unwrap(); + vtpm_quote.verify(&pub_key, &input_data)?; + let _pcrs = vtpm_quote.pcrs_sha256(); + + Ok(Measurements { + platform: PlatformMeasurements::from_dcap_qvl_quote("e).unwrap(), + cvm_image: CvmImageMeasurements::from_dcap_qvl_quote("e).unwrap(), + }) } -#[derive(Serialize)] -struct RuntimeData<'a> { - data: String, // base64url of VarData bytes - #[serde(rename = "dataType")] - data_type: &'a str, // "Binary" in our case +/// The attestation evidence payload that gets sent over the channel +#[derive(Debug, Serialize, Deserialize)] +struct AttestationDocument { + /// TDX quote from the IMDS + tdx_quote_base64: String, + /// Serialized HCL report + hcl_report_base64: String, + /// vTPM related evidence + tpm_attestation: TpmAttest, } -#[derive(Serialize)] -struct TdxVmRequest<'a> { - quote: String, // base64 (TDX quote) - #[serde(rename = "runtimeData", skip_serializing_if = "Option::is_none")] - runtime_data: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - nonce: Option, +#[derive(Debug, Serialize, Deserialize)] +pub struct TpmAttest { + /// vTPM Attestation Key (AK) public key + // TODO do we need this? it is already given in HCL report + pub ak_pub: vtpm::PublicKey, + /// vTPM quotes over the selected PCR bank(s). + pub quote: vtpm::Quote, + /// Raw TCG event log bytes (UEFI + IMA) + /// + /// `/sys/kernel/security/ima/ascii_runtime_measurements`, + /// `/sys/kernel/security/tpm0/binary_bios_measurements`, + pub event_log: Vec, + /// Optional platform / instance metadata used to bind or verify the AK + pub instance_info: Option>, } #[derive(Error, Debug)] @@ -163,6 +159,12 @@ pub enum MaaError { MaaProvider(http::StatusCode, String), #[error("Token is bad UTF8: {0}")] BadUtf8(#[from] FromUtf8Error), + #[error("vTPM quote: {0}")] + VtpmQuote(#[from] vtpm::QuoteError), + #[error("AK public key: {0}")] + AkPub(#[from] vtpm::AKPubError), + #[error("vTPM quote could not be verified: {0}")] + TpmQuoteVerify(#[from] vtpm::VerifyError), } #[cfg(test)] diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index 8896698..23692b8 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -13,7 +13,7 @@ use dcap_qvl::{ use tokio_rustls::rustls::pki_types::CertificateDer; /// For fetching collateral directly from Intel, if no PCCS is specified -const PCS_URL: &str = "https://api.trustedservices.intel.com"; +pub const PCS_URL: &str = "https://api.trustedservices.intel.com"; /// Quote generation using configfs_tsm pub async fn create_dcap_attestation( diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 7a64236..67c996d 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -199,6 +199,7 @@ impl AttestationVerifier { attestation_payload.attestation, cert_chain, exporter, + self.pccs_url.clone(), ) .await? } From ebb69b86aa22280c502b0ad9a24849d47cdc1b29 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 25 Nov 2025 12:47:33 +0100 Subject: [PATCH 14/51] Add NV index reader (for reading AK certificate) --- Cargo.lock | 1 + Cargo.toml | 1 + src/attestation/mod.rs | 1 + src/attestation/nv_index.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 31 insertions(+) create mode 100644 src/attestation/nv_index.rs diff --git a/Cargo.lock b/Cargo.lock index 344170a..54e534e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,7 @@ dependencies = [ "tokio-rustls", "tracing", "tracing-subscriber", + "tss-esapi", "webpki-roots", "x509-parser", ] diff --git a/Cargo.toml b/Cargo.toml index a8c7363..1f42aa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } parity-scale-codec = "3.7.5" openssl = "0.10.75" +tss-esapi = "7.6.0" [dev-dependencies] rcgen = "0.14.5" diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 67c996d..c57a027 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -1,6 +1,7 @@ pub mod azure; pub mod dcap; pub mod measurements; +pub mod nv_index; use measurements::{MeasurementRecord, Measurements}; use parity_scale_codec::{Decode, Encode}; diff --git a/src/attestation/nv_index.rs b/src/attestation/nv_index.rs new file mode 100644 index 0000000..e30a95f --- /dev/null +++ b/src/attestation/nv_index.rs @@ -0,0 +1,28 @@ +use tss_esapi::{ + handles::NvIndexHandle, + interface_types::{resource_handles::NvAuth, session_handles::AuthSession}, + structures::MaxNvBuffer, + tcti_ldr::{DeviceConfig, TctiNameConf}, + Context, +}; + +pub fn get_session_context() -> Result { + let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default()); + let mut context = Context::new(conf)?; + let auth_session = AuthSession::Password; + context.set_sessions((Some(auth_session), None, None)); + Ok(context) +} + +pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, anyhow::Error> { + let handle = NvIndexHandle::from(index); + let size = ctx + .nv_read_public(handle.into())? + .0 + .data_size() + .try_into() + .unwrap_or(0u16); + + let data: MaxNvBuffer = ctx.nv_read(NvAuth::Owner, handle, size, 0)?; + Ok(data.to_vec()) +} From 6306be39b9a86e4900f148b582210a0bc062c99e Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 25 Nov 2025 17:57:41 +0100 Subject: [PATCH 15/51] Get AK certificate from vTPM --- src/attestation/azure.rs | 39 ++++++++++++++++++++++++++----------- src/attestation/nv_index.rs | 2 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 68ef73f..a191694 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -11,8 +11,11 @@ use tokio_rustls::rustls::pki_types::CertificateDer; use crate::attestation::{ self, compute_report_input, measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, + nv_index, }; +const TPM_AK_CERT_IDX: u32 = 0x1C101D0; + pub async fn create_azure_attestation( cert_chain: &[CertificateDer<'_>], exporter: [u8; 32], @@ -27,11 +30,14 @@ pub async fn create_azure_attestation( let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?; - // let quote_b64 = ; - // let runtime_b64 = BASE64_URL_SAFE.encode(hcl_var_data); + let ak_certificate_der = read_ak_certificate_from_tpm()?; let tpm_attestation = TpmAttest { - ak_pub: vtpm::get_ak_pub()?, + ak_certificate_pem: pem_rfc7468::encode_string( + "CERTIFICATE", + pem_rfc7468::LineEnding::default(), + &ak_certificate_der, + )?, quote: vtpm::get_quote(&input_data)?, event_log: Vec::new(), instance_info: None, @@ -94,7 +100,6 @@ pub async fn verify_azure_attestation( .unwrap(); let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; - // let var_data_hash = hcl_report.var_data_sha256(); let hcl_ak_pub = hcl_report.ak_pub()?; let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?; @@ -106,6 +111,10 @@ pub async fn verify_azure_attestation( vtpm_quote.verify(&pub_key, &input_data)?; let _pcrs = vtpm_quote.pcrs_sha256(); + // TODO parse AK certificate + // Check that AK public key matches that from TPM quote + // Verify AK certificate against microsoft root cert + Ok(Measurements { platform: PlatformMeasurements::from_dcap_qvl_quote("e).unwrap(), cvm_image: CvmImageMeasurements::from_dcap_qvl_quote("e).unwrap(), @@ -124,19 +133,23 @@ struct AttestationDocument { } #[derive(Debug, Serialize, Deserialize)] -pub struct TpmAttest { - /// vTPM Attestation Key (AK) public key - // TODO do we need this? it is already given in HCL report - pub ak_pub: vtpm::PublicKey, +struct TpmAttest { + /// Attestation Key certificate from vTPM + ak_certificate_pem: String, /// vTPM quotes over the selected PCR bank(s). - pub quote: vtpm::Quote, + quote: vtpm::Quote, /// Raw TCG event log bytes (UEFI + IMA) /// /// `/sys/kernel/security/ima/ascii_runtime_measurements`, /// `/sys/kernel/security/tpm0/binary_bios_measurements`, - pub event_log: Vec, + event_log: Vec, /// Optional platform / instance metadata used to bind or verify the AK - pub instance_info: Option>, + instance_info: Option>, +} + +fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { + let mut context = nv_index::get_session_context()?; + Ok(nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX)?) } #[derive(Error, Debug)] @@ -165,6 +178,10 @@ pub enum MaaError { AkPub(#[from] vtpm::AKPubError), #[error("vTPM quote could not be verified: {0}")] TpmQuoteVerify(#[from] vtpm::VerifyError), + #[error("vTPM read: {0}")] + TssEsapi(#[from] tss_esapi::Error), + #[error("PEM encode: {0}")] + Pem(#[from] pem_rfc7468::Error), } #[cfg(test)] diff --git a/src/attestation/nv_index.rs b/src/attestation/nv_index.rs index e30a95f..da5748a 100644 --- a/src/attestation/nv_index.rs +++ b/src/attestation/nv_index.rs @@ -14,7 +14,7 @@ pub fn get_session_context() -> Result { Ok(context) } -pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, anyhow::Error> { +pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, tss_esapi::Error> { let handle = NvIndexHandle::from(index); let size = ctx .nv_read_public(handle.into())? From d3fb22e13a9b61f7eaf2a50ee9f4c24fb77663f5 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 26 Nov 2025 08:07:30 +0100 Subject: [PATCH 16/51] Clippy --- src/attestation/azure.rs | 2 +- src/attestation/nv_index.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index a191694..3af390f 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -149,7 +149,7 @@ struct TpmAttest { fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { let mut context = nv_index::get_session_context()?; - Ok(nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX)?) + nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) } #[derive(Error, Debug)] diff --git a/src/attestation/nv_index.rs b/src/attestation/nv_index.rs index da5748a..40b325a 100644 --- a/src/attestation/nv_index.rs +++ b/src/attestation/nv_index.rs @@ -17,7 +17,7 @@ pub fn get_session_context() -> Result { pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, tss_esapi::Error> { let handle = NvIndexHandle::from(index); let size = ctx - .nv_read_public(handle.into())? + .nv_read_public(handle)? .0 .data_size() .try_into() From 3392f22523fdcecad426ea052773d1330b970d1d Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 26 Nov 2025 10:15:44 +0100 Subject: [PATCH 17/51] Refactor input generation out of attestation code --- Cargo.lock | 2 +- src/attestation/azure.rs | 19 +++---------- src/attestation/dcap.rs | 19 ++++--------- src/attestation/mod.rs | 55 ++++++------------------------------- src/lib.rs | 59 ++++++++++++++++++++++++++++++++++------ 5 files changed, 68 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c134c56..d9f2237 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -836,7 +836,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "thiserror", + "thiserror 2.0.17", "tokio", "tokio-rustls", "tracing", diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 3af390f..a355cff 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -6,23 +6,16 @@ use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; use openssl::pkey::PKey; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tokio_rustls::rustls::pki_types::CertificateDer; use crate::attestation::{ - self, compute_report_input, + self, measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, nv_index, }; const TPM_AK_CERT_IDX: u32 = 0x1C101D0; -pub async fn create_azure_attestation( - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], -) -> Result, MaaError> { - let input_data = compute_report_input(cert_chain, exporter) - .map_err(|e| MaaError::InputData(e.to_string()))?; - +pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaError> { let td_report = report::get_report()?; // This makes a request to Azure Instance metadata service and gives us a binary response @@ -54,13 +47,9 @@ pub async fn create_azure_attestation( pub async fn verify_azure_attestation( input: Vec, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], + expected_input_data: [u8; 64], pccs_url: Option, ) -> Result { - let input_data = compute_report_input(cert_chain, exporter) - .map_err(|e| MaaError::InputData(e.to_string()))?; - let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; // Verify TDX quote (same as with DCAP) - TODO deduplicate this code @@ -108,7 +97,7 @@ pub async fn verify_azure_attestation( let vtpm_quote = attestation_document.tpm_attestation.quote; let hcl_ak_pub_der = hcl_ak_pub.key.try_to_der().unwrap(); let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der).unwrap(); - vtpm_quote.verify(&pub_key, &input_data)?; + vtpm_quote.verify(&pub_key, &expected_input_data)?; let _pcrs = vtpm_quote.pcrs_sha256(); // TODO parse AK certificate diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index 23692b8..6ed406e 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -1,6 +1,5 @@ //! Data Center Attestation Primitives (DCAP) evidence generation and verification use crate::attestation::{ - compute_report_input, measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, AttestationError, }; @@ -10,29 +9,21 @@ use dcap_qvl::{ collateral::get_collateral_for_fmspc, quote::{Quote, Report}, }; -use tokio_rustls::rustls::pki_types::CertificateDer; /// For fetching collateral directly from Intel, if no PCCS is specified pub const PCS_URL: &str = "https://api.trustedservices.intel.com"; /// Quote generation using configfs_tsm -pub async fn create_dcap_attestation( - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], -) -> Result, AttestationError> { - let quote_input = compute_report_input(cert_chain, exporter)?; - - Ok(generate_quote(quote_input)?) +pub async fn create_dcap_attestation(input_data: [u8; 64]) -> Result, AttestationError> { + Ok(generate_quote(input_data)?) } /// Verify a DCAP TDX quote, and return the measurement values pub async fn verify_dcap_attestation( input: Vec, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], + expected_input_data: [u8; 64], pccs_url: Option, ) -> Result { - let quote_input = compute_report_input(cert_chain, exporter)?; let (platform_measurements, image_measurements) = if cfg!(not(test)) { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? @@ -55,14 +46,14 @@ pub async fn verify_dcap_attestation( PlatformMeasurements::from_dcap_qvl_quote("e)?, CvmImageMeasurements::from_dcap_qvl_quote("e)?, ); - if get_quote_input_data(quote.report) != quote_input { + if get_quote_input_data(quote.report) != expected_input_data { return Err(AttestationError::InputMismatch); } measurements } else { // In tests we use mock quotes which will fail to verify let quote = tdx_quote::Quote::from_bytes(&input)?; - if quote.report_input_data() != quote_input { + if quote.report_input_data() != expected_input_data { return Err(AttestationError::InputMismatch); } diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 8010d21..727dead 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -11,11 +11,8 @@ use std::{ time::SystemTimeError, }; -use sha2::{Digest, Sha256}; use tdx_quote::QuoteParseError; use thiserror::Error; -use tokio_rustls::rustls::pki_types::CertificateDer; -use x509_parser::prelude::*; /// This is the type sent over the channel to provide an attestation #[derive(Debug, Serialize, Deserialize, Encode, Decode)] @@ -103,30 +100,24 @@ impl AttestationGenerator { /// Generate an attestation exchange message pub async fn generate_attestation( &self, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], + input_data: [u8; 64], ) -> Result { Ok(AttestationExchangeMessage { attestation_type: self.attestation_type, - attestation: self - .generate_attestation_bytes(cert_chain, exporter) - .await?, + attestation: self.generate_attestation_bytes(input_data).await?, }) } /// Generate attestation evidence bytes based on attestation type async fn generate_attestation_bytes( &self, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], + input_data: [u8; 64], ) -> Result, AttestationError> { match self.attestation_type { AttestationType::None => Ok(Vec::new()), - AttestationType::AzureTdx => { - Ok(azure::create_azure_attestation(cert_chain, exporter).await?) - } + AttestationType::AzureTdx => Ok(azure::create_azure_attestation(input_data).await?), AttestationType::Dummy => Err(AttestationError::AttestationTypeNotSupported), - _ => dcap::create_dcap_attestation(cert_chain, exporter).await, + _ => dcap::create_dcap_attestation(input_data).await, } } } @@ -179,8 +170,7 @@ impl AttestationVerifier { pub async fn verify_attestation( &self, attestation_exchange_message: AttestationExchangeMessage, - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], + expected_input_data: [u8; 64], ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type; @@ -198,8 +188,7 @@ impl AttestationVerifier { AttestationType::AzureTdx => { azure::verify_azure_attestation( attestation_exchange_message.attestation, - cert_chain, - exporter, + expected_input_data, self.pccs_url.clone(), ) .await? @@ -210,8 +199,7 @@ impl AttestationVerifier { _ => { dcap::verify_dcap_attestation( attestation_exchange_message.attestation, - cert_chain, - exporter, + expected_input_data, self.pccs_url.clone(), ) .await? @@ -233,33 +221,6 @@ impl AttestationVerifier { } } -/// Given a certificate chain and an exporter (session key material), build the quote input value -/// SHA256(pki) || exporter -pub fn compute_report_input( - cert_chain: &[CertificateDer<'_>], - exporter: [u8; 32], -) -> Result<[u8; 64], AttestationError> { - let mut quote_input = [0u8; 64]; - let pki_hash = get_pki_hash_from_certificate_chain(cert_chain)?; - quote_input[..32].copy_from_slice(&pki_hash); - quote_input[32..].copy_from_slice(&exporter); - Ok(quote_input) -} - -/// Given a certificate chain, get the [Sha256] hash of the public key of the leaf certificate -fn get_pki_hash_from_certificate_chain( - cert_chain: &[CertificateDer<'_>], -) -> Result<[u8; 32], AttestationError> { - let leaf_certificate = cert_chain.first().ok_or(AttestationError::NoCertificate)?; - let (_, cert) = parse_x509_certificate(leaf_certificate.as_ref())?; - let public_key = &cert.tbs_certificate.subject_pki; - let key_bytes = public_key.subject_public_key.as_ref(); - - let mut hasher = Sha256::new(); - hasher.update(key_bytes); - Ok(hasher.finalize().into()) -} - /// An error when generating or verifying an attestation #[derive(Error, Debug)] pub enum AttestationError { diff --git a/src/lib.rs b/src/lib.rs index f95e9a7..b748526 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,10 +8,12 @@ use http_body_util::{combinators::BoxBody, BodyExt}; use hyper::{service::service_fn, Response}; use hyper_util::rt::TokioIo; use parity_scale_codec::{Decode, Encode}; +use sha2::{Digest, Sha256}; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; use tokio_rustls::rustls::server::{VerifierBuilderError, WebPkiClientVerifier}; use tracing::{error, warn}; +use x509_parser::parse_x509_certificate; #[cfg(test)] mod test_helpers; @@ -86,9 +88,11 @@ impl ProxyServer { attestation_verifier: AttestationVerifier, client_auth: bool, ) -> Result { + println!("here"); if attestation_verifier.has_remote_attestion() && !client_auth { return Err(ProxyError::NoClientAuth); } + println!("here2"); let mut server_config = if client_auth { let root_store = @@ -200,12 +204,14 @@ impl ProxyServer { None, // context )?; + let input_data = compute_report_input(&cert_chain, exporter)?; + // Get the TLS certficate chain of the client, if there is one let remote_cert_chain = connection.peer_certificates().map(|c| c.to_owned()); // If we are in a CVM, generate an attestation let attestation = attestation_generator - .generate_attestation(&cert_chain, exporter) + .generate_attestation(input_data) .await? .encode(); @@ -228,12 +234,13 @@ impl ProxyServer { // If we expect an attestaion from the client, verify it and get measurements let measurements = if attestation_verifier.has_remote_attestion() { + let remote_input_data = compute_report_input( + &remote_cert_chain.ok_or(ProxyError::NoClientAuth)?, + exporter, + )?; + attestation_verifier - .verify_attestation( - remote_attestation_message, - &remote_cert_chain.ok_or(ProxyError::NoClientAuth)?, - exporter, - ) + .verify_attestation(remote_attestation_message, remote_input_data) .await? } else { None @@ -613,6 +620,8 @@ impl ProxyClient { .ok_or(ProxyError::NoCertificate)? .to_owned(); + let remote_input_data = compute_report_input(&remote_cert_chain, exporter)?; + // Read a length prefixed attestation from the proxy-server let mut length_bytes = [0; 4]; tls_stream.read_exact(&mut length_bytes).await?; @@ -626,13 +635,16 @@ impl ProxyClient { // Verify the remote attestation against our accepted measurements let measurements = attestation_verifier - .verify_attestation(remote_attestation_message, &remote_cert_chain, exporter) + .verify_attestation(remote_attestation_message, remote_input_data) .await?; // If we are in a CVM, provide an attestation let attestation = if attestation_generator.attestation_type != AttestationType::None { + println!("fff"); + let local_input_data = + compute_report_input(&cert_chain.ok_or(ProxyError::NoClientAuth)?, exporter)?; attestation_generator - .generate_attestation(&cert_chain.ok_or(ProxyError::NoClientAuth)?, exporter) + .generate_attestation(local_input_data) .await? .encode() } else { @@ -720,13 +732,42 @@ async fn get_tls_cert_with_config( let remote_attestation_message = AttestationExchangeMessage::decode(&mut &buf[..])?; + let remote_input_data = compute_report_input(&remote_cert_chain, exporter)?; + let _measurements = attestation_verifier - .verify_attestation(remote_attestation_message, &remote_cert_chain, exporter) + .verify_attestation(remote_attestation_message, remote_input_data) .await?; Ok(remote_cert_chain) } +/// Given a certificate chain and an exporter (session key material), build the quote input value +/// SHA256(pki) || exporter +pub fn compute_report_input( + cert_chain: &[CertificateDer<'_>], + exporter: [u8; 32], +) -> Result<[u8; 64], AttestationError> { + let mut quote_input = [0u8; 64]; + let pki_hash = get_pki_hash_from_certificate_chain(cert_chain)?; + quote_input[..32].copy_from_slice(&pki_hash); + quote_input[32..].copy_from_slice(&exporter); + Ok(quote_input) +} + +/// Given a certificate chain, get the [Sha256] hash of the public key of the leaf certificate +fn get_pki_hash_from_certificate_chain( + cert_chain: &[CertificateDer<'_>], +) -> Result<[u8; 32], AttestationError> { + let leaf_certificate = cert_chain.first().ok_or(AttestationError::NoCertificate)?; + let (_, cert) = parse_x509_certificate(leaf_certificate.as_ref())?; + let public_key = &cert.tbs_certificate.subject_pki; + let key_bytes = public_key.subject_public_key.as_ref(); + + let mut hasher = Sha256::new(); + hasher.update(key_bytes); + Ok(hasher.finalize().into()) +} + /// An error when running a proxy client or server #[derive(Error, Debug)] pub enum ProxyError { From c4b846c9f36cc2a4e434ab26641229d226a9139d Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 26 Nov 2025 10:37:40 +0100 Subject: [PATCH 18/51] Fix dummy server --- .github/workflows/test.yml | 4 +- Cargo.lock | 4 -- dummy-attestation-server/Cargo.toml | 4 -- dummy-attestation-server/src/lib.rs | 67 ++++++++++++---------------- dummy-attestation-server/src/main.rs | 32 +++++++++++-- src/attestation/mod.rs | 2 +- 6 files changed, 59 insertions(+), 54 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82d0005..4052992 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: ${{ runner.os }}-cargo- - name: Run cargo clippy - run: cargo clippy -- -D warnings + run: cargo clippy --workspace -- -D warnings - name: Run cargo test - run: cargo test + run: cargo test --workspace --all-targets diff --git a/Cargo.lock b/Cargo.lock index d9f2237..b27b741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,17 +831,13 @@ dependencies = [ "configfs-tsm", "hex", "parity-scale-codec", - "rcgen", "reqwest", - "rustls-pemfile", "serde", "serde_json", "thiserror 2.0.17", "tokio", - "tokio-rustls", "tracing", "tracing-subscriber", - "webpki-roots", ] [[package]] diff --git a/dummy-attestation-server/Cargo.toml b/dummy-attestation-server/Cargo.toml index 10c5379..26b91cc 100644 --- a/dummy-attestation-server/Cargo.toml +++ b/dummy-attestation-server/Cargo.toml @@ -9,11 +9,8 @@ publish = false attested-tls-proxy = { path = ".." } tokio = { version = "1.48.0", features = ["full"] } axum = "0.8.6" -tokio-rustls = { version = "0.26.4", default-features = false, features = ["ring"] } thiserror = "2.0.17" clap = { version = "4.5.51", features = ["derive", "env"] } -webpki-roots = "1.0.4" -rustls-pemfile = "2.2.0" anyhow = "1.0.100" configfs-tsm = "0.0.2" hex = "0.4.3" @@ -21,7 +18,6 @@ serde_json = "1.0.145" serde = "1.0.228" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } -rcgen = "0.14.5" parity-scale-codec = "3.7.5" reqwest = { version = "0.12.23", default-features = false } diff --git a/dummy-attestation-server/src/lib.rs b/dummy-attestation-server/src/lib.rs index 9181b44..769abdf 100644 --- a/dummy-attestation-server/src/lib.rs +++ b/dummy-attestation-server/src/lib.rs @@ -1,9 +1,8 @@ -use std::{ - net::{IpAddr, SocketAddr}, - sync::Arc, -}; +use std::net::SocketAddr; -use attested_tls_proxy::{attestation::AttestationExchangeMessage, QuoteGenerator}; +use attested_tls_proxy::attestation::{ + AttestationExchangeMessage, AttestationGenerator, AttestationVerifier, +}; use axum::{ extract::{Path, State}, http::StatusCode, @@ -11,16 +10,15 @@ use axum::{ }; use parity_scale_codec::{Decode, Encode}; use tokio::net::TcpListener; -use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; #[derive(Clone)] struct SharedState { - attestation_generator: Arc, + attestation_generator: AttestationGenerator, } pub async fn dummy_attestation_server( listener: TcpListener, - attestation_generator: Arc, + attestation_generator: AttestationGenerator, ) -> anyhow::Result { let addr = listener.local_addr()?; @@ -41,20 +39,21 @@ async fn get_attest( State(shared_state): State, Path(input_data): Path, ) -> Result<(StatusCode, Vec), ServerError> { - let (cert_chain, _) = generate_certificate_chain("0.0.0.0".parse().unwrap()); let input_data: [u8; 64] = hex::decode(input_data).unwrap().try_into().unwrap(); - let attestation = AttestationExchangeMessage::from_attestation_generator( - &cert_chain, - input_data[..32].try_into().unwrap(), - shared_state.attestation_generator, - )? - .encode(); + let attestation = shared_state + .attestation_generator + .generate_attestation(input_data) + .await? + .encode(); Ok((StatusCode::OK, attestation)) } -pub async fn dummy_attestation_client(server_addr: SocketAddr) -> anyhow::Result<()> { +pub async fn dummy_attestation_client( + server_addr: SocketAddr, + attestation_verifier: AttestationVerifier, +) -> anyhow::Result { let input_data = [0; 64]; let response = reqwest::get(format!( "http://{server_addr}/attest/{}", @@ -68,10 +67,14 @@ pub async fn dummy_attestation_client(server_addr: SocketAddr) -> anyhow::Result let remote_attestation_message = AttestationExchangeMessage::decode(&mut &response[..])?; let remote_attestation_type = remote_attestation_message.attestation_type; - println!("{remote_attestation_type}"); - // TODO validate the attestation - Ok(()) + println!("Remote attestation type: {remote_attestation_type}"); + + attestation_verifier + .verify_attestation(remote_attestation_message.clone(), input_data) + .await?; + + Ok(remote_attestation_message) } struct ServerError(pub anyhow::Error); @@ -92,24 +95,6 @@ impl IntoResponse for ServerError { } } -/// Helper to generate a self-signed certificate for testing -fn generate_certificate_chain( - ip: IpAddr, -) -> (Vec>, PrivateKeyDer<'static>) { - let mut params = rcgen::CertificateParams::new(vec![]).unwrap(); - params.subject_alt_names.push(rcgen::SanType::IpAddress(ip)); - params - .distinguished_name - .push(rcgen::DnType::CommonName, ip.to_string()); - - let keypair = rcgen::KeyPair::generate().unwrap(); - let cert = params.self_signed(&keypair).unwrap(); - - let certs = vec![CertificateDer::from(cert)]; - let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(keypair.serialize_der())); - (certs, key) -} - #[cfg(test)] mod tests { @@ -119,13 +104,17 @@ mod tests { #[tokio::test] async fn test_dummy_server() { - let attestation_generator = AttestationType::None.get_quote_generator().unwrap(); + let attestation_generator = AttestationGenerator { + attestation_type: AttestationType::None, + }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let server_addr = listener.local_addr().unwrap(); dummy_attestation_server(listener, attestation_generator) .await .unwrap(); - dummy_attestation_client(server_addr).await.unwrap(); + dummy_attestation_client(server_addr, AttestationVerifier::do_not_verify()) + .await + .unwrap(); } } diff --git a/dummy-attestation-server/src/main.rs b/dummy-attestation-server/src/main.rs index fe1946d..666bcae 100644 --- a/dummy-attestation-server/src/main.rs +++ b/dummy-attestation-server/src/main.rs @@ -1,7 +1,10 @@ -use attested_tls_proxy::attestation::AttestationType; +use attested_tls_proxy::attestation::{ + measurements::get_measurements_from_file, AttestationGenerator, AttestationType, + AttestationVerifier, +}; use clap::{Parser, Subcommand}; use dummy_attestation_server::{dummy_attestation_client, dummy_attestation_server}; -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; use tokio::net::TcpListener; use tracing::level_filters::LevelFilter; @@ -30,6 +33,9 @@ enum CliCommand { Client { /// Socket address of a dummy attestation server server_addr: SocketAddr, + /// Optional path to file containing JSON measurements to be enforced on the server + #[arg(long, env = "SERVER_MEASUREMENTS")] + server_measurements: Option, }, } @@ -64,12 +70,30 @@ async fn main() -> anyhow::Result<()> { serde_json::Value::String(server_attestation_type.unwrap_or("none".to_string())), )?; - let attestation_generator = server_attestation_type.get_quote_generator()?; + let attestation_generator = AttestationGenerator { + attestation_type: server_attestation_type, + }; let listener = TcpListener::bind(listen_addr).await?; dummy_attestation_server(listener, attestation_generator).await?; } - CliCommand::Client { server_addr } => dummy_attestation_client(server_addr).await?, + CliCommand::Client { + server_addr, + server_measurements, + } => { + let attestation_verifier = match server_measurements { + Some(server_measurements) => AttestationVerifier { + accepted_measurements: get_measurements_from_file(server_measurements).await?, + pccs_url: None, + }, + None => AttestationVerifier::do_not_verify(), + }; + + let attestation_message = + dummy_attestation_client(server_addr, attestation_verifier).await?; + + println!("{attestation_message:?}") + } } Ok(()) diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 727dead..400313a 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -15,7 +15,7 @@ use tdx_quote::QuoteParseError; use thiserror::Error; /// This is the type sent over the channel to provide an attestation -#[derive(Debug, Serialize, Deserialize, Encode, Decode)] +#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] pub struct AttestationExchangeMessage { /// What CVM platform is used (including none) pub attestation_type: AttestationType, From 3ad5d013b210e029ec3ea314edd2845c237996a4 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 26 Nov 2025 11:44:35 +0100 Subject: [PATCH 19/51] Dummy server error handling --- dummy-attestation-server/src/lib.rs | 15 +++++++-------- dummy-attestation-server/src/main.rs | 2 ++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/dummy-attestation-server/src/lib.rs b/dummy-attestation-server/src/lib.rs index 769abdf..a4cd951 100644 --- a/dummy-attestation-server/src/lib.rs +++ b/dummy-attestation-server/src/lib.rs @@ -1,5 +1,6 @@ use std::net::SocketAddr; +use anyhow::anyhow; use attested_tls_proxy::attestation::{ AttestationExchangeMessage, AttestationGenerator, AttestationVerifier, }; @@ -19,27 +20,25 @@ struct SharedState { pub async fn dummy_attestation_server( listener: TcpListener, attestation_generator: AttestationGenerator, -) -> anyhow::Result { - let addr = listener.local_addr()?; - +) -> anyhow::Result<()> { let app = axum::Router::new() .route("/attest/{input_data}", axum::routing::get(get_attest)) .with_state(SharedState { attestation_generator, }); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); + axum::serve(listener, app).await?; - Ok(addr) + Ok(()) } async fn get_attest( State(shared_state): State, Path(input_data): Path, ) -> Result<(StatusCode, Vec), ServerError> { - let input_data: [u8; 64] = hex::decode(input_data).unwrap().try_into().unwrap(); + let input_data: [u8; 64] = hex::decode(input_data)? + .try_into() + .map_err(|_| anyhow!("Input data must be 64 bytes"))?; let attestation = shared_state .attestation_generator diff --git a/dummy-attestation-server/src/main.rs b/dummy-attestation-server/src/main.rs index 666bcae..09ec83d 100644 --- a/dummy-attestation-server/src/main.rs +++ b/dummy-attestation-server/src/main.rs @@ -75,6 +75,8 @@ async fn main() -> anyhow::Result<()> { }; let listener = TcpListener::bind(listen_addr).await?; + + println!("Listening on {}", listener.local_addr()?); dummy_attestation_server(listener, attestation_generator).await?; } CliCommand::Client { From 7cd75bafa58b431f5ee902daac4cc61a08ece5c6 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 27 Nov 2025 09:28:58 +0100 Subject: [PATCH 20/51] Write dummy output to file --- dummy-attestation-server/src/lib.rs | 10 ++++++---- dummy-attestation-server/src/main.rs | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dummy-attestation-server/src/lib.rs b/dummy-attestation-server/src/lib.rs index a4cd951..37b9f22 100644 --- a/dummy-attestation-server/src/lib.rs +++ b/dummy-attestation-server/src/lib.rs @@ -17,6 +17,7 @@ struct SharedState { attestation_generator: AttestationGenerator, } +/// An HTTP server which produces test attestations pub async fn dummy_attestation_server( listener: TcpListener, attestation_generator: AttestationGenerator, @@ -32,6 +33,8 @@ pub async fn dummy_attestation_server( Ok(()) } +/// Handler for the GET `/attest/{input_data}` route +/// Input data should be 64 bytes hex async fn get_attest( State(shared_state): State, Path(input_data): Path, @@ -49,6 +52,7 @@ async fn get_attest( Ok((StatusCode::OK, attestation)) } +/// A client helper which makes a request to `/attest` pub async fn dummy_attestation_client( server_addr: SocketAddr, attestation_verifier: AttestationVerifier, @@ -58,11 +62,9 @@ pub async fn dummy_attestation_client( "http://{server_addr}/attest/{}", hex::encode(input_data) )) - .await - .unwrap() + .await? .bytes() - .await - .unwrap(); + .await?; let remote_attestation_message = AttestationExchangeMessage::decode(&mut &response[..])?; let remote_attestation_type = remote_attestation_message.attestation_type; diff --git a/dummy-attestation-server/src/main.rs b/dummy-attestation-server/src/main.rs index 09ec83d..2e9fa70 100644 --- a/dummy-attestation-server/src/main.rs +++ b/dummy-attestation-server/src/main.rs @@ -4,6 +4,7 @@ use attested_tls_proxy::attestation::{ }; use clap::{Parser, Subcommand}; use dummy_attestation_server::{dummy_attestation_client, dummy_attestation_server}; +use parity_scale_codec::Encode; use std::{net::SocketAddr, path::PathBuf}; use tokio::net::TcpListener; use tracing::level_filters::LevelFilter; @@ -94,6 +95,8 @@ async fn main() -> anyhow::Result<()> { let attestation_message = dummy_attestation_client(server_addr, attestation_verifier).await?; + let encoded_attestation_message = attestation_message.encode(); + std::fs::write("attestation_message.bin", encoded_attestation_message)?; println!("{attestation_message:?}") } } From 9aec0798a7686d07658586bf0c176d5176d3db7d Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 28 Nov 2025 12:54:58 +0100 Subject: [PATCH 21/51] Fix dummy test --- dummy-attestation-server/src/lib.rs | 9 ++++++--- dummy-attestation-server/src/main.rs | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dummy-attestation-server/src/lib.rs b/dummy-attestation-server/src/lib.rs index 37b9f22..a3451d1 100644 --- a/dummy-attestation-server/src/lib.rs +++ b/dummy-attestation-server/src/lib.rs @@ -111,9 +111,12 @@ mod tests { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let server_addr = listener.local_addr().unwrap(); - dummy_attestation_server(listener, attestation_generator) - .await - .unwrap(); + + tokio::spawn(async move { + dummy_attestation_server(listener, attestation_generator) + .await + .unwrap(); + }); dummy_attestation_client(server_addr, AttestationVerifier::do_not_verify()) .await .unwrap(); diff --git a/dummy-attestation-server/src/main.rs b/dummy-attestation-server/src/main.rs index e7285aa..a899834 100644 --- a/dummy-attestation-server/src/main.rs +++ b/dummy-attestation-server/src/main.rs @@ -4,7 +4,6 @@ use attested_tls_proxy::attestation::{ }; use clap::{Parser, Subcommand}; use dummy_attestation_server::{dummy_attestation_client, dummy_attestation_server}; -use parity_scale_codec::Encode; use std::{net::SocketAddr, path::PathBuf}; use tokio::net::TcpListener; use tracing::level_filters::LevelFilter; From 4b59b3f8dceb2122cc2572e351de91bf0c5c49b6 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 1 Dec 2025 12:07:14 +0100 Subject: [PATCH 22/51] Add dummy attestation type handler --- dummy-attestation-server/src/lib.rs | 7 +-- dummy-attestation-server/src/main.rs | 5 +- src/attestation/mod.rs | 76 +++++++++++++++++++++++++++- src/lib.rs | 44 ++++------------ src/main.rs | 28 +++++----- 5 files changed, 103 insertions(+), 57 deletions(-) diff --git a/dummy-attestation-server/src/lib.rs b/dummy-attestation-server/src/lib.rs index af12051..d1e5a2f 100644 --- a/dummy-attestation-server/src/lib.rs +++ b/dummy-attestation-server/src/lib.rs @@ -98,16 +98,11 @@ impl IntoResponse for ServerError { #[cfg(test)] mod tests { - - use attested_tls_proxy::attestation::AttestationType; - use super::*; #[tokio::test] async fn test_dummy_server() { - let attestation_generator = AttestationGenerator { - attestation_type: AttestationType::None, - }; + let attestation_generator = AttestationGenerator::with_no_attestation(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let server_addr = listener.local_addr().unwrap(); diff --git a/dummy-attestation-server/src/main.rs b/dummy-attestation-server/src/main.rs index b674f7a..69fc715 100644 --- a/dummy-attestation-server/src/main.rs +++ b/dummy-attestation-server/src/main.rs @@ -76,9 +76,8 @@ async fn main() -> anyhow::Result<()> { serde_json::Value::String(server_attestation_type.unwrap_or("none".to_string())), )?; - let attestation_generator = AttestationGenerator { - attestation_type: server_attestation_type, - }; + let attestation_generator = + AttestationGenerator::new_not_dummy(server_attestation_type)?; let listener = TcpListener::bind(listen_addr).await?; diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index c3fbd3e..3c89c26 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -96,9 +96,58 @@ impl Display for AttestationType { #[derive(Clone)] pub struct AttestationGenerator { pub attestation_type: AttestationType, + dummy_dcap_url: Option, } impl AttestationGenerator { + pub fn new( + attestation_type: AttestationType, + dummy_dcap_url: Option, + ) -> Result { + match attestation_type { + AttestationType::Dummy => Self::new_dummy(dummy_dcap_url), + _ => Self::new_not_dummy(attestation_type), + } + } + + pub fn with_no_attestation() -> Self { + Self { + attestation_type: AttestationType::None, + dummy_dcap_url: None, + } + } + + pub fn new_not_dummy(attestation_type: AttestationType) -> Result { + if attestation_type == AttestationType::Dummy { + return Err(AttestationError::DummyUrl); + } + + Ok(Self { + attestation_type, + dummy_dcap_url: None, + }) + } + + pub fn new_dummy(dummy_dcap_url: Option) -> Result { + match dummy_dcap_url { + Some(url) => { + let url = if url.starts_with("http://") || url.starts_with("https://") { + url.to_string() + } else { + format!("http://{}", url.trim_start_matches("http://")) + }; + + let url = url.strip_suffix('/').unwrap_or(&url).to_string(); + + Ok(Self { + attestation_type: AttestationType::Dummy, + dummy_dcap_url: Some(url), + }) + } + None => Err(AttestationError::DummyUrl), + } + } + /// Generate an attestation exchange message pub async fn generate_attestation( &self, @@ -118,10 +167,31 @@ impl AttestationGenerator { match self.attestation_type { AttestationType::None => Ok(Vec::new()), AttestationType::AzureTdx => Ok(azure::create_azure_attestation(input_data).await?), - AttestationType::Dummy => Err(AttestationError::AttestationTypeNotSupported), + AttestationType::Dummy => self.generate_dummy_attestation(input_data).await, _ => dcap::create_dcap_attestation(input_data).await, } } + + async fn generate_dummy_attestation( + &self, + input_data: [u8; 64], + ) -> Result, AttestationError> { + let url = format!( + "{}/attest/{}", + self.dummy_dcap_url + .clone() + .ok_or(AttestationError::DummyUrl)?, + hex::encode(input_data) + ); + + Ok(reqwest::get(url) + .await + .map_err(|err| AttestationError::DummyServer(err.to_string()))? + .bytes() + .await + .map_err(|err| AttestationError::DummyServer(err.to_string()))? + .to_vec()) + } } /// Allows remote attestations to be verified @@ -266,4 +336,8 @@ pub enum AttestationError { MeasurementsNotAccepted, #[error("MAA: {0}")] Maa(#[from] azure::MaaError), + #[error("Dummy attestation type requires dummy service URL")] + DummyUrl, + #[error("Dummy server: {0}")] + DummyServer(String), } diff --git a/src/lib.rs b/src/lib.rs index 03f2103..0b4f091 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -879,9 +879,7 @@ mod tests { server_config, "127.0.0.1:0", target_addr, - AttestationGenerator { - attestation_type: AttestationType::DcapTdx, - }, + AttestationGenerator::new_not_dummy(AttestationType::DcapTdx).unwrap(), AttestationVerifier::expect_none(), ) .await @@ -897,9 +895,7 @@ mod tests { client_config, "127.0.0.1:0".to_string(), proxy_addr.to_string(), - AttestationGenerator { - attestation_type: AttestationType::None, - }, + AttestationGenerator::with_no_attestation(), AttestationVerifier::mock(), None, ) @@ -957,9 +953,7 @@ mod tests { server_tls_server_config, "127.0.0.1:0", target_addr, - AttestationGenerator { - attestation_type: AttestationType::None, - }, + AttestationGenerator::with_no_attestation(), AttestationVerifier::mock(), ) .await @@ -976,9 +970,7 @@ mod tests { client_tls_client_config, "127.0.0.1:0", proxy_addr.to_string(), - AttestationGenerator { - attestation_type: AttestationType::DcapTdx, - }, + AttestationGenerator::new_not_dummy(AttestationType::DcapTdx).unwrap(), AttestationVerifier::expect_none(), Some(client_cert_chain), ) @@ -1041,9 +1033,7 @@ mod tests { server_tls_server_config, "127.0.0.1:0", target_addr, - AttestationGenerator { - attestation_type: AttestationType::DcapTdx, - }, + AttestationGenerator::new_not_dummy(AttestationType::DcapTdx).unwrap(), AttestationVerifier::mock(), ) .await @@ -1060,9 +1050,7 @@ mod tests { client_tls_client_config, "127.0.0.1:0", proxy_addr.to_string(), - AttestationGenerator { - attestation_type: AttestationType::DcapTdx, - }, + AttestationGenerator::new_not_dummy(AttestationType::DcapTdx).unwrap(), AttestationVerifier::mock(), Some(client_cert_chain), ) @@ -1138,9 +1126,7 @@ mod tests { server_config, "127.0.0.1:0", target_addr, - AttestationGenerator { - attestation_type: AttestationType::DcapTdx, - }, + AttestationGenerator::new_not_dummy(AttestationType::DcapTdx).unwrap(), AttestationVerifier::expect_none(), ) .await @@ -1177,9 +1163,7 @@ mod tests { server_config, "127.0.0.1:0", target_addr, - AttestationGenerator { - attestation_type: AttestationType::None, - }, + AttestationGenerator::with_no_attestation(), AttestationVerifier::expect_none(), ) .await @@ -1195,9 +1179,7 @@ mod tests { client_config, "127.0.0.1:0".to_string(), proxy_addr.to_string(), - AttestationGenerator { - attestation_type: AttestationType::None, - }, + AttestationGenerator::with_no_attestation(), AttestationVerifier::mock(), None, ) @@ -1223,9 +1205,7 @@ mod tests { server_config, "127.0.0.1:0", target_addr, - AttestationGenerator { - attestation_type: AttestationType::DcapTdx, - }, + AttestationGenerator::new_not_dummy(AttestationType::DcapTdx).unwrap(), AttestationVerifier::expect_none(), ) .await @@ -1265,9 +1245,7 @@ mod tests { client_config, "127.0.0.1:0".to_string(), proxy_addr.to_string(), - AttestationGenerator { - attestation_type: AttestationType::None, - }, + AttestationGenerator::with_no_attestation(), attestation_verifier, None, ) diff --git a/src/main.rs b/src/main.rs index 87a1402..b5bae62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,10 +56,10 @@ enum CliCommand { /// Additional CA certificate to verify against (PEM) Defaults to no additional TLS certs. #[arg(long)] tls_ca_certificate: Option, - // TODO missing: - // Name: "dev-dummy-dcap", - // EnvVars: []string{"DEV_DUMMY_DCAP"}, - // Usage: "URL of the remote dummy DCAP service. Only with --client-attestation-type dummy.", + /// URL of the remote dummy attestation service. Only use with --client-attestation-type + /// dummy + #[arg(long)] + dev_dummy_dcap: Option, }, /// Run a proxy server Server { @@ -82,15 +82,15 @@ enum CliCommand { /// enabled. #[arg(long)] client_auth: bool, + /// URL of the remote dummy attestation service. Only use with --server-attestation-type + /// dummy + #[arg(long)] + dev_dummy_dcap: Option, // TODO missing: // Name: "listen-addr-healthcheck", // EnvVars: []string{"LISTEN_ADDR_HEALTHCHECK"}, // Value: "", // Usage: "address to listen on for health checks", - // - // Name: "dev-dummy-dcap", - // EnvVars: []string{"DEV_DUMMY_DCAP"}, - // Usage: "URL of the remote dummy DCAP service. Only with --server-attestation-type dummy.", }, /// Retrieve the attested TLS certificate from a proxy server GetTlsCert { @@ -156,6 +156,7 @@ async fn main() -> anyhow::Result<()> { tls_private_key_path, tls_certificate_path, tls_ca_certificate, + dev_dummy_dcap, } => { let target_addr = target_addr .strip_prefix("https://") @@ -190,9 +191,8 @@ async fn main() -> anyhow::Result<()> { None => None, }; - let client_attestation_generator = AttestationGenerator { - attestation_type: client_attestation_type, - }; + let client_attestation_generator = + AttestationGenerator::new(client_attestation_type, dev_dummy_dcap)?; let client = ProxyClient::new( tls_cert_and_chain, @@ -217,6 +217,7 @@ async fn main() -> anyhow::Result<()> { tls_certificate_path, client_auth, server_attestation_type, + dev_dummy_dcap, } => { let tls_cert_and_chain = load_tls_cert_and_key(tls_certificate_path, tls_private_key_path)?; @@ -225,9 +226,8 @@ async fn main() -> anyhow::Result<()> { serde_json::Value::String(server_attestation_type.unwrap_or("none".to_string())), )?; - let local_attestation_generator = AttestationGenerator { - attestation_type: server_attestation_type, - }; + let local_attestation_generator = + AttestationGenerator::new(server_attestation_type, dev_dummy_dcap)?; let server = ProxyServer::new( tls_cert_and_chain, From 6c5cca2d7b0dace0de6a39de9c44b3a4c88b96bd Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 1 Dec 2025 12:27:08 +0100 Subject: [PATCH 23/51] Dummy DCAP attestation verifier --- dummy-attestation-server/src/lib.rs | 5 ++--- src/attestation/mod.rs | 8 +++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dummy-attestation-server/src/lib.rs b/dummy-attestation-server/src/lib.rs index d1e5a2f..268704c 100644 --- a/dummy-attestation-server/src/lib.rs +++ b/dummy-attestation-server/src/lib.rs @@ -1,9 +1,8 @@ +pub use attested_tls_proxy::attestation::AttestationGenerator; use std::net::SocketAddr; use anyhow::anyhow; -use attested_tls_proxy::attestation::{ - AttestationExchangeMessage, AttestationGenerator, AttestationVerifier, -}; +use attested_tls_proxy::attestation::{AttestationExchangeMessage, AttestationVerifier}; use axum::{ extract::{Path, State}, http::StatusCode, diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 3c89c26..98ba6ff 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -260,7 +260,13 @@ impl AttestationVerifier { .await? } AttestationType::Dummy => { - return Err(AttestationError::AttestationTypeNotSupported); + // Dummy assumes dummy DCAP + dcap::verify_dcap_attestation( + attestation_exchange_message.attestation, + expected_input_data, + self.pccs_url.clone(), + ) + .await? } _ => { dcap::verify_dcap_attestation( From 9a319166efa5e2c32279409963c23010f134536e Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 1 Dec 2025 12:31:55 +0100 Subject: [PATCH 24/51] Rm unneeded files --- .gitignore | 2 +- ca.crt | 30 ------------------------------ 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 ca.crt diff --git a/.gitignore b/.gitignore index c9d112b..1d155a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target remote-cert.crt /quotes - +ca.crt diff --git a/ca.crt b/ca.crt deleted file mode 100644 index 0a78498..0000000 --- a/ca.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFHjCCAwagAwIBAgIUcLc8Vru9fKmqWnBvJn+UwDI6nzYwDQYJKoZIhvcNAQEL -BQAwFTETMBEGA1UEAwwKTXkgVGVzdCBDQTAeFw0yNTExMjgwNzIxNTBaFw0zNTEx -MjYwNzIxNTBaMBUxEzARBgNVBAMMCk15IFRlc3QgQ0EwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQC+c+7/JQk5vkKdqoMjpNRYaRSz0gg73hCpHJ0uzb7c -n0sFe8vJ23ui+VRmZJKodYhTvMiZaJbaUwUERns4wTenKAxrO6Wl+rmXIW81A81g -o4YRQyhN7JZ1mSLFO1zkOZWoxsi7dCsRIoWBJK1+IxWK+joU0vhnrSPM+C+56BMN -41nQtxzwdv+AuZUs8G0mUJbnTiuO9b1y8iPs2xp493XAETpCfqx7SkErL01oY5L8 -aBySrDHAIFio+RRoW0cyDIN++v/5tc8JjSX6VJgBxmVd4H+FgV+TXnbWAg6sySJN -kDpj3lgtEYdGQoETtwxP6eyiG/HjhsHGyY3VqD3DQRKJZnTOt1yFJRvIAow2W7Xe -GKBVKmiftrIoNXbDrb+sTOoBFM+RhmmKrPlu2RJj2I8L/NdzV9mRXyZ45s6KPwcA -963niKtiR6dBkr2oVa2SBTXsiaDj3WZ8COm3LFCDpplzC28tt2vtSnz+UcNiCv9M -VDZcPQgkR3Yvbv1/sOVUqvwAE9jQEzDlGjNtHDw5LGYSqwe0u+gYVkq6hBTsgidR -iVvD8mbjGAKRUjLuZbXX3ZYJn7X2ARDAWCRGHl5bbiJzZWkIYt+1thJ/1BpJhGOw -D5rMj7Fmx3jnKmK/LWj5ioT016xKXcQsVX/eAK+HrRHabv73z2ihZyEOKvbLOK9a -uQIDAQABo2YwZDAdBgNVHQ4EFgQUDdi4+MPNMVAF+mluGMZBt3i2x3cwHwYDVR0j -BBgwFoAUDdi4+MPNMVAF+mluGMZBt3i2x3cwEgYDVR0TAQH/BAgwBgEB/wIBADAO -BgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAGPOj+I98KcrDHJtdKVi -9xwnFAFZHS0E8gI9031w0chJvSXoW+NBSgYsHl6Yrkd5/lbAZJePg6vaRR2w5ErO -y2bYwXgt6+lTfIIFtqQ21Tw1VwzoMS2m7gdJV+w3GbRExn5UOILOkz9MbfO299lP -Hi+/2FYeLuOOailvYi4d0FATBHTlX6fJf340BIV+Vu4FNIdqAFaN5wdgVqhZJ7Nu -IjI/5vHVTApIwaPPgFH5jiY5J5kM37IWD84ey60eaY3oR28qx9Jp4bIVkP154l7A -zBZCl3VnpkqZHIqth97aPhuLy1u/XRGJrwm0sopuMv7h2HPmC88g3vS/kqXrF+Xj -o4GOYTRDRLwcUUg8kJA37ulcgB6efO5RkjWf32iOXO3rpNqBdCafzeLxs0f8a3MZ -Vl3dqFRKKQ27KxvMYchi0lEb9lX1hSnknt0mIeI7omIT2gdrutlGfR22e4OQCX5p -cPR6u8sjvDA3gvet3z0HKcl3j/4RsIOVB4nTpIE6Ty5ua94BZ8QbA3nPqqbROmgH -Sc+/6FREo6q42CUjssxZ7zYEEx+sboNvNYCQF8uzx5NK2/rpZvHknIV+KXBp5aDT -jgc6pdfC53UACufj4K8Y+w9xlyEWBkefnUtXys3uvE3WIqcmPYmOoFxYp+Ws44OA -PUM+UzhD254ttJXVg4FWB3oC ------END CERTIFICATE----- From b123fb4725ee185f8d9c31f6fba04cdfe5614e94 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 1 Dec 2025 16:38:18 +0100 Subject: [PATCH 25/51] Temporary - dont get AK cert from vTPM --- src/attestation/azure.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index a355cff..9fb358d 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -13,7 +13,7 @@ use crate::attestation::{ nv_index, }; -const TPM_AK_CERT_IDX: u32 = 0x1C101D0; +// const TPM_AK_CERT_IDX: u32 = 0x1C101D0; pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaError> { let td_report = report::get_report()?; @@ -23,14 +23,15 @@ pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, M let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?; - let ak_certificate_der = read_ak_certificate_from_tpm()?; + // let ak_certificate_der = read_ak_certificate_from_tpm()?; let tpm_attestation = TpmAttest { - ak_certificate_pem: pem_rfc7468::encode_string( - "CERTIFICATE", - pem_rfc7468::LineEnding::default(), - &ak_certificate_der, - )?, + ak_certificate_pem: "TODO".to_string(), + // ak_certificate_pem: pem_rfc7468::encode_string( + // "CERTIFICATE", + // pem_rfc7468::LineEnding::default(), + // &ak_certificate_der, + // )?, quote: vtpm::get_quote(&input_data)?, event_log: Vec::new(), instance_info: None, @@ -136,10 +137,10 @@ struct TpmAttest { instance_info: Option>, } -fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { - let mut context = nv_index::get_session_context()?; - nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) -} +// fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { +// let mut context = nv_index::get_session_context()?; +// nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) +// } #[derive(Error, Debug)] pub enum MaaError { From 584e320336a1e7d394e9eb50ffd0b0c7eca5091b Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 1 Dec 2025 21:36:15 +0100 Subject: [PATCH 26/51] Add test asset and minor fixes --- src/attestation/azure.rs | 16 +++++++++++++--- test-assets/azure-tdx-1764620247665812984 | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 test-assets/azure-tdx-1764620247665812984 diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 9fb358d..5e1cc0a 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -32,7 +32,7 @@ pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, M // pem_rfc7468::LineEnding::default(), // &ak_certificate_der, // )?, - quote: vtpm::get_quote(&input_data)?, + quote: vtpm::get_quote(&input_data[..32])?, event_log: Vec::new(), instance_info: None, }; @@ -78,7 +78,7 @@ pub async fn verify_azure_attestation( .await .unwrap(); - let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now).unwrap(); + let _verified_report = dcap_qvl::verify::verify(&tdx_quote_bytes, &collateral, now).unwrap(); // Check that hcl_report_bytes (hashed?) matches TDX quote report data // if get_quote_input_data(quote.report) != quote_input { @@ -98,7 +98,7 @@ pub async fn verify_azure_attestation( let vtpm_quote = attestation_document.tpm_attestation.quote; let hcl_ak_pub_der = hcl_ak_pub.key.try_to_der().unwrap(); let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der).unwrap(); - vtpm_quote.verify(&pub_key, &expected_input_data)?; + vtpm_quote.verify(&pub_key, &expected_input_data[..32])?; let _pcrs = vtpm_quote.pcrs_sha256(); // TODO parse AK certificate @@ -195,4 +195,14 @@ mod tests { 64 ); } + + #[tokio::test] + async fn test_verify() { + // Will pass if now = 1764621240 seconds + let attestation_bytes: &'static [u8] = + include_bytes!("../../test-assets/azure-tdx-1764620247665812984"); + verify_azure_attestation(attestation_bytes.to_vec(), [0; 64], None) + .await + .unwrap(); + } } diff --git a/test-assets/azure-tdx-1764620247665812984 b/test-assets/azure-tdx-1764620247665812984 new file mode 100644 index 0000000..bbe43ea --- /dev/null +++ b/test-assets/azure-tdx-1764620247665812984 @@ -0,0 +1 @@ +{"tdx_quote_base64":"BAACAIEAAAAAAAAAk5pyM_ecTKmUCg2zlX8GB3Thpre77mKg7jpNBav8PEkAAAAABwEDAAAAAAAAAAAAAAAAAEm2b6pFHRnrvb6JNxuNrytlqjmE7JARA0Pp4u7BFq8IhQ-iDjsaqah013plOA7n5gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAADnGAYAAAAAABK9vBYJ7wZtt6nseybXUJNG9MpG6EYnQJbLJZObHnEZRAYcoZowxnQ_HlmRSO7kUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQEAAAPAXiakJvRTLw0bYwsFSaBrXW_Hrvh2vOKIV6dtzhUqGslj57AYxSNo9PiaTEm-ZawvKodme-6QVRCGddPYbFoOrcwydyxD9mf5PwaMAOdpOKJNtwow7lCT3oqVghyxhUPqIP5O_j2a8Dz9-Tpa1cQuZpSiimuRmchsKX3gPO3O8GAEoQAAADAxkbBP8ABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAOcAAAAAAAAA5aOntdgwwpU7mFNMbFmjo0_cNOkz9_WJjwqFzwiEa8oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANyeKnxvlI8XR040p_xD7QMPfBVj8bq932NAyC4OVKjFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHpJi_9CXp9wO3djOvudEoQR5boWPVXYM6F7X8QIwOVUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVQhcPV03wFyimgChXn5NVWfyQTse-BNQdLQotqL9Hf7Ft-i9mrUpeS7sanh8nDPB08_JariTf0DrJuO5Lmf8yIAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwUAYg4AAC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlFOGpDQ0JKZWdBd0lCQWdJVkFQbnErb2llZDBLNlhjREN5aUlXc01ESG51bFJNQW9HQ0NxR1NNNDlCQU1DCk1IQXhJakFnQmdOVkJBTU1HVWx1ZEdWc0lGTkhXQ0JRUTBzZ1VHeGhkR1p2Y20wZ1EwRXhHakFZQmdOVkJBb00KRVVsdWRHVnNJRU52Y25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRQpDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1EY3lPVEUxTXpNeE5Gb1hEVE15TURjeU9URTFNek14Ck5Gb3djREVpTUNBR0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFuClA2bUNzUmhHaFUzVDlrSHpCaG1SUHNKM1d2NkxPSzluUFBUY1pxVis2QjlDSDJJdDZCZ0U3VkFMeUlpMGdIemEKYmhzeE5FM2ZKc045NWhaaUFXcmJvNElERERDQ0F3Z3dId1lEVlIwakJCZ3dGb0FVbFc5ZHpiMGI0ZWxBU2NuVQo5RFBPQVZjTDNsUXdhd1lEVlIwZkJHUXdZakJnb0Y2Z1hJWmFhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk5DOXdZMnRqY213L1kyRTkKY0d4aGRHWnZjbTBtWlc1amIyUnBibWM5WkdWeU1CMEdBMVVkRGdRV0JCUXZrTU1jN25RT1k3OTlXM2tBcTJuSQo5bW1LZVRBT0JnTlZIUThCQWY4RUJBTUNCc0F3REFZRFZSMFRBUUgvQkFJd0FEQ0NBamtHQ1NxR1NJYjRUUUVOCkFRU0NBaW93Z2dJbU1CNEdDaXFHU0liNFRRRU5BUUVFRUtUMVh3NS82WG1WTDdROWRhVS85WXN3Z2dGakJnb3EKaGtpRytFMEJEUUVDTUlJQlV6QVFCZ3NxaGtpRytFMEJEUUVDQVFJQkF6QVFCZ3NxaGtpRytFMEJEUUVDQWdJQgpBekFRQmdzcWhraUcrRTBCRFFFQ0F3SUJBakFRQmdzcWhraUcrRTBCRFFFQ0JBSUJBakFRQmdzcWhraUcrRTBCCkRRRUNCUUlCQkRBUUJnc3Foa2lHK0UwQkRRRUNCZ0lCQVRBUUJnc3Foa2lHK0UwQkRRRUNCd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDQ0FJQkJUQVFCZ3NxaGtpRytFMEJEUUVDQ1FJQkFEQVFCZ3NxaGtpRytFMEJEUUVDQ2dJQgpBREFRQmdzcWhraUcrRTBCRFFFQ0N3SUJBREFRQmdzcWhraUcrRTBCRFFFQ0RBSUJBREFRQmdzcWhraUcrRTBCCkRRRUNEUUlCQURBUUJnc3Foa2lHK0UwQkRRRUNEZ0lCQURBUUJnc3Foa2lHK0UwQkRRRUNEd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDRUFJQkFEQVFCZ3NxaGtpRytFMEJEUUVDRVFJQkRUQWZCZ3NxaGtpRytFMEJEUUVDRWdRUQpBd01DQWdRQkFBVUFBQUFBQUFBQUFEQVFCZ29xaGtpRytFMEJEUUVEQkFJQUFEQVVCZ29xaGtpRytFMEJEUUVFCkJBYVF3RzhBQUFBd0R3WUtLb1pJaHZoTkFRMEJCUW9CQVRBZUJnb3Foa2lHK0UwQkRRRUdCQkRjMmREb09seDgKNG0rT09kRzVjKzBkTUVRR0NpcUdTSWI0VFFFTkFRY3dOakFRQmdzcWhraUcrRTBCRFFFSEFRRUIvekFRQmdzcQpoa2lHK0UwQkRRRUhBZ0VCQURBUUJnc3Foa2lHK0UwQkRRRUhBd0VCL3pBS0JnZ3Foa2pPUFFRREFnTkpBREJHCkFpRUFwemRPNEx4amFKVHlVc3pIQ3B3aXVDL05ISzBmV2JpcGR5TlFuMGZmRWYwQ0lRRHRVeU41ZmJNbjlUUHIKWG93R1Y0Y2lIVHJkUEJOZVN2K3JEeEZNTDdVVktBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2xqQ0NBajJnQXdJQkFnSVZBSlZ2WGMyOUcrSHBRRW5KMVBRenpnRlhDOTVVTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEF4SWpBZwpCZ05WQkFNTUdVbHVkR1ZzSUZOSFdDQlFRMHNnVUd4aGRHWnZjbTBnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzCklFTnZjbkJ2Y21GMGFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXgKQ3pBSkJnTlZCQVlUQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU5TQi83dDIxbFhTTwoyQ3V6cHh3NzRlSkI3MkV5REdnVzVyWEN0eDJ0VlRMcTZoS2s2eitVaVJaQ25xUjdwc092Z3FGZVN4bG1UbEpsCmVUbWkyV1l6M3FPQnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlMKQmdOVkhSOEVTekJKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeQpkbWxqWlhNdWFXNTBaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVsVzlkCnpiMGI0ZWxBU2NuVTlEUE9BVmNMM2xRd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUIKQWY4Q0FRQXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdYc1ZraTB3K2k2VllHVzNVRi8yMnVhWGUwWUpEajFVZQpuQStUakQxYWk1Y0NJQ1liMVNBbUQ1eGtmVFZwdm80VW95aVNZeHJEV0xtVVI0Q0k5Tkt5ZlBOKwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDanpDQ0FqU2dBd0lCQWdJVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3dDZ1lJS29aSXpqMEVBd0l3CmFERWFNQmdHQTFVRUF3d1JTVzUwWld3Z1UwZFlJRkp2YjNRZ1EwRXhHakFZQmdOVkJBb01FVWx1ZEdWc0lFTnYKY25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRUNBd0NRMEV4Q3pBSgpCZ05WQkFZVEFsVlRNQjRYRFRFNE1EVXlNVEV3TkRVeE1Gb1hEVFE1TVRJek1USXpOVGsxT1Zvd2FERWFNQmdHCkExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52Y25CdmNtRjAKYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUpCZ05WQkFZVApBbFZUTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQzZuRXdNRElZWk9qL2lQV3NDemFFS2k3CjFPaU9TTFJGaFdHamJuQlZKZlZua1k0dTNJamtEWVlMME14TzRtcXN5WWpsQmFsVFZZeEZQMnNKQks1emxLT0IKdXpDQnVEQWZCZ05WSFNNRUdEQVdnQlFpWlF6V1dwMDBpZk9EdEpWU3YxQWJPU2NHckRCU0JnTlZIUjhFU3pCSgpNRWVnUmFCRGhrRm9kSFJ3Y3pvdkwyTmxjblJwWm1sallYUmxjeTUwY25WemRHVmtjMlZ5ZG1salpYTXVhVzUwClpXd3VZMjl0TDBsdWRHVnNVMGRZVW05dmRFTkJMbVJsY2pBZEJnTlZIUTRFRmdRVUltVU0xbHFkTkluemc3U1YKVXI5UUd6a25CcXd3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUV3Q2dZSQpLb1pJemowRUF3SURTUUF3UmdJaEFPVy81UWtSK1M5Q2lTRGNOb293THVQUkxzV0dmL1lpN0dTWDk0Qmd3VHdnCkFpRUE0SjBsckhvTXMrWG81by9zWDZPOVFXeEhSQXZaVUdPZFJRN2N2cVJYYXFJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","hcl_report_base64":"SENMQQIAAABaCQAAAgAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAAAAAAAAAAAAwMZGwT_AAUAAAAAAAAAAEB73O4ihAAL1Fq3YJOLBeFadfZagSvRh378KKR_plJRdg0_lzrFanQ73FOJV8ioAk0EF89TAb_qtJkcf-PQqX8vuQr5d0gmOU2-DKrvVG4VHWCx7tELBMBIwOniiJl_Gr7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGo1RO-ajyGFbRpEQvwrhKfdSit715Hy11JkCim5iEjF_wEDAAAAAAAHAQMAAAAAAAAAAAAAAAAASbZvqkUdGeu9vok3G42vK2WqOYTskBEDQ-ni7sEWrwiFD6IOOxqpqHTXemU4DufmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA5xgGAAAAAAASvbwWCe8Gbbep7Hsm11CTRvTKRuhGJ0CWyyWTmx5xGUQGHKGaMMZ0Px5ZkUju5FEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJoEAAABAAAABAAAAAEAAACGBAAAeyJrZXlzIjpbeyJraWQiOiJIQ0xBa1B1YiIsImtleV9vcHMiOlsic2lnbiJdLCJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiJwM3g2SUFBQTY4MFViZ3AzdU5tTF93Nm1FeTJjaUd2bkFaVHFwM2tueDZGQmtieTMtZFQ2RlhXVl9NYmM3RUk4RHNQRTk1WURnT2szVEJjZktkNmxBdUw0QTdoeVBZdzc3c1dJV1dySnlBTE9FUnNmQTJiUC1hNzQyVjQ5cFJrWXRYWFI4c1NuYWNuS2Rfd0hNZ0NpVzZFNXJoUUZXTzRINUEybW51TV9xcnhocHRlQ1BidmxvN2NiTFNWOXBHTEtNbnV0RUJDYzBITndLSnZ6ZWQ5WG15cGpWWUJJTTdSbXdoNmw0bk44VzFkY0ZOby1fTnhobTV6YXU4bXo5ZktoTlFMSXpPaWkzWk95TWE0dnYtUzNJYmZoTVBUMDZqMnllbUVXWUNTbGxqQU9paGxTYllteXQtT0k3ZHJUdTZjV0tpQmNYanJKa1ZWMVFKcUdhOWRzNHcifSx7ImtpZCI6IkhDTEVrUHViIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImt0eSI6IlJTQSIsImUiOiJBUUFCIiwibiI6Im9WakUyQUFBRFVmSUlLajJtNjB0dXp2M1NJbDdBVXE2aElTeExfYmlrYWdIeUpTSzJTaWVidVJka25oSWE5MTZSNk1hdlAtdUU0TjRJaXhCMHliZlZ5TGg1YThzcVBNNHFqMnYxN2o3dXp4QWJtTldkTVJvTjRKZHZKNWpOY0FJMUs4ajF5M0pkWGkzZ0d0aWJWTkVuU3RoQ2ZsYzNPdDg1WXZiUlpfYVc5dV95S3hJQUwxWTFWQVE2WnFTaUxPM1B6Q0hUc09BZml2N3B3QUUtb3R5YjVmcFF1bFBxWFVVOGV6a0hSYmFtNUlyYnZCT18wS0FDdmhMaEFvdVB3a2YtREFhVW5uc01kT1ZQZkI1enZtMGJGZnZKTVBRTWNydUE4dVktZk1vbWZGY0tHWlphVXZ2ekNZa2tqd1N6Zmo2NDVLcTd6bUEtNkR4RnV1b2E4enNzdyJ9XSwidm0tY29uZmlndXJhdGlvbiI6eyJyb290LWNlcnQtdGh1bWJwcmludCI6IiIsImNvbnNvbGUtZW5hYmxlZCI6dHJ1ZSwic2VjdXJlLWJvb3QiOmZhbHNlLCJ0cG0tZW5hYmxlZCI6dHJ1ZSwidHBtLXBlcnNpc3RlZCI6dHJ1ZSwidm1VbmlxdWVJZCI6ImJkZDQzOTA0LWRiZjgtNDMzNy04YjkxLTNhZGJhOTJjNWViZCJ9LCJ1c2VyLWRhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","tpm_attestation":{"ak_certificate_pem":"TODO","quote":{"signature":[110,248,159,138,226,196,44,57,45,211,211,146,41,250,37,131,18,8,202,182,40,140,35,234,141,77,101,188,191,68,38,32,125,144,110,133,99,253,56,113,114,89,116,45,193,205,37,31,193,36,89,241,54,235,160,174,148,222,118,62,254,3,89,82,135,246,19,194,191,53,214,80,17,212,150,57,152,241,198,143,153,85,179,27,57,153,133,0,101,194,63,104,133,175,42,40,205,77,133,203,193,179,162,236,214,232,242,250,216,3,179,103,187,107,71,226,230,157,83,200,77,223,222,138,25,16,241,154,212,182,24,22,175,187,96,204,119,208,198,105,46,12,213,14,13,8,199,106,170,63,6,143,20,125,228,59,116,37,120,140,227,105,65,5,82,11,176,245,53,78,134,194,15,51,74,51,48,248,91,228,154,168,232,35,1,219,128,217,70,204,71,139,94,3,26,132,47,222,129,144,5,128,48,73,163,249,120,46,12,150,152,144,137,210,19,37,144,253,19,41,190,105,131,157,175,43,166,113,68,81,127,227,196,119,251,131,188,112,74,168,190,251,218,156,38,98,249,159,14,253,163,183,159,107,103,122],"message":[255,84,67,71,128,24,0,34,0,11,176,196,237,28,251,197,97,211,73,25,195,246,137,118,108,234,140,194,246,203,163,159,152,132,197,213,135,46,152,40,142,215,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,47,175,22,0,0,0,2,0,0,0,0,1,32,32,3,18,0,18,0,3,0,0,0,1,0,11,3,255,255,255,0,32,73,235,12,37,110,154,84,47,86,198,201,8,64,241,53,217,188,196,121,140,27,5,162,10,40,110,110,222,74,249,168,118],"pcrs":[[86,31,111,47,249,11,244,110,95,1,184,215,193,191,150,12,45,100,182,72,51,37,241,141,120,33,124,132,99,15,211,83],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[189,192,76,193,11,79,205,110,159,121,26,105,91,230,132,199,199,242,175,241,156,196,82,235,102,58,28,53,82,47,75,28],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[196,162,90,109,119,4,98,159,99,219,132,210,14,168,219,14,156,224,2,178,128,27,233,163,64,9,31,231,172,88,134,153],[94,18,234,167,32,238,182,179,211,0,70,66,80,242,205,229,18,48,16,198,193,183,6,131,184,20,41,112,3,12,244,221],[180,90,56,59,226,129,211,3,171,159,183,227,211,194,92,114,209,57,251,5,37,19,45,58,244,93,121,15,213,151,11,190],[18,77,175,71,180,214,113,121,167,125,195,193,188,202,25,138,225,238,29,9,74,42,135,153,116,132,46,68,171,152,187,6],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[159,74,87,117,18,44,164,112,62,19,90,154,230,4,30,222,173,0,100,38,46,57,157,241,28,168,81,130,176,241,84,29],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[171,215,198,149,255,219,96,129,233,150,54,238,1,109,19,34,145,156,104,208,73,182,152,179,153,210,42,226,21,161,33,191],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]},"event_log":[],"instance_info":null}} \ No newline at end of file From dd8ca0922188d7bc86c5f34ba9f32d77693c083f Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 2 Dec 2025 08:50:03 +0100 Subject: [PATCH 27/51] Fix nv-index reader --- src/attestation/azure.rs | 23 +++++++++++------------ src/attestation/nv_index.rs | 6 ++++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 5e1cc0a..9d43544 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -13,7 +13,7 @@ use crate::attestation::{ nv_index, }; -// const TPM_AK_CERT_IDX: u32 = 0x1C101D0; +const TPM_AK_CERT_IDX: u32 = 0x1C101D0; pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaError> { let td_report = report::get_report()?; @@ -23,15 +23,14 @@ pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, M let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?; - // let ak_certificate_der = read_ak_certificate_from_tpm()?; + let ak_certificate_der = read_ak_certificate_from_tpm()?; let tpm_attestation = TpmAttest { - ak_certificate_pem: "TODO".to_string(), - // ak_certificate_pem: pem_rfc7468::encode_string( - // "CERTIFICATE", - // pem_rfc7468::LineEnding::default(), - // &ak_certificate_der, - // )?, + ak_certificate_pem: pem_rfc7468::encode_string( + "CERTIFICATE", + pem_rfc7468::LineEnding::default(), + &ak_certificate_der, + )?, quote: vtpm::get_quote(&input_data[..32])?, event_log: Vec::new(), instance_info: None, @@ -137,10 +136,10 @@ struct TpmAttest { instance_info: Option>, } -// fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { -// let mut context = nv_index::get_session_context()?; -// nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) -// } +fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { + let mut context = nv_index::get_session_context()?; + nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) +} #[derive(Error, Debug)] pub enum MaaError { diff --git a/src/attestation/nv_index.rs b/src/attestation/nv_index.rs index 40b325a..2f7174e 100644 --- a/src/attestation/nv_index.rs +++ b/src/attestation/nv_index.rs @@ -1,5 +1,5 @@ use tss_esapi::{ - handles::NvIndexHandle, + handles::{NvIndexHandle, NvIndexTpmHandle}, interface_types::{resource_handles::NvAuth, session_handles::AuthSession}, structures::MaxNvBuffer, tcti_ldr::{DeviceConfig, TctiNameConf}, @@ -15,7 +15,9 @@ pub fn get_session_context() -> Result { } pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, tss_esapi::Error> { - let handle = NvIndexHandle::from(index); + let nv_tpm_handle = NvIndexTpmHandle::new(index)?; + let esys_handle = ctx.tr_from_tpm_public(nv_tpm_handle.into())?; + let handle = NvIndexHandle::from(esys_handle); let size = ctx .nv_read_public(handle)? .0 From 15ad265a64177be6e8206379f96f0dea03d0ec04 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 2 Dec 2025 09:00:42 +0100 Subject: [PATCH 28/51] Fix nv-index reader --- src/attestation/azure.rs | 2 +- src/attestation/nv_index.rs | 16 +++------------- ...47665812984 => azure-tdx-1764662251380464271} | 2 +- 3 files changed, 5 insertions(+), 15 deletions(-) rename test-assets/{azure-tdx-1764620247665812984 => azure-tdx-1764662251380464271} (53%) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 9d43544..1be961b 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -199,7 +199,7 @@ mod tests { async fn test_verify() { // Will pass if now = 1764621240 seconds let attestation_bytes: &'static [u8] = - include_bytes!("../../test-assets/azure-tdx-1764620247665812984"); + include_bytes!("../../test-assets/azure-tdx-1764662251380464271"); verify_azure_attestation(attestation_bytes.to_vec(), [0; 64], None) .await .unwrap(); diff --git a/src/attestation/nv_index.rs b/src/attestation/nv_index.rs index 2f7174e..f38c267 100644 --- a/src/attestation/nv_index.rs +++ b/src/attestation/nv_index.rs @@ -1,7 +1,6 @@ use tss_esapi::{ - handles::{NvIndexHandle, NvIndexTpmHandle}, + handles::NvIndexTpmHandle, interface_types::{resource_handles::NvAuth, session_handles::AuthSession}, - structures::MaxNvBuffer, tcti_ldr::{DeviceConfig, TctiNameConf}, Context, }; @@ -16,15 +15,6 @@ pub fn get_session_context() -> Result { pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, tss_esapi::Error> { let nv_tpm_handle = NvIndexTpmHandle::new(index)?; - let esys_handle = ctx.tr_from_tpm_public(nv_tpm_handle.into())?; - let handle = NvIndexHandle::from(esys_handle); - let size = ctx - .nv_read_public(handle)? - .0 - .data_size() - .try_into() - .unwrap_or(0u16); - - let data: MaxNvBuffer = ctx.nv_read(NvAuth::Owner, handle, size, 0)?; - Ok(data.to_vec()) + let buf = tss_esapi::abstraction::nv::read_full(ctx, NvAuth::Owner, nv_tpm_handle)?; + Ok(buf.to_vec()) } diff --git a/test-assets/azure-tdx-1764620247665812984 b/test-assets/azure-tdx-1764662251380464271 similarity index 53% rename from test-assets/azure-tdx-1764620247665812984 rename to test-assets/azure-tdx-1764662251380464271 index bbe43ea..fce573d 100644 --- a/test-assets/azure-tdx-1764620247665812984 +++ b/test-assets/azure-tdx-1764662251380464271 @@ -1 +1 @@ -{"tdx_quote_base64":"BAACAIEAAAAAAAAAk5pyM_ecTKmUCg2zlX8GB3Thpre77mKg7jpNBav8PEkAAAAABwEDAAAAAAAAAAAAAAAAAEm2b6pFHRnrvb6JNxuNrytlqjmE7JARA0Pp4u7BFq8IhQ-iDjsaqah013plOA7n5gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAADnGAYAAAAAABK9vBYJ7wZtt6nseybXUJNG9MpG6EYnQJbLJZObHnEZRAYcoZowxnQ_HlmRSO7kUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQEAAAPAXiakJvRTLw0bYwsFSaBrXW_Hrvh2vOKIV6dtzhUqGslj57AYxSNo9PiaTEm-ZawvKodme-6QVRCGddPYbFoOrcwydyxD9mf5PwaMAOdpOKJNtwow7lCT3oqVghyxhUPqIP5O_j2a8Dz9-Tpa1cQuZpSiimuRmchsKX3gPO3O8GAEoQAAADAxkbBP8ABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAOcAAAAAAAAA5aOntdgwwpU7mFNMbFmjo0_cNOkz9_WJjwqFzwiEa8oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANyeKnxvlI8XR040p_xD7QMPfBVj8bq932NAyC4OVKjFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHpJi_9CXp9wO3djOvudEoQR5boWPVXYM6F7X8QIwOVUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVQhcPV03wFyimgChXn5NVWfyQTse-BNQdLQotqL9Hf7Ft-i9mrUpeS7sanh8nDPB08_JariTf0DrJuO5Lmf8yIAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwUAYg4AAC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlFOGpDQ0JKZWdBd0lCQWdJVkFQbnErb2llZDBLNlhjREN5aUlXc01ESG51bFJNQW9HQ0NxR1NNNDlCQU1DCk1IQXhJakFnQmdOVkJBTU1HVWx1ZEdWc0lGTkhXQ0JRUTBzZ1VHeGhkR1p2Y20wZ1EwRXhHakFZQmdOVkJBb00KRVVsdWRHVnNJRU52Y25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRQpDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1EY3lPVEUxTXpNeE5Gb1hEVE15TURjeU9URTFNek14Ck5Gb3djREVpTUNBR0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFuClA2bUNzUmhHaFUzVDlrSHpCaG1SUHNKM1d2NkxPSzluUFBUY1pxVis2QjlDSDJJdDZCZ0U3VkFMeUlpMGdIemEKYmhzeE5FM2ZKc045NWhaaUFXcmJvNElERERDQ0F3Z3dId1lEVlIwakJCZ3dGb0FVbFc5ZHpiMGI0ZWxBU2NuVQo5RFBPQVZjTDNsUXdhd1lEVlIwZkJHUXdZakJnb0Y2Z1hJWmFhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk5DOXdZMnRqY213L1kyRTkKY0d4aGRHWnZjbTBtWlc1amIyUnBibWM5WkdWeU1CMEdBMVVkRGdRV0JCUXZrTU1jN25RT1k3OTlXM2tBcTJuSQo5bW1LZVRBT0JnTlZIUThCQWY4RUJBTUNCc0F3REFZRFZSMFRBUUgvQkFJd0FEQ0NBamtHQ1NxR1NJYjRUUUVOCkFRU0NBaW93Z2dJbU1CNEdDaXFHU0liNFRRRU5BUUVFRUtUMVh3NS82WG1WTDdROWRhVS85WXN3Z2dGakJnb3EKaGtpRytFMEJEUUVDTUlJQlV6QVFCZ3NxaGtpRytFMEJEUUVDQVFJQkF6QVFCZ3NxaGtpRytFMEJEUUVDQWdJQgpBekFRQmdzcWhraUcrRTBCRFFFQ0F3SUJBakFRQmdzcWhraUcrRTBCRFFFQ0JBSUJBakFRQmdzcWhraUcrRTBCCkRRRUNCUUlCQkRBUUJnc3Foa2lHK0UwQkRRRUNCZ0lCQVRBUUJnc3Foa2lHK0UwQkRRRUNCd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDQ0FJQkJUQVFCZ3NxaGtpRytFMEJEUUVDQ1FJQkFEQVFCZ3NxaGtpRytFMEJEUUVDQ2dJQgpBREFRQmdzcWhraUcrRTBCRFFFQ0N3SUJBREFRQmdzcWhraUcrRTBCRFFFQ0RBSUJBREFRQmdzcWhraUcrRTBCCkRRRUNEUUlCQURBUUJnc3Foa2lHK0UwQkRRRUNEZ0lCQURBUUJnc3Foa2lHK0UwQkRRRUNEd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDRUFJQkFEQVFCZ3NxaGtpRytFMEJEUUVDRVFJQkRUQWZCZ3NxaGtpRytFMEJEUUVDRWdRUQpBd01DQWdRQkFBVUFBQUFBQUFBQUFEQVFCZ29xaGtpRytFMEJEUUVEQkFJQUFEQVVCZ29xaGtpRytFMEJEUUVFCkJBYVF3RzhBQUFBd0R3WUtLb1pJaHZoTkFRMEJCUW9CQVRBZUJnb3Foa2lHK0UwQkRRRUdCQkRjMmREb09seDgKNG0rT09kRzVjKzBkTUVRR0NpcUdTSWI0VFFFTkFRY3dOakFRQmdzcWhraUcrRTBCRFFFSEFRRUIvekFRQmdzcQpoa2lHK0UwQkRRRUhBZ0VCQURBUUJnc3Foa2lHK0UwQkRRRUhBd0VCL3pBS0JnZ3Foa2pPUFFRREFnTkpBREJHCkFpRUFwemRPNEx4amFKVHlVc3pIQ3B3aXVDL05ISzBmV2JpcGR5TlFuMGZmRWYwQ0lRRHRVeU41ZmJNbjlUUHIKWG93R1Y0Y2lIVHJkUEJOZVN2K3JEeEZNTDdVVktBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2xqQ0NBajJnQXdJQkFnSVZBSlZ2WGMyOUcrSHBRRW5KMVBRenpnRlhDOTVVTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEF4SWpBZwpCZ05WQkFNTUdVbHVkR1ZzSUZOSFdDQlFRMHNnVUd4aGRHWnZjbTBnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzCklFTnZjbkJ2Y21GMGFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXgKQ3pBSkJnTlZCQVlUQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU5TQi83dDIxbFhTTwoyQ3V6cHh3NzRlSkI3MkV5REdnVzVyWEN0eDJ0VlRMcTZoS2s2eitVaVJaQ25xUjdwc092Z3FGZVN4bG1UbEpsCmVUbWkyV1l6M3FPQnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlMKQmdOVkhSOEVTekJKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeQpkbWxqWlhNdWFXNTBaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVsVzlkCnpiMGI0ZWxBU2NuVTlEUE9BVmNMM2xRd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUIKQWY4Q0FRQXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdYc1ZraTB3K2k2VllHVzNVRi8yMnVhWGUwWUpEajFVZQpuQStUakQxYWk1Y0NJQ1liMVNBbUQ1eGtmVFZwdm80VW95aVNZeHJEV0xtVVI0Q0k5Tkt5ZlBOKwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDanpDQ0FqU2dBd0lCQWdJVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3dDZ1lJS29aSXpqMEVBd0l3CmFERWFNQmdHQTFVRUF3d1JTVzUwWld3Z1UwZFlJRkp2YjNRZ1EwRXhHakFZQmdOVkJBb01FVWx1ZEdWc0lFTnYKY25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRUNBd0NRMEV4Q3pBSgpCZ05WQkFZVEFsVlRNQjRYRFRFNE1EVXlNVEV3TkRVeE1Gb1hEVFE1TVRJek1USXpOVGsxT1Zvd2FERWFNQmdHCkExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52Y25CdmNtRjAKYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUpCZ05WQkFZVApBbFZUTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQzZuRXdNRElZWk9qL2lQV3NDemFFS2k3CjFPaU9TTFJGaFdHamJuQlZKZlZua1k0dTNJamtEWVlMME14TzRtcXN5WWpsQmFsVFZZeEZQMnNKQks1emxLT0IKdXpDQnVEQWZCZ05WSFNNRUdEQVdnQlFpWlF6V1dwMDBpZk9EdEpWU3YxQWJPU2NHckRCU0JnTlZIUjhFU3pCSgpNRWVnUmFCRGhrRm9kSFJ3Y3pvdkwyTmxjblJwWm1sallYUmxjeTUwY25WemRHVmtjMlZ5ZG1salpYTXVhVzUwClpXd3VZMjl0TDBsdWRHVnNVMGRZVW05dmRFTkJMbVJsY2pBZEJnTlZIUTRFRmdRVUltVU0xbHFkTkluemc3U1YKVXI5UUd6a25CcXd3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUV3Q2dZSQpLb1pJemowRUF3SURTUUF3UmdJaEFPVy81UWtSK1M5Q2lTRGNOb293THVQUkxzV0dmL1lpN0dTWDk0Qmd3VHdnCkFpRUE0SjBsckhvTXMrWG81by9zWDZPOVFXeEhSQXZaVUdPZFJRN2N2cVJYYXFJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","hcl_report_base64":"SENMQQIAAABaCQAAAgAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAAAAAAAAAAAAwMZGwT_AAUAAAAAAAAAAEB73O4ihAAL1Fq3YJOLBeFadfZagSvRh378KKR_plJRdg0_lzrFanQ73FOJV8ioAk0EF89TAb_qtJkcf-PQqX8vuQr5d0gmOU2-DKrvVG4VHWCx7tELBMBIwOniiJl_Gr7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGo1RO-ajyGFbRpEQvwrhKfdSit715Hy11JkCim5iEjF_wEDAAAAAAAHAQMAAAAAAAAAAAAAAAAASbZvqkUdGeu9vok3G42vK2WqOYTskBEDQ-ni7sEWrwiFD6IOOxqpqHTXemU4DufmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA5xgGAAAAAAASvbwWCe8Gbbep7Hsm11CTRvTKRuhGJ0CWyyWTmx5xGUQGHKGaMMZ0Px5ZkUju5FEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJoEAAABAAAABAAAAAEAAACGBAAAeyJrZXlzIjpbeyJraWQiOiJIQ0xBa1B1YiIsImtleV9vcHMiOlsic2lnbiJdLCJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiJwM3g2SUFBQTY4MFViZ3AzdU5tTF93Nm1FeTJjaUd2bkFaVHFwM2tueDZGQmtieTMtZFQ2RlhXVl9NYmM3RUk4RHNQRTk1WURnT2szVEJjZktkNmxBdUw0QTdoeVBZdzc3c1dJV1dySnlBTE9FUnNmQTJiUC1hNzQyVjQ5cFJrWXRYWFI4c1NuYWNuS2Rfd0hNZ0NpVzZFNXJoUUZXTzRINUEybW51TV9xcnhocHRlQ1BidmxvN2NiTFNWOXBHTEtNbnV0RUJDYzBITndLSnZ6ZWQ5WG15cGpWWUJJTTdSbXdoNmw0bk44VzFkY0ZOby1fTnhobTV6YXU4bXo5ZktoTlFMSXpPaWkzWk95TWE0dnYtUzNJYmZoTVBUMDZqMnllbUVXWUNTbGxqQU9paGxTYllteXQtT0k3ZHJUdTZjV0tpQmNYanJKa1ZWMVFKcUdhOWRzNHcifSx7ImtpZCI6IkhDTEVrUHViIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImt0eSI6IlJTQSIsImUiOiJBUUFCIiwibiI6Im9WakUyQUFBRFVmSUlLajJtNjB0dXp2M1NJbDdBVXE2aElTeExfYmlrYWdIeUpTSzJTaWVidVJka25oSWE5MTZSNk1hdlAtdUU0TjRJaXhCMHliZlZ5TGg1YThzcVBNNHFqMnYxN2o3dXp4QWJtTldkTVJvTjRKZHZKNWpOY0FJMUs4ajF5M0pkWGkzZ0d0aWJWTkVuU3RoQ2ZsYzNPdDg1WXZiUlpfYVc5dV95S3hJQUwxWTFWQVE2WnFTaUxPM1B6Q0hUc09BZml2N3B3QUUtb3R5YjVmcFF1bFBxWFVVOGV6a0hSYmFtNUlyYnZCT18wS0FDdmhMaEFvdVB3a2YtREFhVW5uc01kT1ZQZkI1enZtMGJGZnZKTVBRTWNydUE4dVktZk1vbWZGY0tHWlphVXZ2ekNZa2tqd1N6Zmo2NDVLcTd6bUEtNkR4RnV1b2E4enNzdyJ9XSwidm0tY29uZmlndXJhdGlvbiI6eyJyb290LWNlcnQtdGh1bWJwcmludCI6IiIsImNvbnNvbGUtZW5hYmxlZCI6dHJ1ZSwic2VjdXJlLWJvb3QiOmZhbHNlLCJ0cG0tZW5hYmxlZCI6dHJ1ZSwidHBtLXBlcnNpc3RlZCI6dHJ1ZSwidm1VbmlxdWVJZCI6ImJkZDQzOTA0LWRiZjgtNDMzNy04YjkxLTNhZGJhOTJjNWViZCJ9LCJ1c2VyLWRhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","tpm_attestation":{"ak_certificate_pem":"TODO","quote":{"signature":[110,248,159,138,226,196,44,57,45,211,211,146,41,250,37,131,18,8,202,182,40,140,35,234,141,77,101,188,191,68,38,32,125,144,110,133,99,253,56,113,114,89,116,45,193,205,37,31,193,36,89,241,54,235,160,174,148,222,118,62,254,3,89,82,135,246,19,194,191,53,214,80,17,212,150,57,152,241,198,143,153,85,179,27,57,153,133,0,101,194,63,104,133,175,42,40,205,77,133,203,193,179,162,236,214,232,242,250,216,3,179,103,187,107,71,226,230,157,83,200,77,223,222,138,25,16,241,154,212,182,24,22,175,187,96,204,119,208,198,105,46,12,213,14,13,8,199,106,170,63,6,143,20,125,228,59,116,37,120,140,227,105,65,5,82,11,176,245,53,78,134,194,15,51,74,51,48,248,91,228,154,168,232,35,1,219,128,217,70,204,71,139,94,3,26,132,47,222,129,144,5,128,48,73,163,249,120,46,12,150,152,144,137,210,19,37,144,253,19,41,190,105,131,157,175,43,166,113,68,81,127,227,196,119,251,131,188,112,74,168,190,251,218,156,38,98,249,159,14,253,163,183,159,107,103,122],"message":[255,84,67,71,128,24,0,34,0,11,176,196,237,28,251,197,97,211,73,25,195,246,137,118,108,234,140,194,246,203,163,159,152,132,197,213,135,46,152,40,142,215,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,47,175,22,0,0,0,2,0,0,0,0,1,32,32,3,18,0,18,0,3,0,0,0,1,0,11,3,255,255,255,0,32,73,235,12,37,110,154,84,47,86,198,201,8,64,241,53,217,188,196,121,140,27,5,162,10,40,110,110,222,74,249,168,118],"pcrs":[[86,31,111,47,249,11,244,110,95,1,184,215,193,191,150,12,45,100,182,72,51,37,241,141,120,33,124,132,99,15,211,83],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[189,192,76,193,11,79,205,110,159,121,26,105,91,230,132,199,199,242,175,241,156,196,82,235,102,58,28,53,82,47,75,28],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[196,162,90,109,119,4,98,159,99,219,132,210,14,168,219,14,156,224,2,178,128,27,233,163,64,9,31,231,172,88,134,153],[94,18,234,167,32,238,182,179,211,0,70,66,80,242,205,229,18,48,16,198,193,183,6,131,184,20,41,112,3,12,244,221],[180,90,56,59,226,129,211,3,171,159,183,227,211,194,92,114,209,57,251,5,37,19,45,58,244,93,121,15,213,151,11,190],[18,77,175,71,180,214,113,121,167,125,195,193,188,202,25,138,225,238,29,9,74,42,135,153,116,132,46,68,171,152,187,6],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[159,74,87,117,18,44,164,112,62,19,90,154,230,4,30,222,173,0,100,38,46,57,157,241,28,168,81,130,176,241,84,29],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[171,215,198,149,255,219,96,129,233,150,54,238,1,109,19,34,145,156,104,208,73,182,152,179,153,210,42,226,21,161,33,191],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]},"event_log":[],"instance_info":null}} \ No newline at end of file +{"tdx_quote_base64":"BAACAIEAAAAAAAAAk5pyM_ecTKmUCg2zlX8GB3Thpre77mKg7jpNBav8PEkAAAAABwEDAAAAAAAAAAAAAAAAAEm2b6pFHRnrvb6JNxuNrytlqjmE7JARA0Pp4u7BFq8IhQ-iDjsaqah013plOA7n5gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAADnGAYAAAAAABK9vBYJ7wZtt6nseybXUJNG9MpG6EYnQJbLJZObHnEZRAYcoZowxnQ_HlmRSO7kUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQEAAAdJ_oh6mN-g_v_0efzadA_DOWFK2jYy9p-h269PwkEI4tB-zdOYEa8Uv9hECJmRBRgVQ4z4q2gSz7P56NgkQGmercwydyxD9mf5PwaMAOdpOKJNtwow7lCT3oqVghyxhUPqIP5O_j2a8Dz9-Tpa1cQuZpSiimuRmchsKX3gPO3O8GAEoQAAADAxkbBP8ABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAOcAAAAAAAAA5aOntdgwwpU7mFNMbFmjo0_cNOkz9_WJjwqFzwiEa8oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANyeKnxvlI8XR040p_xD7QMPfBVj8bq932NAyC4OVKjFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHpJi_9CXp9wO3djOvudEoQR5boWPVXYM6F7X8QIwOVUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVQhcPV03wFyimgChXn5NVWfyQTse-BNQdLQotqL9Hf7Ft-i9mrUpeS7sanh8nDPB08_JariTf0DrJuO5Lmf8yIAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwUAYg4AAC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlFOGpDQ0JKZWdBd0lCQWdJVkFQbnErb2llZDBLNlhjREN5aUlXc01ESG51bFJNQW9HQ0NxR1NNNDlCQU1DCk1IQXhJakFnQmdOVkJBTU1HVWx1ZEdWc0lGTkhXQ0JRUTBzZ1VHeGhkR1p2Y20wZ1EwRXhHakFZQmdOVkJBb00KRVVsdWRHVnNJRU52Y25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRQpDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1EY3lPVEUxTXpNeE5Gb1hEVE15TURjeU9URTFNek14Ck5Gb3djREVpTUNBR0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFuClA2bUNzUmhHaFUzVDlrSHpCaG1SUHNKM1d2NkxPSzluUFBUY1pxVis2QjlDSDJJdDZCZ0U3VkFMeUlpMGdIemEKYmhzeE5FM2ZKc045NWhaaUFXcmJvNElERERDQ0F3Z3dId1lEVlIwakJCZ3dGb0FVbFc5ZHpiMGI0ZWxBU2NuVQo5RFBPQVZjTDNsUXdhd1lEVlIwZkJHUXdZakJnb0Y2Z1hJWmFhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk5DOXdZMnRqY213L1kyRTkKY0d4aGRHWnZjbTBtWlc1amIyUnBibWM5WkdWeU1CMEdBMVVkRGdRV0JCUXZrTU1jN25RT1k3OTlXM2tBcTJuSQo5bW1LZVRBT0JnTlZIUThCQWY4RUJBTUNCc0F3REFZRFZSMFRBUUgvQkFJd0FEQ0NBamtHQ1NxR1NJYjRUUUVOCkFRU0NBaW93Z2dJbU1CNEdDaXFHU0liNFRRRU5BUUVFRUtUMVh3NS82WG1WTDdROWRhVS85WXN3Z2dGakJnb3EKaGtpRytFMEJEUUVDTUlJQlV6QVFCZ3NxaGtpRytFMEJEUUVDQVFJQkF6QVFCZ3NxaGtpRytFMEJEUUVDQWdJQgpBekFRQmdzcWhraUcrRTBCRFFFQ0F3SUJBakFRQmdzcWhraUcrRTBCRFFFQ0JBSUJBakFRQmdzcWhraUcrRTBCCkRRRUNCUUlCQkRBUUJnc3Foa2lHK0UwQkRRRUNCZ0lCQVRBUUJnc3Foa2lHK0UwQkRRRUNCd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDQ0FJQkJUQVFCZ3NxaGtpRytFMEJEUUVDQ1FJQkFEQVFCZ3NxaGtpRytFMEJEUUVDQ2dJQgpBREFRQmdzcWhraUcrRTBCRFFFQ0N3SUJBREFRQmdzcWhraUcrRTBCRFFFQ0RBSUJBREFRQmdzcWhraUcrRTBCCkRRRUNEUUlCQURBUUJnc3Foa2lHK0UwQkRRRUNEZ0lCQURBUUJnc3Foa2lHK0UwQkRRRUNEd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDRUFJQkFEQVFCZ3NxaGtpRytFMEJEUUVDRVFJQkRUQWZCZ3NxaGtpRytFMEJEUUVDRWdRUQpBd01DQWdRQkFBVUFBQUFBQUFBQUFEQVFCZ29xaGtpRytFMEJEUUVEQkFJQUFEQVVCZ29xaGtpRytFMEJEUUVFCkJBYVF3RzhBQUFBd0R3WUtLb1pJaHZoTkFRMEJCUW9CQVRBZUJnb3Foa2lHK0UwQkRRRUdCQkRjMmREb09seDgKNG0rT09kRzVjKzBkTUVRR0NpcUdTSWI0VFFFTkFRY3dOakFRQmdzcWhraUcrRTBCRFFFSEFRRUIvekFRQmdzcQpoa2lHK0UwQkRRRUhBZ0VCQURBUUJnc3Foa2lHK0UwQkRRRUhBd0VCL3pBS0JnZ3Foa2pPUFFRREFnTkpBREJHCkFpRUFwemRPNEx4amFKVHlVc3pIQ3B3aXVDL05ISzBmV2JpcGR5TlFuMGZmRWYwQ0lRRHRVeU41ZmJNbjlUUHIKWG93R1Y0Y2lIVHJkUEJOZVN2K3JEeEZNTDdVVktBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2xqQ0NBajJnQXdJQkFnSVZBSlZ2WGMyOUcrSHBRRW5KMVBRenpnRlhDOTVVTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEF4SWpBZwpCZ05WQkFNTUdVbHVkR1ZzSUZOSFdDQlFRMHNnVUd4aGRHWnZjbTBnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzCklFTnZjbkJ2Y21GMGFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXgKQ3pBSkJnTlZCQVlUQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU5TQi83dDIxbFhTTwoyQ3V6cHh3NzRlSkI3MkV5REdnVzVyWEN0eDJ0VlRMcTZoS2s2eitVaVJaQ25xUjdwc092Z3FGZVN4bG1UbEpsCmVUbWkyV1l6M3FPQnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlMKQmdOVkhSOEVTekJKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeQpkbWxqWlhNdWFXNTBaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVsVzlkCnpiMGI0ZWxBU2NuVTlEUE9BVmNMM2xRd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUIKQWY4Q0FRQXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdYc1ZraTB3K2k2VllHVzNVRi8yMnVhWGUwWUpEajFVZQpuQStUakQxYWk1Y0NJQ1liMVNBbUQ1eGtmVFZwdm80VW95aVNZeHJEV0xtVVI0Q0k5Tkt5ZlBOKwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDanpDQ0FqU2dBd0lCQWdJVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3dDZ1lJS29aSXpqMEVBd0l3CmFERWFNQmdHQTFVRUF3d1JTVzUwWld3Z1UwZFlJRkp2YjNRZ1EwRXhHakFZQmdOVkJBb01FVWx1ZEdWc0lFTnYKY25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRUNBd0NRMEV4Q3pBSgpCZ05WQkFZVEFsVlRNQjRYRFRFNE1EVXlNVEV3TkRVeE1Gb1hEVFE1TVRJek1USXpOVGsxT1Zvd2FERWFNQmdHCkExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52Y25CdmNtRjAKYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUpCZ05WQkFZVApBbFZUTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQzZuRXdNRElZWk9qL2lQV3NDemFFS2k3CjFPaU9TTFJGaFdHamJuQlZKZlZua1k0dTNJamtEWVlMME14TzRtcXN5WWpsQmFsVFZZeEZQMnNKQks1emxLT0IKdXpDQnVEQWZCZ05WSFNNRUdEQVdnQlFpWlF6V1dwMDBpZk9EdEpWU3YxQWJPU2NHckRCU0JnTlZIUjhFU3pCSgpNRWVnUmFCRGhrRm9kSFJ3Y3pvdkwyTmxjblJwWm1sallYUmxjeTUwY25WemRHVmtjMlZ5ZG1salpYTXVhVzUwClpXd3VZMjl0TDBsdWRHVnNVMGRZVW05dmRFTkJMbVJsY2pBZEJnTlZIUTRFRmdRVUltVU0xbHFkTkluemc3U1YKVXI5UUd6a25CcXd3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUV3Q2dZSQpLb1pJemowRUF3SURTUUF3UmdJaEFPVy81UWtSK1M5Q2lTRGNOb293THVQUkxzV0dmL1lpN0dTWDk0Qmd3VHdnCkFpRUE0SjBsckhvTXMrWG81by9zWDZPOVFXeEhSQXZaVUdPZFJRN2N2cVJYYXFJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","hcl_report_base64":"SENMQQIAAABaCQAAAgAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAAAAAAAAAAAAwMZGwT_AAUAAAAAAAAAAEB73O4ihAAL1Fq3YJOLBeFadfZagSvRh378KKR_plJRdg0_lzrFanQ73FOJV8ioAk0EF89TAb_qtJkcf-PQqX8vuQr5d0gmOU2-DKrvVG4VHWCx7tELBMBIwOniiJl_Gr7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGo1RO-ajyGFbRpEQvwrhKfdSit715Hy11JkCim5iEjF_wEDAAAAAAAHAQMAAAAAAAAAAAAAAAAASbZvqkUdGeu9vok3G42vK2WqOYTskBEDQ-ni7sEWrwiFD6IOOxqpqHTXemU4DufmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA5xgGAAAAAAASvbwWCe8Gbbep7Hsm11CTRvTKRuhGJ0CWyyWTmx5xGUQGHKGaMMZ0Px5ZkUju5FEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJoEAAABAAAABAAAAAEAAACGBAAAeyJrZXlzIjpbeyJraWQiOiJIQ0xBa1B1YiIsImtleV9vcHMiOlsic2lnbiJdLCJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiJwM3g2SUFBQTY4MFViZ3AzdU5tTF93Nm1FeTJjaUd2bkFaVHFwM2tueDZGQmtieTMtZFQ2RlhXVl9NYmM3RUk4RHNQRTk1WURnT2szVEJjZktkNmxBdUw0QTdoeVBZdzc3c1dJV1dySnlBTE9FUnNmQTJiUC1hNzQyVjQ5cFJrWXRYWFI4c1NuYWNuS2Rfd0hNZ0NpVzZFNXJoUUZXTzRINUEybW51TV9xcnhocHRlQ1BidmxvN2NiTFNWOXBHTEtNbnV0RUJDYzBITndLSnZ6ZWQ5WG15cGpWWUJJTTdSbXdoNmw0bk44VzFkY0ZOby1fTnhobTV6YXU4bXo5ZktoTlFMSXpPaWkzWk95TWE0dnYtUzNJYmZoTVBUMDZqMnllbUVXWUNTbGxqQU9paGxTYllteXQtT0k3ZHJUdTZjV0tpQmNYanJKa1ZWMVFKcUdhOWRzNHcifSx7ImtpZCI6IkhDTEVrUHViIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImt0eSI6IlJTQSIsImUiOiJBUUFCIiwibiI6Im9WakUyQUFBRFVmSUlLajJtNjB0dXp2M1NJbDdBVXE2aElTeExfYmlrYWdIeUpTSzJTaWVidVJka25oSWE5MTZSNk1hdlAtdUU0TjRJaXhCMHliZlZ5TGg1YThzcVBNNHFqMnYxN2o3dXp4QWJtTldkTVJvTjRKZHZKNWpOY0FJMUs4ajF5M0pkWGkzZ0d0aWJWTkVuU3RoQ2ZsYzNPdDg1WXZiUlpfYVc5dV95S3hJQUwxWTFWQVE2WnFTaUxPM1B6Q0hUc09BZml2N3B3QUUtb3R5YjVmcFF1bFBxWFVVOGV6a0hSYmFtNUlyYnZCT18wS0FDdmhMaEFvdVB3a2YtREFhVW5uc01kT1ZQZkI1enZtMGJGZnZKTVBRTWNydUE4dVktZk1vbWZGY0tHWlphVXZ2ekNZa2tqd1N6Zmo2NDVLcTd6bUEtNkR4RnV1b2E4enNzdyJ9XSwidm0tY29uZmlndXJhdGlvbiI6eyJyb290LWNlcnQtdGh1bWJwcmludCI6IiIsImNvbnNvbGUtZW5hYmxlZCI6dHJ1ZSwic2VjdXJlLWJvb3QiOmZhbHNlLCJ0cG0tZW5hYmxlZCI6dHJ1ZSwidHBtLXBlcnNpc3RlZCI6dHJ1ZSwidm1VbmlxdWVJZCI6ImJkZDQzOTA0LWRiZjgtNDMzNy04YjkxLTNhZGJhOTJjNWViZCJ9LCJ1c2VyLWRhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","tpm_attestation":{"ak_certificate_pem":"-----BEGIN CERTIFICATE-----\nMIID7TCCAtWgAwIBAgIQZGTaH3CY2Bt8ETaN4Mt8HzANBgkqhkiG9w0BAQsFADAl\nMSMwIQYDVQQDExpHbG9iYWwgVmlydHVhbCBUUE0gQ0EgLSAwMzAeFw0yNTEyMDEw\nMDAwMDBaFw0yNjExMzAwMDAwMDBaMDgxNjA0BgNVBAMTLTM1M2M2ZDlhNzBiYy5D\nb25maWRlbnRpYWxWTS5BenVyZS53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAKd8eiAAAOvNFG4Kd7jZi/8OphMtnIhr5wGU6qd5J8eh\nQZG8t/nU+hV1lfzG3OxCPA7DxPeWA4DpN0wXHynepQLi+AO4cj2MO+7FiFlqycgC\nzhEbHwNmz/mu+NlePaUZGLV10fLEp2nJynf8BzIAoluhOa4UBVjuB+QNpp7jP6q8\nYabXgj275aO3Gy0lfaRiyjJ7rRAQnNBzcCib83nfV5sqY1WASDO0ZsIepeJzfFtX\nXBTaPvzcYZuc2rvJs/XyoTUCyMzoot2TsjGuL7/ktyG34TD09Oo9snphFmAkpZYw\nDooZUm2JsrfjiO3a07unFiogXF46yZFVdUCahmvXbOMCAwEAAaOCAQQwggEAMA4G\nA1UdDwEB/wQEAwIHgDAYBgNVHSAEETAPMA0GCysGAQQBgjdsgUgCMBwGA1UdJQQV\nMBMGCisGAQQBgjcKAwwGBWeBBQgDMB0GA1UdDgQWBBRqgwVlBrzgn9J7BOoGoSDO\n36kjVDAaBgorBgEEAYI3DQIDBAwWCjYuMi45MjAwLjIwWgYJKwYBBAGCNxUUBE0w\nSwIBBQwPQU1TMjUxMDYxOTA5MDAxDBpXT1JLR1JPVVBcQU1TMjUxMDYxOTA5MDAx\nJAwZVnRwbU1hbmFnZW1lbnRTZXJ2aWNlLmV4ZTAfBgNVHSMEGDAWgBRnCYb4+YFe\nk635yWTnFAramcsKLTANBgkqhkiG9w0BAQsFAAOCAQEAKP34NvDPbHhuNjkDqE4/\nmLyy87rDIhICcgg0eDtDGfCGcJENFGuKngtpQeJjEnY3czvZzCBlVO/iO0W6DbGi\nzqqHRcJsAyPtxUUZi84Gt7xvWtr206oHBFuk4cpUJsPunQQan2bsiO5T4YE7pqhU\nob+i49exg/IgHxLiG+w+/C23vVhLg0GM0zBqgAxuOG1ebLerkb2Fy/pHzzVNuW+g\n6XoUz3efvWC2ZXVV+4bU4TxRKjcJ544uYRTKnF3d3KKRQmuJ5ucvGySW+ofN3+Jo\n6PqvHowc0yAF1b1xzEcMhhtz1Wr5Jlv6yXfJ89WulltMxU+Hkf2SU+pKtz0wpPZ+\nWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAA==\n-----END CERTIFICATE-----\n","quote":{"signature":[135,59,50,25,70,64,229,176,28,41,162,94,237,111,83,172,175,95,193,175,187,237,230,200,179,197,131,184,209,16,107,116,168,110,47,199,140,111,223,4,108,181,42,241,70,190,88,160,195,104,249,161,250,7,175,161,30,75,90,63,237,114,102,198,182,0,204,174,119,194,243,184,124,56,2,18,221,200,103,232,156,29,0,207,160,26,216,154,208,18,153,92,97,188,36,147,220,35,39,201,210,118,252,170,60,43,15,134,84,77,23,188,121,113,19,178,249,159,22,128,167,142,179,240,70,246,106,114,183,14,130,235,3,72,120,240,82,210,250,105,178,161,125,199,173,175,5,49,51,117,146,76,175,34,4,18,91,37,189,173,156,248,44,237,115,254,71,215,220,224,130,30,100,31,189,21,7,156,33,253,30,0,199,59,142,80,47,219,5,124,133,252,239,194,105,246,181,83,110,185,30,103,8,122,181,66,117,58,83,78,60,96,167,100,50,82,127,56,219,94,80,62,65,224,146,24,83,243,116,102,27,224,213,122,94,174,102,124,145,114,125,169,192,201,184,137,187,180,255,85,194,214,201,148,46,59],"message":[255,84,67,71,128,24,0,34,0,11,176,196,237,28,251,197,97,211,73,25,195,246,137,118,108,234,140,194,246,203,163,159,152,132,197,213,135,46,152,40,142,215,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,176,163,127,0,0,0,2,0,0,0,0,1,32,32,3,18,0,18,0,3,0,0,0,1,0,11,3,255,255,255,0,32,73,235,12,37,110,154,84,47,86,198,201,8,64,241,53,217,188,196,121,140,27,5,162,10,40,110,110,222,74,249,168,118],"pcrs":[[86,31,111,47,249,11,244,110,95,1,184,215,193,191,150,12,45,100,182,72,51,37,241,141,120,33,124,132,99,15,211,83],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[189,192,76,193,11,79,205,110,159,121,26,105,91,230,132,199,199,242,175,241,156,196,82,235,102,58,28,53,82,47,75,28],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[196,162,90,109,119,4,98,159,99,219,132,210,14,168,219,14,156,224,2,178,128,27,233,163,64,9,31,231,172,88,134,153],[94,18,234,167,32,238,182,179,211,0,70,66,80,242,205,229,18,48,16,198,193,183,6,131,184,20,41,112,3,12,244,221],[180,90,56,59,226,129,211,3,171,159,183,227,211,194,92,114,209,57,251,5,37,19,45,58,244,93,121,15,213,151,11,190],[18,77,175,71,180,214,113,121,167,125,195,193,188,202,25,138,225,238,29,9,74,42,135,153,116,132,46,68,171,152,187,6],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[159,74,87,117,18,44,164,112,62,19,90,154,230,4,30,222,173,0,100,38,46,57,157,241,28,168,81,130,176,241,84,29],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[171,215,198,149,255,219,96,129,233,150,54,238,1,109,19,34,145,156,104,208,73,182,152,179,153,210,42,226,21,161,33,191],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]},"event_log":[],"instance_info":null}} \ No newline at end of file From 6380c6665940c1ba9a413589dfef9c9511e7dd71 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 2 Dec 2025 09:08:41 +0100 Subject: [PATCH 29/51] Use fixed timestamp in test --- src/attestation/azure.rs | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 1be961b..10bd2ac 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -50,14 +50,25 @@ pub async fn verify_azure_attestation( expected_input_data: [u8; 64], pccs_url: Option, ) -> Result { - let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; - - // Verify TDX quote (same as with DCAP) - TODO deduplicate this code let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() + .expect("Time went backwards") .as_secs(); + verify_azure_attestation_with_given_timestamp(input, expected_input_data, pccs_url, now).await +} + +/// Do the verification, passing in the current time +/// This allows us to test this function without time checks going out of date +async fn verify_azure_attestation_with_given_timestamp( + input: Vec, + expected_input_data: [u8; 64], + pccs_url: Option, + now: u64, +) -> Result { + let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; + + // Verify TDX quote (same as with DCAP) - TODO deduplicate this code let tdx_quote_bytes = BASE64_URL_SAFE .decode(attestation_document.tdx_quote_base64) .unwrap(); @@ -197,11 +208,16 @@ mod tests { #[tokio::test] async fn test_verify() { - // Will pass if now = 1764621240 seconds + let now = 1764621240; let attestation_bytes: &'static [u8] = include_bytes!("../../test-assets/azure-tdx-1764662251380464271"); - verify_azure_attestation(attestation_bytes.to_vec(), [0; 64], None) - .await - .unwrap(); + verify_azure_attestation_with_given_timestamp( + attestation_bytes.to_vec(), + [0; 64], + None, + now, + ) + .await + .unwrap(); } } From 88c54cde7d61f7c0f03dc4d2fdd4091c0e37e71c Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 2 Dec 2025 12:29:13 +0100 Subject: [PATCH 30/51] Attempt to get AK key to match that from tpm quote --- src/attestation/azure.rs | 41 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 10bd2ac..0dd9cd9 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -6,6 +6,7 @@ use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; use openssl::pkey::PKey; use serde::{Deserialize, Serialize}; use thiserror::Error; +use x509_parser::prelude::*; use crate::attestation::{ self, @@ -111,9 +112,21 @@ async fn verify_azure_attestation_with_given_timestamp( vtpm_quote.verify(&pub_key, &expected_input_data[..32])?; let _pcrs = vtpm_quote.pcrs_sha256(); - // TODO parse AK certificate - // Check that AK public key matches that from TPM quote - // Verify AK certificate against microsoft root cert + // Parse AK certificate + let (_type_label, ak_certificate_der) = pem_rfc7468::decode_vec( + attestation_document + .tpm_attestation + .ak_certificate_pem + .as_bytes(), + ) + .unwrap(); + let (_, ak_certificate) = X509Certificate::from_der(&ak_certificate_der).unwrap(); + if !cert_pubkey_matches(&ak_certificate, &pub_key) { + panic!("does not match"); + } + // TODO Check that AK public key matches that from TPM quote + + // TODO Verify AK certificate against microsoft root cert Ok(Measurements { platform: PlatformMeasurements::from_dcap_qvl_quote("e).unwrap(), @@ -152,6 +165,28 @@ fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) } +fn cert_pubkey_matches(cert: &X509Certificate<'_>, key: &PKey) -> bool { + // Extract the SubjectPublicKeyInfo from x509_parser + let spki_der = cert + .tbs_certificate + .subject_pki + .subject_public_key + .data + .clone(); + + // Parse it into an OpenSSL PKey + let cert_pkey = match PKey::public_key_from_der(&spki_der) { + Ok(k) => k, + Err(_) => return false, + }; + + // Compare canonicalized DER encodings of both + match (cert_pkey.public_key_to_der(), key.public_key_to_der()) { + (Ok(a), Ok(b)) => a == b, + _ => panic!("encoding failed"), + } +} + #[derive(Error, Debug)] pub enum MaaError { #[error("Failed to build input data: {0}")] From 7b00adb2b6dbe613eb208ab3074d68e8fb72446e Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 2 Dec 2025 18:23:18 +0100 Subject: [PATCH 31/51] Check AK from HCL report matches that from AK cert --- Cargo.lock | 1 + Cargo.toml | 1 + src/attestation/azure.rs | 124 ++++++++++++++++++++++++++------------- 3 files changed, 85 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b27b741..b05b350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,7 @@ dependencies = [ "hyper", "hyper-util", "josekit", + "num-bigint", "openssl", "parity-scale-codec", "pem-rfc7468", diff --git a/Cargo.toml b/Cargo.toml index 28b8486..c389f9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } parity-scale-codec = "3.7.5" openssl = "0.10.75" tss-esapi = "7.6.0" +num-bigint = "0.4.6" [dev-dependencies] rcgen = "0.14.5" diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 0dd9cd9..29f7c7b 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -16,6 +16,33 @@ use crate::attestation::{ const TPM_AK_CERT_IDX: u32 = 0x1C101D0; +/// The attestation evidence payload that gets sent over the channel +#[derive(Debug, Serialize, Deserialize)] +struct AttestationDocument { + /// TDX quote from the IMDS + tdx_quote_base64: String, + /// Serialized HCL report + hcl_report_base64: String, + /// vTPM related evidence + tpm_attestation: TpmAttest, +} + +/// TPM related components of the attestation document +#[derive(Debug, Serialize, Deserialize)] +struct TpmAttest { + /// Attestation Key certificate from vTPM + ak_certificate_pem: String, + /// vTPM quotes over the selected PCR bank(s). + quote: vtpm::Quote, + /// Raw TCG event log bytes (UEFI + IMA) [currently not used] + /// + /// `/sys/kernel/security/ima/ascii_runtime_measurements`, + /// `/sys/kernel/security/tpm0/binary_bios_measurements`, + event_log: Vec, + /// Optional platform / instance metadata used to bind or verify the AK + instance_info: Option>, +} + pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaError> { let td_report = report::get_report()?; @@ -103,6 +130,19 @@ async fn verify_azure_attestation_with_given_timestamp( let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; let var_data_hash = hcl_report.var_data_sha256(); let hcl_ak_pub = hcl_report.ak_pub()?; + + let runtime_data_raw = hcl_report.var_data(); + + // Check runtime data + let claims: HclRuntimeClaims = serde_json::from_slice(runtime_data_raw)?; + + // TODO check that this matches the AK + let _ak_jwk = claims + .keys + .iter() + .find(|k| k.kid == "HCLAkPub") + .expect("Missing HCLAkPub JWK entry"); + let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?; assert!(var_data_hash == td_report.report_mac.reportdata[..32]); @@ -120,13 +160,15 @@ async fn verify_azure_attestation_with_given_timestamp( .as_bytes(), ) .unwrap(); + let (_, ak_certificate) = X509Certificate::from_der(&ak_certificate_der).unwrap(); + // Check that AK public key matches that from TPM quote if !cert_pubkey_matches(&ak_certificate, &pub_key) { panic!("does not match"); } - // TODO Check that AK public key matches that from TPM quote // TODO Verify AK certificate against microsoft root cert + // TODO Do basic certificate checks (validity, time) Ok(Measurements { platform: PlatformMeasurements::from_dcap_qvl_quote("e).unwrap(), @@ -134,59 +176,59 @@ async fn verify_azure_attestation_with_given_timestamp( }) } -/// The attestation evidence payload that gets sent over the channel -#[derive(Debug, Serialize, Deserialize)] -struct AttestationDocument { - /// TDX quote from the IMDS - tdx_quote_base64: String, - /// Serialized HCL report - hcl_report_base64: String, - /// vTPM related evidence - tpm_attestation: TpmAttest, +#[derive(Debug, Deserialize)] +pub struct Jwk { + #[allow(unused)] + pub kty: String, + pub kid: String, + #[allow(unused)] + pub n: Option, + #[allow(unused)] + pub e: Option, + // other fields ignored } -#[derive(Debug, Serialize, Deserialize)] -struct TpmAttest { - /// Attestation Key certificate from vTPM - ak_certificate_pem: String, - /// vTPM quotes over the selected PCR bank(s). - quote: vtpm::Quote, - /// Raw TCG event log bytes (UEFI + IMA) - /// - /// `/sys/kernel/security/ima/ascii_runtime_measurements`, - /// `/sys/kernel/security/tpm0/binary_bios_measurements`, - event_log: Vec, - /// Optional platform / instance metadata used to bind or verify the AK - instance_info: Option>, +#[derive(Debug, serde::Deserialize)] +struct HclRuntimeClaims { + keys: Vec, + #[allow(unused)] + #[serde(rename = "vm-configuration")] + vm_config: Option, + #[allow(unused)] + #[serde(rename = "user-data")] + user_data: Option, } - fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { let mut context = nv_index::get_session_context()?; nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) } fn cert_pubkey_matches(cert: &X509Certificate<'_>, key: &PKey) -> bool { - // Extract the SubjectPublicKeyInfo from x509_parser - let spki_der = cert - .tbs_certificate - .subject_pki - .subject_public_key - .data - .clone(); - - // Parse it into an OpenSSL PKey - let cert_pkey = match PKey::public_key_from_der(&spki_der) { - Ok(k) => k, + use num_bigint::BigUint; + // 1. Extract RSA public key from cert + let spki = cert.public_key(); + let rsa_from_cert = match spki.parsed() { + Ok(x509_parser::public_key::PublicKey::RSA(rsa)) => rsa, + _ => return false, + }; + + // rsa_from_cert.modulus and exponent are &[u8] big-endian + let n_cert = BigUint::from_bytes_be(rsa_from_cert.modulus); + let e_cert = BigUint::from_bytes_be(rsa_from_cert.exponent); + + // 2. Extract RSA public key from OpenSSL key + let rsa_from_pkey = match key.rsa() { + Ok(r) => r, Err(_) => return false, }; - // Compare canonicalized DER encodings of both - match (cert_pkey.public_key_to_der(), key.public_key_to_der()) { - (Ok(a), Ok(b)) => a == b, - _ => panic!("encoding failed"), - } -} + // OpenSSL’s Rsa struct returns BigNum, convert to bytes + let n_pkey = BigUint::from_bytes_be(&rsa_from_pkey.n().to_vec()); + let e_pkey = BigUint::from_bytes_be(&rsa_from_pkey.e().to_vec()); + // 3. Compare integer values + n_cert == n_pkey && e_cert == e_pkey +} #[derive(Error, Debug)] pub enum MaaError { #[error("Failed to build input data: {0}")] From 680a779dc9c5418f0bb97e6712bad4f8ff7e542f Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 2 Dec 2025 18:45:47 +0100 Subject: [PATCH 32/51] Improve azure verification vTPM checks --- src/attestation/azure.rs | 84 ++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/src/attestation/azure.rs b/src/attestation/azure.rs index 29f7c7b..f84e510 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure.rs @@ -3,6 +3,7 @@ use std::string::FromUtf8Error; use az_tdx_vtpm::{hcl, imds, report, vtpm}; use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; +use num_bigint::BigUint; use openssl::pkey::PKey; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -136,13 +137,14 @@ async fn verify_azure_attestation_with_given_timestamp( // Check runtime data let claims: HclRuntimeClaims = serde_json::from_slice(runtime_data_raw)?; - // TODO check that this matches the AK - let _ak_jwk = claims + let ak_jwk = claims .keys .iter() .find(|k| k.kid == "HCLAkPub") .expect("Missing HCLAkPub JWK entry"); + let ak_from_claims = RsaPubKey::from_jwk(ak_jwk).unwrap(); + let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?; assert!(var_data_hash == td_report.report_mac.reportdata[..32]); @@ -162,10 +164,12 @@ async fn verify_azure_attestation_with_given_timestamp( .unwrap(); let (_, ak_certificate) = X509Certificate::from_der(&ak_certificate_der).unwrap(); - // Check that AK public key matches that from TPM quote - if !cert_pubkey_matches(&ak_certificate, &pub_key) { - panic!("does not match"); - } + + // Check that AK public key matches that from TPM quote and HCL claims + let ak_from_certificate = RsaPubKey::from_certificate(&ak_certificate).unwrap(); + let ak_from_hcl = RsaPubKey::from_openssl_pubkey(&pub_key).unwrap(); + assert!(ak_from_claims == ak_from_hcl); + assert!(ak_from_claims == ak_from_certificate); // TODO Verify AK certificate against microsoft root cert // TODO Do basic certificate checks (validity, time) @@ -203,32 +207,51 @@ fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) } -fn cert_pubkey_matches(cert: &X509Certificate<'_>, key: &PKey) -> bool { - use num_bigint::BigUint; - // 1. Extract RSA public key from cert - let spki = cert.public_key(); - let rsa_from_cert = match spki.parsed() { - Ok(x509_parser::public_key::PublicKey::RSA(rsa)) => rsa, - _ => return false, - }; +#[derive(Debug, PartialEq)] +struct RsaPubKey { + n: BigUint, + e: BigUint, +} - // rsa_from_cert.modulus and exponent are &[u8] big-endian - let n_cert = BigUint::from_bytes_be(rsa_from_cert.modulus); - let e_cert = BigUint::from_bytes_be(rsa_from_cert.exponent); +impl RsaPubKey { + fn from_jwk(jwk: &Jwk) -> anyhow::Result { + if jwk.kty != "RSA" { + anyhow::bail!("JWK is not RSA (kty = {})", jwk.kty); + } - // 2. Extract RSA public key from OpenSSL key - let rsa_from_pkey = match key.rsa() { - Ok(r) => r, - Err(_) => return false, - }; + use base64::engine::general_purpose::URL_SAFE_NO_PAD; + let n_bytes = URL_SAFE_NO_PAD.decode(jwk.n.clone().unwrap())?; + let e_bytes = URL_SAFE_NO_PAD.decode(jwk.e.clone().unwrap())?; + + Ok(Self { + n: BigUint::from_bytes_be(&n_bytes), + e: BigUint::from_bytes_be(&e_bytes), + }) + } - // OpenSSL’s Rsa struct returns BigNum, convert to bytes - let n_pkey = BigUint::from_bytes_be(&rsa_from_pkey.n().to_vec()); - let e_pkey = BigUint::from_bytes_be(&rsa_from_pkey.e().to_vec()); + fn from_certificate(cert: &X509Certificate) -> anyhow::Result { + let spki = cert.public_key(); + let rsa_from_cert = match spki.parsed() { + Ok(x509_parser::public_key::PublicKey::RSA(rsa)) => rsa, + _ => return Err(anyhow::anyhow!("Not rsa")), + }; + + Ok(Self { + n: BigUint::from_bytes_be(rsa_from_cert.modulus), + e: BigUint::from_bytes_be(rsa_from_cert.exponent), + }) + } + + fn from_openssl_pubkey(key: &PKey) -> anyhow::Result { + let rsa_from_pkey = key.rsa()?; - // 3. Compare integer values - n_cert == n_pkey && e_cert == e_pkey + Ok(Self { + n: BigUint::from_bytes_be(&rsa_from_pkey.n().to_vec()), + e: BigUint::from_bytes_be(&rsa_from_pkey.e().to_vec()), + }) + } } + #[derive(Error, Debug)] pub enum MaaError { #[error("Failed to build input data: {0}")] @@ -285,12 +308,15 @@ mod tests { #[tokio::test] async fn test_verify() { - let now = 1764621240; let attestation_bytes: &'static [u8] = include_bytes!("../../test-assets/azure-tdx-1764662251380464271"); + + // To avoid this test stopping working when the certificate is no longer valid + let now = 1764621240; + verify_azure_attestation_with_given_timestamp( attestation_bytes.to_vec(), - [0; 64], + [0; 64], // Input data None, now, ) From 651ec1015a4ace638166b2f217cf1ae7277130c7 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 3 Dec 2025 11:32:56 +0100 Subject: [PATCH 33/51] Verifiy AK cert against microsoft root ca --- Cargo.lock | 4 + Cargo.toml | 4 + src/attestation/azure/ak_certificate.rs | 205 +++++++++++++++++++++ src/attestation/{azure.rs => azure/mod.rs} | 50 ++--- src/attestation/dcap.rs | 2 +- 5 files changed, 243 insertions(+), 22 deletions(-) create mode 100644 src/attestation/azure/ak_certificate.rs rename src/attestation/{azure.rs => azure/mod.rs} (86%) diff --git a/Cargo.lock b/Cargo.lock index b05b350..0b6e816 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,7 @@ name = "attested-tls-proxy" version = "0.1.0" dependencies = [ "anyhow", + "asn1-rs", "axum", "az-tdx-vtpm", "base64 0.22.1", @@ -160,6 +161,7 @@ dependencies = [ "hyper-util", "josekit", "num-bigint", + "once_cell", "openssl", "parity-scale-codec", "pem-rfc7468", @@ -167,11 +169,13 @@ dependencies = [ "rcgen", "reqwest", "rustls-pemfile", + "rustls-webpki 0.103.8", "serde", "serde_json", "sha2", "tdx-quote", "thiserror 2.0.17", + "time", "tokio", "tokio-rustls", "tracing", diff --git a/Cargo.toml b/Cargo.toml index c389f9a..d3d96ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,10 @@ parity-scale-codec = "3.7.5" openssl = "0.10.75" tss-esapi = "7.6.0" num-bigint = "0.4.6" +webpki = { package = "rustls-webpki", version = "0.103.8" } +time = "0.3.44" +asn1-rs = "0.7.1" +once_cell = "1.21.3" [dev-dependencies] rcgen = "0.14.5" diff --git a/src/attestation/azure/ak_certificate.rs b/src/attestation/azure/ak_certificate.rs new file mode 100644 index 0000000..0e50a40 --- /dev/null +++ b/src/attestation/azure/ak_certificate.rs @@ -0,0 +1,205 @@ +//! Generation and verification of AK certificates from the vTPM +use crate::attestation::nv_index; +use std::time::Duration; +use tokio_rustls::rustls::pki_types::{CertificateDer, TrustAnchor, UnixTime}; +use webpki::EndEntityCert; + +/// The NV index where we expect to be able to read the AK certificate from the vTPM +const TPM_AK_CERT_IDX: u32 = 0x1C101D0; + +// microsoftRSADevicesRoot2021 is the root CA certificate used to sign Azure TDX vTPM certificates. +// This is different from the AME root CA used by TrustedLaunch VMs. +// The certificate can be downloaded from: +// http://www.microsoft.com/pkiops/certs/Microsoft%20RSA%20Devices%20Root%20CA%202021.crt +const MICROSOFT_RSA_DEVICES_ROOT_2021: &str = "-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIQGWCAkS2F96VGa+6hm2M3rjANBgkqhkiG9w0BAQwFADBa +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSsw +KQYDVQQDEyJNaWNyb3NvZnQgUlNBIERldmljZXMgUm9vdCBDQSAyMDIxMB4XDTIx +MDgyNjIzMzkxOFoXDTQ2MDgyNjIzNDcxNFowWjELMAkGA1UEBhMCVVMxHjAcBgNV +BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjErMCkGA1UEAxMiTWljcm9zb2Z0IFJT +QSBEZXZpY2VzIFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBALF4kgr3bAptorWmkrM6u47osmLfg67KxZPE4W74Zw5Bu64tjEuzegcB +6lFkoXi2V4eLdIRshk3l14jul6ghCML/6gh4hYiTExky3XMY05wg0d1o+AdhuyvC +anXvQZratosnL+KhR2qFeagthciIrCibIIKX91LvqRl/Eg8uo82fl30gieB40Sun +Pe/SfMJLb7AYbQ95yHK8G1lTFUHkIfPbAY6SfkOBUpNJ6UAtjlAmIaHYpdcdOayf +qXyhW3+Hf0Ou2wiKYJihCqh3TaI2hqmiv4p4CScug9sDcTyafA6OYLyTe3vx7Krn +BOUvkSkTj80GrXSKCWnrw+bE7z0deptPuLS6+n83ImLsBZ3XYhX4iUPmTRSU9vr7 +q0cZA8P8zAzLaeN+uK14l92u/7TMhkp5etmLE9DMd9MtnsLZSy18UpW4ZlBXxt9Z +w/RFKStlNbK5ILsI2HdSjgkF0DxZtNnCiEQehMu5DBfCdXo1P90iJhfF1MD+2Kh5 +xeuDQEC7Dh3gUSXIkOm/72u1fE52r0uY+aH1TCQGbCrijI9Jf78lFbI7L6Ll3YAa +89MrDs2tAQG0SaJdabh4k5orqaJOgaqrrq61RzcMjlZGI3dOdL+f6romKOccFkm0 +k+gwjvZ9xaJ5i9SB6Lq/GrA8YxzjmKHHVPmGGdm/v93R0oNGfyvxAgMBAAGjVDBS +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSERIYG +AJg/LKqzxYnzrC7J5p0JAzAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwF +AAOCAgEAd3RAo42nyNbVvj+mxZ03VV+ceU6nCdgIS8RZfZBxf+lqupRzKUV9UW59 +IRCSeMH3gHfGSVhmwH1AJHkFIhd5meSShF4lPPmvYMmrbfOrwiUunqz2aix/QkRp +geMOe10wm6dEHHAw/eNi3PWhc+jdGJNV0SdnqcwJg/t5db8Y7RCVW+tG3DtEa63U +B4sGNlBbaUffdSdYL5TCRXm2mkcCWruu/gmDTgoabFmI4j9ss0shsIxwqVVEq2zk +EH1ypZrHSmVrTRh9hPHWpkOxnh9yqpGDXcSll09ZZUBUhx7YUX6p+BTVWnuuyR4T +bXS8P6fUS5Q2WF0WR07BrGYlBqomsEwMhth1SmBKn6tXfQyWkgr4pVl5XkkC7Bfv +pmw90csy8ycwog+x4L9kO1Nr6OPwnJ9V39oMifNDxnvYVBX7EhjoiARPp+97feNJ +YwMt4Os/WSeD++IhBB9xVsrI+jZufySQ02C/w1LBFR6zPy+a+v+6WlvMxDBEDWOj +JyDQ6kzkWxIG35klzLnwHybuIsFIIR1QGL1l47eW2dM4hB9oCay6z3FX5xYBIFvA +yp8up+KbjfH/NIWfPBXhYMW64DagB9P2cW5LBRz+AzDA+JF/OdYpb6vxv3lzjLQb +U9zMFwSrzEF5o2Aa/n+xZ90Naj78AYaTM18DalA17037fjucDN8= +-----END CERTIFICATE-----"; + +// azureVirtualTPMRoot2023 is the root CA for Azure vTPM (used by both Trusted Launch and TDX) +// Source: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq +// Valid until: 2048-06-01 +const AZURE_VIRTUAL_TPM_ROOT_2023: &str = "-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQUfQx2iySCIpOKeDZKd5KpzANBgkqhkiG9w0BAQwFADBp +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTow +OAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhv +cml0eSAyMDIzMB4XDTIzMDYwMTE4MDg1M1oXDTQ4MDYwMTE4MTU0MVowaTELMAkG +A1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE6MDgGA1UE +AxMxQXp1cmUgVmlydHVhbCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkg +MjAyMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALoMMwvdRJ7+bW00 +adKE1VemNqJS+268Ure8QcfZXVOsVO22+PL9WRoPnWo0r5dVoomYGbobh4HC72s9 +sGY6BGRe+Ui2LMwuWnirBtOjaJ34r1ZieNMcVNJT/dXW5HN/HLlm/gSKlWzqCEx6 +gFFAQTvyYl/5jYI4Oe05zJ7ojgjK/6ZHXpFysXnyUITJ9qgjn546IJh/G5OMC3mD +fFU7A/GAi+LYaOHSzXj69Lk1vCftNq9DcQHtB7otO0VxFkRLaULcfu/AYHM7FC/S +q6cJb9Au8K/IUhw/5lJSXZawLJwHpcEYzETm2blad0VHsACaLNucZL5wBi8GEusQ +9Wo8W1p1rUCMp89pufxa3Ar9sYZvWeJlvKggWcQVUlhvvIZEnT+fteEvwTdoajl5 +qSvZbDPGCPjb91rSznoiLq8XqgQBBFjnEiTL+ViaZmyZPYUsBvBY3lKXB1l2hgga +hfBIag4j0wcgqlL82SL7pAdGjq0Fou6SKgHnkkrV5CNxUBBVMNCwUoj5mvEjd5mF +7XPgfM98qNABb2Aqtfl+VuCkU/G1XvFoTqS9AkwbLTGFMS9+jCEU2rw6wnKuGv1T +x9iuSdNvsXt8stx4fkVeJvnFpJeAIwBZVgKRSTa3w3099k0mW8qGiMnwCI5SfdZ2 +SJyD4uEmszsnieE6wAWd1tLLg1jvAgMBAAGjVDBSMA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRL/iZalMH2M8ODSCbd8+WwZLKqlTAQ +BgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEALgNAyg8I0ANNO/8I +2BhpTOsbywN2YSmShAmig5h4sCtaJSM1dRXwA+keY6PCXQEt/PRAQAiHNcOF5zbu +OU1Bw/Z5Z7k9okt04eu8CsS2Bpc+POg9js6lBtmigM5LWJCH1goMD0kJYpzkaCzx +1TdD3yjo0xSxgGhabk5Iu1soD3OxhUyIFcxaluhwkiVINt3Jhy7G7VJTlEwkk21A +oOrQxUsJH0f2GXjYShS1r9qLPzLf7ykcOm62jHGmLZVZujBzLIdNk1bljP9VuGW+ +cISBwzkNeEMMFufcL2xh6s/oiUnXicFWvG7E6ioPnayYXrHy3Rh68XLnhfpzeCzv +bz/I4yMV38qGo/cAY2OJpXUuuD/ZbI5rT+lRBEkDW1kxHP8cpwkRwGopV8+gX2KS +UucIIN4l8/rrNDEX8T0b5U+BUqiO7Z5YnxCya/H0ZIwmQnTlLRTU2fW+OGG+xyIr +jMi/0l6/yWPUkIAkNtvS/yO7USRVLPbtGVk3Qre6HcqacCXzEjINcJhGEVg83Y8n +M+Y+a9J0lUnHytMSFZE85h88OseRS2QwqjozUo2j1DowmhSSUv9Na5Ae22ycciBk +EZSq8a4rSlwqthaELNpeoTLUk6iVoUkK/iLvaMvrkdj9yJY1O/gvlfN2aiNTST/2 +bd+PA4RBToG9rXn6vNkUWdbLibU= +-----END CERTIFICATE-----"; + +// globalVirtualTPMCA03 is the intermediate CA that issues TDX vTPM AK certificates +// Source: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq +// Issuer: Azure Virtual TPM Root Certificate Authority 2023 +// Valid: 2025-04-24 to 2027-04-24 +const GLOBAL_VIRTUAL_TPMCA03: &str = "-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgITMwAAAAknQOWscnsOpgAAAAAACTANBgkqhkiG9w0BAQwF +ADBpMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTowOAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAyMDIzMB4XDTI1MDQyNDE4MDExN1oXDTI3MDQyNDE4MDExN1owJTEj +MCEGA1UEAxMaR2xvYmFsIFZpcnR1YWwgVFBNIENBIC0gMDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDYGYtis5ka0cxQkhU11jslgX6wzjR/UXQIFdUn +8juTUMJl91VokwUPX3WfXeog7mtbWyYWD8SI0BSnchRGlV8u3AhcW61/HetHqmIL +tD0c75UATi+gsTQnpwKPA/m38MGGyXFETr3xHXjilUPfIhmxO4ImuNJ0R95bZYhx +bLYmOZpVUcj8oz980An8HlIqSzrskQR6NiuEmikHkHc1/CpoNunrr8kQNPF6gxex +IrvXsKLUAuUqnNtcQWc/8Er5EN9+TdX6AOjUmKriVGbCInP1m/aC+DWH/+aJ/8aD +pKze6fe7OHh2BL9hxqIsmJAStIh4siRdLYTt8hKGmkdzOWnRAgMBAAGjggF/MIIB +ezASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwICBDAXBgNVHSUEEDAO +BgVngQUIAQYFZ4EFCAMwHQYDVR0OBBYEFGcJhvj5gV6TrfnJZOcUCtqZywotMB8G +A1UdIwQYMBaAFEv+JlqUwfYzw4NIJt3z5bBksqqVMHYGA1UdHwRvMG0wa6BpoGeG +ZWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL0F6dXJlJTIwVmly +dHVhbCUyMFRQTSUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIw +MjMuY3JsMIGDBggrBgEFBQcBAQR3MHUwcwYIKwYBBQUHMAKGZ2h0dHA6Ly93d3cu +bWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvQXp1cmUlMjBWaXJ0dWFsJTIwVFBN +JTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMy5jcnQwDQYJ +KoZIhvcNAQEMBQADggIBAJPP3Z2z1zhzUS3qSRVgyoUVnaxCGuMHzPQAZuoPBVpz +wKnv4HqyjMgT8pBtQqxkqAsg7KiqbPfO97bMCHcuqkkfHjw8yg6IYt01RjUjVPKq +lrsY2iw7hFWNWr8SGMa10JdNYNyf5dxob5+mKAwEOhLzKNwq9rM/uIvZky77pNly +RLt55XEPfBMYdI9I8uQ5Uqmrw7mVJfERMfTBhSQF9BrcajAsaLcs7qEUyj0yUdJf +cgZkfCoUEUSPr3OwLHaYeV1J6VidhIYsYo53sXXal91d60NspYgei2nJFei/+R3E +SWnGbPBW+EQ4FbvZXxu57zUMX9mM7lC+GoXLvA6/vtKShEi9ZXl2PSnBQ/R2A7b3 +AXyg4fmMLFausEk6OiuU8E/bvp+gPLOJ8YrX7SAJVuEn+koJaK5G7os5DMIh7/KM +l9cI9WxPwqoWjp4VBfrF4hDOCmKWrqtFUDQCML8qD8RTxlQKQtgeGAcNDfoAuL9K +VtSG5/iIhuyBEFYEHa3vRWbSaHCUzaHJsTmLcz4cp1VDdepzqZRVuErBzJKFnBXb +zRNW32EFmcAUKZImIsE5dgB7y7eiijf33VWNfWmK05fxzQziWFWRYlET4SVc3jMn +PBiY3N8BfK8EBOYbLvzo0qn2n3SAmPhYX3Ag6vbbIHd4Qc8DQKHRV0PB8D3jPGmD +-----END CERTIFICATE-----"; + +/// Verify an AK certificate against azure root CA +pub fn verify_ak_cert_with_azure_roots(ak_cert_der: &[u8], now_secs: u64) -> Result<(), String> { + let ak_cert_der: CertificateDer = ak_cert_der.into(); + let end_entity_cert = EndEntityCert::try_from(&ak_cert_der) + .map_err(|e| format!("Failed to parse AK cert as EndEntityCert: {e:?}"))?; + + let roots = azure_root_anchors(); + let intermediates = azure_intermediate_chain(); + + let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs)); + + end_entity_cert + .verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &roots, + &intermediates, + now, + AnyEku, + None, + None, + ) + .map_err(|e| format!("AK cert chain verification failed: {e:?}"))?; + + Ok(()) +} + +/// Retrieve an AK certificate from the vTPM +pub fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { + let mut context = nv_index::get_session_context()?; + nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) +} + +/// Convert a PEM-encoded cert into a TrustAnchor +fn pem_to_trust_anchor(pem: &str) -> TrustAnchor<'static> { + let (_type_label, der_vec) = pem_rfc7468::decode_vec(pem.as_bytes()).unwrap(); + // Leaking is ok here because plan is to set this up so it is only called once + let leaked: &'static [u8] = Box::leak(der_vec.into_boxed_slice()); + let cert_der: &'static CertificateDer<'static> = + Box::leak(Box::new(CertificateDer::from(leaked))); + webpki::anchor_from_trusted_cert(cert_der).expect("Failed to create trust anchor") +} + +/// Returns the root anchors for azure +fn azure_root_anchors() -> Vec> { + vec![ + // Microsoft RSA Devices Root CA 2021 (older VMs) + pem_to_trust_anchor(MICROSOFT_RSA_DEVICES_ROOT_2021), + // Azure Virtual TPM Root CA 2023 (TDX + newer trusted launch) + pem_to_trust_anchor(AZURE_VIRTUAL_TPM_ROOT_2023), + ] +} + +/// Returns the intermediate chain for azure +fn azure_intermediate_chain() -> Vec> { + let (_type_label, cert_der) = + pem_rfc7468::decode_vec(GLOBAL_VIRTUAL_TPMCA03.as_bytes()).unwrap(); + vec![CertificateDer::from(cert_der)] +} + +/// Allows any EKU - we could change this to only accept 1.3.6.1.4.1.567.10.3.12 which is the EKU +/// given in the AK certificate +struct AnyEku; + +impl webpki::ExtendedKeyUsageValidator for AnyEku { + fn validate(&self, _iter: webpki::KeyPurposeIdIter<'_, '_>) -> Result<(), webpki::Error> { + Ok(()) + } +} + +#[cfg(test)] +#[tokio::test] +async fn root_should_be_fresh() { + let response = reqwest::get( + "http://www.microsoft.com/pkiops/certs/Microsoft%20RSA%20Devices%20Root%20CA%202021.crt", + ) + .await + .unwrap(); + let ca_der = response.bytes().await.unwrap(); + assert_eq!( + pem_rfc7468::decode_vec(MICROSOFT_RSA_DEVICES_ROOT_2021.as_bytes()) + .unwrap() + .1, + ca_der + ); +} diff --git a/src/attestation/azure.rs b/src/attestation/azure/mod.rs similarity index 86% rename from src/attestation/azure.rs rename to src/attestation/azure/mod.rs index f84e510..4086b2e 100644 --- a/src/attestation/azure.rs +++ b/src/attestation/azure/mod.rs @@ -1,4 +1,6 @@ //! Microsoft Azure Attestation (MAA) evidence generation and verification +mod ak_certificate; +use ak_certificate::{read_ak_certificate_from_tpm, verify_ak_cert_with_azure_roots}; use std::string::FromUtf8Error; use az_tdx_vtpm::{hcl, imds, report, vtpm}; @@ -11,12 +13,10 @@ use x509_parser::prelude::*; use crate::attestation::{ self, + dcap::get_quote_input_data, measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, - nv_index, }; -const TPM_AK_CERT_IDX: u32 = 0x1C101D0; - /// The attestation evidence payload that gets sent over the channel #[derive(Debug, Serialize, Deserialize)] struct AttestationDocument { @@ -40,7 +40,7 @@ struct TpmAttest { /// `/sys/kernel/security/ima/ascii_runtime_measurements`, /// `/sys/kernel/security/tpm0/binary_bios_measurements`, event_log: Vec, - /// Optional platform / instance metadata used to bind or verify the AK + /// Optional platform / instance metadata used to bind or verify the AK [currently not used] instance_info: Option>, } @@ -119,22 +119,25 @@ async fn verify_azure_attestation_with_given_timestamp( let _verified_report = dcap_qvl::verify::verify(&tdx_quote_bytes, &collateral, now).unwrap(); - // Check that hcl_report_bytes (hashed?) matches TDX quote report data - // if get_quote_input_data(quote.report) != quote_input { - // return Err(AttestationError::InputMismatch); - // } - let hcl_report_bytes = BASE64_URL_SAFE .decode(attestation_document.hcl_report_base64) .unwrap(); let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; let var_data_hash = hcl_report.var_data_sha256(); - let hcl_ak_pub = hcl_report.ak_pub()?; + println!("var data {}", hex::encode(var_data_hash)); - let runtime_data_raw = hcl_report.var_data(); + // Check that HCL var data hash matches TDX quote report data + let mut expected_tdx_input_data = [0u8; 64]; + expected_tdx_input_data[..32].copy_from_slice(&var_data_hash); + if get_quote_input_data(quote.report.clone()) != expected_tdx_input_data { + return Err(MaaError::TdxQuoteInputMismatch); + } + + let hcl_ak_pub = hcl_report.ak_pub()?; // Check runtime data + let runtime_data_raw = hcl_report.var_data(); let claims: HclRuntimeClaims = serde_json::from_slice(runtime_data_raw)?; let ak_jwk = claims @@ -163,7 +166,10 @@ async fn verify_azure_attestation_with_given_timestamp( ) .unwrap(); - let (_, ak_certificate) = X509Certificate::from_der(&ak_certificate_der).unwrap(); + let (remaining_bytes, ak_certificate) = X509Certificate::from_der(&ak_certificate_der).unwrap(); + + let leaf_len = ak_certificate_der.len() - remaining_bytes.len(); + let ak_certificate_der_without_trailing_data = &ak_certificate_der[..leaf_len]; // Check that AK public key matches that from TPM quote and HCL claims let ak_from_certificate = RsaPubKey::from_certificate(&ak_certificate).unwrap(); @@ -171,8 +177,8 @@ async fn verify_azure_attestation_with_given_timestamp( assert!(ak_from_claims == ak_from_hcl); assert!(ak_from_claims == ak_from_certificate); - // TODO Verify AK certificate against microsoft root cert - // TODO Do basic certificate checks (validity, time) + // Verify the AK certificate against microsoft root cert + verify_ak_cert_with_azure_roots(ak_certificate_der_without_trailing_data, now).unwrap(); Ok(Measurements { platform: PlatformMeasurements::from_dcap_qvl_quote("e).unwrap(), @@ -192,6 +198,7 @@ pub struct Jwk { // other fields ignored } +/// The internal data structure for HCL runtime claims #[derive(Debug, serde::Deserialize)] struct HclRuntimeClaims { keys: Vec, @@ -202,11 +209,8 @@ struct HclRuntimeClaims { #[serde(rename = "user-data")] user_data: Option, } -fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { - let mut context = nv_index::get_session_context()?; - nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) -} +/// This is only used as a common type to compare public keys with different formats #[derive(Debug, PartialEq)] struct RsaPubKey { n: BigUint, @@ -282,6 +286,8 @@ pub enum MaaError { TssEsapi(#[from] tss_esapi::Error), #[error("PEM encode: {0}")] Pem(#[from] pem_rfc7468::Error), + #[error("TDX quote input does not match hashed HCL var data")] + TdxQuoteInputMismatch, } #[cfg(test)] @@ -291,7 +297,7 @@ mod tests { #[tokio::test] async fn test_decode_hcl() { // from cvm-reverse-proxy/internal/attestation/azure/tdx/testdata/hclreport.bin - let hcl_bytes: &'static [u8] = include_bytes!("../../test-assets/hclreport.bin"); + let hcl_bytes: &'static [u8] = include_bytes!("../../../test-assets/hclreport.bin"); let hcl_report = hcl::HclReport::new(hcl_bytes.to_vec()).unwrap(); let hcl_var_data = hcl_report.var_data(); @@ -306,12 +312,14 @@ mod tests { ); } + /// Verify a stored attestation from a test-deployment on azure #[tokio::test] async fn test_verify() { let attestation_bytes: &'static [u8] = - include_bytes!("../../test-assets/azure-tdx-1764662251380464271"); + include_bytes!("../../../test-assets/azure-tdx-1764662251380464271"); - // To avoid this test stopping working when the certificate is no longer valid + // To avoid this test stopping working when the certificate is no longer valid we pass in a + // timestamp let now = 1764621240; verify_azure_attestation_with_given_timestamp( diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index 6ed406e..6ddb39a 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -90,7 +90,7 @@ fn generate_quote(input: [u8; 64]) -> Result, QuoteGenerationError> { } /// Given a [Report] get the input data regardless of report type -fn get_quote_input_data(report: Report) -> [u8; 64] { +pub fn get_quote_input_data(report: Report) -> [u8; 64] { match report { Report::TD10(r) => r.report_data, Report::TD15(r) => r.base.report_data, From c131fc7ecf0f6933e7759de244d1b76cc3f13f89 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 3 Dec 2025 12:01:26 +0100 Subject: [PATCH 34/51] Error handling --- src/attestation/azure/ak_certificate.rs | 27 +++--- src/attestation/azure/mod.rs | 118 ++++++++++++++++-------- 2 files changed, 94 insertions(+), 51 deletions(-) diff --git a/src/attestation/azure/ak_certificate.rs b/src/attestation/azure/ak_certificate.rs index 0e50a40..15ff378 100644 --- a/src/attestation/azure/ak_certificate.rs +++ b/src/attestation/azure/ak_certificate.rs @@ -1,5 +1,5 @@ //! Generation and verification of AK certificates from the vTPM -use crate::attestation::nv_index; +use crate::attestation::{azure::MaaError, nv_index}; use std::time::Duration; use tokio_rustls::rustls::pki_types::{CertificateDer, TrustAnchor, UnixTime}; use webpki::EndEntityCert; @@ -119,27 +119,24 @@ PBiY3N8BfK8EBOYbLvzo0qn2n3SAmPhYX3Ag6vbbIHd4Qc8DQKHRV0PB8D3jPGmD -----END CERTIFICATE-----"; /// Verify an AK certificate against azure root CA -pub fn verify_ak_cert_with_azure_roots(ak_cert_der: &[u8], now_secs: u64) -> Result<(), String> { +pub fn verify_ak_cert_with_azure_roots(ak_cert_der: &[u8], now_secs: u64) -> Result<(), MaaError> { let ak_cert_der: CertificateDer = ak_cert_der.into(); - let end_entity_cert = EndEntityCert::try_from(&ak_cert_der) - .map_err(|e| format!("Failed to parse AK cert as EndEntityCert: {e:?}"))?; + let end_entity_cert = EndEntityCert::try_from(&ak_cert_der)?; let roots = azure_root_anchors(); let intermediates = azure_intermediate_chain(); let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs)); - end_entity_cert - .verify_for_usage( - webpki::ALL_VERIFICATION_ALGS, - &roots, - &intermediates, - now, - AnyEku, - None, - None, - ) - .map_err(|e| format!("AK cert chain verification failed: {e:?}"))?; + end_entity_cert.verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &roots, + &intermediates, + now, + AnyEku, + None, + None, + )?; Ok(()) } diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs index 4086b2e..6601725 100644 --- a/src/attestation/azure/mod.rs +++ b/src/attestation/azure/mod.rs @@ -6,7 +6,7 @@ use std::string::FromUtf8Error; use az_tdx_vtpm::{hcl, imds, report, vtpm}; use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; use num_bigint::BigUint; -use openssl::pkey::PKey; +use openssl::{error::ErrorStack, pkey::PKey}; use serde::{Deserialize, Serialize}; use thiserror::Error; use x509_parser::prelude::*; @@ -98,14 +98,12 @@ async fn verify_azure_attestation_with_given_timestamp( let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; // Verify TDX quote (same as with DCAP) - TODO deduplicate this code - let tdx_quote_bytes = BASE64_URL_SAFE - .decode(attestation_document.tdx_quote_base64) - .unwrap(); + let tdx_quote_bytes = BASE64_URL_SAFE.decode(attestation_document.tdx_quote_base64)?; - let quote = dcap_qvl::quote::Quote::parse(&tdx_quote_bytes).unwrap(); + let quote = dcap_qvl::quote::Quote::parse(&tdx_quote_bytes)?; - let ca = quote.ca().unwrap(); - let fmspc = hex::encode_upper(quote.fmspc().unwrap()); + let ca = quote.ca()?; + let fmspc = hex::encode_upper(quote.fmspc()?); let collateral = dcap_qvl::collateral::get_collateral_for_fmspc( &pccs_url .clone() @@ -114,18 +112,14 @@ async fn verify_azure_attestation_with_given_timestamp( ca, false, // Indicates not SGX ) - .await - .unwrap(); + .await?; - let _verified_report = dcap_qvl::verify::verify(&tdx_quote_bytes, &collateral, now).unwrap(); + let _verified_report = dcap_qvl::verify::verify(&tdx_quote_bytes, &collateral, now)?; - let hcl_report_bytes = BASE64_URL_SAFE - .decode(attestation_document.hcl_report_base64) - .unwrap(); + let hcl_report_bytes = BASE64_URL_SAFE.decode(attestation_document.hcl_report_base64)?; let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; let var_data_hash = hcl_report.var_data_sha256(); - println!("var data {}", hex::encode(var_data_hash)); // Check that HCL var data hash matches TDX quote report data let mut expected_tdx_input_data = [0u8; 64]; @@ -144,16 +138,21 @@ async fn verify_azure_attestation_with_given_timestamp( .keys .iter() .find(|k| k.kid == "HCLAkPub") - .expect("Missing HCLAkPub JWK entry"); + .ok_or(MaaError::ClaimsMissingHCLAkPub)?; - let ak_from_claims = RsaPubKey::from_jwk(ak_jwk).unwrap(); + let ak_from_claims = RsaPubKey::from_jwk(ak_jwk)?; let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?; - assert!(var_data_hash == td_report.report_mac.reportdata[..32]); + if var_data_hash != td_report.report_mac.reportdata[..32] { + return Err(MaaError::TdReportInputMismatch); + } let vtpm_quote = attestation_document.tpm_attestation.quote; - let hcl_ak_pub_der = hcl_ak_pub.key.try_to_der().unwrap(); - let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der).unwrap(); + let hcl_ak_pub_der = hcl_ak_pub + .key + .try_to_der() + .map_err(|_| MaaError::JwkConversion)?; + let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der)?; vtpm_quote.verify(&pub_key, &expected_input_data[..32])?; let _pcrs = vtpm_quote.pcrs_sha256(); @@ -163,26 +162,31 @@ async fn verify_azure_attestation_with_given_timestamp( .tpm_attestation .ak_certificate_pem .as_bytes(), - ) - .unwrap(); + )?; - let (remaining_bytes, ak_certificate) = X509Certificate::from_der(&ak_certificate_der).unwrap(); + let (remaining_bytes, ak_certificate) = X509Certificate::from_der(&ak_certificate_der)?; let leaf_len = ak_certificate_der.len() - remaining_bytes.len(); let ak_certificate_der_without_trailing_data = &ak_certificate_der[..leaf_len]; // Check that AK public key matches that from TPM quote and HCL claims - let ak_from_certificate = RsaPubKey::from_certificate(&ak_certificate).unwrap(); - let ak_from_hcl = RsaPubKey::from_openssl_pubkey(&pub_key).unwrap(); - assert!(ak_from_claims == ak_from_hcl); - assert!(ak_from_claims == ak_from_certificate); + let ak_from_certificate = RsaPubKey::from_certificate(&ak_certificate)?; + let ak_from_hcl = RsaPubKey::from_openssl_pubkey(&pub_key)?; + if ak_from_claims != ak_from_hcl { + return Err(MaaError::AkFromClaimsNotEqualAkFromHcl); + } + if ak_from_claims != ak_from_certificate { + return Err(MaaError::AkFromClaimsNotEqualAkFromCertificate); + } // Verify the AK certificate against microsoft root cert - verify_ak_cert_with_azure_roots(ak_certificate_der_without_trailing_data, now).unwrap(); + verify_ak_cert_with_azure_roots(ak_certificate_der_without_trailing_data, now)?; Ok(Measurements { - platform: PlatformMeasurements::from_dcap_qvl_quote("e).unwrap(), - cvm_image: CvmImageMeasurements::from_dcap_qvl_quote("e).unwrap(), + platform: PlatformMeasurements::from_dcap_qvl_quote("e) + .map_err(|_| MaaError::CannotExtractMeasurementsFromQuote)?, + cvm_image: CvmImageMeasurements::from_dcap_qvl_quote("e) + .map_err(|_| MaaError::CannotExtractMeasurementsFromQuote)?, }) } @@ -218,14 +222,14 @@ struct RsaPubKey { } impl RsaPubKey { - fn from_jwk(jwk: &Jwk) -> anyhow::Result { + fn from_jwk(jwk: &Jwk) -> Result { if jwk.kty != "RSA" { - anyhow::bail!("JWK is not RSA (kty = {})", jwk.kty); + return Err(MaaError::NotRsa); } use base64::engine::general_purpose::URL_SAFE_NO_PAD; - let n_bytes = URL_SAFE_NO_PAD.decode(jwk.n.clone().unwrap())?; - let e_bytes = URL_SAFE_NO_PAD.decode(jwk.e.clone().unwrap())?; + let n_bytes = URL_SAFE_NO_PAD.decode(jwk.n.clone().ok_or(MaaError::JwkParse)?)?; + let e_bytes = URL_SAFE_NO_PAD.decode(jwk.e.clone().ok_or(MaaError::JwkParse)?)?; Ok(Self { n: BigUint::from_bytes_be(&n_bytes), @@ -233,11 +237,11 @@ impl RsaPubKey { }) } - fn from_certificate(cert: &X509Certificate) -> anyhow::Result { + fn from_certificate(cert: &X509Certificate) -> Result { let spki = cert.public_key(); let rsa_from_cert = match spki.parsed() { Ok(x509_parser::public_key::PublicKey::RSA(rsa)) => rsa, - _ => return Err(anyhow::anyhow!("Not rsa")), + _ => return Err(MaaError::NotRsa), }; Ok(Self { @@ -246,7 +250,7 @@ impl RsaPubKey { }) } - fn from_openssl_pubkey(key: &PKey) -> anyhow::Result { + fn from_openssl_pubkey(key: &PKey) -> Result { let rsa_from_pkey = key.rsa()?; Ok(Self { @@ -288,6 +292,48 @@ pub enum MaaError { Pem(#[from] pem_rfc7468::Error), #[error("TDX quote input does not match hashed HCL var data")] TdxQuoteInputMismatch, + #[error("TD report input does not match hashed HCL var data")] + TdReportInputMismatch, + #[error("Base64 decode: {0}")] + Base64(#[from] base64::DecodeError), + #[error("Attestation Key from HCL runtime claims does not match that from HCL report")] + AkFromClaimsNotEqualAkFromHcl, + #[error("Attestation Key from HCL runtime claims does not match that from attestation key certificate")] + AkFromClaimsNotEqualAkFromCertificate, + #[error("WebPKI: {0}")] + WebPki(#[from] webpki::Error), + #[error("Certificate chain is empty")] + NoCertificate, + #[error("X509 parse: {0}")] + X509Parse(#[from] x509_parser::asn1_rs::Err), + #[error("X509: {0}")] + X509(#[from] x509_parser::error::X509Error), + #[error("Quote input is not as expected")] + InputMismatch, + #[error("Configuration mismatch - expected no remote attestation")] + AttestationGivenWhenNoneExpected, + #[error("Configfs-tsm quote generation: {0}")] + QuoteGeneration(#[from] configfs_tsm::QuoteGenerationError), + #[error("SGX quote given when TDX quote expected")] + SgxNotSupported, + #[error("Platform measurements do not match any accepted values")] + UnacceptablePlatformMeasurements, + #[error("OS image measurements do not match any accepted values")] + UnacceptableOsImageMeasurements, + #[error("DCAP quote verification: {0}")] + DcapQvl(#[from] anyhow::Error), + #[error("Cannot convert JSON web key to der")] + JwkConversion, + #[error("OpenSSL: {0}")] + OpenSSL(#[from] ErrorStack), + #[error("Cannot extract measurements from quote")] + CannotExtractMeasurementsFromQuote, + #[error("Expected AK key to be RSA")] + NotRsa, + #[error("JSON web key has missing field")] + JwkParse, + #[error("HCL runtime claims is missing HCLAkPub field")] + ClaimsMissingHCLAkPub, } #[cfg(test)] From f0b085d13a10ce870b32a1f7bec3edd04bd5156e Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 3 Dec 2025 12:09:59 +0100 Subject: [PATCH 35/51] Comments --- src/attestation/azure/mod.rs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs index 6601725..beab806 100644 --- a/src/attestation/azure/mod.rs +++ b/src/attestation/azure/mod.rs @@ -44,6 +44,7 @@ struct TpmAttest { instance_info: Option>, } +/// Generate a TDX attestation on Azure pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaError> { let td_report = report::get_report()?; @@ -74,6 +75,7 @@ pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, M Ok(serde_json::to_vec(&attestation_document)?) } +/// Verify a TDX attestation from Azure pub async fn verify_azure_attestation( input: Vec, expected_input_data: [u8; 64], @@ -130,23 +132,27 @@ async fn verify_azure_attestation_with_given_timestamp( let hcl_ak_pub = hcl_report.ak_pub()?; - // Check runtime data - let runtime_data_raw = hcl_report.var_data(); - let claims: HclRuntimeClaims = serde_json::from_slice(runtime_data_raw)?; + // Get attestation key from runtime claims + let ak_from_claims = { + let runtime_data_raw = hcl_report.var_data(); + let claims: HclRuntimeClaims = serde_json::from_slice(runtime_data_raw)?; - let ak_jwk = claims - .keys - .iter() - .find(|k| k.kid == "HCLAkPub") - .ok_or(MaaError::ClaimsMissingHCLAkPub)?; + let ak_jwk = claims + .keys + .iter() + .find(|k| k.kid == "HCLAkPub") + .ok_or(MaaError::ClaimsMissingHCLAkPub)?; - let ak_from_claims = RsaPubKey::from_jwk(ak_jwk)?; + RsaPubKey::from_jwk(ak_jwk)? + }; + // Check that the TD report input data matches the HCL var data hash let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?; if var_data_hash != td_report.report_mac.reportdata[..32] { return Err(MaaError::TdReportInputMismatch); } + // Verify the vTPM quote let vtpm_quote = attestation_document.tpm_attestation.quote; let hcl_ak_pub_der = hcl_ak_pub .key @@ -154,6 +160,7 @@ async fn verify_azure_attestation_with_given_timestamp( .map_err(|_| MaaError::JwkConversion)?; let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der)?; vtpm_quote.verify(&pub_key, &expected_input_data[..32])?; + let _pcrs = vtpm_quote.pcrs_sha256(); // Parse AK certificate @@ -166,9 +173,6 @@ async fn verify_azure_attestation_with_given_timestamp( let (remaining_bytes, ak_certificate) = X509Certificate::from_der(&ak_certificate_der)?; - let leaf_len = ak_certificate_der.len() - remaining_bytes.len(); - let ak_certificate_der_without_trailing_data = &ak_certificate_der[..leaf_len]; - // Check that AK public key matches that from TPM quote and HCL claims let ak_from_certificate = RsaPubKey::from_certificate(&ak_certificate)?; let ak_from_hcl = RsaPubKey::from_openssl_pubkey(&pub_key)?; @@ -179,6 +183,10 @@ async fn verify_azure_attestation_with_given_timestamp( return Err(MaaError::AkFromClaimsNotEqualAkFromCertificate); } + // Strip trailing data from AK certificate + let leaf_len = ak_certificate_der.len() - remaining_bytes.len(); + let ak_certificate_der_without_trailing_data = &ak_certificate_der[..leaf_len]; + // Verify the AK certificate against microsoft root cert verify_ak_cert_with_azure_roots(ak_certificate_der_without_trailing_data, now)?; @@ -190,8 +198,9 @@ async fn verify_azure_attestation_with_given_timestamp( }) } +/// JSON Web Key used in [HclRuntimeClaims] #[derive(Debug, Deserialize)] -pub struct Jwk { +struct Jwk { #[allow(unused)] pub kty: String, pub kid: String, From 4be7bf02415eb7eaec777e2fba5879df9fb9a901 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 3 Dec 2025 13:22:03 +0100 Subject: [PATCH 36/51] Use lazy building of trust anchor --- src/attestation/azure/ak_certificate.rs | 44 ++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/attestation/azure/ak_certificate.rs b/src/attestation/azure/ak_certificate.rs index 15ff378..9d3aa15 100644 --- a/src/attestation/azure/ak_certificate.rs +++ b/src/attestation/azure/ak_certificate.rs @@ -1,5 +1,6 @@ //! Generation and verification of AK certificates from the vTPM use crate::attestation::{azure::MaaError, nv_index}; +use once_cell::sync::Lazy; use std::time::Duration; use tokio_rustls::rustls::pki_types::{CertificateDer, TrustAnchor, UnixTime}; use webpki::EndEntityCert; @@ -85,7 +86,7 @@ bd+PA4RBToG9rXn6vNkUWdbLibU= // Source: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq // Issuer: Azure Virtual TPM Root Certificate Authority 2023 // Valid: 2025-04-24 to 2027-04-24 -const GLOBAL_VIRTUAL_TPMCA03: &str = "-----BEGIN CERTIFICATE----- +const GLOBAL_VIRTUAL_TPMCA03_PEM: &str = "-----BEGIN CERTIFICATE----- MIIFnDCCA4SgAwIBAgITMwAAAAknQOWscnsOpgAAAAAACTANBgkqhkiG9w0BAQwF ADBpMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u MTowOAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1 @@ -118,20 +119,36 @@ zRNW32EFmcAUKZImIsE5dgB7y7eiijf33VWNfWmK05fxzQziWFWRYlET4SVc3jMn PBiY3N8BfK8EBOYbLvzo0qn2n3SAmPhYX3Ag6vbbIHd4Qc8DQKHRV0PB8D3jPGmD -----END CERTIFICATE-----"; +/// The intermediate chain for azure +static GLOBAL_VIRTUAL_TPMCA03: Lazy>> = Lazy::new(|| { + let (_type_label, cert_der) = + pem_rfc7468::decode_vec(GLOBAL_VIRTUAL_TPMCA03_PEM.as_bytes()).expect("Cannot decode PEM"); + vec![CertificateDer::from(cert_der)] +}); + +/// The root anchors for azure +static AZURE_ROOT_ANCHORS: Lazy>> = Lazy::new(|| { + vec![ + // Microsoft RSA Devices Root CA 2021 (older VMs) + pem_to_trust_anchor(MICROSOFT_RSA_DEVICES_ROOT_2021), + // Azure Virtual TPM Root CA 2023 (TDX + newer trusted launch) + pem_to_trust_anchor(AZURE_VIRTUAL_TPM_ROOT_2023), + ] +}); + /// Verify an AK certificate against azure root CA pub fn verify_ak_cert_with_azure_roots(ak_cert_der: &[u8], now_secs: u64) -> Result<(), MaaError> { let ak_cert_der: CertificateDer = ak_cert_der.into(); let end_entity_cert = EndEntityCert::try_from(&ak_cert_der)?; - let roots = azure_root_anchors(); - let intermediates = azure_intermediate_chain(); + // let intermediates = azure_intermediate_chain(); let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs)); end_entity_cert.verify_for_usage( webpki::ALL_VERIFICATION_ALGS, - &roots, - &intermediates, + &AZURE_ROOT_ANCHORS, + &GLOBAL_VIRTUAL_TPMCA03, now, AnyEku, None, @@ -157,23 +174,6 @@ fn pem_to_trust_anchor(pem: &str) -> TrustAnchor<'static> { webpki::anchor_from_trusted_cert(cert_der).expect("Failed to create trust anchor") } -/// Returns the root anchors for azure -fn azure_root_anchors() -> Vec> { - vec![ - // Microsoft RSA Devices Root CA 2021 (older VMs) - pem_to_trust_anchor(MICROSOFT_RSA_DEVICES_ROOT_2021), - // Azure Virtual TPM Root CA 2023 (TDX + newer trusted launch) - pem_to_trust_anchor(AZURE_VIRTUAL_TPM_ROOT_2023), - ] -} - -/// Returns the intermediate chain for azure -fn azure_intermediate_chain() -> Vec> { - let (_type_label, cert_der) = - pem_rfc7468::decode_vec(GLOBAL_VIRTUAL_TPMCA03.as_bytes()).unwrap(); - vec![CertificateDer::from(cert_der)] -} - /// Allows any EKU - we could change this to only accept 1.3.6.1.4.1.567.10.3.12 which is the EKU /// given in the AK certificate struct AnyEku; From 5160c19169d3268fa5675349de75875d1e082bf1 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 3 Dec 2025 13:27:13 +0100 Subject: [PATCH 37/51] Rm unused deps --- Cargo.lock | 63 ------------------------- Cargo.toml | 2 - dummy-attestation-server/Cargo.toml | 3 -- src/attestation/azure/ak_certificate.rs | 2 - 4 files changed, 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b6e816..25a7b2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aho-corasick" version = "1.1.4" @@ -146,7 +140,6 @@ name = "attested-tls-proxy" version = "0.1.0" dependencies = [ "anyhow", - "asn1-rs", "axum", "az-tdx-vtpm", "base64 0.22.1", @@ -159,7 +152,6 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "josekit", "num-bigint", "once_cell", "openssl", @@ -578,15 +570,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "critical-section" version = "1.2.0" @@ -833,13 +816,10 @@ dependencies = [ "attested-tls-proxy", "axum", "clap", - "configfs-tsm", "hex", "parity-scale-codec", "reqwest", - "serde", "serde_json", - "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -965,16 +945,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1526,23 +1496,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "josekit" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c" -dependencies = [ - "anyhow", - "base64 0.22.1", - "flate2", - "openssl", - "regex", - "serde", - "serde_json", - "thiserror 2.0.17", - "time", -] - [[package]] name = "js-sys" version = "0.3.82" @@ -1692,16 +1645,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "mio" version = "1.1.0" @@ -2761,12 +2704,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - [[package]] name = "slab" version = "0.4.11" diff --git a/Cargo.toml b/Cargo.toml index d3d96ce..563ce0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ az-tdx-vtpm = "0.7.4" serde = "1.0.228" base64 = "0.22.1" reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls-webpki-roots-no-provider"] } -josekit = "0.10.3" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } parity-scale-codec = "3.7.5" @@ -42,7 +41,6 @@ tss-esapi = "7.6.0" num-bigint = "0.4.6" webpki = { package = "rustls-webpki", version = "0.103.8" } time = "0.3.44" -asn1-rs = "0.7.1" once_cell = "1.21.3" [dev-dependencies] diff --git a/dummy-attestation-server/Cargo.toml b/dummy-attestation-server/Cargo.toml index 26b91cc..bfc75f7 100644 --- a/dummy-attestation-server/Cargo.toml +++ b/dummy-attestation-server/Cargo.toml @@ -9,13 +9,10 @@ publish = false attested-tls-proxy = { path = ".." } tokio = { version = "1.48.0", features = ["full"] } axum = "0.8.6" -thiserror = "2.0.17" clap = { version = "4.5.51", features = ["derive", "env"] } anyhow = "1.0.100" -configfs-tsm = "0.0.2" hex = "0.4.3" serde_json = "1.0.145" -serde = "1.0.228" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } parity-scale-codec = "3.7.5" diff --git a/src/attestation/azure/ak_certificate.rs b/src/attestation/azure/ak_certificate.rs index 9d3aa15..1750cec 100644 --- a/src/attestation/azure/ak_certificate.rs +++ b/src/attestation/azure/ak_certificate.rs @@ -141,8 +141,6 @@ pub fn verify_ak_cert_with_azure_roots(ak_cert_der: &[u8], now_secs: u64) -> Res let ak_cert_der: CertificateDer = ak_cert_der.into(); let end_entity_cert = EndEntityCert::try_from(&ak_cert_der)?; - // let intermediates = azure_intermediate_chain(); - let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs)); end_entity_cert.verify_for_usage( From e3fd215e2eb85f43cd67bb1a7c29c76f2abaf18f Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 3 Dec 2025 13:43:19 +0100 Subject: [PATCH 38/51] Gate azure functionality behind a feature flag --- Cargo.toml | 8 ++++-- shell.nix | 1 + src/attestation/azure/ak_certificate.rs | 2 +- src/attestation/azure/mod.rs | 1 + src/attestation/{ => azure}/nv_index.rs | 0 src/attestation/mod.rs | 33 +++++++++++++++++++------ 6 files changed, 34 insertions(+), 11 deletions(-) rename src/attestation/{ => azure}/nv_index.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 563ce0d..08ed8a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ http-body-util = "0.1.3" bytes = "1.10.1" http = "1.3.1" serde_json = "1.0.145" -az-tdx-vtpm = "0.7.4" serde = "1.0.228" base64 = "0.22.1" reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls-webpki-roots-no-provider"] } @@ -37,7 +36,8 @@ tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } parity-scale-codec = "3.7.5" openssl = "0.10.75" -tss-esapi = "7.6.0" +az-tdx-vtpm = { version = "0.7.4", optional = true } +tss-esapi = { version = "7.6.0", optional = true } num-bigint = "0.4.6" webpki = { package = "rustls-webpki", version = "0.103.8" } time = "0.3.44" @@ -46,3 +46,7 @@ once_cell = "1.21.3" [dev-dependencies] rcgen = "0.14.5" axum = "0.8.6" + +[features] +default = ["azure"] +azure = ["tss-esapi", "az-tdx-vtpm"] diff --git a/shell.nix b/shell.nix index f852fe5..a3fe682 100644 --- a/shell.nix +++ b/shell.nix @@ -7,5 +7,6 @@ pkgs.mkShell { buildInputs = with pkgs;[ tpm2-tss + openssl ]; } diff --git a/src/attestation/azure/ak_certificate.rs b/src/attestation/azure/ak_certificate.rs index 1750cec..e00b072 100644 --- a/src/attestation/azure/ak_certificate.rs +++ b/src/attestation/azure/ak_certificate.rs @@ -1,5 +1,5 @@ //! Generation and verification of AK certificates from the vTPM -use crate::attestation::{azure::MaaError, nv_index}; +use crate::attestation::azure::{nv_index, MaaError}; use once_cell::sync::Lazy; use std::time::Duration; use tokio_rustls::rustls::pki_types::{CertificateDer, TrustAnchor, UnixTime}; diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs index beab806..82c1af3 100644 --- a/src/attestation/azure/mod.rs +++ b/src/attestation/azure/mod.rs @@ -1,5 +1,6 @@ //! Microsoft Azure Attestation (MAA) evidence generation and verification mod ak_certificate; +mod nv_index; use ak_certificate::{read_ak_certificate_from_tpm, verify_ak_cert_with_azure_roots}; use std::string::FromUtf8Error; diff --git a/src/attestation/nv_index.rs b/src/attestation/azure/nv_index.rs similarity index 100% rename from src/attestation/nv_index.rs rename to src/attestation/azure/nv_index.rs diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 98ba6ff..55537c2 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -1,7 +1,7 @@ +#[cfg(feature = "azure")] pub mod azure; pub mod dcap; pub mod measurements; -pub mod nv_index; use measurements::Measurements; use parity_scale_codec::{Decode, Encode}; @@ -166,7 +166,16 @@ impl AttestationGenerator { ) -> Result, AttestationError> { match self.attestation_type { AttestationType::None => Ok(Vec::new()), - AttestationType::AzureTdx => Ok(azure::create_azure_attestation(input_data).await?), + AttestationType::AzureTdx => { + #[cfg(feature = "azure")] + { + Ok(azure::create_azure_attestation(input_data).await?) + } + #[cfg(not(feature = "azure"))] + { + Err(AttestationError::AttestationTypeNotSupported) + } + } AttestationType::Dummy => self.generate_dummy_attestation(input_data).await, _ => dcap::create_dcap_attestation(input_data).await, } @@ -252,12 +261,19 @@ impl AttestationVerifier { } } AttestationType::AzureTdx => { - azure::verify_azure_attestation( - attestation_exchange_message.attestation, - expected_input_data, - self.pccs_url.clone(), - ) - .await? + #[cfg(feature = "azure")] + { + azure::verify_azure_attestation( + attestation_exchange_message.attestation, + expected_input_data, + self.pccs_url.clone(), + ) + .await? + } + #[cfg(not(feature = "azure"))] + { + return Err(AttestationError::AttestationTypeNotSupported); + } } AttestationType::Dummy => { // Dummy assumes dummy DCAP @@ -340,6 +356,7 @@ pub enum AttestationError { AttestationTypeNotAccepted, #[error("Measurements not accepted")] MeasurementsNotAccepted, + #[cfg(feature = "azure")] #[error("MAA: {0}")] Maa(#[from] azure::MaaError), #[error("Dummy attestation type requires dummy service URL")] From 43cedd9b11b09c02f542f9a4d6a41eb4aa52f7c2 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 4 Dec 2025 08:33:40 +0100 Subject: [PATCH 39/51] Tidy/refactor DCAP verification --- src/attestation/azure/mod.rs | 73 +++++---------------------------- src/attestation/dcap.rs | 36 ++++++++++++++-- src/attestation/measurements.rs | 14 ++++--- src/attestation/mod.rs | 21 ++-------- 4 files changed, 57 insertions(+), 87 deletions(-) diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs index 82c1af3..e3718c2 100644 --- a/src/attestation/azure/mod.rs +++ b/src/attestation/azure/mod.rs @@ -2,7 +2,6 @@ mod ak_certificate; mod nv_index; use ak_certificate::{read_ak_certificate_from_tpm, verify_ak_cert_with_azure_roots}; -use std::string::FromUtf8Error; use az_tdx_vtpm::{hcl, imds, report, vtpm}; use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; @@ -12,11 +11,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use x509_parser::prelude::*; -use crate::attestation::{ - self, - dcap::get_quote_input_data, - measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, -}; +use crate::attestation::dcap::verify_dcap_attestation; /// The attestation evidence payload that gets sent over the channel #[derive(Debug, Serialize, Deserialize)] @@ -100,25 +95,6 @@ async fn verify_azure_attestation_with_given_timestamp( ) -> Result { let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; - // Verify TDX quote (same as with DCAP) - TODO deduplicate this code - let tdx_quote_bytes = BASE64_URL_SAFE.decode(attestation_document.tdx_quote_base64)?; - - let quote = dcap_qvl::quote::Quote::parse(&tdx_quote_bytes)?; - - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let collateral = dcap_qvl::collateral::get_collateral_for_fmspc( - &pccs_url - .clone() - .unwrap_or(attestation::dcap::PCS_URL.to_string()), - fmspc, - ca, - false, // Indicates not SGX - ) - .await?; - - let _verified_report = dcap_qvl::verify::verify(&tdx_quote_bytes, &collateral, now)?; - let hcl_report_bytes = BASE64_URL_SAFE.decode(attestation_document.hcl_report_base64)?; let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; @@ -127,9 +103,11 @@ async fn verify_azure_attestation_with_given_timestamp( // Check that HCL var data hash matches TDX quote report data let mut expected_tdx_input_data = [0u8; 64]; expected_tdx_input_data[..32].copy_from_slice(&var_data_hash); - if get_quote_input_data(quote.report.clone()) != expected_tdx_input_data { - return Err(MaaError::TdxQuoteInputMismatch); - } + + // Do DCAP verification + let tdx_quote_bytes = BASE64_URL_SAFE.decode(attestation_document.tdx_quote_base64)?; + let measurements = + verify_dcap_attestation(tdx_quote_bytes, expected_tdx_input_data, pccs_url).await?; let hcl_ak_pub = hcl_report.ak_pub()?; @@ -191,12 +169,7 @@ async fn verify_azure_attestation_with_given_timestamp( // Verify the AK certificate against microsoft root cert verify_ak_cert_with_azure_roots(ak_certificate_der_without_trailing_data, now)?; - Ok(Measurements { - platform: PlatformMeasurements::from_dcap_qvl_quote("e) - .map_err(|_| MaaError::CannotExtractMeasurementsFromQuote)?, - cvm_image: CvmImageMeasurements::from_dcap_qvl_quote("e) - .map_err(|_| MaaError::CannotExtractMeasurementsFromQuote)?, - }) + Ok(measurements) } /// JSON Web Key used in [HclRuntimeClaims] @@ -272,8 +245,6 @@ impl RsaPubKey { #[derive(Error, Debug)] pub enum MaaError { - #[error("Failed to build input data: {0}")] - InputData(String), #[error("Report: {0}")] Report(#[from] az_tdx_vtpm::report::ReportError), #[error("IMDS: {0}")] @@ -284,12 +255,6 @@ pub enum MaaError { Hcl(#[from] hcl::HclError), #[error("JSON: {0}")] Json(#[from] serde_json::Error), - #[error("HTTP Client: {0}")] - HttpClient(#[from] reqwest::Error), - #[error("MAA provider response: {0} - {1}")] - MaaProvider(http::StatusCode, String), - #[error("Token is bad UTF8: {0}")] - BadUtf8(#[from] FromUtf8Error), #[error("vTPM quote: {0}")] VtpmQuote(#[from] vtpm::QuoteError), #[error("AK public key: {0}")] @@ -300,8 +265,6 @@ pub enum MaaError { TssEsapi(#[from] tss_esapi::Error), #[error("PEM encode: {0}")] Pem(#[from] pem_rfc7468::Error), - #[error("TDX quote input does not match hashed HCL var data")] - TdxQuoteInputMismatch, #[error("TD report input does not match hashed HCL var data")] TdReportInputMismatch, #[error("Base64 decode: {0}")] @@ -312,27 +275,11 @@ pub enum MaaError { AkFromClaimsNotEqualAkFromCertificate, #[error("WebPKI: {0}")] WebPki(#[from] webpki::Error), - #[error("Certificate chain is empty")] - NoCertificate, #[error("X509 parse: {0}")] X509Parse(#[from] x509_parser::asn1_rs::Err), #[error("X509: {0}")] X509(#[from] x509_parser::error::X509Error), - #[error("Quote input is not as expected")] - InputMismatch, - #[error("Configuration mismatch - expected no remote attestation")] - AttestationGivenWhenNoneExpected, - #[error("Configfs-tsm quote generation: {0}")] - QuoteGeneration(#[from] configfs_tsm::QuoteGenerationError), - #[error("SGX quote given when TDX quote expected")] - SgxNotSupported, - #[error("Platform measurements do not match any accepted values")] - UnacceptablePlatformMeasurements, - #[error("OS image measurements do not match any accepted values")] - UnacceptableOsImageMeasurements, - #[error("DCAP quote verification: {0}")] - DcapQvl(#[from] anyhow::Error), - #[error("Cannot convert JSON web key to der")] + #[error("Cannot encode JSON web key as DER")] JwkConversion, #[error("OpenSSL: {0}")] OpenSSL(#[from] ErrorStack), @@ -344,6 +291,8 @@ pub enum MaaError { JwkParse, #[error("HCL runtime claims is missing HCLAkPub field")] ClaimsMissingHCLAkPub, + #[error("DCAP verification: {0}")] + DcapVerification(#[from] crate::attestation::dcap::DcapVerificationError), } #[cfg(test)] @@ -352,7 +301,7 @@ mod tests { #[tokio::test] async fn test_decode_hcl() { - // from cvm-reverse-proxy/internal/attestation/azure/tdx/testdata/hclreport.bin + // From cvm-reverse-proxy/internal/attestation/azure/tdx/testdata/hclreport.bin let hcl_bytes: &'static [u8] = include_bytes!("../../../test-assets/hclreport.bin"); let hcl_report = hcl::HclReport::new(hcl_bytes.to_vec()).unwrap(); diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index 6ddb39a..c0b3713 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -9,6 +9,7 @@ use dcap_qvl::{ collateral::get_collateral_for_fmspc, quote::{Quote, Report}, }; +use thiserror::Error; /// For fetching collateral directly from Intel, if no PCCS is specified pub const PCS_URL: &str = "https://api.trustedservices.intel.com"; @@ -23,7 +24,7 @@ pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs_url: Option, -) -> Result { +) -> Result { let (platform_measurements, image_measurements) = if cfg!(not(test)) { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? @@ -47,14 +48,14 @@ pub async fn verify_dcap_attestation( CvmImageMeasurements::from_dcap_qvl_quote("e)?, ); if get_quote_input_data(quote.report) != expected_input_data { - return Err(AttestationError::InputMismatch); + return Err(DcapVerificationError::InputMismatch); } measurements } else { // In tests we use mock quotes which will fail to verify let quote = tdx_quote::Quote::from_bytes(&input)?; if quote.report_input_data() != expected_input_data { - return Err(AttestationError::InputMismatch); + return Err(DcapVerificationError::InputMismatch); } ( @@ -97,3 +98,32 @@ pub fn get_quote_input_data(report: Report) -> [u8; 64] { Report::SgxEnclave(r) => r.report_data, } } + +/// An error when generating or verifying an attestation +#[derive(Error, Debug)] +pub enum DcapVerificationError { + // #[error("Certificate chain is empty")] + // NoCertificate, + // #[error("X509 parse: {0}")] + // X509Parse(#[from] x509_parser::asn1_rs::Err), + // #[error("X509: {0}")] + // X509(#[from] x509_parser::error::X509Error), + #[error("Quote input is not as expected")] + InputMismatch, + // #[error("Configuration mismatch - expected no remote attestation")] + // AttestationGivenWhenNoneExpected, + // #[error("Configfs-tsm quote generation: {0}")] + // QuoteGeneration(#[from] configfs_tsm::QuoteGenerationError), + #[error("SGX quote given when TDX quote expected")] + SgxNotSupported, + // #[error("Platform measurements do not match any accepted values")] + // UnacceptablePlatformMeasurements, + // #[error("OS image measurements do not match any accepted values")] + // UnacceptableOsImageMeasurements, + #[error("System Time: {0}")] + SystemTime(#[from] std::time::SystemTimeError), + #[error("DCAP quote verification: {0}")] + DcapQvl(#[from] anyhow::Error), + #[error("Quote parse: {0}")] + QuoteParse(#[from] tdx_quote::QuoteParseError), +} diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index a0e2f39..c06ccaf 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -1,5 +1,5 @@ //! Measurements and policy for enforcing them when validating a remote attestation -use crate::attestation::{AttestationError, AttestationType}; +use crate::attestation::{dcap::DcapVerificationError, AttestationError, AttestationType}; use std::{collections::HashMap, path::PathBuf}; use dcap_qvl::quote::Report; @@ -18,12 +18,14 @@ pub struct PlatformMeasurements { impl PlatformMeasurements { /// Given a quote from the dcap_qvl library, extract the platform measurements - pub fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { + pub fn from_dcap_qvl_quote( + quote: &dcap_qvl::quote::Quote, + ) -> Result { let report = match quote.report { Report::TD10(report) => report, Report::TD15(report) => report.base, Report::SgxEnclave(_) => { - return Err(AttestationError::SgxNotSupported); + return Err(DcapVerificationError::SgxNotSupported); } }; Ok(Self { @@ -53,12 +55,14 @@ pub struct CvmImageMeasurements { impl CvmImageMeasurements { /// Given a quote from the dcap_qvl library, extract the CVM image / application measurements - pub fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { + pub fn from_dcap_qvl_quote( + quote: &dcap_qvl::quote::Quote, + ) -> Result { let report = match quote.report { Report::TD10(report) => report, Report::TD15(report) => report.base, Report::SgxEnclave(_) => { - return Err(AttestationError::SgxNotSupported); + return Err(DcapVerificationError::SgxNotSupported); } }; Ok(Self { diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 55537c2..fa6ae98 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -8,13 +8,12 @@ use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::{ fmt::{self, Display, Formatter}, - time::{SystemTime, SystemTimeError, UNIX_EPOCH}, + time::{SystemTime, UNIX_EPOCH}, }; -use tdx_quote::QuoteParseError; use thiserror::Error; -use crate::attestation::measurements::MeasurementPolicy; +use crate::attestation::{dcap::DcapVerificationError, measurements::MeasurementPolicy}; /// This is the type sent over the channel to provide an attestation #[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] @@ -332,24 +331,12 @@ pub enum AttestationError { X509Parse(#[from] x509_parser::asn1_rs::Err), #[error("X509: {0}")] X509(#[from] x509_parser::error::X509Error), - #[error("Quote input is not as expected")] - InputMismatch, #[error("Configuration mismatch - expected no remote attestation")] AttestationGivenWhenNoneExpected, #[error("Configfs-tsm quote generation: {0}")] QuoteGeneration(#[from] configfs_tsm::QuoteGenerationError), - #[error("SGX quote given when TDX quote expected")] - SgxNotSupported, - #[error("Platform measurements do not match any accepted values")] - UnacceptablePlatformMeasurements, - #[error("OS image measurements do not match any accepted values")] - UnacceptableOsImageMeasurements, - #[error("System Time: {0}")] - SystemTime(#[from] SystemTimeError), - #[error("DCAP quote verification: {0}")] - DcapQvl(#[from] anyhow::Error), - #[error("Quote parse: {0}")] - QuoteParse(#[from] QuoteParseError), + #[error("DCAP verification: {0}")] + DcapVerification(#[from] DcapVerificationError), #[error("Attestation type not supported")] AttestationTypeNotSupported, #[error("Attestation type not accepted")] From 89733c61f9ced413312ddea708b554af545cc7a2 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 4 Dec 2025 08:47:18 +0100 Subject: [PATCH 40/51] Add logging --- src/attestation/azure/ak_certificate.rs | 2 ++ src/attestation/azure/mod.rs | 4 +++- src/attestation/azure/nv_index.rs | 1 + src/attestation/dcap.rs | 21 +++++---------------- src/attestation/mod.rs | 1 + 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/attestation/azure/ak_certificate.rs b/src/attestation/azure/ak_certificate.rs index e00b072..7efc854 100644 --- a/src/attestation/azure/ak_certificate.rs +++ b/src/attestation/azure/ak_certificate.rs @@ -152,12 +152,14 @@ pub fn verify_ak_cert_with_azure_roots(ak_cert_der: &[u8], now_secs: u64) -> Res None, None, )?; + tracing::debug!("Successfully verified AK certificate from vTPM"); Ok(()) } /// Retrieve an AK certificate from the vTPM pub fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { + tracing::debug!("Reading AK certificate from vTPM"); let mut context = nv_index::get_session_context()?; nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) } diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs index e3718c2..ffbaf7e 100644 --- a/src/attestation/azure/mod.rs +++ b/src/attestation/azure/mod.rs @@ -29,7 +29,7 @@ struct AttestationDocument { struct TpmAttest { /// Attestation Key certificate from vTPM ak_certificate_pem: String, - /// vTPM quotes over the selected PCR bank(s). + /// vTPM quote quote: vtpm::Quote, /// Raw TCG event log bytes (UEFI + IMA) [currently not used] /// @@ -68,6 +68,7 @@ pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, M tpm_attestation, }; + tracing::info!("Successfully generated azure attestation: {attestation_document:?}"); Ok(serde_json::to_vec(&attestation_document)?) } @@ -94,6 +95,7 @@ async fn verify_azure_attestation_with_given_timestamp( now: u64, ) -> Result { let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; + tracing::info!("Attempting to verifiy azure attestation: {attestation_document:?}"); let hcl_report_bytes = BASE64_URL_SAFE.decode(attestation_document.hcl_report_base64)?; diff --git a/src/attestation/azure/nv_index.rs b/src/attestation/azure/nv_index.rs index f38c267..d1cfe85 100644 --- a/src/attestation/azure/nv_index.rs +++ b/src/attestation/azure/nv_index.rs @@ -14,6 +14,7 @@ pub fn get_session_context() -> Result { } pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, tss_esapi::Error> { + tracing::debug!("Reading from TPM, nv index: {index}"); let nv_tpm_handle = NvIndexTpmHandle::new(index)?; let buf = tss_esapi::abstraction::nv::read_full(ctx, NvAuth::Owner, nv_tpm_handle)?; Ok(buf.to_vec()) diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index c0b3713..bb77590 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -16,7 +16,9 @@ pub const PCS_URL: &str = "https://api.trustedservices.intel.com"; /// Quote generation using configfs_tsm pub async fn create_dcap_attestation(input_data: [u8; 64]) -> Result, AttestationError> { - Ok(generate_quote(input_data)?) + let quote = generate_quote(input_data)?; + tracing::info!("Generated TDX quote of {} bytes", quote.len()); + Ok(quote) } /// Verify a DCAP TDX quote, and return the measurement values @@ -30,6 +32,7 @@ pub async fn verify_dcap_attestation( .duration_since(std::time::UNIX_EPOCH)? .as_secs(); let quote = Quote::parse(&input)?; + tracing::info!("Verifying DCAP attestation: {quote:?}"); let ca = quote.ca()?; let fmspc = hex::encode_upper(quote.fmspc()?); @@ -99,27 +102,13 @@ pub fn get_quote_input_data(report: Report) -> [u8; 64] { } } -/// An error when generating or verifying an attestation +/// An error when verifying a DCAP attestation #[derive(Error, Debug)] pub enum DcapVerificationError { - // #[error("Certificate chain is empty")] - // NoCertificate, - // #[error("X509 parse: {0}")] - // X509Parse(#[from] x509_parser::asn1_rs::Err), - // #[error("X509: {0}")] - // X509(#[from] x509_parser::error::X509Error), #[error("Quote input is not as expected")] InputMismatch, - // #[error("Configuration mismatch - expected no remote attestation")] - // AttestationGivenWhenNoneExpected, - // #[error("Configfs-tsm quote generation: {0}")] - // QuoteGeneration(#[from] configfs_tsm::QuoteGenerationError), #[error("SGX quote given when TDX quote expected")] SgxNotSupported, - // #[error("Platform measurements do not match any accepted values")] - // UnacceptablePlatformMeasurements, - // #[error("OS image measurements do not match any accepted values")] - // UnacceptableOsImageMeasurements, #[error("System Time: {0}")] SystemTime(#[from] std::time::SystemTimeError), #[error("DCAP quote verification: {0}")] diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index fa6ae98..9006f77 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -172,6 +172,7 @@ impl AttestationGenerator { } #[cfg(not(feature = "azure"))] { + tracing::error!("Attempted to generate an azure attestation but the `azure` feature not enabled"); Err(AttestationError::AttestationTypeNotSupported) } } From d6b37f53c173cfa73ac536ec682a96b69e2b4bcc Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 9 Dec 2025 14:22:45 +0100 Subject: [PATCH 41/51] Switch to `MultiMeasurements` accomodating for azure --- src/attestation/azure/mod.rs | 12 +- src/attestation/dcap.rs | 25 +- src/attestation/measurements.rs | 494 +++++++++++++++++--------------- src/attestation/mod.rs | 4 +- src/lib.rs | 75 ++--- src/test_helpers.rs | 23 +- 6 files changed, 322 insertions(+), 311 deletions(-) diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs index ffbaf7e..3163a79 100644 --- a/src/attestation/azure/mod.rs +++ b/src/attestation/azure/mod.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use x509_parser::prelude::*; -use crate::attestation::dcap::verify_dcap_attestation; +use crate::attestation::{dcap::verify_dcap_attestation, measurements::MultiMeasurements}; /// The attestation evidence payload that gets sent over the channel #[derive(Debug, Serialize, Deserialize)] @@ -77,7 +77,7 @@ pub async fn verify_azure_attestation( input: Vec, expected_input_data: [u8; 64], pccs_url: Option, -) -> Result { +) -> Result { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("Time went backwards") @@ -93,7 +93,7 @@ async fn verify_azure_attestation_with_given_timestamp( expected_input_data: [u8; 64], pccs_url: Option, now: u64, -) -> Result { +) -> Result { let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; tracing::info!("Attempting to verifiy azure attestation: {attestation_document:?}"); @@ -108,7 +108,7 @@ async fn verify_azure_attestation_with_given_timestamp( // Do DCAP verification let tdx_quote_bytes = BASE64_URL_SAFE.decode(attestation_document.tdx_quote_base64)?; - let measurements = + let _dcap_measurements = verify_dcap_attestation(tdx_quote_bytes, expected_tdx_input_data, pccs_url).await?; let hcl_ak_pub = hcl_report.ak_pub()?; @@ -142,7 +142,7 @@ async fn verify_azure_attestation_with_given_timestamp( let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der)?; vtpm_quote.verify(&pub_key, &expected_input_data[..32])?; - let _pcrs = vtpm_quote.pcrs_sha256(); + let pcrs = vtpm_quote.pcrs_sha256(); // Parse AK certificate let (_type_label, ak_certificate_der) = pem_rfc7468::decode_vec( @@ -171,7 +171,7 @@ async fn verify_azure_attestation_with_given_timestamp( // Verify the AK certificate against microsoft root cert verify_ak_cert_with_azure_roots(ak_certificate_der_without_trailing_data, now)?; - Ok(measurements) + Ok(MultiMeasurements::from_pcrs(pcrs)) } /// JSON Web Key used in [HclRuntimeClaims] diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index bb77590..5ed2b33 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -1,8 +1,5 @@ //! Data Center Attestation Primitives (DCAP) evidence generation and verification -use crate::attestation::{ - measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, - AttestationError, -}; +use crate::attestation::{measurements::MultiMeasurements, AttestationError}; use configfs_tsm::QuoteGenerationError; use dcap_qvl::{ @@ -26,8 +23,8 @@ pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs_url: Option, -) -> Result { - let (platform_measurements, image_measurements) = if cfg!(not(test)) { +) -> Result { + let measurements = if cfg!(not(test)) { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs(); @@ -46,10 +43,8 @@ pub async fn verify_dcap_attestation( let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?; - let measurements = ( - PlatformMeasurements::from_dcap_qvl_quote("e)?, - CvmImageMeasurements::from_dcap_qvl_quote("e)?, - ); + let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; + if get_quote_input_data(quote.report) != expected_input_data { return Err(DcapVerificationError::InputMismatch); } @@ -61,16 +56,10 @@ pub async fn verify_dcap_attestation( return Err(DcapVerificationError::InputMismatch); } - ( - PlatformMeasurements::from_tdx_quote("e), - CvmImageMeasurements::from_tdx_quote("e), - ) + MultiMeasurements::from_tdx_quote("e) }; - Ok(Measurements { - platform: platform_measurements, - cvm_image: image_measurements, - }) + Ok(measurements) } /// Create a mock quote for testing on non-confidential hardware diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index c06ccaf..0ed5a65 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -7,54 +7,98 @@ use http::{header::InvalidHeaderValue, HeaderValue}; use serde::Deserialize; use thiserror::Error; -/// Measurements determined by the CVM platform -#[derive(Clone, PartialEq, Debug)] -pub struct PlatformMeasurements { - /// MRTD register value - pub mrtd: [u8; 48], - /// RTMR0 register value - pub rtmr0: [u8; 48], +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum DcapMeasurementRegister { + MRTD, + RTMR0, + RTMR1, + RTMR2, + RTMR3, } -impl PlatformMeasurements { - /// Given a quote from the dcap_qvl library, extract the platform measurements - pub fn from_dcap_qvl_quote( - quote: &dcap_qvl::quote::Quote, - ) -> Result { - let report = match quote.report { - Report::TD10(report) => report, - Report::TD15(report) => report.base, - Report::SgxEnclave(_) => { - return Err(DcapVerificationError::SgxNotSupported); - } - }; - Ok(Self { - mrtd: report.mr_td, - rtmr0: report.rt_mr0, - }) - } - - pub fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { - Self { - mrtd: quote.mrtd(), - rtmr0: quote.rtmr0(), +impl TryFrom for DcapMeasurementRegister { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::MRTD), + 1 => Ok(Self::RTMR0), + 2 => Ok(Self::RTMR1), + 3 => Ok(Self::RTMR2), + 4 => Ok(Self::RTMR3), + _ => Err(()), } } } -/// Measurements determined by the CVM image or application -#[derive(Clone, PartialEq, Debug)] -pub struct CvmImageMeasurements { - /// RTMR1 register value - pub rtmr1: [u8; 48], - /// RTMR2 register value - pub rtmr2: [u8; 48], - /// RTMR3 register value - pub rtmr3: [u8; 48], +#[derive(Debug, Clone, PartialEq)] +pub enum MultiMeasurements { + Dcap(HashMap), + Azure(HashMap), + NoAttestation, } -impl CvmImageMeasurements { - /// Given a quote from the dcap_qvl library, extract the CVM image / application measurements +impl MultiMeasurements { + /// Convert to the JSON format used in HTTP headers + pub fn to_header_format(&self) -> Result { + let measurements_map = match self { + MultiMeasurements::Dcap(dcap_measurements) => dcap_measurements + .iter() + .map(|(register, value)| ((register.clone() as u8).to_string(), hex::encode(value))) + .collect(), + MultiMeasurements::Azure(azure_measurements) => azure_measurements + .iter() + .map(|(index, value)| (index.to_string(), hex::encode(value))) + .collect(), + MultiMeasurements::NoAttestation => HashMap::new(), + }; + + Ok(HeaderValue::from_str(&serde_json::to_string( + &measurements_map, + )?)?) + } + + /// Parse the JSON used in HTTP headers + pub fn from_header_format( + input: &str, + attestation_type: AttestationType, + ) -> Result { + let measurements_map: HashMap = serde_json::from_str(input)?; + + Ok(match attestation_type { + AttestationType::AzureTdx => Self::Azure( + measurements_map + .into_iter() + .map(|(k, v)| { + Ok(( + k as u32, + hex::decode(v)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, + )) + }) + .collect::>()?, + ), + AttestationType::None => Self::NoAttestation, + _ => { + let measurements_map = measurements_map + .into_iter() + .map(|(k, v)| { + Ok(( + k.try_into().unwrap(), + hex::decode(v)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, + )) + }) + .collect::>()?; + Self::Dcap(measurements_map) + } + }) + } + + /// Given a quote from the dcap_qvl library, extract the measurements pub fn from_dcap_qvl_quote( quote: &dcap_qvl::quote::Quote, ) -> Result { @@ -65,79 +109,32 @@ impl CvmImageMeasurements { return Err(DcapVerificationError::SgxNotSupported); } }; - Ok(Self { - rtmr1: report.rt_mr1, - rtmr2: report.rt_mr2, - rtmr3: report.rt_mr3, - }) + Ok(Self::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, report.mr_td), + (DcapMeasurementRegister::RTMR0, report.rt_mr0), + (DcapMeasurementRegister::RTMR1, report.rt_mr1), + (DcapMeasurementRegister::RTMR2, report.rt_mr2), + (DcapMeasurementRegister::RTMR3, report.rt_mr3), + ]))) } pub fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { - Self { - rtmr1: quote.rtmr1(), - rtmr2: quote.rtmr2(), - rtmr3: quote.rtmr3(), - } - } -} - -/// A full set of measurement register values -#[derive(Debug, Clone, PartialEq)] -pub struct Measurements { - pub platform: PlatformMeasurements, - pub cvm_image: CvmImageMeasurements, -} - -impl Measurements { - /// Convert to the JSON format used in HTTP headers - pub fn to_header_format(&self) -> Result { - let mut measurements_map = HashMap::new(); - measurements_map.insert(0, hex::encode(self.platform.mrtd)); - measurements_map.insert(1, hex::encode(self.platform.rtmr0)); - measurements_map.insert(2, hex::encode(self.cvm_image.rtmr1)); - measurements_map.insert(3, hex::encode(self.cvm_image.rtmr2)); - measurements_map.insert(4, hex::encode(self.cvm_image.rtmr3)); - Ok(HeaderValue::from_str(&serde_json::to_string( - &measurements_map, - )?)?) + Self::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, quote.mrtd()), + (DcapMeasurementRegister::RTMR0, quote.rtmr0()), + (DcapMeasurementRegister::RTMR1, quote.rtmr1()), + (DcapMeasurementRegister::RTMR2, quote.rtmr2()), + (DcapMeasurementRegister::RTMR3, quote.rtmr3()), + ])) } - /// Parse the JSON used in HTTP headers - pub fn from_header_format(input: &str) -> Result { - let measurements_map: HashMap = serde_json::from_str(input)?; - let measurements_map: HashMap = measurements_map - .into_iter() - .map(|(k, v)| { - Ok(( - k, - hex::decode(v)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - )) - }) - .collect::>()?; - - Ok(Self { - platform: PlatformMeasurements { - mrtd: *measurements_map - .get(&0) - .ok_or(MeasurementFormatError::MissingValue("MRTD".to_string()))?, - rtmr0: *measurements_map - .get(&1) - .ok_or(MeasurementFormatError::MissingValue("RTMR0".to_string()))?, - }, - cvm_image: CvmImageMeasurements { - rtmr1: *measurements_map - .get(&2) - .ok_or(MeasurementFormatError::MissingValue("RTMR1".to_string()))?, - rtmr2: *measurements_map - .get(&3) - .ok_or(MeasurementFormatError::MissingValue("RTMR2".to_string()))?, - rtmr3: *measurements_map - .get(&4) - .ok_or(MeasurementFormatError::MissingValue("RTMR3".to_string()))?, - }, - }) + pub fn from_pcrs<'a>(pcrs: impl Iterator) -> Self { + Self::Azure( + pcrs.map(|p| p.clone()) + .enumerate() + .map(|(index, value)| (index as u32, value)) + .collect(), + ) } } @@ -161,12 +158,32 @@ pub enum MeasurementFormatError { } /// An accepted measurement value given in the measurements file -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct MeasurementRecord { /// An identifier, for example the name and version of the corresponding OS image pub measurement_id: String, /// The expected measurement register values - pub measurements: Measurements, + pub measurements: MultiMeasurements, +} + +impl MeasurementRecord { + pub fn allow_no_attestation() -> Self { + Self { + measurement_id: "Allow no attestation".to_string(), + measurements: MultiMeasurements::NoAttestation, + } + } + + pub fn allow_any_measurement(attestation_type: AttestationType) -> Self { + Self { + measurement_id: format!("Any measurement for {attestation_type}"), + measurements: match attestation_type { + AttestationType::None => MultiMeasurements::NoAttestation, + AttestationType::AzureTdx => MultiMeasurements::Azure(HashMap::new()), + _ => MultiMeasurements::Dcap(HashMap::new()), + }, + } + } } /// Represents the measurement policy @@ -177,35 +194,35 @@ pub struct MeasurementRecord { pub struct MeasurementPolicy { /// A map of accepted attestation types to accepted measurement values /// A value of None means accept any measurement value for this measurement type - pub(crate) accepted_measurements: HashMap>>, + pub(crate) accepted_measurements: Vec, } impl MeasurementPolicy { /// This will only allow no attestation - and will reject it if one is given pub fn expect_none() -> Self { Self { - accepted_measurements: HashMap::from([(AttestationType::None, None)]), + accepted_measurements: vec![MeasurementRecord::allow_no_attestation()], } } /// Allow any measurements with the given attestation type pub fn single_attestation_type(attestation_type: AttestationType) -> Self { Self { - accepted_measurements: HashMap::from([(attestation_type, None)]), + accepted_measurements: vec![MeasurementRecord::allow_any_measurement(attestation_type)], } } /// Accept any attestation type with any measurements pub fn accept_anything() -> Self { Self { - accepted_measurements: HashMap::from([ - (AttestationType::None, None), - (AttestationType::Dummy, None), - (AttestationType::DcapTdx, None), - (AttestationType::QemuTdx, None), - (AttestationType::AzureTdx, None), - (AttestationType::GcpTdx, None), - ]), + accepted_measurements: vec![ + MeasurementRecord::allow_no_attestation(), + MeasurementRecord::allow_any_measurement(AttestationType::Dummy), + MeasurementRecord::allow_any_measurement(AttestationType::DcapTdx), + MeasurementRecord::allow_any_measurement(AttestationType::QemuTdx), + MeasurementRecord::allow_any_measurement(AttestationType::GcpTdx), + MeasurementRecord::allow_any_measurement(AttestationType::AzureTdx), + ], } } @@ -213,23 +230,16 @@ impl MeasurementPolicy { #[cfg(test)] pub fn mock() -> Self { Self { - accepted_measurements: HashMap::from([( - AttestationType::DcapTdx, - Some(vec![MeasurementRecord { - measurement_id: "test".to_string(), - measurements: Measurements { - platform: PlatformMeasurements { - mrtd: [0; 48], - rtmr0: [0; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [0; 48], - rtmr2: [0; 48], - rtmr3: [0; 48], - }, - }, - }]), - )]), + accepted_measurements: vec![MeasurementRecord { + measurement_id: "test".to_string(), + measurements: MultiMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, [0; 48]), + (DcapMeasurementRegister::RTMR0, [0; 48]), + (DcapMeasurementRegister::RTMR1, [0; 48]), + (DcapMeasurementRegister::RTMR2, [0; 48]), + (DcapMeasurementRegister::RTMR3, [0; 48]), + ])), + }], } } @@ -237,21 +247,45 @@ impl MeasurementPolicy { pub fn check_measurement( &self, attestation_type: AttestationType, - measurements: &Measurements, + measurements: &MultiMeasurements, ) -> Result<(), AttestationError> { - match self.accepted_measurements.get(&attestation_type) { - Some(Some(measurement_set)) => { - if measurement_set - .iter() - .any(|a| &a.measurements == measurements) - { - Ok(()) - } else { - Err(AttestationError::MeasurementsNotAccepted) + if self + .accepted_measurements + .iter() + .any(|measurement_record| match measurements { + MultiMeasurements::Dcap(dcap_measurements) => { + if let MultiMeasurements::Dcap(d) = measurement_record.measurements.clone() { + for (k, v) in dcap_measurements.iter() { + if d[k] != *v { + return false; + } + } + return true; + } + return false; } - } - Some(None) => Ok(()), - None => Err(AttestationError::AttestationTypeNotAccepted), + MultiMeasurements::Azure(azure_measurements) => { + if let MultiMeasurements::Azure(a) = measurement_record.measurements.clone() { + for (k, v) in azure_measurements.iter() { + if a[k] != *v { + false; + } + } + true; + } + false + } + MultiMeasurements::NoAttestation => { + if MultiMeasurements::NoAttestation == measurement_record.measurements.clone() { + return true; + } + false + } + }) + { + Ok(()) + } else { + Err(AttestationError::MeasurementsNotAccepted) } } @@ -259,7 +293,8 @@ impl MeasurementPolicy { pub fn has_remote_attestion(&self) -> bool { !self .accepted_measurements - .contains_key(&AttestationType::None) + .iter() + .any(|a| a.measurements == MultiMeasurements::NoAttestation) } /// Given the path to a JSON file containing measurements, return a [MeasurementPolicy] @@ -285,7 +320,7 @@ impl MeasurementPolicy { let measurements_simple: Vec = serde_json::from_slice(&json_bytes)?; - let mut measurement_policy = HashMap::new(); + let mut measurement_policy = Vec::new(); for measurement in measurements_simple { let attestation_type = @@ -293,42 +328,43 @@ impl MeasurementPolicy { .unwrap(); if let Some(measurements) = measurement.measurements { - let measurement_record = MeasurementRecord { - measurement_id: measurement.measurement_id.unwrap_or_default(), - measurements: Measurements { - platform: PlatformMeasurements { - mrtd: hex::decode(&measurements["0"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - rtmr0: hex::decode(&measurements["1"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - }, - cvm_image: CvmImageMeasurements { - rtmr1: hex::decode(&measurements["2"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - rtmr2: hex::decode(&measurements["3"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - rtmr3: hex::decode(&measurements["4"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - }, - }, + let multi_measurement = match attestation_type { + AttestationType::AzureTdx => { + let azure_measurements = measurements + .into_iter() + .map(|(index, entry)| { + Ok(( + index.parse().unwrap(), + hex::decode(entry.expected)?.try_into().unwrap(), + )) + }) + .collect::, MeasurementFormatError>>()?; + MultiMeasurements::Azure(azure_measurements) + } + AttestationType::None => MultiMeasurements::NoAttestation, + _ => MultiMeasurements::Dcap( + measurements + .into_iter() + .map(|(index, entry)| { + let index: u8 = index.parse().unwrap(); + Ok(( + DcapMeasurementRegister::try_from(index).unwrap(), + hex::decode(entry.expected)?.try_into().unwrap(), + )) + }) + .collect::, + MeasurementFormatError, + >>()?, + ), }; - measurement_policy - .entry(attestation_type) - .and_modify(|maybe_vec: &mut Option>| { - match maybe_vec.as_mut() { - Some(vec) => vec.push(measurement_record.clone()), - None => *maybe_vec = Some(vec![measurement_record.clone()]), - } - }) - .or_insert_with(|| Some(vec![measurement_record])); + measurement_policy.push(MeasurementRecord { + measurement_id: measurement.measurement_id.unwrap_or_default(), + measurements: multi_measurement, + }); } else { - measurement_policy.entry(attestation_type).or_insert(None); + measurement_policy.push(MeasurementRecord::allow_any_measurement(attestation_type)); }; } @@ -342,20 +378,6 @@ impl MeasurementPolicy { mod tests { use super::*; - fn mock_measurements() -> Measurements { - Measurements { - platform: PlatformMeasurements { - mrtd: [0; 48], - rtmr0: [0; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [0; 48], - rtmr2: [0; 48], - rtmr3: [0; 48], - }, - } - } - #[tokio::test] async fn test_read_measurements_file() { let specific_measurements = @@ -363,51 +385,51 @@ mod tests { .await .unwrap(); - assert!(specific_measurements - .accepted_measurements - .get(&AttestationType::DcapTdx) - .unwrap() - .is_some()); - - // Will not match mock measurements - assert!(matches!( - specific_measurements - .check_measurement(AttestationType::DcapTdx, &mock_measurements()) - .unwrap_err(), - AttestationError::MeasurementsNotAccepted - )); - - // Will not match another attestation type - assert!(matches!( - specific_measurements - .check_measurement(AttestationType::None, &mock_measurements()) - .unwrap_err(), - AttestationError::AttestationTypeNotAccepted - )); + // assert!(specific_measurements + // .accepted_measurements + // .get(&AttestationType::DcapTdx) + // .unwrap() + // .is_some()); + // + // // Will not match mock measurements + // assert!(matches!( + // specific_measurements + // .check_measurement(AttestationType::DcapTdx, &mock_measurements()) + // .unwrap_err(), + // AttestationError::MeasurementsNotAccepted + // )); + // + // // Will not match another attestation type + // assert!(matches!( + // specific_measurements + // .check_measurement(AttestationType::None, &mock_measurements()) + // .unwrap_err(), + // AttestationError::AttestationTypeNotAccepted + // )); } #[tokio::test] async fn test_read_measurements_file_non_specific() { - let mock_measurements = mock_measurements(); + // let mock_measurements = mock_measurements(); // This specifies a particular attestation type, but not specific measurements let allowed_attestation_type = MeasurementPolicy::from_file("test-assets/measurements_2.json".into()) .await .unwrap(); - allowed_attestation_type - .check_measurement(AttestationType::DcapTdx, &mock_measurements) - .unwrap(); - - assert!(allowed_attestation_type - .accepted_measurements - .get(&AttestationType::DcapTdx) - .unwrap() - .is_none()); - - // Will match mock measurements - allowed_attestation_type - .check_measurement(AttestationType::DcapTdx, &mock_measurements) - .unwrap(); + // allowed_attestation_type + // .check_measurement(AttestationType::DcapTdx, &mock_measurements) + // .unwrap(); + // + // assert!(allowed_attestation_type + // .accepted_measurements + // .get(&AttestationType::DcapTdx) + // .unwrap() + // .is_none()); + // + // // Will match mock measurements + // allowed_attestation_type + // .check_measurement(AttestationType::DcapTdx, &mock_measurements) + // .unwrap(); } } diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 9006f77..aa50106 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -3,7 +3,7 @@ pub mod azure; pub mod dcap; pub mod measurements; -use measurements::Measurements; +use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::{ @@ -241,7 +241,7 @@ impl AttestationVerifier { &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - ) -> Result, AttestationError> { + ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type; tracing::debug!("Verifing {attestation_type} attestation"); diff --git a/src/lib.rs b/src/lib.rs index 0b4f091..a3fb87f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ pub mod attestation; pub use attestation::AttestationGenerator; -use attestation::{measurements::Measurements, AttestationError, AttestationType}; +use attestation::{measurements::MultiMeasurements, AttestationError, AttestationType}; use bytes::Bytes; use http::HeaderValue; use http_body_util::{combinators::BoxBody, BodyExt}; @@ -557,7 +557,7 @@ impl ProxyClient { cert_chain: Option>>, attestation_generator: AttestationGenerator, attestation_verifier: AttestationVerifier, - ) -> (Http2Sender, Option, AttestationType) { + ) -> (Http2Sender, Option, AttestationType) { let mut delay = Duration::from_secs(1); let max_delay = Duration::from_secs(SERVER_RECONNECT_MAX_BACKOFF_SECS); @@ -592,7 +592,7 @@ impl ProxyClient { cert_chain: Option>>, attestation_generator: AttestationGenerator, attestation_verifier: AttestationVerifier, - ) -> Result<(Http2Sender, Option, AttestationType), ProxyError> { + ) -> Result<(Http2Sender, Option, AttestationType), ProxyError> { // Make a TCP client connection and TLS handshake let out = TcpStream::connect(&target).await?; let mut tls_stream = connector @@ -857,13 +857,13 @@ mod tests { use std::collections::HashMap; use crate::attestation::measurements::{ - CvmImageMeasurements, MeasurementPolicy, MeasurementRecord, PlatformMeasurements, + DcapMeasurementRegister, MeasurementPolicy, MeasurementRecord, MultiMeasurements, }; use super::*; use test_helpers::{ - default_measurements, example_http_service, example_service, generate_certificate_chain, - generate_tls_config, generate_tls_config_with_client_auth, + default_dcap_measurements, example_http_service, example_service, + generate_certificate_chain, generate_tls_config, generate_tls_config_with_client_auth, }; // Server has mock DCAP, client has no attestation and no client auth @@ -913,9 +913,6 @@ mod tests { .unwrap(); let headers = res.headers(); - let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = Measurements::from_header_format(measurements_json).unwrap(); - assert_eq!(measurements, default_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -924,6 +921,12 @@ mod tests { .unwrap(); assert_eq!(attestation_type, AttestationType::DcapTdx.as_str()); + let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); + let measurements = + MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) + .unwrap(); + assert_eq!(measurements, default_dcap_measurements()); + let res_body = res.text().await.unwrap(); assert_eq!(res_body, "No measurements"); } @@ -1004,8 +1007,9 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = Measurements::from_header_format(&res_body).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); + assert_eq!(measurements, default_dcap_measurements()); } // Server has mock DCAP, client has mock DCAP and client auth @@ -1071,8 +1075,10 @@ mod tests { let headers = res.headers(); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = Measurements::from_header_format(measurements_json).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) + .unwrap(); + assert_eq!(measurements, default_dcap_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1085,8 +1091,9 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = Measurements::from_header_format(&res_body).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); + assert_eq!(measurements, default_dcap_measurements()); // Now do another request - to check that the connection has stayed open let res = reqwest::get(format!("http://{}", proxy_client_addr.to_string())) @@ -1095,8 +1102,10 @@ mod tests { let headers = res.headers(); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = Measurements::from_header_format(measurements_json).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) + .unwrap(); + assert_eq!(measurements, default_dcap_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1109,8 +1118,9 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = Measurements::from_header_format(&res_body).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); + assert_eq!(measurements, default_dcap_measurements()); } // Server has mock DCAP, client no attestation - just get the server certificate @@ -1219,23 +1229,16 @@ mod tests { let attestation_verifier = AttestationVerifier { measurement_policy: MeasurementPolicy { - accepted_measurements: HashMap::from([( - AttestationType::DcapTdx, - Some(vec![MeasurementRecord { - measurement_id: "test".to_string(), - measurements: Measurements { - platform: PlatformMeasurements { - mrtd: [0; 48], - rtmr0: [0; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [1; 48], // This differs from the mock measurements given - rtmr2: [0; 48], - rtmr3: [0; 48], - }, - }, - }]), - )]), + accepted_measurements: vec![MeasurementRecord { + measurement_id: "test".to_string(), + measurements: MultiMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, [0; 48]), + (DcapMeasurementRegister::RTMR0, [0; 48]), + (DcapMeasurementRegister::RTMR1, [1; 48]), // This differs from the mock measurements + (DcapMeasurementRegister::RTMR2, [0; 48]), + (DcapMeasurementRegister::RTMR3, [0; 48]), + ])), + }], }, pccs_url: None, log_dcap_quote: false, diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 21ec18f..45b2146 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -1,5 +1,6 @@ use axum::response::IntoResponse; use std::{ + collections::HashMap, net::{IpAddr, SocketAddr}, sync::Arc, }; @@ -12,7 +13,7 @@ use tokio_rustls::rustls::{ }; use crate::{ - attestation::measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, + attestation::measurements::{DcapMeasurementRegister, MultiMeasurements}, MEASUREMENT_HEADER, SUPPORTED_ALPN_PROTOCOL_VERSIONS, }; @@ -169,16 +170,12 @@ pub async fn example_service() -> SocketAddr { addr } -pub fn default_measurements() -> Measurements { - Measurements { - platform: PlatformMeasurements { - mrtd: [0u8; 48], - rtmr0: [0u8; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [0u8; 48], - rtmr2: [0u8; 48], - rtmr3: [0u8; 48], - }, - } +pub fn default_dcap_measurements() -> MultiMeasurements { + MultiMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, [0u8; 48]), + (DcapMeasurementRegister::RTMR0, [0u8; 48]), + (DcapMeasurementRegister::RTMR1, [0u8; 48]), + (DcapMeasurementRegister::RTMR2, [0u8; 48]), + (DcapMeasurementRegister::RTMR3, [0u8; 48]), + ])) } From 029c6af9540afdf6fabff5629f69d05f60af3532 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 9 Dec 2025 14:48:05 +0100 Subject: [PATCH 42/51] Update test --- src/attestation/measurements.rs | 81 +++++++++++++++++---------------- src/attestation/mod.rs | 3 +- src/lib.rs | 1 - 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index 0ed5a65..b64e0bc 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -246,7 +246,6 @@ impl MeasurementPolicy { /// Given an attestation type and set of measurements, check whether they are acceptable pub fn check_measurement( &self, - attestation_type: AttestationType, measurements: &MultiMeasurements, ) -> Result<(), AttestationError> { if self @@ -256,7 +255,7 @@ impl MeasurementPolicy { MultiMeasurements::Dcap(dcap_measurements) => { if let MultiMeasurements::Dcap(d) = measurement_record.measurements.clone() { for (k, v) in dcap_measurements.iter() { - if d[k] != *v { + if d.get(k).is_some_and(|x| x != v) { return false; } } @@ -267,7 +266,7 @@ impl MeasurementPolicy { MultiMeasurements::Azure(azure_measurements) => { if let MultiMeasurements::Azure(a) = measurement_record.measurements.clone() { for (k, v) in azure_measurements.iter() { - if a[k] != *v { + if a.get(k).is_some_and(|x| x != v) { false; } } @@ -376,6 +375,8 @@ impl MeasurementPolicy { #[cfg(test)] mod tests { + use crate::test_helpers::default_dcap_measurements; + use super::*; #[tokio::test] @@ -385,51 +386,53 @@ mod tests { .await .unwrap(); - // assert!(specific_measurements - // .accepted_measurements - // .get(&AttestationType::DcapTdx) - // .unwrap() - // .is_some()); - // - // // Will not match mock measurements - // assert!(matches!( - // specific_measurements - // .check_measurement(AttestationType::DcapTdx, &mock_measurements()) - // .unwrap_err(), - // AttestationError::MeasurementsNotAccepted - // )); - // - // // Will not match another attestation type - // assert!(matches!( - // specific_measurements - // .check_measurement(AttestationType::None, &mock_measurements()) - // .unwrap_err(), - // AttestationError::AttestationTypeNotAccepted - // )); + assert_eq!(specific_measurements.accepted_measurements.len(), 1); + let m = &specific_measurements.accepted_measurements[0]; + if let MultiMeasurements::Dcap(d) = &m.measurements { + assert!(d.contains_key(&DcapMeasurementRegister::MRTD)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR0)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR1)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR2)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR3)); + } else { + panic!("Unexpected measurement type"); + } + + // Will not match mock measurements + assert!(matches!( + specific_measurements + .check_measurement(&default_dcap_measurements()) + .unwrap_err(), + AttestationError::MeasurementsNotAccepted + )); + + // Will not match another attestation type + assert!(matches!( + specific_measurements + .check_measurement(&MultiMeasurements::NoAttestation) + .unwrap_err(), + AttestationError::MeasurementsNotAccepted + )); } #[tokio::test] async fn test_read_measurements_file_non_specific() { - // let mock_measurements = mock_measurements(); // This specifies a particular attestation type, but not specific measurements let allowed_attestation_type = MeasurementPolicy::from_file("test-assets/measurements_2.json".into()) .await .unwrap(); - // allowed_attestation_type - // .check_measurement(AttestationType::DcapTdx, &mock_measurements) - // .unwrap(); - // - // assert!(allowed_attestation_type - // .accepted_measurements - // .get(&AttestationType::DcapTdx) - // .unwrap() - // .is_none()); - // - // // Will match mock measurements - // allowed_attestation_type - // .check_measurement(AttestationType::DcapTdx, &mock_measurements) - // .unwrap(); + allowed_attestation_type + .check_measurement(&default_dcap_measurements()) + .unwrap(); + + // Will not match another attestation type + assert!(matches!( + allowed_attestation_type + .check_measurement(&MultiMeasurements::NoAttestation) + .unwrap_err(), + AttestationError::MeasurementsNotAccepted + )); } } diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index aa50106..1142d30 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -295,8 +295,7 @@ impl AttestationVerifier { }; // Do a measurement / attestation type policy check - self.measurement_policy - .check_measurement(attestation_type, &measurements)?; + self.measurement_policy.check_measurement(&measurements)?; tracing::debug!("Verification successful"); Ok(Some(measurements)) diff --git a/src/lib.rs b/src/lib.rs index a3fb87f..e32a3f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -640,7 +640,6 @@ impl ProxyClient { // If we are in a CVM, provide an attestation let attestation = if attestation_generator.attestation_type != AttestationType::None { - println!("fff"); let local_input_data = compute_report_input(&cert_chain.ok_or(ProxyError::NoClientAuth)?, exporter)?; attestation_generator From 23e5157ed374daeb69b017aa0021c7988d35281c Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 9 Dec 2025 15:02:56 +0100 Subject: [PATCH 43/51] Clippy --- src/attestation/measurements.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index b64e0bc..53082bb 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -130,7 +130,7 @@ impl MultiMeasurements { pub fn from_pcrs<'a>(pcrs: impl Iterator) -> Self { Self::Azure( - pcrs.map(|p| p.clone()) + pcrs.copied() .enumerate() .map(|(index, value)| (index as u32, value)) .collect(), @@ -261,16 +261,16 @@ impl MeasurementPolicy { } return true; } - return false; + false } MultiMeasurements::Azure(azure_measurements) => { if let MultiMeasurements::Azure(a) = measurement_record.measurements.clone() { for (k, v) in azure_measurements.iter() { if a.get(k).is_some_and(|x| x != v) { - false; + return false; } } - true; + return true; } false } From 8f07e04b5dfd8ed29f852b8509323688ef35ef4b Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 9 Dec 2025 15:16:46 +0100 Subject: [PATCH 44/51] Use measurements test asset file from cvm-reverse-proxy --- src/attestation/measurements.rs | 22 +++++++++++++++++++++- test-assets/measurements.json | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index 53082bb..874f515 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -375,6 +375,8 @@ impl MeasurementPolicy { #[cfg(test)] mod tests { + use std::collections::HashSet; + use crate::test_helpers::default_dcap_measurements; use super::*; @@ -386,8 +388,26 @@ mod tests { .await .unwrap(); - assert_eq!(specific_measurements.accepted_measurements.len(), 1); + assert_eq!(specific_measurements.accepted_measurements.len(), 3); + let m = &specific_measurements.accepted_measurements[0]; + if let MultiMeasurements::Azure(a) = &m.measurements { + assert_eq!( + a.keys().collect::>(), + HashSet::from([&9, &4, &11]) + ); + } else { + panic!("Unexpected measurement type"); + } + + let m = &specific_measurements.accepted_measurements[1]; + if let MultiMeasurements::Azure(a) = &m.measurements { + assert_eq!(a.keys().collect::>(), HashSet::from([&9, &4])); + } else { + panic!("Unexpected measurement type"); + } + + let m = &specific_measurements.accepted_measurements[2]; if let MultiMeasurements::Dcap(d) = &m.measurements { assert!(d.contains_key(&DcapMeasurementRegister::MRTD)); assert!(d.contains_key(&DcapMeasurementRegister::RTMR0)); diff --git a/test-assets/measurements.json b/test-assets/measurements.json index f7714b7..7a9097e 100644 --- a/test-assets/measurements.json +++ b/test-assets/measurements.json @@ -1,4 +1,31 @@ [ + { + "measurement_id": "azure-tdx-example-01", + "attestation_type": "azure-tdx", + "measurements": { + "4": { + "expected": "ea92ff762767eae6316794f1641c485d4846bc2b9df2eab6ba7f630ce6f4d66f" + }, + "9": { + "expected": "c9f429296634072d1063a03fb287bed0b2d177b0a504755ad9194cffd90b2489" + }, + "11": { + "expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7" + } + } + }, + { + "measurement_id": "cvm-image-azure-tdx.rootfs-20241107200854.wic.vhd", + "attestation_type": "azure-tdx", + "measurements": { + "4": { + "expected": "1b8cd655f5ebdf50bedabfb5db6b896a0a7c56de54f318103a2de1e7cea57b6b" + }, + "9": { + "expected": "992465f922102234c196f596fdaba86ea16eaa4c264dc425ec26bc2d1c364472" + } + } + }, { "measurement_id": "dcap-tdx-example", "attestation_type": "dcap-tdx", From 67b3faf6a19c3e7319a24e3652890d61fec9c0ca Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 9 Dec 2025 15:24:37 +0100 Subject: [PATCH 45/51] Error handling --- src/attestation/measurements.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index 874f515..469215d 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -7,6 +7,7 @@ use http::{header::InvalidHeaderValue, HeaderValue}; use serde::Deserialize; use thiserror::Error; +/// Represents the measurement register types in a TDX quote #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[repr(u8)] pub enum DcapMeasurementRegister { @@ -18,7 +19,7 @@ pub enum DcapMeasurementRegister { } impl TryFrom for DcapMeasurementRegister { - type Error = (); + type Error = MeasurementFormatError; fn try_from(value: u8) -> Result { match value { @@ -27,11 +28,12 @@ impl TryFrom for DcapMeasurementRegister { 2 => Ok(Self::RTMR1), 3 => Ok(Self::RTMR2), 4 => Ok(Self::RTMR3), - _ => Err(()), + _ => Err(MeasurementFormatError::BadRegisterIndex), } } } +/// Represents a set of measurements values for one of the supported CVM platforms #[derive(Debug, Clone, PartialEq)] pub enum MultiMeasurements { Dcap(HashMap), @@ -86,7 +88,7 @@ impl MultiMeasurements { .into_iter() .map(|(k, v)| { Ok(( - k.try_into().unwrap(), + k.try_into()?, hex::decode(v)? .try_into() .map_err(|_| MeasurementFormatError::BadLength)?, @@ -155,6 +157,8 @@ pub enum MeasurementFormatError { Hex(#[from] hex::FromHexError), #[error("Expected 48 byte value")] BadLength, + #[error("TDX quote register index must be in the ranger 0-3")] + BadRegisterIndex, } /// An accepted measurement value given in the measurements file From 8fa63589726b60bc93de66ba3aad079a23c6d812 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 10 Dec 2025 13:04:00 +0100 Subject: [PATCH 46/51] Error handling --- src/attestation/measurements.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index 469215d..b28fb0e 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -159,6 +159,8 @@ pub enum MeasurementFormatError { BadLength, #[error("TDX quote register index must be in the ranger 0-3")] BadRegisterIndex, + #[error("ParseInt: {0}")] + ParseInt(#[from] std::num::ParseIntError), } /// An accepted measurement value given in the measurements file @@ -327,8 +329,7 @@ impl MeasurementPolicy { for measurement in measurements_simple { let attestation_type = - serde_json::from_value(serde_json::Value::String(measurement.attestation_type)) - .unwrap(); + serde_json::from_value(serde_json::Value::String(measurement.attestation_type))?; if let Some(measurements) = measurement.measurements { let multi_measurement = match attestation_type { @@ -337,8 +338,10 @@ impl MeasurementPolicy { .into_iter() .map(|(index, entry)| { Ok(( - index.parse().unwrap(), - hex::decode(entry.expected)?.try_into().unwrap(), + index.parse()?, + hex::decode(entry.expected)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, )) }) .collect::, MeasurementFormatError>>()?; @@ -349,10 +352,12 @@ impl MeasurementPolicy { measurements .into_iter() .map(|(index, entry)| { - let index: u8 = index.parse().unwrap(); + let index: u8 = index.parse()?; Ok(( - DcapMeasurementRegister::try_from(index).unwrap(), - hex::decode(entry.expected)?.try_into().unwrap(), + DcapMeasurementRegister::try_from(index)?, + hex::decode(entry.expected)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, )) }) .collect:: Date: Wed, 10 Dec 2025 13:33:50 +0100 Subject: [PATCH 47/51] Add check for register index value --- src/attestation/azure/mod.rs | 28 +++++++++++++++++++++++++++- src/attestation/measurements.rs | 8 +++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs index 3163a79..173f95b 100644 --- a/src/attestation/azure/mod.rs +++ b/src/attestation/azure/mod.rs @@ -299,6 +299,8 @@ pub enum MaaError { #[cfg(test)] mod tests { + use crate::attestation::measurements::MeasurementPolicy; + use super::*; #[tokio::test] @@ -329,7 +331,29 @@ mod tests { // timestamp let now = 1764621240; - verify_azure_attestation_with_given_timestamp( + let measurements_json = br#" + [{ + "measurement_id": "cvm-image-azure-tdx.rootfs-20241107200854.wic.vhd", + "attestation_type": "azure-tdx", + "measurements": { + "4": { + "expected": "c4a25a6d7704629f63db84d20ea8db0e9ce002b2801be9a340091fe7ac588699" + }, + "9": { + "expected": "9f4a5775122ca4703e135a9ae6041edead0064262e399df11ca85182b0f1541d" + }, + "11": { + "expected": "abd7c695ffdb6081e99636ee016d1322919c68d049b698b399d22ae215a121bf" + } + } + }] + "#; + + let measurement_policy = MeasurementPolicy::from_json_bytes(measurements_json.to_vec()) + .await + .unwrap(); + + let measurements = verify_azure_attestation_with_given_timestamp( attestation_bytes.to_vec(), [0; 64], // Input data None, @@ -337,5 +361,7 @@ mod tests { ) .await .unwrap(); + + measurement_policy.check_measurement(&measurements).unwrap(); } } diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index b28fb0e..94bd7c1 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -337,8 +337,14 @@ impl MeasurementPolicy { let azure_measurements = measurements .into_iter() .map(|(index, entry)| { + let index = index.parse()?; + + if index > 23 { + return Err(MeasurementFormatError::BadRegisterIndex); + } + Ok(( - index.parse()?, + index, hex::decode(entry.expected)? .try_into() .map_err(|_| MeasurementFormatError::BadLength)?, From 88dbbe1a5c7bd77ccea19859961c66b1e18eed50 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 10 Dec 2025 13:50:41 +0100 Subject: [PATCH 48/51] Rename function, tidy --- src/attestation/measurements.rs | 6 +++--- src/lib.rs | 18 +++++++++--------- src/test_helpers.rs | 22 ++++++---------------- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index 94bd7c1..53038dd 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -392,7 +392,7 @@ impl MeasurementPolicy { mod tests { use std::collections::HashSet; - use crate::test_helpers::default_dcap_measurements; + use crate::test_helpers::mock_dcap_measurements; use super::*; @@ -436,7 +436,7 @@ mod tests { // Will not match mock measurements assert!(matches!( specific_measurements - .check_measurement(&default_dcap_measurements()) + .check_measurement(&mock_dcap_measurements()) .unwrap_err(), AttestationError::MeasurementsNotAccepted )); @@ -459,7 +459,7 @@ mod tests { .unwrap(); allowed_attestation_type - .check_measurement(&default_dcap_measurements()) + .check_measurement(&mock_dcap_measurements()) .unwrap(); // Will not match another attestation type diff --git a/src/lib.rs b/src/lib.rs index e32a3f8..aa5da73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -861,8 +861,8 @@ mod tests { use super::*; use test_helpers::{ - default_dcap_measurements, example_http_service, example_service, - generate_certificate_chain, generate_tls_config, generate_tls_config_with_client_auth, + example_http_service, generate_certificate_chain, generate_tls_config, + generate_tls_config_with_client_auth, mock_dcap_measurements, }; // Server has mock DCAP, client has no attestation and no client auth @@ -924,7 +924,7 @@ mod tests { let measurements = MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) .unwrap(); - assert_eq!(measurements, default_dcap_measurements()); + assert_eq!(measurements, mock_dcap_measurements()); let res_body = res.text().await.unwrap(); assert_eq!(res_body, "No measurements"); @@ -1008,7 +1008,7 @@ mod tests { // handler puts them there) let measurements = MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); - assert_eq!(measurements, default_dcap_measurements()); + assert_eq!(measurements, mock_dcap_measurements()); } // Server has mock DCAP, client has mock DCAP and client auth @@ -1077,7 +1077,7 @@ mod tests { let measurements = MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) .unwrap(); - assert_eq!(measurements, default_dcap_measurements()); + assert_eq!(measurements, mock_dcap_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1092,7 +1092,7 @@ mod tests { // handler puts them there) let measurements = MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); - assert_eq!(measurements, default_dcap_measurements()); + assert_eq!(measurements, mock_dcap_measurements()); // Now do another request - to check that the connection has stayed open let res = reqwest::get(format!("http://{}", proxy_client_addr.to_string())) @@ -1104,7 +1104,7 @@ mod tests { let measurements = MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) .unwrap(); - assert_eq!(measurements, default_dcap_measurements()); + assert_eq!(measurements, mock_dcap_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1119,13 +1119,13 @@ mod tests { // handler puts them there) let measurements = MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); - assert_eq!(measurements, default_dcap_measurements()); + assert_eq!(measurements, mock_dcap_measurements()); } // Server has mock DCAP, client no attestation - just get the server certificate #[tokio::test] async fn test_get_tls_cert() { - let target_addr = example_service().await; + let target_addr = example_http_service().await; let (cert_chain, private_key) = generate_certificate_chain("127.0.0.1".parse().unwrap()); let (server_config, client_config) = generate_tls_config(cert_chain.clone(), private_key); diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 45b2146..b783dff 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -4,7 +4,6 @@ use std::{ net::{IpAddr, SocketAddr}, sync::Arc, }; -use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio_rustls::rustls::{ pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, @@ -121,6 +120,8 @@ pub fn generate_tls_config_with_client_auth( ) } +/// Given a TLS certificate, return a [WebPkiClientVerifier] and [RootCertStore] which will accept +/// that certificate fn client_verifier_from_remote_cert( cert: CertificateDer<'static>, ) -> (Arc, RootCertStore) { @@ -135,6 +136,8 @@ fn client_verifier_from_remote_cert( ) } +/// Simple http server used in tests which returns in the response the measurement header from the +/// request pub async fn example_http_service() -> SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -156,21 +159,8 @@ async fn get_handler(headers: http::HeaderMap) -> impl IntoResponse { .to_string() } -pub async fn example_service() -> SocketAddr { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - tokio::spawn(async move { - loop { - let (mut inbound, _client_addr) = listener.accept().await.unwrap(); - inbound.write_all(b"some data").await.unwrap(); - } - }); - - addr -} - -pub fn default_dcap_measurements() -> MultiMeasurements { +/// All-zero measurment values used in some tests +pub fn mock_dcap_measurements() -> MultiMeasurements { MultiMeasurements::Dcap(HashMap::from([ (DcapMeasurementRegister::MRTD, [0u8; 48]), (DcapMeasurementRegister::RTMR0, [0u8; 48]), From 2740f4904b4c9e1ad94279c23b9a13e3cfa0e8d6 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 10 Dec 2025 19:05:45 +0100 Subject: [PATCH 49/51] Update readme --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f097980..7bb0da8 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Header value: an attestation type given as a string as described below. These are the attestation type names used in the HTTP headers, and the measurements file, and when specifying a local attestation type with the `--client-attestation-type` or `--server-attestation-type` command line options. +- `auto` - detect attestation type (used only when specifying the local attestation type as a command-line argument) - `none` - No attestation provided - `dummy` - Forwards the attestation to a remote service (for testing purposes, not yet supported) - `gcp-tdx` - DCAP TDX on Google Cloud Platform @@ -138,7 +139,13 @@ Following a successful attestation exchange, the client can make HTTP requests u As described above, the server will inject measurement data into the request headers before forwarding them to the target service, and the client will inject measurement data into the response headers before forwarding them to the source client. -### CLI differences from `cvm-reverse-proxy` +## Dependencies and feature flags + +The `azure` feature, for Microsoft Azure attestation requires [tpm2](https://tpm2-software.github.io) to be installed. On Debian-based systems this is provided by [`libtss2-dev`](https://packages.debian.org/trixie/libtss2-dev), and on nix `tpm2-tss`. + +This feature is enabled by default. For non-azure deployments you can compile without this requirement by specifying `--no-default-features`. But note that this is will disabled both generation and verification of azure attestations. + +## CLI differences from `cvm-reverse-proxy` This aims to have a similar command line interface to `cvm-reverse-proxy` but there are some differences: From f275c39cfbde05419810eefe570f5526d880a81e Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 10 Dec 2025 19:08:02 +0100 Subject: [PATCH 50/51] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bb0da8..d09df01 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ As described above, the server will inject measurement data into the request hea The `azure` feature, for Microsoft Azure attestation requires [tpm2](https://tpm2-software.github.io) to be installed. On Debian-based systems this is provided by [`libtss2-dev`](https://packages.debian.org/trixie/libtss2-dev), and on nix `tpm2-tss`. -This feature is enabled by default. For non-azure deployments you can compile without this requirement by specifying `--no-default-features`. But note that this is will disabled both generation and verification of azure attestations. +This feature is enabled by default. For non-azure deployments you can compile without this requirement by specifying `--no-default-features`. But note that this is will disable both generation and verification of azure attestations. ## CLI differences from `cvm-reverse-proxy` From 7a4e23d5565d27cc6d775fcfea782b0b811a338d Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 10 Dec 2025 19:09:49 +0100 Subject: [PATCH 51/51] Rm logging --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index aa5da73..a29493e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,11 +88,9 @@ impl ProxyServer { attestation_verifier: AttestationVerifier, client_auth: bool, ) -> Result { - println!("here"); if attestation_verifier.has_remote_attestion() && !client_auth { return Err(ProxyError::NoClientAuth); } - println!("here2"); let mut server_config = if client_auth { let root_store =