diff --git a/crates/attestation/src/azure/ak_certificate.rs b/crates/attestation/src/azure/ak_certificate.rs index cc2264e..ae637e6 100644 --- a/crates/attestation/src/azure/ak_certificate.rs +++ b/crates/attestation/src/azure/ak_certificate.rs @@ -1,9 +1,10 @@ //! Generation and verification of AK certificates from the vTPM -use std::time::Duration; +use std::{io::Read, time::Duration}; use once_cell::sync::Lazy; use tokio_rustls::rustls::pki_types::{CertificateDer, TrustAnchor, UnixTime}; use webpki::EndEntityCert; +use x509_parser::{extensions::GeneralName, prelude::*}; use crate::azure::{MaaError, nv_index}; @@ -30,8 +31,13 @@ const AZURE_VIRTUAL_TPM_ROOT_2023: &str = // Valid: 2025-04-24 to 2027-04-24 const GLOBAL_VIRTUAL_TPMCA03_PEM: &str = include_str!("../../assets/global-virtual-tpm-ca-03.pem"); -/// The intermediate chain for azure -static GLOBAL_VIRTUAL_TPMCA03: Lazy>> = Lazy::new(|| { +/// Azure intermediate certificates bundled with this crate. +/// +/// This is kept only for backwards compatibility with older evidence that +/// did not serialize AIA-fetched intermediates. New Azure vTPM evidence +/// should carry the AK issuer chain fetched from AIA instead of relying on +/// this bundled, eventually-stale list. +static BUNDLED_AZURE_INTERMEDIATES: Lazy>> = Lazy::new(|| { let (_type_label, cert_der) = pem_rfc7468::decode_vec(GLOBAL_VIRTUAL_TPMCA03_PEM.as_bytes()).expect("Cannot decode PEM"); vec![CertificateDer::from(cert_der)] @@ -50,17 +56,21 @@ static AZURE_ROOT_ANCHORS: Lazy>> = Lazy::new(|| { /// Verify an AK certificate against azure root CA pub(crate) fn verify_ak_cert_with_azure_roots( ak_cert_der: &[u8], + intermediate_cert_ders: &[Vec], now_secs: u64, ) -> Result<(), MaaError> { let ak_cert_der: CertificateDer = ak_cert_der.into(); let end_entity_cert = EndEntityCert::try_from(&ak_cert_der)?; + let mut intermediates = BUNDLED_AZURE_INTERMEDIATES.clone(); + intermediates.extend(intermediate_cert_ders.iter().cloned().map(CertificateDer::from)); + let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs)); end_entity_cert.verify_for_usage( webpki::ALL_VERIFICATION_ALGS, &AZURE_ROOT_ANCHORS, - &GLOBAL_VIRTUAL_TPMCA03, + &intermediates, now, AnyEku, None, @@ -71,6 +81,47 @@ pub(crate) fn verify_ak_cert_with_azure_roots( Ok(()) } +/// Fetch intermediate certificates from the Authority Information Access +/// (AIA) CA Issuers URLs in the leaf and each fetched intermediate. +/// +/// Azure vTPM AK intermediate CAs rotate and the public Trusted Launch FAQ +/// can lag behind the certificates observed in production. Microsoft +/// guidance is to build the chain from the CA Issuers URLs embedded in the +/// AK certificate's AIA extension; see: +/// https://learn.microsoft.com/en-us/answers/questions/5897616/download-intermediate-ca-cert-for-azure-cloud-virt +/// +/// The fetched certificates are untrusted evidence. Verification still pins +/// the Azure vTPM root in `verify_ak_cert_with_azure_roots`. +pub(crate) fn fetch_ak_intermediates_from_aia( + ak_cert: &X509Certificate<'_>, +) -> Result>, MaaError> { + const MAX_AIA_DEPTH: usize = 6; + + let mut intermediates = Vec::new(); + let mut issuer_url = first_ca_issuers_url(ak_cert); + + for _ in 0..MAX_AIA_DEPTH { + let Some(url) = issuer_url else { + break; + }; + + tracing::debug!("Fetching Azure vTPM AK issuer certificate from {url}"); + let issuer_der = fetch_certificate_der(&url)?; + let (_, issuer_cert) = X509Certificate::from_der(&issuer_der)?; + + // Stop before adding the self-signed root. The root is already pinned + // in AZURE_ROOT_ANCHORS and should not come from untrusted evidence. + if issuer_cert.subject() == issuer_cert.issuer() { + break; + } + + issuer_url = first_ca_issuers_url(&issuer_cert); + intermediates.push(issuer_der); + } + + Ok(intermediates) +} + /// Retrieve an AK certificate from the vTPM pub(crate) fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { tracing::debug!("Reading AK certificate from vTPM"); @@ -78,6 +129,47 @@ pub(crate) fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) } +fn first_ca_issuers_url(cert: &X509Certificate<'_>) -> Option { + cert.extensions().iter().find_map(|extension| { + let ParsedExtension::AuthorityInfoAccess(aia) = extension.parsed_extension() else { + return None; + }; + + aia.iter().find_map(|desc| { + if desc.access_method.to_id_string() != "1.3.6.1.5.5.7.48.2" { + return None; + } + + let GeneralName::URI(uri) = &desc.access_location else { + return None; + }; + + Some((*uri).to_string()) + }) + }) +} + +fn fetch_certificate_der(url: &str) -> Result, MaaError> { + if !(url.starts_with("http://") || url.starts_with("https://")) { + return Err(MaaError::UnsupportedAiaUrl { url: url.to_string() }); + } + + let response = ureq::get(url) + .timeout(Duration::from_secs(10)) + .call() + .map_err(|err| MaaError::AiaFetch { url: url.to_string(), source: err })?; + + let mut bytes = Vec::new(); + response.into_reader().take(1024 * 1024).read_to_end(&mut bytes)?; + + if bytes.starts_with(b"-----BEGIN") { + let (_type_label, der) = pem_rfc7468::decode_vec(&bytes)?; + Ok(der) + } else { + Ok(bytes) + } +} + /// Convert a PEM-encoded cert into a TrustAnchor fn pem_to_trust_anchor(pem: &str) -> TrustAnchor<'static> { let (_type_label, der_vec) = pem_rfc7468::decode_vec(pem.as_bytes()).unwrap(); @@ -99,6 +191,44 @@ impl webpki::ExtendedKeyUsageValidator for AnyEku { } } +#[cfg(test)] +#[test] +fn azure_ak_fixture_has_expected_aia_issuer_url() { + let (_type_label, ak_der) = pem_rfc7468::decode_vec( + include_bytes!("../../test-assets/azure-ak-leaf-westus3.pem").as_slice(), + ) + .unwrap(); + let (_, cert) = X509Certificate::from_der(&ak_der).unwrap(); + + assert_eq!( + first_ca_issuers_url(&cert).as_deref(), + Some( + "http://primary-cdn.pki.core.windows.net/westus3/cacerts/azurevtpmicapki/azurevtpmicausw3/cert.cer" + ) + ); +} + +#[cfg(test)] +#[test] +fn verifies_azure_ak_fixture_with_fixture_intermediates() { + let (_type_label, ak_der) = pem_rfc7468::decode_vec( + include_bytes!("../../test-assets/azure-ak-leaf-westus3.pem").as_slice(), + ) + .unwrap(); + let intermediates = [ + include_bytes!("../../test-assets/azure-cloud-virtual-tpm-ca-24.pem").as_slice(), + include_bytes!("../../test-assets/azure-cloud-virtual-tpm-ca-2025.pem").as_slice(), + ] + .into_iter() + .map(|pem| pem_rfc7468::decode_vec(pem).unwrap().1) + .collect::>(); + + // Fixed timestamp within the leaf and intermediate validity windows, so + // this offline fixture test does not expire when wall-clock time advances. + let june_5_2026 = 1_780_617_600; + verify_ak_cert_with_azure_roots(&ak_der, &intermediates, june_5_2026).unwrap(); +} + #[cfg(test)] #[tokio::test] async fn root_should_be_fresh() { diff --git a/crates/attestation/src/azure/mod.rs b/crates/attestation/src/azure/mod.rs index 45d4e4c..d7ce46b 100644 --- a/crates/attestation/src/azure/mod.rs +++ b/crates/attestation/src/azure/mod.rs @@ -3,7 +3,9 @@ mod ak_certificate; mod nv_index; use std::time::Duration; -use ak_certificate::{read_ak_certificate_from_tpm, verify_ak_cert_with_azure_roots}; +use ak_certificate::{ + fetch_ak_intermediates_from_aia, read_ak_certificate_from_tpm, verify_ak_cert_with_azure_roots, +}; use az_tdx_vtpm::{hcl, imds, vtpm}; use base64::{Engine as _, engine::general_purpose::URL_SAFE as BASE64_URL_SAFE}; use dcap_qvl::QuoteCollateralV3; @@ -17,8 +19,7 @@ use x509_parser::prelude::*; use crate::{ dcap::{ - verify_dcap_attestation_with_given_timestamp, - verify_dcap_attestation_with_timestamp_sync, + verify_dcap_attestation_with_given_timestamp, verify_dcap_attestation_with_timestamp_sync, }, measurements::MultiMeasurements, }; @@ -42,6 +43,11 @@ struct AttestationDocument { struct TpmAttest { /// Attestation Key certificate from vTPM ak_certificate_pem: String, + /// Intermediate CA certificates fetched from the AK leaf certificate's + /// AIA CA Issuers URLs. These are untrusted evidence; verification + /// pins the Azure vTPM root CA. + #[serde(default, deserialize_with = "deserialize_ak_intermediate_certificates_pem")] + ak_intermediate_certificates_pem: Vec, /// vTPM quote quote: vtpm::Quote, /// Raw TCG event log bytes (UEFI + IMA) [currently not used] @@ -54,6 +60,29 @@ struct TpmAttest { instance_info: Option>, } +/// Maximum number of evidence-supplied AK intermediate certificates +/// accepted during verification. Azure chains currently observed use 2 +/// intermediates; this allows some rotation/cross-signing headroom while +/// bounding peer-controlled evidence. +const MAX_AK_INTERMEDIATE_CERTIFICATES: usize = 4; + +fn deserialize_ak_intermediate_certificates_pem<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let certificates = Vec::::deserialize(deserializer)?; + if certificates.len() > MAX_AK_INTERMEDIATE_CERTIFICATES { + return Err(serde::de::Error::custom(format_args!( + "too many AK intermediate certificates in evidence: {} > {}", + certificates.len(), + MAX_AK_INTERMEDIATE_CERTIFICATES + ))); + } + Ok(certificates) +} + /// Used during verification to support both sync and async verification /// paths without duplicating code struct PreparedAzureAttestation { @@ -77,13 +106,23 @@ pub fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaErro let td_quote_bytes = imds::get_td_quote(&td_report_from_hcl)?; let ak_certificate_der = read_ak_certificate_from_tpm()?; + let (remaining_bytes, ak_leaf_certificate) = X509Certificate::from_der(&ak_certificate_der)?; + let leaf_len = ak_certificate_der.len() - remaining_bytes.len(); + let ak_leaf_certificate_der = &ak_certificate_der[..leaf_len]; + let ak_intermediate_certificates_der = fetch_ak_intermediates_from_aia(&ak_leaf_certificate)?; let tpm_attestation = TpmAttest { ak_certificate_pem: pem_rfc7468::encode_string( "CERTIFICATE", pem_rfc7468::LineEnding::default(), - &ak_certificate_der, + ak_leaf_certificate_der, )?, + ak_intermediate_certificates_pem: ak_intermediate_certificates_der + .iter() + .map(|der| { + pem_rfc7468::encode_string("CERTIFICATE", pem_rfc7468::LineEnding::default(), der) + }) + .collect::, _>>()?, quote: vtpm::get_quote(&input_data[..32])?, event_log: Vec::new(), instance_info: None, @@ -296,8 +335,15 @@ fn finish_azure_attestation_verification( // Parse AK certificate let (_type_label, ak_certificate_der) = pem_rfc7468::decode_vec(tpm_attestation.ak_certificate_pem.as_bytes())?; + let ak_intermediate_certificate_ders = tpm_attestation + .ak_intermediate_certificates_pem + .iter() + .map(|pem| pem_rfc7468::decode_vec(pem.as_bytes()).map(|(_type_label, der)| der)) + .collect::, _>>()?; let (remaining_bytes, ak_certificate) = X509Certificate::from_der(&ak_certificate_der)?; + let leaf_len = ak_certificate_der.len() - remaining_bytes.len(); + let ak_leaf_certificate_der = &ak_certificate_der[..leaf_len]; // Check that AK public key matches that from TPM quote and HCL claims let ak_from_certificate = RsaPubKey::from_certificate(&ak_certificate)?; @@ -309,12 +355,12 @@ fn finish_azure_attestation_verification( return Err(MaaError::AkFromClaimsNotEqualAkFromCertificate); } - // Strip trailing data from AK certificate - let leaf_len = ak_certificate_der.len() - remaining_bytes.len(); - let ak_certificate_der_without_trailing_data = &ak_certificate_der[..leaf_len]; - // Verify the AK certificate against microsoft root cert - verify_ak_cert_with_azure_roots(ak_certificate_der_without_trailing_data, now)?; + verify_ak_cert_with_azure_roots( + ak_leaf_certificate_der, + &ak_intermediate_certificate_ders, + now, + )?; Ok(MultiMeasurements::from_pcrs(pcrs)) } @@ -452,6 +498,12 @@ pub enum MaaError { Json(#[from] serde_json::Error), #[error("HTTP client: {0}")] Reqwest(#[from] reqwest::Error), + #[error("AIA URL is not HTTP(S): {url}")] + UnsupportedAiaUrl { url: String }, + #[error("Failed to fetch AIA issuer certificate from {url}: {source}")] + AiaFetch { url: String, source: ureq::Error }, + #[error("IO: {0}")] + Io(#[from] std::io::Error), #[error("vTPM quote: {0}")] VtpmQuote(#[from] vtpm::QuoteError), #[error("AK public key: {0}")] diff --git a/crates/attestation/test-assets/azure-ak-leaf-westus3.pem b/crates/attestation/test-assets/azure-ak-leaf-westus3.pem new file mode 100644 index 0000000..2521228 --- /dev/null +++ b/crates/attestation/test-assets/azure-ak-leaf-westus3.pem @@ -0,0 +1,88 @@ +-----BEGIN CERTIFICATE----- +MIIGSDCCBDCgAwIBAgIRAMWpePCJCtDYocQgBibR+eMwDQYJKoZIhvcNAQENBQAw +KjEoMCYGA1UEAxMfQXp1cmUgQ2xvdWQgVmlydHVhbCBUUE0gQ0EgLSAyNDAeFw0y +NjA2MDQwMDAwMDBaFw0yNzA2MDMwMDAwMDBaMDgxNjA0BgNVBAMTLTk5MjE2OTRi +ZDM0Mi5Db25maWRlbnRpYWxWTS5BenVyZS53aW5kb3dzLm5ldDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANdGVyQAAEvbQQ5BsnYyEazDkqx30wMhgBw2 +Dg2GijvnOF2i/EdHhz8em5Xs2oZ3L+VMI+Edh7HpVsgrWwhKGV3LE0LbYANxgPqY +hPPKB5+WaMklX5aeXHkSUqGMEl/eKO0TAvJkCuLxtSrvVyUCZzQIDZ5/mJJVRv/D +kIEiIkLAvrJg6SoH/+MUb4CMBkMyz3vQTreRH2EmZG3+9kO7Pimc8cbntvILAXSe ++bw3EWkRuLjkFAa7eeqe7jUR5IqdAOy+zfiB2dklFTmL5v+49tj56+yBcWFFA8cI +YtBjEzeyLPHBSSYwaxgZgOtOgWM8kwR7B8eLN13EMDCdT6AntQMCAwEAAaOCAlkw +ggJVMBgGA1UdIAQRMA8wDQYLKwYBBAGCN2yBSAIwDAYDVR0TAQH/BAIwADAcBgNV +HSUEFTATBgorBgEEAYI3CgMMBgVngQUIAzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0O +BBYEFKlL0CakAru/SLwTZP44cqbA53+GMB8GA1UdIwQYMBaAFENsulav0M/GpLkV +lVnXYEXzSDJaMIIBuwYIKwYBBQUHAQEEggGtMIIBqTBtBggrBgEFBQcwAoZhaHR0 +cDovL3ByaW1hcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L3dlc3R1czMvY2Fj +ZXJ0cy9henVyZXZ0cG1pY2Fwa2kvYXp1cmV2dHBtaWNhdXN3My9jZXJ0LmNlcjBv +BggrBgEFBQcwAoZjaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93 +cy5uZXQvd2VzdHVzMy9jYWNlcnRzL2F6dXJldnRwbWljYXBraS9henVyZXZ0cG1p +Y2F1c3czL2NlcnQuY2VyMF4GCCsGAQUFBzAChlJodHRwOi8vY3JsLm1pY3Jvc29m +dC5jb20vd2VzdHVzMy9jYWNlcnRzL2F6dXJldnRwbWljYXBraS9henVyZXZ0cG1p +Y2F1c3czL2NlcnQuY2VyMGcGCCsGAQUFBzAChltodHRwOi8vYXp1cmV2dHBtaWNh +cGtpLndlc3R1czMucGtpLmNvcmUud2luZG93cy5uZXQvY2VydGlmaWNhdGVBdXRo +b3JpdGllcy9henVyZXZ0cG1pY2F1c3czMA0GCSqGSIb3DQEBDQUAA4ICAQA3Oenp +LqPeFIZ59il6nJ6rfGAaVtDm8qoVKybQ6K/LlGMd9KsA1vMJoe9aadOpN5b5DAna +PiM7dnB24+XKP4l8z8FYzjP4hmUjL4dKc8u8rB5b7Cn8uc//wIPWzwilOOrIITo8 +U/qIdyPhOqxBqxYPShtw77fjNiDTEivTD0lrbG9J7tp2NIREFTGt7FBZL32ZhuPq +x1eaDhGG+96b9jqtJGn+bYEaWtLyjbE2zu+S5oWU531C9rPbvtU4GP5xgRfFRw3Q +ZfzzLaGK5diVEhoIhfgu3N2Vk3qg5FHNFhUzzvkec85SEaeXFt8q9jNnCp2qkwUM +T55W2DNd+TjVA2QzAcgXldM64oLqRtkSxrrPO7PvixHQj7EQlTpdGyN239trCw3v +bWZzB0R0wuwCW1S6LDtzWVhQaD59NtV+G6tdELtZZLw14X+IHaHqvisGpq+Yik59 +ZTu+vWJnjPGP5Ps7aqlq//Pbtlir4Yv50KYirVeFuoBfI+dq20HdXHrqivvQ/zJG +fiMHysGuWo5CdpUpm/HEvuZA4/3rgpXYZTlXLWoQUmyfZMzbw+GOaJ0WHrrQCi03 +1jyWZM9biXwG+0Gm5TJCRG0ZfPQcUKvN8ilN4VtTseojnV27OEQBXzWSlWLrjv7Q +abM8e9Z9usVtV/4hvm/c2M6VWbkBRUwAYNqtlwAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAA== +-----END CERTIFICATE----- diff --git a/crates/attestation/test-assets/azure-cloud-virtual-tpm-ca-2025.pem b/crates/attestation/test-assets/azure-cloud-virtual-tpm-ca-2025.pem new file mode 100644 index 0000000..a24fea9 --- /dev/null +++ b/crates/attestation/test-assets/azure-cloud-virtual-tpm-ca-2025.pem @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIGqjCCBJKgAwIBAgITMwAAAA3hTgyMJlm7XAAAAAAADTANBgkqhkiG9w0BAQwF +ADBpMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTowOAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAyMDIzMB4XDTI1MDgwNjIxMDIwMloXDTMxMDgwNTIxMDIwMlowKjEo +MCYGA1UEAxMfQXp1cmUgQ2xvdWQgVmlydHVhbCBUUE0gQ0EgMjAyNTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAIa4QCuPJi5ehBDiXG6HYkmy17bgAtrQ +mK/+Ik9MHnmZoRw+Th08dgXZRNZf5A73WCC78r9O1hLqdiOIGf19Ejf8UxU6cRXj +UPpkn1Gfru8G/Rn488R2sJ/OJIncTKbJFVUNEt6HhABXRz/I6xlYLpXoU0HcgfB4 +Cw+8h/jQsXqnVXWrMmjQJ6+DgJR2Yk32zi75NY5Z6j3L0he+nO9P/uZ6ZSLOn/Lj +ftoPVs26Ay6Q/h7bJ7wtcC06k8qHxplimiWrEtNU7A74PaUGWZfgFXeOrUZXSURb +YxqbCTW6isbvS2D7IkNq/OasRVEUY3gNXFu3g8d08JCmvSY6P1peJQN0yvbFx43L +/nfY7CXRhF/sEViTcjkV1/Asa+PUT7yjzE6hf512yfUJtlBU7HXfK1n7sK2BT1xG +ds9N+jpBSXXq7dqWr2OsfmNMDF3wttnjD31OOjxmyq1yR4BuPxhJYH/AHnQQIA+/ +tIcdLgtEKISXzSwcMvKt0X5olzTKFREeEWH/x+i1zPuu4bxUY7Fd0z0PvealExtv +mT8MJ3iFhHRReJolnG8a00DUfyj++dEaSQtgf4liGQZLgBK7g4AesL3vKPgRVNP9 +ex4WEuLULIW78QiG0bOwoswc2pMePWlNVDEVju56VwB2+Y+qNQIO29ySuTf7W9NB +adXUAmuJEykrAgMBAAGjggGIMIIBhDALBgNVHQ8EBAMCAYYwIwYDVR0lBBwwGgYK +KwYBBAGCNwoDDAYFZ4EFCAEGBWeBBQgDMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYD +VR0OBBYEFBxqlvcz0egLZjNmB55lFH2P/rA8MB8GA1UdIwQYMBaAFEv+JlqUwfYz +w4NIJt3z5bBksqqVMHYGA1UdHwRvMG0wa6BpoGeGZWh0dHA6Ly93d3cubWljcm9z +b2Z0LmNvbS9wa2lvcHMvY3JsL0F6dXJlJTIwVmlydHVhbCUyMFRQTSUyMFJvb3Ql +MjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIwMjMuY3JsMIGDBggrBgEFBQcB +AQR3MHUwcwYIKwYBBQUHMAKGZ2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lv +cHMvY2VydHMvQXp1cmUlMjBWaXJ0dWFsJTIwVFBNJTIwUm9vdCUyMENlcnRpZmlj +YXRlJTIwQXV0aG9yaXR5JTIwMjAyMy5jcnQwDQYJKoZIhvcNAQEMBQADggIBADv8 +FY5P/MusECLp6meis4Ujpce/2mq9bEBnSTV/qjTv1i59StJbqCD1uUXTPddeOnFl +jkFai5y09zMka+aJnhhS8PIHhbgFSI4SKCjdC1CVZOQoPTTM/q3/SfJoZ2Zn/PGH +2z+CxFsj7p2CNs843+swiUc+klA+X2hVy4HKEplh5HQmGPK6I3PBT/4P0PqBpqeB +2ylTEMvuhSYtDnTYTGOUCUpnEYeY/THBMLsHVL0+I/Ti5irNaRwurpP3bIPzZd3G +SO+EjQfOq/lGj3f7ipbLdwovIMVtbYuThlRK9AjsiS/mwGIZeuIiRy8T2KUqxkq/ +4Kt6i8gcIAKBgAx0/5BMbLf24KJHjtv7NN/0RissF6qOPa1ivk/rQwfr/FCilND5 +qvnHAgnHPSBKeo6cNQ1YjGl5KAQZuXxl2HWR/Pnv5ukUiBzdkozB+npXaTVQHmTx +NUIRTErbYuCYW+HASCxnhZMYcLha7lM/X6Q5NMEOgz3dPA8mkxGCInZfjYZ2JlDb +Rx9ks9aRqFpoxHuyBxas3Fycuyz77YFE6rr+p7uAQ3o+dZqbO0RatvsNUpO6aTH6 +lUl8xyTwGMw2WV3ATxAaUCtVOAA8xVpJo9Z19PSfepho+g6VKku7eXHPqGR/p9++ +MIy/GuVREnFXQXQt5Ut3pYlREPiZ/k+57avJoQhv +-----END CERTIFICATE----- diff --git a/crates/attestation/test-assets/azure-cloud-virtual-tpm-ca-24.pem b/crates/attestation/test-assets/azure-cloud-virtual-tpm-ca-24.pem new file mode 100644 index 0000000..625b63a --- /dev/null +++ b/crates/attestation/test-assets/azure-cloud-virtual-tpm-ca-24.pem @@ -0,0 +1,50 @@ +-----BEGIN CERTIFICATE----- +MIII6zCCBtOgAwIBAgIRAOy12jSrvPCNMaArM9L/ceIwDQYJKoZIhvcNAQENBQAw +KjEoMCYGA1UEAxMfQXp1cmUgQ2xvdWQgVmlydHVhbCBUUE0gQ0EgMjAyNTAeFw0y +NjAzMTEwMDAwMDBaFw0yOTAzMTAwMDAwMDBaMCoxKDAmBgNVBAMTH0F6dXJlIENs +b3VkIFZpcnR1YWwgVFBNIENBIC0gMjQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQCldZHSm3EVhrg4dBhW+P1fQzPX09vbzYsVvcXe31pLFhlokdJeJAC+ +axrPpZYP8V1DY9m0vPxrj6dm52vzoNErt6zuSRH4lNeQGWkZB6FLagyJwpDSDZ3B +sM6Y54Uwxi6YOG4yCkVjKY2CTvLbGxODEYv1U2NjM/CIQvqob49JBM64Qq/6gWFI +raNbJwA70ILysJwYjVB6YP4NNPuAlITNQyXZRp6k/nKCYPqxqhz/AUSbrJgH0t0T +l5v9zHo7awKV3S5bsdyxcCKTqm2IPittWrfvOF9KEs3ZRMeJPV+0DXp2e4vjKmee +9VFH9AmuHxYOa4MDa+617SxBrIPD/eE7OmdvPoSL3tgcVAtWAotykNWoVBgFdYlz +7M0WuQbZE23jhteP2rMZT5tVYr+hcWrTzzZ2GfNwiGLsilr6uL8YDm7TNE0twZIG +TtZaXJYzgaFFUMA8HpceedLD7ddS1uMUilhPHTmJXpWzRr/MdxrkU1xqo8/lvl2j +zC3EMOVPP8AhMrADMQizYzNJXLOJB24nouw0MTcaHc/VhAyW2ia/6ObYk3vpSOBN +GLmnDEoME23neYLsnOtAMvPciaBQ4pMojnuh2A8iVBKsaLasnVpwhkveBTaupknP +YzH/y9xuAFfxa1VUwouR5IofcstfMg10lWwVJRrW89bKBHZKYvSEPwIDAQABo4IE +CjCCBAYwEgYDVR0TAQH/BAgwBgEB/wIBADAjBgNVHSUEHDAaBgorBgEEAYI3CgMM +BgVngQUIAQYFZ4EFCAMwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBRDbLpWr9DP +xqS5FZVZ12BF80gyWjAfBgNVHSMEGDAWgBQcapb3M9HoC2YzZgeeZRR9j/6wPDCC +AbIGA1UdHwSCAakwggGlMGmgZ6BlhmNodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNv +cmUud2luZG93cy5uZXQvZWFzdHVzMi9jcmxzL2F6dXJldnRwbXBraS9henVyZXZ0 +cG1wb2xpY3ljYWV1czIvY3VycmVudC5jcmwwa6BpoGeGZWh0dHA6Ly9zZWNvbmRh +cnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L2Vhc3R1czIvY3Jscy9henVyZXZ0 +cG1wa2kvYXp1cmV2dHBtcG9saWN5Y2FldXMyL2N1cnJlbnQuY3JsMFqgWKBWhlRo +dHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vZWFzdHVzMi9jcmxzL2F6dXJldnRwbXBr +aS9henVyZXZ0cG1wb2xpY3ljYWV1czIvY3VycmVudC5jcmwwb6BtoGuGaWh0dHA6 +Ly9henVyZXZ0cG1wa2kuZWFzdHVzMi5wa2kuY29yZS53aW5kb3dzLm5ldC9jZXJ0 +aWZpY2F0ZUF1dGhvcml0aWVzL2F6dXJldnRwbXBvbGljeWNhZXVzMi9jdXJyZW50 +LmNybDCCAcMGCCsGAQUFBwEBBIIBtTCCAbEwbwYIKwYBBQUHMAKGY2h0dHA6Ly9w +cmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC9lYXN0dXMyL2NhY2VydHMv +YXp1cmV2dHBtcGtpL2F6dXJldnRwbXBvbGljeWNhZXVzMi9jZXJ0LmNlcjBxBggr +BgEFBQcwAoZlaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5u +ZXQvZWFzdHVzMi9jYWNlcnRzL2F6dXJldnRwbXBraS9henVyZXZ0cG1wb2xpY3lj +YWV1czIvY2VydC5jZXIwYAYIKwYBBQUHMAKGVGh0dHA6Ly9jcmwubWljcm9zb2Z0 +LmNvbS9lYXN0dXMyL2NhY2VydHMvYXp1cmV2dHBtcGtpL2F6dXJldnRwbXBvbGlj +eWNhZXVzMi9jZXJ0LmNlcjBpBggrBgEFBQcwAoZdaHR0cDovL2F6dXJldnRwbXBr +aS5lYXN0dXMyLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9y +aXRpZXMvYXp1cmV2dHBtcG9saWN5Y2FldXMyMA0GCSqGSIb3DQEBDQUAA4ICAQBZ +y/ScjLcaLqOFMMg/wqHHIuL+upqKnDhpl4joXXszc8xIsxYlzUdxUflcvC/midw3 +kPVRz8hNbXfzawHoIfh9/81u59uMk/NOExLflcd3a99sjfNwELmxTr7+nd8CjJmo +wCJoj/y/8noiDlBFTkKty7ngMooYV0XkisOS65McErEWT0hO0Gvb4FKTH2XAuc+7 +8mQe2J73O9SIkw4QljBg36wkSDT9P3R5cMcLbfeAEsa/n1llrg93x+A5Q8Tqwz6O +DrfiTNisaQQpVzhX+/PmdBpFiGspwTu81RGvo0kj5YhMhVoQ6zBuol21xv7VwLxB +oKVrDN59rQ6gnn6JK0QIvNRk0sYipVQP7wh2J1M39DpF0E0+KkkFg3dzUSSfmM8F +fDsaW227gPBjvXIumPOSlAF6b8G+Or/VFIiQ5Cc8Hi3icYCSc9jOtp4UGVvCAcMU +6Fj1aFUCeh1dtbCMAWDMVM08OF0Wae1YpXUuF7SeHkLNRg+DXcdCGb9FCRyJ8QQN +WQkEAe+fIV2WM5W6OUB+S3S4k3PHK0Rah73nACo0Qeh9hMIlhkMdzIgBqFieSo9m +HNIxnnDWlmpTBsqRHNXZP1qgUX2g1gJmG72qLbAlP2rUW/nl5XfWawjMYQyWszV/ +5cdYaa/HYUQm6edHfFiEmadftomvhfmjm8XnVBy9ng== +-----END CERTIFICATE-----