diff --git a/Cargo.lock b/Cargo.lock index 8ebb7cb3..6fd1d579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,12 +387,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -453,9 +447,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "argon2" @@ -521,6 +515,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "asn1_der" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -934,6 +934,30 @@ dependencies = [ "cipher", ] +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "brotli" version = "6.0.0" @@ -1099,11 +1123,10 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -1695,11 +1718,52 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dcap-qvl" +version = "0.4.1" +source = "git+https://github.com/jialez0/dcap-qvl?branch=v0.4.1-allow-expired#d988f7de3f5e82d0d7e8f9bceeb3af9fa8d9daa1" +dependencies = [ + "anyhow", + "asn1_der", + "base64 0.22.1", + "borsh", + "byteorder", + "chrono", + "const-oid", + "dcap-qvl-webpki", + "der", + "derive_more 2.0.1", + "hex", + "log", + "parity-scale-codec", + "pem", + "ring", + "rustls-pki-types", + "scale-info", + "serde", + "serde-human-bytes", + "serde_json", + "urlencoding", + "x509-cert", +] + +[[package]] +name = "dcap-qvl-webpki" +version = "0.103.4+dcap.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0af040afe66c4f26ca05f308482d98bd75a35a80a227d877c2e28c9947a9fa6" +dependencies = [ + "ring", + "rsa", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "der_derive", @@ -1804,7 +1868,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -1819,6 +1892,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "unicode-xid", +] + [[package]] name = "des" version = "0.8.1" @@ -3311,11 +3396,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -3712,9 +3798,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lzma-sys" @@ -4198,30 +4284,28 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.7.4" +version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" +checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" dependencies = [ "arrayvec", "bitvec", "byte-slice-cast", - "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", - "rustversion", "serde", ] [[package]] name = "parity-scale-codec-derive" -version = "3.7.4" +version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" +checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.98", + "syn 1.0.109", ] [[package]] @@ -4738,9 +4822,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -4860,9 +4944,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -5533,6 +5617,31 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "scc" version = "2.3.3" @@ -5741,10 +5850,11 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.218" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -5757,6 +5867,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-human-bytes" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aff481ca1fe108deba0f217b45d9f1d494e7e7f906bcc7366d8a5648c5a1e65" +dependencies = [ + "base64 0.13.1", + "hex", + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.15" @@ -5766,11 +5887,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -5779,15 +5909,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap 2.7.1", "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -7372,6 +7503,7 @@ dependencies = [ "chrono", "codicon", "csv-rs", + "dcap-qvl", "ear", "eventlog", "eventlog-rs", @@ -7404,6 +7536,7 @@ dependencies = [ "tokio", "tonic-build", "tss-esapi", + "urlencoding", "uuid", "veraison-apiclient", "x509-parser", @@ -7466,48 +7599,32 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7515,31 +7632,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.98", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -7665,9 +7782,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" @@ -8090,6 +8207,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 7ef74181..a92f5118 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,4 +66,14 @@ tempfile = "3.4.0" tonic = "0.12" tonic-build = "0.12" serde_yaml = "0.9" -zeroize = { version = "1.7", features = ["derive"] } \ No newline at end of file +zeroize = { version = "1.7", features = ["derive"] } + +# dcap-qvl fork adding a `QuoteVerifier::allow_expired` option, used by the +# pure-Rust TDX verifier backend (feature `tdx-dcap-rust`) to treat expired +# collateral as non-fatal, matching the Intel DCAP QVL (FFI) backend. The fork +# only relaxes the freshness check; signature/chain verification is unchanged. +# See https://github.com/jialez0/dcap-qvl/tree/v0.4.1-allow-expired +# (based on Phala-Network/dcap-qvl v0.4.1). This patch is inert for the default +# build, which does not enable `tdx-dcap-rust` and never compiles dcap-qvl. +[patch.crates-io] +dcap-qvl = { git = "https://github.com/jialez0/dcap-qvl", branch = "v0.4.1-allow-expired" } \ No newline at end of file diff --git a/attestation-service/Cargo.toml b/attestation-service/Cargo.toml index 8fd92dcf..b477ea41 100644 --- a/attestation-service/Cargo.toml +++ b/attestation-service/Cargo.toml @@ -6,7 +6,15 @@ edition = "2021" [features] default = [ "restful-bin", "rvps-grpc", "all-verifier" ] all-verifier = [ "verifier/all-verifier" ] -tdx-verifier = [ "verifier/tdx-verifier" ] +# Like `all-verifier`, but the TDX verifier uses the dcap-qvl backend and SGX is +# dropped (it would still link the DCAP shared library). See +# deps/verifier/src/tdx/verify/native.rs. +all-verifier-rust = [ "verifier/all-verifier-rust" ] +# `tdx-verifier` keeps its historical meaning (DCAP shared library backend) for +# backward compatibility. The dcap-qvl backend is opt-in via `tdx-dcap-rust`. +tdx-verifier = [ "verifier/tdx-dcap-ffi" ] +tdx-dcap-ffi = [ "verifier/tdx-dcap-ffi" ] +tdx-dcap-rust = [ "verifier/tdx-dcap-rust" ] sgx-verifier = [ "verifier/sgx-verifier" ] az-snp-vtpm-verifier = [ "verifier/az-snp-vtpm-verifier" ] az-tdx-vtpm-verifier = [ "verifier/az-tdx-vtpm-verifier" ] diff --git a/attestation-service/Makefile b/attestation-service/Makefile index f75e1daf..ad18b619 100644 --- a/attestation-service/Makefile +++ b/attestation-service/Makefile @@ -8,6 +8,12 @@ BIN_NAMES := grpc-as restful-as DEBUG ?= DESTDIR ?= $(PREFIX)/bin +# Verifier feature set. The default keeps the TDX verifier on the Intel DCAP +# shared library (libsgx_dcap_quoteverify) backend, so the build is unchanged. +# +# To build the shared-library-free TDX backend (dcap-qvl) instead, use: +# make VERIFIER=all-verifier-rust +# It builds on the same Rust toolchain as the default build. VERIFIER ?= all-verifier RVPS_GRPC := true diff --git a/deps/verifier/Cargo.toml b/deps/verifier/Cargo.toml index 10eee663..e184a4f1 100644 --- a/deps/verifier/Cargo.toml +++ b/deps/verifier/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [features] default = ["all-verifier"] all-verifier = [ - "tdx-verifier", + "tdx-dcap-ffi", "sgx-verifier", "snp-verifier", "csv-verifier", @@ -14,15 +14,51 @@ all-verifier = [ "system-verifier", "tpm-verifier", ] +# Same set as `all-verifier`, but the TDX verifier uses the dcap-qvl backend +# instead of the DCAP shared library. `sgx-verifier` is intentionally omitted: +# it still links `libsgx_dcap_quoteverify` via FFI, which would defeat the +# purpose of a shared-library-free build. +all-verifier-rust = [ + "tdx-dcap-rust", + "snp-verifier", + "csv-verifier", + "hygon-dcu-verifier", + "system-verifier", + "tpm-verifier", +] +# TDX quote verification. The `tdx-verifier` umbrella pulls in everything the +# TDX verifier needs *except* the actual quote-verification backend, which must +# be selected separately and exactly once: +# +# * tdx-dcap-ffi - DCAP QVL via FFI, links `libsgx_dcap_quoteverify` (default) +# * tdx-dcap-rust - dcap-qvl, no external DCAP shared library +# +# Enabling `tdx-verifier` on its own (no backend) is a compile error. The +# default build reaches TDX through `all-verifier` -> `tdx-dcap-ffi`, so its +# behaviour is unchanged. tdx-verifier = [ "scroll", - "intel-tee-quote-verification-rs", "gpu-verifier", "openssl", ] +tdx-dcap-ffi = ["tdx-verifier", "intel-tee-quote-verification-rs"] +# Backend built on `dcap-qvl` (with its `ring` crypto backend). Removes the +# dependency on the Intel DCAP shared library (`libsgx_dcap_quoteverify`) and +# its dynamically loaded quote provider. Verification collateral (TCB info, QE +# identity, CRLs) is fetched over HTTPS from a PCCS using the verifier's existing +# reqwest stack. Builds on the same Rust toolchain as the default (FFI) build; +# see deps/verifier/src/tdx/verify/native.rs. +tdx-dcap-rust = [ + "tdx-verifier", + "dcap-qvl", + "x509-parser", + "asn1-rs", + "urlencoding", + "chrono", +] sgx-verifier = ["scroll", "intel-tee-quote-verification-rs"] az-snp-vtpm-verifier = ["az-snp-vtpm", "sev", "snp-verifier"] -az-tdx-vtpm-verifier = ["az-tdx-vtpm", "openssl", "tdx-verifier"] +az-tdx-vtpm-verifier = ["az-tdx-vtpm", "openssl", "tdx-dcap-ffi"] snp-verifier = ["asn1-rs", "openssl", "sev", "x509-parser", "reqwest"] csv-verifier = ["codicon", "csv-rs", "openssl", "tokio/fs"] hygon-dcu-verifier = ["csv-rs"] @@ -87,6 +123,16 @@ sha2.workspace = true sm3 = "0.4.2" tokio = { workspace = true, optional = true, default-features = false } intel-tee-quote-verification-rs = { version = "0.3.0", optional = true } +# TDX/SGX DCAP quote verification (used with its `ring` backend), by the `tdx-dcap-rust` +# backend. Only the offline `verify` core is used (no `report` feature), so no +# aws-lc / reqwest 0.13 is pulled in; collateral is fetched with the verifier's +# own reqwest. Pinned to 0.4.1 for MSRV/dependency-tree stability. +dcap-qvl = { version = "=0.4.1", default-features = false, features = [ + "std", + "ring", + "default-x509", +], optional = true } +urlencoding = { version = "2", optional = true } strum.workspace = true tss-esapi = { version = "7.4.0", optional = true } uuid = { version = "1.0", features = ["v4"], optional = true } diff --git a/deps/verifier/src/tdx/mod.rs b/deps/verifier/src/tdx/mod.rs index 9830eb45..8ef47a8f 100644 --- a/deps/verifier/src/tdx/mod.rs +++ b/deps/verifier/src/tdx/mod.rs @@ -8,14 +8,16 @@ use crate::tdx::claims::generate_parsed_claim; use super::*; use async_trait::async_trait; use base64::Engine; -use quote::{ecdsa_quote_verification, parse_tdx_quote}; +use quote::parse_tdx_quote; use serde::{Deserialize, Serialize}; +use verify::ecdsa_quote_verification; use serde_json::Value; pub(crate) mod claims; pub(crate) mod gpu; pub(crate) mod quote; +pub(crate) mod verify; use crate::tdx::gpu::GpuEvidenceList; diff --git a/deps/verifier/src/tdx/quote.rs b/deps/verifier/src/tdx/quote.rs index 9315ae75..2714a98d 100644 --- a/deps/verifier/src/tdx/quote.rs +++ b/deps/verifier/src/tdx/quote.rs @@ -1,37 +1,7 @@ use anyhow::{anyhow, bail, Result}; use core::fmt; -use log::{debug, warn}; -use qvl::{ - quote3_error_t, sgx_ql_qv_result_t, sgx_ql_qv_supplemental_t, sgx_ql_request_policy_t, - sgx_qv_set_enclave_load_policy, tee_get_supplemental_data_version_and_size, - tee_qv_get_collateral, tee_supp_data_descriptor_t, tee_verify_quote, -}; use scroll::Pread; use serde::Serialize; -use std::mem; -use std::time::{Duration, SystemTime}; - -use intel_tee_quote_verification_rs as qvl; - -/// Human-readable TCB verification status string. -fn qv_result_to_str(result: sgx_ql_qv_result_t) -> &'static str { - match result { - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK => "UpToDate", - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_NEEDED => "ConfigurationNeeded", - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE => "OutOfDate", - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE_CONFIG_NEEDED => { - "OutOfDateConfigurationNeeded" - } - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_SW_HARDENING_NEEDED => "SWHardeningNeeded", - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_AND_SW_HARDENING_NEEDED => { - "ConfigurationAndSWHardeningNeeded" - } - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_INVALID_SIGNATURE => "InvalidSignature", - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_REVOKED => "Revoked", - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_UNSPECIFIED => "Unspecified", - _ => "Unknown", - } -} /// Captures the TCB verification result and supplemental data from DCAP QVL, /// so that upper-layer policy engines can make fine-grained attestation decisions. @@ -376,6 +346,10 @@ impl Quote { body_field!(rtmr_1); body_field!(rtmr_2); body_field!(rtmr_3); + // TDX TEE TCB SVN (16 bytes) from the TD report. Used by the dcap-qvl + // quote-verification backend to reproduce Intel QVL's TCB level matching. + #[cfg(feature = "tdx-dcap-rust")] + body_field!(tcb_svn); } impl fmt::Display for Quote { @@ -456,147 +430,6 @@ pub fn parse_tdx_quote(quote_bin: &[u8]) -> Result { } } -pub async fn ecdsa_quote_verification(quote: &[u8]) -> Result { - let mut supp_data: sgx_ql_qv_supplemental_t = Default::default(); - let mut supp_data_desc = tee_supp_data_descriptor_t { - major_version: 0, - data_size: 0, - p_data: &mut supp_data as *mut sgx_ql_qv_supplemental_t as *mut u8, - }; - - // Call DCAP quote verify library to set QvE loading policy to multi-thread - // We only need to set the policy once; otherwise, it will return the error code 0xe00c (SGX_QL_UNSUPPORTED_LOADING_POLICY) - static INIT: std::sync::Once = std::sync::Once::new(); - INIT.call_once(|| { - match sgx_qv_set_enclave_load_policy( - sgx_ql_request_policy_t::SGX_QL_PERSISTENT_QVE_MULTI_THREAD, - ) { - quote3_error_t::SGX_QL_SUCCESS => { - debug!("Info: sgx_qv_set_enclave_load_policy successfully returned.") - } - err => warn!( - "Error: sgx_qv_set_enclave_load_policy failed: {:#04x}", - err as u32 - ), - } - }); - - match tee_get_supplemental_data_version_and_size(quote) { - Ok((supp_ver, supp_size)) => { - if supp_size == mem::size_of::() as u32 { - debug!("tee_get_quote_supplemental_data_version_and_size successfully returned."); - debug!( - "Info: latest supplemental data major version: {}, minor version: {}, size: {}", - u16::from_be_bytes(supp_ver.to_be_bytes()[..2].try_into()?), - u16::from_be_bytes(supp_ver.to_be_bytes()[2..].try_into()?), - supp_size, - ); - supp_data_desc.data_size = supp_size; - } else { - warn!("Quote supplemental data size is different between DCAP QVL and QvE, please make sure you installed DCAP QVL and QvE from same release.") - } - } - Err(e) => bail!( - "tee_get_quote_supplemental_data_size failed: {:#04x}", - e as u32 - ), - } - - // get collateral - let collateral = match tee_qv_get_collateral(quote) { - Ok(c) => { - debug!("tee_qv_get_collateral successfully returned."); - Some(c) - } - Err(e) => { - warn!("tee_qv_get_collateral failed: {:#04x}", e as u32); - None - } - }; - - // set current time. This is only for sample purposes, in production mode a trusted time should be used. - // - let current_time = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap_or(Duration::ZERO) - .as_secs() as i64; - - let p_supplemental_data = match supp_data_desc.data_size { - 0 => None, - _ => Some(&mut supp_data_desc), - }; - - // call DCAP quote verify library for quote verification - let (collateral_expiration_status, quote_verification_result) = tee_verify_quote( - quote, - collateral.as_ref(), - current_time, - None, - p_supplemental_data, - ) - .map_err(|e| anyhow!("tee_verify_quote failed: {:#04x}", e as u32))?; - - debug!("tee_verify_quote successfully returned."); - - // check verification result - match quote_verification_result { - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK => { - // check verification collateral expiration status - // this value should be considered in your own attestation/verification policy - if collateral_expiration_status == 0 { - debug!("Verification completed successfully."); - } else { - warn!("Verification completed, but collateral is out of date based on 'expiration_check_date' you provided."); - } - } - sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_NEEDED - | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE - | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE_CONFIG_NEEDED - | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_SW_HARDENING_NEEDED - | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_AND_SW_HARDENING_NEEDED => { - warn!( - "Verification completed with Non-terminal result: {:x}", - quote_verification_result as u32 - ); - } - _ => { - bail!( - "Verification completed with Terminal result: {:x}", - quote_verification_result as u32 - ); - } - } - - // Extract advisory IDs from supplemental data (null-terminated C string). - // sa_list is [c_char; 320] (i8 on Linux), containing comma-separated advisory IDs. - let advisory_ids = { - let sa_bytes: Vec = supp_data - .sa_list - .iter() - .take_while(|&&b| b != 0) - .map(|&b| b as u8) - .collect(); - String::from_utf8_lossy(&sa_bytes).to_string() - }; - - let result = TcbVerificationResult { - tcb_status: qv_result_to_str(quote_verification_result).to_string(), - tcb_status_code: quote_verification_result as u32, - collateral_expired: collateral_expiration_status != 0, - earliest_issue_date: supp_data.earliest_issue_date, - latest_issue_date: supp_data.latest_issue_date, - earliest_expiration_date: supp_data.earliest_expiration_date, - tcb_level_date_tag: supp_data.tcb_level_date_tag, - tcb_eval_ref_num: supp_data.tcb_eval_ref_num, - advisory_ids, - tee_type: supp_data.tee_type, - }; - - debug!("TCB verification result: {:?}", result); - - Ok(result) -} - #[cfg(test)] mod tests { use rstest::rstest; @@ -616,45 +449,4 @@ mod tests { let _ = fs::write(format!("{quote_path}.txt"), parsed_quote); } - - /// Test to verify the TDX quote, both in v4 and v5 format. - /// - /// This unit test requires two packages, s.t. `libsgx-dcap-quote-verify-dev` and `libsgx-dcap-default-qpl` - /// On ubuntu 22.04, you need to run the following scripts to install. - /// ```shell - /// curl -L https://download.01.org/intel-sgx/sgx_repo/ubuntu/intel-sgx-deb.key | tee intel-sgx-deb.key | apt-key add - && \ - /// echo 'deb [arch=amd64] https://download.01.org/intel-sgx/sgx_repo/ubuntu jammy main' | tee /etc/apt/sources.list.d/intel-sgx.list && \ - /// apt-get update && \ - /// apt-get install -y libsgx-dcap-default-qpl libsgx-dcap-quote-verify - /// ``` - /// - /// Also, you need to configure DCAP to work with alibaba cloud's PCCS. - /// edit `/etc/sgx_default_qcnl.conf` and replace the whole content with - /// ```json - /// {"pccs_url" :"https://sgx-dcap-server.cn-beijing.aliyuncs.com/sgx/certification/v4/"} - /// ``` - /// - /// The manual modification upon `sgx_default_qcnl.conf` could be promoted after - /// https://github.com/intel/SGXDataCenterAttestationPrimitives/issues/409 is resolved. - /// - /// Finally, DCAP only provides packages on x86-64 platform, thus we only test this on x86-64 - /// platforms. - #[cfg(target_arch = "x86_64")] - #[rstest] - #[ignore] - #[tokio::test] - #[case("./test_data/tdx_quote_4.dat")] - #[ignore] - #[tokio::test] - #[case("./test_data/tdx_quote_5.dat")] - async fn test_verify_tdx_quote(#[case] quote: &str) { - let quote_bin = fs::read(quote).unwrap(); - let res = ecdsa_quote_verification(quote_bin.as_slice()).await; - assert!(res.is_ok(), "{res:?}"); - let tcb_result = res.unwrap(); - println!( - "TCB status: {}, advisory_ids: {}, tcb_level_date_tag: {}", - tcb_result.tcb_status, tcb_result.advisory_ids, tcb_result.tcb_level_date_tag - ); - } } diff --git a/deps/verifier/src/tdx/verify/ffi.rs b/deps/verifier/src/tdx/verify/ffi.rs new file mode 100644 index 00000000..674e61b6 --- /dev/null +++ b/deps/verifier/src/tdx/verify/ffi.rs @@ -0,0 +1,183 @@ +//! DCAP-QVL (Intel `libsgx_dcap_quoteverify`) backend for TDX quote +//! verification. Selected by the `tdx-dcap-ffi` feature. +//! +//! This is the historical behaviour: quote verification is delegated to the +//! Intel DCAP Quote Verification Library through the +//! `intel-tee-quote-verification-rs` FFI bindings, which link against the +//! `libsgx_dcap_quoteverify` shared object at run time and dynamically load +//! the platform quote provider (PCCS collateral) and, optionally, the QvE. + +use anyhow::{anyhow, bail, Result}; +use log::{debug, warn}; +use std::mem; +use std::time::{Duration, SystemTime}; + +use intel_tee_quote_verification_rs as qvl; +use qvl::{ + quote3_error_t, sgx_ql_qv_result_t, sgx_ql_qv_supplemental_t, sgx_ql_request_policy_t, + sgx_qv_set_enclave_load_policy, tee_get_supplemental_data_version_and_size, + tee_qv_get_collateral, tee_supp_data_descriptor_t, tee_verify_quote, +}; + +use crate::tdx::quote::TcbVerificationResult; + +/// Human-readable TCB verification status string. +fn qv_result_to_str(result: sgx_ql_qv_result_t) -> &'static str { + match result { + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK => "UpToDate", + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_NEEDED => "ConfigurationNeeded", + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE => "OutOfDate", + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE_CONFIG_NEEDED => { + "OutOfDateConfigurationNeeded" + } + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_SW_HARDENING_NEEDED => "SWHardeningNeeded", + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_AND_SW_HARDENING_NEEDED => { + "ConfigurationAndSWHardeningNeeded" + } + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_INVALID_SIGNATURE => "InvalidSignature", + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_REVOKED => "Revoked", + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_UNSPECIFIED => "Unspecified", + _ => "Unknown", + } +} + +pub async fn ecdsa_quote_verification(quote: &[u8]) -> Result { + let mut supp_data: sgx_ql_qv_supplemental_t = Default::default(); + let mut supp_data_desc = tee_supp_data_descriptor_t { + major_version: 0, + data_size: 0, + p_data: &mut supp_data as *mut sgx_ql_qv_supplemental_t as *mut u8, + }; + + // Call DCAP quote verify library to set QvE loading policy to multi-thread + // We only need to set the policy once; otherwise, it will return the error code 0xe00c (SGX_QL_UNSUPPORTED_LOADING_POLICY) + static INIT: std::sync::Once = std::sync::Once::new(); + INIT.call_once(|| { + match sgx_qv_set_enclave_load_policy( + sgx_ql_request_policy_t::SGX_QL_PERSISTENT_QVE_MULTI_THREAD, + ) { + quote3_error_t::SGX_QL_SUCCESS => { + debug!("Info: sgx_qv_set_enclave_load_policy successfully returned.") + } + err => warn!( + "Error: sgx_qv_set_enclave_load_policy failed: {:#04x}", + err as u32 + ), + } + }); + + match tee_get_supplemental_data_version_and_size(quote) { + Ok((supp_ver, supp_size)) => { + if supp_size == mem::size_of::() as u32 { + debug!("tee_get_quote_supplemental_data_version_and_size successfully returned."); + debug!( + "Info: latest supplemental data major version: {}, minor version: {}, size: {}", + u16::from_be_bytes(supp_ver.to_be_bytes()[..2].try_into()?), + u16::from_be_bytes(supp_ver.to_be_bytes()[2..].try_into()?), + supp_size, + ); + supp_data_desc.data_size = supp_size; + } else { + warn!("Quote supplemental data size is different between DCAP QVL and QvE, please make sure you installed DCAP QVL and QvE from same release.") + } + } + Err(e) => bail!( + "tee_get_quote_supplemental_data_size failed: {:#04x}", + e as u32 + ), + } + + // get collateral + let collateral = match tee_qv_get_collateral(quote) { + Ok(c) => { + debug!("tee_qv_get_collateral successfully returned."); + Some(c) + } + Err(e) => { + warn!("tee_qv_get_collateral failed: {:#04x}", e as u32); + None + } + }; + + // set current time. This is only for sample purposes, in production mode a trusted time should be used. + // + let current_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() as i64; + + let p_supplemental_data = match supp_data_desc.data_size { + 0 => None, + _ => Some(&mut supp_data_desc), + }; + + // call DCAP quote verify library for quote verification + let (collateral_expiration_status, quote_verification_result) = tee_verify_quote( + quote, + collateral.as_ref(), + current_time, + None, + p_supplemental_data, + ) + .map_err(|e| anyhow!("tee_verify_quote failed: {:#04x}", e as u32))?; + + debug!("tee_verify_quote successfully returned."); + + // check verification result + match quote_verification_result { + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OK => { + // check verification collateral expiration status + // this value should be considered in your own attestation/verification policy + if collateral_expiration_status == 0 { + debug!("Verification completed successfully."); + } else { + warn!("Verification completed, but collateral is out of date based on 'expiration_check_date' you provided."); + } + } + sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_NEEDED + | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE + | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_OUT_OF_DATE_CONFIG_NEEDED + | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_SW_HARDENING_NEEDED + | sgx_ql_qv_result_t::SGX_QL_QV_RESULT_CONFIG_AND_SW_HARDENING_NEEDED => { + warn!( + "Verification completed with Non-terminal result: {:x}", + quote_verification_result as u32 + ); + } + _ => { + bail!( + "Verification completed with Terminal result: {:x}", + quote_verification_result as u32 + ); + } + } + + // Extract advisory IDs from supplemental data (null-terminated C string). + // sa_list is [c_char; 320] (i8 on Linux), containing comma-separated advisory IDs. + let advisory_ids = { + let sa_bytes: Vec = supp_data + .sa_list + .iter() + .take_while(|&&b| b != 0) + .map(|&b| b as u8) + .collect(); + String::from_utf8_lossy(&sa_bytes).to_string() + }; + + let result = TcbVerificationResult { + tcb_status: qv_result_to_str(quote_verification_result).to_string(), + tcb_status_code: quote_verification_result as u32, + collateral_expired: collateral_expiration_status != 0, + earliest_issue_date: supp_data.earliest_issue_date, + latest_issue_date: supp_data.latest_issue_date, + earliest_expiration_date: supp_data.earliest_expiration_date, + tcb_level_date_tag: supp_data.tcb_level_date_tag, + tcb_eval_ref_num: supp_data.tcb_eval_ref_num, + advisory_ids, + tee_type: supp_data.tee_type, + }; + + debug!("TCB verification result: {:?}", result); + + Ok(result) +} diff --git a/deps/verifier/src/tdx/verify/mod.rs b/deps/verifier/src/tdx/verify/mod.rs new file mode 100644 index 00000000..2f12c1eb --- /dev/null +++ b/deps/verifier/src/tdx/verify/mod.rs @@ -0,0 +1,93 @@ +//! TDX quote verification backends. +//! +//! The actual ECDSA quote verification can be provided by one of two +//! mutually-exclusive, compile-time-selected backends: +//! +//! * [`tdx-dcap-ffi`](ffi) — the default. Delegates to the Intel DCAP Quote +//! Verification Library through FFI, linking the `libsgx_dcap_quoteverify` +//! shared object at run time. This is the historical behaviour and is what +//! every default build (`all-verifier`) uses. +//! +//! * `tdx-dcap-rust` — a backend built on the `dcap-qvl` crate that removes the +//! dependency on any external DCAP shared library. It is opt-in; with it +//! disabled the default (FFI) build is unaffected. It builds on the same Rust +//! toolchain as the default build (see the crate-level docs). +//! +//! Both backends expose the same entry point: +//! +//! ```ignore +//! pub async fn ecdsa_quote_verification(quote: &[u8]) -> Result +//! ``` +//! +//! and return the same [`TcbVerificationResult`](crate::tdx::quote::TcbVerificationResult), +//! so nothing above this module needs to know which backend is compiled in. + +#[cfg(all(feature = "tdx-dcap-ffi", feature = "tdx-dcap-rust"))] +compile_error!( + "features `tdx-dcap-ffi` and `tdx-dcap-rust` are mutually exclusive: \ + enable exactly one TDX quote-verification backend" +); + +#[cfg(not(any(feature = "tdx-dcap-ffi", feature = "tdx-dcap-rust")))] +compile_error!( + "the `tdx-verifier` feature requires a backend: enable either \ + `tdx-dcap-ffi` (Intel DCAP shared library, the default) or \ + `tdx-dcap-rust` (dcap-qvl, no external DCAP library)" +); + +#[cfg(feature = "tdx-dcap-ffi")] +mod ffi; +#[cfg(feature = "tdx-dcap-ffi")] +pub(crate) use ffi::ecdsa_quote_verification; + +#[cfg(feature = "tdx-dcap-rust")] +mod native; +#[cfg(feature = "tdx-dcap-rust")] +pub(crate) use native::ecdsa_quote_verification; + +#[cfg(test)] +mod tests { + use super::ecdsa_quote_verification; + use rstest::rstest; + use std::fs; + + /// Test to verify the TDX quote, both in v4 and v5 format. + /// + /// This test is backend-agnostic: it exercises whichever backend + /// (`tdx-dcap-ffi` or `tdx-dcap-rust`) is compiled in. Both must produce + /// the same [`TcbVerificationResult`](crate::tdx::quote::TcbVerificationResult) + /// for a given quote. + /// + /// It is `#[ignore]`d because it needs network access to a PCCS to fetch + /// verification collateral (TCB info, QE identity, CRLs). With the + /// `tdx-dcap-ffi` backend it additionally requires `libsgx-dcap-quote-verify` + /// and the `libsgx-dcap-default-qpl` quote provider to be installed and + /// `/etc/sgx_default_qcnl.conf` to point at a reachable PCCS, e.g.: + /// + /// ```json + /// {"pccs_url" :"https://sgx-dcap-server.cn-beijing.aliyuncs.com/sgx/certification/v4/"} + /// ``` + /// + /// With the `tdx-dcap-rust` backend, collateral is fetched over HTTPS + /// directly, so only network access to the configured PCCS is required. + /// + /// DCAP only ships packages on x86-64, thus we only run this on x86-64. + #[cfg(target_arch = "x86_64")] + #[rstest] + #[ignore] + #[tokio::test] + #[case("./test_data/tdx_quote_4.dat")] + #[ignore] + #[tokio::test] + #[case("./test_data/tdx_quote_5.dat")] + async fn test_verify_tdx_quote(#[case] quote: &str) { + let quote_bin = fs::read(quote).unwrap(); + let res = ecdsa_quote_verification(quote_bin.as_slice()).await; + assert!(res.is_ok(), "{res:?}"); + let tcb_result = res.unwrap(); + println!( + "TCB status: {}, advisory_ids: {}, tcb_level_date_tag: {}", + tcb_result.tcb_status, tcb_result.advisory_ids, tcb_result.tcb_level_date_tag + ); + } +} diff --git a/deps/verifier/src/tdx/verify/native.rs b/deps/verifier/src/tdx/verify/native.rs new file mode 100644 index 00000000..6179b688 --- /dev/null +++ b/deps/verifier/src/tdx/verify/native.rs @@ -0,0 +1,632 @@ +//! TDX quote verification backend that needs no external DCAP shared library. +//! Selected by the `tdx-dcap-rust` feature. +//! +//! This backend removes the dependency on the Intel DCAP shared library +//! (`libsgx_dcap_quoteverify`) and its dynamically loaded quote provider. The +//! ECDSA quote signature, PCK certificate chain, TCB info and QE identity are +//! all verified via the [`dcap_qvl`] crate; verification collateral is fetched +//! over HTTPS from a PCCS using the verifier's own `reqwest` stack (we +//! deliberately do *not* enable `dcap-qvl`'s `report` feature, which would pull +//! in `reqwest` 0.13 / `aws-lc-sys`). +//! +//! Crypto backend: `dcap-qvl` is used with its `ring` crypto backend. `ring` +//! statically links its own crypto (no external shared library), and — unlike +//! the RustCrypto stack — builds on Rust 1.76, so this backend has the same +//! toolchain requirement as the default (FFI) build. With the feature disabled, +//! Cargo prunes this whole subtree, so it has no effect on the default build. +//! +//! PCCS configuration: the collateral endpoint is resolved from, in order, the +//! `PCCS_URL` environment variable, the `pccs_url` / `PCCS_URL` entry in +//! `/etc/sgx_default_qcnl.conf` (the same file the DCAP quote provider reads, so +//! both backends share one configuration), and finally a built-in default. +//! +//! Scope: the collateral fetch currently supports quotes whose certification +//! data embeds the PCK certificate chain (PCK cert type 5), which is what cloud +//! TDX quotes use. Other certification data types return a clear error. +//! +//! The [`TcbVerificationResult`] returned here is populated to match the fields +//! the DCAP QVL (FFI) backend exposes, including `tcb_level_date_tag`, which is +//! reproduced by re-running Intel QVL's TCB-level matching over the platform TCB +//! (SGX TCB components + PCE SVN from the PCK certificate, TDX TEE TCB SVN from +//! the TD report). + +use anyhow::{anyhow, bail, Context, Result}; +use asn1_rs::{Any, FromDer, Oid}; +use dcap_qvl::tcb_info::{TcbComponents, TcbInfo, TcbLevel}; +use dcap_qvl::verify::{QuoteVerifier, VerifiedReport}; +use dcap_qvl::QuoteCollateralV3; +use log::debug; +use std::time::{SystemTime, UNIX_EPOCH}; +use x509_parser::pem::Pem; +use x509_parser::prelude::*; + +use crate::tdx::quote::{parse_tdx_quote, TcbVerificationResult}; + +/// Default PCCS base URL, used only when neither `PCCS_URL` nor the QCNL config +/// file provide one. +const DEFAULT_PCCS_URL: &str = "https://sgx-dcap-server.cn-beijing.aliyuncs.com"; + +/// QCNL config file read by the DCAP quote provider (FFI backend). We fall back +/// to its `pccs_url` so both backends can share one PCCS configuration. +const QCNL_CONF_PATH: &str = "/etc/sgx_default_qcnl.conf"; + +/// TEE type for TDX, matching the value the FFI backend reports in +/// `TcbVerificationResult::tee_type`. +const TEE_TYPE_TDX: u32 = 0x0000_0081; + +/// Intel SGX extension OID (`1.2.840.113741.1.13.1`) and the sub-OIDs used here. +const OID_SGX_EXTENSION: &[u64] = &[1, 2, 840, 113741, 1, 13, 1]; +const OID_SGX_TCB: &[u64] = &[1, 2, 840, 113741, 1, 13, 1, 2]; +const OID_SGX_PCESVN: &[u64] = &[1, 2, 840, 113741, 1, 13, 1, 2, 17]; +const OID_SGX_FMSPC: &[u64] = &[1, 2, 840, 113741, 1, 13, 1, 4]; + +/// Resolve the PCCS base URL: `PCCS_URL` env var, then the QCNL config file, +/// then the built-in default. +fn resolve_pccs_url() -> String { + if let Ok(v) = std::env::var("PCCS_URL") { + if !v.is_empty() { + return v; + } + } + if let Some(v) = std::fs::read_to_string(QCNL_CONF_PATH) + .ok() + .and_then(|c| parse_qcnl_pccs_url(&c)) + { + debug!("dcap-qvl backend: using PCCS URL from {QCNL_CONF_PATH}"); + return v; + } + DEFAULT_PCCS_URL.to_string() +} + +/// Extract the PCCS URL from QCNL config file contents, supporting both the JSON +/// form (`{"pccs_url": "https://..."}`, optionally with `//` comments) and the +/// legacy INI form (`PCCS_URL=https://...`). +fn parse_qcnl_pccs_url(content: &str) -> Option { + for raw in content.lines() { + let line = raw.trim(); + if line.starts_with('#') || line.starts_with("//") { + continue; + } + let Some(key_pos) = line.to_ascii_lowercase().find("pccs_url") else { + continue; + }; + // After the key (INI `=` or JSON `":`), take the URL up to the next + // quote / comma / whitespace. This covers both config styles. + let after = &line[key_pos + "pccs_url".len()..]; + let Some(http_pos) = after.find("http") else { + continue; + }; + let url = &after[http_pos..]; + let end = url + .find(|c: char| c == '"' || c == ',' || c.is_whitespace()) + .unwrap_or(url.len()); + let url = &url[..end]; + if !url.is_empty() { + return Some(url.to_string()); + } + } + None +} + +pub async fn ecdsa_quote_verification(quote: &[u8]) -> Result { + let pccs_url = resolve_pccs_url(); + + // The PCK certificate chain is embedded in the quote's certification data + // (PCK cert type 5). Extract it and derive FMSPC / CA type for collateral. + let pck_chain = extract_pck_chain_pem(quote) + .context("failed to extract embedded PCK certificate chain from quote")?; + let leaf_der = first_cert_der(&pck_chain)?; + let (fmspc, ca) = extract_fmspc_and_ca(&leaf_der)?; + debug!("dcap-qvl backend: fmspc={fmspc}, ca={ca}, pccs={pccs_url}"); + + let collateral = fetch_collateral(&pccs_url, &fmspc, ca, pck_chain.clone()).await?; + + let real_now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + + let dates = CollateralDates::parse(&collateral)?; + let collateral_expired = real_now >= dates.earliest_expiration_date; + debug!( + "dcap-qvl backend: fmspc={fmspc} now={real_now} tcb_next={} expired={collateral_expired}", + dates.tcb_info.next_update + ); + + // Match the FFI (DCAP QVL) backend, which defers these to the policy engine + // rather than hard-failing: + // * allow_debug / allow_service_td -> debug and service TDs are accepted; + // * allow_expired -> expired collateral (TCB info / QE + // identity past its `nextUpdate`) is non-fatal. Signatures and + // certificate chains are still fully verified; only the freshness check + // is relaxed. The real expiry is reported to the policy via + // `collateral_expired` below. This mirrors the FFI backend and, in + // particular, tolerates a PCCS that serves stale collateral for a + // platform's FMSPC. + let report = QuoteVerifier::new_prod() + .allow_debug(true) + .allow_service_td(true) + .allow_expired(true) + .verify(quote, &collateral, real_now as u64) + .map_err(|e| anyhow!("dcap-qvl quote verification failed: {e:#}"))?; + + build_result(quote, &leaf_der, &report, &dates, collateral_expired) +} + +// --------------------------------------------------------------------------- +// PCK chain / certificate parsing +// --------------------------------------------------------------------------- + +/// Extract the PEM PCK certificate chain embedded in the quote's certification +/// data (PCK cert type 5). We locate it by scanning for the PEM boundaries, +/// which avoids re-parsing the whole ECDSA signature structure. +fn extract_pck_chain_pem(quote: &[u8]) -> Result { + const BEGIN: &[u8] = b"-----BEGIN CERTIFICATE-----"; + const END: &[u8] = b"-----END CERTIFICATE-----"; + let start = find_sub(quote, BEGIN) + .context("no PEM certificate found in quote (unsupported PCK cert type?)")?; + let end = + rfind_sub(quote, END).context("malformed PEM certificate chain in quote")? + END.len(); + let pem = std::str::from_utf8("e[start..end]) + .context("PCK certificate chain is not valid UTF-8")?; + Ok(pem.to_string()) +} + +/// DER of the leaf (first) certificate in a PEM chain. +fn first_cert_der(pem_chain: &str) -> Result> { + for pem in Pem::iter_from_buffer(pem_chain.as_bytes()) { + let pem = pem.context("failed to parse PEM block in PCK chain")?; + return Ok(pem.contents); + } + bail!("PCK certificate chain contains no certificate") +} + +/// Extract FMSPC (hex, upper-case) and CA type ("platform"/"processor") from +/// the leaf PCK certificate. +fn extract_fmspc_and_ca(leaf_der: &[u8]) -> Result<(String, &'static str)> { + let (_, cert) = + X509Certificate::from_der(leaf_der).context("failed to parse PCK leaf certificate")?; + + let sgx_ext = sgx_extension(&cert)?; + let entries = parse_der_seq_of_pairs(sgx_ext).context("failed to parse Intel SGX extension")?; + let fmspc_val = entries + .iter() + .find(|(oid, _)| oid == &arcs_str(OID_SGX_FMSPC)) + .map(|(_, v)| v) + .context("SGX extension is missing FMSPC")?; + let fmspc_bytes = der_octet_string(fmspc_val).context("FMSPC is not an OCTET STRING")?; + let fmspc = hex::encode_upper(fmspc_bytes); + + // CA type is derived from the issuer common name. + let issuer = cert.issuer().to_string(); + let ca = if issuer.contains("Platform") { + "platform" + } else if issuer.contains("Processor") { + "processor" + } else { + // Default matches Intel/Phala behaviour when the CN is unexpected. + "processor" + }; + + Ok((fmspc, ca)) +} + +/// Extract the platform SGX TCB: 16 component SVNs and the PCE SVN, from the +/// PCK certificate's SGX extension. Needed to reproduce TCB-level matching. +fn extract_platform_sgx_tcb(leaf_der: &[u8]) -> Result<([u8; 16], u16)> { + let (_, cert) = + X509Certificate::from_der(leaf_der).context("failed to parse PCK leaf certificate")?; + let sgx_ext = sgx_extension(&cert)?; + let entries = parse_der_seq_of_pairs(sgx_ext)?; + + let tcb_val = entries + .iter() + .find(|(oid, _)| oid == &arcs_str(OID_SGX_TCB)) + .map(|(_, v)| v) + .context("SGX extension is missing the TCB entry")?; + // The TCB value is itself a SEQUENCE of (OID, value) pairs. + let tcb_entries = parse_der_seq_of_pairs(tcb_val).context("failed to parse SGX TCB entry")?; + + let mut comps = [0u8; 16]; + for (n, comp) in comps.iter_mut().enumerate() { + // Component OIDs are 1.2.840.113741.1.13.1.2. for n in 0..16. + let mut oid = OID_SGX_TCB.to_vec(); + oid.push((n + 1) as u64); + let v = tcb_entries + .iter() + .find(|(o, _)| o == &arcs_str(&oid)) + .map(|(_, v)| v) + .with_context(|| format!("SGX TCB component {} missing", n + 1))?; + *comp = der_integer_u64(v)? as u8; + } + let pcesvn_val = tcb_entries + .iter() + .find(|(o, _)| o == &arcs_str(OID_SGX_PCESVN)) + .map(|(_, v)| v) + .context("SGX TCB PCESVN missing")?; + let pcesvn = der_integer_u64(pcesvn_val)? as u16; + + Ok((comps, pcesvn)) +} + +fn sgx_extension<'a>(cert: &'a X509Certificate<'a>) -> Result<&'a [u8]> { + cert.extensions() + .iter() + .find(|e| e.oid.to_id_string() == arcs_str(OID_SGX_EXTENSION)) + .map(|e| e.value) + .context("PCK certificate is missing the Intel SGX extension") +} + +// --------------------------------------------------------------------------- +// Collateral fetch (PCCS) +// --------------------------------------------------------------------------- + +async fn fetch_collateral( + pccs_url: &str, + fmspc: &str, + ca: &str, + pck_chain: String, +) -> Result { + let base = pccs_url + .trim_end_matches('/') + .trim_end_matches("/sgx/certification/v4") + .trim_end_matches("/tdx/certification/v4") + .to_string(); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(60)) + .build() + .context("failed to build HTTP client for PCCS")?; + + // PCK CRL (always under the sgx path). + let pckcrl_url = format!("{base}/sgx/certification/v4/pckcrl?ca={ca}&encoding=der"); + let (pck_crl_issuer_chain, pck_crl) = + get_with_header(&client, &pckcrl_url, "SGX-PCK-CRL-Issuer-Chain").await?; + + // TCB info (tdx path for TDX). + let tcb_url = format!("{base}/tdx/certification/v4/tcb?fmspc={fmspc}"); + let (tcb_info_issuer_chain, tcb_body) = + get_with_header(&client, &tcb_url, "TCB-Info-Issuer-Chain").await?; + let tcb_json: serde_json::Value = + serde_json::from_slice(&tcb_body).context("TCB info is not valid JSON")?; + let tcb_info = tcb_json + .get("tcbInfo") + .context("TCB info response missing tcbInfo")? + .to_string(); + let tcb_info_signature = hex::decode( + tcb_json + .get("signature") + .and_then(|v| v.as_str()) + .context("TCB info response missing signature")?, + ) + .context("TCB info signature is not valid hex")?; + + // QE identity (tdx path for TDX). + let qe_url = format!("{base}/tdx/certification/v4/qe/identity?update=standard"); + let (qe_identity_issuer_chain, qe_body) = + get_with_header(&client, &qe_url, "SGX-Enclave-Identity-Issuer-Chain").await?; + let qe_json: serde_json::Value = + serde_json::from_slice(&qe_body).context("QE identity is not valid JSON")?; + let qe_identity = qe_json + .get("enclaveIdentity") + .context("QE identity response missing enclaveIdentity")? + .to_string(); + let qe_identity_signature = hex::decode( + qe_json + .get("signature") + .and_then(|v| v.as_str()) + .context("QE identity response missing signature")?, + ) + .context("QE identity signature is not valid hex")?; + + // Root CA CRL. PCCS serves it hex-encoded under the sgx path. + let rootcacrl_url = format!("{base}/sgx/certification/v4/rootcacrl"); + let root_ca_crl_raw = client + .get(&rootcacrl_url) + .send() + .await + .and_then(|r| r.error_for_status()) + .context("failed to fetch root CA CRL")? + .bytes() + .await + .context("failed to read root CA CRL body")?; + let root_ca_crl = match std::str::from_utf8(&root_ca_crl_raw) + .ok() + .and_then(|s| hex::decode(s.trim()).ok()) + { + Some(der) => der, + None => root_ca_crl_raw.to_vec(), + }; + + Ok(QuoteCollateralV3 { + pck_crl_issuer_chain, + root_ca_crl, + pck_crl, + tcb_info_issuer_chain, + tcb_info, + tcb_info_signature, + qe_identity_issuer_chain, + qe_identity, + qe_identity_signature, + pck_certificate_chain: Some(pck_chain), + }) +} + +/// GET a URL, returning (url-decoded issuer-chain header value, body bytes). +async fn get_with_header( + client: &reqwest::Client, + url: &str, + header: &str, +) -> Result<(String, Vec)> { + let resp = client + .get(url) + .send() + .await + .with_context(|| format!("failed to GET {url}"))? + .error_for_status() + .with_context(|| format!("PCCS returned an error for {url}"))?; + let hdr = resp + .headers() + .get(header) + .with_context(|| format!("PCCS response for {url} missing header {header}"))? + .to_str() + .context("issuer-chain header is not valid ASCII")? + .to_string(); + let hdr = urlencoding::decode(&hdr) + .context("failed to url-decode issuer-chain header")? + .into_owned(); + let body = resp + .bytes() + .await + .with_context(|| format!("failed to read body of {url}"))? + .to_vec(); + Ok((hdr, body)) +} + +// --------------------------------------------------------------------------- +// Result mapping (including TCB-level matching for tcb_level_date_tag) +// --------------------------------------------------------------------------- + +/// Collateral issue/expiration dates (union of TCB info and QE identity), +/// parsed once so they can be used both to clamp the verification timestamp and +/// to populate the result. +struct CollateralDates { + tcb_info: TcbInfo, + earliest_issue_date: i64, + latest_issue_date: i64, + earliest_expiration_date: i64, +} + +impl CollateralDates { + fn parse(collateral: &QuoteCollateralV3) -> Result { + let tcb_info: TcbInfo = + serde_json::from_str(&collateral.tcb_info).context("failed to parse tcb_info JSON")?; + let qe_identity: serde_json::Value = serde_json::from_str(&collateral.qe_identity) + .context("failed to parse qe_identity JSON")?; + + let tcb_issue = parse_iso8601(&tcb_info.issue_date)?; + let tcb_next = parse_iso8601(&tcb_info.next_update)?; + let qe_issue = qe_identity + .get("issueDate") + .and_then(|v| v.as_str()) + .map(parse_iso8601) + .transpose()?; + let qe_next = qe_identity + .get("nextUpdate") + .and_then(|v| v.as_str()) + .map(parse_iso8601) + .transpose()?; + + Ok(Self { + earliest_issue_date: qe_issue.map_or(tcb_issue, |q| tcb_issue.min(q)), + latest_issue_date: qe_issue.map_or(tcb_issue, |q| tcb_issue.max(q)), + earliest_expiration_date: qe_next.map_or(tcb_next, |q| tcb_next.min(q)), + tcb_info, + }) + } +} + +fn build_result( + quote: &[u8], + leaf_der: &[u8], + report: &VerifiedReport, + dates: &CollateralDates, + collateral_expired: bool, +) -> Result { + // Reproduce Intel QVL TCB-level matching to recover the matched level's + // tcb_date (`tcb_level_date_tag`). + let tcb_level_date_tag = match matched_tcb_level(quote, leaf_der, &dates.tcb_info) { + Ok(level) => parse_iso8601(&level.tcb_date)?, + Err(e) => { + debug!("dcap-qvl backend: could not resolve matched TCB level: {e:#}"); + 0 + } + }; + + Ok(TcbVerificationResult { + tcb_status: report.status.clone(), + tcb_status_code: status_to_code(&report.status), + collateral_expired, + earliest_issue_date: dates.earliest_issue_date, + latest_issue_date: dates.latest_issue_date, + earliest_expiration_date: dates.earliest_expiration_date, + tcb_level_date_tag, + tcb_eval_ref_num: dates.tcb_info.tcb_evaluation_data_number, + advisory_ids: report.advisory_ids.join(","), + tee_type: TEE_TYPE_TDX, + }) +} + +/// Find the TCB level the platform is at, mirroring Intel QVL: canonically +/// sort the levels (highest TCB first) and return the first level the platform +/// satisfies component-wise. +fn matched_tcb_level<'a>( + quote: &[u8], + leaf_der: &[u8], + tcb_info: &'a TcbInfo, +) -> Result<&'a TcbLevel> { + let (sgx_comps, pcesvn) = extract_platform_sgx_tcb(leaf_der)?; + let is_tdx = tcb_info.version >= 3 && tcb_info.id == "TDX"; + + // Platform TDX TEE TCB SVN from the TD report. + let tdx_svn: [u8; 16] = if is_tdx { + let parsed = parse_tdx_quote(quote)?; + parsed + .tcb_svn() + .try_into() + .map_err(|_| anyhow!("unexpected TDX TEE TCB SVN length"))? + } else { + [0u8; 16] + }; + + // Canonical order: SGX components desc, then PCE SVN desc, then TDX + // components desc (matches Intel QVL / dcap-qvl `canonicalize_tcb_levels`). + let mut levels: Vec<&TcbLevel> = tcb_info.tcb_levels.iter().collect(); + levels.sort_by(|a, b| { + svns(&b.tcb.sgx_components) + .cmp(&svns(&a.tcb.sgx_components)) + .then(b.tcb.pce_svn.cmp(&a.tcb.pce_svn)) + .then(svns(&b.tcb.tdx_components).cmp(&svns(&a.tcb.tdx_components))) + }); + + for level in levels { + if platform_meets(level, &sgx_comps, pcesvn, &tdx_svn, is_tdx) { + return Ok(level); + } + } + bail!("no TCB level matched the platform") +} + +fn platform_meets( + level: &TcbLevel, + sgx_comps: &[u8; 16], + pcesvn: u16, + tdx_svn: &[u8; 16], + is_tdx: bool, +) -> bool { + for (i, c) in level.tcb.sgx_components.iter().enumerate() { + if sgx_comps.get(i).copied().unwrap_or(0) < c.svn { + return false; + } + } + if pcesvn < level.tcb.pce_svn { + return false; + } + if is_tdx { + for (i, c) in level.tcb.tdx_components.iter().enumerate() { + if tdx_svn.get(i).copied().unwrap_or(0) < c.svn { + return false; + } + } + } + true +} + +fn svns(comps: &[TcbComponents]) -> Vec { + comps.iter().map(|c| c.svn).collect() +} + +/// Map a TCB status string to the numeric code the FFI backend reports in +/// `TcbVerificationResult::tcb_status_code` (the `sgx_ql_qv_result_t` values). +fn status_to_code(status: &str) -> u32 { + match status { + "UpToDate" => 0x0000_0000, + "ConfigurationNeeded" => 0x0000_A001, + "OutOfDate" => 0x0000_A002, + "OutOfDateConfigurationNeeded" => 0x0000_A003, + "InvalidSignature" => 0x0000_A004, + "Revoked" => 0x0000_A005, + "Unspecified" => 0x0000_A006, + "SWHardeningNeeded" => 0x0000_A007, + "ConfigurationAndSWHardeningNeeded" => 0x0000_A008, + _ => 0x0000_A006, // Unspecified + } +} + +// --------------------------------------------------------------------------- +// small DER / date helpers +// --------------------------------------------------------------------------- + +fn parse_iso8601(s: &str) -> Result { + // PCS timestamps are RFC 3339, e.g. "2024-03-13T00:00:00Z". + let dt = chrono::DateTime::parse_from_rfc3339(s) + .or_else(|_| chrono::DateTime::parse_from_rfc3339(&format!("{s}Z"))) + .with_context(|| format!("invalid timestamp: {s}"))?; + Ok(dt.timestamp()) +} + +/// Dotted-decimal string for an OID given as numeric arcs. +fn arcs_str(arcs: &[u64]) -> String { + arcs.iter() + .map(|a| a.to_string()) + .collect::>() + .join(".") +} + +/// Parse a DER `SEQUENCE OF SEQUENCE { OID, value }` into (dotted-OID, raw value +/// DER) pairs. `value` is the raw DER of whatever followed the OID. +fn parse_der_seq_of_pairs(der: &[u8]) -> Result)>> { + let (_, top) = Any::from_der(der).map_err(|e| anyhow!("DER parse error: {e}"))?; + let mut content = top.data; + let mut out = Vec::new(); + while !content.is_empty() { + let (rest, entry) = Any::from_der(content).map_err(|e| anyhow!("DER parse error: {e}"))?; + let (after_oid, oid) = + Oid::from_der(entry.data).map_err(|e| anyhow!("DER OID parse error: {e}"))?; + out.push((oid.to_id_string(), after_oid.to_vec())); + content = rest; + } + Ok(out) +} + +/// Interpret a raw DER value as an OCTET STRING and return its bytes. +fn der_octet_string(der: &[u8]) -> Result> { + let (_, any) = Any::from_der(der).map_err(|e| anyhow!("DER parse error: {e}"))?; + Ok(any.data.to_vec()) +} + +/// Interpret a raw DER value as an INTEGER and return it as u64. +fn der_integer_u64(der: &[u8]) -> Result { + let (_, any) = Any::from_der(der).map_err(|e| anyhow!("DER parse error: {e}"))?; + let mut v: u64 = 0; + for &b in any.data { + v = (v << 8) | b as u64; + } + Ok(v) +} + +fn find_sub(haystack: &[u8], needle: &[u8]) -> Option { + haystack.windows(needle.len()).position(|w| w == needle) +} + +fn rfind_sub(haystack: &[u8], needle: &[u8]) -> Option { + haystack.windows(needle.len()).rposition(|w| w == needle) +} + +#[cfg(test)] +mod tests { + use super::parse_qcnl_pccs_url; + + #[test] + fn parse_qcnl_ini_form() { + let conf = "# comment\nPCCS_URL=https://sgx-dcap-server.cn-hangzhou.aliyuncs.com/sgx/certification/v4/\nUSE_SECURE_CERT=false\n"; + assert_eq!( + parse_qcnl_pccs_url(conf).as_deref(), + Some("https://sgx-dcap-server.cn-hangzhou.aliyuncs.com/sgx/certification/v4/") + ); + } + + #[test] + fn parse_qcnl_json_form() { + let conf = "{\n // Intel default style\n \"pccs_url\": \"https://pccs.example.com/sgx/certification/v4/\",\n \"use_secure_cert\": true\n}\n"; + assert_eq!( + parse_qcnl_pccs_url(conf).as_deref(), + Some("https://pccs.example.com/sgx/certification/v4/") + ); + } + + #[test] + fn parse_qcnl_missing() { + assert_eq!(parse_qcnl_pccs_url("USE_SECURE_CERT=false\n"), None); + } +}