diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93e8fc1..4052992 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,11 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libtss2-dev + - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: @@ -28,7 +33,7 @@ jobs: ${{ runner.os }}-cargo- - name: Run cargo clippy - run: cargo clippy -- -D warnings + run: cargo clippy --workspace -- -D warnings - name: Run cargo test - run: cargo test + run: cargo test --workspace --all-targets diff --git a/.gitignore b/.gitignore index c9d112b..1d155a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target remote-cert.crt /quotes - +ca.crt diff --git a/Cargo.lock b/Cargo.lock index de55d45..bd6566a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 2.0.17", "time", ] @@ -141,6 +141,8 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "az-tdx-vtpm", + "base64 0.22.1", "bytes", "clap", "configfs-tsm", @@ -150,23 +152,29 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "num-bigint", + "once_cell", + "openssl", "parity-scale-codec", "pem-rfc7468", "rand_core 0.6.4", "rcgen", "reqwest", "rustls-pemfile", + "rustls-webpki 0.103.8", "serde", "serde_json", "sha2", "tdx-quote", "tempfile", - "thiserror", + "thiserror 2.0.17", + "time", "tokio", "tokio-rustls", "tower-http", "tracing", "tracing-subscriber", + "tss-esapi", "webpki-roots", "x509-parser", ] @@ -229,24 +237,142 @@ dependencies = [ "tracing", ] +[[package]] +name = "az-cvm-vtpm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3d0900c6757c9674b05b0479236458297026e25fb505186dc8d7735091a21c" +dependencies = [ + "bincode 1.3.3", + "jsonwebkey", + "memoffset", + "openssl", + "serde", + "serde-big-array", + "serde_json", + "sev", + "sha2", + "thiserror 2.0.17", + "tss-esapi", + "zerocopy", +] + +[[package]] +name = "az-tdx-vtpm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04849677b3c0704d4593d89940cde0dc0caad2202bf9fb29352e153782b91ff8" +dependencies = [ + "az-cvm-vtpm", + "base64-url", + "bincode 1.3.3", + "serde", + "serde_json", + "thiserror 2.0.17", + "ureq", + "zerocopy", +] + [[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-url" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5b428e9fb429c6fda7316e9b006f993e6b4c33005e4659339fb5214479dddec" +dependencies = [ + "base64 0.22.1", +] + [[package]] name = "base64ct" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitfield" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -393,6 +519,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "codicon" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12170080f3533d6f09a19f81596f836854d0fa4867dc32c8172b8474b4e9de61" + [[package]] name = "colorchoice" version = "1.0.4" @@ -431,22 +563,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -548,7 +664,7 @@ checksum = "435989ce7ba46ba3f837f9df3c8139469e72ae810e707893b19f8b6b370d14ef" dependencies = [ "anyhow", "asn1_der", - "base64", + "base64 0.22.1", "borsh", "byteorder", "chrono", @@ -662,6 +778,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -745,21 +882,32 @@ dependencies = [ ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ - "cfg-if", + "enumflags2_derive", ] [[package]] -name = "enum-as-inner" -version = "0.6.1" +name = "enumflags2_derive" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ - "heck", "proc-macro2", "quote", "syn", @@ -1044,7 +1192,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -1067,7 +1215,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1081,6 +1229,12 @@ dependencies = [ "digest", ] +[[package]] +name = "hostname-validator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" + [[package]] name = "http" version = "1.3.1" @@ -1173,29 +1327,13 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1208,11 +1346,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.1", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -1338,6 +1474,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "iocuddle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8972d5be69940353d5347a1344cb375d9b457d6809b428b05bb1ca2fb9ce007" + [[package]] name = "ipconfig" version = "0.3.2" @@ -1388,6 +1530,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebkey" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57c852b14147e2bd58c14fde40398864453403ef632b1101db130282ee6e2cc" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "generic-array", + "num-bigint", + "serde", + "serde_json", + "thiserror 1.0.69", + "yasna 0.4.0", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -1421,6 +1580,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1469,12 +1638,31 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "mbox" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d142aeadbc4e8c679fc6d93fbe7efe1c021fa7d80629e615915b519e3bc6de" +dependencies = [ + "libc", + "stable_deref_trait", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1526,23 +1714,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -1594,6 +1765,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1624,6 +1806,15 @@ dependencies = [ "libm", ] +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -1655,7 +1846,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1676,10 +1867,13 @@ dependencies = [ ] [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "openssl-src" +version = "300.5.4+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] [[package]] name = "openssl-sys" @@ -1689,10 +1883,17 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "p256" version = "0.13.2" @@ -1778,7 +1979,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1787,7 +1988,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -1806,6 +2007,41 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "picky-asn1" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295eea0f33c16be21e2a98b908fdd4d73c04dd48c8480991b76dbcf0cb58b212" +dependencies = [ + "oid", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-der" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df7873a9e36d42dadb393bea5e211fe83d793c172afad5fb4ec846ec582793f" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5f20f71a68499ff32310f418a6fad8816eac1a2859ed3f0c5c741389dd6208" +dependencies = [ + "base64 0.21.7", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1916,7 +2152,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.1", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -1937,7 +2173,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -2046,7 +2282,16 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "yasna", + "yasna 0.5.2", +] + +[[package]] +name = "rdrand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" +dependencies = [ + "rand_core 0.6.4", ] [[package]] @@ -2055,7 +2300,30 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2081,25 +2349,20 @@ version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2", "hickory-resolver", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -2111,7 +2374,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tower", "tower-http", @@ -2203,7 +2465,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -2302,15 +2564,6 @@ dependencies = [ "syn", ] -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2331,29 +2584,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.27" @@ -2370,6 +2600,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-human-bytes" version = "0.1.1" @@ -2380,6 +2619,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2437,6 +2686,32 @@ dependencies = [ "serde", ] +[[package]] +name = "sev" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420c6c161b5d6883d8195584a802b114af6c884ed56d937d994e30f7f81d54ec" +dependencies = [ + "base64 0.22.1", + "bincode 2.0.1", + "bitfield 0.19.4", + "bitflags 2.10.0", + "byteorder", + "codicon", + "dirs", + "hex", + "iocuddle", + "lazy_static", + "libc", + "openssl", + "rdrand", + "serde", + "serde-big-array", + "serde_bytes", + "static_assertions", + "uuid", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2536,6 +2811,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2579,27 +2860,6 @@ dependencies = [ "syn", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tagptr" version = "0.2.0" @@ -2612,6 +2872,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tdx-quote" version = "0.0.4" @@ -2639,13 +2905,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2752,16 +3038,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -2833,11 +3109,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-core", "futures-util", @@ -2952,6 +3228,39 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tss-esapi" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ea9ccde878b029392ac97b5be1f470173d06ea41d18ad0bb3c92794c16a0f2" +dependencies = [ + "bitfield 0.14.0", + "enumflags2", + "getrandom 0.2.16", + "hostname-validator", + "log", + "mbox", + "num-derive", + "num-traits", + "oid", + "picky-asn1", + "picky-asn1-x509", + "regex", + "serde", + "tss-esapi-sys", + "zeroize", +] + +[[package]] +name = "tss-esapi-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535cd192581c2ec4d5f82e670b1d3fbba6a23ccce8c85de387642051d7cad5b5" +dependencies = [ + "pkg-config", + "target-lexicon", +] + [[package]] name = "typenum" version = "1.19.0" @@ -2982,6 +3291,26 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "log", + "once_cell", + "serde", + "serde_json", + "url", +] + [[package]] name = "url" version = "2.5.7" @@ -3020,6 +3349,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.4", "js-sys", + "serde", "wasm-bindgen", ] @@ -3041,6 +3371,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "want" version = "0.3.1" @@ -3158,47 +3494,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -3232,7 +3533,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3272,7 +3573,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -3497,7 +3798,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 2.0.17", "time", ] @@ -3523,6 +3824,15 @@ dependencies = [ "x509-ocsp", ] +[[package]] +name = "yasna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" +dependencies = [ + "num-bigint", +] + [[package]] name = "yasna" version = "0.5.2" @@ -3601,6 +3911,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 420f1e4..061364e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,9 @@ license = "MIT" [dependencies] tokio = { version = "1.48.0", features = ["full"] } -tokio-rustls = { version = "0.26.4", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26.4", default-features = false, features = [ + "ring", +] } sha2 = "0.10.9" x509-parser = "0.18.0" thiserror = "2.0.17" @@ -30,14 +32,27 @@ bytes = "1.10.1" http = "1.3.1" serde_json = "1.0.145" serde = "1.0.228" +base64 = "0.22.1" +reqwest = { version = "0.12.23", default-features = false, features = [ + "rustls-tls-webpki-roots-no-provider", +] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } parity-scale-codec = "3.7.5" -reqwest = "0.12.23" +openssl = "0.10.75" +az-tdx-vtpm = { version = "0.7.4", optional = true } +tss-esapi = { version = "7.6.0", optional = true } +num-bigint = "0.4.6" +webpki = { package = "rustls-webpki", version = "0.103.8" } +time = "0.3.44" +once_cell = "1.21.3" axum = "0.8.6" tower-http = { version = "0.6.7", features = ["fs"] } [dev-dependencies] rcgen = "0.14.5" -reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls-webpki-roots-no-provider"] } tempfile = "3.23.0" + +[features] +default = ["azure"] +azure = ["tss-esapi", "az-tdx-vtpm"] diff --git a/README.md b/README.md index f097980..d09df01 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Header value: an attestation type given as a string as described below. These are the attestation type names used in the HTTP headers, and the measurements file, and when specifying a local attestation type with the `--client-attestation-type` or `--server-attestation-type` command line options. +- `auto` - detect attestation type (used only when specifying the local attestation type as a command-line argument) - `none` - No attestation provided - `dummy` - Forwards the attestation to a remote service (for testing purposes, not yet supported) - `gcp-tdx` - DCAP TDX on Google Cloud Platform @@ -138,7 +139,13 @@ Following a successful attestation exchange, the client can make HTTP requests u As described above, the server will inject measurement data into the request headers before forwarding them to the target service, and the client will inject measurement data into the response headers before forwarding them to the source client. -### CLI differences from `cvm-reverse-proxy` +## Dependencies and feature flags + +The `azure` feature, for Microsoft Azure attestation requires [tpm2](https://tpm2-software.github.io) to be installed. On Debian-based systems this is provided by [`libtss2-dev`](https://packages.debian.org/trixie/libtss2-dev), and on nix `tpm2-tss`. + +This feature is enabled by default. For non-azure deployments you can compile without this requirement by specifying `--no-default-features`. But note that this is will disable both generation and verification of azure attestations. + +## CLI differences from `cvm-reverse-proxy` This aims to have a similar command line interface to `cvm-reverse-proxy` but there are some differences: diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..a3fe682 --- /dev/null +++ b/shell.nix @@ -0,0 +1,12 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + + buildInputs = with pkgs;[ + tpm2-tss + openssl + ]; +} diff --git a/src/attestation/azure/ak_certificate.rs b/src/attestation/azure/ak_certificate.rs new file mode 100644 index 0000000..7efc854 --- /dev/null +++ b/src/attestation/azure/ak_certificate.rs @@ -0,0 +1,202 @@ +//! Generation and verification of AK certificates from the vTPM +use crate::attestation::azure::{nv_index, MaaError}; +use once_cell::sync::Lazy; +use std::time::Duration; +use tokio_rustls::rustls::pki_types::{CertificateDer, TrustAnchor, UnixTime}; +use webpki::EndEntityCert; + +/// The NV index where we expect to be able to read the AK certificate from the vTPM +const TPM_AK_CERT_IDX: u32 = 0x1C101D0; + +// microsoftRSADevicesRoot2021 is the root CA certificate used to sign Azure TDX vTPM certificates. +// This is different from the AME root CA used by TrustedLaunch VMs. +// The certificate can be downloaded from: +// http://www.microsoft.com/pkiops/certs/Microsoft%20RSA%20Devices%20Root%20CA%202021.crt +const MICROSOFT_RSA_DEVICES_ROOT_2021: &str = "-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIQGWCAkS2F96VGa+6hm2M3rjANBgkqhkiG9w0BAQwFADBa +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSsw +KQYDVQQDEyJNaWNyb3NvZnQgUlNBIERldmljZXMgUm9vdCBDQSAyMDIxMB4XDTIx +MDgyNjIzMzkxOFoXDTQ2MDgyNjIzNDcxNFowWjELMAkGA1UEBhMCVVMxHjAcBgNV +BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjErMCkGA1UEAxMiTWljcm9zb2Z0IFJT +QSBEZXZpY2VzIFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBALF4kgr3bAptorWmkrM6u47osmLfg67KxZPE4W74Zw5Bu64tjEuzegcB +6lFkoXi2V4eLdIRshk3l14jul6ghCML/6gh4hYiTExky3XMY05wg0d1o+AdhuyvC +anXvQZratosnL+KhR2qFeagthciIrCibIIKX91LvqRl/Eg8uo82fl30gieB40Sun +Pe/SfMJLb7AYbQ95yHK8G1lTFUHkIfPbAY6SfkOBUpNJ6UAtjlAmIaHYpdcdOayf +qXyhW3+Hf0Ou2wiKYJihCqh3TaI2hqmiv4p4CScug9sDcTyafA6OYLyTe3vx7Krn +BOUvkSkTj80GrXSKCWnrw+bE7z0deptPuLS6+n83ImLsBZ3XYhX4iUPmTRSU9vr7 +q0cZA8P8zAzLaeN+uK14l92u/7TMhkp5etmLE9DMd9MtnsLZSy18UpW4ZlBXxt9Z +w/RFKStlNbK5ILsI2HdSjgkF0DxZtNnCiEQehMu5DBfCdXo1P90iJhfF1MD+2Kh5 +xeuDQEC7Dh3gUSXIkOm/72u1fE52r0uY+aH1TCQGbCrijI9Jf78lFbI7L6Ll3YAa +89MrDs2tAQG0SaJdabh4k5orqaJOgaqrrq61RzcMjlZGI3dOdL+f6romKOccFkm0 +k+gwjvZ9xaJ5i9SB6Lq/GrA8YxzjmKHHVPmGGdm/v93R0oNGfyvxAgMBAAGjVDBS +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSERIYG +AJg/LKqzxYnzrC7J5p0JAzAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwF +AAOCAgEAd3RAo42nyNbVvj+mxZ03VV+ceU6nCdgIS8RZfZBxf+lqupRzKUV9UW59 +IRCSeMH3gHfGSVhmwH1AJHkFIhd5meSShF4lPPmvYMmrbfOrwiUunqz2aix/QkRp +geMOe10wm6dEHHAw/eNi3PWhc+jdGJNV0SdnqcwJg/t5db8Y7RCVW+tG3DtEa63U +B4sGNlBbaUffdSdYL5TCRXm2mkcCWruu/gmDTgoabFmI4j9ss0shsIxwqVVEq2zk +EH1ypZrHSmVrTRh9hPHWpkOxnh9yqpGDXcSll09ZZUBUhx7YUX6p+BTVWnuuyR4T +bXS8P6fUS5Q2WF0WR07BrGYlBqomsEwMhth1SmBKn6tXfQyWkgr4pVl5XkkC7Bfv +pmw90csy8ycwog+x4L9kO1Nr6OPwnJ9V39oMifNDxnvYVBX7EhjoiARPp+97feNJ +YwMt4Os/WSeD++IhBB9xVsrI+jZufySQ02C/w1LBFR6zPy+a+v+6WlvMxDBEDWOj +JyDQ6kzkWxIG35klzLnwHybuIsFIIR1QGL1l47eW2dM4hB9oCay6z3FX5xYBIFvA +yp8up+KbjfH/NIWfPBXhYMW64DagB9P2cW5LBRz+AzDA+JF/OdYpb6vxv3lzjLQb +U9zMFwSrzEF5o2Aa/n+xZ90Naj78AYaTM18DalA17037fjucDN8= +-----END CERTIFICATE-----"; + +// azureVirtualTPMRoot2023 is the root CA for Azure vTPM (used by both Trusted Launch and TDX) +// Source: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq +// Valid until: 2048-06-01 +const AZURE_VIRTUAL_TPM_ROOT_2023: &str = "-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQUfQx2iySCIpOKeDZKd5KpzANBgkqhkiG9w0BAQwFADBp +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTow +OAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhv +cml0eSAyMDIzMB4XDTIzMDYwMTE4MDg1M1oXDTQ4MDYwMTE4MTU0MVowaTELMAkG +A1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE6MDgGA1UE +AxMxQXp1cmUgVmlydHVhbCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkg +MjAyMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALoMMwvdRJ7+bW00 +adKE1VemNqJS+268Ure8QcfZXVOsVO22+PL9WRoPnWo0r5dVoomYGbobh4HC72s9 +sGY6BGRe+Ui2LMwuWnirBtOjaJ34r1ZieNMcVNJT/dXW5HN/HLlm/gSKlWzqCEx6 +gFFAQTvyYl/5jYI4Oe05zJ7ojgjK/6ZHXpFysXnyUITJ9qgjn546IJh/G5OMC3mD +fFU7A/GAi+LYaOHSzXj69Lk1vCftNq9DcQHtB7otO0VxFkRLaULcfu/AYHM7FC/S +q6cJb9Au8K/IUhw/5lJSXZawLJwHpcEYzETm2blad0VHsACaLNucZL5wBi8GEusQ +9Wo8W1p1rUCMp89pufxa3Ar9sYZvWeJlvKggWcQVUlhvvIZEnT+fteEvwTdoajl5 +qSvZbDPGCPjb91rSznoiLq8XqgQBBFjnEiTL+ViaZmyZPYUsBvBY3lKXB1l2hgga +hfBIag4j0wcgqlL82SL7pAdGjq0Fou6SKgHnkkrV5CNxUBBVMNCwUoj5mvEjd5mF +7XPgfM98qNABb2Aqtfl+VuCkU/G1XvFoTqS9AkwbLTGFMS9+jCEU2rw6wnKuGv1T +x9iuSdNvsXt8stx4fkVeJvnFpJeAIwBZVgKRSTa3w3099k0mW8qGiMnwCI5SfdZ2 +SJyD4uEmszsnieE6wAWd1tLLg1jvAgMBAAGjVDBSMA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRL/iZalMH2M8ODSCbd8+WwZLKqlTAQ +BgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEALgNAyg8I0ANNO/8I +2BhpTOsbywN2YSmShAmig5h4sCtaJSM1dRXwA+keY6PCXQEt/PRAQAiHNcOF5zbu +OU1Bw/Z5Z7k9okt04eu8CsS2Bpc+POg9js6lBtmigM5LWJCH1goMD0kJYpzkaCzx +1TdD3yjo0xSxgGhabk5Iu1soD3OxhUyIFcxaluhwkiVINt3Jhy7G7VJTlEwkk21A +oOrQxUsJH0f2GXjYShS1r9qLPzLf7ykcOm62jHGmLZVZujBzLIdNk1bljP9VuGW+ +cISBwzkNeEMMFufcL2xh6s/oiUnXicFWvG7E6ioPnayYXrHy3Rh68XLnhfpzeCzv +bz/I4yMV38qGo/cAY2OJpXUuuD/ZbI5rT+lRBEkDW1kxHP8cpwkRwGopV8+gX2KS +UucIIN4l8/rrNDEX8T0b5U+BUqiO7Z5YnxCya/H0ZIwmQnTlLRTU2fW+OGG+xyIr +jMi/0l6/yWPUkIAkNtvS/yO7USRVLPbtGVk3Qre6HcqacCXzEjINcJhGEVg83Y8n +M+Y+a9J0lUnHytMSFZE85h88OseRS2QwqjozUo2j1DowmhSSUv9Na5Ae22ycciBk +EZSq8a4rSlwqthaELNpeoTLUk6iVoUkK/iLvaMvrkdj9yJY1O/gvlfN2aiNTST/2 +bd+PA4RBToG9rXn6vNkUWdbLibU= +-----END CERTIFICATE-----"; + +// globalVirtualTPMCA03 is the intermediate CA that issues TDX vTPM AK certificates +// Source: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq +// Issuer: Azure Virtual TPM Root Certificate Authority 2023 +// Valid: 2025-04-24 to 2027-04-24 +const GLOBAL_VIRTUAL_TPMCA03_PEM: &str = "-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgITMwAAAAknQOWscnsOpgAAAAAACTANBgkqhkiG9w0BAQwF +ADBpMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTowOAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAyMDIzMB4XDTI1MDQyNDE4MDExN1oXDTI3MDQyNDE4MDExN1owJTEj +MCEGA1UEAxMaR2xvYmFsIFZpcnR1YWwgVFBNIENBIC0gMDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDYGYtis5ka0cxQkhU11jslgX6wzjR/UXQIFdUn +8juTUMJl91VokwUPX3WfXeog7mtbWyYWD8SI0BSnchRGlV8u3AhcW61/HetHqmIL +tD0c75UATi+gsTQnpwKPA/m38MGGyXFETr3xHXjilUPfIhmxO4ImuNJ0R95bZYhx +bLYmOZpVUcj8oz980An8HlIqSzrskQR6NiuEmikHkHc1/CpoNunrr8kQNPF6gxex +IrvXsKLUAuUqnNtcQWc/8Er5EN9+TdX6AOjUmKriVGbCInP1m/aC+DWH/+aJ/8aD +pKze6fe7OHh2BL9hxqIsmJAStIh4siRdLYTt8hKGmkdzOWnRAgMBAAGjggF/MIIB +ezASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwICBDAXBgNVHSUEEDAO +BgVngQUIAQYFZ4EFCAMwHQYDVR0OBBYEFGcJhvj5gV6TrfnJZOcUCtqZywotMB8G +A1UdIwQYMBaAFEv+JlqUwfYzw4NIJt3z5bBksqqVMHYGA1UdHwRvMG0wa6BpoGeG +ZWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL0F6dXJlJTIwVmly +dHVhbCUyMFRQTSUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIw +MjMuY3JsMIGDBggrBgEFBQcBAQR3MHUwcwYIKwYBBQUHMAKGZ2h0dHA6Ly93d3cu +bWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvQXp1cmUlMjBWaXJ0dWFsJTIwVFBN +JTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMy5jcnQwDQYJ +KoZIhvcNAQEMBQADggIBAJPP3Z2z1zhzUS3qSRVgyoUVnaxCGuMHzPQAZuoPBVpz +wKnv4HqyjMgT8pBtQqxkqAsg7KiqbPfO97bMCHcuqkkfHjw8yg6IYt01RjUjVPKq +lrsY2iw7hFWNWr8SGMa10JdNYNyf5dxob5+mKAwEOhLzKNwq9rM/uIvZky77pNly +RLt55XEPfBMYdI9I8uQ5Uqmrw7mVJfERMfTBhSQF9BrcajAsaLcs7qEUyj0yUdJf +cgZkfCoUEUSPr3OwLHaYeV1J6VidhIYsYo53sXXal91d60NspYgei2nJFei/+R3E +SWnGbPBW+EQ4FbvZXxu57zUMX9mM7lC+GoXLvA6/vtKShEi9ZXl2PSnBQ/R2A7b3 +AXyg4fmMLFausEk6OiuU8E/bvp+gPLOJ8YrX7SAJVuEn+koJaK5G7os5DMIh7/KM +l9cI9WxPwqoWjp4VBfrF4hDOCmKWrqtFUDQCML8qD8RTxlQKQtgeGAcNDfoAuL9K +VtSG5/iIhuyBEFYEHa3vRWbSaHCUzaHJsTmLcz4cp1VDdepzqZRVuErBzJKFnBXb +zRNW32EFmcAUKZImIsE5dgB7y7eiijf33VWNfWmK05fxzQziWFWRYlET4SVc3jMn +PBiY3N8BfK8EBOYbLvzo0qn2n3SAmPhYX3Ag6vbbIHd4Qc8DQKHRV0PB8D3jPGmD +-----END CERTIFICATE-----"; + +/// The intermediate chain for azure +static GLOBAL_VIRTUAL_TPMCA03: Lazy>> = Lazy::new(|| { + let (_type_label, cert_der) = + pem_rfc7468::decode_vec(GLOBAL_VIRTUAL_TPMCA03_PEM.as_bytes()).expect("Cannot decode PEM"); + vec![CertificateDer::from(cert_der)] +}); + +/// The root anchors for azure +static AZURE_ROOT_ANCHORS: Lazy>> = Lazy::new(|| { + vec![ + // Microsoft RSA Devices Root CA 2021 (older VMs) + pem_to_trust_anchor(MICROSOFT_RSA_DEVICES_ROOT_2021), + // Azure Virtual TPM Root CA 2023 (TDX + newer trusted launch) + pem_to_trust_anchor(AZURE_VIRTUAL_TPM_ROOT_2023), + ] +}); + +/// Verify an AK certificate against azure root CA +pub fn verify_ak_cert_with_azure_roots(ak_cert_der: &[u8], now_secs: u64) -> Result<(), MaaError> { + let ak_cert_der: CertificateDer = ak_cert_der.into(); + let end_entity_cert = EndEntityCert::try_from(&ak_cert_der)?; + + let 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, + now, + AnyEku, + None, + None, + )?; + tracing::debug!("Successfully verified AK certificate from vTPM"); + + Ok(()) +} + +/// Retrieve an AK certificate from the vTPM +pub fn read_ak_certificate_from_tpm() -> Result, tss_esapi::Error> { + tracing::debug!("Reading AK certificate from vTPM"); + let mut context = nv_index::get_session_context()?; + nv_index::read_nv_index(&mut context, TPM_AK_CERT_IDX) +} + +/// Convert a PEM-encoded cert into a TrustAnchor +fn pem_to_trust_anchor(pem: &str) -> TrustAnchor<'static> { + let (_type_label, der_vec) = pem_rfc7468::decode_vec(pem.as_bytes()).unwrap(); + // Leaking is ok here because plan is to set this up so it is only called once + let leaked: &'static [u8] = Box::leak(der_vec.into_boxed_slice()); + let cert_der: &'static CertificateDer<'static> = + Box::leak(Box::new(CertificateDer::from(leaked))); + webpki::anchor_from_trusted_cert(cert_der).expect("Failed to create trust anchor") +} + +/// Allows any EKU - we could change this to only accept 1.3.6.1.4.1.567.10.3.12 which is the EKU +/// given in the AK certificate +struct AnyEku; + +impl webpki::ExtendedKeyUsageValidator for AnyEku { + fn validate(&self, _iter: webpki::KeyPurposeIdIter<'_, '_>) -> Result<(), webpki::Error> { + Ok(()) + } +} + +#[cfg(test)] +#[tokio::test] +async fn root_should_be_fresh() { + let response = reqwest::get( + "http://www.microsoft.com/pkiops/certs/Microsoft%20RSA%20Devices%20Root%20CA%202021.crt", + ) + .await + .unwrap(); + let ca_der = response.bytes().await.unwrap(); + assert_eq!( + pem_rfc7468::decode_vec(MICROSOFT_RSA_DEVICES_ROOT_2021.as_bytes()) + .unwrap() + .1, + ca_der + ); +} diff --git a/src/attestation/azure/mod.rs b/src/attestation/azure/mod.rs new file mode 100644 index 0000000..173f95b --- /dev/null +++ b/src/attestation/azure/mod.rs @@ -0,0 +1,367 @@ +//! Microsoft Azure Attestation (MAA) evidence generation and verification +mod ak_certificate; +mod nv_index; +use ak_certificate::{read_ak_certificate_from_tpm, verify_ak_cert_with_azure_roots}; + +use az_tdx_vtpm::{hcl, imds, report, vtpm}; +use base64::{engine::general_purpose::URL_SAFE as BASE64_URL_SAFE, Engine as _}; +use num_bigint::BigUint; +use openssl::{error::ErrorStack, pkey::PKey}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use x509_parser::prelude::*; + +use crate::attestation::{dcap::verify_dcap_attestation, measurements::MultiMeasurements}; + +/// The attestation evidence payload that gets sent over the channel +#[derive(Debug, Serialize, Deserialize)] +struct AttestationDocument { + /// TDX quote from the IMDS + tdx_quote_base64: String, + /// Serialized HCL report + hcl_report_base64: String, + /// vTPM related evidence + tpm_attestation: TpmAttest, +} + +/// TPM related components of the attestation document +#[derive(Debug, Serialize, Deserialize)] +struct TpmAttest { + /// Attestation Key certificate from vTPM + ak_certificate_pem: String, + /// vTPM quote + quote: vtpm::Quote, + /// Raw TCG event log bytes (UEFI + IMA) [currently not used] + /// + /// `/sys/kernel/security/ima/ascii_runtime_measurements`, + /// `/sys/kernel/security/tpm0/binary_bios_measurements`, + event_log: Vec, + /// Optional platform / instance metadata used to bind or verify the AK [currently not used] + instance_info: Option>, +} + +/// Generate a TDX attestation on Azure +pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaError> { + let td_report = report::get_report()?; + + // This makes a request to Azure Instance metadata service and gives us a binary response + let td_quote_bytes = imds::get_td_quote(&td_report)?; + + let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?; + + let ak_certificate_der = read_ak_certificate_from_tpm()?; + + let tpm_attestation = TpmAttest { + ak_certificate_pem: pem_rfc7468::encode_string( + "CERTIFICATE", + pem_rfc7468::LineEnding::default(), + &ak_certificate_der, + )?, + quote: vtpm::get_quote(&input_data[..32])?, + event_log: Vec::new(), + instance_info: None, + }; + + let attestation_document = AttestationDocument { + tdx_quote_base64: BASE64_URL_SAFE.encode(&td_quote_bytes), + hcl_report_base64: BASE64_URL_SAFE.encode(&hcl_report_bytes), + tpm_attestation, + }; + + tracing::info!("Successfully generated azure attestation: {attestation_document:?}"); + Ok(serde_json::to_vec(&attestation_document)?) +} + +/// Verify a TDX attestation from Azure +pub async fn verify_azure_attestation( + input: Vec, + expected_input_data: [u8; 64], + pccs_url: Option, +) -> Result { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + verify_azure_attestation_with_given_timestamp(input, expected_input_data, pccs_url, now).await +} + +/// Do the verification, passing in the current time +/// This allows us to test this function without time checks going out of date +async fn verify_azure_attestation_with_given_timestamp( + input: Vec, + expected_input_data: [u8; 64], + pccs_url: Option, + now: u64, +) -> Result { + let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; + tracing::info!("Attempting to verifiy azure attestation: {attestation_document:?}"); + + let hcl_report_bytes = BASE64_URL_SAFE.decode(attestation_document.hcl_report_base64)?; + + let hcl_report = hcl::HclReport::new(hcl_report_bytes)?; + let var_data_hash = hcl_report.var_data_sha256(); + + // Check that HCL var data hash matches TDX quote report data + let mut expected_tdx_input_data = [0u8; 64]; + expected_tdx_input_data[..32].copy_from_slice(&var_data_hash); + + // Do DCAP verification + let tdx_quote_bytes = BASE64_URL_SAFE.decode(attestation_document.tdx_quote_base64)?; + let _dcap_measurements = + verify_dcap_attestation(tdx_quote_bytes, expected_tdx_input_data, pccs_url).await?; + + let hcl_ak_pub = hcl_report.ak_pub()?; + + // Get attestation key from runtime claims + let ak_from_claims = { + let runtime_data_raw = hcl_report.var_data(); + let claims: HclRuntimeClaims = serde_json::from_slice(runtime_data_raw)?; + + let ak_jwk = claims + .keys + .iter() + .find(|k| k.kid == "HCLAkPub") + .ok_or(MaaError::ClaimsMissingHCLAkPub)?; + + RsaPubKey::from_jwk(ak_jwk)? + }; + + // Check that the TD report input data matches the HCL var data hash + let td_report: az_tdx_vtpm::tdx::TdReport = hcl_report.try_into()?; + if var_data_hash != td_report.report_mac.reportdata[..32] { + return Err(MaaError::TdReportInputMismatch); + } + + // Verify the vTPM quote + let vtpm_quote = attestation_document.tpm_attestation.quote; + let hcl_ak_pub_der = hcl_ak_pub + .key + .try_to_der() + .map_err(|_| MaaError::JwkConversion)?; + let pub_key = PKey::public_key_from_der(&hcl_ak_pub_der)?; + vtpm_quote.verify(&pub_key, &expected_input_data[..32])?; + + let pcrs = vtpm_quote.pcrs_sha256(); + + // Parse AK certificate + let (_type_label, ak_certificate_der) = pem_rfc7468::decode_vec( + attestation_document + .tpm_attestation + .ak_certificate_pem + .as_bytes(), + )?; + + let (remaining_bytes, ak_certificate) = X509Certificate::from_der(&ak_certificate_der)?; + + // Check that AK public key matches that from TPM quote and HCL claims + let ak_from_certificate = RsaPubKey::from_certificate(&ak_certificate)?; + let ak_from_hcl = RsaPubKey::from_openssl_pubkey(&pub_key)?; + if ak_from_claims != ak_from_hcl { + return Err(MaaError::AkFromClaimsNotEqualAkFromHcl); + } + if ak_from_claims != ak_from_certificate { + 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)?; + + Ok(MultiMeasurements::from_pcrs(pcrs)) +} + +/// JSON Web Key used in [HclRuntimeClaims] +#[derive(Debug, Deserialize)] +struct Jwk { + #[allow(unused)] + pub kty: String, + pub kid: String, + #[allow(unused)] + pub n: Option, + #[allow(unused)] + pub e: Option, + // other fields ignored +} + +/// The internal data structure for HCL runtime claims +#[derive(Debug, serde::Deserialize)] +struct HclRuntimeClaims { + keys: Vec, + #[allow(unused)] + #[serde(rename = "vm-configuration")] + vm_config: Option, + #[allow(unused)] + #[serde(rename = "user-data")] + user_data: Option, +} + +/// This is only used as a common type to compare public keys with different formats +#[derive(Debug, PartialEq)] +struct RsaPubKey { + n: BigUint, + e: BigUint, +} + +impl RsaPubKey { + fn from_jwk(jwk: &Jwk) -> Result { + if jwk.kty != "RSA" { + return Err(MaaError::NotRsa); + } + + use base64::engine::general_purpose::URL_SAFE_NO_PAD; + let n_bytes = URL_SAFE_NO_PAD.decode(jwk.n.clone().ok_or(MaaError::JwkParse)?)?; + let e_bytes = URL_SAFE_NO_PAD.decode(jwk.e.clone().ok_or(MaaError::JwkParse)?)?; + + Ok(Self { + n: BigUint::from_bytes_be(&n_bytes), + e: BigUint::from_bytes_be(&e_bytes), + }) + } + + fn from_certificate(cert: &X509Certificate) -> Result { + let spki = cert.public_key(); + let rsa_from_cert = match spki.parsed() { + Ok(x509_parser::public_key::PublicKey::RSA(rsa)) => rsa, + _ => return Err(MaaError::NotRsa), + }; + + Ok(Self { + n: BigUint::from_bytes_be(rsa_from_cert.modulus), + e: BigUint::from_bytes_be(rsa_from_cert.exponent), + }) + } + + fn from_openssl_pubkey(key: &PKey) -> Result { + let rsa_from_pkey = key.rsa()?; + + Ok(Self { + n: BigUint::from_bytes_be(&rsa_from_pkey.n().to_vec()), + e: BigUint::from_bytes_be(&rsa_from_pkey.e().to_vec()), + }) + } +} + +#[derive(Error, Debug)] +pub enum MaaError { + #[error("Report: {0}")] + Report(#[from] az_tdx_vtpm::report::ReportError), + #[error("IMDS: {0}")] + Imds(#[from] imds::ImdsError), + #[error("vTPM report: {0}")] + VtpmReport(#[from] az_tdx_vtpm::vtpm::ReportError), + #[error("HCL: {0}")] + Hcl(#[from] hcl::HclError), + #[error("JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("vTPM quote: {0}")] + VtpmQuote(#[from] vtpm::QuoteError), + #[error("AK public key: {0}")] + AkPub(#[from] vtpm::AKPubError), + #[error("vTPM quote could not be verified: {0}")] + TpmQuoteVerify(#[from] vtpm::VerifyError), + #[error("vTPM read: {0}")] + TssEsapi(#[from] tss_esapi::Error), + #[error("PEM encode: {0}")] + Pem(#[from] pem_rfc7468::Error), + #[error("TD report input does not match hashed HCL var data")] + TdReportInputMismatch, + #[error("Base64 decode: {0}")] + Base64(#[from] base64::DecodeError), + #[error("Attestation Key from HCL runtime claims does not match that from HCL report")] + AkFromClaimsNotEqualAkFromHcl, + #[error("Attestation Key from HCL runtime claims does not match that from attestation key certificate")] + AkFromClaimsNotEqualAkFromCertificate, + #[error("WebPKI: {0}")] + WebPki(#[from] webpki::Error), + #[error("X509 parse: {0}")] + X509Parse(#[from] x509_parser::asn1_rs::Err), + #[error("X509: {0}")] + X509(#[from] x509_parser::error::X509Error), + #[error("Cannot encode JSON web key as DER")] + JwkConversion, + #[error("OpenSSL: {0}")] + OpenSSL(#[from] ErrorStack), + #[error("Cannot extract measurements from quote")] + CannotExtractMeasurementsFromQuote, + #[error("Expected AK key to be RSA")] + NotRsa, + #[error("JSON web key has missing field")] + JwkParse, + #[error("HCL runtime claims is missing HCLAkPub field")] + ClaimsMissingHCLAkPub, + #[error("DCAP verification: {0}")] + DcapVerification(#[from] crate::attestation::dcap::DcapVerificationError), +} + +#[cfg(test)] +mod tests { + use crate::attestation::measurements::MeasurementPolicy; + + use super::*; + + #[tokio::test] + async fn test_decode_hcl() { + // From cvm-reverse-proxy/internal/attestation/azure/tdx/testdata/hclreport.bin + let hcl_bytes: &'static [u8] = include_bytes!("../../../test-assets/hclreport.bin"); + + let hcl_report = hcl::HclReport::new(hcl_bytes.to_vec()).unwrap(); + let hcl_var_data = hcl_report.var_data(); + let var_data_values: serde_json::Value = serde_json::from_slice(&hcl_var_data).unwrap(); + + // Check that it contains 64 byte user data + assert_eq!( + hex::decode(var_data_values["user-data"].as_str().unwrap()) + .unwrap() + .len(), + 64 + ); + } + + /// Verify a stored attestation from a test-deployment on azure + #[tokio::test] + async fn test_verify() { + let attestation_bytes: &'static [u8] = + include_bytes!("../../../test-assets/azure-tdx-1764662251380464271"); + + // To avoid this test stopping working when the certificate is no longer valid we pass in a + // timestamp + let now = 1764621240; + + let measurements_json = br#" + [{ + "measurement_id": "cvm-image-azure-tdx.rootfs-20241107200854.wic.vhd", + "attestation_type": "azure-tdx", + "measurements": { + "4": { + "expected": "c4a25a6d7704629f63db84d20ea8db0e9ce002b2801be9a340091fe7ac588699" + }, + "9": { + "expected": "9f4a5775122ca4703e135a9ae6041edead0064262e399df11ca85182b0f1541d" + }, + "11": { + "expected": "abd7c695ffdb6081e99636ee016d1322919c68d049b698b399d22ae215a121bf" + } + } + }] + "#; + + let measurement_policy = MeasurementPolicy::from_json_bytes(measurements_json.to_vec()) + .await + .unwrap(); + + let measurements = verify_azure_attestation_with_given_timestamp( + attestation_bytes.to_vec(), + [0; 64], // Input data + None, + now, + ) + .await + .unwrap(); + + measurement_policy.check_measurement(&measurements).unwrap(); + } +} diff --git a/src/attestation/azure/nv_index.rs b/src/attestation/azure/nv_index.rs new file mode 100644 index 0000000..d1cfe85 --- /dev/null +++ b/src/attestation/azure/nv_index.rs @@ -0,0 +1,21 @@ +use tss_esapi::{ + handles::NvIndexTpmHandle, + interface_types::{resource_handles::NvAuth, session_handles::AuthSession}, + tcti_ldr::{DeviceConfig, TctiNameConf}, + Context, +}; + +pub fn get_session_context() -> Result { + let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default()); + let mut context = Context::new(conf)?; + let auth_session = AuthSession::Password; + context.set_sessions((Some(auth_session), None, None)); + Ok(context) +} + +pub fn read_nv_index(ctx: &mut Context, index: u32) -> Result, tss_esapi::Error> { + tracing::debug!("Reading from TPM, nv index: {index}"); + let nv_tpm_handle = NvIndexTpmHandle::new(index)?; + let buf = tss_esapi::abstraction::nv::read_full(ctx, NvAuth::Owner, nv_tpm_handle)?; + Ok(buf.to_vec()) +} diff --git a/src/attestation/dcap.rs b/src/attestation/dcap.rs index 6ddb39a..5ed2b33 100644 --- a/src/attestation/dcap.rs +++ b/src/attestation/dcap.rs @@ -1,21 +1,21 @@ //! Data Center Attestation Primitives (DCAP) evidence generation and verification -use crate::attestation::{ - measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, - AttestationError, -}; +use crate::attestation::{measurements::MultiMeasurements, AttestationError}; use configfs_tsm::QuoteGenerationError; use dcap_qvl::{ collateral::get_collateral_for_fmspc, quote::{Quote, Report}, }; +use thiserror::Error; /// For fetching collateral directly from Intel, if no PCCS is specified pub const PCS_URL: &str = "https://api.trustedservices.intel.com"; /// Quote generation using configfs_tsm pub async fn create_dcap_attestation(input_data: [u8; 64]) -> Result, AttestationError> { - Ok(generate_quote(input_data)?) + let quote = generate_quote(input_data)?; + tracing::info!("Generated TDX quote of {} bytes", quote.len()); + Ok(quote) } /// Verify a DCAP TDX quote, and return the measurement values @@ -23,12 +23,13 @@ pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs_url: Option, -) -> Result { - let (platform_measurements, image_measurements) = if cfg!(not(test)) { +) -> Result { + let measurements = if cfg!(not(test)) { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs(); let quote = Quote::parse(&input)?; + tracing::info!("Verifying DCAP attestation: {quote:?}"); let ca = quote.ca()?; let fmspc = hex::encode_upper(quote.fmspc()?); @@ -42,31 +43,23 @@ pub async fn verify_dcap_attestation( let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?; - let measurements = ( - PlatformMeasurements::from_dcap_qvl_quote("e)?, - CvmImageMeasurements::from_dcap_qvl_quote("e)?, - ); + let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; + if get_quote_input_data(quote.report) != expected_input_data { - return Err(AttestationError::InputMismatch); + return Err(DcapVerificationError::InputMismatch); } measurements } else { // In tests we use mock quotes which will fail to verify let quote = tdx_quote::Quote::from_bytes(&input)?; if quote.report_input_data() != expected_input_data { - return Err(AttestationError::InputMismatch); + return Err(DcapVerificationError::InputMismatch); } - ( - PlatformMeasurements::from_tdx_quote("e), - CvmImageMeasurements::from_tdx_quote("e), - ) + MultiMeasurements::from_tdx_quote("e) }; - Ok(Measurements { - platform: platform_measurements, - cvm_image: image_measurements, - }) + Ok(measurements) } /// Create a mock quote for testing on non-confidential hardware @@ -97,3 +90,18 @@ pub fn get_quote_input_data(report: Report) -> [u8; 64] { Report::SgxEnclave(r) => r.report_data, } } + +/// An error when verifying a DCAP attestation +#[derive(Error, Debug)] +pub enum DcapVerificationError { + #[error("Quote input is not as expected")] + InputMismatch, + #[error("SGX quote given when TDX quote expected")] + SgxNotSupported, + #[error("System Time: {0}")] + SystemTime(#[from] std::time::SystemTimeError), + #[error("DCAP quote verification: {0}")] + DcapQvl(#[from] anyhow::Error), + #[error("Quote parse: {0}")] + QuoteParse(#[from] tdx_quote::QuoteParseError), +} diff --git a/src/attestation/measurements.rs b/src/attestation/measurements.rs index a0e2f39..53038dd 100644 --- a/src/attestation/measurements.rs +++ b/src/attestation/measurements.rs @@ -1,5 +1,5 @@ //! Measurements and policy for enforcing them when validating a remote attestation -use crate::attestation::{AttestationError, AttestationType}; +use crate::attestation::{dcap::DcapVerificationError, AttestationError, AttestationType}; use std::{collections::HashMap, path::PathBuf}; use dcap_qvl::quote::Report; @@ -7,133 +7,136 @@ use http::{header::InvalidHeaderValue, HeaderValue}; use serde::Deserialize; use thiserror::Error; -/// Measurements determined by the CVM platform -#[derive(Clone, PartialEq, Debug)] -pub struct PlatformMeasurements { - /// MRTD register value - pub mrtd: [u8; 48], - /// RTMR0 register value - pub rtmr0: [u8; 48], +/// Represents the measurement register types in a TDX quote +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum DcapMeasurementRegister { + MRTD, + RTMR0, + RTMR1, + RTMR2, + RTMR3, } -impl PlatformMeasurements { - /// Given a quote from the dcap_qvl library, extract the platform measurements - pub fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { - let report = match quote.report { - Report::TD10(report) => report, - Report::TD15(report) => report.base, - Report::SgxEnclave(_) => { - return Err(AttestationError::SgxNotSupported); - } - }; - Ok(Self { - mrtd: report.mr_td, - rtmr0: report.rt_mr0, - }) - } - - pub fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { - Self { - mrtd: quote.mrtd(), - rtmr0: quote.rtmr0(), +impl TryFrom for DcapMeasurementRegister { + type Error = MeasurementFormatError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::MRTD), + 1 => Ok(Self::RTMR0), + 2 => Ok(Self::RTMR1), + 3 => Ok(Self::RTMR2), + 4 => Ok(Self::RTMR3), + _ => Err(MeasurementFormatError::BadRegisterIndex), } } } -/// Measurements determined by the CVM image or application -#[derive(Clone, PartialEq, Debug)] -pub struct CvmImageMeasurements { - /// RTMR1 register value - pub rtmr1: [u8; 48], - /// RTMR2 register value - pub rtmr2: [u8; 48], - /// RTMR3 register value - pub rtmr3: [u8; 48], +/// Represents a set of measurements values for one of the supported CVM platforms +#[derive(Debug, Clone, PartialEq)] +pub enum MultiMeasurements { + Dcap(HashMap), + Azure(HashMap), + NoAttestation, } -impl CvmImageMeasurements { - /// Given a quote from the dcap_qvl library, extract the CVM image / application measurements - pub fn from_dcap_qvl_quote(quote: &dcap_qvl::quote::Quote) -> Result { +impl MultiMeasurements { + /// Convert to the JSON format used in HTTP headers + pub fn to_header_format(&self) -> Result { + let measurements_map = match self { + MultiMeasurements::Dcap(dcap_measurements) => dcap_measurements + .iter() + .map(|(register, value)| ((register.clone() as u8).to_string(), hex::encode(value))) + .collect(), + MultiMeasurements::Azure(azure_measurements) => azure_measurements + .iter() + .map(|(index, value)| (index.to_string(), hex::encode(value))) + .collect(), + MultiMeasurements::NoAttestation => HashMap::new(), + }; + + Ok(HeaderValue::from_str(&serde_json::to_string( + &measurements_map, + )?)?) + } + + /// Parse the JSON used in HTTP headers + pub fn from_header_format( + input: &str, + attestation_type: AttestationType, + ) -> Result { + let measurements_map: HashMap = serde_json::from_str(input)?; + + Ok(match attestation_type { + AttestationType::AzureTdx => Self::Azure( + measurements_map + .into_iter() + .map(|(k, v)| { + Ok(( + k as u32, + hex::decode(v)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, + )) + }) + .collect::>()?, + ), + AttestationType::None => Self::NoAttestation, + _ => { + let measurements_map = measurements_map + .into_iter() + .map(|(k, v)| { + Ok(( + k.try_into()?, + hex::decode(v)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, + )) + }) + .collect::>()?; + Self::Dcap(measurements_map) + } + }) + } + + /// Given a quote from the dcap_qvl library, extract the measurements + pub fn from_dcap_qvl_quote( + quote: &dcap_qvl::quote::Quote, + ) -> Result { let report = match quote.report { Report::TD10(report) => report, Report::TD15(report) => report.base, Report::SgxEnclave(_) => { - return Err(AttestationError::SgxNotSupported); + return Err(DcapVerificationError::SgxNotSupported); } }; - Ok(Self { - rtmr1: report.rt_mr1, - rtmr2: report.rt_mr2, - rtmr3: report.rt_mr3, - }) + Ok(Self::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, report.mr_td), + (DcapMeasurementRegister::RTMR0, report.rt_mr0), + (DcapMeasurementRegister::RTMR1, report.rt_mr1), + (DcapMeasurementRegister::RTMR2, report.rt_mr2), + (DcapMeasurementRegister::RTMR3, report.rt_mr3), + ]))) } pub fn from_tdx_quote(quote: &tdx_quote::Quote) -> Self { - Self { - rtmr1: quote.rtmr1(), - rtmr2: quote.rtmr2(), - rtmr3: quote.rtmr3(), - } + Self::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, quote.mrtd()), + (DcapMeasurementRegister::RTMR0, quote.rtmr0()), + (DcapMeasurementRegister::RTMR1, quote.rtmr1()), + (DcapMeasurementRegister::RTMR2, quote.rtmr2()), + (DcapMeasurementRegister::RTMR3, quote.rtmr3()), + ])) } -} -/// A full set of measurement register values -#[derive(Debug, Clone, PartialEq)] -pub struct Measurements { - pub platform: PlatformMeasurements, - pub cvm_image: CvmImageMeasurements, -} - -impl Measurements { - /// Convert to the JSON format used in HTTP headers - pub fn to_header_format(&self) -> Result { - let mut measurements_map = HashMap::new(); - measurements_map.insert(0, hex::encode(self.platform.mrtd)); - measurements_map.insert(1, hex::encode(self.platform.rtmr0)); - measurements_map.insert(2, hex::encode(self.cvm_image.rtmr1)); - measurements_map.insert(3, hex::encode(self.cvm_image.rtmr2)); - measurements_map.insert(4, hex::encode(self.cvm_image.rtmr3)); - Ok(HeaderValue::from_str(&serde_json::to_string( - &measurements_map, - )?)?) - } - - /// Parse the JSON used in HTTP headers - pub fn from_header_format(input: &str) -> Result { - let measurements_map: HashMap = serde_json::from_str(input)?; - let measurements_map: HashMap = measurements_map - .into_iter() - .map(|(k, v)| { - Ok(( - k, - hex::decode(v)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - )) - }) - .collect::>()?; - - Ok(Self { - platform: PlatformMeasurements { - mrtd: *measurements_map - .get(&0) - .ok_or(MeasurementFormatError::MissingValue("MRTD".to_string()))?, - rtmr0: *measurements_map - .get(&1) - .ok_or(MeasurementFormatError::MissingValue("RTMR0".to_string()))?, - }, - cvm_image: CvmImageMeasurements { - rtmr1: *measurements_map - .get(&2) - .ok_or(MeasurementFormatError::MissingValue("RTMR1".to_string()))?, - rtmr2: *measurements_map - .get(&3) - .ok_or(MeasurementFormatError::MissingValue("RTMR2".to_string()))?, - rtmr3: *measurements_map - .get(&4) - .ok_or(MeasurementFormatError::MissingValue("RTMR3".to_string()))?, - }, - }) + pub fn from_pcrs<'a>(pcrs: impl Iterator) -> Self { + Self::Azure( + pcrs.copied() + .enumerate() + .map(|(index, value)| (index as u32, value)) + .collect(), + ) } } @@ -154,15 +157,39 @@ pub enum MeasurementFormatError { Hex(#[from] hex::FromHexError), #[error("Expected 48 byte value")] BadLength, + #[error("TDX quote register index must be in the ranger 0-3")] + BadRegisterIndex, + #[error("ParseInt: {0}")] + ParseInt(#[from] std::num::ParseIntError), } /// An accepted measurement value given in the measurements file -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct MeasurementRecord { /// An identifier, for example the name and version of the corresponding OS image pub measurement_id: String, /// The expected measurement register values - pub measurements: Measurements, + pub measurements: MultiMeasurements, +} + +impl MeasurementRecord { + pub fn allow_no_attestation() -> Self { + Self { + measurement_id: "Allow no attestation".to_string(), + measurements: MultiMeasurements::NoAttestation, + } + } + + pub fn allow_any_measurement(attestation_type: AttestationType) -> Self { + Self { + measurement_id: format!("Any measurement for {attestation_type}"), + measurements: match attestation_type { + AttestationType::None => MultiMeasurements::NoAttestation, + AttestationType::AzureTdx => MultiMeasurements::Azure(HashMap::new()), + _ => MultiMeasurements::Dcap(HashMap::new()), + }, + } + } } /// Represents the measurement policy @@ -173,35 +200,35 @@ pub struct MeasurementRecord { pub struct MeasurementPolicy { /// A map of accepted attestation types to accepted measurement values /// A value of None means accept any measurement value for this measurement type - pub(crate) accepted_measurements: HashMap>>, + pub(crate) accepted_measurements: Vec, } impl MeasurementPolicy { /// This will only allow no attestation - and will reject it if one is given pub fn expect_none() -> Self { Self { - accepted_measurements: HashMap::from([(AttestationType::None, None)]), + accepted_measurements: vec![MeasurementRecord::allow_no_attestation()], } } /// Allow any measurements with the given attestation type pub fn single_attestation_type(attestation_type: AttestationType) -> Self { Self { - accepted_measurements: HashMap::from([(attestation_type, None)]), + accepted_measurements: vec![MeasurementRecord::allow_any_measurement(attestation_type)], } } /// Accept any attestation type with any measurements pub fn accept_anything() -> Self { Self { - accepted_measurements: HashMap::from([ - (AttestationType::None, None), - (AttestationType::Dummy, None), - (AttestationType::DcapTdx, None), - (AttestationType::QemuTdx, None), - (AttestationType::AzureTdx, None), - (AttestationType::GcpTdx, None), - ]), + accepted_measurements: vec![ + MeasurementRecord::allow_no_attestation(), + MeasurementRecord::allow_any_measurement(AttestationType::Dummy), + MeasurementRecord::allow_any_measurement(AttestationType::DcapTdx), + MeasurementRecord::allow_any_measurement(AttestationType::QemuTdx), + MeasurementRecord::allow_any_measurement(AttestationType::GcpTdx), + MeasurementRecord::allow_any_measurement(AttestationType::AzureTdx), + ], } } @@ -209,45 +236,61 @@ impl MeasurementPolicy { #[cfg(test)] pub fn mock() -> Self { Self { - accepted_measurements: HashMap::from([( - AttestationType::DcapTdx, - Some(vec![MeasurementRecord { - measurement_id: "test".to_string(), - measurements: Measurements { - platform: PlatformMeasurements { - mrtd: [0; 48], - rtmr0: [0; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [0; 48], - rtmr2: [0; 48], - rtmr3: [0; 48], - }, - }, - }]), - )]), + accepted_measurements: vec![MeasurementRecord { + measurement_id: "test".to_string(), + measurements: MultiMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, [0; 48]), + (DcapMeasurementRegister::RTMR0, [0; 48]), + (DcapMeasurementRegister::RTMR1, [0; 48]), + (DcapMeasurementRegister::RTMR2, [0; 48]), + (DcapMeasurementRegister::RTMR3, [0; 48]), + ])), + }], } } /// Given an attestation type and set of measurements, check whether they are acceptable pub fn check_measurement( &self, - attestation_type: AttestationType, - measurements: &Measurements, + measurements: &MultiMeasurements, ) -> Result<(), AttestationError> { - match self.accepted_measurements.get(&attestation_type) { - Some(Some(measurement_set)) => { - if measurement_set - .iter() - .any(|a| &a.measurements == measurements) - { - Ok(()) - } else { - Err(AttestationError::MeasurementsNotAccepted) + if self + .accepted_measurements + .iter() + .any(|measurement_record| match measurements { + MultiMeasurements::Dcap(dcap_measurements) => { + if let MultiMeasurements::Dcap(d) = measurement_record.measurements.clone() { + for (k, v) in dcap_measurements.iter() { + if d.get(k).is_some_and(|x| x != v) { + return false; + } + } + return true; + } + false } - } - Some(None) => Ok(()), - None => Err(AttestationError::AttestationTypeNotAccepted), + MultiMeasurements::Azure(azure_measurements) => { + if let MultiMeasurements::Azure(a) = measurement_record.measurements.clone() { + for (k, v) in azure_measurements.iter() { + if a.get(k).is_some_and(|x| x != v) { + return false; + } + } + return true; + } + false + } + MultiMeasurements::NoAttestation => { + if MultiMeasurements::NoAttestation == measurement_record.measurements.clone() { + return true; + } + false + } + }) + { + Ok(()) + } else { + Err(AttestationError::MeasurementsNotAccepted) } } @@ -255,7 +298,8 @@ impl MeasurementPolicy { pub fn has_remote_attestion(&self) -> bool { !self .accepted_measurements - .contains_key(&AttestationType::None) + .iter() + .any(|a| a.measurements == MultiMeasurements::NoAttestation) } /// Given the path to a JSON file containing measurements, return a [MeasurementPolicy] @@ -281,50 +325,60 @@ impl MeasurementPolicy { let measurements_simple: Vec = serde_json::from_slice(&json_bytes)?; - let mut measurement_policy = HashMap::new(); + let mut measurement_policy = Vec::new(); for measurement in measurements_simple { let attestation_type = - serde_json::from_value(serde_json::Value::String(measurement.attestation_type)) - .unwrap(); + serde_json::from_value(serde_json::Value::String(measurement.attestation_type))?; if let Some(measurements) = measurement.measurements { - let measurement_record = MeasurementRecord { - measurement_id: measurement.measurement_id.unwrap_or_default(), - measurements: Measurements { - platform: PlatformMeasurements { - mrtd: hex::decode(&measurements["0"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - rtmr0: hex::decode(&measurements["1"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - }, - cvm_image: CvmImageMeasurements { - rtmr1: hex::decode(&measurements["2"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - rtmr2: hex::decode(&measurements["3"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - rtmr3: hex::decode(&measurements["4"].expected)? - .try_into() - .map_err(|_| MeasurementFormatError::BadLength)?, - }, - }, + let multi_measurement = match attestation_type { + AttestationType::AzureTdx => { + let azure_measurements = measurements + .into_iter() + .map(|(index, entry)| { + let index = index.parse()?; + + if index > 23 { + return Err(MeasurementFormatError::BadRegisterIndex); + } + + Ok(( + index, + hex::decode(entry.expected)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, + )) + }) + .collect::, MeasurementFormatError>>()?; + MultiMeasurements::Azure(azure_measurements) + } + AttestationType::None => MultiMeasurements::NoAttestation, + _ => MultiMeasurements::Dcap( + measurements + .into_iter() + .map(|(index, entry)| { + let index: u8 = index.parse()?; + Ok(( + DcapMeasurementRegister::try_from(index)?, + hex::decode(entry.expected)? + .try_into() + .map_err(|_| MeasurementFormatError::BadLength)?, + )) + }) + .collect::, + MeasurementFormatError, + >>()?, + ), }; - measurement_policy - .entry(attestation_type) - .and_modify(|maybe_vec: &mut Option>| { - match maybe_vec.as_mut() { - Some(vec) => vec.push(measurement_record.clone()), - None => *maybe_vec = Some(vec![measurement_record.clone()]), - } - }) - .or_insert_with(|| Some(vec![measurement_record])); + measurement_policy.push(MeasurementRecord { + measurement_id: measurement.measurement_id.unwrap_or_default(), + measurements: multi_measurement, + }); } else { - measurement_policy.entry(attestation_type).or_insert(None); + measurement_policy.push(MeasurementRecord::allow_any_measurement(attestation_type)); }; } @@ -336,21 +390,11 @@ impl MeasurementPolicy { #[cfg(test)] mod tests { - use super::*; + use std::collections::HashSet; - fn mock_measurements() -> Measurements { - Measurements { - platform: PlatformMeasurements { - mrtd: [0; 48], - rtmr0: [0; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [0; 48], - rtmr2: [0; 48], - rtmr3: [0; 48], - }, - } - } + use crate::test_helpers::mock_dcap_measurements; + + use super::*; #[tokio::test] async fn test_read_measurements_file() { @@ -359,16 +403,40 @@ mod tests { .await .unwrap(); - assert!(specific_measurements - .accepted_measurements - .get(&AttestationType::DcapTdx) - .unwrap() - .is_some()); + assert_eq!(specific_measurements.accepted_measurements.len(), 3); + + let m = &specific_measurements.accepted_measurements[0]; + if let MultiMeasurements::Azure(a) = &m.measurements { + assert_eq!( + a.keys().collect::>(), + HashSet::from([&9, &4, &11]) + ); + } else { + panic!("Unexpected measurement type"); + } + + let m = &specific_measurements.accepted_measurements[1]; + if let MultiMeasurements::Azure(a) = &m.measurements { + assert_eq!(a.keys().collect::>(), HashSet::from([&9, &4])); + } else { + panic!("Unexpected measurement type"); + } + + let m = &specific_measurements.accepted_measurements[2]; + if let MultiMeasurements::Dcap(d) = &m.measurements { + assert!(d.contains_key(&DcapMeasurementRegister::MRTD)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR0)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR1)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR2)); + assert!(d.contains_key(&DcapMeasurementRegister::RTMR3)); + } else { + panic!("Unexpected measurement type"); + } // Will not match mock measurements assert!(matches!( specific_measurements - .check_measurement(AttestationType::DcapTdx, &mock_measurements()) + .check_measurement(&mock_dcap_measurements()) .unwrap_err(), AttestationError::MeasurementsNotAccepted )); @@ -376,15 +444,14 @@ mod tests { // Will not match another attestation type assert!(matches!( specific_measurements - .check_measurement(AttestationType::None, &mock_measurements()) + .check_measurement(&MultiMeasurements::NoAttestation) .unwrap_err(), - AttestationError::AttestationTypeNotAccepted + AttestationError::MeasurementsNotAccepted )); } #[tokio::test] async fn test_read_measurements_file_non_specific() { - let mock_measurements = mock_measurements(); // This specifies a particular attestation type, but not specific measurements let allowed_attestation_type = MeasurementPolicy::from_file("test-assets/measurements_2.json".into()) @@ -392,18 +459,15 @@ mod tests { .unwrap(); allowed_attestation_type - .check_measurement(AttestationType::DcapTdx, &mock_measurements) + .check_measurement(&mock_dcap_measurements()) .unwrap(); - assert!(allowed_attestation_type - .accepted_measurements - .get(&AttestationType::DcapTdx) - .unwrap() - .is_none()); - - // Will match mock measurements - allowed_attestation_type - .check_measurement(AttestationType::DcapTdx, &mock_measurements) - .unwrap(); + // Will not match another attestation type + assert!(matches!( + allowed_attestation_type + .check_measurement(&MultiMeasurements::NoAttestation) + .unwrap_err(), + AttestationError::MeasurementsNotAccepted + )); } } diff --git a/src/attestation/mod.rs b/src/attestation/mod.rs index 71a52b3..1142d30 100644 --- a/src/attestation/mod.rs +++ b/src/attestation/mod.rs @@ -1,18 +1,19 @@ +#[cfg(feature = "azure")] +pub mod azure; pub mod dcap; pub mod measurements; -use measurements::Measurements; +use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::{ fmt::{self, Display, Formatter}, - time::{SystemTime, SystemTimeError, UNIX_EPOCH}, + time::{SystemTime, UNIX_EPOCH}, }; -use tdx_quote::QuoteParseError; use thiserror::Error; -use crate::attestation::measurements::MeasurementPolicy; +use crate::attestation::{dcap::DcapVerificationError, measurements::MeasurementPolicy}; /// This is the type sent over the channel to provide an attestation #[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] @@ -164,7 +165,17 @@ impl AttestationGenerator { ) -> Result, AttestationError> { match self.attestation_type { AttestationType::None => Ok(Vec::new()), - AttestationType::AzureTdx => Err(AttestationError::AttestationTypeNotSupported), + AttestationType::AzureTdx => { + #[cfg(feature = "azure")] + { + Ok(azure::create_azure_attestation(input_data).await?) + } + #[cfg(not(feature = "azure"))] + { + tracing::error!("Attempted to generate an azure attestation but the `azure` feature not enabled"); + Err(AttestationError::AttestationTypeNotSupported) + } + } AttestationType::Dummy => self.generate_dummy_attestation(input_data).await, _ => dcap::create_dcap_attestation(input_data).await, } @@ -230,7 +241,7 @@ impl AttestationVerifier { &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - ) -> Result, AttestationError> { + ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type; tracing::debug!("Verifing {attestation_type} attestation"); @@ -250,7 +261,19 @@ impl AttestationVerifier { } } AttestationType::AzureTdx => { - return Err(AttestationError::AttestationTypeNotSupported); + #[cfg(feature = "azure")] + { + azure::verify_azure_attestation( + attestation_exchange_message.attestation, + expected_input_data, + self.pccs_url.clone(), + ) + .await? + } + #[cfg(not(feature = "azure"))] + { + return Err(AttestationError::AttestationTypeNotSupported); + } } AttestationType::Dummy => { // Dummy assumes dummy DCAP @@ -272,8 +295,7 @@ impl AttestationVerifier { }; // Do a measurement / attestation type policy check - self.measurement_policy - .check_measurement(attestation_type, &measurements)?; + self.measurement_policy.check_measurement(&measurements)?; tracing::debug!("Verification successful"); Ok(Some(measurements)) @@ -309,30 +331,21 @@ pub enum AttestationError { X509Parse(#[from] x509_parser::asn1_rs::Err), #[error("X509: {0}")] X509(#[from] x509_parser::error::X509Error), - #[error("Quote input is not as expected")] - InputMismatch, #[error("Configuration mismatch - expected no remote attestation")] AttestationGivenWhenNoneExpected, #[error("Configfs-tsm quote generation: {0}")] QuoteGeneration(#[from] configfs_tsm::QuoteGenerationError), - #[error("SGX quote given when TDX quote expected")] - SgxNotSupported, - #[error("Platform measurements do not match any accepted values")] - UnacceptablePlatformMeasurements, - #[error("OS image measurements do not match any accepted values")] - UnacceptableOsImageMeasurements, - #[error("System Time: {0}")] - SystemTime(#[from] SystemTimeError), - #[error("DCAP quote verification: {0}")] - DcapQvl(#[from] anyhow::Error), - #[error("Quote parse: {0}")] - QuoteParse(#[from] QuoteParseError), + #[error("DCAP verification: {0}")] + DcapVerification(#[from] DcapVerificationError), #[error("Attestation type not supported")] AttestationTypeNotSupported, #[error("Attestation type not accepted")] AttestationTypeNotAccepted, #[error("Measurements not accepted")] MeasurementsNotAccepted, + #[cfg(feature = "azure")] + #[error("MAA: {0}")] + Maa(#[from] azure::MaaError), #[error("Dummy attestation type requires dummy service URL")] DummyUrl, #[error("Dummy server: {0}")] diff --git a/src/lib.rs b/src/lib.rs index 3de593f..d0a703d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ pub mod attested_get; pub mod file_server; pub use attestation::AttestationGenerator; -use attestation::{measurements::Measurements, AttestationError, AttestationType}; +use attestation::{measurements::MultiMeasurements, AttestationError, AttestationType}; use bytes::Bytes; use http::HeaderValue; use http_body_util::{combinators::BoxBody, BodyExt}; @@ -557,7 +557,7 @@ impl ProxyClient { cert_chain: Option>>, attestation_generator: AttestationGenerator, attestation_verifier: AttestationVerifier, - ) -> (Http2Sender, Option, AttestationType) { + ) -> (Http2Sender, Option, AttestationType) { let mut delay = Duration::from_secs(1); let max_delay = Duration::from_secs(SERVER_RECONNECT_MAX_BACKOFF_SECS); @@ -592,7 +592,7 @@ impl ProxyClient { cert_chain: Option>>, attestation_generator: AttestationGenerator, attestation_verifier: AttestationVerifier, - ) -> Result<(Http2Sender, Option, AttestationType), ProxyError> { + ) -> Result<(Http2Sender, Option, AttestationType), ProxyError> { // Make a TCP client connection and TLS handshake let out = TcpStream::connect(&target).await?; let mut tls_stream = connector @@ -856,13 +856,13 @@ mod tests { use std::collections::HashMap; use crate::attestation::measurements::{ - CvmImageMeasurements, MeasurementPolicy, MeasurementRecord, PlatformMeasurements, + DcapMeasurementRegister, MeasurementPolicy, MeasurementRecord, MultiMeasurements, }; use super::*; use test_helpers::{ - default_measurements, example_http_service, example_service, generate_certificate_chain, - generate_tls_config, generate_tls_config_with_client_auth, + example_http_service, generate_certificate_chain, generate_tls_config, + generate_tls_config_with_client_auth, mock_dcap_measurements, }; // Server has mock DCAP, client has no attestation and no client auth @@ -912,9 +912,6 @@ mod tests { .unwrap(); let headers = res.headers(); - let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = Measurements::from_header_format(measurements_json).unwrap(); - assert_eq!(measurements, default_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -923,6 +920,12 @@ mod tests { .unwrap(); assert_eq!(attestation_type, AttestationType::DcapTdx.as_str()); + let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); + let measurements = + MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) + .unwrap(); + assert_eq!(measurements, mock_dcap_measurements()); + let res_body = res.text().await.unwrap(); assert_eq!(res_body, "No measurements"); } @@ -1003,8 +1006,9 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = Measurements::from_header_format(&res_body).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); + assert_eq!(measurements, mock_dcap_measurements()); } // Server has mock DCAP, client has mock DCAP and client auth @@ -1070,8 +1074,10 @@ mod tests { let headers = res.headers(); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = Measurements::from_header_format(measurements_json).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) + .unwrap(); + assert_eq!(measurements, mock_dcap_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1084,8 +1090,9 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = Measurements::from_header_format(&res_body).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); + assert_eq!(measurements, mock_dcap_measurements()); // Now do another request - to check that the connection has stayed open let res = reqwest::get(format!("http://{}", proxy_client_addr.to_string())) @@ -1094,8 +1101,10 @@ mod tests { let headers = res.headers(); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = Measurements::from_header_format(measurements_json).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) + .unwrap(); + assert_eq!(measurements, mock_dcap_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1108,14 +1117,15 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = Measurements::from_header_format(&res_body).unwrap(); - assert_eq!(measurements, default_measurements()); + let measurements = + MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); + assert_eq!(measurements, mock_dcap_measurements()); } // Server has mock DCAP, client no attestation - just get the server certificate #[tokio::test] async fn test_get_tls_cert() { - let target_addr = example_service().await; + let target_addr = example_http_service().await; let (cert_chain, private_key) = generate_certificate_chain("127.0.0.1".parse().unwrap()); let (server_config, client_config) = generate_tls_config(cert_chain.clone(), private_key); @@ -1218,23 +1228,16 @@ mod tests { let attestation_verifier = AttestationVerifier { measurement_policy: MeasurementPolicy { - accepted_measurements: HashMap::from([( - AttestationType::DcapTdx, - Some(vec![MeasurementRecord { - measurement_id: "test".to_string(), - measurements: Measurements { - platform: PlatformMeasurements { - mrtd: [0; 48], - rtmr0: [0; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [1; 48], // This differs from the mock measurements given - rtmr2: [0; 48], - rtmr3: [0; 48], - }, - }, - }]), - )]), + accepted_measurements: vec![MeasurementRecord { + measurement_id: "test".to_string(), + measurements: MultiMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, [0; 48]), + (DcapMeasurementRegister::RTMR0, [0; 48]), + (DcapMeasurementRegister::RTMR1, [1; 48]), // This differs from the mock measurements + (DcapMeasurementRegister::RTMR2, [0; 48]), + (DcapMeasurementRegister::RTMR3, [0; 48]), + ])), + }], }, pccs_url: None, log_dcap_quote: false, diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 21ec18f..b783dff 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -1,9 +1,9 @@ use axum::response::IntoResponse; use std::{ + collections::HashMap, net::{IpAddr, SocketAddr}, sync::Arc, }; -use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio_rustls::rustls::{ pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, @@ -12,7 +12,7 @@ use tokio_rustls::rustls::{ }; use crate::{ - attestation::measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements}, + attestation::measurements::{DcapMeasurementRegister, MultiMeasurements}, MEASUREMENT_HEADER, SUPPORTED_ALPN_PROTOCOL_VERSIONS, }; @@ -120,6 +120,8 @@ pub fn generate_tls_config_with_client_auth( ) } +/// Given a TLS certificate, return a [WebPkiClientVerifier] and [RootCertStore] which will accept +/// that certificate fn client_verifier_from_remote_cert( cert: CertificateDer<'static>, ) -> (Arc, RootCertStore) { @@ -134,6 +136,8 @@ fn client_verifier_from_remote_cert( ) } +/// Simple http server used in tests which returns in the response the measurement header from the +/// request pub async fn example_http_service() -> SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -155,30 +159,13 @@ async fn get_handler(headers: http::HeaderMap) -> impl IntoResponse { .to_string() } -pub async fn example_service() -> SocketAddr { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - tokio::spawn(async move { - loop { - let (mut inbound, _client_addr) = listener.accept().await.unwrap(); - inbound.write_all(b"some data").await.unwrap(); - } - }); - - addr -} - -pub fn default_measurements() -> Measurements { - Measurements { - platform: PlatformMeasurements { - mrtd: [0u8; 48], - rtmr0: [0u8; 48], - }, - cvm_image: CvmImageMeasurements { - rtmr1: [0u8; 48], - rtmr2: [0u8; 48], - rtmr3: [0u8; 48], - }, - } +/// All-zero measurment values used in some tests +pub fn mock_dcap_measurements() -> MultiMeasurements { + MultiMeasurements::Dcap(HashMap::from([ + (DcapMeasurementRegister::MRTD, [0u8; 48]), + (DcapMeasurementRegister::RTMR0, [0u8; 48]), + (DcapMeasurementRegister::RTMR1, [0u8; 48]), + (DcapMeasurementRegister::RTMR2, [0u8; 48]), + (DcapMeasurementRegister::RTMR3, [0u8; 48]), + ])) } diff --git a/test-assets/azure-tdx-1764662251380464271 b/test-assets/azure-tdx-1764662251380464271 new file mode 100644 index 0000000..fce573d --- /dev/null +++ b/test-assets/azure-tdx-1764662251380464271 @@ -0,0 +1 @@ +{"tdx_quote_base64":"BAACAIEAAAAAAAAAk5pyM_ecTKmUCg2zlX8GB3Thpre77mKg7jpNBav8PEkAAAAABwEDAAAAAAAAAAAAAAAAAEm2b6pFHRnrvb6JNxuNrytlqjmE7JARA0Pp4u7BFq8IhQ-iDjsaqah013plOA7n5gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAADnGAYAAAAAABK9vBYJ7wZtt6nseybXUJNG9MpG6EYnQJbLJZObHnEZRAYcoZowxnQ_HlmRSO7kUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQEAAAdJ_oh6mN-g_v_0efzadA_DOWFK2jYy9p-h269PwkEI4tB-zdOYEa8Uv9hECJmRBRgVQ4z4q2gSz7P56NgkQGmercwydyxD9mf5PwaMAOdpOKJNtwow7lCT3oqVghyxhUPqIP5O_j2a8Dz9-Tpa1cQuZpSiimuRmchsKX3gPO3O8GAEoQAAADAxkbBP8ABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAAAAAAAAAOcAAAAAAAAA5aOntdgwwpU7mFNMbFmjo0_cNOkz9_WJjwqFzwiEa8oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANyeKnxvlI8XR040p_xD7QMPfBVj8bq932NAyC4OVKjFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHpJi_9CXp9wO3djOvudEoQR5boWPVXYM6F7X8QIwOVUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVQhcPV03wFyimgChXn5NVWfyQTse-BNQdLQotqL9Hf7Ft-i9mrUpeS7sanh8nDPB08_JariTf0DrJuO5Lmf8yIAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHwUAYg4AAC0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlFOGpDQ0JKZWdBd0lCQWdJVkFQbnErb2llZDBLNlhjREN5aUlXc01ESG51bFJNQW9HQ0NxR1NNNDlCQU1DCk1IQXhJakFnQmdOVkJBTU1HVWx1ZEdWc0lGTkhXQ0JRUTBzZ1VHeGhkR1p2Y20wZ1EwRXhHakFZQmdOVkJBb00KRVVsdWRHVnNJRU52Y25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRQpDQXdDUTBFeEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1EY3lPVEUxTXpNeE5Gb1hEVE15TURjeU9URTFNek14Ck5Gb3djREVpTUNBR0ExVUVBd3daU1c1MFpXd2dVMGRZSUZCRFN5QkRaWEowYVdacFkyRjBaVEVhTUJnR0ExVUUKQ2d3UlNXNTBaV3dnUTI5eWNHOXlZWFJwYjI0eEZEQVNCZ05WQkFjTUMxTmhiblJoSUVOc1lYSmhNUXN3Q1FZRApWUVFJREFKRFFURUxNQWtHQTFVRUJoTUNWVk13V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFuClA2bUNzUmhHaFUzVDlrSHpCaG1SUHNKM1d2NkxPSzluUFBUY1pxVis2QjlDSDJJdDZCZ0U3VkFMeUlpMGdIemEKYmhzeE5FM2ZKc045NWhaaUFXcmJvNElERERDQ0F3Z3dId1lEVlIwakJCZ3dGb0FVbFc5ZHpiMGI0ZWxBU2NuVQo5RFBPQVZjTDNsUXdhd1lEVlIwZkJHUXdZakJnb0Y2Z1hJWmFhSFIwY0hNNkx5OWhjR2t1ZEhKMWMzUmxaSE5sCmNuWnBZMlZ6TG1sdWRHVnNMbU52YlM5elozZ3ZZMlZ5ZEdsbWFXTmhkR2x2Ymk5Mk5DOXdZMnRqY213L1kyRTkKY0d4aGRHWnZjbTBtWlc1amIyUnBibWM5WkdWeU1CMEdBMVVkRGdRV0JCUXZrTU1jN25RT1k3OTlXM2tBcTJuSQo5bW1LZVRBT0JnTlZIUThCQWY4RUJBTUNCc0F3REFZRFZSMFRBUUgvQkFJd0FEQ0NBamtHQ1NxR1NJYjRUUUVOCkFRU0NBaW93Z2dJbU1CNEdDaXFHU0liNFRRRU5BUUVFRUtUMVh3NS82WG1WTDdROWRhVS85WXN3Z2dGakJnb3EKaGtpRytFMEJEUUVDTUlJQlV6QVFCZ3NxaGtpRytFMEJEUUVDQVFJQkF6QVFCZ3NxaGtpRytFMEJEUUVDQWdJQgpBekFRQmdzcWhraUcrRTBCRFFFQ0F3SUJBakFRQmdzcWhraUcrRTBCRFFFQ0JBSUJBakFRQmdzcWhraUcrRTBCCkRRRUNCUUlCQkRBUUJnc3Foa2lHK0UwQkRRRUNCZ0lCQVRBUUJnc3Foa2lHK0UwQkRRRUNCd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDQ0FJQkJUQVFCZ3NxaGtpRytFMEJEUUVDQ1FJQkFEQVFCZ3NxaGtpRytFMEJEUUVDQ2dJQgpBREFRQmdzcWhraUcrRTBCRFFFQ0N3SUJBREFRQmdzcWhraUcrRTBCRFFFQ0RBSUJBREFRQmdzcWhraUcrRTBCCkRRRUNEUUlCQURBUUJnc3Foa2lHK0UwQkRRRUNEZ0lCQURBUUJnc3Foa2lHK0UwQkRRRUNEd0lCQURBUUJnc3EKaGtpRytFMEJEUUVDRUFJQkFEQVFCZ3NxaGtpRytFMEJEUUVDRVFJQkRUQWZCZ3NxaGtpRytFMEJEUUVDRWdRUQpBd01DQWdRQkFBVUFBQUFBQUFBQUFEQVFCZ29xaGtpRytFMEJEUUVEQkFJQUFEQVVCZ29xaGtpRytFMEJEUUVFCkJBYVF3RzhBQUFBd0R3WUtLb1pJaHZoTkFRMEJCUW9CQVRBZUJnb3Foa2lHK0UwQkRRRUdCQkRjMmREb09seDgKNG0rT09kRzVjKzBkTUVRR0NpcUdTSWI0VFFFTkFRY3dOakFRQmdzcWhraUcrRTBCRFFFSEFRRUIvekFRQmdzcQpoa2lHK0UwQkRRRUhBZ0VCQURBUUJnc3Foa2lHK0UwQkRRRUhBd0VCL3pBS0JnZ3Foa2pPUFFRREFnTkpBREJHCkFpRUFwemRPNEx4amFKVHlVc3pIQ3B3aXVDL05ISzBmV2JpcGR5TlFuMGZmRWYwQ0lRRHRVeU41ZmJNbjlUUHIKWG93R1Y0Y2lIVHJkUEJOZVN2K3JEeEZNTDdVVktBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQ2xqQ0NBajJnQXdJQkFnSVZBSlZ2WGMyOUcrSHBRRW5KMVBRenpnRlhDOTVVTUFvR0NDcUdTTTQ5QkFNQwpNR2d4R2pBWUJnTlZCQU1NRVVsdWRHVnNJRk5IV0NCU2IyOTBJRU5CTVJvd0dBWURWUVFLREJGSmJuUmxiQ0JECmIzSndiM0poZEdsdmJqRVVNQklHQTFVRUJ3d0xVMkZ1ZEdFZ1EyeGhjbUV4Q3pBSkJnTlZCQWdNQWtOQk1Rc3cKQ1FZRFZRUUdFd0pWVXpBZUZ3MHhPREExTWpFeE1EVXdNVEJhRncwek16QTFNakV4TURVd01UQmFNSEF4SWpBZwpCZ05WQkFNTUdVbHVkR1ZzSUZOSFdDQlFRMHNnVUd4aGRHWnZjbTBnUTBFeEdqQVlCZ05WQkFvTUVVbHVkR1ZzCklFTnZjbkJ2Y21GMGFXOXVNUlF3RWdZRFZRUUhEQXRUWVc1MFlTQkRiR0Z5WVRFTE1Ba0dBMVVFQ0F3Q1EwRXgKQ3pBSkJnTlZCQVlUQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRU5TQi83dDIxbFhTTwoyQ3V6cHh3NzRlSkI3MkV5REdnVzVyWEN0eDJ0VlRMcTZoS2s2eitVaVJaQ25xUjdwc092Z3FGZVN4bG1UbEpsCmVUbWkyV1l6M3FPQnV6Q0J1REFmQmdOVkhTTUVHREFXZ0JRaVpReldXcDAwaWZPRHRKVlN2MUFiT1NjR3JEQlMKQmdOVkhSOEVTekJKTUVlZ1JhQkRoa0ZvZEhSd2N6b3ZMMk5sY25ScFptbGpZWFJsY3k1MGNuVnpkR1ZrYzJWeQpkbWxqWlhNdWFXNTBaV3d1WTI5dEwwbHVkR1ZzVTBkWVVtOXZkRU5CTG1SbGNqQWRCZ05WSFE0RUZnUVVsVzlkCnpiMGI0ZWxBU2NuVTlEUE9BVmNMM2xRd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUIKQWY4Q0FRQXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdYc1ZraTB3K2k2VllHVzNVRi8yMnVhWGUwWUpEajFVZQpuQStUakQxYWk1Y0NJQ1liMVNBbUQ1eGtmVFZwdm80VW95aVNZeHJEV0xtVVI0Q0k5Tkt5ZlBOKwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlDanpDQ0FqU2dBd0lCQWdJVUltVU0xbHFkTkluemc3U1ZVcjlRR3prbkJxd3dDZ1lJS29aSXpqMEVBd0l3CmFERWFNQmdHQTFVRUF3d1JTVzUwWld3Z1UwZFlJRkp2YjNRZ1EwRXhHakFZQmdOVkJBb01FVWx1ZEdWc0lFTnYKY25CdmNtRjBhVzl1TVJRd0VnWURWUVFIREF0VFlXNTBZU0JEYkdGeVlURUxNQWtHQTFVRUNBd0NRMEV4Q3pBSgpCZ05WQkFZVEFsVlRNQjRYRFRFNE1EVXlNVEV3TkRVeE1Gb1hEVFE1TVRJek1USXpOVGsxT1Zvd2FERWFNQmdHCkExVUVBd3dSU1c1MFpXd2dVMGRZSUZKdmIzUWdRMEV4R2pBWUJnTlZCQW9NRVVsdWRHVnNJRU52Y25CdmNtRjAKYVc5dU1SUXdFZ1lEVlFRSERBdFRZVzUwWVNCRGJHRnlZVEVMTUFrR0ExVUVDQXdDUTBFeEN6QUpCZ05WQkFZVApBbFZUTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQzZuRXdNRElZWk9qL2lQV3NDemFFS2k3CjFPaU9TTFJGaFdHamJuQlZKZlZua1k0dTNJamtEWVlMME14TzRtcXN5WWpsQmFsVFZZeEZQMnNKQks1emxLT0IKdXpDQnVEQWZCZ05WSFNNRUdEQVdnQlFpWlF6V1dwMDBpZk9EdEpWU3YxQWJPU2NHckRCU0JnTlZIUjhFU3pCSgpNRWVnUmFCRGhrRm9kSFJ3Y3pvdkwyTmxjblJwWm1sallYUmxjeTUwY25WemRHVmtjMlZ5ZG1salpYTXVhVzUwClpXd3VZMjl0TDBsdWRHVnNVMGRZVW05dmRFTkJMbVJsY2pBZEJnTlZIUTRFRmdRVUltVU0xbHFkTkluemc3U1YKVXI5UUd6a25CcXd3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUV3Q2dZSQpLb1pJemowRUF3SURTUUF3UmdJaEFPVy81UWtSK1M5Q2lTRGNOb293THVQUkxzV0dmL1lpN0dTWDk0Qmd3VHdnCkFpRUE0SjBsckhvTXMrWG81by9zWDZPOVFXeEhSQXZaVUdPZFJRN2N2cVJYYXFJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","hcl_report_base64":"SENMQQIAAABaCQAAAgAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAAAAAAAAAAAAwMZGwT_AAUAAAAAAAAAAEB73O4ihAAL1Fq3YJOLBeFadfZagSvRh378KKR_plJRdg0_lzrFanQ73FOJV8ioAk0EF89TAb_qtJkcf-PQqX8vuQr5d0gmOU2-DKrvVG4VHWCx7tELBMBIwOniiJl_Gr7HT144wmd5zTW6UINWARezcB0-_vIpFW7C7FnsrogOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGo1RO-ajyGFbRpEQvwrhKfdSit715Hy11JkCim5iEjF_wEDAAAAAAAHAQMAAAAAAAAAAAAAAAAASbZvqkUdGeu9vok3G42vK2WqOYTskBEDQ-ni7sEWrwiFD6IOOxqpqHTXemU4DufmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA5xgGAAAAAAASvbwWCe8Gbbep7Hsm11CTRvTKRuhGJ0CWyyWTmx5xGUQGHKGaMMZ0Px5ZkUju5FEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJoEAAABAAAABAAAAAEAAACGBAAAeyJrZXlzIjpbeyJraWQiOiJIQ0xBa1B1YiIsImtleV9vcHMiOlsic2lnbiJdLCJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiJwM3g2SUFBQTY4MFViZ3AzdU5tTF93Nm1FeTJjaUd2bkFaVHFwM2tueDZGQmtieTMtZFQ2RlhXVl9NYmM3RUk4RHNQRTk1WURnT2szVEJjZktkNmxBdUw0QTdoeVBZdzc3c1dJV1dySnlBTE9FUnNmQTJiUC1hNzQyVjQ5cFJrWXRYWFI4c1NuYWNuS2Rfd0hNZ0NpVzZFNXJoUUZXTzRINUEybW51TV9xcnhocHRlQ1BidmxvN2NiTFNWOXBHTEtNbnV0RUJDYzBITndLSnZ6ZWQ5WG15cGpWWUJJTTdSbXdoNmw0bk44VzFkY0ZOby1fTnhobTV6YXU4bXo5ZktoTlFMSXpPaWkzWk95TWE0dnYtUzNJYmZoTVBUMDZqMnllbUVXWUNTbGxqQU9paGxTYllteXQtT0k3ZHJUdTZjV0tpQmNYanJKa1ZWMVFKcUdhOWRzNHcifSx7ImtpZCI6IkhDTEVrUHViIiwia2V5X29wcyI6WyJlbmNyeXB0Il0sImt0eSI6IlJTQSIsImUiOiJBUUFCIiwibiI6Im9WakUyQUFBRFVmSUlLajJtNjB0dXp2M1NJbDdBVXE2aElTeExfYmlrYWdIeUpTSzJTaWVidVJka25oSWE5MTZSNk1hdlAtdUU0TjRJaXhCMHliZlZ5TGg1YThzcVBNNHFqMnYxN2o3dXp4QWJtTldkTVJvTjRKZHZKNWpOY0FJMUs4ajF5M0pkWGkzZ0d0aWJWTkVuU3RoQ2ZsYzNPdDg1WXZiUlpfYVc5dV95S3hJQUwxWTFWQVE2WnFTaUxPM1B6Q0hUc09BZml2N3B3QUUtb3R5YjVmcFF1bFBxWFVVOGV6a0hSYmFtNUlyYnZCT18wS0FDdmhMaEFvdVB3a2YtREFhVW5uc01kT1ZQZkI1enZtMGJGZnZKTVBRTWNydUE4dVktZk1vbWZGY0tHWlphVXZ2ekNZa2tqd1N6Zmo2NDVLcTd6bUEtNkR4RnV1b2E4enNzdyJ9XSwidm0tY29uZmlndXJhdGlvbiI6eyJyb290LWNlcnQtdGh1bWJwcmludCI6IiIsImNvbnNvbGUtZW5hYmxlZCI6dHJ1ZSwic2VjdXJlLWJvb3QiOmZhbHNlLCJ0cG0tZW5hYmxlZCI6dHJ1ZSwidHBtLXBlcnNpc3RlZCI6dHJ1ZSwidm1VbmlxdWVJZCI6ImJkZDQzOTA0LWRiZjgtNDMzNy04YjkxLTNhZGJhOTJjNWViZCJ9LCJ1c2VyLWRhdGEiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJ9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","tpm_attestation":{"ak_certificate_pem":"-----BEGIN CERTIFICATE-----\nMIID7TCCAtWgAwIBAgIQZGTaH3CY2Bt8ETaN4Mt8HzANBgkqhkiG9w0BAQsFADAl\nMSMwIQYDVQQDExpHbG9iYWwgVmlydHVhbCBUUE0gQ0EgLSAwMzAeFw0yNTEyMDEw\nMDAwMDBaFw0yNjExMzAwMDAwMDBaMDgxNjA0BgNVBAMTLTM1M2M2ZDlhNzBiYy5D\nb25maWRlbnRpYWxWTS5BenVyZS53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAKd8eiAAAOvNFG4Kd7jZi/8OphMtnIhr5wGU6qd5J8eh\nQZG8t/nU+hV1lfzG3OxCPA7DxPeWA4DpN0wXHynepQLi+AO4cj2MO+7FiFlqycgC\nzhEbHwNmz/mu+NlePaUZGLV10fLEp2nJynf8BzIAoluhOa4UBVjuB+QNpp7jP6q8\nYabXgj275aO3Gy0lfaRiyjJ7rRAQnNBzcCib83nfV5sqY1WASDO0ZsIepeJzfFtX\nXBTaPvzcYZuc2rvJs/XyoTUCyMzoot2TsjGuL7/ktyG34TD09Oo9snphFmAkpZYw\nDooZUm2JsrfjiO3a07unFiogXF46yZFVdUCahmvXbOMCAwEAAaOCAQQwggEAMA4G\nA1UdDwEB/wQEAwIHgDAYBgNVHSAEETAPMA0GCysGAQQBgjdsgUgCMBwGA1UdJQQV\nMBMGCisGAQQBgjcKAwwGBWeBBQgDMB0GA1UdDgQWBBRqgwVlBrzgn9J7BOoGoSDO\n36kjVDAaBgorBgEEAYI3DQIDBAwWCjYuMi45MjAwLjIwWgYJKwYBBAGCNxUUBE0w\nSwIBBQwPQU1TMjUxMDYxOTA5MDAxDBpXT1JLR1JPVVBcQU1TMjUxMDYxOTA5MDAx\nJAwZVnRwbU1hbmFnZW1lbnRTZXJ2aWNlLmV4ZTAfBgNVHSMEGDAWgBRnCYb4+YFe\nk635yWTnFAramcsKLTANBgkqhkiG9w0BAQsFAAOCAQEAKP34NvDPbHhuNjkDqE4/\nmLyy87rDIhICcgg0eDtDGfCGcJENFGuKngtpQeJjEnY3czvZzCBlVO/iO0W6DbGi\nzqqHRcJsAyPtxUUZi84Gt7xvWtr206oHBFuk4cpUJsPunQQan2bsiO5T4YE7pqhU\nob+i49exg/IgHxLiG+w+/C23vVhLg0GM0zBqgAxuOG1ebLerkb2Fy/pHzzVNuW+g\n6XoUz3efvWC2ZXVV+4bU4TxRKjcJ544uYRTKnF3d3KKRQmuJ5ucvGySW+ofN3+Jo\n6PqvHowc0yAF1b1xzEcMhhtz1Wr5Jlv6yXfJ89WulltMxU+Hkf2SU+pKtz0wpPZ+\nWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAA==\n-----END CERTIFICATE-----\n","quote":{"signature":[135,59,50,25,70,64,229,176,28,41,162,94,237,111,83,172,175,95,193,175,187,237,230,200,179,197,131,184,209,16,107,116,168,110,47,199,140,111,223,4,108,181,42,241,70,190,88,160,195,104,249,161,250,7,175,161,30,75,90,63,237,114,102,198,182,0,204,174,119,194,243,184,124,56,2,18,221,200,103,232,156,29,0,207,160,26,216,154,208,18,153,92,97,188,36,147,220,35,39,201,210,118,252,170,60,43,15,134,84,77,23,188,121,113,19,178,249,159,22,128,167,142,179,240,70,246,106,114,183,14,130,235,3,72,120,240,82,210,250,105,178,161,125,199,173,175,5,49,51,117,146,76,175,34,4,18,91,37,189,173,156,248,44,237,115,254,71,215,220,224,130,30,100,31,189,21,7,156,33,253,30,0,199,59,142,80,47,219,5,124,133,252,239,194,105,246,181,83,110,185,30,103,8,122,181,66,117,58,83,78,60,96,167,100,50,82,127,56,219,94,80,62,65,224,146,24,83,243,116,102,27,224,213,122,94,174,102,124,145,114,125,169,192,201,184,137,187,180,255,85,194,214,201,148,46,59],"message":[255,84,67,71,128,24,0,34,0,11,176,196,237,28,251,197,97,211,73,25,195,246,137,118,108,234,140,194,246,203,163,159,152,132,197,213,135,46,152,40,142,215,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,176,163,127,0,0,0,2,0,0,0,0,1,32,32,3,18,0,18,0,3,0,0,0,1,0,11,3,255,255,255,0,32,73,235,12,37,110,154,84,47,86,198,201,8,64,241,53,217,188,196,121,140,27,5,162,10,40,110,110,222,74,249,168,118],"pcrs":[[86,31,111,47,249,11,244,110,95,1,184,215,193,191,150,12,45,100,182,72,51,37,241,141,120,33,124,132,99,15,211,83],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[189,192,76,193,11,79,205,110,159,121,26,105,91,230,132,199,199,242,175,241,156,196,82,235,102,58,28,53,82,47,75,28],[61,69,140,254,85,204,3,234,31,68,63,21,98,190,236,141,245,28,117,225,74,159,207,154,114,52,161,63,25,142,121,105],[196,162,90,109,119,4,98,159,99,219,132,210,14,168,219,14,156,224,2,178,128,27,233,163,64,9,31,231,172,88,134,153],[94,18,234,167,32,238,182,179,211,0,70,66,80,242,205,229,18,48,16,198,193,183,6,131,184,20,41,112,3,12,244,221],[180,90,56,59,226,129,211,3,171,159,183,227,211,194,92,114,209,57,251,5,37,19,45,58,244,93,121,15,213,151,11,190],[18,77,175,71,180,214,113,121,167,125,195,193,188,202,25,138,225,238,29,9,74,42,135,153,116,132,46,68,171,152,187,6],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[159,74,87,117,18,44,164,112,62,19,90,154,230,4,30,222,173,0,100,38,46,57,157,241,28,168,81,130,176,241,84,29],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[171,215,198,149,255,219,96,129,233,150,54,238,1,109,19,34,145,156,104,208,73,182,152,179,153,210,42,226,21,161,33,191],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]},"event_log":[],"instance_info":null}} \ No newline at end of file diff --git a/test-assets/hclreport.bin b/test-assets/hclreport.bin new file mode 100644 index 0000000..ff9494a Binary files /dev/null and b/test-assets/hclreport.bin differ diff --git a/test-assets/measurements.json b/test-assets/measurements.json index f7714b7..7a9097e 100644 --- a/test-assets/measurements.json +++ b/test-assets/measurements.json @@ -1,4 +1,31 @@ [ + { + "measurement_id": "azure-tdx-example-01", + "attestation_type": "azure-tdx", + "measurements": { + "4": { + "expected": "ea92ff762767eae6316794f1641c485d4846bc2b9df2eab6ba7f630ce6f4d66f" + }, + "9": { + "expected": "c9f429296634072d1063a03fb287bed0b2d177b0a504755ad9194cffd90b2489" + }, + "11": { + "expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7" + } + } + }, + { + "measurement_id": "cvm-image-azure-tdx.rootfs-20241107200854.wic.vhd", + "attestation_type": "azure-tdx", + "measurements": { + "4": { + "expected": "1b8cd655f5ebdf50bedabfb5db6b896a0a7c56de54f318103a2de1e7cea57b6b" + }, + "9": { + "expected": "992465f922102234c196f596fdaba86ea16eaa4c264dc425ec26bc2d1c364472" + } + } + }, { "measurement_id": "dcap-tdx-example", "attestation_type": "dcap-tdx",