diff --git a/Cargo.lock b/Cargo.lock index 08abe81..81e0888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,15 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -334,11 +343,47 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attest-measure" +version = "0.0.1" +source = "git+https://github.com/easy-tee/attest.git?rev=a64f147362b8948e2288015e476c40d04b11b661#a64f147362b8948e2288015e476c40d04b11b661" +dependencies = [ + "anyhow", + "attest-types", + "authenticode", + "crc32fast", + "hex", + "hex-literal", + "object", + "prost", + "rsa", + "serde", + "serde_json", + "serde_with", + "sha2", + "thiserror 2.0.18", + "ureq 3.3.0", + "x509-parser 0.18.1", +] + +[[package]] +name = "attest-types" +version = "0.0.1" +source = "git+https://github.com/easy-tee/attest.git?rev=a64f147362b8948e2288015e476c40d04b11b661#a64f147362b8948e2288015e476c40d04b11b661" +dependencies = [ + "parity-scale-codec", + "serde", + "serde_json", + "serde_with", +] + [[package]] name = "attestation" version = "0.0.1" dependencies = [ "anyhow", + "attest-measure", + "attest-types", "az-tdx-vtpm", "base64 0.22.1", "dcap-qvl 0.3.12 (git+https://github.com/Phala-Network/dcap-qvl.git?rev=f1dcc65371e941a7b83e3234833d23a1fb232ab1)", @@ -365,7 +410,7 @@ dependencies = [ "tokio-rustls", "tracing", "tss-esapi", - "ureq", + "ureq 2.12.1", "x509-parser 0.18.1", ] @@ -407,6 +452,24 @@ dependencies = [ "yasna 0.5.2", ] +[[package]] +name = "authenticode" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86c421a87e3dd1a3024c86e0787106b6ba40d9b434fe0ebeffbd24a242dc144d" +dependencies = [ + "cms", + "const-oid", + "der", + "digest", + "object", + "rsa", + "sha1", + "sha2", + "spki", + "x509-cert", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -520,7 +583,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", - "ureq", + "ureq 2.12.1", "zerocopy", ] @@ -743,6 +806,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -846,8 +918,10 @@ version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ + "iana-time-zone", "num-traits", "serde", + "windows-link", ] [[package]] @@ -926,6 +1000,18 @@ dependencies = [ "cc", ] +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid", + "der", + "spki", + "x509-cert", +] + [[package]] name = "codicon" version = "3.0.0" @@ -1363,6 +1449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -1497,6 +1584,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1929,6 +2022,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1962,6 +2061,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + [[package]] name = "hex_fmt" version = "0.3.0" @@ -2156,6 +2261,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -2293,6 +2422,17 @@ dependencies = [ "syn", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -2811,6 +2951,15 @@ dependencies = [ "libm", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid" version = "0.2.1" @@ -3228,6 +3377,29 @@ dependencies = [ "yansi", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quinn" version = "0.11.9" @@ -3485,6 +3657,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.3" @@ -3843,6 +4035,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3978,7 +4194,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap", + "indexmap 2.13.0", "itoa", "memchr", "serde", @@ -4009,6 +4225,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sev" version = "6.3.1" @@ -4493,7 +4741,7 @@ version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ - "indexmap", + "indexmap 2.13.0", "toml_datetime", "toml_parser", "winnow", @@ -4734,6 +4982,35 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots 1.0.7", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http 1.4.0", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -4752,6 +5029,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4929,7 +5212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.13.0", "wasm-encoder", "wasmparser", ] @@ -4942,7 +5225,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.13.0", "semver", ] @@ -5008,12 +5291,65 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -5209,7 +5545,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.13.0", "prettyplease", "syn", "wasm-metadata", @@ -5240,7 +5576,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.11.0", - "indexmap", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -5259,7 +5595,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.13.0", "log", "semver", "serde", diff --git a/crates/attestation-provider-server/src/lib.rs b/crates/attestation-provider-server/src/lib.rs index 1db9232..e13632b 100644 --- a/crates/attestation-provider-server/src/lib.rs +++ b/crates/attestation-provider-server/src/lib.rs @@ -55,7 +55,7 @@ pub async fn attestation_provider_client( .await?; let remote_attestation_message = AttestationExchangeMessage::decode(&mut &response[..])?; - let remote_attestation_type = remote_attestation_message.attestation_type; + let remote_attestation_type = remote_attestation_message.attestation_type(); println!("Remote attestation type: {remote_attestation_type}"); diff --git a/crates/attestation/Cargo.toml b/crates/attestation/Cargo.toml index 138d819..52403b3 100644 --- a/crates/attestation/Cargo.toml +++ b/crates/attestation/Cargo.toml @@ -11,8 +11,10 @@ keywords = ["attestation", "CVM", "TDX"] dcap-qvl = { workspace = true, features = ["danger-allow-tcb-override"] } pccs = { workspace = true } mock-tdx = { workspace = true, optional = true } -tokio = { workspace = true, features = ["fs"] } +tokio = { workspace = true, features = ["fs", "rt", "rt-multi-thread"] } tokio-rustls = { workspace = true, default-features = false } +attest-types = { git = "https://github.com/easy-tee/attest.git", rev = "a64f147362b8948e2288015e476c40d04b11b661" } +attest-measure = {git = "https://github.com/easy-tee/attest.git", rev = "a64f147362b8948e2288015e476c40d04b11b661" } anyhow = "1.0.100" pem-rfc7468 = { version = "0.7.0", features = ["std"] } diff --git a/crates/attestation/README.md b/crates/attestation/README.md index 5620969..d487292 100644 --- a/crates/attestation/README.md +++ b/crates/attestation/README.md @@ -106,6 +106,13 @@ These objects have the following fields: - `measurements` - an object with fields referring to the five measurement registers. Field names are the same as for the measurement headers (see below). +- `dcap_image_hashes` - an alternative to `measurements` that pins the hashes + of the boot components (UKI, kernel, initrd, cmdline, GPT disk GUID) rather + than raw register values. The verifier reconstructs the expected RTMRs at + check time from these hashes and platform-fetched firmware. See + [Portable measurement policies](#portable-measurement-policies) below. + +A record may set either `measurements` or `dcap_image_hashes`, not both. Each measurement register entry supports two mutually exclusive fields: @@ -190,10 +197,11 @@ compatibility: The only mandatory field is `attestation_type`. If an attestation type is -specified, but no measurements, *any* measurements will be accepted for this -attestation type. The measurements can still be checked up-stream by the source -client or target service using header injection described below. But it is then -up to these external programs to reject unacceptable measurements. +specified with neither `measurements` nor `dcap_image_hashes`, *any* +measurements will be accepted for this attestation type. The measurements can +still be checked up-stream by the source client or target service using header +injection for example. But it is then up to external programs to reject +unacceptable measurements. ### Measurement field names @@ -240,3 +248,52 @@ Legacy numeric field names are still supported for backwards compatibility: - "2" - RTMR1 - "3" - RTMR2 - "4" - RTMR3 + +### Portable measurement policies + +The `measurements` format above specifies register values, so any change +to platform-injected values (firmware, RAM size, disk count, ACPI tables) +changes the expected register values even when the OS image is unchanged. + +The `dcap_image_hashes` alternative allows you to specify the OS image's +boot-component hashes instead, and the verifier reconstructs the expected +register values from those hashes plus platform metadata fetched attest +verification time. The same policy record then matches the same OS images +across platform variants. + +This can be done with the `attest measure` CLI from +[Easy-TEE/attest](https://github.com/Easy-TEE/attest) which outputs five +hex-encoded SHA-384 values: + +- `uki_authenticode` - authenticode hash of the UKI (unified kernel image) +- `kernel_authenticode` - authenticode hash of the kernel binary +- `cmdline_hash` - hash of the kernel command line +- `initrd_hash` - hash of the initramfs +- `gpt_disk_guid_hash` - hash derived from GPT partition GUIDs + +Example: + +```JSON +[ + { + "measurement_id": "flashbox-l1-v1.0.0", + "attestation_type": "gcp-tdx", + "dcap_image_hashes": { + "uki_authenticode": "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc", + "kernel_authenticode": "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c", + "cmdline_hash": "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87", + "initrd_hash": "a5b3d4742045e7d08aa19953c35098e784826b01a84f60568fa69f1a848dafd96ec98b8df616d6142779c9b97318166b", + "gpt_disk_guid_hash": "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282" + } + } +] +``` + +#### Supported attestation types for portable measurements + +Portable policies currently only work with the `"gcp-tdx"` attestation type. +For GCP, the verifier fetches the platform firmware blob from Google's metadata +service (keyed by MRTD) and combines it with the image hashes to reconstruct +the expected registers. Support for other attestation types is planned; but +`dcap_image_hashes` record with any other attestation type is rejected when +parsing from JSON. diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index dca2c10..e8f1767 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -254,16 +254,14 @@ pub fn verify_dcap_attestation_sync( /// Create a mock quote for testing on non-confidential hardware #[cfg(any(test, feature = "mock"))] -fn generate_quote(input: [u8; 64]) -> Result, tdx_attest::TdxAttestError> { - generate_mock_tdx_quote(input).map_err(|error| { - tdx_attest::TdxAttestError::QuoteFailure(format!("mock-tdx quote generation: {error}")) - }) +fn generate_quote(input: [u8; 64]) -> Result, AttestationError> { + generate_mock_tdx_quote(input).map_err(|error| AttestationError::Mock(format!("{error}"))) } /// Create a quote #[cfg(not(any(test, feature = "mock")))] -fn generate_quote(input: [u8; 64]) -> Result, tdx_attest::TdxAttestError> { - tdx_attest::get_quote(&input) +fn generate_quote(input: [u8; 64]) -> Result, AttestationError> { + Ok(tdx_attest::get_quote(&input)?) } /// Given a [Report] get the input data regardless of report type @@ -363,7 +361,7 @@ mod tests { .unwrap(); assert_eq!(async_measurements, sync_measurements); - measurement_policy.check_measurement(&async_measurements).unwrap(); + measurement_policy.check_measurement(&async_measurements, None).unwrap(); } // This specifically tests a quote which has outdated TCB level from Azure @@ -407,12 +405,10 @@ mod tests { .unwrap(); let pccs = Pccs::new(Some(mock_pcs.base_url.clone())); let expected_input_data = [0xA5; 64]; - let attestation_bytes = create_dcap_attestation(expected_input_data).unwrap(); + let quote = create_dcap_attestation(expected_input_data).unwrap(); let measurements = - verify_dcap_attestation(attestation_bytes, expected_input_data, Some(pccs)) - .await - .unwrap(); + verify_dcap_attestation(quote, expected_input_data, Some(pccs)).await.unwrap(); assert_eq!(measurements, crate::measurements::mock_dcap_measurements()); assert_eq!(mock_pcs.tcb_call_count(), 1); diff --git a/crates/attestation/src/gcp.rs b/crates/attestation/src/gcp.rs new file mode 100644 index 0000000..26f2488 --- /dev/null +++ b/crates/attestation/src/gcp.rs @@ -0,0 +1,188 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use attest_measure::dcap::DcapFirmware; +use thiserror::Error; + +/// Maps MRTD values to GCP firmware to avoid re-fetching on subsequent +/// verification +#[derive(Clone, Debug, Default)] +pub(crate) struct GcpFirmwareCache { + cache: Arc>>, +} + +impl GcpFirmwareCache { + pub(crate) fn new() -> Self { + Self { cache: Default::default() } + } + + /// Retrieve firmware from cache or fetch if not present + pub(crate) fn get_or_fetch( + &self, + mrtd: [u8; 48], + ) -> Result { + if let Some(firmware) = + self.cache.read().map_err(|_| GcpFirmwareCacheError::CacheLock)?.get(&mrtd).cloned() + { + return Ok(firmware); + } + + let firmware = fetch_firmware(mrtd)?; + self.cache + .write() + .map_err(|_| GcpFirmwareCacheError::CacheLock)? + .insert(mrtd, firmware.clone()); + Ok(firmware) + } +} + +/// Fetch firmware from Google. If we are running inside a mutli-threaded +/// tokio runtime the blocking HTTP fetch is wrapped in `spawn_blocking` +pub(crate) fn fetch_firmware(mrtd: [u8; 48]) -> Result { + match tokio::runtime::Handle::try_current() { + Ok(handle) + if matches!(handle.runtime_flavor(), tokio::runtime::RuntimeFlavor::MultiThread) => + { + tokio::task::block_in_place(|| { + handle.block_on(async move { + tokio::task::spawn_blocking(move || DcapFirmware::from_google(mrtd)) + .await + .map_err(|err| GcpFirmwareCacheError::Join(err.to_string()))? + .map_err(GcpFirmwareCacheError::from) + }) + }) + } + _ => DcapFirmware::from_google(mrtd).map_err(GcpFirmwareCacheError::from), + } +} + +#[derive(Debug, Error)] +pub(crate) enum GcpFirmwareCacheError { + #[error("Cache lock poisoned")] + CacheLock, + #[error("Firmware fetch: {0}")] + Firmware(#[from] attest_measure::dcap::GoogleError), + #[error("Firmware fetch task join: {0}")] + Join(String), +} + +#[cfg(test)] +mod tests { + use attest_measure::dcap::DcapFirmware; + use attest_types::{AcpiHashes, DcapImageHashes}; + use dcap_qvl::quote::Quote; + + use crate::{ + PlatformMetadata, + dcap::{get_quote_input_data, verify_dcap_attestation_with_given_timestamp}, + gcp::GcpFirmwareCache, + measurements::{ExpectedMeasurements, MeasurementPolicy, MeasurementRecord}, + }; + + /// Timestamp used with test fixture + const GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP: u64 = 1_782_809_233; + + /// Create a firmware cache with given firmware loaded + fn create_cache_with_firmware(firmware: DcapFirmware) -> GcpFirmwareCache { + let cache = GcpFirmwareCache::new(); + cache.cache.write().unwrap().insert(firmware.mrtd, firmware); + cache + } + + fn decode_dcap_hash(input: &str) -> [u8; 48] { + hex::decode(input).unwrap().try_into().unwrap() + } + + /// Image hashes associated with test fixture + fn gcp_portable_image_hashes() -> DcapImageHashes { + DcapImageHashes { + uki_authenticode: decode_dcap_hash( + "82500f977e16a1e3fd47db792ac9c9fdd69caa73d8e719fe4489416355f23f5d0863ad796febfc1241bc3e868c3649a6", + ), + kernel_authenticode: decode_dcap_hash( + "b2a6076ae199d325e553a5102cf1f4a18b5e67e36b33261ef20352052199ec5853b5133c0231b16f1198bb086f1cbfac", + ), + cmdline_hash: decode_dcap_hash( + "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87", + ), + initrd_hash: decode_dcap_hash( + "99251a9997f552ce98364e3f7311ca47471e299b6fdb31226d738a10577959ab741cc2e7b8c268236153de568265d3f2", + ), + gpt_disk_guid_hash: decode_dcap_hash( + "488fa3f08aae01c1a46b497319e8a7d3b7335c9ff4f4d7fe6a3dd62c844b03de22157c0303be58f10e3152687778e68d", + ), + } + } + + /// Platform metadata associated with test fixture + fn gcp_portable_platform_metadata() -> PlatformMetadata { + PlatformMetadata { + attestation_type: attest_types::AttestationType::GcpTdx, + ram_bytes: 17_179_869_184, + num_disks: 1, + acpi: Some(AcpiHashes { + loader: [ + 246, 12, 53, 229, 59, 178, 27, 70, 117, 207, 168, 219, 49, 14, 200, 142, 56, + 205, 54, 157, 141, 70, 58, 205, 222, 129, 81, 34, 250, 139, 137, 59, 136, 150, + 165, 120, 59, 83, 136, 86, 105, 62, 215, 100, 93, 219, 137, 126, + ], + rsdp: [ + 80, 157, 207, 225, 11, 235, 93, 71, 12, 64, 242, 94, 48, 137, 83, 112, 148, + 136, 49, 185, 207, 121, 219, 21, 217, 119, 231, 187, 168, 235, 66, 247, 32, 2, + 18, 7, 26, 216, 177, 157, 96, 17, 117, 151, 121, 236, 237, 90, + ], + tables: [ + 11, 176, 175, 160, 8, 135, 59, 220, 32, 222, 224, 247, 65, 218, 120, 150, 194, + 191, 238, 233, 74, 229, 46, 155, 219, 249, 75, 200, 124, 50, 208, 74, 75, 31, + 29, 130, 68, 144, 241, 218, 229, 116, 255, 109, 78, 75, 176, 179, + ], + }), + } + } + + #[tokio::test] + async fn test_gcp_tdx_portable_policy_with_stored_collateral() { + let attestation_bytes: &'static [u8] = + include_bytes!("../test-assets/gcp-tdx-1782809233226668671"); + let collateral_bytes: &'static [u8] = + include_bytes!("../test-assets/gcp-tdx-collateral-1782809233226668671.yaml"); + let firmware_bytes: &'static [u8] = + include_bytes!("../test-assets/gcp-tdx-firmware-1782809233226668671.yaml"); + + let expected_input_data = { + let quote = Quote::parse(attestation_bytes).unwrap(); + get_quote_input_data(quote.report) + }; + + let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); + let firmware = serde_saphyr::from_slice(firmware_bytes).unwrap(); + let measurements = verify_dcap_attestation_with_given_timestamp( + attestation_bytes.to_vec(), + expected_input_data, + None, + Some(collateral), + GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP, + false, + ) + .await + .unwrap(); + + let measurement_policy = MeasurementPolicy { + accepted_measurements: vec![MeasurementRecord { + measurement_id: "gcp-tdx-portable-image-hashes".to_string(), + measurements: ExpectedMeasurements::Image(gcp_portable_image_hashes()), + }], + }; + let gcp_firmware_cache = create_cache_with_firmware(firmware); + + measurement_policy + .check_measurement_with_gcp_cache( + &measurements, + Some(gcp_portable_platform_metadata()), + Some(&gcp_firmware_cache), + ) + .unwrap(); + } +} diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 73bc5ba..8d66cae 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -3,8 +3,8 @@ #[cfg(feature = "azure")] pub mod azure; pub mod dcap; +mod gcp; pub mod measurements; - use std::{ fmt::{self, Display, Formatter}, io::Read, @@ -12,6 +12,8 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +use attest_measure::platform::PlatformError; +pub use attest_types::{AttestationEvidence, PlatformMetadata}; use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use pccs::{Pccs, PccsError}; @@ -26,29 +28,31 @@ const GCP_METADATA_API: &str = "http://metadata.google.internal"; /// An attestation payload together with its type #[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] pub struct AttestationExchangeMessage { - /// What CVM platform is used (including none) - pub attestation_type: AttestationType, - /// The attestation evidence as bytes - in the case of DCAP this is a - /// quote - pub attestation: Vec, + /// Attestation payload with platform metadata, if present. + /// `None` means no evidence presented. + pub attestation_evidence: Option, } impl AttestationExchangeMessage { /// Create an empty attestation payload for the case that we are running /// in a non-confidential environment pub fn without_attestation() -> Self { - Self { attestation_type: AttestationType::None, attestation: Vec::new() } + Self { attestation_evidence: None } } /// Extract the measurements from the attestation, if present, but do /// not verify pub fn get_measurements(&self) -> Result, AttestationError> { - match self.attestation_type { + let Some(attestation_evidence) = &self.attestation_evidence else { + return Ok(None); + }; + + match self.attestation_type() { AttestationType::None => Ok(None), AttestationType::AzureTdx => { #[cfg(feature = "azure")] { - Ok(Some(azure::get_measurements(&self.attestation)?)) + Ok(Some(azure::get_measurements(&attestation_evidence.quote)?)) } #[cfg(not(feature = "azure"))] { @@ -56,12 +60,50 @@ impl AttestationExchangeMessage { } } AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { - let quote = dcap_qvl::verify::Quote::parse(&self.attestation) + let quote = dcap_qvl::verify::Quote::parse(&attestation_evidence.quote) .map_err(DcapVerificationError::from)?; Ok(Some(MultiMeasurements::from_dcap_qvl_quote("e)?)) } } } + + pub fn attestation_type(&self) -> AttestationType { + self.attestation_evidence + .as_ref() + .map(|evidence| evidence.platform.attestation_type.into()) + .unwrap_or(AttestationType::None) + } +} + +impl From for AttestationExchangeMessage { + fn from(attestation_evidence: AttestationEvidence) -> Self { + Self { attestation_evidence: Some(attestation_evidence) } + } +} + +impl From for AttestationType { + fn from(attestation_type: attest_types::AttestationType) -> Self { + match attestation_type { + attest_types::AttestationType::GcpTdx => AttestationType::GcpTdx, + attest_types::AttestationType::AzureTdx => AttestationType::AzureTdx, + attest_types::AttestationType::SelfHostedTdx => AttestationType::QemuTdx, + } + } +} + +impl TryFrom for attest_types::AttestationType { + type Error = AttestationError; + + fn try_from(attestation_type: AttestationType) -> Result { + match attestation_type { + AttestationType::None => Err(AttestationError::AttestationTypeNotAccepted), + AttestationType::AzureTdx => Ok(attest_types::AttestationType::AzureTdx), + AttestationType::GcpTdx => Ok(attest_types::AttestationType::GcpTdx), + AttestationType::DcapTdx | AttestationType::QemuTdx => { + Ok(attest_types::AttestationType::SelfHostedTdx) + } + } + } } /// Type of attestation used @@ -177,7 +219,7 @@ impl AttestationGenerator { attestation_provider_url: Option, ) -> Result { if attestation_provider_url.is_some() { - // If a remote provide is used, dont do detection + // If a remote provider is used, dont do detection let attestation_type = serde_json::from_value(serde_json::Value::String( attestation_type_string.ok_or(AttestationError::AttestationTypeNotGiven)?, ))?; @@ -204,37 +246,43 @@ impl AttestationGenerator { if let Some(url) = &self.attestation_provider_url { Self::use_attestation_provider(url, self.attestation_type, input_data) } else { - Ok(AttestationExchangeMessage { - attestation_type: self.attestation_type, - attestation: self.generate_attestation_bytes(input_data)?, - }) - } - } - - /// Generate attestation evidence bytes based on attestation type, with - /// given input data - fn generate_attestation_bytes( - &self, - input_data: [u8; 64], - ) -> Result, AttestationError> { - match self.attestation_type { - AttestationType::None => Ok(Vec::new()), - AttestationType::AzureTdx => { - #[cfg(feature = "azure")] - { - Ok(azure::create_azure_attestation(input_data)?) + match self.attestation_type { + AttestationType::None => Ok(AttestationExchangeMessage::without_attestation()), + AttestationType::AzureTdx => { + #[cfg(feature = "azure")] + { + let platform = attest_measure::platform::metadata_for( + self.attestation_type.try_into()?, + )?; + Ok(AttestationExchangeMessage { + attestation_evidence: Some(AttestationEvidence { + quote: azure::create_azure_attestation(input_data)?, + platform, + }), + }) + } + #[cfg(not(feature = "azure"))] + { + tracing::error!( + "Attempted to generate an azure attestation but the `azure` feature not enabled" + ); + Err(AttestationError::AttestationTypeNotSupported) + } } - #[cfg(not(feature = "azure"))] - { - tracing::error!( - "Attempted to generate an azure attestation but the `azure` feature not enabled" - ); - Err(AttestationError::AttestationTypeNotSupported) + AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { + #[cfg(any(test, feature = "mock"))] + let platform = mock_platform_metadata(self.attestation_type)?; + #[cfg(not(any(test, feature = "mock")))] + let platform = + attest_measure::platform::metadata_for(self.attestation_type.try_into()?)?; + Ok(AttestationExchangeMessage { + attestation_evidence: Some(AttestationEvidence { + quote: dcap::create_dcap_attestation(input_data)?, + platform, + }), + }) } } - AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { - dcap::create_dcap_attestation(input_data) - } } } @@ -245,6 +293,10 @@ impl AttestationGenerator { attestation_type: AttestationType, input_data: [u8; 64], ) -> Result { + if attestation_type == AttestationType::None { + return Ok(AttestationExchangeMessage::without_attestation()); + } + let url = format!("{}/attest/{}", url, hex::encode(input_data)); let mut response = ureq::get(&url) @@ -257,13 +309,8 @@ impl AttestationGenerator { .read_to_end(&mut body) .map_err(|err| AttestationError::AttestationProvider(err.to_string()))?; - // If the response is not already wrapped in an attestation exchange - // message, wrap it in one - if let Ok(message) = AttestationExchangeMessage::decode(&mut &body[..]) { - Ok(message) - } else { - Ok(AttestationExchangeMessage { attestation_type, attestation: body }) - } + AttestationExchangeMessage::decode(&mut &body[..]) + .map_err(|err| AttestationError::AttestationProvider(err.to_string())) } } @@ -284,14 +331,17 @@ pub struct AttestationVerifier { pub override_azure_outdated_tcb: bool, /// Internal cache for collateral pub internal_pccs: Option, + /// Cached GCP firmware blobs indexed by MRTD + known_gcp_firmware: gcp::GcpFirmwareCache, } impl AttestationVerifier { - pub fn new( + fn build( measurement_policy: MeasurementPolicy, pccs_url: Option, dump_dcap_quotes: bool, override_azure_outdated_tcb: bool, + known_gcp_firmware: gcp::GcpFirmwareCache, ) -> Self { Self { measurement_policy, @@ -299,9 +349,25 @@ impl AttestationVerifier { dump_dcap_quotes, override_azure_outdated_tcb, internal_pccs: Some(Pccs::new(pccs_url)), + known_gcp_firmware, } } + pub fn new( + measurement_policy: MeasurementPolicy, + pccs_url: Option, + dump_dcap_quotes: bool, + override_azure_outdated_tcb: bool, + ) -> Self { + Self::build( + measurement_policy, + pccs_url, + dump_dcap_quotes, + override_azure_outdated_tcb, + gcp::GcpFirmwareCache::new(), + ) + } + /// Create an [AttestationVerifier] which will only allow no attestation /// and will reject if one is given pub fn expect_none() -> Self { @@ -311,6 +377,7 @@ impl AttestationVerifier { dump_dcap_quotes: false, override_azure_outdated_tcb: false, internal_pccs: None, + known_gcp_firmware: gcp::GcpFirmwareCache::new(), } } @@ -323,6 +390,7 @@ impl AttestationVerifier { dump_dcap_quotes: false, override_azure_outdated_tcb: false, internal_pccs: None, + known_gcp_firmware: gcp::GcpFirmwareCache::new(), } } @@ -335,6 +403,7 @@ impl AttestationVerifier { dump_dcap_quotes: false, override_azure_outdated_tcb: false, internal_pccs: Some(Pccs::new(Some(pccs_url))), + known_gcp_firmware: gcp::GcpFirmwareCache::new(), } } @@ -364,7 +433,7 @@ impl AttestationVerifier { attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], ) -> Result, AttestationError> { - let attestation_type = attestation_exchange_message.attestation_type; + let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); if self.dump_dcap_quotes { @@ -376,7 +445,7 @@ impl AttestationVerifier { if self.has_remote_attestation() { return Err(AttestationError::AttestationTypeNotAccepted); } - if attestation_exchange_message.attestation.is_empty() { + if attestation_exchange_message.attestation_evidence.is_none() { return Ok(None); } else { return Err(AttestationError::AttestationGivenWhenNoneExpected); @@ -385,8 +454,12 @@ impl AttestationVerifier { AttestationType::AzureTdx => { #[cfg(feature = "azure")] { + let attestation_evidence = attestation_exchange_message + .attestation_evidence + .as_ref() + .ok_or(AttestationError::AttestationTypeNotAccepted)?; azure::verify_azure_attestation( - attestation_exchange_message.attestation, + attestation_evidence.quote.clone(), expected_input_data, self.internal_pccs.clone(), self.override_azure_outdated_tcb, @@ -399,8 +472,12 @@ impl AttestationVerifier { } } AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { + let attestation_evidence = attestation_exchange_message + .attestation_evidence + .as_ref() + .ok_or(AttestationError::AttestationTypeNotAccepted)?; dcap::verify_dcap_attestation( - attestation_exchange_message.attestation, + attestation_evidence.quote.clone(), expected_input_data, self.internal_pccs.clone(), ) @@ -409,7 +486,15 @@ impl AttestationVerifier { }; // Do a measurement / attestation type policy check - self.measurement_policy.check_measurement(&measurements)?; + let platform_metadata = attestation_exchange_message + .attestation_evidence + .as_ref() + .map(|evidence| evidence.platform.clone()); + self.measurement_policy.check_measurement_with_gcp_cache( + &measurements, + platform_metadata, + Some(&self.known_gcp_firmware), + )?; tracing::debug!("Verification successful"); Ok(Some(measurements)) @@ -420,7 +505,7 @@ impl AttestationVerifier { attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], ) -> Result, AttestationError> { - let attestation_type = attestation_exchange_message.attestation_type; + let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); if self.dump_dcap_quotes { @@ -432,7 +517,7 @@ impl AttestationVerifier { if self.has_remote_attestation() { return Err(AttestationError::AttestationTypeNotAccepted); } - if attestation_exchange_message.attestation.is_empty() { + if attestation_exchange_message.attestation_evidence.is_none() { return Ok(None); } else { return Err(AttestationError::AttestationGivenWhenNoneExpected); @@ -441,9 +526,13 @@ impl AttestationVerifier { AttestationType::AzureTdx => { #[cfg(feature = "azure")] { + let attestation_evidence = attestation_exchange_message + .attestation_evidence + .as_ref() + .ok_or(AttestationError::AttestationTypeNotAccepted)?; let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; azure::verify_azure_attestation_sync( - attestation_exchange_message.attestation, + attestation_evidence.quote.clone(), expected_input_data, pccs, self.override_azure_outdated_tcb, @@ -455,6 +544,10 @@ impl AttestationVerifier { } } AttestationType::DcapTdx | AttestationType::QemuTdx | AttestationType::GcpTdx => { + let attestation_evidence = attestation_exchange_message + .attestation_evidence + .as_ref() + .ok_or(AttestationError::AttestationTypeNotAccepted)?; #[cfg(any(test, feature = "mock"))] let pccs = self.internal_pccs.clone().unwrap_or_else(|| Pccs::new_without_prewarm(None)); @@ -462,7 +555,7 @@ impl AttestationVerifier { let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; dcap::verify_dcap_attestation_sync( - attestation_exchange_message.attestation, + attestation_evidence.quote.clone(), expected_input_data, pccs, )? @@ -470,7 +563,15 @@ impl AttestationVerifier { }; // Do a measurement / attestation type policy check - self.measurement_policy.check_measurement(&measurements)?; + let platform_metadata = attestation_exchange_message + .attestation_evidence + .as_ref() + .map(|evidence| evidence.platform.clone()); + self.measurement_policy.check_measurement_with_gcp_cache( + &measurements, + platform_metadata, + Some(&self.known_gcp_firmware), + )?; tracing::debug!("Verification successful"); Ok(Some(measurements)) @@ -484,12 +585,13 @@ impl AttestationVerifier { /// Write attestation data to a log file fn log_attestation(attestation: &AttestationExchangeMessage) { - if attestation.attestation_type != AttestationType::None { + if let Some(attestation_evidence) = &attestation.attestation_evidence { let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_nanos(); - let filename = format!("quotes/{}-{}", attestation.attestation_type, timestamp); - let attestation_bytes = attestation.attestation.clone(); + let attestation_type = attestation.attestation_type(); + let filename = format!("quotes/{attestation_type}-{timestamp}"); + let attestation_bytes = attestation_evidence.quote.clone(); if let Ok(handle) = tokio::runtime::Handle::try_current() { handle.spawn(async move { if let Err(err) = tokio::fs::write(&filename, attestation_bytes).await { @@ -571,6 +673,19 @@ fn is_local_ip(ip: IpAddr) -> bool { } } +#[cfg(any(test, feature = "mock"))] +/// Create mock platform metadata for tests +pub fn mock_platform_metadata( + attestation_type: AttestationType, +) -> Result { + Ok(PlatformMetadata { + attestation_type: attestation_type.try_into()?, + ram_bytes: 0, + num_disks: 0, + acpi: None, + }) +} + /// An error when generating or verifying an attestation #[derive(Error, Debug)] pub enum AttestationError { @@ -609,40 +724,19 @@ pub enum AttestationError { Pccs(#[from] PccsError), #[error("Sync verification requested but no PCCS configured")] NoPccs, + #[cfg(any(test, feature = "mock"))] + #[error("Cannot create mock attestation: {0}")] + Mock(String), + #[error("Cannot retrieve platform metadata: {0}")] + PlatformMetadata(#[from] PlatformError), } #[cfg(test)] mod tests { use mock_tdx::mock_pcs::{MockPcsConfig, spawn_mock_pcs_server}; - use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpListener, - }; use super::*; - async fn spawn_test_attestation_provider_server(body: Vec) -> std::net::SocketAddr { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - tokio::spawn(async move { - if let Ok((mut socket, _)) = listener.accept().await { - let mut buf = [0u8; 1024]; - let _ = socket.read(&mut buf).await; - - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", - body.len() - ); - let _ = socket.write_all(response.as_bytes()).await; - let _ = socket.write_all(&body).await; - let _ = socket.shutdown().await; - } - }); - - addr - } - #[test] fn attestation_detection_does_not_panic() { // We dont enforce what platform the test is run on, only that the function @@ -655,43 +749,14 @@ mod tests { let _ = running_on_gcp(); } - #[tokio::test(flavor = "multi_thread")] - async fn attestation_provider_response_is_wrapped_if_needed() { - let input_data = [0u8; 64]; - - let encoded_message = AttestationExchangeMessage { - attestation_type: AttestationType::None, - attestation: vec![1, 2, 3], - } - .encode(); - - let encoded_addr = spawn_test_attestation_provider_server(encoded_message).await; - let encoded_url = format!("http://{encoded_addr}"); - let decoded = AttestationGenerator::use_attestation_provider( - &encoded_url, - AttestationType::GcpTdx, - input_data, - ) - .unwrap(); - assert_eq!(decoded.attestation_type, AttestationType::None); - assert_eq!(decoded.attestation, vec![1, 2, 3]); - - let raw_addr = spawn_test_attestation_provider_server(vec![9, 8]).await; - let raw_url = format!("http://{raw_addr}"); - let wrapped = AttestationGenerator::use_attestation_provider( - &raw_url, - AttestationType::DcapTdx, - input_data, - ) - .unwrap(); - assert_eq!(wrapped.attestation_type, AttestationType::DcapTdx); - assert_eq!(wrapped.attestation, vec![9, 8]); - } - #[tokio::test] async fn mock_verifier_supports_sync_verification() { let input_data = [7u8; 64]; - let attestation = dcap::create_dcap_attestation(input_data).unwrap(); + let quote = dcap::create_dcap_attestation(input_data).unwrap(); + let attestation_evidence = AttestationEvidence { + quote, + platform: mock_platform_metadata(AttestationType::GcpTdx).unwrap(), + }; let mock_pcs_server = spawn_mock_pcs_server(MockPcsConfig::default()).await.unwrap(); @@ -700,10 +765,7 @@ mod tests { pccs.ready().await.unwrap(); } - let result = verifier.verify_attestation_sync( - AttestationExchangeMessage { attestation_type: AttestationType::DcapTdx, attestation }, - input_data, - ); + let result = verifier.verify_attestation_sync(attestation_evidence.into(), input_data); assert!(result.is_ok(), "expected sync mock verification to succeed: {result:?}"); } diff --git a/crates/attestation/src/measurements.rs b/crates/attestation/src/measurements.rs index db8c2c9..f42dfe3 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation/src/measurements.rs @@ -2,12 +2,20 @@ //! attestation use std::{collections::HashMap, fmt, fmt::Formatter, net::IpAddr, path::PathBuf}; +use attest_measure::dcap::expected_dcap_registers; +use attest_types::{AttestationType as ImageAttestationType, DcapImageHashes, PlatformMetadata}; use dcap_qvl::quote::Report; use http::{HeaderValue, header::InvalidHeaderValue, uri::InvalidUri}; use serde::Deserialize; use thiserror::Error; +use tracing::warn; -use crate::{AttestationError, AttestationType, dcap::DcapVerificationError}; +use crate::{ + AttestationError, + AttestationType, + dcap::DcapVerificationError, + gcp::{GcpFirmwareCache, fetch_firmware}, +}; /// Represents the measurement register types in a TDX quote #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -135,6 +143,7 @@ impl fmt::Debug for AzureHexDebug<'_> { /// Expected measurement values for policy enforcement #[derive(Debug, Clone, PartialEq)] pub enum ExpectedMeasurements { + Image(DcapImageHashes), Dcap(HashMap>), Azure(HashMap>), NoAttestation, @@ -267,6 +276,10 @@ pub enum MeasurementFormatError { NoExpectedValue(String), #[error("Measurement entry for register '{0}' has empty 'expected_any' list")] EmptyExpectedAny(String), + #[error("Measurement record has both 'measurements' and 'dcap_image_hashes' — set only one")] + BothMeasurementsAndDcapImageHashes, + #[error("Attestation type '{0}' does not support 'dcap_image_hashes'")] + DcapImageHashesUnsupportedAttestationType(String), } /// An accepted measurement value given in the measurements file @@ -375,20 +388,125 @@ impl MeasurementPolicy { pub fn check_measurement( &self, measurements: &MultiMeasurements, + platform_metadata: Option, + ) -> Result<(), AttestationError> { + self.check_measurement_with_gcp_cache(measurements, platform_metadata, None) + } + + /// Given an attestation type and set of measurements, check whether + /// they are acceptable, passing an optional cache for known GCP + /// firmware + pub(crate) fn check_measurement_with_gcp_cache( + &self, + measurements: &MultiMeasurements, + platform_metadata: Option, + known_gcp_firmware: Option<&GcpFirmwareCache>, ) -> Result<(), AttestationError> { if self.accepted_measurements.iter().any(|measurement_record| match measurements { MultiMeasurements::Dcap(dcap_measurements) => { - if let ExpectedMeasurements::Dcap(expected) = &measurement_record.measurements { - // All measurements in our policy must be given and must match - for (k, v) in expected.iter() { - match dcap_measurements.get(k) { - Some(actual_value) if v.iter().any(|v| actual_value == v) => {} - _ => return false, + match &measurement_record.measurements { + ExpectedMeasurements::Dcap(expected) => { + // All measurements in our policy must be given and must match + for (k, v) in expected.iter() { + match dcap_measurements.get(k) { + Some(actual_value) if v.iter().any(|v| actual_value == v) => {} + _ => return false, + } } + true } - return true; + ExpectedMeasurements::Image(image_hashes) => { + let Some(platform_metadata) = &platform_metadata else { + return false; + }; + let firmware = match platform_metadata.attestation_type { + ImageAttestationType::GcpTdx => { + let Some(mrtd) = + dcap_measurements.get(&DcapMeasurementRegister::MRTD) + else { + warn!( + "Could not match image hash measurement due to missing MRTD" + ); + return false; + }; + + let result = if let Some(cache) = known_gcp_firmware { + cache.get_or_fetch(*mrtd) + } else { + fetch_firmware(*mrtd) + }; + match result { + Ok(firmware) => Some(firmware), + Err(err) => { + warn!( + "Could not match image hash measurement - failed to fetch or verify Google firmware: {err:?}" + ); + return false; + } + } + } + // These may be supported in the future but currently regarded as too + // experimental to work with 'portable' measurement policies + ImageAttestationType::SelfHostedTdx => { + warn!("Attempting to match portable measurement policy with bare metal TDX - not yet supported"); + return false; + }, + ImageAttestationType::AzureTdx => { + warn!("Attempting to match portable measurement policy with Azure TDX - not yet supported"); + return false; + }, + }; + + let expected_measurements = match expected_dcap_registers( + image_hashes, + platform_metadata, + firmware.as_ref(), + ) { + Ok(expected) => expected, + Err(err) => { + warn!("Failed to compute expected DCAP registers: {err:?}"); + return false; + } + }; + + if let Some(expected_mrtd) = expected_measurements.mrtd { + match dcap_measurements.get(&DcapMeasurementRegister::MRTD) { + Some(mrtd) if mrtd == &expected_mrtd => {} + _ => return false, + } + } else { + // This will only be the case with SelfHostedTdx which currently would + // already bail with the check above + return false; + } + + if let Some(expected_rtmr0) = expected_measurements.rtmr0 { + match dcap_measurements.get(&DcapMeasurementRegister::RTMR0) { + Some(rtmr0) if rtmr0 == &expected_rtmr0 => {} + _ => return false, + } + } else { + // This will only be the case with SelfHostedTdx which currently would + // already bail with the check above + return false; + } + + if let Some(rtmr1) = dcap_measurements.get(&DcapMeasurementRegister::RTMR1) + && rtmr1 != &expected_measurements.rtmr1 + { + return false; + } + + if let Some(rtmr2) = dcap_measurements.get(&DcapMeasurementRegister::RTMR2) + && rtmr2 != &expected_measurements.rtmr2 + { + return false; + } + + true + } + ExpectedMeasurements::Azure(_) | ExpectedMeasurements::NoAttestation => false, } - false } MultiMeasurements::Azure(azure_measurements) => { if let ExpectedMeasurements::Azure(expected) = &measurement_record.measurements { @@ -452,6 +570,9 @@ impl MeasurementPolicy { measurement_id: Option, attestation_type: String, measurements: Option>, + /// Image-component hashes for "portable" verification. + /// Mutually exclusive with `measurements`. + dcap_image_hashes: Option, } /// Measurement entry for a single register in the measurements JSON @@ -510,11 +631,14 @@ impl MeasurementPolicy { let mut measurement_policy = Vec::new(); for record in records_simple { - let attestation_type = - serde_json::from_value(serde_json::Value::String(record.attestation_type))?; + let attestation_type: AttestationType = + serde_json::from_value(serde_json::Value::String(record.attestation_type.clone()))?; - if let Some(measurements) = record.measurements { - let expected_measurements = match attestation_type { + let expected_measurements = match (record.measurements, record.dcap_image_hashes) { + (Some(_), Some(_)) => { + return Err(MeasurementFormatError::BothMeasurementsAndDcapImageHashes); + } + (Some(measurements), None) => match attestation_type { AttestationType::None => ExpectedMeasurements::NoAttestation, AttestationType::AzureTdx => { let azure_measurements = measurements @@ -543,15 +667,33 @@ impl MeasurementPolicy { MeasurementFormatError, >>()?, ), - }; - - measurement_policy.push(MeasurementRecord { - measurement_id: record.measurement_id.unwrap_or_default(), - measurements: expected_measurements, - }); - } else { - measurement_policy.push(MeasurementRecord::allow_any_measurement(attestation_type)); + }, + (None, Some(image_hashes)) => match attestation_type { + // Currently only GCP is supported for portable measurement policy - but support + // for other types is planned + AttestationType::GcpTdx => ExpectedMeasurements::Image(image_hashes), + AttestationType::DcapTdx | + AttestationType::QemuTdx | + AttestationType::None | + AttestationType::AzureTdx => { + return Err( + MeasurementFormatError::DcapImageHashesUnsupportedAttestationType( + record.attestation_type, + ), + ); + } + }, + (None, None) => { + measurement_policy + .push(MeasurementRecord::allow_any_measurement(attestation_type)); + continue; + } }; + + measurement_policy.push(MeasurementRecord { + measurement_id: record.measurement_id.unwrap_or_default(), + measurements: expected_measurements, + }); } Ok(MeasurementPolicy { accepted_measurements: measurement_policy }) @@ -576,8 +718,37 @@ impl MeasurementPolicy { mod tests { use std::collections::HashSet; + use attest_measure::dcap::{DcapFirmware, expected_dcap_registers}; + use attest_types::AcpiHashes; + use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; + use super::*; + /// MRTD from the pinned GCP firmware snapshot test asset + const GCP_FIRMWARE_MRTD: &str = "feb7486608382c1ff0e15b4648ddc0acea6ca974eb53e3529f4c4bd5ffbaa20bf335cb75965cea65fe473aed9647c162"; + /// CFV from the same pinned GCP firmware snapshot test asset + const GCP_FIRMWARE_CFV: &str = "9cb6bf09aea7b4acb8549e328d0edd6f15defc0b00d744bb9fb5bab0962bc5c70f69d233e96dbc7c1105ba085781dc88"; + /// Base64-encoded HOB template from the historical GCP firmware asset + /// in the attest repo. + const GCP_HOB_TEMPLATE_B64: &str = "AQA4AAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASJKAAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAA4v8AAAAAAAAeAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAA4P8AAAAAAAACAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAgQAAAAAAAAABAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAACwgAAAAAAAACAAAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAACQgAAAAAAAACAAAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAgAAAAAAAAGAAAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAABwAAEAAAAAAAAAAAAACAAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAABwAAEABggAAAAAAAADAAAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAABwAAEADQgAAAAAAAADAAAAAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAABwAAEAAAggAAAAAAAAB+vwAAAAADADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAABwAAEAAAAAABAAAAAAAAQAMAAAA="; + /// Offset used by the historical HOB template to patch the RAM length + /// field. + const GCP_HOB_LENGTH_OFFSET: usize = 0x240; + /// RAM threshold embedded in the historical GCP HOB template snapshot. + const GCP_RAM_THRESHOLD: u64 = 3 << 30; + + fn gcp_firmware_fixture() -> DcapFirmware { + DcapFirmware { + mrtd: hex::decode(GCP_FIRMWARE_MRTD).unwrap().try_into().unwrap(), + cfv: hex::decode(GCP_FIRMWARE_CFV).unwrap().try_into().unwrap(), + hob: attest_measure::dcap::HobTemplate { + bytes: BASE64_STANDARD.decode(GCP_HOB_TEMPLATE_B64).unwrap(), + length_offset: GCP_HOB_LENGTH_OFFSET, + ram_threshold: GCP_RAM_THRESHOLD, + }, + } + } + #[tokio::test] async fn test_read_measurements_file() { let specific_measurements = @@ -612,20 +783,22 @@ mod tests { // Will not match mock measurements assert!(matches!( - specific_measurements.check_measurement(&mock_dcap_measurements()).unwrap_err(), + specific_measurements.check_measurement(&mock_dcap_measurements(), None).unwrap_err(), AttestationError::MeasurementsNotAccepted )); // Will not match another attestation type assert!(matches!( - specific_measurements.check_measurement(&MultiMeasurements::NoAttestation).unwrap_err(), + specific_measurements + .check_measurement(&MultiMeasurements::NoAttestation, None) + .unwrap_err(), AttestationError::MeasurementsNotAccepted )); // A non-specific measurement fails assert!(matches!( specific_measurements - .check_measurement(&MultiMeasurements::Azure(HashMap::new())) + .check_measurement(&MultiMeasurements::Azure(HashMap::new()), None) .unwrap_err(), AttestationError::MeasurementsNotAccepted )); @@ -638,17 +811,68 @@ mod tests { let allowed_attestation_type = MeasurementPolicy::from_file("test-assets/measurements_2.json".into()).await.unwrap(); - allowed_attestation_type.check_measurement(&mock_dcap_measurements()).unwrap(); + allowed_attestation_type.check_measurement(&mock_dcap_measurements(), None).unwrap(); // Will not match another attestation type assert!(matches!( allowed_attestation_type - .check_measurement(&MultiMeasurements::NoAttestation) + .check_measurement(&MultiMeasurements::NoAttestation, None) .unwrap_err(), AttestationError::MeasurementsNotAccepted )); } + #[test] + fn test_gcp_image_hash_measurement_policy_accepts_matching_measurements() { + fn decode_hash(input: &str) -> [u8; 48] { + hex::decode(input).unwrap().try_into().unwrap() + } + + // Result of measuring a flashbox-l1 image + let image_hashes = DcapImageHashes { + uki_authenticode: decode_hash( + "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc", + ), + kernel_authenticode: decode_hash( + "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c", + ), + cmdline_hash: decode_hash( + "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87", + ), + initrd_hash: decode_hash( + "a5b3d4742045e7d08aa19953c35098e784826b01a84f60568fa69f1a848dafd96ec98b8df616d6142779c9b97318166b", + ), + gpt_disk_guid_hash: decode_hash( + "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282", + ), + }; + let policy = MeasurementPolicy { + accepted_measurements: vec![MeasurementRecord { + measurement_id: "image-hash-policy".to_string(), + measurements: ExpectedMeasurements::Image(image_hashes.clone()), + }], + }; + let platform_metadata = PlatformMetadata { + attestation_type: attest_types::AttestationType::GcpTdx, + ram_bytes: 4 * 1024 * 1024 * 1024, + num_disks: 1, + acpi: Some(AcpiHashes { loader: [0x11; 48], rsdp: [0x22; 48], tables: [0x33; 48] }), + }; + let firmware = gcp_firmware_fixture(); + let expected_measurements = + expected_dcap_registers(&image_hashes, &platform_metadata, Some(&firmware)).unwrap(); + + let measurements = MultiMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, expected_measurements.mrtd.unwrap()), + (DcapMeasurementRegister::RTMR0, expected_measurements.rtmr0.unwrap()), + (DcapMeasurementRegister::RTMR1, expected_measurements.rtmr1), + (DcapMeasurementRegister::RTMR2, expected_measurements.rtmr2), + (DcapMeasurementRegister::RTMR3, mock_tdx::MOCK_RTMR3), + ])); + + policy.check_measurement(&measurements, Some(platform_metadata)).unwrap(); + } + #[tokio::test] async fn test_buildernet_measurements() { // Refresh this fixture explicitly with: @@ -662,13 +886,13 @@ mod tests { assert!(!policy.accepted_measurements.is_empty()); assert!(matches!( - policy.check_measurement(&MultiMeasurements::NoAttestation).unwrap_err(), + policy.check_measurement(&MultiMeasurements::NoAttestation, None).unwrap_err(), AttestationError::MeasurementsNotAccepted )); // A non-specific measurement fails assert!(matches!( - policy.check_measurement(&MultiMeasurements::Azure(HashMap::new())).unwrap_err(), + policy.check_measurement(&MultiMeasurements::Azure(HashMap::new()), None).unwrap_err(), AttestationError::MeasurementsNotAccepted )); } @@ -702,6 +926,86 @@ mod tests { } } + /// A JSON policy that pins image-component hashes rather than raw + /// register values must parse into [`ExpectedMeasurements::Image`] + /// so the verifier can reconstruct the expected RTMRs from platform + /// metadata and firmware + #[tokio::test] + async fn test_parse_image_hash_policy() { + let json = r#"[ + { + "measurement_id": "gcp-image-hash-example", + "attestation_type": "gcp-tdx", + "dcap_image_hashes": { + "uki_authenticode": "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc", + "kernel_authenticode": "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c", + "cmdline_hash": "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87", + "initrd_hash": "a5b3d4742045e7d08aa19953c35098e784826b01a84f60568fa69f1a848dafd96ec98b8df616d6142779c9b97318166b", + "gpt_disk_guid_hash": "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282" + } + } + ]"#; + + let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); + assert_eq!(policy.accepted_measurements.len(), 1); + assert!(matches!( + policy.accepted_measurements[0].measurements, + ExpectedMeasurements::Image(_) + )); + } + + /// A record cannot specify both raw register values and image-component + /// hashes as they express the same intent through different schemas. + #[tokio::test] + async fn test_parse_rejects_both_measurements_and_dcap_image_hashes() { + let json = r#"[ + { + "attestation_type": "gcp-tdx", + "measurements": { + "mrtd": { + "expected_any": [ + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ] + } + }, + "dcap_image_hashes": { + "uki_authenticode": "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc", + "kernel_authenticode": "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c", + "cmdline_hash": "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87", + "initrd_hash": "a5b3d4742045e7d08aa19953c35098e784826b01a84f60568fa69f1a848dafd96ec98b8df616d6142779c9b97318166b", + "gpt_disk_guid_hash": "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282" + } + } + ]"#; + + let result = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()); + assert!(matches!(result, Err(MeasurementFormatError::BothMeasurementsAndDcapImageHashes))); + } + + /// Using one of the not yet supported attestation types with + /// dcap_image_hashes fails + #[tokio::test] + async fn test_parse_rejects_unsupported_attestation_type_for_image_hashes() { + let json = r#"[ + { + "attestation_type": "azure-tdx", + "dcap_image_hashes": { + "uki_authenticode": "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc", + "kernel_authenticode": "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c", + "cmdline_hash": "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87", + "initrd_hash": "a5b3d4742045e7d08aa19953c35098e784826b01a84f60568fa69f1a848dafd96ec98b8df616d6142779c9b97318166b", + "gpt_disk_guid_hash": "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282" + } + } + ]"#; + + let result = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()); + assert!(matches!( + result, + Err(MeasurementFormatError::DcapImageHashesUnsupportedAttestationType(_)) + )); + } + #[tokio::test] async fn test_check_measurement_with_or_semantics() { let json = r#"[ @@ -724,17 +1028,17 @@ mod tests { // First value should match let measurements1 = MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0u8; 48])])); - assert!(policy.check_measurement(&measurements1).is_ok()); + assert!(policy.check_measurement(&measurements1, None).is_ok()); // Second value should also match let measurements2 = MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0x11u8; 48])])); - assert!(policy.check_measurement(&measurements2).is_ok()); + assert!(policy.check_measurement(&measurements2, None).is_ok()); // Different value should not match let measurements3 = MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0x22u8; 48])])); - assert!(policy.check_measurement(&measurements3).is_err()); + assert!(policy.check_measurement(&measurements3, None).is_err()); } #[tokio::test] @@ -814,21 +1118,21 @@ mod tests { (DcapMeasurementRegister::MRTD, [0u8; 48]), (DcapMeasurementRegister::RTMR0, [0x11u8; 48]), ])); - assert!(policy.check_measurement(&measurements1).is_ok()); + assert!(policy.check_measurement(&measurements1, None).is_ok()); // Both match (single + second of any) let measurements2 = MultiMeasurements::Dcap(HashMap::from([ (DcapMeasurementRegister::MRTD, [0u8; 48]), (DcapMeasurementRegister::RTMR0, [0x22u8; 48]), ])); - assert!(policy.check_measurement(&measurements2).is_ok()); + assert!(policy.check_measurement(&measurements2, None).is_ok()); // Single matches but any doesn't let measurements3 = MultiMeasurements::Dcap(HashMap::from([ (DcapMeasurementRegister::MRTD, [0u8; 48]), (DcapMeasurementRegister::RTMR0, [0x33u8; 48]), ])); - assert!(policy.check_measurement(&measurements3).is_err()); + assert!(policy.check_measurement(&measurements3, None).is_err()); } #[tokio::test] diff --git a/crates/attestation/test-assets/gcp-tdx-1782809233226668671 b/crates/attestation/test-assets/gcp-tdx-1782809233226668671 new file mode 100644 index 0000000..d04b298 Binary files /dev/null and b/crates/attestation/test-assets/gcp-tdx-1782809233226668671 differ diff --git a/crates/attestation/test-assets/gcp-tdx-collateral-1782809233226668671.yaml b/crates/attestation/test-assets/gcp-tdx-collateral-1782809233226668671.yaml new file mode 100644 index 0000000..567d688 --- /dev/null +++ b/crates/attestation/test-assets/gcp-tdx-collateral-1782809233226668671.yaml @@ -0,0 +1,109 @@ +pck_crl_issuer_chain: | + -----BEGIN CERTIFICATE----- + MIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC + MGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD + b3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw + CQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg + BgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs + IENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex + CzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO + 2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJl + eTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS + BgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy + dmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d + zb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB + Af8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1Ue + nA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+ + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw + aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv + cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ + BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG + A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 + aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT + AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 + 1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB + uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ + MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 + ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV + Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI + KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg + AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= + -----END CERTIFICATE----- +root_ca_crl: >- + 308201223081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232363133303430305a170d3237303232363133303430305aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020349003046022100c252ed59c795ba2b11496a4a99758bb8cbc380a1ebbb0865be69f2c4b38bb6400221009a7d8b03602a9ee2d62322d759166d6933d24d9dfa01ab3fde4520691d715bd7 +pck_crl: >- + 30820d1830820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303633303037353632315a170d3236303733303037353632315a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303633303037353632315a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303633303037353632315a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303633303037353632315a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303633303037353632315a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303633303037353632315a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303633303037353632315a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303633303037353632315a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303633303037353632315a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303633303037353632315a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303633303037353632315a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303633303037353632315a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303633303037353632315a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303633303037353632315a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303633303037353632315a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303633303037353632315a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303633303037353632315a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303633303037353632315a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303633303037353632315a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303633303037353632315a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303633303037353632315a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303633303037353632315a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303633303037353632315a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303633303037353632315a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303633303037353632315a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303633303037353632315a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303633303037353632315a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303633303037353632315a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303633303037353632315a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303633303037353632315a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303633303037353632315a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303633303037353632315a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303633303037353632315a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303633303037353632315a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303633303037353632315a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303633303037353632315a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303633303037353632315a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303633303037353632315a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303633303037353632315a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303633303037353632315a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303633303037353632315a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303633303037353632315a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303633303037353632315a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303633303037353632315a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303633303037353632315a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303633303037353632315a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303633303037353632315a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303633303037353632315a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303633303037353632315a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303633303037353632315a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303633303037353632315a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303633303037353632315a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303633303037353632315a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303633303037353632315a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303633303037353632315a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303633303037353632315a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303633303037353632315a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303633303037353632315a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d0403020349003046022100f6319860a35e9dc53a9693abe9e82bfcdd2e3f3774cb3e10547f742e241b09bc022100f16e9c2545ed59911c6b02ad73cb48204797b00a4428c421a7d30eeab7ed4c56 +tcb_info_issuer_chain: | + -----BEGIN CERTIFICATE----- + MIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw + aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv + cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ + BgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG + A1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw + b3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD + VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv + P+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju + ypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f + BEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz + LmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK + QEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG + SM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh + AKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5 + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw + aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv + cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ + BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG + A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 + aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT + AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 + 1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB + uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ + MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 + ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV + Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI + KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg + AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= + -----END CERTIFICATE----- +tcb_info: "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-06-30T07:53:47Z\",\"nextUpdate\":\"2026-07-30T07:53:47Z\",\"fmspc\":\"00806F050000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":19,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2025-05-14T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2025-05-14T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":8,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":8,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":6,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":8,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2025-05-14T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":7,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":7,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":7,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01010\",\"INTEL-SA-01036\",\"INTEL-SA-01076\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":6,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":6,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":6,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00960\",\"INTEL-SA-00982\",\"INTEL-SA-00986\",\"INTEL-SA-01010\",\"INTEL-SA-01036\",\"INTEL-SA-01076\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":5,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2023-02-15T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00837\",\"INTEL-SA-00960\",\"INTEL-SA-00982\",\"INTEL-SA-00986\",\"INTEL-SA-01010\",\"INTEL-SA-01036\",\"INTEL-SA-01076\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":5,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-00960\",\"INTEL-SA-00982\",\"INTEL-SA-00986\",\"INTEL-SA-01010\",\"INTEL-SA-01036\",\"INTEL-SA-01076\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}" +tcb_info_signature: >- + 9a0940311cebac796d0c1e6378331a21d712afcbc81b6cb2accd222cef33a55bda5e6a94eb406fb6506911c5293d6b848e9d77a776938821e6eaecc20f37a759 +qe_identity_issuer_chain: | + -----BEGIN CERTIFICATE----- + MIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw + aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv + cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ + BgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG + A1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw + b3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD + VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv + P+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju + ypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f + BEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz + LmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK + QEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG + SM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh + AKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5 + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw + aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv + cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ + BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG + A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 + aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT + AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 + 1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB + uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ + MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 + ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV + Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI + KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg + AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= + -----END CERTIFICATE----- +qe_identity: "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-06-30T08:12:19Z\",\"nextUpdate\":\"2026-07-30T08:12:19Z\",\"tcbEvaluationDataNumber\":19,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2025-05-14T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}" +qe_identity_signature: >- + 078a0202d36801e43dd3bfb7b8f77cefc2ed23558cb3472550c21f43a0bdffd7b10bc2a70c50b589c4f07d0df53e7a4be9082775ac4232f5c17a39f1e24da491 diff --git a/crates/attestation/test-assets/gcp-tdx-firmware-1782809233226668671.yaml b/crates/attestation/test-assets/gcp-tdx-firmware-1782809233226668671.yaml new file mode 100644 index 0000000..65618b6 --- /dev/null +++ b/crates/attestation/test-assets/gcp-tdx-firmware-1782809233226668671.yaml @@ -0,0 +1,9 @@ +mrtd: >- + 9bf86e6280ec4282b8b5822d8166410a456cdb720109aa799f0011fa63df1de3ee5e35e293fc410c061433163acb03a6 +cfv: >- + d10d31022a1ef32ee501f31745257f84e7c753a8b79a3fdb82ffda9dc57684a1cfefba3edcdabc8b4ea791c17dd8a02b +hob: + bytes: >- + 010038000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000489280000000000003003000000000000000000000000000000000000000000000000000070000000000e2ff0000000000001e000000000003003000000000000000000000000000000000000000000000000000070000000000e0ff000000000000020000000000030030000000000000000000000000000000000000000000000000000700000000008100000000000000010000000000030030000000000000000000000000000000000000000000000000000700000000b08000000000000020000000000000030030000000000000000000000000000000000000000000000000000700000000908000000000000020000000000000030030000000000000000000000000000000000000000000000000000700000000008000000000000060000000000000030030000000000000000000000000000000000000000000070000000700001000000000000000000000800000000000030030000000000000000000000000000000000000000000070000000700001000608000000000000030000000000000030030000000000000000000000000000000000000000000070000000700001000d080000000000000300000000000000300300000000000000000000000000000000000000000000700000007000010000082000000000000007ebf00000000030030000000000000000000000000000000000000000000070000000700001000000000010000000000000000000000 + length_offset: 576 + ram_threshold: 3221225472 diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 1495611..a9396ed 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -7,10 +7,12 @@ use std::{ }; pub use attestation::{ + AttestationEvidence, AttestationExchangeMessage, AttestationGenerator, AttestationType, AttestationVerifier, + PlatformMetadata, }; use ra_tls::{ attestation::{Attestation, AttestationQuote, VersionedAttestation}, @@ -534,16 +536,13 @@ impl AttestedCertificateVerifier { ra_tls::attestation::from_cert(cert) && let AttestationQuote::DstackTdx(tdx_quote) = attestation.quote { - if let Ok(message) = - serde_json::from_slice::(&tdx_quote.quote) - { - return Ok(message); - } - - return Ok(AttestationExchangeMessage { - attestation_type: AttestationType::DcapTdx, - attestation: tdx_quote.quote, - }); + return serde_json::from_slice::(&tdx_quote.quote).map_err( + |err| { + rustls::Error::General(format!( + "Failed to parse AttestationExchangeMessage: {err:?}" + )) + }, + ); } // If that fails, extract and parse the extension