From 2617ef19f53bf706738a53bbf2f5bbb4cb79653e Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 12 Nov 2025 10:07:07 +0100 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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 962e28c8bce315bcd4939abd8ba408109003dade Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 26 Nov 2025 09:30:53 +0100 Subject: [PATCH 17/22] Add dummy attestation server/client --- Cargo.lock | 24 +++++ Cargo.toml | 3 + dummy-attestation-server/Cargo.toml | 28 ++++++ dummy-attestation-server/src/lib.rs | 131 +++++++++++++++++++++++++++ dummy-attestation-server/src/main.rs | 76 ++++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 dummy-attestation-server/Cargo.toml create mode 100644 dummy-attestation-server/src/lib.rs create mode 100644 dummy-attestation-server/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 2af1c6b..5d758a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -655,6 +655,30 @@ dependencies = [ "syn", ] +[[package]] +name = "dummy-attestation-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "attested-tls-proxy", + "axum", + "clap", + "configfs-tsm", + "hex", + "parity-scale-codec", + "rcgen", + "reqwest", + "rustls-pemfile", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-rustls", + "tracing", + "tracing-subscriber", + "webpki-roots", +] + [[package]] name = "ecdsa" version = "0.16.9" diff --git a/Cargo.toml b/Cargo.toml index 4c62518..a5e7085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = [".", "dummy-attestation-server"] + [package] name = "attested-tls-proxy" version = "0.1.0" diff --git a/dummy-attestation-server/Cargo.toml b/dummy-attestation-server/Cargo.toml new file mode 100644 index 0000000..10c5379 --- /dev/null +++ b/dummy-attestation-server/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "dummy-attestation-server" +version = "0.1.0" +edition = "2024" +license = "MIT" +publish = false + +[dependencies] +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" +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 } + +[dev-dependencies] diff --git a/dummy-attestation-server/src/lib.rs b/dummy-attestation-server/src/lib.rs new file mode 100644 index 0000000..9181b44 --- /dev/null +++ b/dummy-attestation-server/src/lib.rs @@ -0,0 +1,131 @@ +use std::{ + net::{IpAddr, SocketAddr}, + sync::Arc, +}; + +use attested_tls_proxy::{attestation::AttestationExchangeMessage, QuoteGenerator}; +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; +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, +} + +pub async fn dummy_attestation_server( + listener: TcpListener, + attestation_generator: Arc, +) -> anyhow::Result { + let addr = listener.local_addr()?; + + 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(); + }); + + Ok(addr) +} + +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(); + + Ok((StatusCode::OK, attestation)) +} + +pub async fn dummy_attestation_client(server_addr: SocketAddr) -> anyhow::Result<()> { + let input_data = [0; 64]; + let response = reqwest::get(format!( + "http://{server_addr}/attest/{}", + hex::encode(input_data) + )) + .await + .unwrap() + .bytes() + .await + .unwrap(); + + 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(()) +} + +struct ServerError(pub anyhow::Error); + +impl From for ServerError +where + E: Into, +{ + fn from(err: E) -> Self { + ServerError(err.into()) + } +} + +impl IntoResponse for ServerError { + fn into_response(self) -> Response { + eprintln!("{:?}", self.0); + (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response() + } +} + +/// 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 { + + use attested_tls_proxy::attestation::AttestationType; + + use super::*; + + #[tokio::test] + async fn test_dummy_server() { + let attestation_generator = AttestationType::None.get_quote_generator().unwrap(); + + 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(); + } +} diff --git a/dummy-attestation-server/src/main.rs b/dummy-attestation-server/src/main.rs new file mode 100644 index 0000000..fe1946d --- /dev/null +++ b/dummy-attestation-server/src/main.rs @@ -0,0 +1,76 @@ +use attested_tls_proxy::attestation::AttestationType; +use clap::{Parser, Subcommand}; +use dummy_attestation_server::{dummy_attestation_client, dummy_attestation_server}; +use std::net::SocketAddr; +use tokio::net::TcpListener; +use tracing::level_filters::LevelFilter; + +#[derive(Parser, Debug, Clone)] +#[clap(version, about, long_about = None)] +struct Cli { + #[clap(subcommand)] + command: CliCommand, + /// Log debug messages + #[arg(long, global = true)] + log_debug: bool, + /// Log in JSON format + #[arg(long, global = true)] + log_json: bool, +} +#[derive(Subcommand, Debug, Clone)] +enum CliCommand { + Server { + /// Socket address to listen on + #[arg(short, long, default_value = "0.0.0.0:0", env = "LISTEN_ADDR")] + listen_addr: SocketAddr, + /// Type of attestation to present (defaults to none) + #[arg(long)] + server_attestation_type: Option, + }, + Client { + /// Socket address of a dummy attestation server + server_addr: SocketAddr, + }, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let level_filter = if cli.log_debug { + LevelFilter::DEBUG + } else { + LevelFilter::WARN + }; + + let env_filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(level_filter.into()) + .from_env_lossy(); + + let subscriber = tracing_subscriber::fmt::Subscriber::builder().with_env_filter(env_filter); + + if cli.log_json { + subscriber.json().init(); + } else { + subscriber.pretty().init(); + } + + match cli.command { + CliCommand::Server { + listen_addr, + server_attestation_type, + } => { + let server_attestation_type: AttestationType = serde_json::from_value( + serde_json::Value::String(server_attestation_type.unwrap_or("none".to_string())), + )?; + + let attestation_generator = server_attestation_type.get_quote_generator()?; + + let listener = TcpListener::bind(listen_addr).await?; + dummy_attestation_server(listener, attestation_generator).await?; + } + CliCommand::Client { server_addr } => dummy_attestation_client(server_addr).await?, + } + + Ok(()) +} From 3392f22523fdcecad426ea052773d1330b970d1d Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 26 Nov 2025 10:15:44 +0100 Subject: [PATCH 18/22] 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 19/22] 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 20/22] 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 21/22] 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 22/22] 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;