From 2df90cd09c9616f6c436e2f11835fe086058226c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Feb 2026 02:35:56 +0000 Subject: [PATCH 01/33] WIP: Add policy validation support --- Cargo.lock | 162 ++ Cargo.toml | 8 +- rego/qal_script.rego | 1038 +++++++++++++ src/collateral.rs | 7 +- src/constants.rs | 5 + src/intel.rs | 36 +- src/lib.rs | 24 +- src/oids.rs | 8 + src/policy.rs | 1752 ++++++++++++++++++++++ src/python.rs | 19 +- src/tcb_info.rs | 132 +- src/utils.rs | 27 + src/verify.rs | 541 +++++-- tests/near/contracts/gas-test/Cargo.lock | 2 +- tests/near/contracts/gas-test/src/lib.rs | 9 +- tests/verify_quote.rs | 855 ++++++++++- 16 files changed, 4404 insertions(+), 221 deletions(-) create mode 100644 rego/qal_script.rego create mode 100644 src/policy.rs diff --git a/Cargo.lock b/Cargo.lock index 1ae4054..9b64464 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.101" @@ -169,8 +178,22 @@ version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ + "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", ] [[package]] @@ -354,6 +377,7 @@ dependencies = [ "pem", "pyo3", "pyo3-async-runtimes", + "regorus", "reqwest", "ring", "rustls-pki-types", @@ -1028,6 +1052,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1350,6 +1398,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1550,6 +1608,24 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1878,6 +1954,51 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "regorus" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4431401ee52bd814219a0d5475d7825e82a17aa337ab718660820914f06267fd" +dependencies = [ + "anyhow", + "chrono", + "chrono-tz", + "lazy_static", + "num-bigint", + "num-traits", + "regex", + "semver", + "serde", + "serde_json", + "spin", + "thiserror", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -2255,6 +2376,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -2860,6 +2987,41 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index b79477c..338ba64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,11 @@ signature = { version = "2.2", default-features = false, optional = true } dcap-qvl-webpki = { version = "=0.103.4", features = ["alloc"] } +# Rego policy engine (optional, for Intel QAL compatibility) +regorus = { version = "0.9", optional = true, default-features = false, features = [ + "arc", "time", "regex", "semver", +] } + [dev-dependencies] hex-literal = "1.1.0" @@ -89,7 +94,7 @@ name = "dcap_qvl" crate-type = ["cdylib", "rlib"] [features] -default = ["std", "report", "ring", "rustcrypto"] +default = ["std", "report", "ring", "rustcrypto", "rego"] std = [ "serde/std", "scale/std", @@ -112,6 +117,7 @@ ring = ["dep:ring", "dcap-qvl-webpki/ring", "_anycrypto"] rustcrypto = ["dep:sha2", "dep:p256", "dep:signature", "dcap-qvl-webpki/rustcrypto", "_anycrypto"] _anycrypto = [] contract = ["getrandom"] +rego = ["dep:regorus", "std", "serde_json"] [lints.clippy] unwrap_used = "deny" diff --git a/rego/qal_script.rego b/rego/qal_script.rego new file mode 100644 index 0000000..adff4c7 --- /dev/null +++ b/rego/qal_script.rego @@ -0,0 +1,1038 @@ +package dcap.quote.appraisal + +import future.keywords.contains +import future.keywords.every +import future.keywords.if +import future.keywords.in + +# +# Constant value of each class id +# +sgx_id := "3123ec35-8d38-4ea5-87a5-d6c48b567570" + +enclave_id := "bef7cb8c-31aa-42c1-854c-10db005d5c41" + +tdx10_id := "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f" + +tdx15_id := "f708b97f-0fb2-4e6b-8b03-8a5bcd1221d3" + +tdqe_id := "3769258c-75e6-4bc7-8d72-d2b0e224cad2" + +guest_td10_id := "a1e4ee9c-a12e-48ac-bed0-e3f89297f687" + +guest_td15_id := "45b734fc-aa4e-4c3d-ad28-e43d08880e68" + +# +# UINT64_MAX for checking time.parse_rfc3339_ns return value +# +uint64_max := 18446744073709551615 + +# +# Utility rule to get matched report and policy based on class_id +# +collect_bundle[id] := bundle if { + some report in input.qvl_result + some policy in input.policies.policy_array + is_string(report.environment.class_id) + is_string(policy.environment.class_id) + lower(report.environment.class_id) == lower(policy.environment.class_id) + id := report.environment.class_id + bundle := {"report": report, "policy": policy} +} + +report_in_policy contains id if { + some report in input.qvl_result + some policy in input.policies.policy_array + is_string(report.environment.class_id) + is_string(policy.environment.class_id) + lower(report.environment.class_id) == lower(policy.environment.class_id) + id := report.environment.class_id +} + +# +# Utility rule to get report which doesn't has corresponding policy +# +report_not_in_policy contains report if { + some report in input.qvl_result + is_string(report.environment.class_id) + id := lower(report.environment.class_id) + not report_in_policy[id] +} + +# +# Utility rule to get quote hash +# +quote_hash contains hash if { + some qh in input.qvl_result + is_string(qh.quote_hash) + is_string(qh.algo) + hash := qh +} + +# +# Utility rule to get optional user data +# +optional_ud contains user_data if { + some ud in input.qvl_result + is_string(ud.user_data) + user_data := ud.user_data +} + +# +# Section 1: Format the final appraisal output +# +final_appraisal_result contains output if { + count(quote_hash) != 0 + count(optional_ud) != 0 + some user_data + user_data_str := optional_ud[user_data] + output := { + "overall_appraisal_result": final_ret, + "appraisal_check_date": time.now_ns(), + "nonce": rand.intn("appraisal", 1000000000000000), + "quote_hash": quote_hash, + "user_data": user_data_str, + "appraised_reports": appraisal_result, + "certification_data": certification_data, + } +} + +final_appraisal_result contains output if { + count(quote_hash) == 0 + count(optional_ud) == 0 + output := { + "overall_appraisal_result": final_ret, + "appraisal_check_date": time.now_ns(), + "nonce": rand.intn("appraisal", 1000000000000000), + "appraised_reports": appraisal_result, + "certification_data": certification_data, + } +} + +final_appraisal_result contains output if { + count(quote_hash) != 0 + count(optional_ud) == 0 + output := { + "overall_appraisal_result": final_ret, + "appraisal_check_date": time.now_ns(), + "nonce": rand.intn("appraisal", 1000000000000000), + "quote_hash": quote_hash, + "appraised_reports": appraisal_result, + "certification_data": certification_data, + } +} + +final_appraisal_result contains output if { + count(quote_hash) == 0 + count(optional_ud) != 0 + some user_data + user_data_str := optional_ud[user_data] + output := { + "overall_appraisal_result": final_ret, + "appraisal_check_date": time.now_ns(), + "nonce": rand.intn("appraisal", 1000000000000000), + "user_data": user_data_str, + "appraised_reports": appraisal_result, + "certification_data": certification_data, + } +} + +# Get final appraisal return value +default final_ret := 0 + +final_ret := 1 if { + count(appraisal_result) > 0 + every output in appraisal_result { + output.appraisal_result == 1 + } +} else := 0 if { + count(appraisal_result) > 0 + some output in appraisal_result + output.appraisal_result == 0 +} else := -1 if { + count(appraisal_result) > 0 + every output in appraisal_result { + output.appraisal_result != 0 + } + some ret in appraisal_result + ret.appraisal_result == -1 +} + +# +# Section 2: Try to get appraisal result for each report and corresponding policy +# +# appraise report for TDX 1.5 platform +appraisal_result contains appraisal_output if { + some item in collect_bundle + item.report.environment.class_id == tdx15_id + appraise_ret := platform_appraisal_ret(item) + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + "policy": item.policy, + "detailed_result": platform_sub_ret(item), + } +} + +# +# appraise report for TDX 1.0 platform +# +appraisal_result contains appraisal_output if { + some item in collect_bundle + item.report.environment.class_id == tdx10_id + appraise_ret := platform_appraisal_ret(item) + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + "policy": item.policy, + "detailed_result": platform_sub_ret(item), + } +} + +# +# appraise report for TD QE +# +appraisal_result contains appraisal_output if { + some item in collect_bundle + item.report.environment.class_id == tdqe_id + appraise_ret := td_qe_appraisal_ret(item) + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + "policy": item.policy, + "detailed_result": td_qe_sub_ret(item), + } +} + +# +# appraise report for guest TD 1.5 +# +appraisal_result contains appraisal_output if { + some item in collect_bundle + item.report.environment.class_id == guest_td15_id + appraise_ret := td_appraisal_ret(item) + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + "policy": item.policy, + "detailed_result": td_sub_ret(item), + } +} + +# +# appraise report for guest TD 1.0 +# +appraisal_result contains appraisal_output if { + some item in collect_bundle + item.report.environment.class_id == guest_td10_id + appraise_ret := td_appraisal_ret(item) + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + "policy": item.policy, + "detailed_result": td_sub_ret(item), + } +} + +# +# appraise report for SGX platform +# +appraisal_result contains appraisal_output if { + some item in collect_bundle + item.report.environment.class_id == sgx_id + appraise_ret := platform_appraisal_ret(item) + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + "policy": item.policy, + "detailed_result": platform_sub_ret(item), + } +} + +# +# appraise report for SGX enclave +# +appraisal_result contains appraisal_output if { + some item in collect_bundle + item.report.environment.class_id == enclave_id + appraise_ret := enclave_appraisal_ret(item) + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + "policy": item.policy, + "detailed_result": enclave_sub_ret(item), + } +} + +# +# appraise report for those report which doesn't have policy +# +appraisal_result contains appraisal_output if { + some item in report_not_in_policy + appraise_ret := -1 + + appraisal_output := { + "appraisal_result": appraise_ret, + "report": {"environment": item.report.environment, "measurement": item.report.measurement}, + } +} + +# +# Extract certification data from QVL report +# Suppose QVL report should always has one certification data +# +certification_data contains cert_data if { + some item in collect_bundle + + cert_data := item.report.certification_data +} + +# +# Section 3: Platform TCB appraisal +# +# Check to see if any in a set of status are not in the policy +default unaccepted_tcb_status_present(_) := false + +# tcb status check fail in below cases +# a. accepted_tcb_status is a single string, and tcb_status is not same between policy and report +# b. accepted_tcb_status is an array of string, and one of tcb_status in report is not in accpeted_tcb_status +unaccepted_tcb_status_present(bundle) if { + some status in bundle.report.measurement.tcb_status + is_string(status) + is_string(bundle.policy.reference.accepted_tcb_status) + upper(status) != upper(bundle.policy.reference.accepted_tcb_status) +} + +unaccepted_tcb_status_present(bundle) if { + # support user to input string or 'array of string' + is_array(bundle.policy.reference.accepted_tcb_status) + upper_accepted_tcb := [val | + some status in bundle.policy.reference.accepted_tcb_status + val := upper(status) + ] + some status in bundle.report.measurement.tcb_status + not upper(status) in upper_accepted_tcb +} + +default tcb_status_present(_) := false + +tcb_status_present(bundle) if { + # only accept array of string in QVL output + is_array(bundle.report.measurement.tcb_status) +} + +default tcb_uptodate_check(_) := false + +tcb_uptodate_check(bundle) if { + is_array(bundle.policy.reference.accepted_tcb_status) + + # tcb status must have UpToDate + basic_status := "UPTODATE" + upper_accepted_tcb := [val | + some status in bundle.policy.reference.accepted_tcb_status + val := upper(status) + ] + basic_status in upper_accepted_tcb +} + +tcb_uptodate_check(bundle) if { + is_string(bundle.policy.reference.accepted_tcb_status) + + # tcb status must have UpToDate + basic_status := "UPTODATE" + basic_status == upper(bundle.policy.reference.accepted_tcb_status) +} + +# Appraise required tcb_status - this must not contain any strings that are not in the policy +default tcb_status_ok(_) := false + +# tcb status is OK if none of the individual status are rejected and the input contains a tcb_status +tcb_status_ok(bundle) if { + tcb_status_present(bundle) + tcb_uptodate_check(bundle) + not unaccepted_tcb_status_present(bundle) +} + +# Appraise required platform_tcb expiration_date_check +# if policy.reference.collateral_grace_period is provided, +# then the platform TCB earliest_expiration_date must be within the grace period +default expiration_date_check_ok(_) := false + +expiration_date_check_ok(bundle) if { + not bundle.policy.reference.collateral_grace_period +} + +# If user defines collateral_grace_period +# min_eval_num must not be present +expiration_date_check_ok(bundle) if { + is_number(bundle.policy.reference.collateral_grace_period) + not bundle.policy.reference.min_eval_num + + # Convert grace period from seconds to ns + grace_period := bundle.policy.reference.collateral_grace_period * 1000000000 + expiration_date := time.parse_rfc3339_ns(bundle.report.measurement.earliest_expiration_date) + expiration_date != uint64_max + expiration_date + grace_period >= time.now_ns() +} + +# Appraise platform_tcb.tcb_level_date_tag +# if policies.reference.platform_grace_period is provided, then +# the platform_tcb.tcb_level_date_tag must be within the grace period +default earliest_accepted_tcb_level_date_tag_ok(_) := false + +tcb_level_date_tag_basic_check(bundle) if { + is_number(bundle.policy.reference.platform_grace_period) + is_number(bundle.policy.reference.collateral_grace_period) + bundle.policy.reference.collateral_grace_period == 0 + not bundle.policy.reference.min_eval_num + basic_status := ["UPTODATE", "OUTOFDATE"] + is_array(bundle.policy.reference.accepted_tcb_status) + upper_accepted_tcb := [val | + some status in bundle.policy.reference.accepted_tcb_status + val := upper(status) + ] + every status in basic_status { + status in upper_accepted_tcb + } +} + +earliest_accepted_tcb_level_date_tag_ok(bundle) if { + not bundle.policy.reference.platform_grace_period +} + +# If current TCB status in report is one of "UpToDate", "ConfigurationNeeded", "SWHardeningNeeded" or "TDRelaunchAdvised" +# and collateral has no expiry, then ignore the check +earliest_accepted_tcb_level_date_tag_ok(bundle) if { + tcb_level_date_tag_basic_check(bundle) + expiration_date_check_ok(bundle) + ignored_status := ["UPTODATE", "CONFIGURATIONNEEDED", "SWHARDENINGNEEDED", "TDRELAUNCHADVISED"] + every status in bundle.report.measurement.tcb_status { + upper(status) in ignored_status + } +} + +# If user defines platform_grace_period, then collateral_grace_period must be 0 +# accepted_tcb_status must include UpToDate and OutOfDate +# min_eval_num must not be present +earliest_accepted_tcb_level_date_tag_ok(bundle) if { + tcb_level_date_tag_basic_check(bundle) + grace_period := bundle.policy.reference.platform_grace_period * 1000000000 + expiration_date := time.parse_rfc3339_ns(bundle.report.measurement.tcb_level_date_tag) + expiration_date != uint64_max + expiration_date + grace_period >= time.now_ns() +} + +# Appriasal platform_tcb tcb_level_date_tag +# if policies.reference.min_tcb_level_date is provided, then +# the platform_tcb.tcb_level_date_tag must not be before the policy +# min_tcb_level_date +default accepted_tcb_level_date_tag_ok(_) := false + +accepted_tcb_level_date_tag_ok(bundle) if { + not bundle.policy.sgx_platform.reference.min_tcb_level_date +} + +accepted_tcb_level_date_tag_ok(bundle) if { + is_string(bundle.policy.reference.min_tcb_level_date) + min_tcb_date := time.parse_rfc3339_ns(bundle.policy.reference.min_tcb_level_date) + min_tcb_date != uint64_max + tcb_level_date := time.parse_rfc3339_ns(bundle.report.measurement.tcb_level_date_tag) + tcb_level_date != uint64_max + tcb_level_date >= min_tcb_date +} + +# Appraise optional platform_tcb tcb_eval_num +default tcb_eval_num_ok(_) := false + +tcb_eval_num_ok(bundle) if { + not bundle.policy.reference.min_eval_num +} + +# If user defines min_eval_num, then platform_grace_period must not be present +# collateral_grace_period also must not be present +# accepted_tcb_status must include UpToDate +tcb_eval_num_ok(bundle) if { + is_number(bundle.report.measurement.tcb_eval_num) + is_number(bundle.policy.reference.min_eval_num) + not bundle.policy.reference.platform_grace_period + not bundle.policy.reference.collateral_grace_period + bundle.report.measurement.tcb_eval_num >= bundle.policy.reference.min_eval_num +} + +# Appraise optional platform_tcb platform_provider_id +default platform_provider_id_ok(_) := false + +platform_provider_id_ok(bundle) if { + not bundle.policy.reference.accepted_platform_provider_ids +} + +platform_provider_id_ok(bundle) if { + some provider_id in bundle.policy.reference.accepted_platform_provider_ids + is_string(provider_id) + is_string(bundle.report.measurement.platform_provider_id) + lower(provider_id) == lower(bundle.report.measurement.platform_provider_id) +} + +# Appraise sgx_type - all required_sgx_type in policy should not be missing +# sgx_type has swtiched from string to integer (0, 1, 2) +# Suppose sgx_type in QVL output should be one of 0, 1, 2 +default sgx_types_ok(_) := false + +sgx_types_ok(bundle) if { + not bundle.policy.reference.accepted_sgx_types +} + +sgx_types_ok(bundle) if { + is_array(bundle.policy.reference.accepted_sgx_types) + is_number(bundle.report.measurement.sgx_type) + bundle.report.measurement.sgx_type in bundle.policy.reference.accepted_sgx_types +} + +sgx_types_ok(bundle) if { + is_number(bundle.policy.reference.accepted_sgx_types) + is_number(bundle.report.measurement.sgx_type) + bundle.report.measurement.sgx_type == bundle.policy.reference.accepted_sgx_types +} + +# Appraise dynamic_platform, only fail in below situation +# policy 'allow_dynamic_platform = false' AND report 'dynamic_platform = true' +default dynamic_platform_ok(_) := false + +dynamic_platform_ok(bundle) if { + not dynamic_platform_fail(bundle) +} + +default dynamic_platform_fail(_) := false + +dynamic_platform_fail(bundle) if { + bundle.report.measurement.is_dynamic_platform + bundle.policy.reference.allow_dynamic_platform == false +} + +# Appraise cached_keys, only fail in below situation +# policy 'allow_cached_keys = false' AND report 'cached_keys = true' +default cached_keys_ok(_) := false + +cached_keys_ok(bundle) if { + not cached_keys_fail(bundle) +} + +default cached_keys_fail(_) := false + +cached_keys_fail(bundle) if { + bundle.report.measurement.cached_keys + bundle.policy.reference.allow_cached_keys == false +} + +# Appraise smt_enabled, only fail in below situation +# policy 'allow_smt_enabled = false' AND report 'smt_enabled = true' +default smt_enabled_ok(_) := false + +smt_enabled_ok(bundle) if { + not smt_enabled_fail(bundle) +} + +default smt_enabled_fail(_) := false + +smt_enabled_fail(bundle) if { + bundle.report.measurement.smt_enabled + bundle.policy.reference.allow_smt_enabled == false +} + +# Appraise optional platform_tcb advisory_ids +default advisory_ids_ok(_) := false + +advisory_ids_ok(bundle) if { + not advisory_ids_rejected(bundle) +} + +advisory_ids_rejected(bundle) if { + some report_id in bundle.report.measurement.advisory_ids + some policy_id in bundle.policy.reference.rejected_advisory_ids + upper(report_id) == upper(policy_id) +} + +default platform_tcb_policy_present(_) := false + +platform_tcb_policy_present(bundle) if { + bundle.policy + lower(bundle.policy.environment.class_id) == lower(bundle.report.environment.class_id) +} + +# Sum up platform TCB appraisal +default platform_appraisal_ret(_) := 0 + +platform_appraisal_ret(bundle) := -1 if { + not platform_tcb_policy_present(bundle) +} else := 1 if { + tcb_status_ok(bundle) + expiration_date_check_ok(bundle) + earliest_accepted_tcb_level_date_tag_ok(bundle) + accepted_tcb_level_date_tag_ok(bundle) + tcb_eval_num_ok(bundle) + platform_provider_id_ok(bundle) + dynamic_platform_ok(bundle) + cached_keys_ok(bundle) + smt_enabled_ok(bundle) + advisory_ids_ok(bundle) + sgx_types_ok(bundle) +} else := 0 + +# Try to output return value for each platform sub function +platform_sub_ret(bundle) := {{ + "tcb_status_check": tcb_status_ok(bundle), + "expiration_date_check": expiration_date_check_ok(bundle), + "earliest_accepted_tcb_level_date_tag_check": earliest_accepted_tcb_level_date_tag_ok(bundle), + "accepted_tcb_level_date_tag_check": accepted_tcb_level_date_tag_ok(bundle), + "tcb_eval_num_check": tcb_eval_num_ok(bundle), + "platform_provider_id_check": platform_provider_id_ok(bundle), + "dynamic_platform_check": dynamic_platform_ok(bundle), + "cached_keys_check": cached_keys_ok(bundle), + "smt_enabled_check": smt_enabled_ok(bundle), + "advisory_ids_check": advisory_ids_ok(bundle), + "sgx_types_check": sgx_types_ok(bundle), +}} + +# +# Section 4: TD QE appraisal, reuse part of functions in platform appraisal +# +default td_qe_policy_present(_) := false + +td_qe_policy_present(bundle) if { + bundle.policy + lower(bundle.policy.environment.class_id) == lower(bundle.report.environment.class_id) +} + +# Sum up platform TCB appraisal +default td_qe_appraisal_ret(_) := 0 + +td_qe_appraisal_ret(bundle) := -1 if { + not td_qe_policy_present(bundle) +} else := 1 if { + tcb_status_ok(bundle) + expiration_date_check_ok(bundle) + earliest_accepted_tcb_level_date_tag_ok(bundle) + accepted_tcb_level_date_tag_ok(bundle) + tcb_eval_num_ok(bundle) +} else := 0 + +# Try to output return value for each platform sub function +td_qe_sub_ret(bundle) := {{ + "td_qe_tcb_status_check": tcb_status_ok(bundle), + "td_qe_expiration_date_check": expiration_date_check_ok(bundle), + "td_qe_earliest_accepted_tcb_level_date_tag_check": earliest_accepted_tcb_level_date_tag_ok(bundle), + "td_qe_accepted_tcb_level_date_tag_check": accepted_tcb_level_date_tag_ok(bundle), + "td_qe_tcb_eval_num_check": tcb_eval_num_ok(bundle), +}} + +# +# Section 5: application enclave appraisal +# +default application_enclave_tcb_policy_present(_) := false + +application_enclave_tcb_policy_present(bundle) if { + bundle.policy + lower(bundle.policy.environment.class_id) == lower(bundle.report.environment.class_id) +} + +# Appraise optional enclave_identity miscselect +default miscselect_ok(_) := false + +miscselect_ok(bundle) if { + not bundle.policy.reference.sgx_miscselect +} + +miscselect_ok(bundle) if { + hex2int := { + "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, + "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15, + } + value := split(upper(bundle.report.measurement.sgx_miscselect), "") + policy := split(upper(bundle.policy.reference.sgx_miscselect), "") + mask := split(upper(bundle.policy.reference.sgx_miscselect_mask), "") + equal_num := count({i | + mask[i] + bits.and(hex2int[value[i]], hex2int[mask[i]]) == bits.and(hex2int[policy[i]], hex2int[mask[i]]) + }) + orig_num := count(mask) + equal_num == orig_num +} + +# Appraise required enclave_identity attributes +default attributes_ok(_) := false + +attributes_ok(bundle) if { + hex2int := { + "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, + "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15, + } + value := split(upper(bundle.report.measurement.sgx_attributes), "") + policy := split(upper(bundle.policy.reference.sgx_attributes), "") + mask := split(upper(bundle.policy.reference.sgx_attributes_mask), "") + equal_num := count({i | + mask[i] + bits.and(hex2int[value[i]], hex2int[mask[i]]) == bits.and(hex2int[policy[i]], hex2int[mask[i]]) + }) + orig_num := count(mask) + equal_num == orig_num +} + +# Appraise optional enclave_identity ce_attributes +default ce_attributes_ok(_) := false + +ce_attributes_ok(bundle) if { + not bundle.policy.reference.sgx_ce_attributes +} + +ce_attributes_ok(bundle) if { + hex2int := { + "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, + "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15, + } + value := split(upper(bundle.report.measurement.sgx_ce_attributes), "") + policy := split(upper(bundle.policy.reference.sgx_ce_attributes), "") + mask := split(upper(bundle.policy.reference.sgx_ce_attributes_mask), "") + equal_num := count({i | + mask[i] + bits.and(hex2int[value[i]], hex2int[mask[i]]) == bits.and(hex2int[policy[i]], hex2int[mask[i]]) + }) + orig_num := count(mask) + equal_num == orig_num +} + +default mrenclave_ok(_) := false + +mrenclave_ok(bundle) if { + not bundle.policy.reference.sgx_mrenclave +} + +mrenclave_ok(bundle) if { + lower(bundle.report.measurement.sgx_mrenclave) == lower(bundle.policy.reference.sgx_mrenclave) +} + +default mrsigner_ok(_) := false + +mrsigner_ok(bundle) if { + not bundle.policy.reference.sgx_mrsigner +} + +mrsigner_ok(bundle) if { + lower(bundle.report.measurement.sgx_mrsigner) == lower(bundle.policy.reference.sgx_mrsigner) +} + +default isvprod_id_ok(_) := false + +isvprod_id_ok(bundle) if { + not bundle.policy.reference.sgx_isvprodid +} + +isvprod_id_ok(bundle) if { + is_number(bundle.report.measurement.sgx_isvprodid) + is_number(bundle.policy.reference.sgx_isvprodid) + bundle.report.measurement.sgx_isvprodid == bundle.policy.reference.sgx_isvprodid +} + +default isvsvn_ok(_) := false + +isvsvn_ok(bundle) if { + not bundle.policy.reference.sgx_isvsvn_min +} + +isvsvn_ok(bundle) if { + is_number(bundle.report.measurement.sgx_isvsvn) + is_number(bundle.policy.reference.sgx_isvsvn_min) + bundle.report.measurement.sgx_isvsvn >= bundle.policy.reference.sgx_isvsvn_min +} + +# Appraise optional kss fields: configid, configsvn_min, isvextprodid, isvfamilyid +default configid_ok(_) := false + +configid_ok(bundle) if { + not bundle.policy.reference.sgx_configid +} + +configid_ok(bundle) if { + is_string(bundle.report.measurement.sgx_configid) + is_string(bundle.policy.reference.sgx_configid) + lower(bundle.report.measurement.sgx_configid) == lower(bundle.policy.reference.sgx_configid) +} + +default configsvn_ok(_) := false + +configsvn_ok(bundle) if { + not bundle.policy.reference.sgx_configsvn_min +} + +configsvn_ok(bundle) if { + is_number(bundle.report.measurement.sgx_configsvn) + is_number(bundle.policy.reference.sgx_configsvn_min) + bundle.report.measurement.sgx_configsvn >= bundle.policy.reference.sgx_configsvn_min +} + +default isvextprodid_ok(_) := false + +isvextprodid_ok(bundle) if { + not bundle.policy.reference.sgx_isvextprodid +} + +isvextprodid_ok(bundle) if { + is_string(bundle.report.measurement.sgx_isvextprodid) + is_string(bundle.policy.reference.sgx_isvextprodid) + lower(bundle.report.measurement.sgx_isvextprodid) == lower(bundle.policy.reference.sgx_isvextprodid) +} + +default isvfamilyid_ok(_) := false + +isvfamilyid_ok(bundle) if { + not bundle.policy.reference.sgx_isvfamilyid +} + +isvfamilyid_ok(bundle) if { + is_string(bundle.report.measurement.sgx_isvfamilyid) + is_string(bundle.policy.reference.sgx_isvfamilyid) + lower(bundle.report.measurement.sgx_isvfamilyid) == bundle.policy.reference.sgx_isvfamilyid +} + +# Sum up enclave appraisal +default enclave_appraisal_ret(_) := 0 + +enclave_appraisal_ret(bundle) := -1 if { + not application_enclave_tcb_policy_present(bundle) +} else := 1 if { + miscselect_ok(bundle) + attributes_ok(bundle) + ce_attributes_ok(bundle) + mrenclave_ok(bundle) + mrsigner_ok(bundle) + isvprod_id_ok(bundle) + isvsvn_ok(bundle) + configid_ok(bundle) + configsvn_ok(bundle) + isvextprodid_ok(bundle) + isvfamilyid_ok(bundle) +} else := 0 + +# Try to output return value for each enclave sub function +enclave_sub_ret(bundle) := {{ + "sgx_miscselcect_check": miscselect_ok(bundle), + "sgx_attributes_check": attributes_ok(bundle), + "sgx_ce_attributes_check": ce_attributes_ok(bundle), + "sgx_mrenclave_check": mrenclave_ok(bundle), + "sgx_mrsigner_check": mrsigner_ok(bundle), + "sgx_isvprod_id_check": isvprod_id_ok(bundle), + "sgx_isvsvn_check": isvsvn_ok(bundle), + "sgx_configid_check": configid_ok(bundle), + "sgx_configsvn_check": configsvn_ok(bundle), + "sgx_isvextprodid_check": isvextprodid_ok(bundle), + "sgx_isvfamilyid_check": isvfamilyid_ok(bundle), +}} + +# +# Section 6: TD appraisal +# +default td_tcb_policy_present(_) := false + +td_tcb_policy_present(bundle) if { + bundle.policy + lower(bundle.policy.environment.class_id) == lower(bundle.report.environment.class_id) +} + +# Appraise required guest td attributes +default td_attributes_ok(_) := false + +td_attributes_ok(bundle) if { + not bundle.policy.reference.tdx_attributes + not bundle.policy.reference.tdx_attributes_mask +} + +td_attributes_ok(bundle) if { + hex2int := { + "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, + "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15, + } + value := split(upper(bundle.report.measurement.tdx_attributes), "") + policy := split(upper(bundle.policy.reference.tdx_attributes), "") + mask := split(upper(bundle.policy.reference.tdx_attributes_mask), "") + equal_num := count({i | + mask[i] + bits.and(hex2int[value[i]], hex2int[mask[i]]) == bits.and(hex2int[policy[i]], hex2int[mask[i]]) + }) + orig_num := count(mask) + equal_num == orig_num +} + +# Appraise optional guest td xfam +default td_xfam_ok(_) := false + +td_xfam_ok(bundle) if { + not bundle.policy.reference.tdx_xfam + not bundle.policy.reference.tdx_xfam_mask +} + +td_xfam_ok(bundle) if { + hex2int := { + "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, + "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15, + } + value := split(upper(bundle.report.measurement.tdx_xfam), "") + policy := split(upper(bundle.policy.reference.tdx_xfam), "") + mask := split(upper(bundle.policy.reference.tdx_xfam_mask), "") + equal_num := count({i | + mask[i] + bits.and(hex2int[value[i]], hex2int[mask[i]]) == bits.and(hex2int[policy[i]], hex2int[mask[i]]) + }) + orig_num := count(mask) + equal_num == orig_num +} + +# Appraise guest td tdx_mrconfigid, tdx_mrowner, tdx_mrownerconfig and tdx_mrtd +default td_mrconfigid_ok(_) := false + +td_mrconfigid_ok(bundle) if { + not bundle.policy.reference.tdx_mrconfigid +} + +td_mrconfigid_ok(bundle) if { + is_string(bundle.report.measurement.tdx_mrconfigid) + is_string(bundle.policy.reference.tdx_mrconfigid) + lower(bundle.report.measurement.tdx_mrconfigid) == lower(bundle.policy.reference.tdx_mrconfigid) +} + +default td_mrowner_ok(_) := false + +td_mrowner_ok(bundle) if { + not bundle.policy.reference.tdx_mrowner +} + +td_mrowner_ok(bundle) if { + is_string(bundle.report.measurement.tdx_mrowner) + is_string(bundle.policy.reference.tdx_mrowner) + lower(bundle.report.measurement.tdx_mrowner) == lower(bundle.policy.reference.tdx_mrowner) +} + +default td_mrownerconfig_ok(_) := false + +td_mrownerconfig_ok(bundle) if { + not bundle.policy.reference.tdx_mrownerconfig +} + +td_mrownerconfig_ok(bundle) if { + is_string(bundle.report.measurement.tdx_mrownerconfig) + is_string(bundle.policy.reference.tdx_mrownerconfig) + lower(bundle.report.measurement.tdx_mrownerconfig) == lower(bundle.policy.reference.tdx_mrownerconfig) +} + +default td_mrtd_ok(_) := false + +td_mrtd_ok(bundle) if { + not bundle.policy.reference.tdx_mrtd +} + +td_mrtd_ok(bundle) if { + is_string(bundle.report.measurement.tdx_mrtd) + is_string(bundle.policy.reference.tdx_mrtd) + lower(bundle.report.measurement.tdx_mrtd) == lower(bundle.policy.reference.tdx_mrtd) +} + +# Appraise optional rtmr 0~4 +default td_rtmr0_ok(_) := false + +td_rtmr0_ok(bundle) if { + not bundle.policy.reference.tdx_rtmr0 +} + +td_rtmr0_ok(bundle) if { + is_string(bundle.report.measurement.tdx_rtmr0) + is_string(bundle.policy.reference.tdx_rtmr0) + lower(bundle.report.measurement.tdx_rtmr0) == lower(bundle.policy.reference.tdx_rtmr0) +} + +default td_rtmr1_ok(_) := false + +td_rtmr1_ok(bundle) if { + not bundle.policy.reference.tdx_rtmr1 +} + +td_rtmr1_ok(bundle) if { + is_string(bundle.report.measurement.tdx_rtmr1) + is_string(bundle.policy.reference.tdx_rtmr1) + lower(bundle.report.measurement.tdx_rtmr1) == lower(bundle.policy.reference.tdx_rtmr1) +} + +default td_rtmr2_ok(_) := false + +td_rtmr2_ok(bundle) if { + not bundle.policy.reference.tdx_rtmr2 +} + +td_rtmr2_ok(bundle) if { + is_string(bundle.report.measurement.tdx_rtmr2) + is_string(bundle.policy.reference.tdx_rtmr2) + lower(bundle.report.measurement.tdx_rtmr2) == lower(bundle.policy.reference.tdx_rtmr2) +} + +default td_rtmr3_ok(_) := false + +td_rtmr3_ok(bundle) if { + not bundle.policy.reference.tdx_rtmr3 +} + +td_rtmr3_ok(bundle) if { + is_string(bundle.report.measurement.tdx_rtmr3) + is_string(bundle.policy.reference.tdx_rtmr3) + lower(bundle.report.measurement.tdx_rtmr3) == lower(bundle.policy.reference.tdx_rtmr3) +} + +# Appraise optional tdx mrservicetd, only available for TDX 1.5 +default td_mrservicetd_ok(_) := false + +td_mrservicetd_ok(bundle) if { + not bundle.policy.reference.tdx_mrservicetd +} + +td_mrservicetd_ok(bundle) if { + is_string(bundle.report.measurement.tdx_mrservicetd) + is_string(bundle.policy.reference.tdx_mrservicetd) + lower(bundle.report.measurement.tdx_mrservicetd) == lower(bundle.policy.reference.tdx_mrservicetd) +} + +# Sum up TD appraisal +default td_appraisal_ret(_) := 0 + +td_appraisal_ret(bundle) := -1 if { + not td_tcb_policy_present(bundle) +} else := 1 if { + td_attributes_ok(bundle) + td_xfam_ok(bundle) + td_mrconfigid_ok(bundle) + td_mrowner_ok(bundle) + td_mrownerconfig_ok(bundle) + td_mrtd_ok(bundle) + td_rtmr0_ok(bundle) + td_rtmr1_ok(bundle) + td_rtmr2_ok(bundle) + td_rtmr3_ok(bundle) + td_mrservicetd_ok(bundle) +} else := 0 + +# Try to output return value for each platform sub function +td_sub_ret(bundle) := {{ + "td_attributes_check": td_attributes_ok(bundle), + "td_xfam_check": td_xfam_ok(bundle), + "td_mrconfigid_check": td_mrconfigid_ok(bundle), + "td_mrowner_check": td_mrowner_ok(bundle), + "td_mrownerconfig_check": td_mrownerconfig_ok(bundle), + "td_mrtd_check": td_mrtd_ok(bundle), + "td_rtmr0_check": td_rtmr0_ok(bundle), + "td_rtmr1_check": td_rtmr1_ok(bundle), + "td_rtmr2_check": td_rtmr2_ok(bundle), + "td_rtmr3_check": td_rtmr3_ok(bundle), + "td_mrservicetd_check": td_mrservicetd_ok(bundle), +}} diff --git a/src/collateral.rs b/src/collateral.rs index 8e6188c..4e049bd 100644 --- a/src/collateral.rs +++ b/src/collateral.rs @@ -423,7 +423,7 @@ pub async fn get_collateral_from_pcs(quote: &[u8]) -> Result get_collateral(INTEL_PCS_URL, quote).await } -/// Get collateral and verify the quote (uses ring backend). +/// Get collateral and verify the quote, returning [`QuoteVerificationResult`](crate::verify::QuoteVerificationResult). /// /// # Arguments /// @@ -433,7 +433,8 @@ pub async fn get_collateral_from_pcs(quote: &[u8]) -> Result pub async fn get_collateral_and_verify( quote: &[u8], pccs_url: Option<&str>, -) -> Result { +) -> Result { + use crate::verify::QuoteVerifier; use std::time::SystemTime; let pccs_url = pccs_url @@ -445,7 +446,7 @@ pub async fn get_collateral_and_verify( .duration_since(SystemTime::UNIX_EPOCH) .context("Failed to get current time")? .as_secs(); - crate::verify::verify(quote, &collateral, now) + QuoteVerifier::new_prod_default_crypto().verify(quote, &collateral, now) } #[cfg(test)] diff --git a/src/constants.rs b/src/constants.rs index 63b068b..441c008 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,9 +1,14 @@ #![allow(dead_code)] +/// MR_SIGNER measurement (32 bytes) pub type MrSigner = [u8; 32]; +/// MR_ENCLAVE measurement (32 bytes) pub type MrEnclave = [u8; 32]; +/// FMSPC - Firmware Security Version & Package Configuration (6 bytes) pub type Fmspc = [u8; 6]; +/// CPU SVN - Security Version Number for CPU microcode (16 bytes) pub type CpuSvn = [u8; 16]; +/// SVN - Security Version Number (16-bit) pub type Svn = u16; pub const ATTESTATION_KEY_TYPE_ECDSA256_WITH_P256_CURVE: u16 = 2; diff --git a/src/intel.rs b/src/intel.rs index b9593cf..62fb0a4 100644 --- a/src/intel.rs +++ b/src/intel.rs @@ -11,7 +11,7 @@ use crate::{ utils, }; -/// Parsed values from the Intel SGX extension. +/// Parsed values from the Intel SGX extension in a PCK certificate. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PckExtension { pub ppid: Vec, @@ -22,6 +22,15 @@ pub struct PckExtension { pub sgx_type: u64, pub platform_instance_id: Option>, pub raw_extension: Vec, + /// Whether the platform can be extended with additional packages + /// (Platform CA certs only; `None` for Processor CA certs) + pub dynamic_platform: Option, + /// Whether platform root keys are cached by SGX Registration Backend + /// (Platform CA certs only; `None` for Processor CA certs) + pub cached_keys: Option, + /// Whether SMT (simultaneous multithreading / hyperthreading) is enabled + /// (Platform CA certs only; `None` for Processor CA certs) + pub smt_enabled: Option, } impl PckExtension { @@ -74,6 +83,20 @@ pub fn parse_pck_extension(cert_der: &[u8]) -> Result { let sgx_type = decode_enumerated(&find_extension_required(&[oids::SGX_TYPE], &extension)?)?; let platform_instance_id = find_extension_optional(&[oids::PLATFORM_INSTANCE_ID], &extension)?; + // Configuration flags (only present in Platform CA certs, under OID 1.2.840.113741.1.13.1.7) + let dynamic_platform = + find_extension_optional(&[oids::CONFIGURATION, oids::DYNAMIC_PLATFORM], &extension)? + .map(|v| decode_boolean(&v)) + .transpose()?; + let cached_keys = + find_extension_optional(&[oids::CONFIGURATION, oids::CACHED_KEYS], &extension)? + .map(|v| decode_boolean(&v)) + .transpose()?; + let smt_enabled = + find_extension_optional(&[oids::CONFIGURATION, oids::SMT_ENABLED], &extension)? + .map(|v| decode_boolean(&v)) + .transpose()?; + Ok(PckExtension { ppid, cpu_svn, @@ -83,6 +106,9 @@ pub fn parse_pck_extension(cert_der: &[u8]) -> Result { sgx_type, platform_instance_id, raw_extension: extension, + dynamic_platform, + cached_keys, + smt_enabled, }) } @@ -176,6 +202,14 @@ fn find_recursive<'a>( Ok(None) } +fn decode_boolean(bytes: &[u8]) -> Result { + match bytes[..] { + [0x00] => Ok(false), + [_] => Ok(true), + _ => bail!("Unexpected BOOLEAN length: {}", bytes.len()), + } +} + fn decode_enumerated(bytes: &[u8]) -> Result { match bytes[..] { [byte0] => Ok(u64::from(byte0)), diff --git a/src/lib.rs b/src/lib.rs index 1ba7bd5..42e03de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,8 @@ //! //! ```no_run //! use dcap_qvl::collateral::get_collateral; -//! use dcap_qvl::verify::verify; +//! use dcap_qvl::verify::{QuoteVerifier, ring}; +//! use dcap_qvl::QuotePolicy; //! use dcap_qvl::PHALA_PCCS_URL; //! //! #[tokio::main] @@ -30,7 +31,9 @@ //! let collateral = get_collateral(&pccs_url, "e).await.expect("failed to get collateral"); //! //! let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); -//! let report = verify("e, &collateral, now).expect("failed to verify quote"); +//! let verifier = QuoteVerifier::new_prod(ring::backend()); +//! let result = verifier.verify("e, &collateral, now).expect("verification failed"); +//! let report = result.validate(&QuotePolicy::strict(now)).expect("policy validation failed"); //! println!("{:?}", report); //! } //! ``` @@ -83,12 +86,25 @@ pub mod oids; mod constants; pub mod intel; -mod qe_identity; -mod tcb_info; +pub mod qe_identity; +pub mod tcb_info; mod utils; +// Common type aliases +pub use constants::{CpuSvn, Fmspc, MrEnclave, MrSigner, Svn}; + +// Re-export commonly used types +pub use qe_identity::{QeIdentity, QeTcb, QeTcbLevel}; +pub use tcb_info::{Tcb, TcbComponents, TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory}; +pub use policy::{PckCertFlag, Policy, QuotePolicy, SupplementalData}; +pub use verify::QuoteVerificationResult; + +#[cfg(feature = "rego")] +pub use policy::{RegoPolicy, RegoPolicySet}; + pub mod quote; pub mod verify; +pub mod policy; #[cfg(feature = "python")] pub mod python; diff --git a/src/oids.rs b/src/oids.rs index cc56959..a7c0051 100644 --- a/src/oids.rs +++ b/src/oids.rs @@ -11,5 +11,13 @@ pub const PCEID: ObjectIdentifier = oid("1.2.840.113741.1.13.1.3"); pub const FMSPC: ObjectIdentifier = oid("1.2.840.113741.1.13.1.4"); pub const SGX_TYPE: ObjectIdentifier = oid("1.2.840.113741.1.13.1.5"); pub const PLATFORM_INSTANCE_ID: ObjectIdentifier = oid("1.2.840.113741.1.13.1.6"); +/// Configuration sequence (Platform CA certs only) +pub const CONFIGURATION: ObjectIdentifier = oid("1.2.840.113741.1.13.1.7"); +/// Whether platform can be extended with additional packages +pub const DYNAMIC_PLATFORM: ObjectIdentifier = oid("1.2.840.113741.1.13.1.7.1"); +/// Whether platform root keys are cached by SGX Registration Backend +pub const CACHED_KEYS: ObjectIdentifier = oid("1.2.840.113741.1.13.1.7.2"); +/// Whether platform has SMT (simultaneous multithreading) enabled +pub const SMT_ENABLED: ObjectIdentifier = oid("1.2.840.113741.1.13.1.7.3"); pub const PCESVN: ObjectIdentifier = oid("1.2.840.113741.1.13.1.2.17"); pub const CPUSVN: ObjectIdentifier = oid("1.2.840.113741.1.13.1.2.18"); diff --git a/src/policy.rs b/src/policy.rs new file mode 100644 index 0000000..3fec9fb --- /dev/null +++ b/src/policy.rs @@ -0,0 +1,1752 @@ +use core::time::Duration; + +use anyhow::{bail, Result}; + +use { + crate::constants::*, + crate::qe_identity::QeTcbLevel, + crate::quote::EnclaveReport, + crate::tcb_info::{TcbLevel, TcbStatus}, + alloc::string::String, + alloc::vec::Vec, +}; + + +/// Policy trait for customizing quote verification behavior. +/// +/// Implement this trait to define custom validation logic for [`SupplementalData`]. +/// The library provides [`QuotePolicy`] as a comprehensive built-in implementation +/// that covers all common checks from Intel's Appraisal framework. +/// +/// For most use cases, [`QuotePolicy`] with its builder methods is sufficient: +/// ```ignore +/// use dcap_qvl::QuotePolicy; +/// use dcap_qvl::TcbStatus; +/// +/// use core::time::Duration; +/// +/// let policy = QuotePolicy::strict(now_unix_secs) +/// .allow_status(TcbStatus::SWHardeningNeeded) +/// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) +/// .accept_advisory("INTEL-SA-00334"); +/// ``` +/// +/// Implement this trait directly only for logic that [`QuotePolicy`] cannot express. +pub trait Policy { + /// Validate supplemental data against this policy. + /// + /// Return `Ok(())` to accept, or `Err(...)` to reject. + fn validate(&self, data: &SupplementalData) -> Result<()>; +} + +/// Status-based verification policy. +/// +/// By default, the policy is strict: only `UpToDate` status is accepted. +/// Comprehensive verification policy with builder pattern. +/// +/// Covers all checks from Intel's Appraisal framework (`qal_script.rego`). +/// Strict by default: only `UpToDate`, no grace period, no advisory tolerance. +/// +/// # Example +/// ```ignore +/// use dcap_qvl::QuotePolicy; +/// use dcap_qvl::TcbStatus; +/// +/// // Strict: only UpToDate, collateral must not be expired +/// let policy = QuotePolicy::strict(now); +/// +/// // With 90-day collateral grace period +/// use core::time::Duration; +/// let policy = QuotePolicy::strict(now) +/// .allow_status(TcbStatus::SWHardeningNeeded) +/// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) +/// .accept_advisory("INTEL-SA-00334"); +/// ``` +#[derive(Clone, Debug)] +pub struct QuotePolicy { + acceptable_statuses: u8, + + // Current time + grace periods (mutually exclusive, default 0 = no tolerance) + now: u64, + collateral_grace_period: u64, + platform_grace_period: u64, + + // TCB evaluation + min_tcb_eval_data_number: Option, + + // Advisory whitelist (all advisories in quote must be in this set) + accepted_advisory_ids: Vec, + + // Platform flags (default false = reject if True) + allow_dynamic_platform: bool, + allow_cached_keys: bool, + allow_smt: bool, + + // SGX type whitelist (None = skip check) + accepted_sgx_types: Option>, +} + +impl QuotePolicy { + const UP_TO_DATE: u8 = 1 << 0; + const SW_HARDENING_NEEDED: u8 = 1 << 1; + const CONFIGURATION_NEEDED: u8 = 1 << 2; + const CONFIGURATION_AND_SW_HARDENING_NEEDED: u8 = 1 << 3; + const OUT_OF_DATE: u8 = 1 << 4; + const OUT_OF_DATE_CONFIGURATION_NEEDED: u8 = 1 << 5; + + fn status_to_flag(status: TcbStatus) -> u8 { + match status { + TcbStatus::UpToDate => Self::UP_TO_DATE, + TcbStatus::SWHardeningNeeded => Self::SW_HARDENING_NEEDED, + TcbStatus::ConfigurationNeeded => Self::CONFIGURATION_NEEDED, + TcbStatus::ConfigurationAndSWHardeningNeeded => { + Self::CONFIGURATION_AND_SW_HARDENING_NEEDED + } + TcbStatus::OutOfDate => Self::OUT_OF_DATE, + TcbStatus::OutOfDateConfigurationNeeded => Self::OUT_OF_DATE_CONFIGURATION_NEEDED, + TcbStatus::Revoked => 0, + } + } + + fn new_with_statuses(now: u64, acceptable_statuses: u8) -> Self { + Self { + acceptable_statuses, + now, + collateral_grace_period: 0, + platform_grace_period: 0, + min_tcb_eval_data_number: None, + accepted_advisory_ids: Vec::new(), + allow_dynamic_platform: false, + allow_cached_keys: false, + allow_smt: false, + accepted_sgx_types: None, + } + } + + /// Create a strict policy: only `UpToDate` status is accepted, + /// no grace period, no advisory tolerance. + pub fn strict(now_secs: u64) -> Self { + Self::new_with_statuses(now_secs, Self::UP_TO_DATE) + } + + /// Allow an additional TCB status. + pub fn allow_status(mut self, status: TcbStatus) -> Self { + self.acceptable_statuses |= Self::status_to_flag(status); + self + } + + /// Set collateral grace period (default: zero). Accepts quotes where + /// `earliest_expiration_date + grace_period >= now`. + /// + /// Must be zero if [`platform_grace_period`](Self::platform_grace_period) is non-zero. + pub fn collateral_grace_period(mut self, duration: Duration) -> Self { + self.collateral_grace_period = duration.as_secs(); + self + } + + /// Set platform grace period (default: zero). When TCB status is + /// OutOfDate or OutOfDateConfigurationNeeded, accepts quotes where + /// `tcb_level_date_tag + grace_period >= now`. Skipped for UpToDate/ConfigNeeded/SWHardening. + /// + /// Must be zero if [`collateral_grace_period`](Self::collateral_grace_period) is non-zero. + pub fn platform_grace_period(mut self, duration: Duration) -> Self { + self.platform_grace_period = duration.as_secs(); + self + } + + /// Set minimum TCB evaluation data number. Rejects quotes with + /// `tcb_eval_data_number` below this threshold. + pub fn min_tcb_eval_data_number(mut self, min: u32) -> Self { + self.min_tcb_eval_data_number = Some(min); + self + } + + /// Accept a specific advisory ID. All advisories in the quote must be in + /// the accepted set or validation fails. By default the set is empty, + /// rejecting any quote with advisories. + pub fn accept_advisory(mut self, id: impl Into) -> Self { + self.accepted_advisory_ids.push(id.into()); + self + } + + /// Set whether dynamic platforms are allowed. If `false` (default), rejects + /// quotes where `dynamic_platform` is `True`. + pub fn allow_dynamic_platform(mut self, allow: bool) -> Self { + self.allow_dynamic_platform = allow; + self + } + + /// Set whether cached keys are allowed. If `false` (default), rejects + /// quotes where `cached_keys` is `True`. + pub fn allow_cached_keys(mut self, allow: bool) -> Self { + self.allow_cached_keys = allow; + self + } + + /// Set whether SMT (simultaneous multithreading / hyperthreading) is allowed. + /// If `false` (default), rejects quotes where `smt_enabled` is `True`. + pub fn allow_smt(mut self, allow: bool) -> Self { + self.allow_smt = allow; + self + } + + /// Set accepted SGX types (0=Standard, 1=Scalable, 2=ScalableWithIntegrity). + /// Rejects quotes with `sgx_type` not in this list. Default: skip check. + pub fn accepted_sgx_types(mut self, types: &[u8]) -> Self { + self.accepted_sgx_types = Some(types.to_vec()); + self + } + + /// Check if a TCB status is acceptable according to this policy. + pub fn is_status_acceptable(&self, status: TcbStatus) -> bool { + let flag = Self::status_to_flag(status); + (self.acceptable_statuses & flag) != 0 + } +} + +impl Policy for QuotePolicy { + fn validate(&self, data: &SupplementalData) -> Result<()> { + // 1. TCB status whitelist + if !self.is_status_acceptable(data.tcb_status) { + bail!( + "TCB status {:?} is not acceptable by policy", + data.tcb_status + ); + } + + // 2. Advisory ID whitelist + for id in &data.advisory_ids { + if !self + .accepted_advisory_ids + .iter() + .any(|a| a.eq_ignore_ascii_case(id)) + { + bail!("Advisory ID {id} is not in the accepted set"); + } + } + + // 3 & 4. Grace periods (mutually exclusive) + if self.collateral_grace_period > 0 && self.platform_grace_period > 0 { + bail!("collateral_grace_period and platform_grace_period are mutually exclusive"); + } + + // 3. Collateral expiration: earliest_expiration_date + grace >= now + // Always checked. With grace=0, this only rejects if collateral is already expired + // (which verify() already enforces, so this catches offline/delayed validation). + if data + .earliest_expiration_date + .saturating_add(self.collateral_grace_period) + < self.now + { + bail!( + "Collateral expired: earliest_expiration_date {} + grace {} < now {}", + data.earliest_expiration_date, + self.collateral_grace_period, + self.now + ); + } + + // 4. Platform TCB freshness: tcb_level_date_tag + grace >= now + // Only checked when TCB status indicates the platform is out-of-date. + // For "good" statuses (UpToDate, ConfigurationNeeded, SWHardeningNeeded), + // tcb_level_date_tag is always in the past and irrelevant. + // Matches Intel Rego: skip for UpToDate/ConfigNeeded/SWHardening. + { + let is_out_of_date = matches!( + data.tcb_status, + TcbStatus::OutOfDate | TcbStatus::OutOfDateConfigurationNeeded + ); + if is_out_of_date + && data + .tcb_level_date_tag + .saturating_add(self.platform_grace_period) + < self.now + { + bail!( + "Platform TCB too old: tcb_level_date_tag {} + grace {} < now {}", + data.tcb_level_date_tag, + self.platform_grace_period, + self.now + ); + } + } + + // 5. Minimum TCB evaluation data number + if let Some(min) = self.min_tcb_eval_data_number { + if data.tcb_eval_data_number < min { + bail!( + "TCB eval data number {} is below minimum {}", + data.tcb_eval_data_number, + min + ); + } + } + + // 6. Dynamic platform flag + if !self.allow_dynamic_platform && data.dynamic_platform == PckCertFlag::True { + bail!("Dynamic platform is not allowed by policy"); + } + + // 7. Cached keys flag + if !self.allow_cached_keys && data.cached_keys == PckCertFlag::True { + bail!("Cached keys are not allowed by policy"); + } + + // 8. SMT flag + if !self.allow_smt && data.smt_enabled == PckCertFlag::True { + bail!("SMT (hyperthreading) is not allowed by policy"); + } + + // 9. SGX type whitelist + if let Some(ref types) = self.accepted_sgx_types { + if !types.contains(&data.sgx_type) { + bail!( + "SGX type {} is not in accepted types {:?}", + data.sgx_type, + types + ); + } + } + + Ok(()) + } +} + +/// PCK certificate flag, matching Intel's `pck_cert_flag_enum_t`. +/// +/// These flags are only present in PCK certificates issued by the **Platform CA**. +/// For Processor CA certificates, the value is [`Undefined`](PckCertFlag::Undefined). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PckCertFlag { + /// The flag is explicitly false (ASN.1 BOOLEAN FALSE). + False = 0, + /// The flag is explicitly true (ASN.1 BOOLEAN TRUE). + True = 1, + /// The flag is not present in the certificate (Processor CA certs). + Undefined = 2, +} + +impl From> for PckCertFlag { + fn from(v: Option) -> Self { + match v { + Some(true) => PckCertFlag::True, + Some(false) => PckCertFlag::False, + None => PckCertFlag::Undefined, + } + } +} + +/// Supplemental data from quote verification, analogous to Intel's `sgx_ql_qv_supplemental_t`. +/// +/// Contains all information needed for policy decisions. This data is publicly +/// accessible for inspection, but the enclave report is only released after +/// passing a [`Policy`] via [`QuoteVerificationResult::validate()`]. +/// +/// Field names and semantics follow Intel's official QVL supplemental data structure. +pub struct SupplementalData { + // ── Merged TCB result ─────────────────────────────────────────────── + /// Merged TCB status (worst of platform TCB + QE TCB). + pub tcb_status: TcbStatus, + + /// Merged advisory IDs (union of platform + QE advisories). + /// Comma-separated list in Intel's struct (`sa_list`). + pub advisory_ids: Vec, + + // ── Collateral time window ────────────────────────────────────────── + /// Earliest issue date across **all** collateral pieces (UTC, unix seconds). + /// Corresponds to Intel's `earliest_issue_date`. + pub earliest_issue_date: u64, + + /// Latest issue date across **all** collateral pieces (UTC, unix seconds). + /// Corresponds to Intel's `latest_issue_date`. + pub latest_issue_date: u64, + + /// Earliest expiration date across **all** collateral pieces (UTC, unix seconds). + /// Corresponds to Intel's `earliest_expiration_date`. + pub earliest_expiration_date: u64, + + /// The SGX platform's TCB level date tag (UTC, unix seconds). + /// The platform is not vulnerable to any Security Advisories with an SGX TCB + /// impact released on or before this date. + /// Corresponds to Intel's `tcb_level_date_tag`. + pub tcb_level_date_tag: u64, + + // ── CRL information ───────────────────────────────────────────────── + /// CRL number from the PCK Certificate Revocation List. + /// Corresponds to Intel's `pck_crl_num`. + pub pck_crl_num: u32, + + /// CRL number from the Root CA Certificate Revocation List. + /// Corresponds to Intel's `root_ca_crl_num`. + pub root_ca_crl_num: u32, + + // ── TCB evaluation ────────────────────────────────────────────────── + /// Lower of the TCBInfo and QEIdentity `tcbEvaluationDataNumber` values. + /// Corresponds to Intel's `tcb_eval_ref_num`. + pub tcb_eval_data_number: u32, + + // ── Root of trust ─────────────────────────────────────────────────── + /// ID of the collateral's root signer: SHA-384 hash of the Root CA's + /// raw public key bytes (BIT STRING content from SubjectPublicKeyInfo). + /// Corresponds to Intel's `root_key_id`. + pub root_key_id: [u8; 48], + + // ── Platform identity from PCK certificate ────────────────────────── + /// Platform Provisioning ID (PPID) from the PCK certificate. + /// Can be used for platform ownership checks. + /// Corresponds to Intel's `pck_ppid`. + pub ppid: Vec, + + /// CPU Security Version Number from the PCK certificate (16 bytes). + /// Corresponds to Intel's `tcb_cpusvn`. + pub cpu_svn: CpuSvn, + + /// PCE ISV Security Version Number from the PCK certificate. + /// Corresponds to Intel's `tcb_pce_isvsvn`. + pub pce_svn: Svn, + + /// PCE ID of the remote platform. + /// Corresponds to Intel's `pce_id`. + pub pce_id: u16, + + /// FMSPC — Firmware Security Version & Package Configuration (6 bytes) + /// from the PCK certificate. Not directly in Intel's supplemental struct + /// but essential for TCB level matching. + pub fmspc: Fmspc, + + // ── TEE and SGX type ──────────────────────────────────────────────── + /// TEE type: `0x00000000` for SGX, `0x00000081` for TDX. + /// Corresponds to Intel's `tee_type`. + pub tee_type: u32, + + /// SGX memory protection type from the PCK certificate: + /// - 0 = Standard + /// - 1 = Scalable + /// - 2 = Scalable with Integrity + /// + /// Corresponds to Intel's `sgx_type`. + pub sgx_type: u8, + + // ── Platform instance (Platform CA certs only) ────────────────────── + /// Platform Instance ID (16 bytes). Only present for Multi-Package + /// platforms (PCK certificates issued by Platform CA). + /// Corresponds to Intel's `platform_instance_id`. + pub platform_instance_id: Option<[u8; 16]>, + + /// Whether the platform can be extended with additional packages + /// via Package Add calls to SGX Registration Backend. + /// Only relevant to PCK certificates issued by Platform CA. + /// Corresponds to Intel's `dynamic_platform`. + pub dynamic_platform: PckCertFlag, + + /// Whether platform root keys are cached by SGX Registration Backend. + /// Only relevant to PCK certificates issued by Platform CA. + /// Corresponds to Intel's `cached_keys`. + pub cached_keys: PckCertFlag, + + /// Whether the platform has SMT (simultaneous multithreading / hyperthreading) + /// enabled. Only relevant to PCK certificates issued by Platform CA. + /// Corresponds to Intel's `smt_enabled`. + pub smt_enabled: PckCertFlag, + + // ── Full TCB level details ────────────────────────────────────────── + /// The matched platform TCB level (includes `tcb_date`, `tcb_status`, `advisory_ids`). + pub platform_tcb_level: TcbLevel, + + /// The matched QE TCB level (includes `tcb_date`, `tcb_status`, `advisory_ids`). + pub qe_tcb_level: QeTcbLevel, + + // ── QE report (for multi-measurement Rego) ──────────────────────────── + /// The QE's enclave report. Needed for QE Identity Rego measurement (TDX) + /// and available for inspection. + pub qe_report: EnclaveReport, + + /// TCB evaluation data number from QE Identity (unmerged). + /// `tcb_eval_data_number` is min(TCBInfo, QEIdentity); this is the QE-specific value. + pub qe_tcb_eval_data_number: u32, +} + +// ============================================================================= +// RegoPolicy — Intel QAL-compatible policy evaluation via regorus +// ============================================================================= + +#[cfg(feature = "rego")] +pub(crate) mod rego_policy { + use super::*; + use serde_json::json; + + /// Convert a unix timestamp (seconds) to an RFC3339 string. + /// Returns an empty string for timestamp 0 (matching Intel's behavior of omitting the field). + fn unix_to_rfc3339(secs: u64) -> String { + chrono::DateTime::from_timestamp(secs as i64, 0) + .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) + .unwrap_or_default() + } + + /// Convert `TcbStatus` to the JSON string array that Intel's Rego expects. + /// + /// This matches Intel's `qv_result_tcb_status_map` in qve.cpp. + fn tcb_status_to_rego_array(status: TcbStatus) -> serde_json::Value { + match status { + TcbStatus::UpToDate => json!(["UpToDate"]), + TcbStatus::SWHardeningNeeded => json!(["UpToDate", "SWHardeningNeeded"]), + TcbStatus::ConfigurationNeeded => json!(["UpToDate", "ConfigurationNeeded"]), + TcbStatus::ConfigurationAndSWHardeningNeeded => { + json!(["UpToDate", "SWHardeningNeeded", "ConfigurationNeeded"]) + } + TcbStatus::OutOfDate => json!(["OutOfDate"]), + TcbStatus::OutOfDateConfigurationNeeded => { + json!(["OutOfDate", "ConfigurationNeeded"]) + } + TcbStatus::Revoked => json!(["Revoked"]), + } + } + + impl SupplementalData { + /// Convert to the JSON `measurement` object that Intel's `qal_script.rego` expects. + /// + /// This matches the JSON construction in Intel's `qve.cpp` (lines 2135-2216). + pub fn to_rego_measurement(&self) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + // tcb_status: array of strings + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(self.tcb_status), + ); + + // Time fields as RFC3339 strings + let earliest_issue = unix_to_rfc3339(self.earliest_issue_date); + if !earliest_issue.is_empty() { + m.insert("earliest_issue_date".into(), json!(earliest_issue)); + } + let latest_issue = unix_to_rfc3339(self.latest_issue_date); + if !latest_issue.is_empty() { + m.insert("latest_issue_date".into(), json!(latest_issue)); + } + let earliest_exp = unix_to_rfc3339(self.earliest_expiration_date); + if !earliest_exp.is_empty() { + m.insert("earliest_expiration_date".into(), json!(earliest_exp)); + } + let tcb_date = unix_to_rfc3339(self.tcb_level_date_tag); + if !tcb_date.is_empty() { + m.insert("tcb_level_date_tag".into(), json!(tcb_date)); + } + + // CRL numbers + m.insert("pck_crl_num".into(), json!(self.pck_crl_num)); + m.insert("root_ca_crl_num".into(), json!(self.root_ca_crl_num)); + + // TCB eval number + m.insert("tcb_eval_num".into(), json!(self.tcb_eval_data_number)); + + // SGX type (note: Rego reads "sgx_type", Intel C++ writes "sgx_types") + m.insert("sgx_type".into(), json!(self.sgx_type)); + + // Platform flags: only emit if not Undefined (matching Intel C++) + if self.dynamic_platform != PckCertFlag::Undefined { + m.insert( + "is_dynamic_platform".into(), + json!(self.dynamic_platform == PckCertFlag::True), + ); + } + if self.cached_keys != PckCertFlag::Undefined { + m.insert( + "cached_keys".into(), + json!(self.cached_keys == PckCertFlag::True), + ); + } + if self.smt_enabled != PckCertFlag::Undefined { + m.insert( + "smt_enabled".into(), + json!(self.smt_enabled == PckCertFlag::True), + ); + } + + // Advisory IDs + if !self.advisory_ids.is_empty() { + m.insert("advisory_ids".into(), json!(self.advisory_ids)); + } + + // FMSPC as hex uppercase string + m.insert("fmspc".into(), json!(hex::encode_upper(self.fmspc))); + + // Root key ID as hex uppercase string + m.insert( + "root_key_id".into(), + json!(hex::encode_upper(self.root_key_id)), + ); + + serde_json::Value::Object(m) + } + + /// Generate platform TCB measurement JSON using **unmerged** platform status. + /// + /// Unlike [`to_rego_measurement()`] which uses the merged `tcb_status`, + /// this uses `platform_tcb_level.tcb_status` and `platform_tcb_level.advisory_ids`, + /// suitable for multi-measurement Rego input alongside QE and tenant measurements. + pub fn to_platform_rego_measurement(&self) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + // tcb_status from platform (unmerged) + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(self.platform_tcb_level.tcb_status), + ); + + // Time fields as RFC3339 strings + let earliest_issue = unix_to_rfc3339(self.earliest_issue_date); + if !earliest_issue.is_empty() { + m.insert("earliest_issue_date".into(), json!(earliest_issue)); + } + let latest_issue = unix_to_rfc3339(self.latest_issue_date); + if !latest_issue.is_empty() { + m.insert("latest_issue_date".into(), json!(latest_issue)); + } + let earliest_exp = unix_to_rfc3339(self.earliest_expiration_date); + if !earliest_exp.is_empty() { + m.insert("earliest_expiration_date".into(), json!(earliest_exp)); + } + let tcb_date = unix_to_rfc3339(self.tcb_level_date_tag); + if !tcb_date.is_empty() { + m.insert("tcb_level_date_tag".into(), json!(tcb_date)); + } + + m.insert("pck_crl_num".into(), json!(self.pck_crl_num)); + m.insert("root_ca_crl_num".into(), json!(self.root_ca_crl_num)); + m.insert("tcb_eval_num".into(), json!(self.tcb_eval_data_number)); + m.insert("sgx_type".into(), json!(self.sgx_type)); + + if self.dynamic_platform != PckCertFlag::Undefined { + m.insert( + "is_dynamic_platform".into(), + json!(self.dynamic_platform == PckCertFlag::True), + ); + } + if self.cached_keys != PckCertFlag::Undefined { + m.insert( + "cached_keys".into(), + json!(self.cached_keys == PckCertFlag::True), + ); + } + if self.smt_enabled != PckCertFlag::Undefined { + m.insert( + "smt_enabled".into(), + json!(self.smt_enabled == PckCertFlag::True), + ); + } + + // Advisory IDs from platform (unmerged) + if !self.platform_tcb_level.advisory_ids.is_empty() { + m.insert( + "advisory_ids".into(), + json!(self.platform_tcb_level.advisory_ids), + ); + } + + m.insert("fmspc".into(), json!(hex::encode_upper(self.fmspc))); + m.insert( + "root_key_id".into(), + json!(hex::encode_upper(self.root_key_id)), + ); + + serde_json::Value::Object(m) + } + + /// Generate QE Identity measurement JSON for Rego appraisal. + /// + /// Uses the unmerged QE TCB level data. Only meaningful for TDX quotes + /// (SGX does not have a QE Identity qvl_result in Intel's format). + pub fn to_qe_rego_measurement(&self) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + // tcb_status from QE (unmerged) + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(self.qe_tcb_level.tcb_status), + ); + + // tcb_level_date_tag from QE TCB level's tcb_date + let qe_tcb_date = chrono::DateTime::parse_from_rfc3339(&self.qe_tcb_level.tcb_date) + .ok() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(0); + let qe_date_str = unix_to_rfc3339(qe_tcb_date); + if !qe_date_str.is_empty() { + m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); + } + + // Time window fields (collateral-wide, same as platform) + let earliest_issue = unix_to_rfc3339(self.earliest_issue_date); + if !earliest_issue.is_empty() { + m.insert("earliest_issue_date".into(), json!(earliest_issue)); + } + let latest_issue = unix_to_rfc3339(self.latest_issue_date); + if !latest_issue.is_empty() { + m.insert("latest_issue_date".into(), json!(latest_issue)); + } + let earliest_exp = unix_to_rfc3339(self.earliest_expiration_date); + if !earliest_exp.is_empty() { + m.insert("earliest_expiration_date".into(), json!(earliest_exp)); + } + + // QE-specific tcb_eval_num + m.insert("tcb_eval_num".into(), json!(self.qe_tcb_eval_data_number)); + + m.insert( + "root_key_id".into(), + json!(hex::encode_upper(self.root_key_id)), + ); + + serde_json::Value::Object(m) + } + } + + // ── Tenant measurement helpers ───────────────────────────────────────── + + use crate::quote::{Report, TDReport10, TDReport15}; + + /// Generate SGX enclave measurement JSON from an `EnclaveReport`. + /// + /// KSS fields are extracted from reserved areas matching Intel's `sgx_report_body_t` layout: + /// - `isv_ext_prod_id`: reserved1\[12..28\] (16B at offset 32) + /// - `config_id`: reserved3\[32..96\] (64B at offset 192) + /// - `config_svn`: reserved4\[0..2\] (u16 LE at offset 260) + /// - `isv_family_id`: reserved4\[44..60\] (16B at offset 304) + pub(crate) fn sgx_enclave_measurement(report: &EnclaveReport) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + m.insert( + "sgx_miscselect".into(), + json!(hex::encode_upper(report.misc_select.to_le_bytes())), + ); + m.insert( + "sgx_attributes".into(), + json!(hex::encode_upper(report.attributes)), + ); + m.insert( + "sgx_mrenclave".into(), + json!(hex::encode_upper(report.mr_enclave)), + ); + m.insert( + "sgx_mrsigner".into(), + json!(hex::encode_upper(report.mr_signer)), + ); + m.insert("sgx_isvprodid".into(), json!(report.isv_prod_id)); + m.insert("sgx_isvsvn".into(), json!(report.isv_svn)); + m.insert( + "sgx_reportdata".into(), + json!(hex::encode_upper(report.report_data)), + ); + + // KSS fields from reserved areas (Intel sgx_report_body_t layout) + if let Some(ext_prod_id) = report.reserved1.get(12..28) { + m.insert( + "sgx_isvextprodid".into(), + json!(hex::encode_upper(ext_prod_id)), + ); + } + if let Some(config_id) = report.reserved3.get(32..96) { + m.insert( + "sgx_configid".into(), + json!(hex::encode_upper(config_id)), + ); + } + if let Some(config_svn_bytes) = report.reserved4.get(0..2).and_then(|s| <[u8; 2]>::try_from(s).ok()) { + let config_svn = u16::from_le_bytes(config_svn_bytes); + m.insert("sgx_configsvn".into(), json!(config_svn)); + } + if let Some(family_id) = report.reserved4.get(44..60) { + m.insert( + "sgx_isvfamilyid".into(), + json!(hex::encode_upper(family_id)), + ); + } + + serde_json::Value::Object(m) + } + + /// Generate TDX TD 1.0 measurement JSON from a `TDReport10`. + fn td10_measurement(report: &TDReport10) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + m.insert( + "tdx_attributes".into(), + json!(hex::encode_upper(report.td_attributes)), + ); + m.insert( + "tdx_xfam".into(), + json!(hex::encode_upper(report.xfam)), + ); + m.insert( + "tdx_mrtd".into(), + json!(hex::encode_upper(report.mr_td)), + ); + m.insert( + "tdx_mrconfigid".into(), + json!(hex::encode_upper(report.mr_config_id)), + ); + m.insert( + "tdx_mrowner".into(), + json!(hex::encode_upper(report.mr_owner)), + ); + m.insert( + "tdx_mrownerconfig".into(), + json!(hex::encode_upper(report.mr_owner_config)), + ); + m.insert( + "tdx_rtmr0".into(), + json!(hex::encode_upper(report.rt_mr0)), + ); + m.insert( + "tdx_rtmr1".into(), + json!(hex::encode_upper(report.rt_mr1)), + ); + m.insert( + "tdx_rtmr2".into(), + json!(hex::encode_upper(report.rt_mr2)), + ); + m.insert( + "tdx_rtmr3".into(), + json!(hex::encode_upper(report.rt_mr3)), + ); + m.insert( + "tdx_reportdata".into(), + json!(hex::encode_upper(report.report_data)), + ); + + serde_json::Value::Object(m) + } + + /// Generate TDX TD 1.5 measurement JSON from a `TDReport15`. + fn td15_measurement(report: &TDReport15) -> serde_json::Value { + let mut m = td10_measurement(&report.base); + if let Some(obj) = m.as_object_mut() { + obj.insert( + "tdx_mrservicetd".into(), + json!(hex::encode_upper(report.mr_service_td)), + ); + } + m + } + + /// Generate tenant measurement JSON from a `Report`. + pub(crate) fn tenant_measurement(report: &Report) -> serde_json::Value { + match report { + Report::SgxEnclave(er) => sgx_enclave_measurement(er), + Report::TD10(td) => td10_measurement(td), + Report::TD15(td) => td15_measurement(td), + } + } + + /// Returns the tenant class_id for the given report type. + pub(crate) fn tenant_class_id(report: &Report) -> &'static str { + match report { + Report::SgxEnclave(_) => "bef7cb8c-31aa-42c1-854c-10db005d5c41", + Report::TD10(_) => "a1e4ee9c-a12e-48ac-bed0-e3f89297f687", + Report::TD15(_) => "45b734fc-aa4e-4c3d-ad28-e43d08880e68", + } + } + + /// Returns the platform class_id for the given report type and tee_type. + pub(crate) fn platform_class_id(report: &Report, tee_type: u32) -> &'static str { + match (report, tee_type) { + (Report::TD10(_), _) => "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f", + (Report::TD15(_), _) => "f708b97f-0fb2-4e6b-8b03-8a5bcd1221d3", + _ => "3123ec35-8d38-4ea5-87a5-d6c48b567570", // SGX + } + } + + // ── RegoPolicySet ────────────────────────────────────────────────────── + + /// A set of Rego policies for multi-measurement appraisal. + /// + /// Accepts multiple policy JSON objects (one per class_id). The Rego engine + /// matches each `qvl_result` entry to its corresponding policy by `class_id`. + /// + /// This provides full Intel QAL compatibility with separate evaluation of + /// platform TCB, QE identity, and tenant measurements. + /// + /// # Example + /// + /// ```no_run + /// use dcap_qvl::RegoPolicySet; + /// + /// let platform_policy = r#"{ + /// "environment": { "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570" }, + /// "reference": { "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 } + /// }"#; + /// let enclave_policy = r#"{ + /// "environment": { "class_id": "bef7cb8c-31aa-42c1-854c-10db005d5c41" }, + /// "reference": { "sgx_mrenclave": "ABCD..." } + /// }"#; + /// let policies = RegoPolicySet::new(&[platform_policy, enclave_policy]).unwrap(); + /// ``` + pub struct RegoPolicySet { + engine: regorus::Engine, + policies: Vec, + } + + impl RegoPolicySet { + /// Create a `RegoPolicySet` from multiple Intel JSON policy strings. + /// + /// Uses the bundled `qal_script.rego`. Each JSON must have `environment.class_id`. + pub fn new(policy_jsons: &[&str]) -> Result { + Self::with_rego(policy_jsons, include_str!("../rego/qal_script.rego")) + } + + /// Create a `RegoPolicySet` with a custom Rego script. + pub fn with_rego(policy_jsons: &[&str], rego_source: &str) -> Result { + let mut engine = regorus::Engine::new(); + engine + .add_policy("qal_script.rego".into(), rego_source.into()) + .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; + + let mut policies = Vec::new(); + for json_str in policy_jsons { + let policy: serde_json::Value = serde_json::from_str(json_str) + .map_err(|e| anyhow::anyhow!("Failed to parse policy JSON: {e}"))?; + // Validate that class_id exists + policy + .get("environment") + .and_then(|e| e.get("class_id")) + .and_then(|c| c.as_str()) + .ok_or_else(|| anyhow::anyhow!("Policy JSON missing environment.class_id"))?; + policies.push(policy); + } + + Ok(Self { engine, policies }) + } + + /// Evaluate the Rego engine with the given qvl_result entries. + pub(crate) fn eval_rego(&self, qvl_result: Vec) -> Result<()> { + let mut engine = self.engine.clone(); + + let input = json!({ + "qvl_result": qvl_result, + "policies": { + "policy_array": &self.policies, + } + }); + + let input_str = serde_json::to_string(&input) + .map_err(|e| anyhow::anyhow!("Failed to serialize Rego input: {e}"))?; + engine + .set_input_json(&input_str) + .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; + + let result = engine + .eval_rule("data.dcap.quote.appraisal.final_ret".into()) + .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; + + let result_json = result + .to_json_str() + .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; + + match result_json.trim() { + "1" => Ok(()), + "0" => { + let detail = engine + .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) + .ok() + .and_then(|v| v.to_json_str().ok()); + if let Some(detail) = detail { + bail!("Rego appraisal failed: {detail}"); + } + bail!("Rego appraisal failed (result = 0)"); + } + "-1" => bail!("No policy matched the report class_id"), + other => bail!("Unexpected Rego appraisal result: {other}"), + } + } + } + + /// Policy implementation that evaluates Intel's `qal_script.rego` via the + /// [regorus](https://github.com/microsoft/regorus) Rego interpreter. + /// + /// This provides bit-exact compatibility with Intel's Quote Appraisal Library (QAL). + /// Users provide a JSON policy in Intel's format (the `reference` object from a + /// Quote Appraisal Policy), and the Rego script evaluates it against the + /// [`SupplementalData`] converted to Intel's measurement JSON format. + /// + /// # Example + /// + /// ```no_run + /// use dcap_qvl::RegoPolicy; + /// + /// let policy_json = r#"{ + /// "environment": { + /// "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570", + /// "description": "Strict SGX platform TCB policy" + /// }, + /// "reference": { + /// "accepted_tcb_status": ["UpToDate"], + /// "collateral_grace_period": 0 + /// } + /// }"#; + /// let policy = RegoPolicy::new(policy_json).expect("invalid policy"); + /// ``` + pub struct RegoPolicy { + engine: regorus::Engine, + policy_json: serde_json::Value, + class_id: String, + } + + impl RegoPolicy { + /// Create a `RegoPolicy` from an Intel JSON policy string. + /// + /// Uses the bundled `qal_script.rego` (from Intel's DCAP source). + /// The JSON must contain `environment.class_id` to identify the policy type. + pub fn new(policy_json: &str) -> Result { + Self::with_rego(policy_json, include_str!("../rego/qal_script.rego")) + } + + /// Create a `RegoPolicy` with a custom Rego script. + /// + /// Use this to provide an updated or modified version of `qal_script.rego`. + pub fn with_rego(policy_json: &str, rego_source: &str) -> Result { + let mut engine = regorus::Engine::new(); + engine + .add_policy("qal_script.rego".into(), rego_source.into()) + .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; + + let policy: serde_json::Value = serde_json::from_str(policy_json) + .map_err(|e| anyhow::anyhow!("Failed to parse policy JSON: {e}"))?; + + let class_id = policy + .get("environment") + .and_then(|e| e.get("class_id")) + .and_then(|c| c.as_str()) + .ok_or_else(|| anyhow::anyhow!("Policy JSON missing environment.class_id"))? + .to_string(); + + Ok(Self { + engine, + policy_json: policy, + class_id, + }) + } + } + + impl Policy for RegoPolicy { + fn validate(&self, data: &SupplementalData) -> Result<()> { + let mut engine = self.engine.clone(); + + // Build the Rego input matching Intel's QAL format + let measurement = data.to_rego_measurement(); + let input = json!({ + "qvl_result": [{ + "environment": { "class_id": &self.class_id }, + "measurement": measurement, + }], + "policies": { + "policy_array": [&self.policy_json] + } + }); + + let input_str = serde_json::to_string(&input) + .map_err(|e| anyhow::anyhow!("Failed to serialize Rego input: {e}"))?; + engine + .set_input_json(&input_str) + .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; + + // Evaluate `final_ret` directly (1=pass, 0=fail, -1=no policy). + // We avoid `final_appraisal_result` which uses `rand.intn` (not available + // in regorus) and `time.now_ns` for nonce/timestamp decorating. + let result = engine + .eval_rule("data.dcap.quote.appraisal.final_ret".into()) + .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; + + let result_json = result + .to_json_str() + .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; + + match result_json.trim() { + "1" => Ok(()), + "0" => { + // Try to get detailed sub-check results for the error message + let detail = engine + .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) + .ok() + .and_then(|v| v.to_json_str().ok()); + if let Some(detail) = detail { + bail!("Rego appraisal failed: {detail}"); + } + bail!("Rego appraisal failed (result = 0)"); + } + "-1" => bail!("No policy matched the report class_id"), + other => bail!("Unexpected Rego appraisal result: {other}"), + } + } + } +} + +#[cfg(feature = "rego")] +pub use rego_policy::RegoPolicy; +#[cfg(feature = "rego")] +pub use rego_policy::RegoPolicySet; + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use crate::tcb_info::TcbStatus::*; + + // ═══════════════════════════════════════════════════════════════════ + // QuotePolicy tests + // ═══════════════════════════════════════════════════════════════════ + + fn make_test_supplemental(tcb_status: TcbStatus) -> SupplementalData { + use crate::qe_identity::{QeTcb, QeTcbLevel}; + use crate::tcb_info::{Tcb, TcbComponents, TcbLevel}; + + SupplementalData { + tcb_status, + advisory_ids: vec![], + earliest_issue_date: 1_700_000_000, + latest_issue_date: 1_700_100_000, + earliest_expiration_date: 1_703_000_000, // ~2023-12-19 + tcb_level_date_tag: 1_690_000_000, // ~2023-07-22 + pck_crl_num: 1, + root_ca_crl_num: 1, + tcb_eval_data_number: 17, + root_key_id: [0u8; 48], + ppid: vec![0u8; 16], + cpu_svn: [0u8; 16], + pce_svn: 13, + pce_id: 0, + fmspc: [0u8; 6], + tee_type: 0, + sgx_type: 0, + platform_instance_id: None, + dynamic_platform: PckCertFlag::Undefined, + cached_keys: PckCertFlag::Undefined, + smt_enabled: PckCertFlag::Undefined, + platform_tcb_level: TcbLevel { + tcb: Tcb { + sgx_components: vec![TcbComponents { svn: 0 }; 16], + tdx_components: vec![], + pce_svn: 13, + }, + tcb_date: "2023-07-22T00:00:00Z".to_string(), + tcb_status: tcb_status, + advisory_ids: vec![], + }, + qe_tcb_level: QeTcbLevel { + tcb: QeTcb { isvsvn: 8 }, + tcb_date: "2024-03-13T00:00:00Z".to_string(), + tcb_status: UpToDate, + advisory_ids: vec![], + }, + qe_report: crate::quote::EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0, + reserved1: [0u8; 28], + attributes: [0u8; 16], + mr_enclave: [0u8; 32], + reserved2: [0u8; 32], + mr_signer: [0u8; 32], + reserved3: [0u8; 96], + isv_prod_id: 1, + isv_svn: 8, + reserved4: [0u8; 60], + report_data: [0u8; 64], + }, + qe_tcb_eval_data_number: 17, + } + } + + // -- TCB status checks -- + + #[test] + fn policy_strict_accepts_up_to_date() { + let data = make_test_supplemental(UpToDate); + let policy = QuotePolicy::strict(1_702_000_000); // within collateral window + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_strict_rejects_sw_hardening() { + let data = make_test_supplemental(SWHardeningNeeded); + let policy = QuotePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("TCB status"), "{err}"); + } + + #[test] + fn policy_out_of_date_with_fresh_tcb_date_accepts() { + let mut data = make_test_supplemental(OutOfDate); + data.tcb_level_date_tag = 1_702_000_000; + let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDate); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_allow_status_builder() { + let data = make_test_supplemental(SWHardeningNeeded); + let policy = QuotePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded); + assert!(policy.validate(&data).is_ok()); + } + + // -- Advisory ID whitelist -- + + #[test] + fn policy_rejects_unknown_advisory() { + let mut data = make_test_supplemental(UpToDate); + data.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + let policy = QuotePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); + } + + #[test] + fn policy_accepts_whitelisted_advisory() { + let mut data = make_test_supplemental(UpToDate); + data.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + let policy = QuotePolicy::strict(1_702_000_000).accept_advisory("INTEL-SA-00615"); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_advisory_case_insensitive() { + let mut data = make_test_supplemental(UpToDate); + data.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + let policy = QuotePolicy::strict(1_702_000_000).accept_advisory("intel-sa-00615"); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_empty_advisories_passes() { + let data = make_test_supplemental(UpToDate); + assert!(data.advisory_ids.is_empty()); + let policy = QuotePolicy::strict(1_702_000_000); + // Empty advisory list in quote → nothing to check against whitelist → passes + assert!(policy.validate(&data).is_ok()); + } + + // -- Collateral grace period -- + + #[test] + fn policy_collateral_expired_no_grace_rejects() { + let data = make_test_supplemental(UpToDate); + // earliest_expiration_date = 1_703_000_000, now = 1_704_000_000 → expired + let policy = QuotePolicy::strict(1_704_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Collateral expired"), "{err}"); + } + + #[test] + fn policy_collateral_expired_with_grace_accepts() { + let data = make_test_supplemental(UpToDate); + // earliest_expiration_date = 1_703_000_000, now = 1_704_000_000 + // grace = 2_000_000 → 1_703M + 2M = 1_705M >= 1_704M → ok + let policy = QuotePolicy::strict(1_704_000_000) + .collateral_grace_period(Duration::from_secs(2_000_000)); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_collateral_expired_grace_too_short_rejects() { + let data = make_test_supplemental(UpToDate); + // earliest_expiration_date = 1_703_000_000, now = 1_704_000_000 + // grace = 500_000 → 1_703M + 0.5M = 1_703_500_000 < 1_704M → reject + let policy = QuotePolicy::strict(1_704_000_000) + .collateral_grace_period(Duration::from_secs(500_000)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Collateral expired"), "{err}"); + } + + #[test] + fn policy_collateral_not_expired_zero_grace_passes() { + let data = make_test_supplemental(UpToDate); + // earliest_expiration_date = 1_703_000_000, now = 1_702_000_000 → not expired + let policy = QuotePolicy::strict(1_702_000_000); + assert!(policy.validate(&data).is_ok()); + } + + // -- Platform grace period -- + + #[test] + fn policy_platform_grace_skipped_for_up_to_date() { + let data = make_test_supplemental(UpToDate); + // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000 + // grace = 0, but check is skipped because status is UpToDate + let policy = QuotePolicy::strict(1_702_000_000); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_skipped_for_sw_hardening() { + let data = make_test_supplemental(SWHardeningNeeded); + let policy = QuotePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded); + // SWHardeningNeeded is a "good" status → platform grace skipped + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_skipped_for_config_needed() { + let data = make_test_supplemental(ConfigurationNeeded); + let policy = QuotePolicy::strict(1_702_000_000).allow_status(ConfigurationNeeded); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_checked_for_out_of_date_rejects() { + let data = make_test_supplemental(OutOfDate); + // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000, grace = 0 + // 1_690M + 0 < 1_702M → reject + let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDate); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Platform TCB too old"), "{err}"); + } + + #[test] + fn policy_platform_grace_checked_for_out_of_date_accepts_with_grace() { + let data = make_test_supplemental(OutOfDate); + // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000 + // grace = 13_000_000 → 1_690M + 13M = 1_703M >= 1_702M → ok + let policy = QuotePolicy::strict(1_702_000_000) + .allow_status(OutOfDate) + .platform_grace_period(Duration::from_secs(13_000_000)); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_too_short_rejects() { + let data = make_test_supplemental(OutOfDate); + // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000 + // grace = 11_000_000 → 1_690M + 11M = 1_701M < 1_702M → reject + let policy = QuotePolicy::strict(1_702_000_000) + .allow_status(OutOfDate) + .platform_grace_period(Duration::from_secs(11_000_000)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Platform TCB too old"), "{err}"); + } + + #[test] + fn policy_platform_grace_checked_for_out_of_date_config_needed() { + let data = make_test_supplemental(OutOfDateConfigurationNeeded); + // grace = 0 → reject + let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDateConfigurationNeeded); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Platform TCB too old"), "{err}"); + } + + #[test] + fn policy_grace_periods_mutually_exclusive() { + let data = make_test_supplemental(UpToDate); + let policy = QuotePolicy::strict(1_702_000_000) + .collateral_grace_period(Duration::from_secs(100)) + .platform_grace_period(Duration::from_secs(100)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("mutually exclusive"), "{err}"); + } + + // -- min_tcb_eval_data_number -- + + #[test] + fn policy_min_eval_num_rejects_below() { + let data = make_test_supplemental(UpToDate); + assert_eq!(data.tcb_eval_data_number, 17); + let policy = QuotePolicy::strict(1_702_000_000).min_tcb_eval_data_number(20); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("below minimum"), "{err}"); + } + + #[test] + fn policy_min_eval_num_accepts_equal() { + let data = make_test_supplemental(UpToDate); + let policy = QuotePolicy::strict(1_702_000_000).min_tcb_eval_data_number(17); + assert!(policy.validate(&data).is_ok()); + } + + // -- Platform flags -- + + #[test] + fn policy_rejects_dynamic_platform_true() { + let mut data = make_test_supplemental(UpToDate); + data.dynamic_platform = PckCertFlag::True; + let policy = QuotePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Dynamic platform"), "{err}"); + } + + #[test] + fn policy_allows_dynamic_platform_when_configured() { + let mut data = make_test_supplemental(UpToDate); + data.dynamic_platform = PckCertFlag::True; + let policy = QuotePolicy::strict(1_702_000_000).allow_dynamic_platform(true); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_undefined_platform_flags_pass() { + // Processor CA certs have Undefined flags — should never be rejected + let data = make_test_supplemental(UpToDate); + assert_eq!(data.dynamic_platform, PckCertFlag::Undefined); + assert_eq!(data.cached_keys, PckCertFlag::Undefined); + assert_eq!(data.smt_enabled, PckCertFlag::Undefined); + let policy = QuotePolicy::strict(1_702_000_000); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_rejects_smt_true() { + let mut data = make_test_supplemental(UpToDate); + data.smt_enabled = PckCertFlag::True; + let policy = QuotePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("SMT"), "{err}"); + } + + #[test] + fn policy_rejects_cached_keys_true() { + let mut data = make_test_supplemental(UpToDate); + data.cached_keys = PckCertFlag::True; + let policy = QuotePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Cached keys"), "{err}"); + } + + // -- SGX type whitelist -- + + #[test] + fn policy_sgx_type_not_configured_passes() { + let data = make_test_supplemental(UpToDate); + let policy = QuotePolicy::strict(1_702_000_000); + // No accepted_sgx_types set → skip check + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_sgx_type_whitelist_rejects() { + let mut data = make_test_supplemental(UpToDate); + data.sgx_type = 1; // Scalable + let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0]); // Only Standard + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("SGX type"), "{err}"); + } + + #[test] + fn policy_sgx_type_whitelist_accepts() { + let mut data = make_test_supplemental(UpToDate); + data.sgx_type = 1; // Scalable + let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0, 1, 2]); + assert!(policy.validate(&data).is_ok()); + } + + // -- RegoPolicy tests (require "rego" feature) -- + + #[cfg(feature = "rego")] + mod rego_tests { + use super::*; + + const SGX_PLATFORM_CLASS_ID: &str = "3123ec35-8d38-4ea5-87a5-d6c48b567570"; + + /// Create test supplemental data with future expiration dates. + /// + /// The Rego engine uses `time.now_ns()` (real wall clock) for expiration + /// checks, so test data must have dates in the future to pass. + fn make_rego_supplemental(status: TcbStatus) -> SupplementalData { + let mut data = make_test_supplemental(status); + // Set dates far in the future so expiration_date_check passes + data.earliest_expiration_date = 2_000_000_000; // 2033-05-18 + data.earliest_issue_date = 1_900_000_000; // 2030-03-17 + data.latest_issue_date = 1_900_100_000; + data + } + + fn policy_json(reference: &str) -> String { + format!( + r#"{{ + "environment": {{ + "class_id": "{SGX_PLATFORM_CLASS_ID}", + "description": "Test policy" + }}, + "reference": {reference} + }}"# + ) + } + + #[test] + fn rego_strict_accepts_up_to_date() { + let data = make_rego_supplemental(UpToDate); + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.validate(&data); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_strict_rejects_out_of_date() { + let data = make_rego_supplemental(OutOfDate); + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected appraisal failure, got: {err}" + ); + } + + #[test] + fn rego_permissive_accepts_out_of_date() { + let data = make_rego_supplemental(OutOfDate); + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate", "OutOfDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.validate(&data); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_rejects_advisory() { + let mut data = make_rego_supplemental(UpToDate); + data.advisory_ids = vec!["INTEL-SA-00334".into()]; + let json = policy_json( + r#"{ + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0, + "rejected_advisory_ids": ["INTEL-SA-00334"] + }"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected advisory rejection, got: {err}" + ); + } + + #[test] + fn rego_platform_grace_period_accepts() { + let mut data = make_rego_supplemental(OutOfDate); + // tcb_level_date_tag in the past, but huge grace period covers it + data.tcb_level_date_tag = 1_690_000_000; // 2023-07-22 + let json = policy_json( + r#"{ + "accepted_tcb_status": ["UpToDate", "OutOfDate"], + "collateral_grace_period": 0, + "platform_grace_period": 999999999 + }"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.validate(&data); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_expiration_check_rejects_expired_collateral() { + let mut data = make_rego_supplemental(UpToDate); + // Set expiration in the past + data.earliest_expiration_date = 1_703_000_000; // 2023-12-19 + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected expiration failure, got: {err}" + ); + } + + #[test] + fn rego_no_collateral_grace_skips_expiration_check() { + let mut data = make_rego_supplemental(UpToDate); + // Expired collateral, but no collateral_grace_period in policy → skip check + data.earliest_expiration_date = 1_703_000_000; // 2023-12-19 + let json = policy_json(r#"{"accepted_tcb_status": ["UpToDate"]}"#); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.validate(&data); + assert!( + result.is_ok(), + "expected Ok (no expiration check), got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_missing_class_id_errors() { + let json = r#"{"reference": {"accepted_tcb_status": ["UpToDate"]}}"#; + assert!(RegoPolicy::new(json).is_err()); + } + + #[test] + fn rego_to_measurement_tcb_status_mapping() { + let data = make_test_supplemental(ConfigurationAndSWHardeningNeeded); + let m = data.to_rego_measurement(); + let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); + assert_eq!(statuses.len(), 3); + assert_eq!(statuses[0], "UpToDate"); + assert_eq!(statuses[1], "SWHardeningNeeded"); + assert_eq!(statuses[2], "ConfigurationNeeded"); + } + + #[test] + fn rego_to_measurement_omits_undefined_flags() { + let data = make_test_supplemental(UpToDate); + assert_eq!(data.dynamic_platform, PckCertFlag::Undefined); + let m = data.to_rego_measurement(); + assert!(m.get("is_dynamic_platform").is_none()); + assert!(m.get("cached_keys").is_none()); + assert!(m.get("smt_enabled").is_none()); + } + + #[test] + fn rego_to_measurement_includes_true_flags() { + let mut data = make_test_supplemental(UpToDate); + data.dynamic_platform = PckCertFlag::True; + data.cached_keys = PckCertFlag::False; + data.smt_enabled = PckCertFlag::True; + let m = data.to_rego_measurement(); + assert_eq!(m.get("is_dynamic_platform").unwrap(), true); + assert_eq!(m.get("cached_keys").unwrap(), false); + assert_eq!(m.get("smt_enabled").unwrap(), true); + } + + // ═══════════════════════════════════════════════════════════════════ + // Multi-measurement tests + // ═══════════════════════════════════════════════════════════════════ + + #[test] + fn rego_platform_measurement_uses_unmerged_status() { + let mut data = make_test_supplemental(UpToDate); + // Merged status is UpToDate, but platform-specific is OutOfDate + data.platform_tcb_level.tcb_status = OutOfDate; + data.platform_tcb_level.advisory_ids = vec!["INTEL-SA-00001".into()]; + let m = data.to_platform_rego_measurement(); + let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); + // OutOfDate maps to ["UpToDate", "OutOfDate"] + assert!(statuses.contains(&serde_json::json!("OutOfDate"))); + // Advisory from platform, not merged + let advisories = m.get("advisory_ids").unwrap().as_array().unwrap(); + assert_eq!(advisories, &[serde_json::json!("INTEL-SA-00001")]); + } + + #[test] + fn rego_qe_measurement_fields() { + let data = make_rego_supplemental(UpToDate); + let m = data.to_qe_rego_measurement(); + // QE measurement should have tcb_status from qe_tcb_level + assert!(m.get("tcb_status").is_some()); + // Should have tcb_eval_num from qe_tcb_eval_data_number + assert_eq!(m.get("tcb_eval_num").unwrap(), 17); + // Should have root_key_id + assert!(m.get("root_key_id").is_some()); + // Should have time fields + assert!(m.get("earliest_issue_date").is_some()); + assert!(m.get("latest_issue_date").is_some()); + assert!(m.get("earliest_expiration_date").is_some()); + assert!(m.get("tcb_level_date_tag").is_some()); + } + + #[test] + fn rego_sgx_enclave_measurement_fields() { + use crate::quote::EnclaveReport; + + let mut report = EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0x12345678, + reserved1: [0u8; 28], + attributes: [0xAA; 16], + mr_enclave: [0xBB; 32], + reserved2: [0u8; 32], + mr_signer: [0xCC; 32], + reserved3: [0u8; 96], + isv_prod_id: 42, + isv_svn: 7, + reserved4: [0u8; 60], + report_data: [0xDD; 64], + }; + // Set KSS fields in reserved areas + // isv_ext_prod_id at reserved1[12..28] + report.reserved1[12..28].copy_from_slice(&[0x11; 16]); + // config_id at reserved3[32..96] + report.reserved3[32..96].copy_from_slice(&[0x22; 64]); + // config_svn at reserved4[0..2] + report.reserved4[0..2].copy_from_slice(&42u16.to_le_bytes()); + // isv_family_id at reserved4[44..60] + report.reserved4[44..60].copy_from_slice(&[0x33; 16]); + + let m = rego_policy::sgx_enclave_measurement(&report); + assert!(m.get("sgx_mrenclave").is_some()); + assert!(m.get("sgx_mrsigner").is_some()); + assert_eq!(m.get("sgx_isvprodid").unwrap(), 42); + assert_eq!(m.get("sgx_isvsvn").unwrap(), 7); + assert!(m.get("sgx_reportdata").is_some()); + assert!(m.get("sgx_configid").is_some()); + assert_eq!(m.get("sgx_configsvn").unwrap(), 42); + assert!(m.get("sgx_isvextprodid").is_some()); + assert!(m.get("sgx_isvfamilyid").is_some()); + } + + #[test] + fn rego_policy_set_sgx_platform_accepts() { + let data = make_rego_supplemental(UpToDate); + let platform_json = format!( + r#"{{ + "environment": {{ "class_id": "{SGX_PLATFORM_CLASS_ID}" }}, + "reference": {{ "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 }} + }}"# + ); + let policies = RegoPolicySet::new(&[&platform_json]).unwrap(); + let qvl_result = vec![serde_json::json!({ + "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, + "measurement": data.to_platform_rego_measurement(), + })]; + assert!( + policies.eval_rego(qvl_result).is_ok(), + "expected Ok, got: {:?}", + { + let qvl_result2 = vec![serde_json::json!({ + "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, + "measurement": data.to_platform_rego_measurement(), + })]; + policies.eval_rego(qvl_result2).unwrap_err() + } + ); + } + + #[test] + fn rego_policy_set_class_id_mismatch_fails() { + let data = make_rego_supplemental(UpToDate); + // Policy expects TDX platform class_id, but measurement is SGX + let tdx_class_id = "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f"; + let policy_json = format!( + r#"{{ + "environment": {{ "class_id": "{tdx_class_id}" }}, + "reference": {{ "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 }} + }}"# + ); + let policies = RegoPolicySet::new(&[&policy_json]).unwrap(); + let qvl_result = vec![serde_json::json!({ + "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, + "measurement": data.to_platform_rego_measurement(), + })]; + // Mismatched class_ids → no bundle matched → empty appraisal → fail + let err = policies.eval_rego(qvl_result).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected appraisal failure on class_id mismatch, got: {err}" + ); + } + } +} diff --git a/src/python.rs b/src/python.rs index 2f59478..d74a163 100644 --- a/src/python.rs +++ b/src/python.rs @@ -8,7 +8,7 @@ use crate::{ collateral::get_collateral_for_fmspc, intel, quote::{EnclaveReport, Header, Quote, Report, TDReport10, TDReport15}, - verify::{verify, VerifiedReport}, + verify::{QuoteVerifier, VerifiedReport}, QuoteCollateralV3, }; @@ -544,10 +544,10 @@ fn py_verify( now_secs: u64, ) -> PyResult { let quote_bytes = raw_quote.as_bytes(); - - match verify(quote_bytes, &collateral.inner, now_secs) { - Ok(verified_report) => Ok(PyVerifiedReport { - inner: verified_report, + let verifier = QuoteVerifier::new_prod(crate::verify::ring::backend()); + match verifier.verify(quote_bytes, &collateral.inner, now_secs) { + Ok(supplemental) => Ok(PyVerifiedReport { + inner: supplemental.into_report(), }), Err(e) => Err(PyValueError::new_err(format!("Verification failed: {e:?}"))), } @@ -563,13 +563,10 @@ fn py_verify_with_root_ca( let quote_bytes = raw_quote.as_bytes(); let root_ca = root_ca_der.as_bytes(); - let verifier = crate::verify::QuoteVerifier::new( - root_ca.to_vec(), - crate::verify::default_crypto::backend(), - ); + let verifier = QuoteVerifier::new(root_ca.to_vec(), crate::verify::ring::backend()); match verifier.verify(quote_bytes, &collateral.inner, now_secs) { - Ok(verified_report) => Ok(PyVerifiedReport { - inner: verified_report, + Ok(supplemental) => Ok(PyVerifiedReport { + inner: supplemental.into_report(), }), Err(e) => Err(PyValueError::new_err(format!("Verification failed: {e:?}"))), } diff --git a/src/tcb_info.rs b/src/tcb_info.rs index a5905cf..57f1f40 100644 --- a/src/tcb_info.rs +++ b/src/tcb_info.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use alloc::string::String; use alloc::vec::Vec; use derive_more::Display; @@ -57,22 +59,32 @@ pub struct TcbComponents { pub svn: u8, } -#[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, Display, -)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, Display)] #[display("{_variant}")] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] pub enum TcbStatus { UpToDate, - OutOfDateConfigurationNeeded, - OutOfDate, - ConfigurationAndSWHardeningNeeded, - ConfigurationNeeded, SWHardeningNeeded, + ConfigurationNeeded, + ConfigurationAndSWHardeningNeeded, + OutOfDate, + OutOfDateConfigurationNeeded, Revoked, } +impl Ord for TcbStatus { + fn cmp(&self, other: &Self) -> Ordering { + self.severity().cmp(&other.severity()) + } +} + +impl PartialOrd for TcbStatus { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl TcbStatus { fn severity(&self) -> u8 { match self { @@ -86,19 +98,19 @@ impl TcbStatus { } } - /// Returns true if the TCB status is acceptable to let the caller decide - /// whether to accept the quote or not. + /// Converge platform TCB status with QE TCB status. /// - /// Currently, `Revoked` status is considered invalid and will cause the verification to fail. - pub(crate) fn is_valid(&self) -> bool { - match self { - Self::UpToDate => true, - Self::SWHardeningNeeded => true, - Self::ConfigurationNeeded => true, - Self::ConfigurationAndSWHardeningNeeded => true, - Self::OutOfDate => true, - Self::OutOfDateConfigurationNeeded => true, - Self::Revoked => false, + /// Matches Intel QVL's `convergeTcbStatusWithQeTcbStatus()` from + /// `TcbLevelCheck.cpp`. The QE status can only be UpToDate, OutOfDate, + /// or Revoked (from QE Identity verification). + fn converge_with_qe(self, qe: TcbStatus) -> TcbStatus { + use TcbStatus::*; + match (qe, self) { + // QE is OutOfDate: escalate platform status + (OutOfDate, ConfigurationNeeded | ConfigurationAndSWHardeningNeeded) => { + OutOfDateConfigurationNeeded + } + _ => qe.max(self), } } } @@ -124,13 +136,11 @@ impl TcbStatusWithAdvisory { } } - /// Merge two TCB statuses, taking the worse status and combining advisory IDs + /// Merge platform TCB status with QE TCB status, following Intel QVL's + /// `convergeTcbStatusWithQeTcbStatus()` logic. `self` is the platform + /// status, `other` is the QE status. pub fn merge(self, other: &TcbStatusWithAdvisory) -> Self { - let final_status = if other.status.severity() > self.status.severity() { - other.status - } else { - self.status - }; + let final_status = self.status.converge_with_qe(other.status); let mut advisory_ids = self.advisory_ids; for id in &other.advisory_ids { @@ -151,30 +161,70 @@ mod tests { use super::*; use TcbStatus::*; + fn merge(platform: TcbStatus, qe: TcbStatus) -> TcbStatus { + TcbStatusWithAdvisory::new(platform, vec![]) + .merge(&TcbStatusWithAdvisory::new(qe, vec![])) + .status + } + + // ── QE UpToDate: pass through platform status ────────────────────── #[test] - fn test_tcb_status_merge_both_up_to_date() { - let a = TcbStatusWithAdvisory::new(UpToDate, vec![]); - let b = TcbStatusWithAdvisory::new(UpToDate, vec![]); - let result = a.merge(&b); - assert_eq!(result.status, UpToDate); - assert!(result.advisory_ids.is_empty()); + fn qe_uptodate_passes_through() { + assert_eq!(merge(UpToDate, UpToDate), UpToDate); + assert_eq!(merge(SWHardeningNeeded, UpToDate), SWHardeningNeeded); + assert_eq!(merge(ConfigurationNeeded, UpToDate), ConfigurationNeeded); + assert_eq!( + merge(ConfigurationAndSWHardeningNeeded, UpToDate), + ConfigurationAndSWHardeningNeeded + ); + assert_eq!(merge(OutOfDate, UpToDate), OutOfDate); + assert_eq!( + merge(OutOfDateConfigurationNeeded, UpToDate), + OutOfDateConfigurationNeeded + ); + assert_eq!(merge(Revoked, UpToDate), Revoked); } + // ── QE OutOfDate: escalate platform status ───────────────────────── #[test] - fn test_tcb_status_merge_takes_worse() { - let a = TcbStatusWithAdvisory::new(UpToDate, vec![]); - let b = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]); - let result = a.merge(&b); - assert_eq!(result.status, OutOfDate); - assert_eq!(result.advisory_ids, vec!["INTEL-SA-00001"]); + fn qe_outofdate_escalates() { + assert_eq!(merge(UpToDate, OutOfDate), OutOfDate); + assert_eq!(merge(SWHardeningNeeded, OutOfDate), OutOfDate); + assert_eq!( + merge(ConfigurationNeeded, OutOfDate), + OutOfDateConfigurationNeeded + ); + assert_eq!( + merge(ConfigurationAndSWHardeningNeeded, OutOfDate), + OutOfDateConfigurationNeeded + ); + } + + #[test] + fn qe_outofdate_already_worse_keeps() { + assert_eq!(merge(OutOfDate, OutOfDate), OutOfDate); + assert_eq!( + merge(OutOfDateConfigurationNeeded, OutOfDate), + OutOfDateConfigurationNeeded + ); + assert_eq!(merge(Revoked, OutOfDate), Revoked); + } + + // ── QE Revoked: always revoked ───────────────────────────────────── + #[test] + fn qe_revoked_always_revoked() { + assert_eq!(merge(UpToDate, Revoked), Revoked); + assert_eq!(merge(SWHardeningNeeded, Revoked), Revoked); + assert_eq!(merge(OutOfDate, Revoked), Revoked); + assert_eq!(merge(ConfigurationNeeded, Revoked), Revoked); } + // ── Advisory ID merging ──────────────────────────────────────────── #[test] - fn test_tcb_status_merge_combines_advisories() { + fn merge_combines_advisories() { let a = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]); - let b = TcbStatusWithAdvisory::new(SWHardeningNeeded, vec!["INTEL-SA-00002".into()]); + let b = TcbStatusWithAdvisory::new(UpToDate, vec!["INTEL-SA-00002".into()]); let result = a.merge(&b); - assert_eq!(result.status, OutOfDate); assert_eq!( result.advisory_ids, vec!["INTEL-SA-00001", "INTEL-SA-00002"] @@ -182,7 +232,7 @@ mod tests { } #[test] - fn test_tcb_status_merge_deduplicates_advisories() { + fn merge_deduplicates_advisories() { let a = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]); let b = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]); let result = a.merge(&b); diff --git a/src/utils.rs b/src/utils.rs index 9abedef..9c07931 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -139,6 +139,33 @@ pub fn encode_as_der(data: &[u8]) -> Result> { Ok(writer.finish().context("Failed to finish writer")?.to_vec()) } +/// Extract the CRL Number (OID 2.5.29.20) from a DER-encoded CRL. +/// +/// Returns `Ok(0)` if the CRL Number extension is not present. +pub fn extract_crl_number(crl_der: &[u8]) -> Result { + use der::Decode as _; + let crl = x509_cert::crl::CertificateList::from_der(crl_der).context("Failed to parse CRL")?; + let Some(extensions) = &crl.tbs_cert_list.crl_extensions else { + return Ok(0); + }; + for ext in extensions.iter() { + // OID 2.5.29.20 = id-ce-cRLNumber + if ext.extn_id.to_string() == "2.5.29.20" { + // CRL Number is encoded as an ASN.1 INTEGER + let crl_num = + der::asn1::UintRef::from_der(ext.extn_value.as_bytes()).context("CRL number")?; + let bytes = crl_num.as_bytes(); + // Convert big-endian bytes to u32 (CRL numbers are typically small) + let mut val: u32 = 0; + for &b in bytes { + val = val.checked_shl(8).context("CRL number too large for u32")? | u32::from(b); + } + return Ok(val); + } + } + Ok(0) +} + /// Parse CRL DER bytes into CertRevocationList objects. /// Call this once and pass the results to `verify_certificate_chain`. pub fn parse_crls(crl_der: &[&[u8]]) -> Result>> { diff --git a/src/verify.rs b/src/verify.rs index ec58b71..55a1c0b 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -7,13 +7,19 @@ use scale::Decode; use { crate::constants::*, crate::intel, - crate::qe_identity::QeIdentity, - crate::tcb_info::{TcbInfo, TcbStatusWithAdvisory}, + crate::policy::{PckCertFlag, Policy, SupplementalData}, + crate::qe_identity::{QeIdentity, QeTcbLevel}, + crate::tcb_info::{TcbInfo, TcbLevel, TcbStatusWithAdvisory}, alloc::string::String, alloc::vec::Vec, }; pub use crate::quote::{AuthData, EnclaveReport, Quote}; + +#[cfg(feature = "ring")] +pub(crate) use self::ring as default_crypto; +#[cfg(all(not(feature = "ring"), feature = "rustcrypto"))] +pub(crate) use self::rustcrypto as default_crypto; use crate::{ quote::{Report, TDAttributes}, utils::{encode_as_der, extract_certs, parse_crls, verify_certificate_chain}, @@ -22,14 +28,10 @@ use crate::{ quote::{TDReport10, TDReport15}, QuoteCollateralV3, }; + use rustls_pki_types::CertificateDer; use serde::{Deserialize, Serialize}; -#[cfg(feature = "ring")] -pub(crate) use self::ring as default_crypto; -#[cfg(all(not(feature = "ring"), feature = "rustcrypto"))] -pub(crate) use self::rustcrypto as default_crypto; - /// Crypto backend configuration for quote verification. /// /// Holds the signature verification algorithm and SHA-256 implementation @@ -40,6 +42,8 @@ pub struct CryptoBackend { pub sig_algo: &'static dyn rustls_pki_types::SignatureVerificationAlgorithm, /// SHA-256 hash function pub sha256: fn(&[u8]) -> [u8; 32], + /// SHA-384 hash function (used for root_key_id computation) + pub sha384: fn(&[u8]) -> [u8; 48], } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -82,6 +86,98 @@ use borsh::BorshSchema; #[cfg(feature = "borsh")] use borsh::{BorshDeserialize, BorshSerialize}; +/// Result of cryptographic quote verification, before policy validation. +/// +/// The enclave report is private — it can only be obtained by passing a [`Policy`] +/// via [`validate()`](Self::validate). +/// The [`supplemental`](Self::supplemental) field is public for inspection. +/// +/// ```ignore +/// let result = verifier.verify("e, &collateral, now)?; +/// // Inspect supplemental data before committing +/// println!("TCB status: {:?}", result.supplemental.tcb_status); +/// // Apply policy to get the report +/// let report = result.validate(&QuotePolicy::strict(now))?; +/// ``` +pub struct QuoteVerificationResult { + report: Report, + /// Supplemental data for policy decisions (publicly accessible). + pub supplemental: SupplementalData, +} + +impl QuoteVerificationResult { + /// Validate against a policy, consuming self into [`VerifiedReport`] on success. + pub fn validate(self, policy: &P) -> Result { + policy.validate(&self.supplemental)?; + Ok(self.into_verified_report()) + } + + /// Convert directly into [`VerifiedReport`] without applying any policy. + /// + /// Use this only when you have already performed your own validation + /// or intentionally want to skip policy checks. + pub fn into_report(self) -> VerifiedReport { + self.into_verified_report() + } + + fn into_verified_report(self) -> VerifiedReport { + VerifiedReport { + status: self.supplemental.tcb_status.to_string(), + advisory_ids: self.supplemental.advisory_ids, + report: self.report, + ppid: self.supplemental.ppid, + platform_tcb_level: self.supplemental.platform_tcb_level, + qe_tcb_level: self.supplemental.qe_tcb_level, + } + } +} + +#[cfg(feature = "rego")] +impl QuoteVerificationResult { + /// Generate Intel-format `qvl_result` array for Rego appraisal. + /// + /// SGX quotes produce 2 entries (platform + enclave). + /// TDX quotes produce 3 entries (platform + QE identity + TD). + pub fn to_rego_qvl_result(&self) -> Vec { + use crate::policy::rego_policy::{platform_class_id, tenant_class_id, tenant_measurement}; + + let mut result = Vec::new(); + + // 1. Platform TCB measurement + let platform_cid = platform_class_id(&self.report, self.supplemental.tee_type); + result.push(serde_json::json!({ + "environment": { "class_id": platform_cid }, + "measurement": self.supplemental.to_platform_rego_measurement(), + })); + + // 2. QE Identity measurement (TDX only) + if matches!(self.report, Report::TD10(_) | Report::TD15(_)) { + result.push(serde_json::json!({ + "environment": { "class_id": "3769258c-75e6-4bc7-8d72-d2b0e224cad2" }, + "measurement": self.supplemental.to_qe_rego_measurement(), + })); + } + + // 3. Tenant measurement (enclave or TD report) + let tenant_cid = tenant_class_id(&self.report); + result.push(serde_json::json!({ + "environment": { "class_id": tenant_cid }, + "measurement": tenant_measurement(&self.report), + })); + + result + } + + /// Validate against a [`RegoPolicySet`], consuming self into [`VerifiedReport`] on success. + /// + /// This is the multi-measurement equivalent of [`validate()`](Self::validate). + pub fn validate_rego(self, policies: &crate::policy::RegoPolicySet) -> Result { + let qvl_result = self.to_rego_qvl_result(); + policies.eval_rego(qvl_result)?; + Ok(self.into_verified_report()) + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] @@ -91,14 +187,14 @@ pub struct VerifiedReport { pub report: Report, #[serde(with = "serde_bytes")] pub ppid: Vec, - pub qe_status: TcbStatusWithAdvisory, - pub platform_status: TcbStatusWithAdvisory, + pub platform_tcb_level: TcbLevel, + pub qe_tcb_level: QeTcbLevel, } /// Quote verifier with configurable root certificate and crypto backend. /// -/// This allows using custom root certificates for testing or private deployments, -/// and selecting between different cryptographic backends (ring or rustcrypto). +/// Returns [`QuoteVerificationResult`] from cryptographic verification. +/// The caller applies a [`Policy`] via [`QuoteVerificationResult::validate()`]. pub struct QuoteVerifier { root_ca_der: Vec, backend: CryptoBackend, @@ -113,27 +209,26 @@ impl QuoteVerifier { } } - /// Create a new verifier using Intel's production root certificate with ring backend. + /// Create a new verifier using Intel's production root certificate. pub fn new_prod(backend: CryptoBackend) -> Self { Self::new(TRUSTED_ROOT_CA_DER.to_vec(), backend) } - /// Verify a quote with the configured root certificate - /// - /// # Arguments - /// * `raw_quote` - The raw quote bytes - /// * `collateral` - The quote collateral - /// * `now_secs` - Current time in seconds since UNIX epoch + #[cfg(feature = "_anycrypto")] + pub fn new_prod_default_crypto() -> Self { + Self::new_prod(default_crypto::backend()) + } + + /// Perform cryptographic verification, returning [`QuoteVerificationResult`]. /// - /// # Returns - /// * `Ok(VerifiedReport)` - The verified report - /// * `Err(Error)` - The error + /// This does NOT apply any policy. Use [`QuoteVerificationResult::validate()`] + /// to apply a policy and obtain a [`VerifiedReport`]. pub fn verify( &self, raw_quote: &[u8], collateral: &QuoteCollateralV3, now_secs: u64, - ) -> Result { + ) -> Result { verify_impl( raw_quote, collateral, @@ -144,54 +239,6 @@ impl QuoteVerifier { } } -#[cfg(all(feature = "js", feature = "_anycrypto"))] -#[wasm_bindgen] -pub fn js_verify( - raw_quote: JsValue, - quote_collateral: JsValue, - now: u64, -) -> Result { - let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) - .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; - let quote_collateral = serde_wasm_bindgen::from_value::(quote_collateral)?; - - let verified_report = verify(&raw_quote, "e_collateral, now).map_err(|e| { - let error_msg = format_error_chain(&e); - serde_wasm_bindgen::to_value(&error_msg) - .unwrap_or_else(|_| JsValue::from_str("Failed to encode Error")) - })?; - - serde_wasm_bindgen::to_value(&verified_report) - .map_err(|_| JsValue::from_str("Failed to encode verified_report")) -} - -#[cfg(all(feature = "js", feature = "_anycrypto"))] -#[wasm_bindgen] -pub fn js_verify_with_root_ca( - raw_quote: JsValue, - quote_collateral: JsValue, - root_ca_der: JsValue, - now: u64, -) -> Result { - let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) - .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; - let quote_collateral = serde_wasm_bindgen::from_value::(quote_collateral)?; - let root_ca_der: Vec = serde_wasm_bindgen::from_value(root_ca_der) - .map_err(|_| JsValue::from_str("Failed to decode root_ca_der"))?; - - let verifier = QuoteVerifier::new(root_ca_der, default_crypto::backend()); - let verified_report = verifier - .verify(&raw_quote, "e_collateral, now) - .map_err(|e| { - let error_msg = format_error_chain(&e); - serde_wasm_bindgen::to_value(&error_msg) - .unwrap_or_else(|_| JsValue::from_str("Failed to encode Error")) - })?; - - serde_wasm_bindgen::to_value(&verified_report) - .map_err(|_| JsValue::from_str("Failed to encode verified_report")) -} - #[cfg(feature = "js")] #[wasm_bindgen] pub async fn js_get_collateral(pccs_url: JsValue, raw_quote: JsValue) -> Result { @@ -356,12 +403,31 @@ fn verify_pck_cert_chain( // Extract Intel extension data from PCK cert (parsed once) let pck_ext = intel::parse_pck_extension(pck_leaf)?; + // Convert pce_id bytes to u16 (big-endian) + let pce_id = match pck_ext.pce_id.as_slice() { + [hi, lo] => u16::from_be_bytes([*hi, *lo]), + [lo] => u16::from(*lo), + _ => 0, + }; + + // Convert platform_instance_id to fixed-size array + let platform_instance_id = pck_ext.platform_instance_id.as_ref().and_then(|v| { + let arr: [u8; 16] = v.as_slice().try_into().ok()?; + Some(arr) + }); + Ok(PckCertChainResult { pck_leaf_der: pck_leaf.as_ref().to_vec(), ppid: pck_ext.ppid, cpu_svn: pck_ext.cpu_svn, pce_svn: pck_ext.pce_svn, fmspc: pck_ext.fmspc, + pce_id, + sgx_type: pck_ext.sgx_type as u8, + platform_instance_id, + dynamic_platform: pck_ext.dynamic_platform.into(), + cached_keys: pck_ext.cached_keys.into(), + smt_enabled: pck_ext.smt_enabled.into(), }) } @@ -372,6 +438,12 @@ struct PckCertChainResult { cpu_svn: [u8; 16], pce_svn: u16, fmspc: [u8; 6], + pce_id: u16, + sgx_type: u8, + platform_instance_id: Option<[u8; 16]>, + dynamic_platform: PckCertFlag, + cached_keys: PckCertFlag, + smt_enabled: PckCertFlag, } // ============================================================================= @@ -469,7 +541,7 @@ fn verify_isv_report_signature( // Step 8: Match Platform TCB (PCK Cert's CPU_SVN/PCE_SVN/FMSPC vs TCB Info) // ============================================================================= -/// Match platform TCB level and return status with advisory IDs +/// Match platform TCB level and return the matched TcbLevel fn match_platform_tcb( tcb_info: &TcbInfo, quote: &Quote, @@ -477,7 +549,7 @@ fn match_platform_tcb( cpu_svn: &[u8], pce_svn: u16, fmspc: &[u8], -) -> Result { +) -> Result { // Verify FMSPC matches let tcb_fmspc = hex::decode(&tcb_info.fmspc) .ok() @@ -537,11 +609,8 @@ fn match_platform_tcb( } } - // Found matching level - return Ok(TcbStatusWithAdvisory::new( - tcb_level.tcb_status, - tcb_level.advisory_ids.clone(), - )); + // Found matching level - return the full TcbLevel + return Ok(tcb_level.clone()); } bail!("No matching TCB level found"); @@ -551,7 +620,8 @@ fn match_platform_tcb( // Main verification flow following the trust chain // ============================================================================= -/// Internal implementation that uses QuoteVerifier +/// Cryptographic verification of a quote. Returns [`SupplementalData`] without +/// applying any policy — the caller decides acceptance via [`SupplementalData::validate()`]. /// /// Trust chain verification order: /// 1. Verify TCB Info signature (Intel Root -> TCB Signing Cert -> TCB Info JSON) @@ -570,7 +640,7 @@ fn verify_impl( now_secs: u64, root_ca_der: &[u8], backend: &CryptoBackend, -) -> Result { +) -> Result { // Setup trust anchor and time let root_ca = CertificateDer::from_slice(root_ca_der); let trust_anchor = @@ -578,9 +648,27 @@ fn verify_impl( let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs)); let raw_crls = [&collateral.root_ca_crl[..], &collateral.pck_crl]; + // Compute root_key_id: SHA-384 of root CA's raw public key bytes + // (the BIT STRING content from SubjectPublicKeyInfo, excluding algorithm OID). + // Matches Intel QVL's use of X509_get0_pubkey_bitstr(). + let root_key_id = { + let root_cert: x509_cert::Certificate = + der::Decode::from_der(root_ca_der).context("Failed to parse root CA for SPKI")?; + let raw_key = root_cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(); + (backend.sha384)(raw_key) + }; + // Check root CA against CRL webpki::check_single_cert_crl(root_ca_der, &raw_crls, now)?; + // Extract CRL numbers before parsing into webpki types + let root_ca_crl_num = crate::utils::extract_crl_number(&collateral.root_ca_crl).unwrap_or(0); + let pck_crl_num = crate::utils::extract_crl_number(&collateral.pck_crl).unwrap_or(0); + // Parse CRLs once for reuse across all certificate chain verifications let crls = parse_crls(&raw_crls)?; @@ -645,14 +733,14 @@ fn verify_impl( // Step 5: Verify QE Report content (hash check) verify_qe_report_data(&qe_report, &auth_data, backend)?; - // Step 6: Verify QE Report policy - let qe_status = verify_qe_identity_policy(&qe_report, &qe_identity)?; + // Step 6: Verify QE Report policy (returns matched QeTcbLevel) + let qe_tcb_level = verify_qe_identity_policy(&qe_report, &qe_identity)?; // Step 7: Verify ISV Report signature verify_isv_report_signature(raw_quote, "e, &auth_data, backend)?; - // Step 8: Match Platform TCB - let platform_status = match_platform_tcb( + // Step 8: Match Platform TCB (returns matched TcbLevel) + let platform_tcb_level = match_platform_tcb( &tcb_info, "e, tee_type, @@ -661,25 +749,203 @@ fn verify_impl( &pck_result.fmspc, )?; - // Step 9 & 10: QE TCB matching is done in verify_qe_identity_policy, merge statuses - let final_status = platform_status.clone().merge(&qe_status); - if !final_status.status.is_valid() { - bail!("TCB status is invalid: {:?}", final_status.status); - } + // Step 9 & 10: Merge statuses (take worst) + let platform_status = TcbStatusWithAdvisory::new( + platform_tcb_level.tcb_status, + platform_tcb_level.advisory_ids.clone(), + ); + let qe_status = + TcbStatusWithAdvisory::new(qe_tcb_level.tcb_status, qe_tcb_level.advisory_ids.clone()); + let final_status = platform_status.merge(&qe_status); // Validate report attributes (debug mode check, etc.) validate_attrs("e.report)?; - Ok(VerifiedReport { - status: final_status.status.to_string(), - advisory_ids: final_status.advisory_ids, + // Compute collateral time window fields + // Re-extract PCK cert chain for date computation (already verified in step 3) + let pck_certs_for_dates = if let Some(pem_chain) = &collateral.pck_certificate_chain { + extract_certs(pem_chain.as_bytes()).unwrap_or_default() + } else if auth_data.certification_data.cert_type == PCK_CERT_CHAIN { + extract_certs(&auth_data.certification_data.body.data).unwrap_or_default() + } else { + Vec::new() + }; + let (earliest_issue_date, latest_issue_date, earliest_expiration_date) = + compute_collateral_time_window(collateral, &pck_certs_for_dates, &tcb_info, &qe_identity)?; + + // tcb_level_date_tag: parse the matched platform TCB level's tcb_date + let tcb_level_date_tag = chrono::DateTime::parse_from_rfc3339(&platform_tcb_level.tcb_date) + .ok() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(0); + + // tcb_eval_data_number: lower of TCBInfo and QEIdentity values + let tcb_eval_data_number = tcb_info + .tcb_evaluation_data_number + .min(qe_identity.tcb_evaluation_data_number); + + Ok(QuoteVerificationResult { report: quote.report, - ppid: pck_result.ppid, - qe_status, - platform_status, + supplemental: SupplementalData { + tcb_status: final_status.status, + advisory_ids: final_status.advisory_ids, + earliest_issue_date, + latest_issue_date, + earliest_expiration_date, + tcb_level_date_tag, + pck_crl_num, + root_ca_crl_num, + tcb_eval_data_number, + root_key_id, + ppid: pck_result.ppid, + cpu_svn: pck_result.cpu_svn, + pce_svn: pck_result.pce_svn, + pce_id: pck_result.pce_id, + fmspc: pck_result.fmspc, + tee_type: quote.header.tee_type, + sgx_type: pck_result.sgx_type, + platform_instance_id: pck_result.platform_instance_id, + dynamic_platform: pck_result.dynamic_platform, + cached_keys: pck_result.cached_keys, + smt_enabled: pck_result.smt_enabled, + platform_tcb_level, + qe_tcb_level, + qe_report, + qe_tcb_eval_data_number: qe_identity.tcb_evaluation_data_number, + }, }) } +/// Compute the collateral time window: earliest issue, latest issue, earliest expiration. +/// +/// Matches Intel QVL's `qve_get_collateral_dates()` which considers **8 date sources**: +/// 1. Root CA CRL thisUpdate/nextUpdate +/// 2. PCK CRL thisUpdate/nextUpdate +/// 3. PCK CRL issuer certificate chain notBefore/notAfter +/// 4. PCK certificate chain notBefore/notAfter +/// 5. TCBInfo issuer certificate chain notBefore/notAfter +/// 6. QEIdentity issuer certificate chain notBefore/notAfter +/// 7. TCBInfo JSON issueDate/nextUpdate +/// 8. QEIdentity JSON issueDate/nextUpdate +fn compute_collateral_time_window( + collateral: &QuoteCollateralV3, + pck_cert_chain: &[CertificateDer<'_>], + tcb_info: &TcbInfo, + qe_identity: &QeIdentity, +) -> Result<(u64, u64, u64)> { + fn parse_rfc3339_ts(s: &str) -> Option { + chrono::DateTime::parse_from_rfc3339(s) + .ok() + .map(|dt| dt.timestamp() as u64) + } + + fn parse_crl_dates(crl_der: &[u8]) -> Result<(u64, Option)> { + use der::Decode as _; + let crl = x509_cert::crl::CertificateList::from_der(crl_der) + .context("Failed to parse CRL for time window")?; + let this_update = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); + let next_update = crl + .tbs_cert_list + .next_update + .map(|t| t.to_unix_duration().as_secs()); + Ok((this_update, next_update)) + } + + /// Extract notBefore/notAfter from a PEM certificate chain and fold into min/max accumulators. + fn fold_cert_chain_dates( + pem_chain: &[u8], + earliest_issue: &mut u64, + latest_issue: &mut u64, + earliest_expiration: &mut u64, + ) -> Result<()> { + let certs = extract_certs(pem_chain)?; + fold_der_cert_dates(&certs, earliest_issue, latest_issue, earliest_expiration) + } + + fn fold_der_cert_dates( + certs: &[CertificateDer<'_>], + earliest_issue: &mut u64, + latest_issue: &mut u64, + earliest_expiration: &mut u64, + ) -> Result<()> { + use der::Decode as _; + for cert_der in certs { + let cert = x509_cert::Certificate::from_der(cert_der) + .context("Failed to parse certificate for time window")?; + let not_before = cert + .tbs_certificate + .validity + .not_before + .to_unix_duration() + .as_secs(); + let not_after = cert + .tbs_certificate + .validity + .not_after + .to_unix_duration() + .as_secs(); + *earliest_issue = (*earliest_issue).min(not_before); + *latest_issue = (*latest_issue).max(not_before); + *earliest_expiration = (*earliest_expiration).min(not_after); + } + Ok(()) + } + + // TCBInfo dates (already parsed upstream) + let tcb_issue = parse_rfc3339_ts(&tcb_info.issue_date).context("TCBInfo issueDate")?; + let tcb_next = parse_rfc3339_ts(&tcb_info.next_update).context("TCBInfo nextUpdate")?; + + // QEIdentity dates (already parsed upstream) + let qe_issue = parse_rfc3339_ts(&qe_identity.issue_date).context("QEIdentity issueDate")?; + let qe_next = parse_rfc3339_ts(&qe_identity.next_update).context("QEIdentity nextUpdate")?; + + let mut earliest_issue = tcb_issue.min(qe_issue); + let mut latest_issue = tcb_issue.max(qe_issue); + let mut earliest_expiration = tcb_next.min(qe_next); + + // Include CRL dates (sources 1 & 2) + for crl_der in [&collateral.root_ca_crl[..], &collateral.pck_crl[..]] { + let (this_update, next_update) = parse_crl_dates(crl_der)?; + earliest_issue = earliest_issue.min(this_update); + latest_issue = latest_issue.max(this_update); + if let Some(next) = next_update { + earliest_expiration = earliest_expiration.min(next); + } + } + + // Include certificate chain dates (sources 3-6) + // PCK CRL issuer chain (same PEM as pck_crl_issuer_chain) + fold_cert_chain_dates( + collateral.pck_crl_issuer_chain.as_bytes(), + &mut earliest_issue, + &mut latest_issue, + &mut earliest_expiration, + )?; + // PCK certificate chain + fold_der_cert_dates( + pck_cert_chain, + &mut earliest_issue, + &mut latest_issue, + &mut earliest_expiration, + )?; + // TCBInfo issuer chain + fold_cert_chain_dates( + collateral.tcb_info_issuer_chain.as_bytes(), + &mut earliest_issue, + &mut latest_issue, + &mut earliest_expiration, + )?; + // QEIdentity issuer chain + fold_cert_chain_dates( + collateral.qe_identity_issuer_chain.as_bytes(), + &mut earliest_issue, + &mut latest_issue, + &mut earliest_expiration, + )?; + + Ok((earliest_issue, latest_issue, earliest_expiration)) +} + fn validate_sgx_attrs(report: &EnclaveReport) -> Result<()> { let is_debug = report.attributes[0] & 0x02 != 0; if is_debug { @@ -733,23 +999,21 @@ pub mod ring { out } + fn ring_sha384(data: &[u8]) -> [u8; 48] { + let digest = ::ring::digest::digest(&::ring::digest::SHA384, data); + let mut out = [0u8; 48]; + out.copy_from_slice(digest.as_ref()); + out + } + /// Returns a [`CryptoBackend`] backed by ring. pub fn backend() -> CryptoBackend { CryptoBackend { sig_algo: webpki::ring::ECDSA_P256_SHA256, sha256: ring_sha256, + sha384: ring_sha384, } } - - /// Verify a quote using Intel's trusted root CA and ring backend. - pub fn verify( - raw_quote: &[u8], - collateral: &QuoteCollateralV3, - now_secs: u64, - ) -> Result { - QuoteVerifier::new(TRUSTED_ROOT_CA_DER.to_vec(), backend()) - .verify(raw_quote, collateral, now_secs) - } } /// RustCrypto backend module. @@ -764,43 +1028,21 @@ pub mod rustcrypto { sha2::Sha256::digest(data).into() } + fn rustcrypto_sha384(data: &[u8]) -> [u8; 48] { + use sha2::Digest; + sha2::Sha384::digest(data).into() + } + /// Returns a [`CryptoBackend`] backed by RustCrypto. pub fn backend() -> CryptoBackend { CryptoBackend { sig_algo: webpki::rustcrypto::ECDSA_P256_SHA256, sha256: rustcrypto_sha256, + sha384: rustcrypto_sha384, } } - - /// Verify a quote using Intel's trusted root CA and RustCrypto backend. - pub fn verify( - raw_quote: &[u8], - collateral: &QuoteCollateralV3, - now_secs: u64, - ) -> Result { - QuoteVerifier::new(TRUSTED_ROOT_CA_DER.to_vec(), backend()) - .verify(raw_quote, collateral, now_secs) - } } -/// Verify a quote using Intel's trusted root CA (ring backend). -/// -/// This is a backwards-compatible convenience function that uses the ring backend. -/// For rustcrypto, use [`rustcrypto::verify()`]. -/// -/// # Arguments -/// -/// * `raw_quote` - The raw quote to verify. Supported SGX and TDX quotes. -/// * `quote_collateral` - The quote collateral to verify. Can be obtained from PCCS by `get_collateral`. -/// * `now` - The current time in seconds since the Unix epoch -/// -/// # Returns -/// -/// * `Ok(VerifiedReport)` - The verified report -/// * `Err(Error)` - The error -#[cfg(feature = "_anycrypto")] -pub use self::default_crypto::verify; - // ============================================================================= // Step 6 & 9: Verify QE Report policy and match QE TCB // ============================================================================= @@ -814,11 +1056,11 @@ pub use self::default_crypto::verify; /// - ATTRIBUTES match after applying the mask /// - ISVSVN meets minimum requirement from QE Identity TCB levels (Step 9) /// -/// Returns the QE TCB status and advisory IDs based on the QE's ISVSVN. +/// Returns the matched QeTcbLevel based on the QE's ISVSVN. fn verify_qe_identity_policy( qe_report: &EnclaveReport, qe_identity: &QeIdentity, -) -> Result { +) -> Result { // Verify MRSIGNER if qe_report.mr_signer != qe_identity.mrsigner { bail!( @@ -881,17 +1123,14 @@ fn verify_qe_identity_policy( /// Match QE ISVSVN against QE Identity TCB levels /// /// TCB levels are expected to be sorted from highest to lowest ISVSVN. -/// Returns the status and advisory IDs for the matching level. +/// Returns the matched QeTcbLevel. fn match_qe_tcb_level( isv_svn: u16, tcb_levels: &[crate::qe_identity::QeTcbLevel], -) -> Result { +) -> Result { for tcb_level in tcb_levels { if isv_svn >= tcb_level.tcb.isvsvn { - return Ok(TcbStatusWithAdvisory::new( - tcb_level.tcb_status, - tcb_level.advisory_ids.clone(), - )); + return Ok(tcb_level.clone()); } } @@ -1094,9 +1333,9 @@ mod tests { let result = verify_qe_identity_policy(&qe_report, &qe_identity); assert!(result.is_ok()); - let status = result.unwrap(); - assert_eq!(status.status, UpToDate); - assert!(status.advisory_ids.is_empty()); + let tcb_level = result.unwrap(); + assert_eq!(tcb_level.tcb_status, UpToDate); + assert!(tcb_level.advisory_ids.is_empty()); } #[test] @@ -1107,9 +1346,9 @@ mod tests { let result = verify_qe_identity_policy(&qe_report, &qe_identity); assert!(result.is_ok()); - let status = result.unwrap(); - assert_eq!(status.status, OutOfDate); - assert_eq!(status.advisory_ids, vec!["INTEL-SA-00615"]); + let tcb_level = result.unwrap(); + assert_eq!(tcb_level.tcb_status, OutOfDate); + assert_eq!(tcb_level.advisory_ids, vec!["INTEL-SA-00615"]); } #[test] @@ -1120,8 +1359,8 @@ mod tests { let result = verify_qe_identity_policy(&qe_report, &qe_identity); assert!(result.is_ok()); - let status = result.unwrap(); - assert_eq!(status.status, UpToDate); // Matches first level (isvsvn >= 8) + let tcb_level = result.unwrap(); + assert_eq!(tcb_level.tcb_status, UpToDate); // Matches first level (isvsvn >= 8) } #[test] @@ -1148,9 +1387,9 @@ mod tests { let result = verify_qe_identity_policy(&qe_report, &qe_identity); assert!(result.is_ok()); - let status = result.unwrap(); + let tcb_level = result.unwrap(); // Should match level with isvsvn=6 (7 >= 6) - assert_eq!(status.status, OutOfDate); - assert_eq!(status.advisory_ids, vec!["INTEL-SA-00615"]); + assert_eq!(tcb_level.tcb_status, OutOfDate); + assert_eq!(tcb_level.advisory_ids, vec!["INTEL-SA-00615"]); } } diff --git a/tests/near/contracts/gas-test/Cargo.lock b/tests/near/contracts/gas-test/Cargo.lock index 74d1e4c..3cc75bd 100644 --- a/tests/near/contracts/gas-test/Cargo.lock +++ b/tests/near/contracts/gas-test/Cargo.lock @@ -592,7 +592,7 @@ dependencies = [ [[package]] name = "dcap-qvl" -version = "0.3.10" +version = "0.3.11" dependencies = [ "anyhow", "asn1_der", diff --git a/tests/near/contracts/gas-test/src/lib.rs b/tests/near/contracts/gas-test/src/lib.rs index c0eb7af..b8114f2 100644 --- a/tests/near/contracts/gas-test/src/lib.rs +++ b/tests/near/contracts/gas-test/src/lib.rs @@ -1,6 +1,6 @@ extern crate alloc; -use dcap_qvl::{verify::verify, QuoteCollateralV3}; +use dcap_qvl::{verify::{QuoteVerifier, ring}, QuoteCollateralV3}; use hex::decode; use near_sdk::{env, log, near}; @@ -57,9 +57,10 @@ impl Contract { // Get current timestamp in seconds let timestamp_s = get_block_timestamp_secs(); - // Call dcap-qvl::verify::verify() directly - match verify("e_bytes, &collateral_data, timestamp_s) { - Ok(_verified_report) => { + // Call dcap-qvl verify + let verifier = QuoteVerifier::new_prod(ring::backend()); + match verifier.verify("e_bytes, &collateral_data, timestamp_s) { + Ok(_supplemental) => { log!("Verification result: Success"); true } diff --git a/tests/verify_quote.rs b/tests/verify_quote.rs index 098a411..970d55e 100644 --- a/tests/verify_quote.rs +++ b/tests/verify_quote.rs @@ -11,14 +11,25 @@ pub fn verify( collateral: &QuoteCollateralV3, now_secs: u64, ) -> anyhow::Result { - use dcap_qvl::verify::{ring, rustcrypto}; - let ring_result = ring::verify(raw_quote, collateral, now_secs); - let rustcrypto_result = rustcrypto::verify(raw_quote, collateral, now_secs); + use dcap_qvl::verify::{ring, rustcrypto, QuoteVerifier}; + + let ring_verifier = QuoteVerifier::new_prod(ring::backend()); + let rustcrypto_verifier = QuoteVerifier::new_prod(rustcrypto::backend()); + + let ring_result = ring_verifier + .verify(raw_quote, collateral, now_secs) + .map(|s| s.into_report()); + let rustcrypto_result = rustcrypto_verifier + .verify(raw_quote, collateral, now_secs) + .map(|s| s.into_report()); + assert_eq!( ring_result.map_err(|e| e.to_string()), rustcrypto_result.map_err(|e| e.to_string()) ); - ring::verify(raw_quote, collateral, now_secs) + ring_verifier + .verify(raw_quote, collateral, now_secs) + .map(|s| s.into_report()) } fn now_from_collateral(collateral: &QuoteCollateralV3) -> u64 { @@ -88,6 +99,238 @@ fn could_parse_sgx_quote() { ); } +/// Cross-validate all SupplementalData fields against independently computed values. +#[test] +fn sgx_supplemental_data_cross_validation() { + use dcap_qvl::verify::{ring, QuoteVerifier}; + use dcap_qvl::PckCertFlag; + + let raw_quote = include_bytes!("../sample/sgx_quote").to_vec(); + let collateral: QuoteCollateralV3 = + serde_json::from_slice(include_bytes!("../sample/sgx_quote_collateral.json")).unwrap(); + let now = now_from_collateral(&collateral); + + let verifier = QuoteVerifier::new_prod(ring::backend()); + let result = verifier.verify(&raw_quote, &collateral, now).unwrap(); + let s = &result.supplemental; + + // Parse quote for later use + let parsed_quote = Quote::decode(&mut &raw_quote[..]).unwrap(); + + // ── TCB status ────────────────────────────────────────────────────── + assert_eq!( + s.tcb_status.to_string(), + "ConfigurationAndSWHardeningNeeded" + ); + assert_eq!(s.advisory_ids, ["INTEL-SA-00289", "INTEL-SA-00615"]); + + // ── Collateral time window ────────────────────────────────────────── + // Independently compute using all 8 sources matching Intel QVL: + // TCBInfo, QEIdentity, 2 CRLs, 4 certificate chains + fn parse_ts(json: &str, field: &str) -> u64 { + let v: Value = serde_json::from_str(json).unwrap(); + chrono::DateTime::parse_from_rfc3339(v[field].as_str().unwrap()) + .unwrap() + .timestamp() as u64 + } + fn crl_dates(der: &[u8]) -> (u64, u64) { + let crl = CertificateList::from_der(der).unwrap(); + let this = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); + let next = crl + .tbs_cert_list + .next_update + .unwrap() + .to_unix_duration() + .as_secs(); + (this, next) + } + fn cert_chain_dates(pem: &[u8]) -> Vec<(u64, u64)> { + let certs = pem::parse_many(pem).unwrap(); + certs + .iter() + .map(|c| { + let cert: x509_cert::Certificate = DerDecode::from_der(c.contents()).unwrap(); + let nb = cert + .tbs_certificate + .validity + .not_before + .to_unix_duration() + .as_secs(); + let na = cert + .tbs_certificate + .validity + .not_after + .to_unix_duration() + .as_secs(); + (nb, na) + }) + .collect() + } + + let tcb_issue = parse_ts(&collateral.tcb_info, "issueDate"); + let tcb_next = parse_ts(&collateral.tcb_info, "nextUpdate"); + let qe_issue = parse_ts(&collateral.qe_identity, "issueDate"); + let qe_next = parse_ts(&collateral.qe_identity, "nextUpdate"); + let (root_crl_this, root_crl_next) = crl_dates(&collateral.root_ca_crl); + let (pck_crl_this, pck_crl_next) = crl_dates(&collateral.pck_crl); + + let mut expected_earliest_issue = tcb_issue.min(qe_issue).min(root_crl_this).min(pck_crl_this); + let mut expected_latest_issue = tcb_issue.max(qe_issue).max(root_crl_this).max(pck_crl_this); + let mut expected_earliest_expiration = + tcb_next.min(qe_next).min(root_crl_next).min(pck_crl_next); + + // Include certificate chain dates (4 chains) + let pck_chain = dcap_qvl::intel::extract_cert_chain(&parsed_quote).unwrap(); + for cert_der in &pck_chain { + let cert: x509_cert::Certificate = DerDecode::from_der(cert_der).unwrap(); + let nb = cert + .tbs_certificate + .validity + .not_before + .to_unix_duration() + .as_secs(); + let na = cert + .tbs_certificate + .validity + .not_after + .to_unix_duration() + .as_secs(); + expected_earliest_issue = expected_earliest_issue.min(nb); + expected_latest_issue = expected_latest_issue.max(nb); + expected_earliest_expiration = expected_earliest_expiration.min(na); + } + for pem_chain in [ + collateral.pck_crl_issuer_chain.as_bytes(), + collateral.tcb_info_issuer_chain.as_bytes(), + collateral.qe_identity_issuer_chain.as_bytes(), + ] { + for (nb, na) in cert_chain_dates(pem_chain) { + expected_earliest_issue = expected_earliest_issue.min(nb); + expected_latest_issue = expected_latest_issue.max(nb); + expected_earliest_expiration = expected_earliest_expiration.min(na); + } + } + + assert_eq!(s.earliest_issue_date, expected_earliest_issue); + assert_eq!(s.latest_issue_date, expected_latest_issue); + assert_eq!(s.earliest_expiration_date, expected_earliest_expiration); + + // ── tcb_level_date_tag ────────────────────────────────────────────── + let expected_tcb_date = chrono::DateTime::parse_from_rfc3339(&s.platform_tcb_level.tcb_date) + .unwrap() + .timestamp() as u64; + assert_eq!(s.tcb_level_date_tag, expected_tcb_date); + + // ── CRL numbers ───────────────────────────────────────────────────── + // Parse CRL numbers independently + fn extract_crl_num(crl_der: &[u8]) -> u32 { + let crl = CertificateList::from_der(crl_der).unwrap(); + if let Some(exts) = &crl.tbs_cert_list.crl_extensions { + for ext in exts.iter() { + if ext.extn_id.to_string() == "2.5.29.20" { + let num = + ::from_der(ext.extn_value.as_bytes()) + .unwrap(); + let bytes = num.as_bytes(); + let mut val: u32 = 0; + for &b in bytes { + val = (val << 8) | u32::from(b); + } + return val; + } + } + } + 0 + } + assert_eq!(s.root_ca_crl_num, extract_crl_num(&collateral.root_ca_crl)); + assert_eq!(s.pck_crl_num, extract_crl_num(&collateral.pck_crl)); + + // ── tcb_eval_data_number ──────────────────────────────────────────── + let tcb_info_parsed: dcap_qvl::TcbInfo = serde_json::from_str(&collateral.tcb_info).unwrap(); + let qe_id_parsed: dcap_qvl::QeIdentity = serde_json::from_str(&collateral.qe_identity).unwrap(); + let expected_eval_num = tcb_info_parsed + .tcb_evaluation_data_number + .min(qe_id_parsed.tcb_evaluation_data_number); + assert_eq!(s.tcb_eval_data_number, expected_eval_num); + + // ── root_key_id ───────────────────────────────────────────────────── + // SHA-384 of root CA raw public key bytes (BIT STRING content), + // matching Intel QVL's X509_get0_pubkey_bitstr() + let root_ca_der = include_bytes!("../src/TrustedRootCA.der"); + let root_cert: x509_cert::Certificate = DerDecode::from_der(root_ca_der).unwrap(); + let raw_pub_key = root_cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(); + let expected_root_key_id: [u8; 48] = { + use sha2::Digest; + sha2::Sha384::digest(raw_pub_key).into() + }; + assert_eq!(s.root_key_id, expected_root_key_id); + + // ── PCK certificate fields ────────────────────────────────────────── + // Parse PCK cert extension independently from the quote's embedded chain + let pck_chain_der = dcap_qvl::intel::extract_cert_chain(&parsed_quote).unwrap(); + let pck_ext = dcap_qvl::intel::parse_pck_extension(&pck_chain_der[0]).unwrap(); + + assert_eq!(s.cpu_svn, pck_ext.cpu_svn); + assert_eq!(s.pce_svn, pck_ext.pce_svn); + assert_eq!(s.fmspc, pck_ext.fmspc); + assert_eq!(s.ppid, pck_ext.ppid); + assert_eq!(s.sgx_type, pck_ext.sgx_type as u8); + + // pce_id: from raw extension bytes + let expected_pce_id = match pck_ext.pce_id.len() { + 2 => u16::from_be_bytes([pck_ext.pce_id[0], pck_ext.pce_id[1]]), + 1 => u16::from(pck_ext.pce_id[0]), + _ => 0, + }; + assert_eq!(s.pce_id, expected_pce_id); + + // ── TEE type ──────────────────────────────────────────────────────── + assert_eq!(s.tee_type, 0x00000000); // SGX + + // ── Platform instance (Processor CA → should be None/Undefined) ───── + // Sample quote uses Processor CA, so platform_instance_id should be None + // and config flags should be Undefined + assert_eq!(s.dynamic_platform, PckCertFlag::Undefined); + assert_eq!(s.cached_keys, PckCertFlag::Undefined); + assert_eq!(s.smt_enabled, PckCertFlag::Undefined); + + // ── TCB levels ────────────────────────────────────────────────────── + assert!(!s.platform_tcb_level.tcb_date.is_empty()); + assert!(!s.qe_tcb_level.tcb_date.is_empty()); + + // Verify ring and rustcrypto produce identical supplemental data + let rustcrypto_verifier = QuoteVerifier::new_prod(dcap_qvl::verify::rustcrypto::backend()); + let rc_result = rustcrypto_verifier + .verify(&raw_quote, &collateral, now) + .unwrap(); + let rc = &rc_result.supplemental; + assert_eq!(s.tcb_status, rc.tcb_status); + assert_eq!(s.advisory_ids, rc.advisory_ids); + assert_eq!(s.earliest_issue_date, rc.earliest_issue_date); + assert_eq!(s.latest_issue_date, rc.latest_issue_date); + assert_eq!(s.earliest_expiration_date, rc.earliest_expiration_date); + assert_eq!(s.tcb_level_date_tag, rc.tcb_level_date_tag); + assert_eq!(s.pck_crl_num, rc.pck_crl_num); + assert_eq!(s.root_ca_crl_num, rc.root_ca_crl_num); + assert_eq!(s.tcb_eval_data_number, rc.tcb_eval_data_number); + assert_eq!(s.root_key_id, rc.root_key_id); + assert_eq!(s.ppid, rc.ppid); + assert_eq!(s.cpu_svn, rc.cpu_svn); + assert_eq!(s.pce_svn, rc.pce_svn); + assert_eq!(s.pce_id, rc.pce_id); + assert_eq!(s.fmspc, rc.fmspc); + assert_eq!(s.tee_type, rc.tee_type); + assert_eq!(s.sgx_type, rc.sgx_type); + assert_eq!(s.platform_instance_id, rc.platform_instance_id); + assert_eq!(s.dynamic_platform, rc.dynamic_platform); + assert_eq!(s.cached_keys, rc.cached_keys); + assert_eq!(s.smt_enabled, rc.smt_enabled); +} + #[test] fn could_parse_tdx_quote() { let raw_quote = include_bytes!("../sample/tdx_quote"); @@ -101,3 +344,607 @@ fn could_parse_tdx_quote() { assert_eq!(tcb_status.status, "UpToDate"); assert!(tcb_status.advisory_ids.is_empty()); } + +/// Print all SupplementalData fields side-by-side: our result vs independently computed. +#[test] +fn print_supplemental_data_comparison() { + use dcap_qvl::verify::{ring, QuoteVerifier}; + + fn ts_to_utc(ts: u64) -> String { + chrono::DateTime::from_timestamp(ts as i64, 0) + .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()) + .unwrap_or_else(|| format!("{ts}")) + } + + fn parse_ts(json: &str, field: &str) -> u64 { + let v: Value = serde_json::from_str(json).unwrap(); + chrono::DateTime::parse_from_rfc3339(v[field].as_str().unwrap()) + .unwrap() + .timestamp() as u64 + } + + fn crl_dates(der: &[u8]) -> (u64, u64) { + let crl = CertificateList::from_der(der).unwrap(); + let this = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); + let next = crl + .tbs_cert_list + .next_update + .unwrap() + .to_unix_duration() + .as_secs(); + (this, next) + } + + fn extract_crl_num(crl_der: &[u8]) -> u32 { + let crl = CertificateList::from_der(crl_der).unwrap(); + if let Some(exts) = &crl.tbs_cert_list.crl_extensions { + for ext in exts.iter() { + if ext.extn_id.to_string() == "2.5.29.20" { + let num = + ::from_der(ext.extn_value.as_bytes()) + .unwrap(); + let bytes = num.as_bytes(); + let mut val: u32 = 0; + for &b in bytes { + val = (val << 8) | u32::from(b); + } + return val; + } + } + } + 0 + } + + fn cert_chain_dates_pem(pem: &[u8]) -> Vec<(u64, u64)> { + pem::parse_many(pem) + .unwrap() + .iter() + .map(|c| { + let cert: x509_cert::Certificate = DerDecode::from_der(c.contents()).unwrap(); + let nb = cert + .tbs_certificate + .validity + .not_before + .to_unix_duration() + .as_secs(); + let na = cert + .tbs_certificate + .validity + .not_after + .to_unix_duration() + .as_secs(); + (nb, na) + }) + .collect() + } + + fn cert_chain_dates_der(chain: &[Vec]) -> Vec<(u64, u64)> { + chain + .iter() + .map(|der| { + let cert: x509_cert::Certificate = DerDecode::from_der(der).unwrap(); + let nb = cert + .tbs_certificate + .validity + .not_before + .to_unix_duration() + .as_secs(); + let na = cert + .tbs_certificate + .validity + .not_after + .to_unix_duration() + .as_secs(); + (nb, na) + }) + .collect() + } + + // Compute expected time window from 8 sources + fn compute_time_window( + collateral: &QuoteCollateralV3, + pck_chain: &[Vec], + ) -> (u64, u64, u64) { + let tcb_issue = parse_ts(&collateral.tcb_info, "issueDate"); + let tcb_next = parse_ts(&collateral.tcb_info, "nextUpdate"); + let qe_issue = parse_ts(&collateral.qe_identity, "issueDate"); + let qe_next = parse_ts(&collateral.qe_identity, "nextUpdate"); + let (root_crl_this, root_crl_next) = crl_dates(&collateral.root_ca_crl); + let (pck_crl_this, pck_crl_next) = crl_dates(&collateral.pck_crl); + + let mut ei = tcb_issue.min(qe_issue).min(root_crl_this).min(pck_crl_this); + let mut li = tcb_issue.max(qe_issue).max(root_crl_this).max(pck_crl_this); + let mut ee = tcb_next.min(qe_next).min(root_crl_next).min(pck_crl_next); + + for (nb, na) in cert_chain_dates_der(pck_chain) { + ei = ei.min(nb); + li = li.max(nb); + ee = ee.min(na); + } + for pem in [ + collateral.pck_crl_issuer_chain.as_bytes(), + collateral.tcb_info_issuer_chain.as_bytes(), + collateral.qe_identity_issuer_chain.as_bytes(), + ] { + for (nb, na) in cert_chain_dates_pem(pem) { + ei = ei.min(nb); + li = li.max(nb); + ee = ee.min(na); + } + } + (ei, li, ee) + } + + fn compute_root_key_id() -> [u8; 48] { + let root_ca_der = include_bytes!("../src/TrustedRootCA.der"); + let root_cert: x509_cert::Certificate = DerDecode::from_der(root_ca_der).unwrap(); + let raw_pub_key = root_cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(); + use sha2::Digest; + sha2::Sha384::digest(raw_pub_key).into() + } + + // ═══════════════════════════════════════════════════════════════════ + // SGX Quote + // ═══════════════════════════════════════════════════════════════════ + println!("\n{:=<80}", ""); + println!("SGX Quote — SupplementalData (ours vs independently computed)"); + println!("{:=<80}", ""); + + let raw_quote = include_bytes!("../sample/sgx_quote").to_vec(); + let collateral: QuoteCollateralV3 = + serde_json::from_slice(include_bytes!("../sample/sgx_quote_collateral.json")).unwrap(); + let now = now_from_collateral(&collateral); + let parsed_quote = Quote::decode(&mut &raw_quote[..]).unwrap(); + let pck_chain = dcap_qvl::intel::extract_cert_chain(&parsed_quote).unwrap(); + let pck_ext = dcap_qvl::intel::parse_pck_extension(&pck_chain[0]).unwrap(); + + let verifier = QuoteVerifier::new_prod(ring::backend()); + let result = verifier.verify(&raw_quote, &collateral, now).unwrap(); + let s = &result.supplemental; + + let tcb_info: dcap_qvl::TcbInfo = serde_json::from_str(&collateral.tcb_info).unwrap(); + let qe_id: dcap_qvl::QeIdentity = serde_json::from_str(&collateral.qe_identity).unwrap(); + let (exp_ei, exp_li, exp_ee) = compute_time_window(&collateral, &pck_chain); + let exp_tcb_date = chrono::DateTime::parse_from_rfc3339(&s.platform_tcb_level.tcb_date) + .unwrap() + .timestamp() as u64; + let exp_eval_num = tcb_info + .tcb_evaluation_data_number + .min(qe_id.tcb_evaluation_data_number); + let exp_root_key_id = compute_root_key_id(); + let exp_pce_id = match pck_ext.pce_id.len() { + 2 => u16::from_be_bytes([pck_ext.pce_id[0], pck_ext.pce_id[1]]), + 1 => u16::from(pck_ext.pce_id[0]), + _ => 0, + }; + + println!( + "{:<40} {:<40} {:<40}", + "Field", "Our Value", "Expected (independent)" + ); + println!("{:-<40} {:-<40} {:-<40}", "", "", ""); + println!( + "{:<40} {:<40} {:<40}", + "tcb_status", + format!("{:?}", s.tcb_status), + "ConfigurationAndSWHardeningNeeded" + ); + println!( + "{:<40} {:<40} {:<40}", + "advisory_ids", + format!("{:?}", s.advisory_ids), + "[INTEL-SA-00289, INTEL-SA-00615]" + ); + println!( + "{:<40} {:<40} {:<40}", + "earliest_issue_date", + format!( + "{} ({})", + s.earliest_issue_date, + ts_to_utc(s.earliest_issue_date) + ), + format!("{} ({})", exp_ei, ts_to_utc(exp_ei)) + ); + println!( + "{:<40} {:<40} {:<40}", + "latest_issue_date", + format!( + "{} ({})", + s.latest_issue_date, + ts_to_utc(s.latest_issue_date) + ), + format!("{} ({})", exp_li, ts_to_utc(exp_li)) + ); + println!( + "{:<40} {:<40} {:<40}", + "earliest_expiration_date", + format!( + "{} ({})", + s.earliest_expiration_date, + ts_to_utc(s.earliest_expiration_date) + ), + format!("{} ({})", exp_ee, ts_to_utc(exp_ee)) + ); + println!( + "{:<40} {:<40} {:<40}", + "tcb_level_date_tag", + format!( + "{} ({})", + s.tcb_level_date_tag, + ts_to_utc(s.tcb_level_date_tag) + ), + format!("{} ({})", exp_tcb_date, ts_to_utc(exp_tcb_date)) + ); + println!( + "{:<40} {:<40} {:<40}", + "pck_crl_num", + format!("{}", s.pck_crl_num), + format!("{}", extract_crl_num(&collateral.pck_crl)) + ); + println!( + "{:<40} {:<40} {:<40}", + "root_ca_crl_num", + format!("{}", s.root_ca_crl_num), + format!("{}", extract_crl_num(&collateral.root_ca_crl)) + ); + println!( + "{:<40} {:<40} {:<40}", + "tcb_eval_data_number", + format!("{}", s.tcb_eval_data_number), + format!( + "{} (min of {} and {})", + exp_eval_num, tcb_info.tcb_evaluation_data_number, qe_id.tcb_evaluation_data_number + ) + ); + println!( + "{:<40} {:<40} {:<40}", + "root_key_id", + hex::encode(&s.root_key_id[..24]) + "...", + hex::encode(&exp_root_key_id[..24]) + "..." + ); + println!( + "{:<40} {:<40} {:<40}", + "ppid", + hex::encode(&s.ppid), + hex::encode(&pck_ext.ppid) + ); + println!( + "{:<40} {:<40} {:<40}", + "cpu_svn", + hex::encode(s.cpu_svn), + hex::encode(pck_ext.cpu_svn) + ); + println!( + "{:<40} {:<40} {:<40}", + "pce_svn", + format!("{}", s.pce_svn), + format!("{}", pck_ext.pce_svn) + ); + println!( + "{:<40} {:<40} {:<40}", + "pce_id", + format!("{}", s.pce_id), + format!("{}", exp_pce_id) + ); + println!( + "{:<40} {:<40} {:<40}", + "fmspc", + hex::encode(s.fmspc), + hex::encode(pck_ext.fmspc) + ); + println!( + "{:<40} {:<40} {:<40}", + "tee_type", + format!("0x{:08X}", s.tee_type), + "0x00000000 (SGX)" + ); + println!( + "{:<40} {:<40} {:<40}", + "sgx_type", + format!("{}", s.sgx_type), + format!("{}", pck_ext.sgx_type) + ); + println!( + "{:<40} {:<40} {:<40}", + "platform_instance_id", + format!("{:?}", s.platform_instance_id), + format!("{:?}", pck_ext.platform_instance_id) + ); + println!( + "{:<40} {:<40} {:<40}", + "dynamic_platform", + format!("{:?}", s.dynamic_platform), + format!("{:?}", pck_ext.dynamic_platform) + ); + println!( + "{:<40} {:<40} {:<40}", + "cached_keys", + format!("{:?}", s.cached_keys), + format!("{:?}", pck_ext.cached_keys) + ); + println!( + "{:<40} {:<40} {:<40}", + "smt_enabled", + format!("{:?}", s.smt_enabled), + format!("{:?}", pck_ext.smt_enabled) + ); + println!( + "{:<40} {:<40}", + "platform_tcb_level.tcb_date", &s.platform_tcb_level.tcb_date + ); + println!( + "{:<40} {:<40}", + "platform_tcb_level.tcb_status", + format!("{:?}", s.platform_tcb_level.tcb_status) + ); + println!( + "{:<40} {:<40}", + "qe_tcb_level.tcb_date", &s.qe_tcb_level.tcb_date + ); + println!( + "{:<40} {:<40}", + "qe_tcb_level.tcb_status", + format!("{:?}", s.qe_tcb_level.tcb_status) + ); + + // ═══════════════════════════════════════════════════════════════════ + // TDX Quote + // ═══════════════════════════════════════════════════════════════════ + println!("\n{:=<80}", ""); + println!("TDX Quote — SupplementalData (ours vs independently computed)"); + println!("{:=<80}", ""); + + let raw_quote_tdx = include_bytes!("../sample/tdx_quote"); + let collateral_tdx: QuoteCollateralV3 = + serde_json::from_slice(include_bytes!("../sample/tdx_quote_collateral.json")).unwrap(); + let now_tdx = now_from_collateral(&collateral_tdx); + let parsed_quote_tdx = Quote::decode(&mut &raw_quote_tdx[..]).unwrap(); + let pck_chain_tdx = dcap_qvl::intel::extract_cert_chain(&parsed_quote_tdx).unwrap(); + let pck_ext_tdx = dcap_qvl::intel::parse_pck_extension(&pck_chain_tdx[0]).unwrap(); + + let result_tdx = verifier + .verify(raw_quote_tdx, &collateral_tdx, now_tdx) + .unwrap(); + let t = &result_tdx.supplemental; + + let tcb_info_tdx: dcap_qvl::TcbInfo = serde_json::from_str(&collateral_tdx.tcb_info).unwrap(); + let qe_id_tdx: dcap_qvl::QeIdentity = + serde_json::from_str(&collateral_tdx.qe_identity).unwrap(); + let (exp_ei_t, exp_li_t, exp_ee_t) = compute_time_window(&collateral_tdx, &pck_chain_tdx); + let exp_tcb_date_t = chrono::DateTime::parse_from_rfc3339(&t.platform_tcb_level.tcb_date) + .unwrap() + .timestamp() as u64; + let exp_eval_num_t = tcb_info_tdx + .tcb_evaluation_data_number + .min(qe_id_tdx.tcb_evaluation_data_number); + let exp_pce_id_t = match pck_ext_tdx.pce_id.len() { + 2 => u16::from_be_bytes([pck_ext_tdx.pce_id[0], pck_ext_tdx.pce_id[1]]), + 1 => u16::from(pck_ext_tdx.pce_id[0]), + _ => 0, + }; + + println!( + "{:<40} {:<40} {:<40}", + "Field", "Our Value", "Expected (independent)" + ); + println!("{:-<40} {:-<40} {:-<40}", "", "", ""); + println!( + "{:<40} {:<40} {:<40}", + "tcb_status", + format!("{:?}", t.tcb_status), + "UpToDate" + ); + println!( + "{:<40} {:<40} {:<40}", + "advisory_ids", + format!("{:?}", t.advisory_ids), + "[]" + ); + println!( + "{:<40} {:<40} {:<40}", + "earliest_issue_date", + format!( + "{} ({})", + t.earliest_issue_date, + ts_to_utc(t.earliest_issue_date) + ), + format!("{} ({})", exp_ei_t, ts_to_utc(exp_ei_t)) + ); + println!( + "{:<40} {:<40} {:<40}", + "latest_issue_date", + format!( + "{} ({})", + t.latest_issue_date, + ts_to_utc(t.latest_issue_date) + ), + format!("{} ({})", exp_li_t, ts_to_utc(exp_li_t)) + ); + println!( + "{:<40} {:<40} {:<40}", + "earliest_expiration_date", + format!( + "{} ({})", + t.earliest_expiration_date, + ts_to_utc(t.earliest_expiration_date) + ), + format!("{} ({})", exp_ee_t, ts_to_utc(exp_ee_t)) + ); + println!( + "{:<40} {:<40} {:<40}", + "tcb_level_date_tag", + format!( + "{} ({})", + t.tcb_level_date_tag, + ts_to_utc(t.tcb_level_date_tag) + ), + format!("{} ({})", exp_tcb_date_t, ts_to_utc(exp_tcb_date_t)) + ); + println!( + "{:<40} {:<40} {:<40}", + "pck_crl_num", + format!("{}", t.pck_crl_num), + format!("{}", extract_crl_num(&collateral_tdx.pck_crl)) + ); + println!( + "{:<40} {:<40} {:<40}", + "root_ca_crl_num", + format!("{}", t.root_ca_crl_num), + format!("{}", extract_crl_num(&collateral_tdx.root_ca_crl)) + ); + println!( + "{:<40} {:<40} {:<40}", + "tcb_eval_data_number", + format!("{}", t.tcb_eval_data_number), + format!( + "{} (min of {} and {})", + exp_eval_num_t, + tcb_info_tdx.tcb_evaluation_data_number, + qe_id_tdx.tcb_evaluation_data_number + ) + ); + println!( + "{:<40} {:<40} {:<40}", + "root_key_id", + hex::encode(&t.root_key_id[..24]) + "...", + hex::encode(&exp_root_key_id[..24]) + "..." + ); + println!( + "{:<40} {:<40} {:<40}", + "ppid", + hex::encode(&t.ppid), + hex::encode(&pck_ext_tdx.ppid) + ); + println!( + "{:<40} {:<40} {:<40}", + "cpu_svn", + hex::encode(t.cpu_svn), + hex::encode(pck_ext_tdx.cpu_svn) + ); + println!( + "{:<40} {:<40} {:<40}", + "pce_svn", + format!("{}", t.pce_svn), + format!("{}", pck_ext_tdx.pce_svn) + ); + println!( + "{:<40} {:<40} {:<40}", + "pce_id", + format!("{}", t.pce_id), + format!("{}", exp_pce_id_t) + ); + println!( + "{:<40} {:<40} {:<40}", + "fmspc", + hex::encode(t.fmspc), + hex::encode(pck_ext_tdx.fmspc) + ); + println!( + "{:<40} {:<40} {:<40}", + "tee_type", + format!("0x{:08X}", t.tee_type), + "0x00000081 (TDX)" + ); + println!( + "{:<40} {:<40} {:<40}", + "sgx_type", + format!("{}", t.sgx_type), + format!("{}", pck_ext_tdx.sgx_type) + ); + println!( + "{:<40} {:<40} {:<40}", + "platform_instance_id", + format!("{:?}", t.platform_instance_id), + format!("{:?}", pck_ext_tdx.platform_instance_id) + ); + println!( + "{:<40} {:<40} {:<40}", + "dynamic_platform", + format!("{:?}", t.dynamic_platform), + format!("{:?}", pck_ext_tdx.dynamic_platform) + ); + println!( + "{:<40} {:<40} {:<40}", + "cached_keys", + format!("{:?}", t.cached_keys), + format!("{:?}", pck_ext_tdx.cached_keys) + ); + println!( + "{:<40} {:<40} {:<40}", + "smt_enabled", + format!("{:?}", t.smt_enabled), + format!("{:?}", pck_ext_tdx.smt_enabled) + ); + println!( + "{:<40} {:<40}", + "platform_tcb_level.tcb_date", &t.platform_tcb_level.tcb_date + ); + println!( + "{:<40} {:<40}", + "platform_tcb_level.tcb_status", + format!("{:?}", t.platform_tcb_level.tcb_status) + ); + println!( + "{:<40} {:<40}", + "qe_tcb_level.tcb_date", &t.qe_tcb_level.tcb_date + ); + println!( + "{:<40} {:<40}", + "qe_tcb_level.tcb_status", + format!("{:?}", t.qe_tcb_level.tcb_status) + ); +} + +/// Cross-validate TDX supplemental data fields. +#[test] +fn tdx_supplemental_data_cross_validation() { + use dcap_qvl::verify::{ring, QuoteVerifier}; + + let raw_quote = include_bytes!("../sample/tdx_quote"); + let collateral: QuoteCollateralV3 = + serde_json::from_slice(include_bytes!("../sample/tdx_quote_collateral.json")).unwrap(); + let now = now_from_collateral(&collateral); + + let verifier = QuoteVerifier::new_prod(ring::backend()); + let result = verifier.verify(raw_quote, &collateral, now).unwrap(); + let s = &result.supplemental; + + // TDX quote should have tee_type = 0x81 + assert_eq!(s.tee_type, 0x00000081); + assert_eq!(s.tcb_status.to_string(), "UpToDate"); + assert!(s.advisory_ids.is_empty()); + + // Time window fields should be populated + assert!(s.earliest_issue_date > 0); + assert!(s.latest_issue_date >= s.earliest_issue_date); + assert!(s.earliest_expiration_date > s.latest_issue_date); + assert!(s.tcb_level_date_tag > 0); + + // root_key_id should match SHA-384 of Intel root CA raw public key bytes + let root_ca_der = include_bytes!("../src/TrustedRootCA.der"); + let root_cert: x509_cert::Certificate = DerDecode::from_der(root_ca_der).unwrap(); + let raw_pub_key = root_cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(); + let expected_root_key_id: [u8; 48] = { + use sha2::Digest; + sha2::Sha384::digest(raw_pub_key).into() + }; + assert_eq!(s.root_key_id, expected_root_key_id); + + // Verify ring == rustcrypto for all fields + let rc_verifier = QuoteVerifier::new_prod(dcap_qvl::verify::rustcrypto::backend()); + let rc_result = rc_verifier.verify(raw_quote, &collateral, now).unwrap(); + let rc = &rc_result.supplemental; + assert_eq!(s.tee_type, rc.tee_type); + assert_eq!(s.tcb_status, rc.tcb_status); + assert_eq!(s.root_key_id, rc.root_key_id); + assert_eq!(s.earliest_issue_date, rc.earliest_issue_date); + assert_eq!(s.tcb_eval_data_number, rc.tcb_eval_data_number); +} From a74b476cf0177d7346dbdc2bf98c835e2636e4e6 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Feb 2026 10:13:52 +0000 Subject: [PATCH 02/33] WIP: policy branch checkpoint --- src/policy.rs | 17 +++++++++++++++++ src/verify.rs | 13 ++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/policy.rs b/src/policy.rs index 3fec9fb..25cd644 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -449,6 +449,12 @@ pub struct SupplementalData { /// Corresponds to Intel's `smt_enabled`. pub smt_enabled: PckCertFlag, + /// Platform Provider ID from the PCK certificate. + /// Only present for Multi-Package platforms (Platform CA certs). + /// Used by Rego's `platform_provider_id_ok` check against + /// `policy.reference.accepted_platform_provider_ids`. + pub platform_provider_id: Option, + // ── Full TCB level details ────────────────────────────────────────── /// The matched platform TCB level (includes `tcb_date`, `tcb_status`, `advisory_ids`). pub platform_tcb_level: TcbLevel, @@ -563,6 +569,11 @@ pub(crate) mod rego_policy { ); } + // Platform provider ID (only emit if present) + if let Some(ref provider_id) = self.platform_provider_id { + m.insert("platform_provider_id".into(), json!(provider_id)); + } + // Advisory IDs if !self.advisory_ids.is_empty() { m.insert("advisory_ids".into(), json!(self.advisory_ids)); @@ -636,6 +647,11 @@ pub(crate) mod rego_policy { ); } + // Platform provider ID (only emit if present) + if let Some(ref provider_id) = self.platform_provider_id { + m.insert("platform_provider_id".into(), json!(provider_id)); + } + // Advisory IDs from platform (unmerged) if !self.platform_tcb_level.advisory_ids.is_empty() { m.insert( @@ -1122,6 +1138,7 @@ mod tests { dynamic_platform: PckCertFlag::Undefined, cached_keys: PckCertFlag::Undefined, smt_enabled: PckCertFlag::Undefined, + platform_provider_id: None, platform_tcb_level: TcbLevel { tcb: Tcb { sgx_components: vec![TcbComponents { svn: 0 }; 16], diff --git a/src/verify.rs b/src/verify.rs index 55a1c0b..68cb230 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -160,9 +160,19 @@ impl QuoteVerificationResult { // 3. Tenant measurement (enclave or TD report) let tenant_cid = tenant_class_id(&self.report); + let mut tenant_m = tenant_measurement(&self.report); + // For SGX enclave, add sgx_ce_attributes from the QE report + if let Report::SgxEnclave(_) = &self.report { + if let Some(obj) = tenant_m.as_object_mut() { + obj.insert( + "sgx_ce_attributes".into(), + serde_json::json!(hex::encode_upper(self.supplemental.qe_report.attributes)), + ); + } + } result.push(serde_json::json!({ "environment": { "class_id": tenant_cid }, - "measurement": tenant_measurement(&self.report), + "measurement": tenant_m, })); result @@ -808,6 +818,7 @@ fn verify_impl( dynamic_platform: pck_result.dynamic_platform, cached_keys: pck_result.cached_keys, smt_enabled: pck_result.smt_enabled, + platform_provider_id: None, // not yet extracted from PCK cert platform_tcb_level, qe_tcb_level, qe_report, From 17beb8c6bdcff2e83b7824721498dfb80c65963b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 5 Mar 2026 09:24:34 +0000 Subject: [PATCH 03/33] feat: lazy SupplementalData for gas-efficient verify() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SupplementalData is no longer built during verify() — it's computed on demand via result.supplemental() or internally by validate(). This keeps the verify() path minimal (crypto only), saving ~2 TGas on NEAR (175 TGas, matching pre-policy baseline). - QuoteVerificationResult stores verification intermediates privately - supplemental() method builds the full struct lazily (root_key_id, CRL numbers, tcb_date_tag, earliest_expiration) - into_report() works without ever constructing SupplementalData - Rego path calls supplemental() internally when needed --- cli/src/main.rs | 10 +- src/collateral.rs | 2 +- src/lib.rs | 7 +- src/policy.rs | 908 ++++++++++------------- src/python.rs | 4 +- src/verify.rs | 313 +++++--- tests/near/contracts/gas-test/src/lib.rs | 2 +- tests/verify_quote.rs | 783 +++---------------- 8 files changed, 732 insertions(+), 1297 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 59b629e..ceae928 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -9,7 +9,8 @@ use clap::{Args, Parser, Subcommand}; use dcap_qvl::collateral::{get_collateral, PHALA_PCCS_URL}; use dcap_qvl::intel; use dcap_qvl::quote::Quote; -use dcap_qvl::verify::verify; +use dcap_qvl::verify::{ring, QuoteVerifier}; +use dcap_qvl::QuotePolicy; use der::Decode; use serde::Serialize; use x509_cert::Certificate; @@ -103,7 +104,12 @@ async fn command_verify_quote(args: VerifyQuoteArgs) -> Result<()> { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs(); - let report = verify("e, &collateral, now).context("Failed to verify quote")?; + let verifier = QuoteVerifier::new_prod(ring::backend()); + let result = verifier + .verify("e, collateral, now) + .context("Failed to verify quote")?; + let report = result + .into_report(); println!( "{}", serde_json::to_string(&report).context("Failed to serialize report")? diff --git a/src/collateral.rs b/src/collateral.rs index 4e049bd..9ee13ea 100644 --- a/src/collateral.rs +++ b/src/collateral.rs @@ -446,7 +446,7 @@ pub async fn get_collateral_and_verify( .duration_since(SystemTime::UNIX_EPOCH) .context("Failed to get current time")? .as_secs(); - QuoteVerifier::new_prod_default_crypto().verify(quote, &collateral, now) + QuoteVerifier::new_prod_default_crypto().verify(quote, collateral, now) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 42e03de..3a787e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ //! //! let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); //! let verifier = QuoteVerifier::new_prod(ring::backend()); -//! let result = verifier.verify("e, &collateral, now).expect("verification failed"); +//! let result = verifier.verify("e, collateral, now).expect("verification failed"); //! let report = result.validate(&QuotePolicy::strict(now)).expect("policy validation failed"); //! println!("{:?}", report); //! } @@ -96,7 +96,10 @@ pub use constants::{CpuSvn, Fmspc, MrEnclave, MrSigner, Svn}; // Re-export commonly used types pub use qe_identity::{QeIdentity, QeTcb, QeTcbLevel}; pub use tcb_info::{Tcb, TcbComponents, TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory}; -pub use policy::{PckCertFlag, Policy, QuotePolicy, SupplementalData}; +pub use policy::{ + PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, QuotePolicy, SupplementalData, + TcbVerdict, +}; pub use verify::QuoteVerificationResult; #[cfg(feature = "rego")] diff --git a/src/policy.rs b/src/policy.rs index 25cd644..d44627a 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -207,15 +207,15 @@ impl QuotePolicy { impl Policy for QuotePolicy { fn validate(&self, data: &SupplementalData) -> Result<()> { // 1. TCB status whitelist - if !self.is_status_acceptable(data.tcb_status) { + if !self.is_status_acceptable(data.tcb.status) { bail!( "TCB status {:?} is not acceptable by policy", - data.tcb_status + data.tcb.status ); } // 2. Advisory ID whitelist - for id in &data.advisory_ids { + for id in &data.tcb.advisory_ids { if !self .accepted_advisory_ids .iter() @@ -230,41 +230,38 @@ impl Policy for QuotePolicy { bail!("collateral_grace_period and platform_grace_period are mutually exclusive"); } - // 3. Collateral expiration: earliest_expiration_date + grace >= now - // Always checked. With grace=0, this only rejects if collateral is already expired - // (which verify() already enforces, so this catches offline/delayed validation). + // 3. Collateral expiration: earliest_expiration + grace >= now if data - .earliest_expiration_date + .tcb + .earliest_expiration .saturating_add(self.collateral_grace_period) < self.now { bail!( - "Collateral expired: earliest_expiration_date {} + grace {} < now {}", - data.earliest_expiration_date, + "Collateral expired: earliest_expiration {} + grace {} < now {}", + data.tcb.earliest_expiration, self.collateral_grace_period, self.now ); } - // 4. Platform TCB freshness: tcb_level_date_tag + grace >= now + // 4. Platform TCB freshness: tcb_date_tag + grace >= now // Only checked when TCB status indicates the platform is out-of-date. - // For "good" statuses (UpToDate, ConfigurationNeeded, SWHardeningNeeded), - // tcb_level_date_tag is always in the past and irrelevant. - // Matches Intel Rego: skip for UpToDate/ConfigNeeded/SWHardening. { let is_out_of_date = matches!( - data.tcb_status, + data.tcb.status, TcbStatus::OutOfDate | TcbStatus::OutOfDateConfigurationNeeded ); if is_out_of_date && data - .tcb_level_date_tag + .platform + .tcb_date_tag .saturating_add(self.platform_grace_period) < self.now { bail!( - "Platform TCB too old: tcb_level_date_tag {} + grace {} < now {}", - data.tcb_level_date_tag, + "Platform TCB too old: tcb_date_tag {} + grace {} < now {}", + data.platform.tcb_date_tag, self.platform_grace_period, self.now ); @@ -273,36 +270,36 @@ impl Policy for QuotePolicy { // 5. Minimum TCB evaluation data number if let Some(min) = self.min_tcb_eval_data_number { - if data.tcb_eval_data_number < min { + if data.tcb.eval_data_number < min { bail!( "TCB eval data number {} is below minimum {}", - data.tcb_eval_data_number, + data.tcb.eval_data_number, min ); } } // 6. Dynamic platform flag - if !self.allow_dynamic_platform && data.dynamic_platform == PckCertFlag::True { + if !self.allow_dynamic_platform && data.platform.pck.dynamic_platform == PckCertFlag::True { bail!("Dynamic platform is not allowed by policy"); } // 7. Cached keys flag - if !self.allow_cached_keys && data.cached_keys == PckCertFlag::True { + if !self.allow_cached_keys && data.platform.pck.cached_keys == PckCertFlag::True { bail!("Cached keys are not allowed by policy"); } // 8. SMT flag - if !self.allow_smt && data.smt_enabled == PckCertFlag::True { + if !self.allow_smt && data.platform.pck.smt_enabled == PckCertFlag::True { bail!("SMT (hyperthreading) is not allowed by policy"); } // 9. SGX type whitelist if let Some(ref types) = self.accepted_sgx_types { - if !types.contains(&data.sgx_type) { + if !types.contains(&data.platform.pck.sgx_type) { bail!( "SGX type {} is not in accepted types {:?}", - data.sgx_type, + data.platform.pck.sgx_type, types ); } @@ -336,140 +333,90 @@ impl From> for PckCertFlag { } } -/// Supplemental data from quote verification, analogous to Intel's `sgx_ql_qv_supplemental_t`. +/// Supplemental data from quote verification. /// -/// Contains all information needed for policy decisions. This data is publicly -/// accessible for inspection, but the enclave report is only released after -/// passing a [`Policy`] via [`QuoteVerificationResult::validate()`]. -/// -/// Field names and semantics follow Intel's official QVL supplemental data structure. +/// Organized into structured sub-groups: +/// - [`tcb`](Self::tcb): Merged TCB verdict +/// - [`platform`](Self::platform): Platform-level details from PCK certificate and TCB matching +/// - [`qe`](Self::qe): QE (Quoting Enclave) verification results pub struct SupplementalData { - // ── Merged TCB result ─────────────────────────────────────────────── - /// Merged TCB status (worst of platform TCB + QE TCB). - pub tcb_status: TcbStatus, + /// TEE type: `0x00000000` for SGX, `0x00000081` for TDX. + pub tee_type: u32, + /// Merged TCB verdict (worst of platform + QE). + pub tcb: TcbVerdict, + /// Platform verification details. + pub platform: PlatformInfo, + /// QE verification details. + pub qe: QeInfo, +} +/// Merged TCB verdict from platform and QE status convergence. +/// +/// Uses Intel's `convergeTcbStatusWithQeTcbStatus` logic to produce the +/// worst-case status and union of advisory IDs. +pub struct TcbVerdict { + /// Merged TCB status (worst of platform TCB + QE TCB). + pub status: TcbStatus, /// Merged advisory IDs (union of platform + QE advisories). - /// Comma-separated list in Intel's struct (`sa_list`). pub advisory_ids: Vec, + /// Lower of TCBInfo and QEIdentity `tcbEvaluationDataNumber` values. + pub eval_data_number: u32, + /// Earliest expiration from TCBInfo nextUpdate, QEIdentity nextUpdate, + /// and CRL nextUpdate (4 sources). Does **not** include certificate chain + /// notAfter — use full time window via Rego for that. + pub earliest_expiration: u64, +} - // ── Collateral time window ────────────────────────────────────────── - /// Earliest issue date across **all** collateral pieces (UTC, unix seconds). - /// Corresponds to Intel's `earliest_issue_date`. - pub earliest_issue_date: u64, - - /// Latest issue date across **all** collateral pieces (UTC, unix seconds). - /// Corresponds to Intel's `latest_issue_date`. - pub latest_issue_date: u64, - - /// Earliest expiration date across **all** collateral pieces (UTC, unix seconds). - /// Corresponds to Intel's `earliest_expiration_date`. - pub earliest_expiration_date: u64, - - /// The SGX platform's TCB level date tag (UTC, unix seconds). - /// The platform is not vulnerable to any Security Advisories with an SGX TCB - /// impact released on or before this date. - /// Corresponds to Intel's `tcb_level_date_tag`. - pub tcb_level_date_tag: u64, - - // ── CRL information ───────────────────────────────────────────────── - /// CRL number from the PCK Certificate Revocation List. - /// Corresponds to Intel's `pck_crl_num`. +/// Platform-level verification results. +pub struct PlatformInfo { + /// The matched platform TCB level (unmerged). + pub tcb_level: TcbLevel, + /// Platform TCB level date as unix timestamp (precomputed from `tcb_level.tcb_date`). + pub tcb_date_tag: u64, + /// PCK certificate identity fields. + pub pck: PckIdentity, + /// SHA-384 of root CA's raw public key bytes, matching Intel's `root_key_id`. + pub root_key_id: [u8; 48], + /// CRL number from PCK Certificate Revocation List. pub pck_crl_num: u32, - - /// CRL number from the Root CA Certificate Revocation List. - /// Corresponds to Intel's `root_ca_crl_num`. + /// CRL number from Root CA Certificate Revocation List. pub root_ca_crl_num: u32, +} - // ── TCB evaluation ────────────────────────────────────────────────── - /// Lower of the TCBInfo and QEIdentity `tcbEvaluationDataNumber` values. - /// Corresponds to Intel's `tcb_eval_ref_num`. +/// QE (Quoting Enclave) verification results. +pub struct QeInfo { + /// The matched QE TCB level (unmerged). + pub tcb_level: QeTcbLevel, + /// The QE's enclave report. + pub report: EnclaveReport, + /// TCB evaluation data number from QE Identity (unmerged). pub tcb_eval_data_number: u32, +} - // ── Root of trust ─────────────────────────────────────────────────── - /// ID of the collateral's root signer: SHA-384 hash of the Root CA's - /// raw public key bytes (BIT STRING content from SubjectPublicKeyInfo). - /// Corresponds to Intel's `root_key_id`. - pub root_key_id: [u8; 48], - - // ── Platform identity from PCK certificate ────────────────────────── - /// Platform Provisioning ID (PPID) from the PCK certificate. - /// Can be used for platform ownership checks. - /// Corresponds to Intel's `pck_ppid`. +/// PCK certificate identity fields. +pub struct PckIdentity { + /// Platform Provisioning ID (PPID). pub ppid: Vec, - - /// CPU Security Version Number from the PCK certificate (16 bytes). - /// Corresponds to Intel's `tcb_cpusvn`. + /// CPU Security Version Number (16 bytes). pub cpu_svn: CpuSvn, - - /// PCE ISV Security Version Number from the PCK certificate. - /// Corresponds to Intel's `tcb_pce_isvsvn`. + /// PCE ISV Security Version Number. pub pce_svn: Svn, - - /// PCE ID of the remote platform. - /// Corresponds to Intel's `pce_id`. + /// PCE ID. pub pce_id: u16, - - /// FMSPC — Firmware Security Version & Package Configuration (6 bytes) - /// from the PCK certificate. Not directly in Intel's supplemental struct - /// but essential for TCB level matching. + /// FMSPC (6 bytes). pub fmspc: Fmspc, - - // ── TEE and SGX type ──────────────────────────────────────────────── - /// TEE type: `0x00000000` for SGX, `0x00000081` for TDX. - /// Corresponds to Intel's `tee_type`. - pub tee_type: u32, - - /// SGX memory protection type from the PCK certificate: - /// - 0 = Standard - /// - 1 = Scalable - /// - 2 = Scalable with Integrity - /// - /// Corresponds to Intel's `sgx_type`. + /// SGX type: 0=Standard, 1=Scalable, 2=ScalableWithIntegrity. pub sgx_type: u8, - - // ── Platform instance (Platform CA certs only) ────────────────────── - /// Platform Instance ID (16 bytes). Only present for Multi-Package - /// platforms (PCK certificates issued by Platform CA). - /// Corresponds to Intel's `platform_instance_id`. + /// Platform Instance ID (16 bytes, Platform CA only). pub platform_instance_id: Option<[u8; 16]>, - - /// Whether the platform can be extended with additional packages - /// via Package Add calls to SGX Registration Backend. - /// Only relevant to PCK certificates issued by Platform CA. - /// Corresponds to Intel's `dynamic_platform`. + /// Dynamic platform flag. pub dynamic_platform: PckCertFlag, - - /// Whether platform root keys are cached by SGX Registration Backend. - /// Only relevant to PCK certificates issued by Platform CA. - /// Corresponds to Intel's `cached_keys`. + /// Cached keys flag. pub cached_keys: PckCertFlag, - - /// Whether the platform has SMT (simultaneous multithreading / hyperthreading) - /// enabled. Only relevant to PCK certificates issued by Platform CA. - /// Corresponds to Intel's `smt_enabled`. + /// SMT (hyperthreading) flag. pub smt_enabled: PckCertFlag, - - /// Platform Provider ID from the PCK certificate. - /// Only present for Multi-Package platforms (Platform CA certs). - /// Used by Rego's `platform_provider_id_ok` check against - /// `policy.reference.accepted_platform_provider_ids`. + /// Platform Provider ID (Platform CA only, for Rego). pub platform_provider_id: Option, - - // ── Full TCB level details ────────────────────────────────────────── - /// The matched platform TCB level (includes `tcb_date`, `tcb_status`, `advisory_ids`). - pub platform_tcb_level: TcbLevel, - - /// The matched QE TCB level (includes `tcb_date`, `tcb_status`, `advisory_ids`). - pub qe_tcb_level: QeTcbLevel, - - // ── QE report (for multi-measurement Rego) ──────────────────────────── - /// The QE's enclave report. Needed for QE Identity Rego measurement (TDX) - /// and available for inspection. - pub qe_report: EnclaveReport, - - /// TCB evaluation data number from QE Identity (unmerged). - /// `tcb_eval_data_number` is min(TCBInfo, QEIdentity); this is the QE-specific value. - pub qe_tcb_eval_data_number: u32, } // ============================================================================= @@ -508,214 +455,150 @@ pub(crate) mod rego_policy { } } - impl SupplementalData { - /// Convert to the JSON `measurement` object that Intel's `qal_script.rego` expects. - /// - /// This matches the JSON construction in Intel's `qve.cpp` (lines 2135-2216). - pub fn to_rego_measurement(&self) -> serde_json::Value { - let mut m = serde_json::Map::new(); - - // tcb_status: array of strings - m.insert( - "tcb_status".into(), - tcb_status_to_rego_array(self.tcb_status), - ); - - // Time fields as RFC3339 strings - let earliest_issue = unix_to_rfc3339(self.earliest_issue_date); - if !earliest_issue.is_empty() { - m.insert("earliest_issue_date".into(), json!(earliest_issue)); - } - let latest_issue = unix_to_rfc3339(self.latest_issue_date); - if !latest_issue.is_empty() { - m.insert("latest_issue_date".into(), json!(latest_issue)); - } - let earliest_exp = unix_to_rfc3339(self.earliest_expiration_date); - if !earliest_exp.is_empty() { - m.insert("earliest_expiration_date".into(), json!(earliest_exp)); - } - let tcb_date = unix_to_rfc3339(self.tcb_level_date_tag); - if !tcb_date.is_empty() { - m.insert("tcb_level_date_tag".into(), json!(tcb_date)); - } - - // CRL numbers - m.insert("pck_crl_num".into(), json!(self.pck_crl_num)); - m.insert("root_ca_crl_num".into(), json!(self.root_ca_crl_num)); - - // TCB eval number - m.insert("tcb_eval_num".into(), json!(self.tcb_eval_data_number)); - - // SGX type (note: Rego reads "sgx_type", Intel C++ writes "sgx_types") - m.insert("sgx_type".into(), json!(self.sgx_type)); - - // Platform flags: only emit if not Undefined (matching Intel C++) - if self.dynamic_platform != PckCertFlag::Undefined { - m.insert( - "is_dynamic_platform".into(), - json!(self.dynamic_platform == PckCertFlag::True), - ); - } - if self.cached_keys != PckCertFlag::Undefined { - m.insert( - "cached_keys".into(), - json!(self.cached_keys == PckCertFlag::True), - ); - } - if self.smt_enabled != PckCertFlag::Undefined { - m.insert( - "smt_enabled".into(), - json!(self.smt_enabled == PckCertFlag::True), - ); - } - - // Platform provider ID (only emit if present) - if let Some(ref provider_id) = self.platform_provider_id { - m.insert("platform_provider_id".into(), json!(provider_id)); - } + /// Full collateral time window (expensive to compute, only for Rego). + pub(crate) struct CollateralTimeWindow { + pub earliest_issue_date: u64, + pub latest_issue_date: u64, + pub earliest_expiration_date: u64, + } - // Advisory IDs - if !self.advisory_ids.is_empty() { - m.insert("advisory_ids".into(), json!(self.advisory_ids)); - } + /// Build common platform fields into a Rego measurement JSON map. + fn insert_platform_fields( + m: &mut serde_json::Map, + data: &SupplementalData, + tw: &CollateralTimeWindow, + ) { + // Time fields as RFC3339 strings + let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); + if !earliest_issue.is_empty() { + m.insert("earliest_issue_date".into(), json!(earliest_issue)); + } + let latest_issue = unix_to_rfc3339(tw.latest_issue_date); + if !latest_issue.is_empty() { + m.insert("latest_issue_date".into(), json!(latest_issue)); + } + let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); + if !earliest_exp.is_empty() { + m.insert("earliest_expiration_date".into(), json!(earliest_exp)); + } + let tcb_date = unix_to_rfc3339(data.platform.tcb_date_tag); + if !tcb_date.is_empty() { + m.insert("tcb_level_date_tag".into(), json!(tcb_date)); + } - // FMSPC as hex uppercase string - m.insert("fmspc".into(), json!(hex::encode_upper(self.fmspc))); + m.insert("pck_crl_num".into(), json!(data.platform.pck_crl_num)); + m.insert("root_ca_crl_num".into(), json!(data.platform.root_ca_crl_num)); + m.insert("tcb_eval_num".into(), json!(data.tcb.eval_data_number)); + m.insert("sgx_type".into(), json!(data.platform.pck.sgx_type)); - // Root key ID as hex uppercase string + if data.platform.pck.dynamic_platform != PckCertFlag::Undefined { m.insert( - "root_key_id".into(), - json!(hex::encode_upper(self.root_key_id)), + "is_dynamic_platform".into(), + json!(data.platform.pck.dynamic_platform == PckCertFlag::True), ); - - serde_json::Value::Object(m) } - - /// Generate platform TCB measurement JSON using **unmerged** platform status. - /// - /// Unlike [`to_rego_measurement()`] which uses the merged `tcb_status`, - /// this uses `platform_tcb_level.tcb_status` and `platform_tcb_level.advisory_ids`, - /// suitable for multi-measurement Rego input alongside QE and tenant measurements. - pub fn to_platform_rego_measurement(&self) -> serde_json::Value { - let mut m = serde_json::Map::new(); - - // tcb_status from platform (unmerged) + if data.platform.pck.cached_keys != PckCertFlag::Undefined { m.insert( - "tcb_status".into(), - tcb_status_to_rego_array(self.platform_tcb_level.tcb_status), + "cached_keys".into(), + json!(data.platform.pck.cached_keys == PckCertFlag::True), ); - - // Time fields as RFC3339 strings - let earliest_issue = unix_to_rfc3339(self.earliest_issue_date); - if !earliest_issue.is_empty() { - m.insert("earliest_issue_date".into(), json!(earliest_issue)); - } - let latest_issue = unix_to_rfc3339(self.latest_issue_date); - if !latest_issue.is_empty() { - m.insert("latest_issue_date".into(), json!(latest_issue)); - } - let earliest_exp = unix_to_rfc3339(self.earliest_expiration_date); - if !earliest_exp.is_empty() { - m.insert("earliest_expiration_date".into(), json!(earliest_exp)); - } - let tcb_date = unix_to_rfc3339(self.tcb_level_date_tag); - if !tcb_date.is_empty() { - m.insert("tcb_level_date_tag".into(), json!(tcb_date)); - } - - m.insert("pck_crl_num".into(), json!(self.pck_crl_num)); - m.insert("root_ca_crl_num".into(), json!(self.root_ca_crl_num)); - m.insert("tcb_eval_num".into(), json!(self.tcb_eval_data_number)); - m.insert("sgx_type".into(), json!(self.sgx_type)); - - if self.dynamic_platform != PckCertFlag::Undefined { - m.insert( - "is_dynamic_platform".into(), - json!(self.dynamic_platform == PckCertFlag::True), - ); - } - if self.cached_keys != PckCertFlag::Undefined { - m.insert( - "cached_keys".into(), - json!(self.cached_keys == PckCertFlag::True), - ); - } - if self.smt_enabled != PckCertFlag::Undefined { - m.insert( - "smt_enabled".into(), - json!(self.smt_enabled == PckCertFlag::True), - ); - } - - // Platform provider ID (only emit if present) - if let Some(ref provider_id) = self.platform_provider_id { - m.insert("platform_provider_id".into(), json!(provider_id)); - } - - // Advisory IDs from platform (unmerged) - if !self.platform_tcb_level.advisory_ids.is_empty() { - m.insert( - "advisory_ids".into(), - json!(self.platform_tcb_level.advisory_ids), - ); - } - - m.insert("fmspc".into(), json!(hex::encode_upper(self.fmspc))); + } + if data.platform.pck.smt_enabled != PckCertFlag::Undefined { m.insert( - "root_key_id".into(), - json!(hex::encode_upper(self.root_key_id)), + "smt_enabled".into(), + json!(data.platform.pck.smt_enabled == PckCertFlag::True), ); + } - serde_json::Value::Object(m) + if let Some(ref provider_id) = data.platform.pck.platform_provider_id { + m.insert("platform_provider_id".into(), json!(provider_id)); } - /// Generate QE Identity measurement JSON for Rego appraisal. - /// - /// Uses the unmerged QE TCB level data. Only meaningful for TDX quotes - /// (SGX does not have a QE Identity qvl_result in Intel's format). - pub fn to_qe_rego_measurement(&self) -> serde_json::Value { - let mut m = serde_json::Map::new(); + m.insert("fmspc".into(), json!(hex::encode_upper(data.platform.pck.fmspc))); + m.insert( + "root_key_id".into(), + json!(hex::encode_upper(data.platform.root_key_id)), + ); + } + + /// Build merged Rego measurement (single-measurement path). + pub(crate) fn build_merged_measurement( + data: &SupplementalData, + tw: &CollateralTimeWindow, + ) -> serde_json::Value { + let mut m = serde_json::Map::new(); + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(data.tcb.status), + ); + insert_platform_fields(&mut m, data, tw); + if !data.tcb.advisory_ids.is_empty() { + m.insert("advisory_ids".into(), json!(data.tcb.advisory_ids)); + } + serde_json::Value::Object(m) + } - // tcb_status from QE (unmerged) + /// Build platform TCB measurement using **unmerged** platform status. + pub(crate) fn build_platform_measurement( + data: &SupplementalData, + tw: &CollateralTimeWindow, + ) -> serde_json::Value { + let mut m = serde_json::Map::new(); + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(data.platform.tcb_level.tcb_status), + ); + insert_platform_fields(&mut m, data, tw); + if !data.platform.tcb_level.advisory_ids.is_empty() { m.insert( - "tcb_status".into(), - tcb_status_to_rego_array(self.qe_tcb_level.tcb_status), + "advisory_ids".into(), + json!(data.platform.tcb_level.advisory_ids), ); + } + serde_json::Value::Object(m) + } - // tcb_level_date_tag from QE TCB level's tcb_date - let qe_tcb_date = chrono::DateTime::parse_from_rfc3339(&self.qe_tcb_level.tcb_date) - .ok() - .map(|dt| dt.timestamp() as u64) - .unwrap_or(0); - let qe_date_str = unix_to_rfc3339(qe_tcb_date); - if !qe_date_str.is_empty() { - m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); - } - - // Time window fields (collateral-wide, same as platform) - let earliest_issue = unix_to_rfc3339(self.earliest_issue_date); - if !earliest_issue.is_empty() { - m.insert("earliest_issue_date".into(), json!(earliest_issue)); - } - let latest_issue = unix_to_rfc3339(self.latest_issue_date); - if !latest_issue.is_empty() { - m.insert("latest_issue_date".into(), json!(latest_issue)); - } - let earliest_exp = unix_to_rfc3339(self.earliest_expiration_date); - if !earliest_exp.is_empty() { - m.insert("earliest_expiration_date".into(), json!(earliest_exp)); - } + /// Build QE Identity measurement for Rego appraisal (TDX). + pub(crate) fn build_qe_measurement( + data: &SupplementalData, + tw: &CollateralTimeWindow, + ) -> serde_json::Value { + let mut m = serde_json::Map::new(); - // QE-specific tcb_eval_num - m.insert("tcb_eval_num".into(), json!(self.qe_tcb_eval_data_number)); + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(data.qe.tcb_level.tcb_status), + ); - m.insert( - "root_key_id".into(), - json!(hex::encode_upper(self.root_key_id)), - ); + let qe_tcb_date = chrono::DateTime::parse_from_rfc3339(&data.qe.tcb_level.tcb_date) + .ok() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(0); + let qe_date_str = unix_to_rfc3339(qe_tcb_date); + if !qe_date_str.is_empty() { + m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); + } - serde_json::Value::Object(m) + let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); + if !earliest_issue.is_empty() { + m.insert("earliest_issue_date".into(), json!(earliest_issue)); + } + let latest_issue = unix_to_rfc3339(tw.latest_issue_date); + if !latest_issue.is_empty() { + m.insert("latest_issue_date".into(), json!(latest_issue)); + } + let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); + if !earliest_exp.is_empty() { + m.insert("earliest_expiration_date".into(), json!(earliest_exp)); } + + m.insert("tcb_eval_num".into(), json!(data.qe.tcb_eval_data_number)); + m.insert( + "root_key_id".into(), + json!(hex::encode_upper(data.platform.root_key_id)), + ); + + serde_json::Value::Object(m) } // ── Tenant measurement helpers ───────────────────────────────────────── @@ -936,44 +819,54 @@ pub(crate) mod rego_policy { /// Evaluate the Rego engine with the given qvl_result entries. pub(crate) fn eval_rego(&self, qvl_result: Vec) -> Result<()> { - let mut engine = self.engine.clone(); - - let input = json!({ - "qvl_result": qvl_result, - "policies": { - "policy_array": &self.policies, - } - }); + let policy_refs: Vec<&serde_json::Value> = self.policies.iter().collect(); + eval_rego_engine(&self.engine, &policy_refs, qvl_result) + } + } - let input_str = serde_json::to_string(&input) - .map_err(|e| anyhow::anyhow!("Failed to serialize Rego input: {e}"))?; - engine - .set_input_json(&input_str) - .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; - - let result = engine - .eval_rule("data.dcap.quote.appraisal.final_ret".into()) - .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; - - let result_json = result - .to_json_str() - .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; - - match result_json.trim() { - "1" => Ok(()), - "0" => { - let detail = engine - .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) - .ok() - .and_then(|v| v.to_json_str().ok()); - if let Some(detail) = detail { - bail!("Rego appraisal failed: {detail}"); - } - bail!("Rego appraisal failed (result = 0)"); + /// Shared Rego evaluation logic used by both `RegoPolicy` and `RegoPolicySet`. + fn eval_rego_engine( + engine: ®orus::Engine, + policies: &[&serde_json::Value], + qvl_result: Vec, + ) -> Result<()> { + let mut engine = engine.clone(); + + let input = json!({ + "qvl_result": qvl_result, + "policies": { + "policy_array": policies, + } + }); + + let input_str = serde_json::to_string(&input) + .map_err(|e| anyhow::anyhow!("Failed to serialize Rego input: {e}"))?; + engine + .set_input_json(&input_str) + .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; + + let result = engine + .eval_rule("data.dcap.quote.appraisal.final_ret".into()) + .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; + + let result_json = result + .to_json_str() + .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; + + match result_json.trim() { + "1" => Ok(()), + "0" => { + let detail = engine + .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) + .ok() + .and_then(|v| v.to_json_str().ok()); + if let Some(detail) = detail { + bail!("Rego appraisal failed: {detail}"); } - "-1" => bail!("No policy matched the report class_id"), - other => bail!("Unexpected Rego appraisal result: {other}"), + bail!("Rego appraisal failed (result = 0)"); } + "-1" => bail!("No policy matched the report class_id"), + other => bail!("Unexpected Rego appraisal result: {other}"), } } @@ -1044,55 +937,19 @@ pub(crate) mod rego_policy { } } - impl Policy for RegoPolicy { - fn validate(&self, data: &SupplementalData) -> Result<()> { - let mut engine = self.engine.clone(); - - // Build the Rego input matching Intel's QAL format - let measurement = data.to_rego_measurement(); - let input = json!({ - "qvl_result": [{ - "environment": { "class_id": &self.class_id }, - "measurement": measurement, - }], - "policies": { - "policy_array": [&self.policy_json] - } - }); - - let input_str = serde_json::to_string(&input) - .map_err(|e| anyhow::anyhow!("Failed to serialize Rego input: {e}"))?; - engine - .set_input_json(&input_str) - .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; - - // Evaluate `final_ret` directly (1=pass, 0=fail, -1=no policy). - // We avoid `final_appraisal_result` which uses `rand.intn` (not available - // in regorus) and `time.now_ns` for nonce/timestamp decorating. - let result = engine - .eval_rule("data.dcap.quote.appraisal.final_ret".into()) - .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; - - let result_json = result - .to_json_str() - .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; - - match result_json.trim() { - "1" => Ok(()), - "0" => { - // Try to get detailed sub-check results for the error message - let detail = engine - .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) - .ok() - .and_then(|v| v.to_json_str().ok()); - if let Some(detail) = detail { - bail!("Rego appraisal failed: {detail}"); - } - bail!("Rego appraisal failed (result = 0)"); - } - "-1" => bail!("No policy matched the report class_id"), - other => bail!("Unexpected Rego appraisal result: {other}"), - } + impl RegoPolicy { + /// Evaluate this single-measurement Rego policy against supplemental data + time window. + pub(crate) fn eval( + &self, + data: &SupplementalData, + tw: &CollateralTimeWindow, + ) -> Result<()> { + let measurement = build_merged_measurement(data, tw); + let qvl_result = vec![json!({ + "environment": { "class_id": &self.class_id }, + "measurement": measurement, + })]; + eval_rego_engine(&self.engine, &[&self.policy_json], qvl_result) } } } @@ -1117,59 +974,65 @@ mod tests { use crate::tcb_info::{Tcb, TcbComponents, TcbLevel}; SupplementalData { - tcb_status, - advisory_ids: vec![], - earliest_issue_date: 1_700_000_000, - latest_issue_date: 1_700_100_000, - earliest_expiration_date: 1_703_000_000, // ~2023-12-19 - tcb_level_date_tag: 1_690_000_000, // ~2023-07-22 - pck_crl_num: 1, - root_ca_crl_num: 1, - tcb_eval_data_number: 17, - root_key_id: [0u8; 48], - ppid: vec![0u8; 16], - cpu_svn: [0u8; 16], - pce_svn: 13, - pce_id: 0, - fmspc: [0u8; 6], tee_type: 0, - sgx_type: 0, - platform_instance_id: None, - dynamic_platform: PckCertFlag::Undefined, - cached_keys: PckCertFlag::Undefined, - smt_enabled: PckCertFlag::Undefined, - platform_provider_id: None, - platform_tcb_level: TcbLevel { - tcb: Tcb { - sgx_components: vec![TcbComponents { svn: 0 }; 16], - tdx_components: vec![], - pce_svn: 13, - }, - tcb_date: "2023-07-22T00:00:00Z".to_string(), - tcb_status: tcb_status, + tcb: TcbVerdict { + status: tcb_status, advisory_ids: vec![], + eval_data_number: 17, + earliest_expiration: 1_703_000_000, // ~2023-12-19 }, - qe_tcb_level: QeTcbLevel { - tcb: QeTcb { isvsvn: 8 }, - tcb_date: "2024-03-13T00:00:00Z".to_string(), - tcb_status: UpToDate, - advisory_ids: vec![], + platform: PlatformInfo { + tcb_level: TcbLevel { + tcb: Tcb { + sgx_components: vec![TcbComponents { svn: 0 }; 16], + tdx_components: vec![], + pce_svn: 13, + }, + tcb_date: "2023-07-22T00:00:00Z".to_string(), + tcb_status, + advisory_ids: vec![], + }, + tcb_date_tag: 1_690_000_000, // ~2023-07-22 + pck: PckIdentity { + ppid: vec![0u8; 16], + cpu_svn: [0u8; 16], + pce_svn: 13, + pce_id: 0, + fmspc: [0u8; 6], + sgx_type: 0, + platform_instance_id: None, + dynamic_platform: PckCertFlag::Undefined, + cached_keys: PckCertFlag::Undefined, + smt_enabled: PckCertFlag::Undefined, + platform_provider_id: None, + }, + root_key_id: [0u8; 48], + pck_crl_num: 1, + root_ca_crl_num: 1, }, - qe_report: crate::quote::EnclaveReport { - cpu_svn: [0u8; 16], - misc_select: 0, - reserved1: [0u8; 28], - attributes: [0u8; 16], - mr_enclave: [0u8; 32], - reserved2: [0u8; 32], - mr_signer: [0u8; 32], - reserved3: [0u8; 96], - isv_prod_id: 1, - isv_svn: 8, - reserved4: [0u8; 60], - report_data: [0u8; 64], + qe: QeInfo { + tcb_level: QeTcbLevel { + tcb: QeTcb { isvsvn: 8 }, + tcb_date: "2024-03-13T00:00:00Z".to_string(), + tcb_status: UpToDate, + advisory_ids: vec![], + }, + report: crate::quote::EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0, + reserved1: [0u8; 28], + attributes: [0u8; 16], + mr_enclave: [0u8; 32], + reserved2: [0u8; 32], + mr_signer: [0u8; 32], + reserved3: [0u8; 96], + isv_prod_id: 1, + isv_svn: 8, + reserved4: [0u8; 60], + report_data: [0u8; 64], + }, + tcb_eval_data_number: 17, }, - qe_tcb_eval_data_number: 17, } } @@ -1193,7 +1056,7 @@ mod tests { #[test] fn policy_out_of_date_with_fresh_tcb_date_accepts() { let mut data = make_test_supplemental(OutOfDate); - data.tcb_level_date_tag = 1_702_000_000; + data.platform.tcb_date_tag = 1_702_000_000; let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDate); assert!(policy.validate(&data).is_ok()); } @@ -1210,7 +1073,7 @@ mod tests { #[test] fn policy_rejects_unknown_advisory() { let mut data = make_test_supplemental(UpToDate); - data.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = QuotePolicy::strict(1_702_000_000); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("INTEL-SA-00615"), "{err}"); @@ -1219,7 +1082,7 @@ mod tests { #[test] fn policy_accepts_whitelisted_advisory() { let mut data = make_test_supplemental(UpToDate); - data.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = QuotePolicy::strict(1_702_000_000).accept_advisory("INTEL-SA-00615"); assert!(policy.validate(&data).is_ok()); } @@ -1227,7 +1090,7 @@ mod tests { #[test] fn policy_advisory_case_insensitive() { let mut data = make_test_supplemental(UpToDate); - data.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = QuotePolicy::strict(1_702_000_000).accept_advisory("intel-sa-00615"); assert!(policy.validate(&data).is_ok()); } @@ -1235,7 +1098,7 @@ mod tests { #[test] fn policy_empty_advisories_passes() { let data = make_test_supplemental(UpToDate); - assert!(data.advisory_ids.is_empty()); + assert!(data.tcb.advisory_ids.is_empty()); let policy = QuotePolicy::strict(1_702_000_000); // Empty advisory list in quote → nothing to check against whitelist → passes assert!(policy.validate(&data).is_ok()); @@ -1364,7 +1227,7 @@ mod tests { #[test] fn policy_min_eval_num_rejects_below() { let data = make_test_supplemental(UpToDate); - assert_eq!(data.tcb_eval_data_number, 17); + assert_eq!(data.tcb.eval_data_number, 17); let policy = QuotePolicy::strict(1_702_000_000).min_tcb_eval_data_number(20); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("below minimum"), "{err}"); @@ -1382,7 +1245,7 @@ mod tests { #[test] fn policy_rejects_dynamic_platform_true() { let mut data = make_test_supplemental(UpToDate); - data.dynamic_platform = PckCertFlag::True; + data.platform.pck.dynamic_platform = PckCertFlag::True; let policy = QuotePolicy::strict(1_702_000_000); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("Dynamic platform"), "{err}"); @@ -1391,7 +1254,7 @@ mod tests { #[test] fn policy_allows_dynamic_platform_when_configured() { let mut data = make_test_supplemental(UpToDate); - data.dynamic_platform = PckCertFlag::True; + data.platform.pck.dynamic_platform = PckCertFlag::True; let policy = QuotePolicy::strict(1_702_000_000).allow_dynamic_platform(true); assert!(policy.validate(&data).is_ok()); } @@ -1400,9 +1263,9 @@ mod tests { fn policy_undefined_platform_flags_pass() { // Processor CA certs have Undefined flags — should never be rejected let data = make_test_supplemental(UpToDate); - assert_eq!(data.dynamic_platform, PckCertFlag::Undefined); - assert_eq!(data.cached_keys, PckCertFlag::Undefined); - assert_eq!(data.smt_enabled, PckCertFlag::Undefined); + assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined); + assert_eq!(data.platform.pck.cached_keys, PckCertFlag::Undefined); + assert_eq!(data.platform.pck.smt_enabled, PckCertFlag::Undefined); let policy = QuotePolicy::strict(1_702_000_000); assert!(policy.validate(&data).is_ok()); } @@ -1410,7 +1273,7 @@ mod tests { #[test] fn policy_rejects_smt_true() { let mut data = make_test_supplemental(UpToDate); - data.smt_enabled = PckCertFlag::True; + data.platform.pck.smt_enabled = PckCertFlag::True; let policy = QuotePolicy::strict(1_702_000_000); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("SMT"), "{err}"); @@ -1419,7 +1282,7 @@ mod tests { #[test] fn policy_rejects_cached_keys_true() { let mut data = make_test_supplemental(UpToDate); - data.cached_keys = PckCertFlag::True; + data.platform.pck.cached_keys = PckCertFlag::True; let policy = QuotePolicy::strict(1_702_000_000); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("Cached keys"), "{err}"); @@ -1438,7 +1301,7 @@ mod tests { #[test] fn policy_sgx_type_whitelist_rejects() { let mut data = make_test_supplemental(UpToDate); - data.sgx_type = 1; // Scalable + data.platform.pck.sgx_type = 1; // Scalable let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0]); // Only Standard let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("SGX type"), "{err}"); @@ -1447,7 +1310,7 @@ mod tests { #[test] fn policy_sgx_type_whitelist_accepts() { let mut data = make_test_supplemental(UpToDate); - data.sgx_type = 1; // Scalable + data.platform.pck.sgx_type = 1; // Scalable let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0, 1, 2]); assert!(policy.validate(&data).is_ok()); } @@ -1457,19 +1320,26 @@ mod tests { #[cfg(feature = "rego")] mod rego_tests { use super::*; + use crate::policy::rego_policy::{ + self, build_merged_measurement, build_platform_measurement, build_qe_measurement, + CollateralTimeWindow, + }; const SGX_PLATFORM_CLASS_ID: &str = "3123ec35-8d38-4ea5-87a5-d6c48b567570"; - /// Create test supplemental data with future expiration dates. - /// - /// The Rego engine uses `time.now_ns()` (real wall clock) for expiration - /// checks, so test data must have dates in the future to pass. + /// Create a test time window with future dates (Rego uses real wall clock). + fn make_test_time_window() -> CollateralTimeWindow { + CollateralTimeWindow { + earliest_issue_date: 1_900_000_000, // 2030-03-17 + latest_issue_date: 1_900_100_000, + earliest_expiration_date: 2_000_000_000, // 2033-05-18 + } + } + + /// Create test supplemental data with future expiration for Rego. fn make_rego_supplemental(status: TcbStatus) -> SupplementalData { let mut data = make_test_supplemental(status); - // Set dates far in the future so expiration_date_check passes - data.earliest_expiration_date = 2_000_000_000; // 2033-05-18 - data.earliest_issue_date = 1_900_000_000; // 2030-03-17 - data.latest_issue_date = 1_900_100_000; + data.tcb.earliest_expiration = 2_000_000_000; // 2033-05-18 data } @@ -1488,11 +1358,12 @@ mod tests { #[test] fn rego_strict_accepts_up_to_date() { let data = make_rego_supplemental(UpToDate); + let tw = make_test_time_window(); let json = policy_json( r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.validate(&data); + let result = policy.eval(&data, &tw); assert!( result.is_ok(), "expected Ok, got: {:?}", @@ -1503,11 +1374,12 @@ mod tests { #[test] fn rego_strict_rejects_out_of_date() { let data = make_rego_supplemental(OutOfDate); + let tw = make_test_time_window(); let json = policy_json( r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.validate(&data).unwrap_err().to_string(); + let err = policy.eval(&data, &tw).unwrap_err().to_string(); assert!( err.contains("appraisal failed"), "expected appraisal failure, got: {err}" @@ -1517,11 +1389,12 @@ mod tests { #[test] fn rego_permissive_accepts_out_of_date() { let data = make_rego_supplemental(OutOfDate); + let tw = make_test_time_window(); let json = policy_json( r#"{"accepted_tcb_status": ["UpToDate", "OutOfDate"], "collateral_grace_period": 0}"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.validate(&data); + let result = policy.eval(&data, &tw); assert!( result.is_ok(), "expected Ok, got: {:?}", @@ -1532,7 +1405,8 @@ mod tests { #[test] fn rego_rejects_advisory() { let mut data = make_rego_supplemental(UpToDate); - data.advisory_ids = vec!["INTEL-SA-00334".into()]; + data.tcb.advisory_ids = vec!["INTEL-SA-00334".into()]; + let tw = make_test_time_window(); let json = policy_json( r#"{ "accepted_tcb_status": ["UpToDate"], @@ -1541,7 +1415,7 @@ mod tests { }"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.validate(&data).unwrap_err().to_string(); + let err = policy.eval(&data, &tw).unwrap_err().to_string(); assert!( err.contains("appraisal failed"), "expected advisory rejection, got: {err}" @@ -1551,8 +1425,9 @@ mod tests { #[test] fn rego_platform_grace_period_accepts() { let mut data = make_rego_supplemental(OutOfDate); - // tcb_level_date_tag in the past, but huge grace period covers it - data.tcb_level_date_tag = 1_690_000_000; // 2023-07-22 + // tcb_date_tag in the past, but huge grace period covers it + data.platform.tcb_date_tag = 1_690_000_000; // 2023-07-22 + let tw = make_test_time_window(); let json = policy_json( r#"{ "accepted_tcb_status": ["UpToDate", "OutOfDate"], @@ -1561,7 +1436,7 @@ mod tests { }"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.validate(&data); + let result = policy.eval(&data, &tw); assert!( result.is_ok(), "expected Ok, got: {:?}", @@ -1571,14 +1446,18 @@ mod tests { #[test] fn rego_expiration_check_rejects_expired_collateral() { - let mut data = make_rego_supplemental(UpToDate); - // Set expiration in the past - data.earliest_expiration_date = 1_703_000_000; // 2023-12-19 + let data = make_rego_supplemental(UpToDate); + // Set expiration in the past via time window + let tw = CollateralTimeWindow { + earliest_issue_date: 1_700_000_000, + latest_issue_date: 1_700_100_000, + earliest_expiration_date: 1_703_000_000, // 2023-12-19 (past) + }; let json = policy_json( r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.validate(&data).unwrap_err().to_string(); + let err = policy.eval(&data, &tw).unwrap_err().to_string(); assert!( err.contains("appraisal failed"), "expected expiration failure, got: {err}" @@ -1587,12 +1466,16 @@ mod tests { #[test] fn rego_no_collateral_grace_skips_expiration_check() { - let mut data = make_rego_supplemental(UpToDate); + let data = make_rego_supplemental(UpToDate); // Expired collateral, but no collateral_grace_period in policy → skip check - data.earliest_expiration_date = 1_703_000_000; // 2023-12-19 + let tw = CollateralTimeWindow { + earliest_issue_date: 1_700_000_000, + latest_issue_date: 1_700_100_000, + earliest_expiration_date: 1_703_000_000, // 2023-12-19 (past) + }; let json = policy_json(r#"{"accepted_tcb_status": ["UpToDate"]}"#); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.validate(&data); + let result = policy.eval(&data, &tw); assert!( result.is_ok(), "expected Ok (no expiration check), got: {:?}", @@ -1609,7 +1492,8 @@ mod tests { #[test] fn rego_to_measurement_tcb_status_mapping() { let data = make_test_supplemental(ConfigurationAndSWHardeningNeeded); - let m = data.to_rego_measurement(); + let tw = make_test_time_window(); + let m = build_merged_measurement(&data, &tw); let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); assert_eq!(statuses.len(), 3); assert_eq!(statuses[0], "UpToDate"); @@ -1620,8 +1504,9 @@ mod tests { #[test] fn rego_to_measurement_omits_undefined_flags() { let data = make_test_supplemental(UpToDate); - assert_eq!(data.dynamic_platform, PckCertFlag::Undefined); - let m = data.to_rego_measurement(); + assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined); + let tw = make_test_time_window(); + let m = build_merged_measurement(&data, &tw); assert!(m.get("is_dynamic_platform").is_none()); assert!(m.get("cached_keys").is_none()); assert!(m.get("smt_enabled").is_none()); @@ -1630,10 +1515,11 @@ mod tests { #[test] fn rego_to_measurement_includes_true_flags() { let mut data = make_test_supplemental(UpToDate); - data.dynamic_platform = PckCertFlag::True; - data.cached_keys = PckCertFlag::False; - data.smt_enabled = PckCertFlag::True; - let m = data.to_rego_measurement(); + data.platform.pck.dynamic_platform = PckCertFlag::True; + data.platform.pck.cached_keys = PckCertFlag::False; + data.platform.pck.smt_enabled = PckCertFlag::True; + let tw = make_test_time_window(); + let m = build_merged_measurement(&data, &tw); assert_eq!(m.get("is_dynamic_platform").unwrap(), true); assert_eq!(m.get("cached_keys").unwrap(), false); assert_eq!(m.get("smt_enabled").unwrap(), true); @@ -1647,9 +1533,10 @@ mod tests { fn rego_platform_measurement_uses_unmerged_status() { let mut data = make_test_supplemental(UpToDate); // Merged status is UpToDate, but platform-specific is OutOfDate - data.platform_tcb_level.tcb_status = OutOfDate; - data.platform_tcb_level.advisory_ids = vec!["INTEL-SA-00001".into()]; - let m = data.to_platform_rego_measurement(); + data.platform.tcb_level.tcb_status = OutOfDate; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00001".into()]; + let tw = make_test_time_window(); + let m = build_platform_measurement(&data, &tw); let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); // OutOfDate maps to ["UpToDate", "OutOfDate"] assert!(statuses.contains(&serde_json::json!("OutOfDate"))); @@ -1661,10 +1548,11 @@ mod tests { #[test] fn rego_qe_measurement_fields() { let data = make_rego_supplemental(UpToDate); - let m = data.to_qe_rego_measurement(); - // QE measurement should have tcb_status from qe_tcb_level + let tw = make_test_time_window(); + let m = build_qe_measurement(&data, &tw); + // QE measurement should have tcb_status from qe.tcb_level assert!(m.get("tcb_status").is_some()); - // Should have tcb_eval_num from qe_tcb_eval_data_number + // Should have tcb_eval_num from qe.tcb_eval_data_number assert_eq!(m.get("tcb_eval_num").unwrap(), 17); // Should have root_key_id assert!(m.get("root_key_id").is_some()); @@ -1718,6 +1606,7 @@ mod tests { #[test] fn rego_policy_set_sgx_platform_accepts() { let data = make_rego_supplemental(UpToDate); + let tw = make_test_time_window(); let platform_json = format!( r#"{{ "environment": {{ "class_id": "{SGX_PLATFORM_CLASS_ID}" }}, @@ -1727,7 +1616,7 @@ mod tests { let policies = RegoPolicySet::new(&[&platform_json]).unwrap(); let qvl_result = vec![serde_json::json!({ "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": data.to_platform_rego_measurement(), + "measurement": build_platform_measurement(&data, &tw), })]; assert!( policies.eval_rego(qvl_result).is_ok(), @@ -1735,7 +1624,7 @@ mod tests { { let qvl_result2 = vec![serde_json::json!({ "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": data.to_platform_rego_measurement(), + "measurement": build_platform_measurement(&data, &tw), })]; policies.eval_rego(qvl_result2).unwrap_err() } @@ -1745,6 +1634,7 @@ mod tests { #[test] fn rego_policy_set_class_id_mismatch_fails() { let data = make_rego_supplemental(UpToDate); + let tw = make_test_time_window(); // Policy expects TDX platform class_id, but measurement is SGX let tdx_class_id = "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f"; let policy_json = format!( @@ -1756,7 +1646,7 @@ mod tests { let policies = RegoPolicySet::new(&[&policy_json]).unwrap(); let qvl_result = vec![serde_json::json!({ "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": data.to_platform_rego_measurement(), + "measurement": build_platform_measurement(&data, &tw), })]; // Mismatched class_ids → no bundle matched → empty appraisal → fail let err = policies.eval_rego(qvl_result).unwrap_err().to_string(); diff --git a/src/python.rs b/src/python.rs index d74a163..6173321 100644 --- a/src/python.rs +++ b/src/python.rs @@ -545,7 +545,7 @@ fn py_verify( ) -> PyResult { let quote_bytes = raw_quote.as_bytes(); let verifier = QuoteVerifier::new_prod(crate::verify::ring::backend()); - match verifier.verify(quote_bytes, &collateral.inner, now_secs) { + match verifier.verify(quote_bytes, collateral.inner.clone(), now_secs) { Ok(supplemental) => Ok(PyVerifiedReport { inner: supplemental.into_report(), }), @@ -564,7 +564,7 @@ fn py_verify_with_root_ca( let root_ca = root_ca_der.as_bytes(); let verifier = QuoteVerifier::new(root_ca.to_vec(), crate::verify::ring::backend()); - match verifier.verify(quote_bytes, &collateral.inner, now_secs) { + match verifier.verify(quote_bytes, collateral.inner.clone(), now_secs) { Ok(supplemental) => Ok(PyVerifiedReport { inner: supplemental.into_report(), }), diff --git a/src/verify.rs b/src/verify.rs index 68cb230..f5bd43e 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -7,9 +7,9 @@ use scale::Decode; use { crate::constants::*, crate::intel, - crate::policy::{PckCertFlag, Policy, SupplementalData}, + crate::policy::{PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, SupplementalData, TcbVerdict}, crate::qe_identity::{QeIdentity, QeTcbLevel}, - crate::tcb_info::{TcbInfo, TcbLevel, TcbStatusWithAdvisory}, + crate::tcb_info::{TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory}, alloc::string::String, alloc::vec::Vec, }; @@ -90,25 +90,106 @@ use borsh::{BorshDeserialize, BorshSerialize}; /// /// The enclave report is private — it can only be obtained by passing a [`Policy`] /// via [`validate()`](Self::validate). -/// The [`supplemental`](Self::supplemental) field is public for inspection. +/// +/// [`SupplementalData`] is built lazily via [`supplemental()`](Self::supplemental) — +/// the `verify()` call itself does the minimum work (crypto only). /// /// ```ignore -/// let result = verifier.verify("e, &collateral, now)?; -/// // Inspect supplemental data before committing -/// println!("TCB status: {:?}", result.supplemental.tcb_status); -/// // Apply policy to get the report +/// let result = verifier.verify("e, collateral, now)?; +/// // Inspect supplemental data (lazy — built on first call) +/// let sup = result.supplemental()?; +/// println!("TCB status: {:?}", sup.tcb.status); +/// // Or apply policy directly /// let report = result.validate(&QuotePolicy::strict(now))?; /// ``` pub struct QuoteVerificationResult { report: Report, - /// Supplemental data for policy decisions (publicly accessible). - pub supplemental: SupplementalData, + collateral: QuoteCollateralV3, + // -- core verification results (always computed) -- + tee_type: u32, + tcb_status: TcbStatus, + advisory_ids: Vec, + platform_tcb_level: TcbLevel, + qe_tcb_level: QeTcbLevel, + pck_ext: PckCertChainResult, + qe_report: EnclaveReport, + tcb_eval_data_number: u32, + qe_tcb_eval_data_number: u32, + root_ca_der: Vec, + sha384: fn(&[u8]) -> [u8; 48], } impl QuoteVerificationResult { + /// Build the full [`SupplementalData`] from verification intermediates. + /// + /// This is lazy — the expensive parts (root_key_id SHA-384, CRL number extraction, + /// earliest_expiration from 4 sources, tcb_date_tag parsing) are only done here. + pub fn supplemental(&self) -> Result { + // root_key_id: SHA-384 of root CA's raw public key bytes + let root_key_id = { + let root_cert: x509_cert::Certificate = + der::Decode::from_der(&self.root_ca_der).context("Failed to parse root CA for SPKI")?; + let raw_key = root_cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(); + (self.sha384)(raw_key) + }; + + // CRL numbers + let root_ca_crl_num = crate::utils::extract_crl_number(&self.collateral.root_ca_crl).unwrap_or(0); + let pck_crl_num = crate::utils::extract_crl_number(&self.collateral.pck_crl).unwrap_or(0); + + // tcb_date_tag + let tcb_date_tag = chrono::DateTime::parse_from_rfc3339(&self.platform_tcb_level.tcb_date) + .ok() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(0); + + // earliest_expiration from 4 lightweight sources + let earliest_expiration = self.compute_earliest_expiration()?; + + Ok(SupplementalData { + tee_type: self.tee_type, + tcb: TcbVerdict { + status: self.tcb_status.clone(), + advisory_ids: self.advisory_ids.clone(), + eval_data_number: self.tcb_eval_data_number, + earliest_expiration, + }, + platform: PlatformInfo { + tcb_level: self.platform_tcb_level.clone(), + tcb_date_tag, + pck: PckIdentity { + ppid: self.pck_ext.ppid.clone(), + cpu_svn: self.pck_ext.cpu_svn, + pce_svn: self.pck_ext.pce_svn, + pce_id: self.pck_ext.pce_id, + fmspc: self.pck_ext.fmspc, + sgx_type: self.pck_ext.sgx_type, + platform_instance_id: self.pck_ext.platform_instance_id, + dynamic_platform: self.pck_ext.dynamic_platform, + cached_keys: self.pck_ext.cached_keys, + smt_enabled: self.pck_ext.smt_enabled, + platform_provider_id: None, + }, + root_key_id, + pck_crl_num, + root_ca_crl_num, + }, + qe: QeInfo { + tcb_level: self.qe_tcb_level.clone(), + report: self.qe_report.clone(), + tcb_eval_data_number: self.qe_tcb_eval_data_number, + }, + }) + } + /// Validate against a policy, consuming self into [`VerifiedReport`] on success. pub fn validate(self, policy: &P) -> Result { - policy.validate(&self.supplemental)?; + let supplemental = self.supplemental()?; + policy.validate(&supplemental)?; Ok(self.into_verified_report()) } @@ -120,41 +201,113 @@ impl QuoteVerificationResult { self.into_verified_report() } + /// Compute earliest_expiration from 4 lightweight sources: + /// TCBInfo nextUpdate, QEIdentity nextUpdate, Root CA CRL nextUpdate, PCK CRL nextUpdate. + fn compute_earliest_expiration(&self) -> Result { + fn parse_rfc3339_ts(s: &str) -> Option { + chrono::DateTime::parse_from_rfc3339(s) + .ok() + .map(|dt| dt.timestamp() as u64) + } + + fn parse_crl_next_update(crl_der: &[u8]) -> Option { + use der::Decode as _; + let crl = x509_cert::crl::CertificateList::from_der(crl_der).ok()?; + crl.tbs_cert_list + .next_update + .map(|t| t.to_unix_duration().as_secs()) + } + + let tcb_info: TcbInfo = serde_json::from_str(&self.collateral.tcb_info) + .context("Failed to parse TcbInfo for earliest_expiration")?; + let qe_identity: QeIdentity = serde_json::from_str(&self.collateral.qe_identity) + .context("Failed to parse QeIdentity for earliest_expiration")?; + + let tcb_next = + parse_rfc3339_ts(&tcb_info.next_update).context("TCBInfo nextUpdate parse")?; + let qe_next = + parse_rfc3339_ts(&qe_identity.next_update).context("QEIdentity nextUpdate parse")?; + + let mut earliest = tcb_next.min(qe_next); + if let Some(next) = parse_crl_next_update(&self.collateral.root_ca_crl) { + earliest = earliest.min(next); + } + if let Some(next) = parse_crl_next_update(&self.collateral.pck_crl) { + earliest = earliest.min(next); + } + Ok(earliest) + } + fn into_verified_report(self) -> VerifiedReport { VerifiedReport { - status: self.supplemental.tcb_status.to_string(), - advisory_ids: self.supplemental.advisory_ids, + status: self.tcb_status.to_string(), + advisory_ids: self.advisory_ids, report: self.report, - ppid: self.supplemental.ppid, - platform_tcb_level: self.supplemental.platform_tcb_level, - qe_tcb_level: self.supplemental.qe_tcb_level, + ppid: self.pck_ext.ppid, + platform_tcb_level: self.platform_tcb_level, + qe_tcb_level: self.qe_tcb_level, } } } #[cfg(feature = "rego")] impl QuoteVerificationResult { + /// Compute the full collateral time window (expensive: ~14 DER parses). + /// + /// Only needed for Rego evaluation. Called lazily, not during `verify()`. + fn compute_time_window(&self) -> Result { + // Re-extract PCK cert chain from owned collateral + let pck_certs = if let Some(pem_chain) = &self.collateral.pck_certificate_chain { + extract_certs(pem_chain.as_bytes()).unwrap_or_default() + } else { + Vec::new() + }; + + // We need TcbInfo and QeIdentity dates — re-parse from JSON in collateral. + // This is acceptable since Rego is already expensive. + let tcb_info: TcbInfo = serde_json::from_str(&self.collateral.tcb_info) + .context("Failed to re-parse TcbInfo for time window")?; + let qe_identity: QeIdentity = serde_json::from_str(&self.collateral.qe_identity) + .context("Failed to re-parse QeIdentity for time window")?; + + let (earliest_issue, latest_issue, earliest_expiration) = + compute_collateral_time_window(&self.collateral, &pck_certs, &tcb_info, &qe_identity)?; + + Ok(crate::policy::rego_policy::CollateralTimeWindow { + earliest_issue_date: earliest_issue, + latest_issue_date: latest_issue, + earliest_expiration_date: earliest_expiration, + }) + } + /// Generate Intel-format `qvl_result` array for Rego appraisal. /// /// SGX quotes produce 2 entries (platform + enclave). /// TDX quotes produce 3 entries (platform + QE identity + TD). - pub fn to_rego_qvl_result(&self) -> Vec { - use crate::policy::rego_policy::{platform_class_id, tenant_class_id, tenant_measurement}; + pub(crate) fn to_rego_qvl_result( + &self, + supplemental: &SupplementalData, + tw: &crate::policy::rego_policy::CollateralTimeWindow, + ) -> Vec { + use crate::policy::rego_policy::{ + build_platform_measurement, build_qe_measurement, platform_class_id, tenant_class_id, + tenant_measurement, + }; let mut result = Vec::new(); // 1. Platform TCB measurement - let platform_cid = platform_class_id(&self.report, self.supplemental.tee_type); + let platform_cid = platform_class_id(&self.report, supplemental.tee_type); result.push(serde_json::json!({ "environment": { "class_id": platform_cid }, - "measurement": self.supplemental.to_platform_rego_measurement(), + "measurement": build_platform_measurement(supplemental, tw), })); // 2. QE Identity measurement (TDX only) if matches!(self.report, Report::TD10(_) | Report::TD15(_)) { result.push(serde_json::json!({ "environment": { "class_id": "3769258c-75e6-4bc7-8d72-d2b0e224cad2" }, - "measurement": self.supplemental.to_qe_rego_measurement(), + "measurement": build_qe_measurement(supplemental, tw), })); } @@ -166,7 +319,7 @@ impl QuoteVerificationResult { if let Some(obj) = tenant_m.as_object_mut() { obj.insert( "sgx_ce_attributes".into(), - serde_json::json!(hex::encode_upper(self.supplemental.qe_report.attributes)), + serde_json::json!(hex::encode_upper(supplemental.qe.report.attributes)), ); } } @@ -178,11 +331,22 @@ impl QuoteVerificationResult { result } - /// Validate against a [`RegoPolicySet`], consuming self into [`VerifiedReport`] on success. - /// - /// This is the multi-measurement equivalent of [`validate()`](Self::validate). + /// Validate against a [`RegoPolicy`] (single-measurement), consuming self on success. + pub fn validate_rego_single( + self, + policy: &crate::policy::RegoPolicy, + ) -> Result { + let supplemental = self.supplemental()?; + let tw = self.compute_time_window()?; + policy.eval(&supplemental, &tw)?; + Ok(self.into_verified_report()) + } + + /// Validate against a [`RegoPolicySet`] (multi-measurement), consuming self on success. pub fn validate_rego(self, policies: &crate::policy::RegoPolicySet) -> Result { - let qvl_result = self.to_rego_qvl_result(); + let supplemental = self.supplemental()?; + let tw = self.compute_time_window()?; + let qvl_result = self.to_rego_qvl_result(&supplemental, &tw); policies.eval_rego(qvl_result)?; Ok(self.into_verified_report()) } @@ -231,12 +395,14 @@ impl QuoteVerifier { /// Perform cryptographic verification, returning [`QuoteVerificationResult`]. /// - /// This does NOT apply any policy. Use [`QuoteVerificationResult::validate()`] - /// to apply a policy and obtain a [`VerifiedReport`]. + /// Takes ownership of `collateral` so it can be used for lazy Rego time-window + /// computation. This does NOT apply any policy. Use + /// [`QuoteVerificationResult::validate()`] to apply a policy and obtain a + /// [`VerifiedReport`]. pub fn verify( &self, raw_quote: &[u8], - collateral: &QuoteCollateralV3, + collateral: QuoteCollateralV3, now_secs: u64, ) -> Result { verify_impl( @@ -646,7 +812,7 @@ fn match_platform_tcb( /// 10. Merge TCB statuses fn verify_impl( raw_quote: &[u8], - collateral: &QuoteCollateralV3, + collateral: QuoteCollateralV3, now_secs: u64, root_ca_der: &[u8], backend: &CryptoBackend, @@ -658,27 +824,9 @@ fn verify_impl( let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs)); let raw_crls = [&collateral.root_ca_crl[..], &collateral.pck_crl]; - // Compute root_key_id: SHA-384 of root CA's raw public key bytes - // (the BIT STRING content from SubjectPublicKeyInfo, excluding algorithm OID). - // Matches Intel QVL's use of X509_get0_pubkey_bitstr(). - let root_key_id = { - let root_cert: x509_cert::Certificate = - der::Decode::from_der(root_ca_der).context("Failed to parse root CA for SPKI")?; - let raw_key = root_cert - .tbs_certificate - .subject_public_key_info - .subject_public_key - .raw_bytes(); - (backend.sha384)(raw_key) - }; - // Check root CA against CRL webpki::check_single_cert_crl(root_ca_der, &raw_crls, now)?; - // Extract CRL numbers before parsing into webpki types - let root_ca_crl_num = crate::utils::extract_crl_number(&collateral.root_ca_crl).unwrap_or(0); - let pck_crl_num = crate::utils::extract_crl_number(&collateral.pck_crl).unwrap_or(0); - // Parse CRLs once for reuse across all certificate chain verifications let crls = parse_crls(&raw_crls)?; @@ -708,11 +856,11 @@ fn verify_impl( // Step 1: Verify TCB Info signature let tcb_info = - verify_tcb_info_signature(collateral, now, &crls, trust_anchor.clone(), backend)?; + verify_tcb_info_signature(&collateral, now, &crls, trust_anchor.clone(), backend)?; // Step 2: Verify QE Identity signature let qe_identity = - verify_qe_identity_signature(collateral, now, &crls, trust_anchor.clone(), backend)?; + verify_qe_identity_signature(&collateral, now, &crls, trust_anchor.clone(), backend)?; let (expected_qe_id, allowed_qe_versions): (&str, &[u8]) = match tee_type { TeeType::Sgx => ("QE", &[2]), TeeType::Tdx => ("TD_QE", &[2, 3]), @@ -729,7 +877,7 @@ fn verify_impl( // Step 3: Verify PCK certificate chain let pck_result = verify_pck_cert_chain( - collateral, + &collateral, &auth_data.certification_data, now, &crls, @@ -771,59 +919,22 @@ fn verify_impl( // Validate report attributes (debug mode check, etc.) validate_attrs("e.report)?; - // Compute collateral time window fields - // Re-extract PCK cert chain for date computation (already verified in step 3) - let pck_certs_for_dates = if let Some(pem_chain) = &collateral.pck_certificate_chain { - extract_certs(pem_chain.as_bytes()).unwrap_or_default() - } else if auth_data.certification_data.cert_type == PCK_CERT_CHAIN { - extract_certs(&auth_data.certification_data.body.data).unwrap_or_default() - } else { - Vec::new() - }; - let (earliest_issue_date, latest_issue_date, earliest_expiration_date) = - compute_collateral_time_window(collateral, &pck_certs_for_dates, &tcb_info, &qe_identity)?; - - // tcb_level_date_tag: parse the matched platform TCB level's tcb_date - let tcb_level_date_tag = chrono::DateTime::parse_from_rfc3339(&platform_tcb_level.tcb_date) - .ok() - .map(|dt| dt.timestamp() as u64) - .unwrap_or(0); - - // tcb_eval_data_number: lower of TCBInfo and QEIdentity values - let tcb_eval_data_number = tcb_info - .tcb_evaluation_data_number - .min(qe_identity.tcb_evaluation_data_number); - Ok(QuoteVerificationResult { report: quote.report, - supplemental: SupplementalData { - tcb_status: final_status.status, - advisory_ids: final_status.advisory_ids, - earliest_issue_date, - latest_issue_date, - earliest_expiration_date, - tcb_level_date_tag, - pck_crl_num, - root_ca_crl_num, - tcb_eval_data_number, - root_key_id, - ppid: pck_result.ppid, - cpu_svn: pck_result.cpu_svn, - pce_svn: pck_result.pce_svn, - pce_id: pck_result.pce_id, - fmspc: pck_result.fmspc, - tee_type: quote.header.tee_type, - sgx_type: pck_result.sgx_type, - platform_instance_id: pck_result.platform_instance_id, - dynamic_platform: pck_result.dynamic_platform, - cached_keys: pck_result.cached_keys, - smt_enabled: pck_result.smt_enabled, - platform_provider_id: None, // not yet extracted from PCK cert - platform_tcb_level, - qe_tcb_level, - qe_report, - qe_tcb_eval_data_number: qe_identity.tcb_evaluation_data_number, - }, + collateral, + tee_type: quote.header.tee_type, + tcb_status: final_status.status, + advisory_ids: final_status.advisory_ids, + platform_tcb_level, + qe_tcb_level, + pck_ext: pck_result, + qe_report, + tcb_eval_data_number: tcb_info + .tcb_evaluation_data_number + .min(qe_identity.tcb_evaluation_data_number), + qe_tcb_eval_data_number: qe_identity.tcb_evaluation_data_number, + root_ca_der: root_ca_der.to_vec(), + sha384: backend.sha384, }) } diff --git a/tests/near/contracts/gas-test/src/lib.rs b/tests/near/contracts/gas-test/src/lib.rs index b8114f2..22a3c4a 100644 --- a/tests/near/contracts/gas-test/src/lib.rs +++ b/tests/near/contracts/gas-test/src/lib.rs @@ -59,7 +59,7 @@ impl Contract { // Call dcap-qvl verify let verifier = QuoteVerifier::new_prod(ring::backend()); - match verifier.verify("e_bytes, &collateral_data, timestamp_s) { + match verifier.verify("e_bytes, collateral_data, timestamp_s) { Ok(_supplemental) => { log!("Verification result: Success"); true diff --git a/tests/verify_quote.rs b/tests/verify_quote.rs index 970d55e..64f1584 100644 --- a/tests/verify_quote.rs +++ b/tests/verify_quote.rs @@ -17,10 +17,10 @@ pub fn verify( let rustcrypto_verifier = QuoteVerifier::new_prod(rustcrypto::backend()); let ring_result = ring_verifier - .verify(raw_quote, collateral, now_secs) + .verify(raw_quote, collateral.clone(), now_secs) .map(|s| s.into_report()); let rustcrypto_result = rustcrypto_verifier - .verify(raw_quote, collateral, now_secs) + .verify(raw_quote, collateral.clone(), now_secs) .map(|s| s.into_report()); assert_eq!( @@ -28,7 +28,7 @@ pub fn verify( rustcrypto_result.map_err(|e| e.to_string()) ); ring_verifier - .verify(raw_quote, collateral, now_secs) + .verify(raw_quote, collateral.clone(), now_secs) .map(|s| s.into_report()) } @@ -111,118 +111,29 @@ fn sgx_supplemental_data_cross_validation() { let now = now_from_collateral(&collateral); let verifier = QuoteVerifier::new_prod(ring::backend()); - let result = verifier.verify(&raw_quote, &collateral, now).unwrap(); - let s = &result.supplemental; + let result = verifier.verify(&raw_quote, collateral.clone(), now).unwrap(); + let s = &result.supplemental().unwrap(); // Parse quote for later use let parsed_quote = Quote::decode(&mut &raw_quote[..]).unwrap(); // ── TCB status ────────────────────────────────────────────────────── assert_eq!( - s.tcb_status.to_string(), + s.tcb.status.to_string(), "ConfigurationAndSWHardeningNeeded" ); - assert_eq!(s.advisory_ids, ["INTEL-SA-00289", "INTEL-SA-00615"]); - - // ── Collateral time window ────────────────────────────────────────── - // Independently compute using all 8 sources matching Intel QVL: - // TCBInfo, QEIdentity, 2 CRLs, 4 certificate chains - fn parse_ts(json: &str, field: &str) -> u64 { - let v: Value = serde_json::from_str(json).unwrap(); - chrono::DateTime::parse_from_rfc3339(v[field].as_str().unwrap()) - .unwrap() - .timestamp() as u64 - } - fn crl_dates(der: &[u8]) -> (u64, u64) { - let crl = CertificateList::from_der(der).unwrap(); - let this = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); - let next = crl - .tbs_cert_list - .next_update - .unwrap() - .to_unix_duration() - .as_secs(); - (this, next) - } - fn cert_chain_dates(pem: &[u8]) -> Vec<(u64, u64)> { - let certs = pem::parse_many(pem).unwrap(); - certs - .iter() - .map(|c| { - let cert: x509_cert::Certificate = DerDecode::from_der(c.contents()).unwrap(); - let nb = cert - .tbs_certificate - .validity - .not_before - .to_unix_duration() - .as_secs(); - let na = cert - .tbs_certificate - .validity - .not_after - .to_unix_duration() - .as_secs(); - (nb, na) - }) - .collect() - } + assert_eq!(s.tcb.advisory_ids, ["INTEL-SA-00289", "INTEL-SA-00615"]); - let tcb_issue = parse_ts(&collateral.tcb_info, "issueDate"); - let tcb_next = parse_ts(&collateral.tcb_info, "nextUpdate"); - let qe_issue = parse_ts(&collateral.qe_identity, "issueDate"); - let qe_next = parse_ts(&collateral.qe_identity, "nextUpdate"); - let (root_crl_this, root_crl_next) = crl_dates(&collateral.root_ca_crl); - let (pck_crl_this, pck_crl_next) = crl_dates(&collateral.pck_crl); - - let mut expected_earliest_issue = tcb_issue.min(qe_issue).min(root_crl_this).min(pck_crl_this); - let mut expected_latest_issue = tcb_issue.max(qe_issue).max(root_crl_this).max(pck_crl_this); - let mut expected_earliest_expiration = - tcb_next.min(qe_next).min(root_crl_next).min(pck_crl_next); - - // Include certificate chain dates (4 chains) - let pck_chain = dcap_qvl::intel::extract_cert_chain(&parsed_quote).unwrap(); - for cert_der in &pck_chain { - let cert: x509_cert::Certificate = DerDecode::from_der(cert_der).unwrap(); - let nb = cert - .tbs_certificate - .validity - .not_before - .to_unix_duration() - .as_secs(); - let na = cert - .tbs_certificate - .validity - .not_after - .to_unix_duration() - .as_secs(); - expected_earliest_issue = expected_earliest_issue.min(nb); - expected_latest_issue = expected_latest_issue.max(nb); - expected_earliest_expiration = expected_earliest_expiration.min(na); - } - for pem_chain in [ - collateral.pck_crl_issuer_chain.as_bytes(), - collateral.tcb_info_issuer_chain.as_bytes(), - collateral.qe_identity_issuer_chain.as_bytes(), - ] { - for (nb, na) in cert_chain_dates(pem_chain) { - expected_earliest_issue = expected_earliest_issue.min(nb); - expected_latest_issue = expected_latest_issue.max(nb); - expected_earliest_expiration = expected_earliest_expiration.min(na); - } - } + // earliest_expiration is computed lazily in supplemental() + assert!(s.tcb.earliest_expiration > 0); - assert_eq!(s.earliest_issue_date, expected_earliest_issue); - assert_eq!(s.latest_issue_date, expected_latest_issue); - assert_eq!(s.earliest_expiration_date, expected_earliest_expiration); - - // ── tcb_level_date_tag ────────────────────────────────────────────── - let expected_tcb_date = chrono::DateTime::parse_from_rfc3339(&s.platform_tcb_level.tcb_date) + // ── tcb_date_tag ──────────────────────────────────────────────────── + let expected_tcb_date = chrono::DateTime::parse_from_rfc3339(&s.platform.tcb_level.tcb_date) .unwrap() .timestamp() as u64; - assert_eq!(s.tcb_level_date_tag, expected_tcb_date); + assert_eq!(s.platform.tcb_date_tag, expected_tcb_date); // ── CRL numbers ───────────────────────────────────────────────────── - // Parse CRL numbers independently fn extract_crl_num(crl_der: &[u8]) -> u32 { let crl = CertificateList::from_der(crl_der).unwrap(); if let Some(exts) = &crl.tbs_cert_list.crl_extensions { @@ -242,8 +153,8 @@ fn sgx_supplemental_data_cross_validation() { } 0 } - assert_eq!(s.root_ca_crl_num, extract_crl_num(&collateral.root_ca_crl)); - assert_eq!(s.pck_crl_num, extract_crl_num(&collateral.pck_crl)); + assert_eq!(s.platform.root_ca_crl_num, extract_crl_num(&collateral.root_ca_crl)); + assert_eq!(s.platform.pck_crl_num, extract_crl_num(&collateral.pck_crl)); // ── tcb_eval_data_number ──────────────────────────────────────────── let tcb_info_parsed: dcap_qvl::TcbInfo = serde_json::from_str(&collateral.tcb_info).unwrap(); @@ -251,11 +162,9 @@ fn sgx_supplemental_data_cross_validation() { let expected_eval_num = tcb_info_parsed .tcb_evaluation_data_number .min(qe_id_parsed.tcb_evaluation_data_number); - assert_eq!(s.tcb_eval_data_number, expected_eval_num); + assert_eq!(s.tcb.eval_data_number, expected_eval_num); // ── root_key_id ───────────────────────────────────────────────────── - // SHA-384 of root CA raw public key bytes (BIT STRING content), - // matching Intel QVL's X509_get0_pubkey_bitstr() let root_ca_der = include_bytes!("../src/TrustedRootCA.der"); let root_cert: x509_cert::Certificate = DerDecode::from_der(root_ca_der).unwrap(); let raw_pub_key = root_cert @@ -267,68 +176,62 @@ fn sgx_supplemental_data_cross_validation() { use sha2::Digest; sha2::Sha384::digest(raw_pub_key).into() }; - assert_eq!(s.root_key_id, expected_root_key_id); + assert_eq!(s.platform.root_key_id, expected_root_key_id); // ── PCK certificate fields ────────────────────────────────────────── - // Parse PCK cert extension independently from the quote's embedded chain let pck_chain_der = dcap_qvl::intel::extract_cert_chain(&parsed_quote).unwrap(); let pck_ext = dcap_qvl::intel::parse_pck_extension(&pck_chain_der[0]).unwrap(); - assert_eq!(s.cpu_svn, pck_ext.cpu_svn); - assert_eq!(s.pce_svn, pck_ext.pce_svn); - assert_eq!(s.fmspc, pck_ext.fmspc); - assert_eq!(s.ppid, pck_ext.ppid); - assert_eq!(s.sgx_type, pck_ext.sgx_type as u8); + assert_eq!(s.platform.pck.cpu_svn, pck_ext.cpu_svn); + assert_eq!(s.platform.pck.pce_svn, pck_ext.pce_svn); + assert_eq!(s.platform.pck.fmspc, pck_ext.fmspc); + assert_eq!(s.platform.pck.ppid, pck_ext.ppid); + assert_eq!(s.platform.pck.sgx_type, pck_ext.sgx_type as u8); - // pce_id: from raw extension bytes let expected_pce_id = match pck_ext.pce_id.len() { 2 => u16::from_be_bytes([pck_ext.pce_id[0], pck_ext.pce_id[1]]), 1 => u16::from(pck_ext.pce_id[0]), _ => 0, }; - assert_eq!(s.pce_id, expected_pce_id); + assert_eq!(s.platform.pck.pce_id, expected_pce_id); // ── TEE type ──────────────────────────────────────────────────────── assert_eq!(s.tee_type, 0x00000000); // SGX - // ── Platform instance (Processor CA → should be None/Undefined) ───── - // Sample quote uses Processor CA, so platform_instance_id should be None - // and config flags should be Undefined - assert_eq!(s.dynamic_platform, PckCertFlag::Undefined); - assert_eq!(s.cached_keys, PckCertFlag::Undefined); - assert_eq!(s.smt_enabled, PckCertFlag::Undefined); + // ── Platform instance (Processor CA → should be Undefined) ────────── + assert_eq!(s.platform.pck.dynamic_platform, PckCertFlag::Undefined); + assert_eq!(s.platform.pck.cached_keys, PckCertFlag::Undefined); + assert_eq!(s.platform.pck.smt_enabled, PckCertFlag::Undefined); // ── TCB levels ────────────────────────────────────────────────────── - assert!(!s.platform_tcb_level.tcb_date.is_empty()); - assert!(!s.qe_tcb_level.tcb_date.is_empty()); + assert!(!s.platform.tcb_level.tcb_date.is_empty()); + assert!(!s.qe.tcb_level.tcb_date.is_empty()); // Verify ring and rustcrypto produce identical supplemental data let rustcrypto_verifier = QuoteVerifier::new_prod(dcap_qvl::verify::rustcrypto::backend()); let rc_result = rustcrypto_verifier - .verify(&raw_quote, &collateral, now) + .verify(&raw_quote, collateral.clone(), now) .unwrap(); - let rc = &rc_result.supplemental; - assert_eq!(s.tcb_status, rc.tcb_status); - assert_eq!(s.advisory_ids, rc.advisory_ids); - assert_eq!(s.earliest_issue_date, rc.earliest_issue_date); - assert_eq!(s.latest_issue_date, rc.latest_issue_date); - assert_eq!(s.earliest_expiration_date, rc.earliest_expiration_date); - assert_eq!(s.tcb_level_date_tag, rc.tcb_level_date_tag); - assert_eq!(s.pck_crl_num, rc.pck_crl_num); - assert_eq!(s.root_ca_crl_num, rc.root_ca_crl_num); - assert_eq!(s.tcb_eval_data_number, rc.tcb_eval_data_number); - assert_eq!(s.root_key_id, rc.root_key_id); - assert_eq!(s.ppid, rc.ppid); - assert_eq!(s.cpu_svn, rc.cpu_svn); - assert_eq!(s.pce_svn, rc.pce_svn); - assert_eq!(s.pce_id, rc.pce_id); - assert_eq!(s.fmspc, rc.fmspc); + let rc = &rc_result.supplemental().unwrap(); + assert_eq!(s.tcb.status, rc.tcb.status); + assert_eq!(s.tcb.advisory_ids, rc.tcb.advisory_ids); + assert_eq!(s.tcb.earliest_expiration, rc.tcb.earliest_expiration); + assert_eq!(s.platform.tcb_date_tag, rc.platform.tcb_date_tag); + assert_eq!(s.platform.pck_crl_num, rc.platform.pck_crl_num); + assert_eq!(s.platform.root_ca_crl_num, rc.platform.root_ca_crl_num); + assert_eq!(s.tcb.eval_data_number, rc.tcb.eval_data_number); + assert_eq!(s.platform.root_key_id, rc.platform.root_key_id); + assert_eq!(s.platform.pck.ppid, rc.platform.pck.ppid); + assert_eq!(s.platform.pck.cpu_svn, rc.platform.pck.cpu_svn); + assert_eq!(s.platform.pck.pce_svn, rc.platform.pck.pce_svn); + assert_eq!(s.platform.pck.pce_id, rc.platform.pck.pce_id); + assert_eq!(s.platform.pck.fmspc, rc.platform.pck.fmspc); assert_eq!(s.tee_type, rc.tee_type); - assert_eq!(s.sgx_type, rc.sgx_type); - assert_eq!(s.platform_instance_id, rc.platform_instance_id); - assert_eq!(s.dynamic_platform, rc.dynamic_platform); - assert_eq!(s.cached_keys, rc.cached_keys); - assert_eq!(s.smt_enabled, rc.smt_enabled); + assert_eq!(s.platform.pck.sgx_type, rc.platform.pck.sgx_type); + assert_eq!(s.platform.pck.platform_instance_id, rc.platform.pck.platform_instance_id); + assert_eq!(s.platform.pck.dynamic_platform, rc.platform.pck.dynamic_platform); + assert_eq!(s.platform.pck.cached_keys, rc.platform.pck.cached_keys); + assert_eq!(s.platform.pck.smt_enabled, rc.platform.pck.smt_enabled); } #[test] @@ -345,7 +248,7 @@ fn could_parse_tdx_quote() { assert!(tcb_status.advisory_ids.is_empty()); } -/// Print all SupplementalData fields side-by-side: our result vs independently computed. +/// Print key SupplementalData fields for both SGX and TDX quotes. #[test] fn print_supplemental_data_comparison() { use dcap_qvl::verify::{ring, QuoteVerifier}; @@ -356,547 +259,71 @@ fn print_supplemental_data_comparison() { .unwrap_or_else(|| format!("{ts}")) } - fn parse_ts(json: &str, field: &str) -> u64 { - let v: Value = serde_json::from_str(json).unwrap(); - chrono::DateTime::parse_from_rfc3339(v[field].as_str().unwrap()) - .unwrap() - .timestamp() as u64 - } - - fn crl_dates(der: &[u8]) -> (u64, u64) { - let crl = CertificateList::from_der(der).unwrap(); - let this = crl.tbs_cert_list.this_update.to_unix_duration().as_secs(); - let next = crl - .tbs_cert_list - .next_update - .unwrap() - .to_unix_duration() - .as_secs(); - (this, next) - } - - fn extract_crl_num(crl_der: &[u8]) -> u32 { - let crl = CertificateList::from_der(crl_der).unwrap(); - if let Some(exts) = &crl.tbs_cert_list.crl_extensions { - for ext in exts.iter() { - if ext.extn_id.to_string() == "2.5.29.20" { - let num = - ::from_der(ext.extn_value.as_bytes()) - .unwrap(); - let bytes = num.as_bytes(); - let mut val: u32 = 0; - for &b in bytes { - val = (val << 8) | u32::from(b); - } - return val; - } - } - } - 0 - } - - fn cert_chain_dates_pem(pem: &[u8]) -> Vec<(u64, u64)> { - pem::parse_many(pem) - .unwrap() - .iter() - .map(|c| { - let cert: x509_cert::Certificate = DerDecode::from_der(c.contents()).unwrap(); - let nb = cert - .tbs_certificate - .validity - .not_before - .to_unix_duration() - .as_secs(); - let na = cert - .tbs_certificate - .validity - .not_after - .to_unix_duration() - .as_secs(); - (nb, na) - }) - .collect() - } - - fn cert_chain_dates_der(chain: &[Vec]) -> Vec<(u64, u64)> { - chain - .iter() - .map(|der| { - let cert: x509_cert::Certificate = DerDecode::from_der(der).unwrap(); - let nb = cert - .tbs_certificate - .validity - .not_before - .to_unix_duration() - .as_secs(); - let na = cert - .tbs_certificate - .validity - .not_after - .to_unix_duration() - .as_secs(); - (nb, na) - }) - .collect() - } - - // Compute expected time window from 8 sources - fn compute_time_window( - collateral: &QuoteCollateralV3, - pck_chain: &[Vec], - ) -> (u64, u64, u64) { - let tcb_issue = parse_ts(&collateral.tcb_info, "issueDate"); - let tcb_next = parse_ts(&collateral.tcb_info, "nextUpdate"); - let qe_issue = parse_ts(&collateral.qe_identity, "issueDate"); - let qe_next = parse_ts(&collateral.qe_identity, "nextUpdate"); - let (root_crl_this, root_crl_next) = crl_dates(&collateral.root_ca_crl); - let (pck_crl_this, pck_crl_next) = crl_dates(&collateral.pck_crl); - - let mut ei = tcb_issue.min(qe_issue).min(root_crl_this).min(pck_crl_this); - let mut li = tcb_issue.max(qe_issue).max(root_crl_this).max(pck_crl_this); - let mut ee = tcb_next.min(qe_next).min(root_crl_next).min(pck_crl_next); - - for (nb, na) in cert_chain_dates_der(pck_chain) { - ei = ei.min(nb); - li = li.max(nb); - ee = ee.min(na); - } - for pem in [ - collateral.pck_crl_issuer_chain.as_bytes(), - collateral.tcb_info_issuer_chain.as_bytes(), - collateral.qe_identity_issuer_chain.as_bytes(), - ] { - for (nb, na) in cert_chain_dates_pem(pem) { - ei = ei.min(nb); - li = li.max(nb); - ee = ee.min(na); - } - } - (ei, li, ee) - } - - fn compute_root_key_id() -> [u8; 48] { - let root_ca_der = include_bytes!("../src/TrustedRootCA.der"); - let root_cert: x509_cert::Certificate = DerDecode::from_der(root_ca_der).unwrap(); - let raw_pub_key = root_cert - .tbs_certificate - .subject_public_key_info - .subject_public_key - .raw_bytes(); - use sha2::Digest; - sha2::Sha384::digest(raw_pub_key).into() - } + let verifier = QuoteVerifier::new_prod(ring::backend()); // ═══════════════════════════════════════════════════════════════════ // SGX Quote // ═══════════════════════════════════════════════════════════════════ println!("\n{:=<80}", ""); - println!("SGX Quote — SupplementalData (ours vs independently computed)"); + println!("SGX Quote — SupplementalData"); println!("{:=<80}", ""); let raw_quote = include_bytes!("../sample/sgx_quote").to_vec(); let collateral: QuoteCollateralV3 = serde_json::from_slice(include_bytes!("../sample/sgx_quote_collateral.json")).unwrap(); let now = now_from_collateral(&collateral); - let parsed_quote = Quote::decode(&mut &raw_quote[..]).unwrap(); - let pck_chain = dcap_qvl::intel::extract_cert_chain(&parsed_quote).unwrap(); - let pck_ext = dcap_qvl::intel::parse_pck_extension(&pck_chain[0]).unwrap(); - let verifier = QuoteVerifier::new_prod(ring::backend()); - let result = verifier.verify(&raw_quote, &collateral, now).unwrap(); - let s = &result.supplemental; - - let tcb_info: dcap_qvl::TcbInfo = serde_json::from_str(&collateral.tcb_info).unwrap(); - let qe_id: dcap_qvl::QeIdentity = serde_json::from_str(&collateral.qe_identity).unwrap(); - let (exp_ei, exp_li, exp_ee) = compute_time_window(&collateral, &pck_chain); - let exp_tcb_date = chrono::DateTime::parse_from_rfc3339(&s.platform_tcb_level.tcb_date) - .unwrap() - .timestamp() as u64; - let exp_eval_num = tcb_info - .tcb_evaluation_data_number - .min(qe_id.tcb_evaluation_data_number); - let exp_root_key_id = compute_root_key_id(); - let exp_pce_id = match pck_ext.pce_id.len() { - 2 => u16::from_be_bytes([pck_ext.pce_id[0], pck_ext.pce_id[1]]), - 1 => u16::from(pck_ext.pce_id[0]), - _ => 0, - }; - - println!( - "{:<40} {:<40} {:<40}", - "Field", "Our Value", "Expected (independent)" - ); - println!("{:-<40} {:-<40} {:-<40}", "", "", ""); - println!( - "{:<40} {:<40} {:<40}", - "tcb_status", - format!("{:?}", s.tcb_status), - "ConfigurationAndSWHardeningNeeded" - ); - println!( - "{:<40} {:<40} {:<40}", - "advisory_ids", - format!("{:?}", s.advisory_ids), - "[INTEL-SA-00289, INTEL-SA-00615]" - ); - println!( - "{:<40} {:<40} {:<40}", - "earliest_issue_date", - format!( - "{} ({})", - s.earliest_issue_date, - ts_to_utc(s.earliest_issue_date) - ), - format!("{} ({})", exp_ei, ts_to_utc(exp_ei)) - ); - println!( - "{:<40} {:<40} {:<40}", - "latest_issue_date", - format!( - "{} ({})", - s.latest_issue_date, - ts_to_utc(s.latest_issue_date) - ), - format!("{} ({})", exp_li, ts_to_utc(exp_li)) - ); - println!( - "{:<40} {:<40} {:<40}", - "earliest_expiration_date", - format!( - "{} ({})", - s.earliest_expiration_date, - ts_to_utc(s.earliest_expiration_date) - ), - format!("{} ({})", exp_ee, ts_to_utc(exp_ee)) - ); - println!( - "{:<40} {:<40} {:<40}", - "tcb_level_date_tag", - format!( - "{} ({})", - s.tcb_level_date_tag, - ts_to_utc(s.tcb_level_date_tag) - ), - format!("{} ({})", exp_tcb_date, ts_to_utc(exp_tcb_date)) - ); - println!( - "{:<40} {:<40} {:<40}", - "pck_crl_num", - format!("{}", s.pck_crl_num), - format!("{}", extract_crl_num(&collateral.pck_crl)) - ); - println!( - "{:<40} {:<40} {:<40}", - "root_ca_crl_num", - format!("{}", s.root_ca_crl_num), - format!("{}", extract_crl_num(&collateral.root_ca_crl)) - ); - println!( - "{:<40} {:<40} {:<40}", - "tcb_eval_data_number", - format!("{}", s.tcb_eval_data_number), - format!( - "{} (min of {} and {})", - exp_eval_num, tcb_info.tcb_evaluation_data_number, qe_id.tcb_evaluation_data_number - ) - ); - println!( - "{:<40} {:<40} {:<40}", - "root_key_id", - hex::encode(&s.root_key_id[..24]) + "...", - hex::encode(&exp_root_key_id[..24]) + "..." - ); - println!( - "{:<40} {:<40} {:<40}", - "ppid", - hex::encode(&s.ppid), - hex::encode(&pck_ext.ppid) - ); - println!( - "{:<40} {:<40} {:<40}", - "cpu_svn", - hex::encode(s.cpu_svn), - hex::encode(pck_ext.cpu_svn) - ); - println!( - "{:<40} {:<40} {:<40}", - "pce_svn", - format!("{}", s.pce_svn), - format!("{}", pck_ext.pce_svn) - ); - println!( - "{:<40} {:<40} {:<40}", - "pce_id", - format!("{}", s.pce_id), - format!("{}", exp_pce_id) - ); - println!( - "{:<40} {:<40} {:<40}", - "fmspc", - hex::encode(s.fmspc), - hex::encode(pck_ext.fmspc) - ); - println!( - "{:<40} {:<40} {:<40}", - "tee_type", - format!("0x{:08X}", s.tee_type), - "0x00000000 (SGX)" - ); - println!( - "{:<40} {:<40} {:<40}", - "sgx_type", - format!("{}", s.sgx_type), - format!("{}", pck_ext.sgx_type) - ); - println!( - "{:<40} {:<40} {:<40}", - "platform_instance_id", - format!("{:?}", s.platform_instance_id), - format!("{:?}", pck_ext.platform_instance_id) - ); - println!( - "{:<40} {:<40} {:<40}", - "dynamic_platform", - format!("{:?}", s.dynamic_platform), - format!("{:?}", pck_ext.dynamic_platform) - ); - println!( - "{:<40} {:<40} {:<40}", - "cached_keys", - format!("{:?}", s.cached_keys), - format!("{:?}", pck_ext.cached_keys) - ); - println!( - "{:<40} {:<40} {:<40}", - "smt_enabled", - format!("{:?}", s.smt_enabled), - format!("{:?}", pck_ext.smt_enabled) - ); - println!( - "{:<40} {:<40}", - "platform_tcb_level.tcb_date", &s.platform_tcb_level.tcb_date - ); - println!( - "{:<40} {:<40}", - "platform_tcb_level.tcb_status", - format!("{:?}", s.platform_tcb_level.tcb_status) - ); - println!( - "{:<40} {:<40}", - "qe_tcb_level.tcb_date", &s.qe_tcb_level.tcb_date - ); - println!( - "{:<40} {:<40}", - "qe_tcb_level.tcb_status", - format!("{:?}", s.qe_tcb_level.tcb_status) - ); + let result = verifier.verify(&raw_quote, collateral.clone(), now).unwrap(); + let s = &result.supplemental().unwrap(); + + println!("{:<40} {:?}", "tcb.status", s.tcb.status); + println!("{:<40} {:?}", "tcb.advisory_ids", s.tcb.advisory_ids); + println!("{:<40} {} ({})", "tcb.earliest_expiration", s.tcb.earliest_expiration, ts_to_utc(s.tcb.earliest_expiration)); + println!("{:<40} {}", "tcb.eval_data_number", s.tcb.eval_data_number); + println!("{:<40} {} ({})", "platform.tcb_date_tag", s.platform.tcb_date_tag, ts_to_utc(s.platform.tcb_date_tag)); + println!("{:<40} {}", "platform.pck_crl_num", s.platform.pck_crl_num); + println!("{:<40} {}", "platform.root_ca_crl_num", s.platform.root_ca_crl_num); + println!("{:<40} {}...", "platform.root_key_id", hex::encode(&s.platform.root_key_id[..24])); + println!("{:<40} {}", "platform.pck.fmspc", hex::encode(s.platform.pck.fmspc)); + println!("{:<40} {}", "platform.pck.sgx_type", s.platform.pck.sgx_type); + println!("{:<40} {:?}", "platform.pck.dynamic_platform", s.platform.pck.dynamic_platform); + println!("{:<40} {:?}", "platform.pck.cached_keys", s.platform.pck.cached_keys); + println!("{:<40} {:?}", "platform.pck.smt_enabled", s.platform.pck.smt_enabled); + println!("{:<40} 0x{:08X}", "tee_type", s.tee_type); + println!("{:<40} {:?}", "platform.tcb_level.tcb_status", s.platform.tcb_level.tcb_status); + println!("{:<40} {:?}", "qe.tcb_level.tcb_status", s.qe.tcb_level.tcb_status); // ═══════════════════════════════════════════════════════════════════ // TDX Quote // ═══════════════════════════════════════════════════════════════════ println!("\n{:=<80}", ""); - println!("TDX Quote — SupplementalData (ours vs independently computed)"); + println!("TDX Quote — SupplementalData"); println!("{:=<80}", ""); let raw_quote_tdx = include_bytes!("../sample/tdx_quote"); let collateral_tdx: QuoteCollateralV3 = serde_json::from_slice(include_bytes!("../sample/tdx_quote_collateral.json")).unwrap(); let now_tdx = now_from_collateral(&collateral_tdx); - let parsed_quote_tdx = Quote::decode(&mut &raw_quote_tdx[..]).unwrap(); - let pck_chain_tdx = dcap_qvl::intel::extract_cert_chain(&parsed_quote_tdx).unwrap(); - let pck_ext_tdx = dcap_qvl::intel::parse_pck_extension(&pck_chain_tdx[0]).unwrap(); - - let result_tdx = verifier - .verify(raw_quote_tdx, &collateral_tdx, now_tdx) - .unwrap(); - let t = &result_tdx.supplemental; - - let tcb_info_tdx: dcap_qvl::TcbInfo = serde_json::from_str(&collateral_tdx.tcb_info).unwrap(); - let qe_id_tdx: dcap_qvl::QeIdentity = - serde_json::from_str(&collateral_tdx.qe_identity).unwrap(); - let (exp_ei_t, exp_li_t, exp_ee_t) = compute_time_window(&collateral_tdx, &pck_chain_tdx); - let exp_tcb_date_t = chrono::DateTime::parse_from_rfc3339(&t.platform_tcb_level.tcb_date) - .unwrap() - .timestamp() as u64; - let exp_eval_num_t = tcb_info_tdx - .tcb_evaluation_data_number - .min(qe_id_tdx.tcb_evaluation_data_number); - let exp_pce_id_t = match pck_ext_tdx.pce_id.len() { - 2 => u16::from_be_bytes([pck_ext_tdx.pce_id[0], pck_ext_tdx.pce_id[1]]), - 1 => u16::from(pck_ext_tdx.pce_id[0]), - _ => 0, - }; - println!( - "{:<40} {:<40} {:<40}", - "Field", "Our Value", "Expected (independent)" - ); - println!("{:-<40} {:-<40} {:-<40}", "", "", ""); - println!( - "{:<40} {:<40} {:<40}", - "tcb_status", - format!("{:?}", t.tcb_status), - "UpToDate" - ); - println!( - "{:<40} {:<40} {:<40}", - "advisory_ids", - format!("{:?}", t.advisory_ids), - "[]" - ); - println!( - "{:<40} {:<40} {:<40}", - "earliest_issue_date", - format!( - "{} ({})", - t.earliest_issue_date, - ts_to_utc(t.earliest_issue_date) - ), - format!("{} ({})", exp_ei_t, ts_to_utc(exp_ei_t)) - ); - println!( - "{:<40} {:<40} {:<40}", - "latest_issue_date", - format!( - "{} ({})", - t.latest_issue_date, - ts_to_utc(t.latest_issue_date) - ), - format!("{} ({})", exp_li_t, ts_to_utc(exp_li_t)) - ); - println!( - "{:<40} {:<40} {:<40}", - "earliest_expiration_date", - format!( - "{} ({})", - t.earliest_expiration_date, - ts_to_utc(t.earliest_expiration_date) - ), - format!("{} ({})", exp_ee_t, ts_to_utc(exp_ee_t)) - ); - println!( - "{:<40} {:<40} {:<40}", - "tcb_level_date_tag", - format!( - "{} ({})", - t.tcb_level_date_tag, - ts_to_utc(t.tcb_level_date_tag) - ), - format!("{} ({})", exp_tcb_date_t, ts_to_utc(exp_tcb_date_t)) - ); - println!( - "{:<40} {:<40} {:<40}", - "pck_crl_num", - format!("{}", t.pck_crl_num), - format!("{}", extract_crl_num(&collateral_tdx.pck_crl)) - ); - println!( - "{:<40} {:<40} {:<40}", - "root_ca_crl_num", - format!("{}", t.root_ca_crl_num), - format!("{}", extract_crl_num(&collateral_tdx.root_ca_crl)) - ); - println!( - "{:<40} {:<40} {:<40}", - "tcb_eval_data_number", - format!("{}", t.tcb_eval_data_number), - format!( - "{} (min of {} and {})", - exp_eval_num_t, - tcb_info_tdx.tcb_evaluation_data_number, - qe_id_tdx.tcb_evaluation_data_number - ) - ); - println!( - "{:<40} {:<40} {:<40}", - "root_key_id", - hex::encode(&t.root_key_id[..24]) + "...", - hex::encode(&exp_root_key_id[..24]) + "..." - ); - println!( - "{:<40} {:<40} {:<40}", - "ppid", - hex::encode(&t.ppid), - hex::encode(&pck_ext_tdx.ppid) - ); - println!( - "{:<40} {:<40} {:<40}", - "cpu_svn", - hex::encode(t.cpu_svn), - hex::encode(pck_ext_tdx.cpu_svn) - ); - println!( - "{:<40} {:<40} {:<40}", - "pce_svn", - format!("{}", t.pce_svn), - format!("{}", pck_ext_tdx.pce_svn) - ); - println!( - "{:<40} {:<40} {:<40}", - "pce_id", - format!("{}", t.pce_id), - format!("{}", exp_pce_id_t) - ); - println!( - "{:<40} {:<40} {:<40}", - "fmspc", - hex::encode(t.fmspc), - hex::encode(pck_ext_tdx.fmspc) - ); - println!( - "{:<40} {:<40} {:<40}", - "tee_type", - format!("0x{:08X}", t.tee_type), - "0x00000081 (TDX)" - ); - println!( - "{:<40} {:<40} {:<40}", - "sgx_type", - format!("{}", t.sgx_type), - format!("{}", pck_ext_tdx.sgx_type) - ); - println!( - "{:<40} {:<40} {:<40}", - "platform_instance_id", - format!("{:?}", t.platform_instance_id), - format!("{:?}", pck_ext_tdx.platform_instance_id) - ); - println!( - "{:<40} {:<40} {:<40}", - "dynamic_platform", - format!("{:?}", t.dynamic_platform), - format!("{:?}", pck_ext_tdx.dynamic_platform) - ); - println!( - "{:<40} {:<40} {:<40}", - "cached_keys", - format!("{:?}", t.cached_keys), - format!("{:?}", pck_ext_tdx.cached_keys) - ); - println!( - "{:<40} {:<40} {:<40}", - "smt_enabled", - format!("{:?}", t.smt_enabled), - format!("{:?}", pck_ext_tdx.smt_enabled) - ); - println!( - "{:<40} {:<40}", - "platform_tcb_level.tcb_date", &t.platform_tcb_level.tcb_date - ); - println!( - "{:<40} {:<40}", - "platform_tcb_level.tcb_status", - format!("{:?}", t.platform_tcb_level.tcb_status) - ); - println!( - "{:<40} {:<40}", - "qe_tcb_level.tcb_date", &t.qe_tcb_level.tcb_date - ); - println!( - "{:<40} {:<40}", - "qe_tcb_level.tcb_status", - format!("{:?}", t.qe_tcb_level.tcb_status) - ); + let result_tdx = verifier.verify(raw_quote_tdx, collateral_tdx.clone(), now_tdx).unwrap(); + let t = &result_tdx.supplemental().unwrap(); + + println!("{:<40} {:?}", "tcb.status", t.tcb.status); + println!("{:<40} {:?}", "tcb.advisory_ids", t.tcb.advisory_ids); + println!("{:<40} {} ({})", "tcb.earliest_expiration", t.tcb.earliest_expiration, ts_to_utc(t.tcb.earliest_expiration)); + println!("{:<40} {}", "tcb.eval_data_number", t.tcb.eval_data_number); + println!("{:<40} {} ({})", "platform.tcb_date_tag", t.platform.tcb_date_tag, ts_to_utc(t.platform.tcb_date_tag)); + println!("{:<40} {}", "platform.pck_crl_num", t.platform.pck_crl_num); + println!("{:<40} {}", "platform.root_ca_crl_num", t.platform.root_ca_crl_num); + println!("{:<40} {}...", "platform.root_key_id", hex::encode(&t.platform.root_key_id[..24])); + println!("{:<40} {}", "platform.pck.fmspc", hex::encode(t.platform.pck.fmspc)); + println!("{:<40} {}", "platform.pck.sgx_type", t.platform.pck.sgx_type); + println!("{:<40} {:?}", "platform.pck.dynamic_platform", t.platform.pck.dynamic_platform); + println!("{:<40} {:?}", "platform.pck.cached_keys", t.platform.pck.cached_keys); + println!("{:<40} {:?}", "platform.pck.smt_enabled", t.platform.pck.smt_enabled); + println!("{:<40} 0x{:08X}", "tee_type", t.tee_type); + println!("{:<40} {:?}", "platform.tcb_level.tcb_status", t.platform.tcb_level.tcb_status); + println!("{:<40} {:?}", "qe.tcb_level.tcb_status", t.qe.tcb_level.tcb_status); } /// Cross-validate TDX supplemental data fields. @@ -910,19 +337,17 @@ fn tdx_supplemental_data_cross_validation() { let now = now_from_collateral(&collateral); let verifier = QuoteVerifier::new_prod(ring::backend()); - let result = verifier.verify(raw_quote, &collateral, now).unwrap(); - let s = &result.supplemental; + let result = verifier.verify(raw_quote, collateral.clone(), now).unwrap(); + let s = &result.supplemental().unwrap(); // TDX quote should have tee_type = 0x81 assert_eq!(s.tee_type, 0x00000081); - assert_eq!(s.tcb_status.to_string(), "UpToDate"); - assert!(s.advisory_ids.is_empty()); + assert_eq!(s.tcb.status.to_string(), "UpToDate"); + assert!(s.tcb.advisory_ids.is_empty()); - // Time window fields should be populated - assert!(s.earliest_issue_date > 0); - assert!(s.latest_issue_date >= s.earliest_issue_date); - assert!(s.earliest_expiration_date > s.latest_issue_date); - assert!(s.tcb_level_date_tag > 0); + // Fields should be populated (computed lazily in supplemental()) + assert!(s.tcb.earliest_expiration > 0); + assert!(s.platform.tcb_date_tag > 0); // root_key_id should match SHA-384 of Intel root CA raw public key bytes let root_ca_der = include_bytes!("../src/TrustedRootCA.der"); @@ -936,15 +361,15 @@ fn tdx_supplemental_data_cross_validation() { use sha2::Digest; sha2::Sha384::digest(raw_pub_key).into() }; - assert_eq!(s.root_key_id, expected_root_key_id); + assert_eq!(s.platform.root_key_id, expected_root_key_id); // Verify ring == rustcrypto for all fields let rc_verifier = QuoteVerifier::new_prod(dcap_qvl::verify::rustcrypto::backend()); - let rc_result = rc_verifier.verify(raw_quote, &collateral, now).unwrap(); - let rc = &rc_result.supplemental; + let rc_result = rc_verifier.verify(raw_quote, collateral.clone(), now).unwrap(); + let rc = &rc_result.supplemental().unwrap(); assert_eq!(s.tee_type, rc.tee_type); - assert_eq!(s.tcb_status, rc.tcb_status); - assert_eq!(s.root_key_id, rc.root_key_id); - assert_eq!(s.earliest_issue_date, rc.earliest_issue_date); - assert_eq!(s.tcb_eval_data_number, rc.tcb_eval_data_number); + assert_eq!(s.tcb.status, rc.tcb.status); + assert_eq!(s.platform.root_key_id, rc.platform.root_key_id); + assert_eq!(s.tcb.earliest_expiration, rc.tcb.earliest_expiration); + assert_eq!(s.tcb.eval_data_number, rc.tcb.eval_data_number); } From 2268e84de7445b1483196ee9623393659ee584a6 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 6 Mar 2026 04:33:22 +0000 Subject: [PATCH 04/33] fix: eliminate redundant JSON/CRL parsing in Rego path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rego validation was parsing TcbInfo + QeIdentity JSON twice: once in supplemental() → compute_earliest_expiration(), and again in compute_time_window(). Now Rego path uses build_supplemental(tw.earliest_expiration_date) to reuse the value from compute_time_window() directly. --- src/verify.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/verify.rs b/src/verify.rs index f5bd43e..bfdf886 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -125,10 +125,20 @@ impl QuoteVerificationResult { /// This is lazy — the expensive parts (root_key_id SHA-384, CRL number extraction, /// earliest_expiration from 4 sources, tcb_date_tag parsing) are only done here. pub fn supplemental(&self) -> Result { + let earliest_expiration = self.compute_earliest_expiration()?; + Ok(self.build_supplemental(earliest_expiration)) + } + + /// Build `SupplementalData` with a pre-computed earliest_expiration value. + /// + /// Avoids redundant JSON/CRL parsing when the caller already has the value + /// (e.g., from `compute_time_window()` which produces a superset). + fn build_supplemental(&self, earliest_expiration: u64) -> SupplementalData { // root_key_id: SHA-384 of root CA's raw public key bytes let root_key_id = { + // root_ca_der was already validated during verify(), so unwrap is safe let root_cert: x509_cert::Certificate = - der::Decode::from_der(&self.root_ca_der).context("Failed to parse root CA for SPKI")?; + der::Decode::from_der(&self.root_ca_der).expect("root CA already validated"); let raw_key = root_cert .tbs_certificate .subject_public_key_info @@ -147,10 +157,7 @@ impl QuoteVerificationResult { .map(|dt| dt.timestamp() as u64) .unwrap_or(0); - // earliest_expiration from 4 lightweight sources - let earliest_expiration = self.compute_earliest_expiration()?; - - Ok(SupplementalData { + SupplementalData { tee_type: self.tee_type, tcb: TcbVerdict { status: self.tcb_status.clone(), @@ -183,7 +190,7 @@ impl QuoteVerificationResult { report: self.qe_report.clone(), tcb_eval_data_number: self.qe_tcb_eval_data_number, }, - }) + } } /// Validate against a policy, consuming self into [`VerifiedReport`] on success. @@ -336,16 +343,16 @@ impl QuoteVerificationResult { self, policy: &crate::policy::RegoPolicy, ) -> Result { - let supplemental = self.supplemental()?; let tw = self.compute_time_window()?; + let supplemental = self.build_supplemental(tw.earliest_expiration_date); policy.eval(&supplemental, &tw)?; Ok(self.into_verified_report()) } /// Validate against a [`RegoPolicySet`] (multi-measurement), consuming self on success. pub fn validate_rego(self, policies: &crate::policy::RegoPolicySet) -> Result { - let supplemental = self.supplemental()?; let tw = self.compute_time_window()?; + let supplemental = self.build_supplemental(tw.earliest_expiration_date); let qvl_result = self.to_rego_qvl_result(&supplemental, &tw); policies.eval_rego(qvl_result)?; Ok(self.into_verified_report()) From f8d63d7a829d8d3165b4626a2ce126e237b528c2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 02:57:34 +0000 Subject: [PATCH 05/33] =?UTF-8?q?refactor:=20rename=20QuotePolicy=E2=86=92?= =?UTF-8?q?SimplePolicy,=20split=20policy=20into=20submodules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split monolithic src/policy.rs into policy/{mod,simple,rego}.rs - Rename QuotePolicy to SimplePolicy for clarity - Advisory skip only during platform grace for pure OutOfDate (collateral grace and OutOfDateConfigurationNeeded don't skip) - Update all imports and re-exports across lib, cli, verify --- cli/src/main.rs | 2 +- src/lib.rs | 6 +- src/policy.rs | 1659 ---------------------- src/policy/mod.rs | 157 ++ src/policy/rego.rs | 914 ++++++++++++ src/policy/simple.rs | 673 +++++++++ src/verify.rs | 10 +- tests/near/contracts/gas-test/Cargo.lock | 2 +- 8 files changed, 1754 insertions(+), 1669 deletions(-) delete mode 100644 src/policy.rs create mode 100644 src/policy/mod.rs create mode 100644 src/policy/rego.rs create mode 100644 src/policy/simple.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index ceae928..fdce903 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,7 +10,7 @@ use dcap_qvl::collateral::{get_collateral, PHALA_PCCS_URL}; use dcap_qvl::intel; use dcap_qvl::quote::Quote; use dcap_qvl::verify::{ring, QuoteVerifier}; -use dcap_qvl::QuotePolicy; +use dcap_qvl::SimplePolicy; use der::Decode; use serde::Serialize; use x509_cert::Certificate; diff --git a/src/lib.rs b/src/lib.rs index 3a787e5..ddf26b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ //! ```no_run //! use dcap_qvl::collateral::get_collateral; //! use dcap_qvl::verify::{QuoteVerifier, ring}; -//! use dcap_qvl::QuotePolicy; +//! use dcap_qvl::SimplePolicy; //! use dcap_qvl::PHALA_PCCS_URL; //! //! #[tokio::main] @@ -33,7 +33,7 @@ //! let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); //! let verifier = QuoteVerifier::new_prod(ring::backend()); //! let result = verifier.verify("e, collateral, now).expect("verification failed"); -//! let report = result.validate(&QuotePolicy::strict(now)).expect("policy validation failed"); +//! let report = result.validate(&SimplePolicy::strict(now)).expect("policy validation failed"); //! println!("{:?}", report); //! } //! ``` @@ -97,7 +97,7 @@ pub use constants::{CpuSvn, Fmspc, MrEnclave, MrSigner, Svn}; pub use qe_identity::{QeIdentity, QeTcb, QeTcbLevel}; pub use tcb_info::{Tcb, TcbComponents, TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory}; pub use policy::{ - PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, QuotePolicy, SupplementalData, + PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, SimplePolicy, SupplementalData, TcbVerdict, }; pub use verify::QuoteVerificationResult; diff --git a/src/policy.rs b/src/policy.rs deleted file mode 100644 index d44627a..0000000 --- a/src/policy.rs +++ /dev/null @@ -1,1659 +0,0 @@ -use core::time::Duration; - -use anyhow::{bail, Result}; - -use { - crate::constants::*, - crate::qe_identity::QeTcbLevel, - crate::quote::EnclaveReport, - crate::tcb_info::{TcbLevel, TcbStatus}, - alloc::string::String, - alloc::vec::Vec, -}; - - -/// Policy trait for customizing quote verification behavior. -/// -/// Implement this trait to define custom validation logic for [`SupplementalData`]. -/// The library provides [`QuotePolicy`] as a comprehensive built-in implementation -/// that covers all common checks from Intel's Appraisal framework. -/// -/// For most use cases, [`QuotePolicy`] with its builder methods is sufficient: -/// ```ignore -/// use dcap_qvl::QuotePolicy; -/// use dcap_qvl::TcbStatus; -/// -/// use core::time::Duration; -/// -/// let policy = QuotePolicy::strict(now_unix_secs) -/// .allow_status(TcbStatus::SWHardeningNeeded) -/// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) -/// .accept_advisory("INTEL-SA-00334"); -/// ``` -/// -/// Implement this trait directly only for logic that [`QuotePolicy`] cannot express. -pub trait Policy { - /// Validate supplemental data against this policy. - /// - /// Return `Ok(())` to accept, or `Err(...)` to reject. - fn validate(&self, data: &SupplementalData) -> Result<()>; -} - -/// Status-based verification policy. -/// -/// By default, the policy is strict: only `UpToDate` status is accepted. -/// Comprehensive verification policy with builder pattern. -/// -/// Covers all checks from Intel's Appraisal framework (`qal_script.rego`). -/// Strict by default: only `UpToDate`, no grace period, no advisory tolerance. -/// -/// # Example -/// ```ignore -/// use dcap_qvl::QuotePolicy; -/// use dcap_qvl::TcbStatus; -/// -/// // Strict: only UpToDate, collateral must not be expired -/// let policy = QuotePolicy::strict(now); -/// -/// // With 90-day collateral grace period -/// use core::time::Duration; -/// let policy = QuotePolicy::strict(now) -/// .allow_status(TcbStatus::SWHardeningNeeded) -/// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) -/// .accept_advisory("INTEL-SA-00334"); -/// ``` -#[derive(Clone, Debug)] -pub struct QuotePolicy { - acceptable_statuses: u8, - - // Current time + grace periods (mutually exclusive, default 0 = no tolerance) - now: u64, - collateral_grace_period: u64, - platform_grace_period: u64, - - // TCB evaluation - min_tcb_eval_data_number: Option, - - // Advisory whitelist (all advisories in quote must be in this set) - accepted_advisory_ids: Vec, - - // Platform flags (default false = reject if True) - allow_dynamic_platform: bool, - allow_cached_keys: bool, - allow_smt: bool, - - // SGX type whitelist (None = skip check) - accepted_sgx_types: Option>, -} - -impl QuotePolicy { - const UP_TO_DATE: u8 = 1 << 0; - const SW_HARDENING_NEEDED: u8 = 1 << 1; - const CONFIGURATION_NEEDED: u8 = 1 << 2; - const CONFIGURATION_AND_SW_HARDENING_NEEDED: u8 = 1 << 3; - const OUT_OF_DATE: u8 = 1 << 4; - const OUT_OF_DATE_CONFIGURATION_NEEDED: u8 = 1 << 5; - - fn status_to_flag(status: TcbStatus) -> u8 { - match status { - TcbStatus::UpToDate => Self::UP_TO_DATE, - TcbStatus::SWHardeningNeeded => Self::SW_HARDENING_NEEDED, - TcbStatus::ConfigurationNeeded => Self::CONFIGURATION_NEEDED, - TcbStatus::ConfigurationAndSWHardeningNeeded => { - Self::CONFIGURATION_AND_SW_HARDENING_NEEDED - } - TcbStatus::OutOfDate => Self::OUT_OF_DATE, - TcbStatus::OutOfDateConfigurationNeeded => Self::OUT_OF_DATE_CONFIGURATION_NEEDED, - TcbStatus::Revoked => 0, - } - } - - fn new_with_statuses(now: u64, acceptable_statuses: u8) -> Self { - Self { - acceptable_statuses, - now, - collateral_grace_period: 0, - platform_grace_period: 0, - min_tcb_eval_data_number: None, - accepted_advisory_ids: Vec::new(), - allow_dynamic_platform: false, - allow_cached_keys: false, - allow_smt: false, - accepted_sgx_types: None, - } - } - - /// Create a strict policy: only `UpToDate` status is accepted, - /// no grace period, no advisory tolerance. - pub fn strict(now_secs: u64) -> Self { - Self::new_with_statuses(now_secs, Self::UP_TO_DATE) - } - - /// Allow an additional TCB status. - pub fn allow_status(mut self, status: TcbStatus) -> Self { - self.acceptable_statuses |= Self::status_to_flag(status); - self - } - - /// Set collateral grace period (default: zero). Accepts quotes where - /// `earliest_expiration_date + grace_period >= now`. - /// - /// Must be zero if [`platform_grace_period`](Self::platform_grace_period) is non-zero. - pub fn collateral_grace_period(mut self, duration: Duration) -> Self { - self.collateral_grace_period = duration.as_secs(); - self - } - - /// Set platform grace period (default: zero). When TCB status is - /// OutOfDate or OutOfDateConfigurationNeeded, accepts quotes where - /// `tcb_level_date_tag + grace_period >= now`. Skipped for UpToDate/ConfigNeeded/SWHardening. - /// - /// Must be zero if [`collateral_grace_period`](Self::collateral_grace_period) is non-zero. - pub fn platform_grace_period(mut self, duration: Duration) -> Self { - self.platform_grace_period = duration.as_secs(); - self - } - - /// Set minimum TCB evaluation data number. Rejects quotes with - /// `tcb_eval_data_number` below this threshold. - pub fn min_tcb_eval_data_number(mut self, min: u32) -> Self { - self.min_tcb_eval_data_number = Some(min); - self - } - - /// Accept a specific advisory ID. All advisories in the quote must be in - /// the accepted set or validation fails. By default the set is empty, - /// rejecting any quote with advisories. - pub fn accept_advisory(mut self, id: impl Into) -> Self { - self.accepted_advisory_ids.push(id.into()); - self - } - - /// Set whether dynamic platforms are allowed. If `false` (default), rejects - /// quotes where `dynamic_platform` is `True`. - pub fn allow_dynamic_platform(mut self, allow: bool) -> Self { - self.allow_dynamic_platform = allow; - self - } - - /// Set whether cached keys are allowed. If `false` (default), rejects - /// quotes where `cached_keys` is `True`. - pub fn allow_cached_keys(mut self, allow: bool) -> Self { - self.allow_cached_keys = allow; - self - } - - /// Set whether SMT (simultaneous multithreading / hyperthreading) is allowed. - /// If `false` (default), rejects quotes where `smt_enabled` is `True`. - pub fn allow_smt(mut self, allow: bool) -> Self { - self.allow_smt = allow; - self - } - - /// Set accepted SGX types (0=Standard, 1=Scalable, 2=ScalableWithIntegrity). - /// Rejects quotes with `sgx_type` not in this list. Default: skip check. - pub fn accepted_sgx_types(mut self, types: &[u8]) -> Self { - self.accepted_sgx_types = Some(types.to_vec()); - self - } - - /// Check if a TCB status is acceptable according to this policy. - pub fn is_status_acceptable(&self, status: TcbStatus) -> bool { - let flag = Self::status_to_flag(status); - (self.acceptable_statuses & flag) != 0 - } -} - -impl Policy for QuotePolicy { - fn validate(&self, data: &SupplementalData) -> Result<()> { - // 1. TCB status whitelist - if !self.is_status_acceptable(data.tcb.status) { - bail!( - "TCB status {:?} is not acceptable by policy", - data.tcb.status - ); - } - - // 2. Advisory ID whitelist - for id in &data.tcb.advisory_ids { - if !self - .accepted_advisory_ids - .iter() - .any(|a| a.eq_ignore_ascii_case(id)) - { - bail!("Advisory ID {id} is not in the accepted set"); - } - } - - // 3 & 4. Grace periods (mutually exclusive) - if self.collateral_grace_period > 0 && self.platform_grace_period > 0 { - bail!("collateral_grace_period and platform_grace_period are mutually exclusive"); - } - - // 3. Collateral expiration: earliest_expiration + grace >= now - if data - .tcb - .earliest_expiration - .saturating_add(self.collateral_grace_period) - < self.now - { - bail!( - "Collateral expired: earliest_expiration {} + grace {} < now {}", - data.tcb.earliest_expiration, - self.collateral_grace_period, - self.now - ); - } - - // 4. Platform TCB freshness: tcb_date_tag + grace >= now - // Only checked when TCB status indicates the platform is out-of-date. - { - let is_out_of_date = matches!( - data.tcb.status, - TcbStatus::OutOfDate | TcbStatus::OutOfDateConfigurationNeeded - ); - if is_out_of_date - && data - .platform - .tcb_date_tag - .saturating_add(self.platform_grace_period) - < self.now - { - bail!( - "Platform TCB too old: tcb_date_tag {} + grace {} < now {}", - data.platform.tcb_date_tag, - self.platform_grace_period, - self.now - ); - } - } - - // 5. Minimum TCB evaluation data number - if let Some(min) = self.min_tcb_eval_data_number { - if data.tcb.eval_data_number < min { - bail!( - "TCB eval data number {} is below minimum {}", - data.tcb.eval_data_number, - min - ); - } - } - - // 6. Dynamic platform flag - if !self.allow_dynamic_platform && data.platform.pck.dynamic_platform == PckCertFlag::True { - bail!("Dynamic platform is not allowed by policy"); - } - - // 7. Cached keys flag - if !self.allow_cached_keys && data.platform.pck.cached_keys == PckCertFlag::True { - bail!("Cached keys are not allowed by policy"); - } - - // 8. SMT flag - if !self.allow_smt && data.platform.pck.smt_enabled == PckCertFlag::True { - bail!("SMT (hyperthreading) is not allowed by policy"); - } - - // 9. SGX type whitelist - if let Some(ref types) = self.accepted_sgx_types { - if !types.contains(&data.platform.pck.sgx_type) { - bail!( - "SGX type {} is not in accepted types {:?}", - data.platform.pck.sgx_type, - types - ); - } - } - - Ok(()) - } -} - -/// PCK certificate flag, matching Intel's `pck_cert_flag_enum_t`. -/// -/// These flags are only present in PCK certificates issued by the **Platform CA**. -/// For Processor CA certificates, the value is [`Undefined`](PckCertFlag::Undefined). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PckCertFlag { - /// The flag is explicitly false (ASN.1 BOOLEAN FALSE). - False = 0, - /// The flag is explicitly true (ASN.1 BOOLEAN TRUE). - True = 1, - /// The flag is not present in the certificate (Processor CA certs). - Undefined = 2, -} - -impl From> for PckCertFlag { - fn from(v: Option) -> Self { - match v { - Some(true) => PckCertFlag::True, - Some(false) => PckCertFlag::False, - None => PckCertFlag::Undefined, - } - } -} - -/// Supplemental data from quote verification. -/// -/// Organized into structured sub-groups: -/// - [`tcb`](Self::tcb): Merged TCB verdict -/// - [`platform`](Self::platform): Platform-level details from PCK certificate and TCB matching -/// - [`qe`](Self::qe): QE (Quoting Enclave) verification results -pub struct SupplementalData { - /// TEE type: `0x00000000` for SGX, `0x00000081` for TDX. - pub tee_type: u32, - /// Merged TCB verdict (worst of platform + QE). - pub tcb: TcbVerdict, - /// Platform verification details. - pub platform: PlatformInfo, - /// QE verification details. - pub qe: QeInfo, -} - -/// Merged TCB verdict from platform and QE status convergence. -/// -/// Uses Intel's `convergeTcbStatusWithQeTcbStatus` logic to produce the -/// worst-case status and union of advisory IDs. -pub struct TcbVerdict { - /// Merged TCB status (worst of platform TCB + QE TCB). - pub status: TcbStatus, - /// Merged advisory IDs (union of platform + QE advisories). - pub advisory_ids: Vec, - /// Lower of TCBInfo and QEIdentity `tcbEvaluationDataNumber` values. - pub eval_data_number: u32, - /// Earliest expiration from TCBInfo nextUpdate, QEIdentity nextUpdate, - /// and CRL nextUpdate (4 sources). Does **not** include certificate chain - /// notAfter — use full time window via Rego for that. - pub earliest_expiration: u64, -} - -/// Platform-level verification results. -pub struct PlatformInfo { - /// The matched platform TCB level (unmerged). - pub tcb_level: TcbLevel, - /// Platform TCB level date as unix timestamp (precomputed from `tcb_level.tcb_date`). - pub tcb_date_tag: u64, - /// PCK certificate identity fields. - pub pck: PckIdentity, - /// SHA-384 of root CA's raw public key bytes, matching Intel's `root_key_id`. - pub root_key_id: [u8; 48], - /// CRL number from PCK Certificate Revocation List. - pub pck_crl_num: u32, - /// CRL number from Root CA Certificate Revocation List. - pub root_ca_crl_num: u32, -} - -/// QE (Quoting Enclave) verification results. -pub struct QeInfo { - /// The matched QE TCB level (unmerged). - pub tcb_level: QeTcbLevel, - /// The QE's enclave report. - pub report: EnclaveReport, - /// TCB evaluation data number from QE Identity (unmerged). - pub tcb_eval_data_number: u32, -} - -/// PCK certificate identity fields. -pub struct PckIdentity { - /// Platform Provisioning ID (PPID). - pub ppid: Vec, - /// CPU Security Version Number (16 bytes). - pub cpu_svn: CpuSvn, - /// PCE ISV Security Version Number. - pub pce_svn: Svn, - /// PCE ID. - pub pce_id: u16, - /// FMSPC (6 bytes). - pub fmspc: Fmspc, - /// SGX type: 0=Standard, 1=Scalable, 2=ScalableWithIntegrity. - pub sgx_type: u8, - /// Platform Instance ID (16 bytes, Platform CA only). - pub platform_instance_id: Option<[u8; 16]>, - /// Dynamic platform flag. - pub dynamic_platform: PckCertFlag, - /// Cached keys flag. - pub cached_keys: PckCertFlag, - /// SMT (hyperthreading) flag. - pub smt_enabled: PckCertFlag, - /// Platform Provider ID (Platform CA only, for Rego). - pub platform_provider_id: Option, -} - -// ============================================================================= -// RegoPolicy — Intel QAL-compatible policy evaluation via regorus -// ============================================================================= - -#[cfg(feature = "rego")] -pub(crate) mod rego_policy { - use super::*; - use serde_json::json; - - /// Convert a unix timestamp (seconds) to an RFC3339 string. - /// Returns an empty string for timestamp 0 (matching Intel's behavior of omitting the field). - fn unix_to_rfc3339(secs: u64) -> String { - chrono::DateTime::from_timestamp(secs as i64, 0) - .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) - .unwrap_or_default() - } - - /// Convert `TcbStatus` to the JSON string array that Intel's Rego expects. - /// - /// This matches Intel's `qv_result_tcb_status_map` in qve.cpp. - fn tcb_status_to_rego_array(status: TcbStatus) -> serde_json::Value { - match status { - TcbStatus::UpToDate => json!(["UpToDate"]), - TcbStatus::SWHardeningNeeded => json!(["UpToDate", "SWHardeningNeeded"]), - TcbStatus::ConfigurationNeeded => json!(["UpToDate", "ConfigurationNeeded"]), - TcbStatus::ConfigurationAndSWHardeningNeeded => { - json!(["UpToDate", "SWHardeningNeeded", "ConfigurationNeeded"]) - } - TcbStatus::OutOfDate => json!(["OutOfDate"]), - TcbStatus::OutOfDateConfigurationNeeded => { - json!(["OutOfDate", "ConfigurationNeeded"]) - } - TcbStatus::Revoked => json!(["Revoked"]), - } - } - - /// Full collateral time window (expensive to compute, only for Rego). - pub(crate) struct CollateralTimeWindow { - pub earliest_issue_date: u64, - pub latest_issue_date: u64, - pub earliest_expiration_date: u64, - } - - /// Build common platform fields into a Rego measurement JSON map. - fn insert_platform_fields( - m: &mut serde_json::Map, - data: &SupplementalData, - tw: &CollateralTimeWindow, - ) { - // Time fields as RFC3339 strings - let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); - if !earliest_issue.is_empty() { - m.insert("earliest_issue_date".into(), json!(earliest_issue)); - } - let latest_issue = unix_to_rfc3339(tw.latest_issue_date); - if !latest_issue.is_empty() { - m.insert("latest_issue_date".into(), json!(latest_issue)); - } - let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); - if !earliest_exp.is_empty() { - m.insert("earliest_expiration_date".into(), json!(earliest_exp)); - } - let tcb_date = unix_to_rfc3339(data.platform.tcb_date_tag); - if !tcb_date.is_empty() { - m.insert("tcb_level_date_tag".into(), json!(tcb_date)); - } - - m.insert("pck_crl_num".into(), json!(data.platform.pck_crl_num)); - m.insert("root_ca_crl_num".into(), json!(data.platform.root_ca_crl_num)); - m.insert("tcb_eval_num".into(), json!(data.tcb.eval_data_number)); - m.insert("sgx_type".into(), json!(data.platform.pck.sgx_type)); - - if data.platform.pck.dynamic_platform != PckCertFlag::Undefined { - m.insert( - "is_dynamic_platform".into(), - json!(data.platform.pck.dynamic_platform == PckCertFlag::True), - ); - } - if data.platform.pck.cached_keys != PckCertFlag::Undefined { - m.insert( - "cached_keys".into(), - json!(data.platform.pck.cached_keys == PckCertFlag::True), - ); - } - if data.platform.pck.smt_enabled != PckCertFlag::Undefined { - m.insert( - "smt_enabled".into(), - json!(data.platform.pck.smt_enabled == PckCertFlag::True), - ); - } - - if let Some(ref provider_id) = data.platform.pck.platform_provider_id { - m.insert("platform_provider_id".into(), json!(provider_id)); - } - - m.insert("fmspc".into(), json!(hex::encode_upper(data.platform.pck.fmspc))); - m.insert( - "root_key_id".into(), - json!(hex::encode_upper(data.platform.root_key_id)), - ); - } - - /// Build merged Rego measurement (single-measurement path). - pub(crate) fn build_merged_measurement( - data: &SupplementalData, - tw: &CollateralTimeWindow, - ) -> serde_json::Value { - let mut m = serde_json::Map::new(); - m.insert( - "tcb_status".into(), - tcb_status_to_rego_array(data.tcb.status), - ); - insert_platform_fields(&mut m, data, tw); - if !data.tcb.advisory_ids.is_empty() { - m.insert("advisory_ids".into(), json!(data.tcb.advisory_ids)); - } - serde_json::Value::Object(m) - } - - /// Build platform TCB measurement using **unmerged** platform status. - pub(crate) fn build_platform_measurement( - data: &SupplementalData, - tw: &CollateralTimeWindow, - ) -> serde_json::Value { - let mut m = serde_json::Map::new(); - m.insert( - "tcb_status".into(), - tcb_status_to_rego_array(data.platform.tcb_level.tcb_status), - ); - insert_platform_fields(&mut m, data, tw); - if !data.platform.tcb_level.advisory_ids.is_empty() { - m.insert( - "advisory_ids".into(), - json!(data.platform.tcb_level.advisory_ids), - ); - } - serde_json::Value::Object(m) - } - - /// Build QE Identity measurement for Rego appraisal (TDX). - pub(crate) fn build_qe_measurement( - data: &SupplementalData, - tw: &CollateralTimeWindow, - ) -> serde_json::Value { - let mut m = serde_json::Map::new(); - - m.insert( - "tcb_status".into(), - tcb_status_to_rego_array(data.qe.tcb_level.tcb_status), - ); - - let qe_tcb_date = chrono::DateTime::parse_from_rfc3339(&data.qe.tcb_level.tcb_date) - .ok() - .map(|dt| dt.timestamp() as u64) - .unwrap_or(0); - let qe_date_str = unix_to_rfc3339(qe_tcb_date); - if !qe_date_str.is_empty() { - m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); - } - - let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); - if !earliest_issue.is_empty() { - m.insert("earliest_issue_date".into(), json!(earliest_issue)); - } - let latest_issue = unix_to_rfc3339(tw.latest_issue_date); - if !latest_issue.is_empty() { - m.insert("latest_issue_date".into(), json!(latest_issue)); - } - let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); - if !earliest_exp.is_empty() { - m.insert("earliest_expiration_date".into(), json!(earliest_exp)); - } - - m.insert("tcb_eval_num".into(), json!(data.qe.tcb_eval_data_number)); - m.insert( - "root_key_id".into(), - json!(hex::encode_upper(data.platform.root_key_id)), - ); - - serde_json::Value::Object(m) - } - - // ── Tenant measurement helpers ───────────────────────────────────────── - - use crate::quote::{Report, TDReport10, TDReport15}; - - /// Generate SGX enclave measurement JSON from an `EnclaveReport`. - /// - /// KSS fields are extracted from reserved areas matching Intel's `sgx_report_body_t` layout: - /// - `isv_ext_prod_id`: reserved1\[12..28\] (16B at offset 32) - /// - `config_id`: reserved3\[32..96\] (64B at offset 192) - /// - `config_svn`: reserved4\[0..2\] (u16 LE at offset 260) - /// - `isv_family_id`: reserved4\[44..60\] (16B at offset 304) - pub(crate) fn sgx_enclave_measurement(report: &EnclaveReport) -> serde_json::Value { - let mut m = serde_json::Map::new(); - - m.insert( - "sgx_miscselect".into(), - json!(hex::encode_upper(report.misc_select.to_le_bytes())), - ); - m.insert( - "sgx_attributes".into(), - json!(hex::encode_upper(report.attributes)), - ); - m.insert( - "sgx_mrenclave".into(), - json!(hex::encode_upper(report.mr_enclave)), - ); - m.insert( - "sgx_mrsigner".into(), - json!(hex::encode_upper(report.mr_signer)), - ); - m.insert("sgx_isvprodid".into(), json!(report.isv_prod_id)); - m.insert("sgx_isvsvn".into(), json!(report.isv_svn)); - m.insert( - "sgx_reportdata".into(), - json!(hex::encode_upper(report.report_data)), - ); - - // KSS fields from reserved areas (Intel sgx_report_body_t layout) - if let Some(ext_prod_id) = report.reserved1.get(12..28) { - m.insert( - "sgx_isvextprodid".into(), - json!(hex::encode_upper(ext_prod_id)), - ); - } - if let Some(config_id) = report.reserved3.get(32..96) { - m.insert( - "sgx_configid".into(), - json!(hex::encode_upper(config_id)), - ); - } - if let Some(config_svn_bytes) = report.reserved4.get(0..2).and_then(|s| <[u8; 2]>::try_from(s).ok()) { - let config_svn = u16::from_le_bytes(config_svn_bytes); - m.insert("sgx_configsvn".into(), json!(config_svn)); - } - if let Some(family_id) = report.reserved4.get(44..60) { - m.insert( - "sgx_isvfamilyid".into(), - json!(hex::encode_upper(family_id)), - ); - } - - serde_json::Value::Object(m) - } - - /// Generate TDX TD 1.0 measurement JSON from a `TDReport10`. - fn td10_measurement(report: &TDReport10) -> serde_json::Value { - let mut m = serde_json::Map::new(); - - m.insert( - "tdx_attributes".into(), - json!(hex::encode_upper(report.td_attributes)), - ); - m.insert( - "tdx_xfam".into(), - json!(hex::encode_upper(report.xfam)), - ); - m.insert( - "tdx_mrtd".into(), - json!(hex::encode_upper(report.mr_td)), - ); - m.insert( - "tdx_mrconfigid".into(), - json!(hex::encode_upper(report.mr_config_id)), - ); - m.insert( - "tdx_mrowner".into(), - json!(hex::encode_upper(report.mr_owner)), - ); - m.insert( - "tdx_mrownerconfig".into(), - json!(hex::encode_upper(report.mr_owner_config)), - ); - m.insert( - "tdx_rtmr0".into(), - json!(hex::encode_upper(report.rt_mr0)), - ); - m.insert( - "tdx_rtmr1".into(), - json!(hex::encode_upper(report.rt_mr1)), - ); - m.insert( - "tdx_rtmr2".into(), - json!(hex::encode_upper(report.rt_mr2)), - ); - m.insert( - "tdx_rtmr3".into(), - json!(hex::encode_upper(report.rt_mr3)), - ); - m.insert( - "tdx_reportdata".into(), - json!(hex::encode_upper(report.report_data)), - ); - - serde_json::Value::Object(m) - } - - /// Generate TDX TD 1.5 measurement JSON from a `TDReport15`. - fn td15_measurement(report: &TDReport15) -> serde_json::Value { - let mut m = td10_measurement(&report.base); - if let Some(obj) = m.as_object_mut() { - obj.insert( - "tdx_mrservicetd".into(), - json!(hex::encode_upper(report.mr_service_td)), - ); - } - m - } - - /// Generate tenant measurement JSON from a `Report`. - pub(crate) fn tenant_measurement(report: &Report) -> serde_json::Value { - match report { - Report::SgxEnclave(er) => sgx_enclave_measurement(er), - Report::TD10(td) => td10_measurement(td), - Report::TD15(td) => td15_measurement(td), - } - } - - /// Returns the tenant class_id for the given report type. - pub(crate) fn tenant_class_id(report: &Report) -> &'static str { - match report { - Report::SgxEnclave(_) => "bef7cb8c-31aa-42c1-854c-10db005d5c41", - Report::TD10(_) => "a1e4ee9c-a12e-48ac-bed0-e3f89297f687", - Report::TD15(_) => "45b734fc-aa4e-4c3d-ad28-e43d08880e68", - } - } - - /// Returns the platform class_id for the given report type and tee_type. - pub(crate) fn platform_class_id(report: &Report, tee_type: u32) -> &'static str { - match (report, tee_type) { - (Report::TD10(_), _) => "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f", - (Report::TD15(_), _) => "f708b97f-0fb2-4e6b-8b03-8a5bcd1221d3", - _ => "3123ec35-8d38-4ea5-87a5-d6c48b567570", // SGX - } - } - - // ── RegoPolicySet ────────────────────────────────────────────────────── - - /// A set of Rego policies for multi-measurement appraisal. - /// - /// Accepts multiple policy JSON objects (one per class_id). The Rego engine - /// matches each `qvl_result` entry to its corresponding policy by `class_id`. - /// - /// This provides full Intel QAL compatibility with separate evaluation of - /// platform TCB, QE identity, and tenant measurements. - /// - /// # Example - /// - /// ```no_run - /// use dcap_qvl::RegoPolicySet; - /// - /// let platform_policy = r#"{ - /// "environment": { "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570" }, - /// "reference": { "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 } - /// }"#; - /// let enclave_policy = r#"{ - /// "environment": { "class_id": "bef7cb8c-31aa-42c1-854c-10db005d5c41" }, - /// "reference": { "sgx_mrenclave": "ABCD..." } - /// }"#; - /// let policies = RegoPolicySet::new(&[platform_policy, enclave_policy]).unwrap(); - /// ``` - pub struct RegoPolicySet { - engine: regorus::Engine, - policies: Vec, - } - - impl RegoPolicySet { - /// Create a `RegoPolicySet` from multiple Intel JSON policy strings. - /// - /// Uses the bundled `qal_script.rego`. Each JSON must have `environment.class_id`. - pub fn new(policy_jsons: &[&str]) -> Result { - Self::with_rego(policy_jsons, include_str!("../rego/qal_script.rego")) - } - - /// Create a `RegoPolicySet` with a custom Rego script. - pub fn with_rego(policy_jsons: &[&str], rego_source: &str) -> Result { - let mut engine = regorus::Engine::new(); - engine - .add_policy("qal_script.rego".into(), rego_source.into()) - .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; - - let mut policies = Vec::new(); - for json_str in policy_jsons { - let policy: serde_json::Value = serde_json::from_str(json_str) - .map_err(|e| anyhow::anyhow!("Failed to parse policy JSON: {e}"))?; - // Validate that class_id exists - policy - .get("environment") - .and_then(|e| e.get("class_id")) - .and_then(|c| c.as_str()) - .ok_or_else(|| anyhow::anyhow!("Policy JSON missing environment.class_id"))?; - policies.push(policy); - } - - Ok(Self { engine, policies }) - } - - /// Evaluate the Rego engine with the given qvl_result entries. - pub(crate) fn eval_rego(&self, qvl_result: Vec) -> Result<()> { - let policy_refs: Vec<&serde_json::Value> = self.policies.iter().collect(); - eval_rego_engine(&self.engine, &policy_refs, qvl_result) - } - } - - /// Shared Rego evaluation logic used by both `RegoPolicy` and `RegoPolicySet`. - fn eval_rego_engine( - engine: ®orus::Engine, - policies: &[&serde_json::Value], - qvl_result: Vec, - ) -> Result<()> { - let mut engine = engine.clone(); - - let input = json!({ - "qvl_result": qvl_result, - "policies": { - "policy_array": policies, - } - }); - - let input_str = serde_json::to_string(&input) - .map_err(|e| anyhow::anyhow!("Failed to serialize Rego input: {e}"))?; - engine - .set_input_json(&input_str) - .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; - - let result = engine - .eval_rule("data.dcap.quote.appraisal.final_ret".into()) - .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; - - let result_json = result - .to_json_str() - .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; - - match result_json.trim() { - "1" => Ok(()), - "0" => { - let detail = engine - .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) - .ok() - .and_then(|v| v.to_json_str().ok()); - if let Some(detail) = detail { - bail!("Rego appraisal failed: {detail}"); - } - bail!("Rego appraisal failed (result = 0)"); - } - "-1" => bail!("No policy matched the report class_id"), - other => bail!("Unexpected Rego appraisal result: {other}"), - } - } - - /// Policy implementation that evaluates Intel's `qal_script.rego` via the - /// [regorus](https://github.com/microsoft/regorus) Rego interpreter. - /// - /// This provides bit-exact compatibility with Intel's Quote Appraisal Library (QAL). - /// Users provide a JSON policy in Intel's format (the `reference` object from a - /// Quote Appraisal Policy), and the Rego script evaluates it against the - /// [`SupplementalData`] converted to Intel's measurement JSON format. - /// - /// # Example - /// - /// ```no_run - /// use dcap_qvl::RegoPolicy; - /// - /// let policy_json = r#"{ - /// "environment": { - /// "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570", - /// "description": "Strict SGX platform TCB policy" - /// }, - /// "reference": { - /// "accepted_tcb_status": ["UpToDate"], - /// "collateral_grace_period": 0 - /// } - /// }"#; - /// let policy = RegoPolicy::new(policy_json).expect("invalid policy"); - /// ``` - pub struct RegoPolicy { - engine: regorus::Engine, - policy_json: serde_json::Value, - class_id: String, - } - - impl RegoPolicy { - /// Create a `RegoPolicy` from an Intel JSON policy string. - /// - /// Uses the bundled `qal_script.rego` (from Intel's DCAP source). - /// The JSON must contain `environment.class_id` to identify the policy type. - pub fn new(policy_json: &str) -> Result { - Self::with_rego(policy_json, include_str!("../rego/qal_script.rego")) - } - - /// Create a `RegoPolicy` with a custom Rego script. - /// - /// Use this to provide an updated or modified version of `qal_script.rego`. - pub fn with_rego(policy_json: &str, rego_source: &str) -> Result { - let mut engine = regorus::Engine::new(); - engine - .add_policy("qal_script.rego".into(), rego_source.into()) - .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; - - let policy: serde_json::Value = serde_json::from_str(policy_json) - .map_err(|e| anyhow::anyhow!("Failed to parse policy JSON: {e}"))?; - - let class_id = policy - .get("environment") - .and_then(|e| e.get("class_id")) - .and_then(|c| c.as_str()) - .ok_or_else(|| anyhow::anyhow!("Policy JSON missing environment.class_id"))? - .to_string(); - - Ok(Self { - engine, - policy_json: policy, - class_id, - }) - } - } - - impl RegoPolicy { - /// Evaluate this single-measurement Rego policy against supplemental data + time window. - pub(crate) fn eval( - &self, - data: &SupplementalData, - tw: &CollateralTimeWindow, - ) -> Result<()> { - let measurement = build_merged_measurement(data, tw); - let qvl_result = vec![json!({ - "environment": { "class_id": &self.class_id }, - "measurement": measurement, - })]; - eval_rego_engine(&self.engine, &[&self.policy_json], qvl_result) - } - } -} - -#[cfg(feature = "rego")] -pub use rego_policy::RegoPolicy; -#[cfg(feature = "rego")] -pub use rego_policy::RegoPolicySet; - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::tcb_info::TcbStatus::*; - - // ═══════════════════════════════════════════════════════════════════ - // QuotePolicy tests - // ═══════════════════════════════════════════════════════════════════ - - fn make_test_supplemental(tcb_status: TcbStatus) -> SupplementalData { - use crate::qe_identity::{QeTcb, QeTcbLevel}; - use crate::tcb_info::{Tcb, TcbComponents, TcbLevel}; - - SupplementalData { - tee_type: 0, - tcb: TcbVerdict { - status: tcb_status, - advisory_ids: vec![], - eval_data_number: 17, - earliest_expiration: 1_703_000_000, // ~2023-12-19 - }, - platform: PlatformInfo { - tcb_level: TcbLevel { - tcb: Tcb { - sgx_components: vec![TcbComponents { svn: 0 }; 16], - tdx_components: vec![], - pce_svn: 13, - }, - tcb_date: "2023-07-22T00:00:00Z".to_string(), - tcb_status, - advisory_ids: vec![], - }, - tcb_date_tag: 1_690_000_000, // ~2023-07-22 - pck: PckIdentity { - ppid: vec![0u8; 16], - cpu_svn: [0u8; 16], - pce_svn: 13, - pce_id: 0, - fmspc: [0u8; 6], - sgx_type: 0, - platform_instance_id: None, - dynamic_platform: PckCertFlag::Undefined, - cached_keys: PckCertFlag::Undefined, - smt_enabled: PckCertFlag::Undefined, - platform_provider_id: None, - }, - root_key_id: [0u8; 48], - pck_crl_num: 1, - root_ca_crl_num: 1, - }, - qe: QeInfo { - tcb_level: QeTcbLevel { - tcb: QeTcb { isvsvn: 8 }, - tcb_date: "2024-03-13T00:00:00Z".to_string(), - tcb_status: UpToDate, - advisory_ids: vec![], - }, - report: crate::quote::EnclaveReport { - cpu_svn: [0u8; 16], - misc_select: 0, - reserved1: [0u8; 28], - attributes: [0u8; 16], - mr_enclave: [0u8; 32], - reserved2: [0u8; 32], - mr_signer: [0u8; 32], - reserved3: [0u8; 96], - isv_prod_id: 1, - isv_svn: 8, - reserved4: [0u8; 60], - report_data: [0u8; 64], - }, - tcb_eval_data_number: 17, - }, - } - } - - // -- TCB status checks -- - - #[test] - fn policy_strict_accepts_up_to_date() { - let data = make_test_supplemental(UpToDate); - let policy = QuotePolicy::strict(1_702_000_000); // within collateral window - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_strict_rejects_sw_hardening() { - let data = make_test_supplemental(SWHardeningNeeded); - let policy = QuotePolicy::strict(1_702_000_000); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("TCB status"), "{err}"); - } - - #[test] - fn policy_out_of_date_with_fresh_tcb_date_accepts() { - let mut data = make_test_supplemental(OutOfDate); - data.platform.tcb_date_tag = 1_702_000_000; - let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDate); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_allow_status_builder() { - let data = make_test_supplemental(SWHardeningNeeded); - let policy = QuotePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded); - assert!(policy.validate(&data).is_ok()); - } - - // -- Advisory ID whitelist -- - - #[test] - fn policy_rejects_unknown_advisory() { - let mut data = make_test_supplemental(UpToDate); - data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - let policy = QuotePolicy::strict(1_702_000_000); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("INTEL-SA-00615"), "{err}"); - } - - #[test] - fn policy_accepts_whitelisted_advisory() { - let mut data = make_test_supplemental(UpToDate); - data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - let policy = QuotePolicy::strict(1_702_000_000).accept_advisory("INTEL-SA-00615"); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_advisory_case_insensitive() { - let mut data = make_test_supplemental(UpToDate); - data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - let policy = QuotePolicy::strict(1_702_000_000).accept_advisory("intel-sa-00615"); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_empty_advisories_passes() { - let data = make_test_supplemental(UpToDate); - assert!(data.tcb.advisory_ids.is_empty()); - let policy = QuotePolicy::strict(1_702_000_000); - // Empty advisory list in quote → nothing to check against whitelist → passes - assert!(policy.validate(&data).is_ok()); - } - - // -- Collateral grace period -- - - #[test] - fn policy_collateral_expired_no_grace_rejects() { - let data = make_test_supplemental(UpToDate); - // earliest_expiration_date = 1_703_000_000, now = 1_704_000_000 → expired - let policy = QuotePolicy::strict(1_704_000_000); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("Collateral expired"), "{err}"); - } - - #[test] - fn policy_collateral_expired_with_grace_accepts() { - let data = make_test_supplemental(UpToDate); - // earliest_expiration_date = 1_703_000_000, now = 1_704_000_000 - // grace = 2_000_000 → 1_703M + 2M = 1_705M >= 1_704M → ok - let policy = QuotePolicy::strict(1_704_000_000) - .collateral_grace_period(Duration::from_secs(2_000_000)); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_collateral_expired_grace_too_short_rejects() { - let data = make_test_supplemental(UpToDate); - // earliest_expiration_date = 1_703_000_000, now = 1_704_000_000 - // grace = 500_000 → 1_703M + 0.5M = 1_703_500_000 < 1_704M → reject - let policy = QuotePolicy::strict(1_704_000_000) - .collateral_grace_period(Duration::from_secs(500_000)); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("Collateral expired"), "{err}"); - } - - #[test] - fn policy_collateral_not_expired_zero_grace_passes() { - let data = make_test_supplemental(UpToDate); - // earliest_expiration_date = 1_703_000_000, now = 1_702_000_000 → not expired - let policy = QuotePolicy::strict(1_702_000_000); - assert!(policy.validate(&data).is_ok()); - } - - // -- Platform grace period -- - - #[test] - fn policy_platform_grace_skipped_for_up_to_date() { - let data = make_test_supplemental(UpToDate); - // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000 - // grace = 0, but check is skipped because status is UpToDate - let policy = QuotePolicy::strict(1_702_000_000); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_platform_grace_skipped_for_sw_hardening() { - let data = make_test_supplemental(SWHardeningNeeded); - let policy = QuotePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded); - // SWHardeningNeeded is a "good" status → platform grace skipped - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_platform_grace_skipped_for_config_needed() { - let data = make_test_supplemental(ConfigurationNeeded); - let policy = QuotePolicy::strict(1_702_000_000).allow_status(ConfigurationNeeded); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_platform_grace_checked_for_out_of_date_rejects() { - let data = make_test_supplemental(OutOfDate); - // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000, grace = 0 - // 1_690M + 0 < 1_702M → reject - let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDate); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("Platform TCB too old"), "{err}"); - } - - #[test] - fn policy_platform_grace_checked_for_out_of_date_accepts_with_grace() { - let data = make_test_supplemental(OutOfDate); - // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000 - // grace = 13_000_000 → 1_690M + 13M = 1_703M >= 1_702M → ok - let policy = QuotePolicy::strict(1_702_000_000) - .allow_status(OutOfDate) - .platform_grace_period(Duration::from_secs(13_000_000)); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_platform_grace_too_short_rejects() { - let data = make_test_supplemental(OutOfDate); - // tcb_level_date_tag = 1_690_000_000, now = 1_702_000_000 - // grace = 11_000_000 → 1_690M + 11M = 1_701M < 1_702M → reject - let policy = QuotePolicy::strict(1_702_000_000) - .allow_status(OutOfDate) - .platform_grace_period(Duration::from_secs(11_000_000)); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("Platform TCB too old"), "{err}"); - } - - #[test] - fn policy_platform_grace_checked_for_out_of_date_config_needed() { - let data = make_test_supplemental(OutOfDateConfigurationNeeded); - // grace = 0 → reject - let policy = QuotePolicy::strict(1_702_000_000).allow_status(OutOfDateConfigurationNeeded); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("Platform TCB too old"), "{err}"); - } - - #[test] - fn policy_grace_periods_mutually_exclusive() { - let data = make_test_supplemental(UpToDate); - let policy = QuotePolicy::strict(1_702_000_000) - .collateral_grace_period(Duration::from_secs(100)) - .platform_grace_period(Duration::from_secs(100)); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("mutually exclusive"), "{err}"); - } - - // -- min_tcb_eval_data_number -- - - #[test] - fn policy_min_eval_num_rejects_below() { - let data = make_test_supplemental(UpToDate); - assert_eq!(data.tcb.eval_data_number, 17); - let policy = QuotePolicy::strict(1_702_000_000).min_tcb_eval_data_number(20); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("below minimum"), "{err}"); - } - - #[test] - fn policy_min_eval_num_accepts_equal() { - let data = make_test_supplemental(UpToDate); - let policy = QuotePolicy::strict(1_702_000_000).min_tcb_eval_data_number(17); - assert!(policy.validate(&data).is_ok()); - } - - // -- Platform flags -- - - #[test] - fn policy_rejects_dynamic_platform_true() { - let mut data = make_test_supplemental(UpToDate); - data.platform.pck.dynamic_platform = PckCertFlag::True; - let policy = QuotePolicy::strict(1_702_000_000); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("Dynamic platform"), "{err}"); - } - - #[test] - fn policy_allows_dynamic_platform_when_configured() { - let mut data = make_test_supplemental(UpToDate); - data.platform.pck.dynamic_platform = PckCertFlag::True; - let policy = QuotePolicy::strict(1_702_000_000).allow_dynamic_platform(true); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_undefined_platform_flags_pass() { - // Processor CA certs have Undefined flags — should never be rejected - let data = make_test_supplemental(UpToDate); - assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined); - assert_eq!(data.platform.pck.cached_keys, PckCertFlag::Undefined); - assert_eq!(data.platform.pck.smt_enabled, PckCertFlag::Undefined); - let policy = QuotePolicy::strict(1_702_000_000); - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_rejects_smt_true() { - let mut data = make_test_supplemental(UpToDate); - data.platform.pck.smt_enabled = PckCertFlag::True; - let policy = QuotePolicy::strict(1_702_000_000); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("SMT"), "{err}"); - } - - #[test] - fn policy_rejects_cached_keys_true() { - let mut data = make_test_supplemental(UpToDate); - data.platform.pck.cached_keys = PckCertFlag::True; - let policy = QuotePolicy::strict(1_702_000_000); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("Cached keys"), "{err}"); - } - - // -- SGX type whitelist -- - - #[test] - fn policy_sgx_type_not_configured_passes() { - let data = make_test_supplemental(UpToDate); - let policy = QuotePolicy::strict(1_702_000_000); - // No accepted_sgx_types set → skip check - assert!(policy.validate(&data).is_ok()); - } - - #[test] - fn policy_sgx_type_whitelist_rejects() { - let mut data = make_test_supplemental(UpToDate); - data.platform.pck.sgx_type = 1; // Scalable - let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0]); // Only Standard - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("SGX type"), "{err}"); - } - - #[test] - fn policy_sgx_type_whitelist_accepts() { - let mut data = make_test_supplemental(UpToDate); - data.platform.pck.sgx_type = 1; // Scalable - let policy = QuotePolicy::strict(1_702_000_000).accepted_sgx_types(&[0, 1, 2]); - assert!(policy.validate(&data).is_ok()); - } - - // -- RegoPolicy tests (require "rego" feature) -- - - #[cfg(feature = "rego")] - mod rego_tests { - use super::*; - use crate::policy::rego_policy::{ - self, build_merged_measurement, build_platform_measurement, build_qe_measurement, - CollateralTimeWindow, - }; - - const SGX_PLATFORM_CLASS_ID: &str = "3123ec35-8d38-4ea5-87a5-d6c48b567570"; - - /// Create a test time window with future dates (Rego uses real wall clock). - fn make_test_time_window() -> CollateralTimeWindow { - CollateralTimeWindow { - earliest_issue_date: 1_900_000_000, // 2030-03-17 - latest_issue_date: 1_900_100_000, - earliest_expiration_date: 2_000_000_000, // 2033-05-18 - } - } - - /// Create test supplemental data with future expiration for Rego. - fn make_rego_supplemental(status: TcbStatus) -> SupplementalData { - let mut data = make_test_supplemental(status); - data.tcb.earliest_expiration = 2_000_000_000; // 2033-05-18 - data - } - - fn policy_json(reference: &str) -> String { - format!( - r#"{{ - "environment": {{ - "class_id": "{SGX_PLATFORM_CLASS_ID}", - "description": "Test policy" - }}, - "reference": {reference} - }}"# - ) - } - - #[test] - fn rego_strict_accepts_up_to_date() { - let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); - let json = policy_json( - r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, - ); - let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); - assert!( - result.is_ok(), - "expected Ok, got: {:?}", - result.unwrap_err() - ); - } - - #[test] - fn rego_strict_rejects_out_of_date() { - let data = make_rego_supplemental(OutOfDate); - let tw = make_test_time_window(); - let json = policy_json( - r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, - ); - let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.eval(&data, &tw).unwrap_err().to_string(); - assert!( - err.contains("appraisal failed"), - "expected appraisal failure, got: {err}" - ); - } - - #[test] - fn rego_permissive_accepts_out_of_date() { - let data = make_rego_supplemental(OutOfDate); - let tw = make_test_time_window(); - let json = policy_json( - r#"{"accepted_tcb_status": ["UpToDate", "OutOfDate"], "collateral_grace_period": 0}"#, - ); - let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); - assert!( - result.is_ok(), - "expected Ok, got: {:?}", - result.unwrap_err() - ); - } - - #[test] - fn rego_rejects_advisory() { - let mut data = make_rego_supplemental(UpToDate); - data.tcb.advisory_ids = vec!["INTEL-SA-00334".into()]; - let tw = make_test_time_window(); - let json = policy_json( - r#"{ - "accepted_tcb_status": ["UpToDate"], - "collateral_grace_period": 0, - "rejected_advisory_ids": ["INTEL-SA-00334"] - }"#, - ); - let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.eval(&data, &tw).unwrap_err().to_string(); - assert!( - err.contains("appraisal failed"), - "expected advisory rejection, got: {err}" - ); - } - - #[test] - fn rego_platform_grace_period_accepts() { - let mut data = make_rego_supplemental(OutOfDate); - // tcb_date_tag in the past, but huge grace period covers it - data.platform.tcb_date_tag = 1_690_000_000; // 2023-07-22 - let tw = make_test_time_window(); - let json = policy_json( - r#"{ - "accepted_tcb_status": ["UpToDate", "OutOfDate"], - "collateral_grace_period": 0, - "platform_grace_period": 999999999 - }"#, - ); - let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); - assert!( - result.is_ok(), - "expected Ok, got: {:?}", - result.unwrap_err() - ); - } - - #[test] - fn rego_expiration_check_rejects_expired_collateral() { - let data = make_rego_supplemental(UpToDate); - // Set expiration in the past via time window - let tw = CollateralTimeWindow { - earliest_issue_date: 1_700_000_000, - latest_issue_date: 1_700_100_000, - earliest_expiration_date: 1_703_000_000, // 2023-12-19 (past) - }; - let json = policy_json( - r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, - ); - let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.eval(&data, &tw).unwrap_err().to_string(); - assert!( - err.contains("appraisal failed"), - "expected expiration failure, got: {err}" - ); - } - - #[test] - fn rego_no_collateral_grace_skips_expiration_check() { - let data = make_rego_supplemental(UpToDate); - // Expired collateral, but no collateral_grace_period in policy → skip check - let tw = CollateralTimeWindow { - earliest_issue_date: 1_700_000_000, - latest_issue_date: 1_700_100_000, - earliest_expiration_date: 1_703_000_000, // 2023-12-19 (past) - }; - let json = policy_json(r#"{"accepted_tcb_status": ["UpToDate"]}"#); - let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); - assert!( - result.is_ok(), - "expected Ok (no expiration check), got: {:?}", - result.unwrap_err() - ); - } - - #[test] - fn rego_missing_class_id_errors() { - let json = r#"{"reference": {"accepted_tcb_status": ["UpToDate"]}}"#; - assert!(RegoPolicy::new(json).is_err()); - } - - #[test] - fn rego_to_measurement_tcb_status_mapping() { - let data = make_test_supplemental(ConfigurationAndSWHardeningNeeded); - let tw = make_test_time_window(); - let m = build_merged_measurement(&data, &tw); - let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); - assert_eq!(statuses.len(), 3); - assert_eq!(statuses[0], "UpToDate"); - assert_eq!(statuses[1], "SWHardeningNeeded"); - assert_eq!(statuses[2], "ConfigurationNeeded"); - } - - #[test] - fn rego_to_measurement_omits_undefined_flags() { - let data = make_test_supplemental(UpToDate); - assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined); - let tw = make_test_time_window(); - let m = build_merged_measurement(&data, &tw); - assert!(m.get("is_dynamic_platform").is_none()); - assert!(m.get("cached_keys").is_none()); - assert!(m.get("smt_enabled").is_none()); - } - - #[test] - fn rego_to_measurement_includes_true_flags() { - let mut data = make_test_supplemental(UpToDate); - data.platform.pck.dynamic_platform = PckCertFlag::True; - data.platform.pck.cached_keys = PckCertFlag::False; - data.platform.pck.smt_enabled = PckCertFlag::True; - let tw = make_test_time_window(); - let m = build_merged_measurement(&data, &tw); - assert_eq!(m.get("is_dynamic_platform").unwrap(), true); - assert_eq!(m.get("cached_keys").unwrap(), false); - assert_eq!(m.get("smt_enabled").unwrap(), true); - } - - // ═══════════════════════════════════════════════════════════════════ - // Multi-measurement tests - // ═══════════════════════════════════════════════════════════════════ - - #[test] - fn rego_platform_measurement_uses_unmerged_status() { - let mut data = make_test_supplemental(UpToDate); - // Merged status is UpToDate, but platform-specific is OutOfDate - data.platform.tcb_level.tcb_status = OutOfDate; - data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00001".into()]; - let tw = make_test_time_window(); - let m = build_platform_measurement(&data, &tw); - let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); - // OutOfDate maps to ["UpToDate", "OutOfDate"] - assert!(statuses.contains(&serde_json::json!("OutOfDate"))); - // Advisory from platform, not merged - let advisories = m.get("advisory_ids").unwrap().as_array().unwrap(); - assert_eq!(advisories, &[serde_json::json!("INTEL-SA-00001")]); - } - - #[test] - fn rego_qe_measurement_fields() { - let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); - let m = build_qe_measurement(&data, &tw); - // QE measurement should have tcb_status from qe.tcb_level - assert!(m.get("tcb_status").is_some()); - // Should have tcb_eval_num from qe.tcb_eval_data_number - assert_eq!(m.get("tcb_eval_num").unwrap(), 17); - // Should have root_key_id - assert!(m.get("root_key_id").is_some()); - // Should have time fields - assert!(m.get("earliest_issue_date").is_some()); - assert!(m.get("latest_issue_date").is_some()); - assert!(m.get("earliest_expiration_date").is_some()); - assert!(m.get("tcb_level_date_tag").is_some()); - } - - #[test] - fn rego_sgx_enclave_measurement_fields() { - use crate::quote::EnclaveReport; - - let mut report = EnclaveReport { - cpu_svn: [0u8; 16], - misc_select: 0x12345678, - reserved1: [0u8; 28], - attributes: [0xAA; 16], - mr_enclave: [0xBB; 32], - reserved2: [0u8; 32], - mr_signer: [0xCC; 32], - reserved3: [0u8; 96], - isv_prod_id: 42, - isv_svn: 7, - reserved4: [0u8; 60], - report_data: [0xDD; 64], - }; - // Set KSS fields in reserved areas - // isv_ext_prod_id at reserved1[12..28] - report.reserved1[12..28].copy_from_slice(&[0x11; 16]); - // config_id at reserved3[32..96] - report.reserved3[32..96].copy_from_slice(&[0x22; 64]); - // config_svn at reserved4[0..2] - report.reserved4[0..2].copy_from_slice(&42u16.to_le_bytes()); - // isv_family_id at reserved4[44..60] - report.reserved4[44..60].copy_from_slice(&[0x33; 16]); - - let m = rego_policy::sgx_enclave_measurement(&report); - assert!(m.get("sgx_mrenclave").is_some()); - assert!(m.get("sgx_mrsigner").is_some()); - assert_eq!(m.get("sgx_isvprodid").unwrap(), 42); - assert_eq!(m.get("sgx_isvsvn").unwrap(), 7); - assert!(m.get("sgx_reportdata").is_some()); - assert!(m.get("sgx_configid").is_some()); - assert_eq!(m.get("sgx_configsvn").unwrap(), 42); - assert!(m.get("sgx_isvextprodid").is_some()); - assert!(m.get("sgx_isvfamilyid").is_some()); - } - - #[test] - fn rego_policy_set_sgx_platform_accepts() { - let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); - let platform_json = format!( - r#"{{ - "environment": {{ "class_id": "{SGX_PLATFORM_CLASS_ID}" }}, - "reference": {{ "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 }} - }}"# - ); - let policies = RegoPolicySet::new(&[&platform_json]).unwrap(); - let qvl_result = vec![serde_json::json!({ - "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": build_platform_measurement(&data, &tw), - })]; - assert!( - policies.eval_rego(qvl_result).is_ok(), - "expected Ok, got: {:?}", - { - let qvl_result2 = vec![serde_json::json!({ - "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": build_platform_measurement(&data, &tw), - })]; - policies.eval_rego(qvl_result2).unwrap_err() - } - ); - } - - #[test] - fn rego_policy_set_class_id_mismatch_fails() { - let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); - // Policy expects TDX platform class_id, but measurement is SGX - let tdx_class_id = "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f"; - let policy_json = format!( - r#"{{ - "environment": {{ "class_id": "{tdx_class_id}" }}, - "reference": {{ "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 }} - }}"# - ); - let policies = RegoPolicySet::new(&[&policy_json]).unwrap(); - let qvl_result = vec![serde_json::json!({ - "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": build_platform_measurement(&data, &tw), - })]; - // Mismatched class_ids → no bundle matched → empty appraisal → fail - let err = policies.eval_rego(qvl_result).unwrap_err().to_string(); - assert!( - err.contains("appraisal failed"), - "expected appraisal failure on class_id mismatch, got: {err}" - ); - } - } -} diff --git a/src/policy/mod.rs b/src/policy/mod.rs new file mode 100644 index 0000000..e385f36 --- /dev/null +++ b/src/policy/mod.rs @@ -0,0 +1,157 @@ +use anyhow::Result; + +use { + crate::constants::*, + crate::qe_identity::QeTcbLevel, + crate::quote::EnclaveReport, + crate::tcb_info::{TcbLevel, TcbStatus}, + alloc::string::String, + alloc::vec::Vec, +}; + +mod simple; +pub use simple::SimplePolicy; + +#[cfg(feature = "rego")] +pub(crate) mod rego; +#[cfg(feature = "rego")] +pub use rego::RegoPolicy; +#[cfg(feature = "rego")] +pub use rego::RegoPolicySet; + +/// Policy trait for customizing quote verification behavior. +/// +/// Implement this trait to define custom validation logic for [`SupplementalData`]. +/// The library provides [`SimplePolicy`] as a comprehensive built-in implementation +/// that covers all common checks from Intel's Appraisal framework. +/// +/// For most use cases, [`SimplePolicy`] with its builder methods is sufficient: +/// ```ignore +/// use dcap_qvl::SimplePolicy; +/// use dcap_qvl::TcbStatus; +/// +/// use core::time::Duration; +/// +/// let policy = SimplePolicy::strict(now_unix_secs) +/// .allow_status(TcbStatus::SWHardeningNeeded) +/// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) +/// .accept_advisory("INTEL-SA-00334"); +/// ``` +/// +/// Implement this trait directly only for logic that [`SimplePolicy`] cannot express. +pub trait Policy { + /// Validate supplemental data against this policy. + /// + /// Return `Ok(())` to accept, or `Err(...)` to reject. + fn validate(&self, data: &SupplementalData) -> Result<()>; +} + +/// PCK certificate flag, matching Intel's `pck_cert_flag_enum_t`. +/// +/// These flags are only present in PCK certificates issued by the **Platform CA**. +/// For Processor CA certificates, the value is [`Undefined`](PckCertFlag::Undefined). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PckCertFlag { + /// The flag is explicitly false (ASN.1 BOOLEAN FALSE). + False = 0, + /// The flag is explicitly true (ASN.1 BOOLEAN TRUE). + True = 1, + /// The flag is not present in the certificate (Processor CA certs). + Undefined = 2, +} + +impl From> for PckCertFlag { + fn from(v: Option) -> Self { + match v { + Some(true) => PckCertFlag::True, + Some(false) => PckCertFlag::False, + None => PckCertFlag::Undefined, + } + } +} + +/// Supplemental data from quote verification. +/// +/// Organized into structured sub-groups: +/// - [`tcb`](Self::tcb): Merged TCB verdict +/// - [`platform`](Self::platform): Platform-level details from PCK certificate and TCB matching +/// - [`qe`](Self::qe): QE (Quoting Enclave) verification results +pub struct SupplementalData { + /// TEE type: `0x00000000` for SGX, `0x00000081` for TDX. + pub tee_type: u32, + /// Merged TCB verdict (worst of platform + QE). + pub tcb: TcbVerdict, + /// Platform verification details. + pub platform: PlatformInfo, + /// QE verification details. + pub qe: QeInfo, +} + +/// Merged TCB verdict from platform and QE status convergence. +/// +/// Uses Intel's `convergeTcbStatusWithQeTcbStatus` logic to produce the +/// worst-case status and union of advisory IDs. +pub struct TcbVerdict { + /// Merged TCB status (worst of platform TCB + QE TCB). + pub status: TcbStatus, + /// Merged advisory IDs (union of platform + QE advisories). + pub advisory_ids: Vec, + /// Lower of TCBInfo and QEIdentity `tcbEvaluationDataNumber` values. + pub eval_data_number: u32, + /// Earliest expiration from TCBInfo nextUpdate, QEIdentity nextUpdate, + /// and CRL nextUpdate (4 sources). Does **not** include certificate chain + /// notAfter — use full time window via Rego for that. + pub earliest_expiration: u64, +} + +/// Platform-level verification results. +pub struct PlatformInfo { + /// The matched platform TCB level (unmerged). + pub tcb_level: TcbLevel, + /// Platform TCB level date as unix timestamp (precomputed from `tcb_level.tcb_date`). + pub tcb_date_tag: u64, + /// PCK certificate identity fields. + pub pck: PckIdentity, + /// SHA-384 of root CA's raw public key bytes, matching Intel's `root_key_id`. + pub root_key_id: [u8; 48], + /// CRL number from PCK Certificate Revocation List. + pub pck_crl_num: u32, + /// CRL number from Root CA Certificate Revocation List. + pub root_ca_crl_num: u32, +} + +/// QE (Quoting Enclave) verification results. +pub struct QeInfo { + /// The matched QE TCB level (unmerged). + pub tcb_level: QeTcbLevel, + /// The QE's enclave report. + pub report: EnclaveReport, + /// TCB evaluation data number from QE Identity (unmerged). + pub tcb_eval_data_number: u32, +} + +/// PCK certificate identity fields. +pub struct PckIdentity { + /// Platform Provisioning ID (PPID). + pub ppid: Vec, + /// CPU Security Version Number (16 bytes). + pub cpu_svn: CpuSvn, + /// PCE ISV Security Version Number. + pub pce_svn: Svn, + /// PCE ID. + pub pce_id: u16, + /// FMSPC (6 bytes). + pub fmspc: Fmspc, + /// SGX type: 0=Standard, 1=Scalable, 2=ScalableWithIntegrity. + pub sgx_type: u8, + /// Platform Instance ID (16 bytes, Platform CA only). + pub platform_instance_id: Option<[u8; 16]>, + /// Dynamic platform flag. + pub dynamic_platform: PckCertFlag, + /// Cached keys flag. + pub cached_keys: PckCertFlag, + /// SMT (hyperthreading) flag. + pub smt_enabled: PckCertFlag, + /// Platform Provider ID (Platform CA only, for Rego). + pub platform_provider_id: Option, +} diff --git a/src/policy/rego.rs b/src/policy/rego.rs new file mode 100644 index 0000000..bd457e6 --- /dev/null +++ b/src/policy/rego.rs @@ -0,0 +1,914 @@ +use super::*; +use serde_json::json; + +use anyhow::{bail, Result}; + +/// Convert a unix timestamp (seconds) to an RFC3339 string. +/// Returns an empty string for timestamp 0 (matching Intel's behavior of omitting the field). +fn unix_to_rfc3339(secs: u64) -> String { + chrono::DateTime::from_timestamp(secs as i64, 0) + .map(|dt| dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) + .unwrap_or_default() +} + +/// Convert `TcbStatus` to the JSON string array that Intel's Rego expects. +/// +/// This matches Intel's `qv_result_tcb_status_map` in qve.cpp. +fn tcb_status_to_rego_array(status: TcbStatus) -> serde_json::Value { + match status { + TcbStatus::UpToDate => json!(["UpToDate"]), + TcbStatus::SWHardeningNeeded => json!(["UpToDate", "SWHardeningNeeded"]), + TcbStatus::ConfigurationNeeded => json!(["UpToDate", "ConfigurationNeeded"]), + TcbStatus::ConfigurationAndSWHardeningNeeded => { + json!(["UpToDate", "SWHardeningNeeded", "ConfigurationNeeded"]) + } + TcbStatus::OutOfDate => json!(["OutOfDate"]), + TcbStatus::OutOfDateConfigurationNeeded => { + json!(["OutOfDate", "ConfigurationNeeded"]) + } + TcbStatus::Revoked => json!(["Revoked"]), + } +} + +/// Full collateral time window (expensive to compute, only for Rego). +pub(crate) struct CollateralTimeWindow { + pub earliest_issue_date: u64, + pub latest_issue_date: u64, + pub earliest_expiration_date: u64, +} + +/// Build common platform fields into a Rego measurement JSON map. +fn insert_platform_fields( + m: &mut serde_json::Map, + data: &SupplementalData, + tw: &CollateralTimeWindow, +) { + // Time fields as RFC3339 strings + let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); + if !earliest_issue.is_empty() { + m.insert("earliest_issue_date".into(), json!(earliest_issue)); + } + let latest_issue = unix_to_rfc3339(tw.latest_issue_date); + if !latest_issue.is_empty() { + m.insert("latest_issue_date".into(), json!(latest_issue)); + } + let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); + if !earliest_exp.is_empty() { + m.insert("earliest_expiration_date".into(), json!(earliest_exp)); + } + let tcb_date = unix_to_rfc3339(data.platform.tcb_date_tag); + if !tcb_date.is_empty() { + m.insert("tcb_level_date_tag".into(), json!(tcb_date)); + } + + m.insert("pck_crl_num".into(), json!(data.platform.pck_crl_num)); + m.insert("root_ca_crl_num".into(), json!(data.platform.root_ca_crl_num)); + m.insert("tcb_eval_num".into(), json!(data.tcb.eval_data_number)); + m.insert("sgx_type".into(), json!(data.platform.pck.sgx_type)); + + if data.platform.pck.dynamic_platform != PckCertFlag::Undefined { + m.insert( + "is_dynamic_platform".into(), + json!(data.platform.pck.dynamic_platform == PckCertFlag::True), + ); + } + if data.platform.pck.cached_keys != PckCertFlag::Undefined { + m.insert( + "cached_keys".into(), + json!(data.platform.pck.cached_keys == PckCertFlag::True), + ); + } + if data.platform.pck.smt_enabled != PckCertFlag::Undefined { + m.insert( + "smt_enabled".into(), + json!(data.platform.pck.smt_enabled == PckCertFlag::True), + ); + } + + if let Some(ref provider_id) = data.platform.pck.platform_provider_id { + m.insert("platform_provider_id".into(), json!(provider_id)); + } + + m.insert("fmspc".into(), json!(hex::encode_upper(data.platform.pck.fmspc))); + m.insert( + "root_key_id".into(), + json!(hex::encode_upper(data.platform.root_key_id)), + ); +} + +/// Build merged Rego measurement (single-measurement path). +pub(crate) fn build_merged_measurement( + data: &SupplementalData, + tw: &CollateralTimeWindow, +) -> serde_json::Value { + let mut m = serde_json::Map::new(); + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(data.tcb.status), + ); + insert_platform_fields(&mut m, data, tw); + if !data.tcb.advisory_ids.is_empty() { + m.insert("advisory_ids".into(), json!(data.tcb.advisory_ids)); + } + serde_json::Value::Object(m) +} + +/// Build platform TCB measurement using **unmerged** platform status. +pub(crate) fn build_platform_measurement( + data: &SupplementalData, + tw: &CollateralTimeWindow, +) -> serde_json::Value { + let mut m = serde_json::Map::new(); + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(data.platform.tcb_level.tcb_status), + ); + insert_platform_fields(&mut m, data, tw); + if !data.platform.tcb_level.advisory_ids.is_empty() { + m.insert( + "advisory_ids".into(), + json!(data.platform.tcb_level.advisory_ids), + ); + } + serde_json::Value::Object(m) +} + +/// Build QE Identity measurement for Rego appraisal (TDX). +pub(crate) fn build_qe_measurement( + data: &SupplementalData, + tw: &CollateralTimeWindow, +) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + m.insert( + "tcb_status".into(), + tcb_status_to_rego_array(data.qe.tcb_level.tcb_status), + ); + + let qe_tcb_date = chrono::DateTime::parse_from_rfc3339(&data.qe.tcb_level.tcb_date) + .ok() + .map(|dt| dt.timestamp() as u64) + .unwrap_or(0); + let qe_date_str = unix_to_rfc3339(qe_tcb_date); + if !qe_date_str.is_empty() { + m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); + } + + let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); + if !earliest_issue.is_empty() { + m.insert("earliest_issue_date".into(), json!(earliest_issue)); + } + let latest_issue = unix_to_rfc3339(tw.latest_issue_date); + if !latest_issue.is_empty() { + m.insert("latest_issue_date".into(), json!(latest_issue)); + } + let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); + if !earliest_exp.is_empty() { + m.insert("earliest_expiration_date".into(), json!(earliest_exp)); + } + + m.insert("tcb_eval_num".into(), json!(data.qe.tcb_eval_data_number)); + m.insert( + "root_key_id".into(), + json!(hex::encode_upper(data.platform.root_key_id)), + ); + + serde_json::Value::Object(m) +} + +// ── Tenant measurement helpers ───────────────────────────────────────── + +use crate::quote::{Report, TDReport10, TDReport15}; + +/// Generate SGX enclave measurement JSON from an `EnclaveReport`. +/// +/// KSS fields are extracted from reserved areas matching Intel's `sgx_report_body_t` layout: +/// - `isv_ext_prod_id`: reserved1\[12..28\] (16B at offset 32) +/// - `config_id`: reserved3\[32..96\] (64B at offset 192) +/// - `config_svn`: reserved4\[0..2\] (u16 LE at offset 260) +/// - `isv_family_id`: reserved4\[44..60\] (16B at offset 304) +pub(crate) fn sgx_enclave_measurement(report: &EnclaveReport) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + m.insert( + "sgx_miscselect".into(), + json!(hex::encode_upper(report.misc_select.to_le_bytes())), + ); + m.insert( + "sgx_attributes".into(), + json!(hex::encode_upper(report.attributes)), + ); + m.insert( + "sgx_mrenclave".into(), + json!(hex::encode_upper(report.mr_enclave)), + ); + m.insert( + "sgx_mrsigner".into(), + json!(hex::encode_upper(report.mr_signer)), + ); + m.insert("sgx_isvprodid".into(), json!(report.isv_prod_id)); + m.insert("sgx_isvsvn".into(), json!(report.isv_svn)); + m.insert( + "sgx_reportdata".into(), + json!(hex::encode_upper(report.report_data)), + ); + + // KSS fields from reserved areas (Intel sgx_report_body_t layout) + if let Some(ext_prod_id) = report.reserved1.get(12..28) { + m.insert( + "sgx_isvextprodid".into(), + json!(hex::encode_upper(ext_prod_id)), + ); + } + if let Some(config_id) = report.reserved3.get(32..96) { + m.insert( + "sgx_configid".into(), + json!(hex::encode_upper(config_id)), + ); + } + if let Some(config_svn_bytes) = report.reserved4.get(0..2).and_then(|s| <[u8; 2]>::try_from(s).ok()) { + let config_svn = u16::from_le_bytes(config_svn_bytes); + m.insert("sgx_configsvn".into(), json!(config_svn)); + } + if let Some(family_id) = report.reserved4.get(44..60) { + m.insert( + "sgx_isvfamilyid".into(), + json!(hex::encode_upper(family_id)), + ); + } + + serde_json::Value::Object(m) +} + +/// Generate TDX TD 1.0 measurement JSON from a `TDReport10`. +fn td10_measurement(report: &TDReport10) -> serde_json::Value { + let mut m = serde_json::Map::new(); + + m.insert( + "tdx_attributes".into(), + json!(hex::encode_upper(report.td_attributes)), + ); + m.insert( + "tdx_xfam".into(), + json!(hex::encode_upper(report.xfam)), + ); + m.insert( + "tdx_mrtd".into(), + json!(hex::encode_upper(report.mr_td)), + ); + m.insert( + "tdx_mrconfigid".into(), + json!(hex::encode_upper(report.mr_config_id)), + ); + m.insert( + "tdx_mrowner".into(), + json!(hex::encode_upper(report.mr_owner)), + ); + m.insert( + "tdx_mrownerconfig".into(), + json!(hex::encode_upper(report.mr_owner_config)), + ); + m.insert( + "tdx_rtmr0".into(), + json!(hex::encode_upper(report.rt_mr0)), + ); + m.insert( + "tdx_rtmr1".into(), + json!(hex::encode_upper(report.rt_mr1)), + ); + m.insert( + "tdx_rtmr2".into(), + json!(hex::encode_upper(report.rt_mr2)), + ); + m.insert( + "tdx_rtmr3".into(), + json!(hex::encode_upper(report.rt_mr3)), + ); + m.insert( + "tdx_reportdata".into(), + json!(hex::encode_upper(report.report_data)), + ); + + serde_json::Value::Object(m) +} + +/// Generate TDX TD 1.5 measurement JSON from a `TDReport15`. +fn td15_measurement(report: &TDReport15) -> serde_json::Value { + let mut m = td10_measurement(&report.base); + if let Some(obj) = m.as_object_mut() { + obj.insert( + "tdx_mrservicetd".into(), + json!(hex::encode_upper(report.mr_service_td)), + ); + } + m +} + +/// Generate tenant measurement JSON from a `Report`. +pub(crate) fn tenant_measurement(report: &Report) -> serde_json::Value { + match report { + Report::SgxEnclave(er) => sgx_enclave_measurement(er), + Report::TD10(td) => td10_measurement(td), + Report::TD15(td) => td15_measurement(td), + } +} + +/// Returns the tenant class_id for the given report type. +pub(crate) fn tenant_class_id(report: &Report) -> &'static str { + match report { + Report::SgxEnclave(_) => "bef7cb8c-31aa-42c1-854c-10db005d5c41", + Report::TD10(_) => "a1e4ee9c-a12e-48ac-bed0-e3f89297f687", + Report::TD15(_) => "45b734fc-aa4e-4c3d-ad28-e43d08880e68", + } +} + +/// Returns the platform class_id for the given report type and tee_type. +pub(crate) fn platform_class_id(report: &Report, tee_type: u32) -> &'static str { + match (report, tee_type) { + (Report::TD10(_), _) => "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f", + (Report::TD15(_), _) => "f708b97f-0fb2-4e6b-8b03-8a5bcd1221d3", + _ => "3123ec35-8d38-4ea5-87a5-d6c48b567570", // SGX + } +} + +// ── RegoPolicySet ────────────────────────────────────────────────────── + +/// A set of Rego policies for multi-measurement appraisal. +/// +/// Accepts multiple policy JSON objects (one per class_id). The Rego engine +/// matches each `qvl_result` entry to its corresponding policy by `class_id`. +/// +/// This provides full Intel QAL compatibility with separate evaluation of +/// platform TCB, QE identity, and tenant measurements. +/// +/// # Example +/// +/// ```no_run +/// use dcap_qvl::RegoPolicySet; +/// +/// let platform_policy = r#"{ +/// "environment": { "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570" }, +/// "reference": { "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 } +/// }"#; +/// let enclave_policy = r#"{ +/// "environment": { "class_id": "bef7cb8c-31aa-42c1-854c-10db005d5c41" }, +/// "reference": { "sgx_mrenclave": "ABCD..." } +/// }"#; +/// let policies = RegoPolicySet::new(&[platform_policy, enclave_policy]).unwrap(); +/// ``` +pub struct RegoPolicySet { + engine: regorus::Engine, + policies: Vec, +} + +impl RegoPolicySet { + /// Create a `RegoPolicySet` from multiple Intel JSON policy strings. + /// + /// Uses the bundled `qal_script.rego`. Each JSON must have `environment.class_id`. + pub fn new(policy_jsons: &[&str]) -> Result { + Self::with_rego(policy_jsons, include_str!("../../rego/qal_script.rego")) + } + + /// Create a `RegoPolicySet` with a custom Rego script. + pub fn with_rego(policy_jsons: &[&str], rego_source: &str) -> Result { + let mut engine = regorus::Engine::new(); + engine + .add_policy("qal_script.rego".into(), rego_source.into()) + .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; + + let mut policies = Vec::new(); + for json_str in policy_jsons { + let policy: serde_json::Value = serde_json::from_str(json_str) + .map_err(|e| anyhow::anyhow!("Failed to parse policy JSON: {e}"))?; + // Validate that class_id exists + policy + .get("environment") + .and_then(|e| e.get("class_id")) + .and_then(|c| c.as_str()) + .ok_or_else(|| anyhow::anyhow!("Policy JSON missing environment.class_id"))?; + policies.push(policy); + } + + Ok(Self { engine, policies }) + } + + /// Evaluate the Rego engine with the given qvl_result entries. + pub(crate) fn eval_rego(&self, qvl_result: Vec) -> Result<()> { + let policy_refs: Vec<&serde_json::Value> = self.policies.iter().collect(); + eval_rego_engine(&self.engine, &policy_refs, qvl_result) + } +} + +/// Shared Rego evaluation logic used by both `RegoPolicy` and `RegoPolicySet`. +fn eval_rego_engine( + engine: ®orus::Engine, + policies: &[&serde_json::Value], + qvl_result: Vec, +) -> Result<()> { + let mut engine = engine.clone(); + + let input = json!({ + "qvl_result": qvl_result, + "policies": { + "policy_array": policies, + } + }); + + let input_str = serde_json::to_string(&input) + .map_err(|e| anyhow::anyhow!("Failed to serialize Rego input: {e}"))?; + engine + .set_input_json(&input_str) + .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; + + let result = engine + .eval_rule("data.dcap.quote.appraisal.final_ret".into()) + .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; + + let result_json = result + .to_json_str() + .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; + + match result_json.trim() { + "1" => Ok(()), + "0" => { + let detail = engine + .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) + .ok() + .and_then(|v| v.to_json_str().ok()); + if let Some(detail) = detail { + bail!("Rego appraisal failed: {detail}"); + } + bail!("Rego appraisal failed (result = 0)"); + } + "-1" => bail!("No policy matched the report class_id"), + other => bail!("Unexpected Rego appraisal result: {other}"), + } +} + +/// Policy implementation that evaluates Intel's `qal_script.rego` via the +/// [regorus](https://github.com/microsoft/regorus) Rego interpreter. +/// +/// This provides bit-exact compatibility with Intel's Quote Appraisal Library (QAL). +/// Users provide a JSON policy in Intel's format (the `reference` object from a +/// Quote Appraisal Policy), and the Rego script evaluates it against the +/// [`SupplementalData`] converted to Intel's measurement JSON format. +/// +/// # Example +/// +/// ```no_run +/// use dcap_qvl::RegoPolicy; +/// +/// let policy_json = r#"{ +/// "environment": { +/// "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570", +/// "description": "Strict SGX platform TCB policy" +/// }, +/// "reference": { +/// "accepted_tcb_status": ["UpToDate"], +/// "collateral_grace_period": 0 +/// } +/// }"#; +/// let policy = RegoPolicy::new(policy_json).expect("invalid policy"); +/// ``` +pub struct RegoPolicy { + engine: regorus::Engine, + policy_json: serde_json::Value, + class_id: String, +} + +impl RegoPolicy { + /// Create a `RegoPolicy` from an Intel JSON policy string. + /// + /// Uses the bundled `qal_script.rego` (from Intel's DCAP source). + /// The JSON must contain `environment.class_id` to identify the policy type. + pub fn new(policy_json: &str) -> Result { + Self::with_rego(policy_json, include_str!("../../rego/qal_script.rego")) + } + + /// Create a `RegoPolicy` with a custom Rego script. + /// + /// Use this to provide an updated or modified version of `qal_script.rego`. + pub fn with_rego(policy_json: &str, rego_source: &str) -> Result { + let mut engine = regorus::Engine::new(); + engine + .add_policy("qal_script.rego".into(), rego_source.into()) + .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; + + let policy: serde_json::Value = serde_json::from_str(policy_json) + .map_err(|e| anyhow::anyhow!("Failed to parse policy JSON: {e}"))?; + + let class_id = policy + .get("environment") + .and_then(|e| e.get("class_id")) + .and_then(|c| c.as_str()) + .ok_or_else(|| anyhow::anyhow!("Policy JSON missing environment.class_id"))? + .to_string(); + + Ok(Self { + engine, + policy_json: policy, + class_id, + }) + } +} + +impl RegoPolicy { + /// Evaluate this single-measurement Rego policy against supplemental data + time window. + pub(crate) fn eval( + &self, + data: &SupplementalData, + tw: &CollateralTimeWindow, + ) -> Result<()> { + let measurement = build_merged_measurement(data, tw); + let qvl_result = vec![json!({ + "environment": { "class_id": &self.class_id }, + "measurement": measurement, + })]; + eval_rego_engine(&self.engine, &[&self.policy_json], qvl_result) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use crate::policy::{PckCertFlag, PckIdentity, PlatformInfo, QeInfo, TcbVerdict}; + use crate::tcb_info::TcbStatus::*; + + const SGX_PLATFORM_CLASS_ID: &str = "3123ec35-8d38-4ea5-87a5-d6c48b567570"; + + fn make_test_supplemental(tcb_status: TcbStatus) -> SupplementalData { + use crate::qe_identity::{QeTcb, QeTcbLevel}; + use crate::tcb_info::{Tcb, TcbComponents, TcbLevel}; + + SupplementalData { + tee_type: 0, + tcb: TcbVerdict { + status: tcb_status, + advisory_ids: vec![], + eval_data_number: 17, + earliest_expiration: 1_703_000_000, + }, + platform: PlatformInfo { + tcb_level: TcbLevel { + tcb: Tcb { + sgx_components: vec![TcbComponents { svn: 0 }; 16], + tdx_components: vec![], + pce_svn: 13, + }, + tcb_date: "2023-07-22T00:00:00Z".to_string(), + tcb_status, + advisory_ids: vec![], + }, + tcb_date_tag: 1_690_000_000, + pck: PckIdentity { + ppid: vec![0u8; 16], + cpu_svn: [0u8; 16], + pce_svn: 13, + pce_id: 0, + fmspc: [0u8; 6], + sgx_type: 0, + platform_instance_id: None, + dynamic_platform: PckCertFlag::Undefined, + cached_keys: PckCertFlag::Undefined, + smt_enabled: PckCertFlag::Undefined, + platform_provider_id: None, + }, + root_key_id: [0u8; 48], + pck_crl_num: 1, + root_ca_crl_num: 1, + }, + qe: QeInfo { + tcb_level: QeTcbLevel { + tcb: QeTcb { isvsvn: 8 }, + tcb_date: "2024-03-13T00:00:00Z".to_string(), + tcb_status: UpToDate, + advisory_ids: vec![], + }, + report: crate::quote::EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0, + reserved1: [0u8; 28], + attributes: [0u8; 16], + mr_enclave: [0u8; 32], + reserved2: [0u8; 32], + mr_signer: [0u8; 32], + reserved3: [0u8; 96], + isv_prod_id: 1, + isv_svn: 8, + reserved4: [0u8; 60], + report_data: [0u8; 64], + }, + tcb_eval_data_number: 17, + }, + } + } + + /// Create a test time window with future dates (Rego uses real wall clock). + fn make_test_time_window() -> CollateralTimeWindow { + CollateralTimeWindow { + earliest_issue_date: 1_900_000_000, + latest_issue_date: 1_900_100_000, + earliest_expiration_date: 2_000_000_000, + } + } + + /// Create test supplemental data with future expiration for Rego. + fn make_rego_supplemental(status: TcbStatus) -> SupplementalData { + let mut data = make_test_supplemental(status); + data.tcb.earliest_expiration = 2_000_000_000; + data + } + + fn policy_json(reference: &str) -> String { + format!( + r#"{{ + "environment": {{ + "class_id": "{SGX_PLATFORM_CLASS_ID}", + "description": "Test policy" + }}, + "reference": {reference} + }}"# + ) + } + + #[test] + fn rego_strict_accepts_up_to_date() { + let data = make_rego_supplemental(UpToDate); + let tw = make_test_time_window(); + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.eval(&data, &tw); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_strict_rejects_out_of_date() { + let data = make_rego_supplemental(OutOfDate); + let tw = make_test_time_window(); + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let err = policy.eval(&data, &tw).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected appraisal failure, got: {err}" + ); + } + + #[test] + fn rego_permissive_accepts_out_of_date() { + let data = make_rego_supplemental(OutOfDate); + let tw = make_test_time_window(); + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate", "OutOfDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.eval(&data, &tw); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_rejects_advisory() { + let mut data = make_rego_supplemental(UpToDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00334".into()]; + let tw = make_test_time_window(); + let json = policy_json( + r#"{ + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0, + "rejected_advisory_ids": ["INTEL-SA-00334"] + }"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let err = policy.eval(&data, &tw).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected advisory rejection, got: {err}" + ); + } + + #[test] + fn rego_platform_grace_period_accepts() { + let mut data = make_rego_supplemental(OutOfDate); + data.platform.tcb_date_tag = 1_690_000_000; + let tw = make_test_time_window(); + let json = policy_json( + r#"{ + "accepted_tcb_status": ["UpToDate", "OutOfDate"], + "collateral_grace_period": 0, + "platform_grace_period": 999999999 + }"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.eval(&data, &tw); + assert!( + result.is_ok(), + "expected Ok, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_expiration_check_rejects_expired_collateral() { + let data = make_rego_supplemental(UpToDate); + let tw = CollateralTimeWindow { + earliest_issue_date: 1_700_000_000, + latest_issue_date: 1_700_100_000, + earliest_expiration_date: 1_703_000_000, + }; + let json = policy_json( + r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, + ); + let policy = RegoPolicy::new(&json).unwrap(); + let err = policy.eval(&data, &tw).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected expiration failure, got: {err}" + ); + } + + #[test] + fn rego_no_collateral_grace_skips_expiration_check() { + let data = make_rego_supplemental(UpToDate); + let tw = CollateralTimeWindow { + earliest_issue_date: 1_700_000_000, + latest_issue_date: 1_700_100_000, + earliest_expiration_date: 1_703_000_000, + }; + let json = policy_json(r#"{"accepted_tcb_status": ["UpToDate"]}"#); + let policy = RegoPolicy::new(&json).unwrap(); + let result = policy.eval(&data, &tw); + assert!( + result.is_ok(), + "expected Ok (no expiration check), got: {:?}", + result.unwrap_err() + ); + } + + #[test] + fn rego_missing_class_id_errors() { + let json = r#"{"reference": {"accepted_tcb_status": ["UpToDate"]}}"#; + assert!(RegoPolicy::new(json).is_err()); + } + + #[test] + fn rego_to_measurement_tcb_status_mapping() { + let data = make_test_supplemental(ConfigurationAndSWHardeningNeeded); + let tw = make_test_time_window(); + let m = build_merged_measurement(&data, &tw); + let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); + assert_eq!(statuses.len(), 3); + assert_eq!(statuses[0], "UpToDate"); + assert_eq!(statuses[1], "SWHardeningNeeded"); + assert_eq!(statuses[2], "ConfigurationNeeded"); + } + + #[test] + fn rego_to_measurement_omits_undefined_flags() { + let data = make_test_supplemental(UpToDate); + assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined); + let tw = make_test_time_window(); + let m = build_merged_measurement(&data, &tw); + assert!(m.get("is_dynamic_platform").is_none()); + assert!(m.get("cached_keys").is_none()); + assert!(m.get("smt_enabled").is_none()); + } + + #[test] + fn rego_to_measurement_includes_true_flags() { + let mut data = make_test_supplemental(UpToDate); + data.platform.pck.dynamic_platform = PckCertFlag::True; + data.platform.pck.cached_keys = PckCertFlag::False; + data.platform.pck.smt_enabled = PckCertFlag::True; + let tw = make_test_time_window(); + let m = build_merged_measurement(&data, &tw); + assert_eq!(m.get("is_dynamic_platform").unwrap(), true); + assert_eq!(m.get("cached_keys").unwrap(), false); + assert_eq!(m.get("smt_enabled").unwrap(), true); + } + + #[test] + fn rego_platform_measurement_uses_unmerged_status() { + let mut data = make_test_supplemental(UpToDate); + data.platform.tcb_level.tcb_status = OutOfDate; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00001".into()]; + let tw = make_test_time_window(); + let m = build_platform_measurement(&data, &tw); + let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); + assert!(statuses.contains(&serde_json::json!("OutOfDate"))); + let advisories = m.get("advisory_ids").unwrap().as_array().unwrap(); + assert_eq!(advisories, &[serde_json::json!("INTEL-SA-00001")]); + } + + #[test] + fn rego_qe_measurement_fields() { + let data = make_rego_supplemental(UpToDate); + let tw = make_test_time_window(); + let m = build_qe_measurement(&data, &tw); + assert!(m.get("tcb_status").is_some()); + assert_eq!(m.get("tcb_eval_num").unwrap(), 17); + assert!(m.get("root_key_id").is_some()); + assert!(m.get("earliest_issue_date").is_some()); + assert!(m.get("latest_issue_date").is_some()); + assert!(m.get("earliest_expiration_date").is_some()); + assert!(m.get("tcb_level_date_tag").is_some()); + } + + #[test] + fn rego_sgx_enclave_measurement_fields() { + use crate::quote::EnclaveReport; + + let mut report = EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0x12345678, + reserved1: [0u8; 28], + attributes: [0xAA; 16], + mr_enclave: [0xBB; 32], + reserved2: [0u8; 32], + mr_signer: [0xCC; 32], + reserved3: [0u8; 96], + isv_prod_id: 42, + isv_svn: 7, + reserved4: [0u8; 60], + report_data: [0xDD; 64], + }; + report.reserved1[12..28].copy_from_slice(&[0x11; 16]); + report.reserved3[32..96].copy_from_slice(&[0x22; 64]); + report.reserved4[0..2].copy_from_slice(&42u16.to_le_bytes()); + report.reserved4[44..60].copy_from_slice(&[0x33; 16]); + + let m = sgx_enclave_measurement(&report); + assert!(m.get("sgx_mrenclave").is_some()); + assert!(m.get("sgx_mrsigner").is_some()); + assert_eq!(m.get("sgx_isvprodid").unwrap(), 42); + assert_eq!(m.get("sgx_isvsvn").unwrap(), 7); + assert!(m.get("sgx_reportdata").is_some()); + assert!(m.get("sgx_configid").is_some()); + assert_eq!(m.get("sgx_configsvn").unwrap(), 42); + assert!(m.get("sgx_isvextprodid").is_some()); + assert!(m.get("sgx_isvfamilyid").is_some()); + } + + #[test] + fn rego_policy_set_sgx_platform_accepts() { + let data = make_rego_supplemental(UpToDate); + let tw = make_test_time_window(); + let platform_json = format!( + r#"{{ + "environment": {{ "class_id": "{SGX_PLATFORM_CLASS_ID}" }}, + "reference": {{ "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 }} + }}"# + ); + let policies = RegoPolicySet::new(&[&platform_json]).unwrap(); + let qvl_result = vec![serde_json::json!({ + "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, + "measurement": build_platform_measurement(&data, &tw), + })]; + assert!( + policies.eval_rego(qvl_result).is_ok(), + "expected Ok, got: {:?}", + { + let qvl_result2 = vec![serde_json::json!({ + "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, + "measurement": build_platform_measurement(&data, &tw), + })]; + policies.eval_rego(qvl_result2).unwrap_err() + } + ); + } + + #[test] + fn rego_policy_set_class_id_mismatch_fails() { + let data = make_rego_supplemental(UpToDate); + let tw = make_test_time_window(); + let tdx_class_id = "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f"; + let policy_json = format!( + r#"{{ + "environment": {{ "class_id": "{tdx_class_id}" }}, + "reference": {{ "accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0 }} + }}"# + ); + let policies = RegoPolicySet::new(&[&policy_json]).unwrap(); + let qvl_result = vec![serde_json::json!({ + "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, + "measurement": build_platform_measurement(&data, &tw), + })]; + let err = policies.eval_rego(qvl_result).unwrap_err().to_string(); + assert!( + err.contains("appraisal failed"), + "expected appraisal failure on class_id mismatch, got: {err}" + ); + } +} diff --git a/src/policy/simple.rs b/src/policy/simple.rs new file mode 100644 index 0000000..c63b4aa --- /dev/null +++ b/src/policy/simple.rs @@ -0,0 +1,673 @@ +use core::time::Duration; + +use anyhow::{bail, Result}; + +use { + super::{PckCertFlag, Policy, SupplementalData}, + crate::tcb_info::TcbStatus, + alloc::string::String, + alloc::vec::Vec, +}; + +/// Status-based verification policy. +/// +/// By default, the policy is strict: only `UpToDate` status is accepted. +/// Comprehensive verification policy with builder pattern. +/// +/// Covers all checks from Intel's Appraisal framework (`qal_script.rego`). +/// Strict by default: only `UpToDate`, no grace period, no advisory tolerance. +/// +/// # Example +/// ```ignore +/// use dcap_qvl::SimplePolicy; +/// use dcap_qvl::TcbStatus; +/// +/// // Strict: only UpToDate, collateral must not be expired +/// let policy = SimplePolicy::strict(now); +/// +/// // With 90-day collateral grace period +/// use core::time::Duration; +/// let policy = SimplePolicy::strict(now) +/// .allow_status(TcbStatus::SWHardeningNeeded) +/// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) +/// .accept_advisory("INTEL-SA-00334"); +/// ``` +#[derive(Clone, Debug)] +pub struct SimplePolicy { + acceptable_statuses: u8, + + // Current time + grace periods (mutually exclusive, default 0 = no tolerance) + now: u64, + collateral_grace_period: u64, + platform_grace_period: u64, + + // TCB evaluation + min_tcb_eval_data_number: Option, + + // Advisory whitelist (all advisories in quote must be in this set) + accepted_advisory_ids: Vec, + + // Platform flags (default false = reject if True) + allow_dynamic_platform: bool, + allow_cached_keys: bool, + allow_smt: bool, + + // SGX type whitelist (None = skip check) + accepted_sgx_types: Option>, +} + +impl SimplePolicy { + const UP_TO_DATE: u8 = 1 << 0; + const SW_HARDENING_NEEDED: u8 = 1 << 1; + const CONFIGURATION_NEEDED: u8 = 1 << 2; + const CONFIGURATION_AND_SW_HARDENING_NEEDED: u8 = 1 << 3; + const OUT_OF_DATE: u8 = 1 << 4; + const OUT_OF_DATE_CONFIGURATION_NEEDED: u8 = 1 << 5; + + fn status_to_flag(status: TcbStatus) -> u8 { + match status { + TcbStatus::UpToDate => Self::UP_TO_DATE, + TcbStatus::SWHardeningNeeded => Self::SW_HARDENING_NEEDED, + TcbStatus::ConfigurationNeeded => Self::CONFIGURATION_NEEDED, + TcbStatus::ConfigurationAndSWHardeningNeeded => { + Self::CONFIGURATION_AND_SW_HARDENING_NEEDED + } + TcbStatus::OutOfDate => Self::OUT_OF_DATE, + TcbStatus::OutOfDateConfigurationNeeded => Self::OUT_OF_DATE_CONFIGURATION_NEEDED, + TcbStatus::Revoked => 0, + } + } + + fn new_with_statuses(now: u64, acceptable_statuses: u8) -> Self { + Self { + acceptable_statuses, + now, + collateral_grace_period: 0, + platform_grace_period: 0, + min_tcb_eval_data_number: None, + accepted_advisory_ids: Vec::new(), + allow_dynamic_platform: false, + allow_cached_keys: false, + allow_smt: false, + accepted_sgx_types: None, + } + } + + /// Create a strict policy: only `UpToDate` status is accepted, + /// no grace period, no advisory tolerance. + pub fn strict(now_secs: u64) -> Self { + Self::new_with_statuses(now_secs, Self::UP_TO_DATE) + } + + /// Allow an additional TCB status. + pub fn allow_status(mut self, status: TcbStatus) -> Self { + self.acceptable_statuses |= Self::status_to_flag(status); + self + } + + /// Set collateral grace period (default: zero). Accepts quotes where + /// `earliest_expiration_date + grace_period >= now`. + /// + /// Must be zero if [`platform_grace_period`](Self::platform_grace_period) is non-zero. + pub fn collateral_grace_period(mut self, duration: Duration) -> Self { + self.collateral_grace_period = duration.as_secs(); + self + } + + /// Set platform grace period (default: zero). When TCB status is + /// OutOfDate or OutOfDateConfigurationNeeded, accepts quotes where + /// `tcb_level_date_tag + grace_period >= now`. Skipped for UpToDate/ConfigNeeded/SWHardening. + /// + /// Must be zero if [`collateral_grace_period`](Self::collateral_grace_period) is non-zero. + pub fn platform_grace_period(mut self, duration: Duration) -> Self { + self.platform_grace_period = duration.as_secs(); + self + } + + /// Set minimum TCB evaluation data number. Rejects quotes with + /// `tcb_eval_data_number` below this threshold. + pub fn min_tcb_eval_data_number(mut self, min: u32) -> Self { + self.min_tcb_eval_data_number = Some(min); + self + } + + /// Accept a specific advisory ID. All advisories in the quote must be in + /// the accepted set or validation fails. By default the set is empty, + /// rejecting any quote with advisories. + pub fn accept_advisory(mut self, id: impl Into) -> Self { + self.accepted_advisory_ids.push(id.into()); + self + } + + /// Set whether dynamic platforms are allowed. If `false` (default), rejects + /// quotes where `dynamic_platform` is `True`. + pub fn allow_dynamic_platform(mut self, allow: bool) -> Self { + self.allow_dynamic_platform = allow; + self + } + + /// Set whether cached keys are allowed. If `false` (default), rejects + /// quotes where `cached_keys` is `True`. + pub fn allow_cached_keys(mut self, allow: bool) -> Self { + self.allow_cached_keys = allow; + self + } + + /// Set whether SMT (simultaneous multithreading / hyperthreading) is allowed. + /// If `false` (default), rejects quotes where `smt_enabled` is `True`. + pub fn allow_smt(mut self, allow: bool) -> Self { + self.allow_smt = allow; + self + } + + /// Set accepted SGX types (0=Standard, 1=Scalable, 2=ScalableWithIntegrity). + /// Rejects quotes with `sgx_type` not in this list. Default: skip check. + pub fn accepted_sgx_types(mut self, types: &[u8]) -> Self { + self.accepted_sgx_types = Some(types.to_vec()); + self + } + + /// Check if a TCB status is acceptable according to this policy. + pub fn is_status_acceptable(&self, status: TcbStatus) -> bool { + let flag = Self::status_to_flag(status); + (self.acceptable_statuses & flag) != 0 + } +} + +impl Policy for SimplePolicy { + fn validate(&self, data: &SupplementalData) -> Result<()> { + // 1. TCB status whitelist + if !self.is_status_acceptable(data.tcb.status) { + bail!( + "TCB status {:?} is not acceptable by policy", + data.tcb.status + ); + } + + // 3 & 4. Grace periods (mutually exclusive) + if self.collateral_grace_period > 0 && self.platform_grace_period > 0 { + bail!("collateral_grace_period and platform_grace_period are mutually exclusive"); + } + + // 3. Collateral expiration: earliest_expiration + grace >= now + if data + .tcb + .earliest_expiration + .saturating_add(self.collateral_grace_period) + < self.now + { + bail!( + "Collateral expired: earliest_expiration {} + grace {} < now {}", + data.tcb.earliest_expiration, + self.collateral_grace_period, + self.now + ); + } + + // 4. Platform TCB freshness: tcb_date_tag + grace >= now + // Only checked when TCB status indicates the platform is out-of-date. + let is_out_of_date = matches!( + data.tcb.status, + TcbStatus::OutOfDate | TcbStatus::OutOfDateConfigurationNeeded + ); + if is_out_of_date + && data + .platform + .tcb_date_tag + .saturating_add(self.platform_grace_period) + < self.now + { + bail!( + "Platform TCB too old: tcb_date_tag {} + grace {} < now {}", + data.platform.tcb_date_tag, + self.platform_grace_period, + self.now + ); + } + + // Determine if we're within a platform grace window — advisory checks are skipped + // only for pure OutOfDate during platform grace. Collateral grace does NOT skip + // advisories (stale collateral doesn't invalidate advisory data). + // OutOfDateConfigurationNeeded does NOT skip either (Configuration advisories + // are unrelated to the OutOfDate grace window). + let in_platform_grace = + self.platform_grace_period > 0 && data.tcb.status == TcbStatus::OutOfDate; + + // 2. Advisory ID whitelist (skipped only during platform grace for pure OutOfDate) + if !in_platform_grace { + for id in &data.tcb.advisory_ids { + if !self + .accepted_advisory_ids + .iter() + .any(|a| a.eq_ignore_ascii_case(id)) + { + bail!("Advisory ID {id} is not in the accepted set"); + } + } + } + + // 5. Minimum TCB evaluation data number + if let Some(min) = self.min_tcb_eval_data_number { + if data.tcb.eval_data_number < min { + bail!( + "TCB eval data number {} is below minimum {}", + data.tcb.eval_data_number, + min + ); + } + } + + // 6. Dynamic platform flag + if !self.allow_dynamic_platform && data.platform.pck.dynamic_platform == PckCertFlag::True { + bail!("Dynamic platform is not allowed by policy"); + } + + // 7. Cached keys flag + if !self.allow_cached_keys && data.platform.pck.cached_keys == PckCertFlag::True { + bail!("Cached keys are not allowed by policy"); + } + + // 8. SMT flag + if !self.allow_smt && data.platform.pck.smt_enabled == PckCertFlag::True { + bail!("SMT (hyperthreading) is not allowed by policy"); + } + + // 9. SGX type whitelist + if let Some(ref types) = self.accepted_sgx_types { + if !types.contains(&data.platform.pck.sgx_type) { + bail!( + "SGX type {} is not in accepted types {:?}", + data.platform.pck.sgx_type, + types + ); + } + } + + Ok(()) + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use crate::policy::{PckIdentity, PlatformInfo, QeInfo, TcbVerdict}; + use crate::tcb_info::TcbStatus::*; + + fn make_test_supplemental(tcb_status: TcbStatus) -> SupplementalData { + use crate::qe_identity::{QeTcb, QeTcbLevel}; + use crate::tcb_info::{Tcb, TcbComponents, TcbLevel}; + + SupplementalData { + tee_type: 0, + tcb: TcbVerdict { + status: tcb_status, + advisory_ids: vec![], + eval_data_number: 17, + earliest_expiration: 1_703_000_000, // ~2023-12-19 + }, + platform: PlatformInfo { + tcb_level: TcbLevel { + tcb: Tcb { + sgx_components: vec![TcbComponents { svn: 0 }; 16], + tdx_components: vec![], + pce_svn: 13, + }, + tcb_date: "2023-07-22T00:00:00Z".to_string(), + tcb_status, + advisory_ids: vec![], + }, + tcb_date_tag: 1_690_000_000, // ~2023-07-22 + pck: PckIdentity { + ppid: vec![0u8; 16], + cpu_svn: [0u8; 16], + pce_svn: 13, + pce_id: 0, + fmspc: [0u8; 6], + sgx_type: 0, + platform_instance_id: None, + dynamic_platform: PckCertFlag::Undefined, + cached_keys: PckCertFlag::Undefined, + smt_enabled: PckCertFlag::Undefined, + platform_provider_id: None, + }, + root_key_id: [0u8; 48], + pck_crl_num: 1, + root_ca_crl_num: 1, + }, + qe: QeInfo { + tcb_level: QeTcbLevel { + tcb: QeTcb { isvsvn: 8 }, + tcb_date: "2024-03-13T00:00:00Z".to_string(), + tcb_status: UpToDate, + advisory_ids: vec![], + }, + report: crate::quote::EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0, + reserved1: [0u8; 28], + attributes: [0u8; 16], + mr_enclave: [0u8; 32], + reserved2: [0u8; 32], + mr_signer: [0u8; 32], + reserved3: [0u8; 96], + isv_prod_id: 1, + isv_svn: 8, + reserved4: [0u8; 60], + report_data: [0u8; 64], + }, + tcb_eval_data_number: 17, + }, + } + } + + // -- TCB status checks -- + + #[test] + fn policy_strict_accepts_up_to_date() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_702_000_000); // within collateral window + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_strict_rejects_sw_hardening() { + let data = make_test_supplemental(SWHardeningNeeded); + let policy = SimplePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("TCB status"), "{err}"); + } + + #[test] + fn policy_out_of_date_with_fresh_tcb_date_accepts() { + let mut data = make_test_supplemental(OutOfDate); + data.platform.tcb_date_tag = 1_702_000_000; + let policy = SimplePolicy::strict(1_702_000_000).allow_status(OutOfDate); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_allow_status_builder() { + let data = make_test_supplemental(SWHardeningNeeded); + let policy = SimplePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded); + assert!(policy.validate(&data).is_ok()); + } + + // -- Advisory ID whitelist -- + + #[test] + fn policy_rejects_unknown_advisory() { + let mut data = make_test_supplemental(UpToDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + let policy = SimplePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); + } + + #[test] + fn policy_accepts_whitelisted_advisory() { + let mut data = make_test_supplemental(UpToDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + let policy = SimplePolicy::strict(1_702_000_000).accept_advisory("INTEL-SA-00615"); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_advisory_case_insensitive() { + let mut data = make_test_supplemental(UpToDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + let policy = SimplePolicy::strict(1_702_000_000).accept_advisory("intel-sa-00615"); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_empty_advisories_passes() { + let data = make_test_supplemental(UpToDate); + assert!(data.tcb.advisory_ids.is_empty()); + let policy = SimplePolicy::strict(1_702_000_000); + // Empty advisory list in quote → nothing to check against whitelist → passes + assert!(policy.validate(&data).is_ok()); + } + + // -- Collateral grace period -- + + #[test] + fn policy_collateral_expired_no_grace_rejects() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_704_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Collateral expired"), "{err}"); + } + + #[test] + fn policy_collateral_expired_with_grace_accepts() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_704_000_000) + .collateral_grace_period(Duration::from_secs(2_000_000)); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_collateral_expired_grace_too_short_rejects() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_704_000_000) + .collateral_grace_period(Duration::from_secs(500_000)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Collateral expired"), "{err}"); + } + + #[test] + fn policy_collateral_not_expired_zero_grace_passes() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_702_000_000); + assert!(policy.validate(&data).is_ok()); + } + + // -- Platform grace period -- + + #[test] + fn policy_platform_grace_skipped_for_up_to_date() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_702_000_000); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_skipped_for_sw_hardening() { + let data = make_test_supplemental(SWHardeningNeeded); + let policy = SimplePolicy::strict(1_702_000_000).allow_status(SWHardeningNeeded); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_skipped_for_config_needed() { + let data = make_test_supplemental(ConfigurationNeeded); + let policy = SimplePolicy::strict(1_702_000_000).allow_status(ConfigurationNeeded); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_checked_for_out_of_date_rejects() { + let data = make_test_supplemental(OutOfDate); + let policy = SimplePolicy::strict(1_702_000_000).allow_status(OutOfDate); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Platform TCB too old"), "{err}"); + } + + #[test] + fn policy_platform_grace_checked_for_out_of_date_accepts_with_grace() { + let data = make_test_supplemental(OutOfDate); + let policy = SimplePolicy::strict(1_702_000_000) + .allow_status(OutOfDate) + .platform_grace_period(Duration::from_secs(13_000_000)); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_platform_grace_too_short_rejects() { + let data = make_test_supplemental(OutOfDate); + let policy = SimplePolicy::strict(1_702_000_000) + .allow_status(OutOfDate) + .platform_grace_period(Duration::from_secs(11_000_000)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Platform TCB too old"), "{err}"); + } + + #[test] + fn policy_platform_grace_checked_for_out_of_date_config_needed() { + let data = make_test_supplemental(OutOfDateConfigurationNeeded); + let policy = SimplePolicy::strict(1_702_000_000).allow_status(OutOfDateConfigurationNeeded); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Platform TCB too old"), "{err}"); + } + + #[test] + fn policy_grace_periods_mutually_exclusive() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_702_000_000) + .collateral_grace_period(Duration::from_secs(100)) + .platform_grace_period(Duration::from_secs(100)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("mutually exclusive"), "{err}"); + } + + // -- min_tcb_eval_data_number -- + + #[test] + fn policy_min_eval_num_rejects_below() { + let data = make_test_supplemental(UpToDate); + assert_eq!(data.tcb.eval_data_number, 17); + let policy = SimplePolicy::strict(1_702_000_000).min_tcb_eval_data_number(20); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("below minimum"), "{err}"); + } + + #[test] + fn policy_min_eval_num_accepts_equal() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_702_000_000).min_tcb_eval_data_number(17); + assert!(policy.validate(&data).is_ok()); + } + + // -- Platform flags -- + + #[test] + fn policy_rejects_dynamic_platform_true() { + let mut data = make_test_supplemental(UpToDate); + data.platform.pck.dynamic_platform = PckCertFlag::True; + let policy = SimplePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Dynamic platform"), "{err}"); + } + + #[test] + fn policy_allows_dynamic_platform_when_configured() { + let mut data = make_test_supplemental(UpToDate); + data.platform.pck.dynamic_platform = PckCertFlag::True; + let policy = SimplePolicy::strict(1_702_000_000).allow_dynamic_platform(true); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_undefined_platform_flags_pass() { + let data = make_test_supplemental(UpToDate); + assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined); + assert_eq!(data.platform.pck.cached_keys, PckCertFlag::Undefined); + assert_eq!(data.platform.pck.smt_enabled, PckCertFlag::Undefined); + let policy = SimplePolicy::strict(1_702_000_000); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_rejects_smt_true() { + let mut data = make_test_supplemental(UpToDate); + data.platform.pck.smt_enabled = PckCertFlag::True; + let policy = SimplePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("SMT"), "{err}"); + } + + #[test] + fn policy_rejects_cached_keys_true() { + let mut data = make_test_supplemental(UpToDate); + data.platform.pck.cached_keys = PckCertFlag::True; + let policy = SimplePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("Cached keys"), "{err}"); + } + + // -- SGX type whitelist -- + + #[test] + fn policy_sgx_type_not_configured_passes() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_702_000_000); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_sgx_type_whitelist_rejects() { + let mut data = make_test_supplemental(UpToDate); + data.platform.pck.sgx_type = 1; + let policy = SimplePolicy::strict(1_702_000_000).accepted_sgx_types(&[0]); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("SGX type"), "{err}"); + } + + #[test] + fn policy_sgx_type_whitelist_accepts() { + let mut data = make_test_supplemental(UpToDate); + data.platform.pck.sgx_type = 1; + let policy = SimplePolicy::strict(1_702_000_000).accepted_sgx_types(&[0, 1, 2]); + assert!(policy.validate(&data).is_ok()); + } + + // -- Advisory skipped during grace -- + + #[test] + fn policy_advisory_checked_during_collateral_grace() { + let mut data = make_test_supplemental(UpToDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + // Collateral grace doesn't skip advisory checks — stale collateral + // doesn't invalidate advisory data. + let policy = SimplePolicy::strict(1_704_000_000) + .collateral_grace_period(Duration::from_secs(2_000_000)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); + } + + #[test] + fn policy_advisory_skipped_during_platform_grace() { + let mut data = make_test_supplemental(OutOfDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + // now=1_702_000_000, tcb_date_tag=1_690_000_000 (old), + // grace=13_000_000 → within grace → advisories skipped + let policy = SimplePolicy::strict(1_702_000_000) + .allow_status(OutOfDate) + .platform_grace_period(Duration::from_secs(13_000_000)); + assert!(policy.validate(&data).is_ok()); + } + + #[test] + fn policy_advisory_not_skipped_for_out_of_date_config_needed() { + // OutOfDateConfigurationNeeded should NOT skip advisory checks during grace, + // because the Configuration advisories are unrelated to the OutOfDate grace. + let mut data = make_test_supplemental(OutOfDateConfigurationNeeded); + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + let policy = SimplePolicy::strict(1_702_000_000) + .allow_status(OutOfDateConfigurationNeeded) + .platform_grace_period(Duration::from_secs(13_000_000)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); + } + + #[test] + fn policy_advisory_checked_without_grace() { + let mut data = make_test_supplemental(UpToDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + // No grace period → advisories checked normally + let policy = SimplePolicy::strict(1_702_000_000); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); + } +} diff --git a/src/verify.rs b/src/verify.rs index bfdf886..5e90d97 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -100,7 +100,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; /// let sup = result.supplemental()?; /// println!("TCB status: {:?}", sup.tcb.status); /// // Or apply policy directly -/// let report = result.validate(&QuotePolicy::strict(now))?; +/// let report = result.validate(&SimplePolicy::strict(now))?; /// ``` pub struct QuoteVerificationResult { report: Report, @@ -262,7 +262,7 @@ impl QuoteVerificationResult { /// Compute the full collateral time window (expensive: ~14 DER parses). /// /// Only needed for Rego evaluation. Called lazily, not during `verify()`. - fn compute_time_window(&self) -> Result { + fn compute_time_window(&self) -> Result { // Re-extract PCK cert chain from owned collateral let pck_certs = if let Some(pem_chain) = &self.collateral.pck_certificate_chain { extract_certs(pem_chain.as_bytes()).unwrap_or_default() @@ -280,7 +280,7 @@ impl QuoteVerificationResult { let (earliest_issue, latest_issue, earliest_expiration) = compute_collateral_time_window(&self.collateral, &pck_certs, &tcb_info, &qe_identity)?; - Ok(crate::policy::rego_policy::CollateralTimeWindow { + Ok(crate::policy::rego::CollateralTimeWindow { earliest_issue_date: earliest_issue, latest_issue_date: latest_issue, earliest_expiration_date: earliest_expiration, @@ -294,9 +294,9 @@ impl QuoteVerificationResult { pub(crate) fn to_rego_qvl_result( &self, supplemental: &SupplementalData, - tw: &crate::policy::rego_policy::CollateralTimeWindow, + tw: &crate::policy::rego::CollateralTimeWindow, ) -> Vec { - use crate::policy::rego_policy::{ + use crate::policy::rego::{ build_platform_measurement, build_qe_measurement, platform_class_id, tenant_class_id, tenant_measurement, }; diff --git a/tests/near/contracts/gas-test/Cargo.lock b/tests/near/contracts/gas-test/Cargo.lock index 3cc75bd..d47fa2e 100644 --- a/tests/near/contracts/gas-test/Cargo.lock +++ b/tests/near/contracts/gas-test/Cargo.lock @@ -592,7 +592,7 @@ dependencies = [ [[package]] name = "dcap-qvl" -version = "0.3.11" +version = "0.3.12" dependencies = [ "anyhow", "asn1_der", From fa9a36df63195950ae2962b3db1cd2a6dac3f830 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 03:52:21 +0000 Subject: [PATCH 06/33] =?UTF-8?q?docs:=20add=20policy=20guide,=20rename=20?= =?UTF-8?q?into=5Freport=20=E2=86=92=20into=5Freport=5Funchecked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create docs/policy.md with comprehensive policy validation guide - Rename into_report() to into_report_unchecked() with warning docs - Add doc comments to CollateralTimeWindow fields - Update README with two-phase API example and policy section --- README.md | 34 +++++++++- cli/src/main.rs | 2 +- docs/policy.md | 153 ++++++++++++++++++++++++++++++++++++++++++ src/policy/rego.rs | 14 +++- src/python.rs | 4 +- src/verify.rs | 17 +++-- tests/verify_quote.rs | 6 +- 7 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 docs/policy.md diff --git a/README.md b/README.md index a7bc3d5..ae097ff 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ use dcap_qvl::verify::rustcrypto::verify; ```rust use dcap_qvl::collateral::get_collateral; -// Use explicit backend for predictable behavior -use dcap_qvl::verify::ring::verify; +use dcap_qvl::verify::{QuoteVerifier, ring}; +use dcap_qvl::SimplePolicy; use dcap_qvl::PHALA_PCCS_URL; #[tokio::main] @@ -86,11 +86,39 @@ async fn main() { let collateral = get_collateral(&pccs_url, "e).await.expect("failed to get collateral"); let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); - let report = verify("e, &collateral, now).expect("failed to verify quote"); + + // Phase 1: Cryptographic verification + let verifier = QuoteVerifier::new_prod(ring::backend()); + let result = verifier.verify("e, collateral, now).expect("verification failed"); + + // Phase 2: Policy validation + let report = result.validate(&SimplePolicy::strict(now)).expect("policy failed"); println!("{:?}", report); } ``` +# Policy Validation + +After cryptographic verification, apply a **policy** to check TCB status, advisory IDs, collateral freshness, and platform flags. + +```rust +use dcap_qvl::{SimplePolicy, TcbStatus}; +use core::time::Duration; + +// Strict: only UpToDate (default) +let policy = SimplePolicy::strict(now); + +// Relaxed: accept OutOfDate + 30-day collateral grace +let policy = SimplePolicy::strict(now) + .allow_status(TcbStatus::OutOfDate) + .collateral_grace_period(Duration::from_secs(30 * 24 * 3600)) + .accept_advisory("INTEL-SA-00334"); +``` + +For custom validation logic, implement the `Policy` trait directly. + +See [docs/policy.md](docs/policy.md) for the complete policy guide, including grace period semantics, platform flags, `RegoPolicy`, and custom `Policy` trait examples. + # Python Bindings diff --git a/cli/src/main.rs b/cli/src/main.rs index fdce903..931fed9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -109,7 +109,7 @@ async fn command_verify_quote(args: VerifyQuoteArgs) -> Result<()> { .verify("e, collateral, now) .context("Failed to verify quote")?; let report = result - .into_report(); + .into_report_unchecked(); println!( "{}", serde_json::to_string(&report).context("Failed to serialize report")? diff --git a/docs/policy.md b/docs/policy.md new file mode 100644 index 0000000..4f9c09c --- /dev/null +++ b/docs/policy.md @@ -0,0 +1,153 @@ +# Policy Validation + +After cryptographic verification, `dcap-qvl` supports a **policy validation** phase that checks the platform's TCB (Trusted Computing Base) status, advisory IDs, collateral freshness, and platform configuration flags. + +## Two-Phase Verification + +``` +verify() ──► QuoteVerificationResult ──► validate(policy) ──► VerifiedReport + │ │ │ + │ ├─ supplemental() ├─ SimplePolicy (built-in) + │ │ (lazy, on demand) ├─ RegoPolicy (Intel Rego script) + │ │ └─ impl Policy (custom) + │ └─ into_report_unchecked() + │ (skip policy) + crypto only inspect data enforce rules +``` + +- **`verify()`** — performs cryptographic verification only (signature, certificate chain, CRL, QE identity). Returns `QuoteVerificationResult`. +- **`supplemental()`** — lazily builds `SupplementalData` with TCB status, advisory IDs, platform flags, etc. +- **`validate(policy)`** — applies a `Policy` to the supplemental data. Returns `VerifiedReport` on success. +- **`into_report_unchecked()`** — skips policy validation entirely (use when you handle validation externally). + +## SimplePolicy + +The built-in policy with 9 checks from Intel's Appraisal framework. Strict by default — only `UpToDate` status, no grace period, no advisory tolerance. + +### Basic Usage + +```rust +use dcap_qvl::verify::{QuoteVerifier, ring}; +use dcap_qvl::SimplePolicy; + +let verifier = QuoteVerifier::new_prod(ring::backend()); +let result = verifier.verify("e, collateral, now)?; + +// Strict: only UpToDate, collateral must not be expired +let report = result.validate(&SimplePolicy::strict(now))?; +``` + +### Builder Methods + +```rust +use dcap_qvl::{SimplePolicy, TcbStatus}; +use core::time::Duration; + +let policy = SimplePolicy::strict(now) + // Accept additional TCB statuses + .allow_status(TcbStatus::SWHardeningNeeded) + .allow_status(TcbStatus::ConfigurationNeeded) + // Accept specific advisory IDs (case-insensitive) + .accept_advisory("INTEL-SA-00334") + .accept_advisory("INTEL-SA-00615") + // Collateral freshness: accept expired collateral within grace window + .collateral_grace_period(Duration::from_secs(30 * 24 * 3600)) // 30 days + // Minimum TCB evaluation data number + .min_tcb_eval_data_number(17) + // Platform flags (default: reject True) + .allow_dynamic_platform(true) + .allow_cached_keys(true) + .allow_smt(true) + // SGX type whitelist (default: skip check) + .accepted_sgx_types(&[0, 1]); // Standard + Scalable +``` + +### The 9 Checks + +| # | Check | Default | Builder | +|---|-------|---------|---------| +| 1 | **TCB status whitelist** | Only `UpToDate` | `.allow_status(...)` | +| 2 | **Advisory ID whitelist** | Empty set (reject any) | `.accept_advisory(...)` | +| 3 | **Collateral expiration** | `earliest_expiration >= now` | `.collateral_grace_period(Duration)` | +| 4 | **Platform TCB freshness** | Only for OutOfDate statuses | `.platform_grace_period(Duration)` | +| 5 | **Min TCB eval data number** | Skip | `.min_tcb_eval_data_number(n)` | +| 6 | **Dynamic platform flag** | Reject `True` | `.allow_dynamic_platform(true)` | +| 7 | **Cached keys flag** | Reject `True` | `.allow_cached_keys(true)` | +| 8 | **SMT flag** | Reject `True` | `.allow_smt(true)` | +| 9 | **SGX type whitelist** | Skip | `.accepted_sgx_types(&[0, 1, 2])` | + +### Grace Period Behavior + +**Collateral grace** (`collateral_grace_period`): Extends the collateral expiration window. If `earliest_expiration + grace >= now`, the quote is accepted. Does **not** skip advisory checks — stale collateral doesn't invalidate advisory data. + +**Platform grace** (`platform_grace_period`): For `OutOfDate` / `OutOfDateConfigurationNeeded` statuses, checks `tcb_date_tag + grace >= now`. For pure `OutOfDate`, advisory checks are skipped during the grace window (the advisories are inherent to the out-of-date level). For `OutOfDateConfigurationNeeded`, advisories are still checked (the Configuration advisories are unrelated to the OutOfDate aspect). + +The two grace periods are **mutually exclusive** — setting both to non-zero causes a validation error. + +### Platform Flags (Three-State) + +Platform flags (`dynamic_platform`, `cached_keys`, `smt_enabled`) use `PckCertFlag` with three values: + +| Value | Meaning | Default behavior | +|-------|---------|-----------------| +| `True` | Flag is set | **Rejected** | +| `False` | Flag is explicitly unset | Accepted | +| `Undefined` | Not present (Processor CA certs) | Accepted | + +Only `True` is rejected by default. `False` and `Undefined` always pass. + +## Custom Policy + +For logic that `SimplePolicy` cannot express, implement the `Policy` trait directly: + +```rust +use dcap_qvl::{Policy, SupplementalData, TcbStatus}; +use anyhow::{bail, Result}; + +struct MyPolicy { + now: u64, + grace_secs: u64, +} + +impl Policy for MyPolicy { + fn validate(&self, data: &SupplementalData) -> Result<()> { + let in_grace = data.platform.tcb_date_tag + .saturating_add(self.grace_secs) >= self.now; + + // Conditional logic based on grace window + if !in_grace && data.tcb.status != TcbStatus::UpToDate { + bail!("Only UpToDate accepted outside grace period"); + } + + // Check specific advisories even during grace + for id in &data.tcb.advisory_ids { + if id == "INTEL-SA-00220" { + bail!("Critical advisory {id} always rejected"); + } + } + + Ok(()) + } +} +``` + +## RegoPolicy (feature: `rego`) + +Runs Intel's official `qal_script.rego` via the `regorus` Rego interpreter. Accepts a JSON policy string matching Intel's format: + +```rust +use dcap_qvl::RegoPolicy; + +let policy_json = r#"{ + "policy": { + "tcb_status": ["UpToDate", "SWHardeningNeeded"], + "collateral_grace_period": 7776000 + } +}"#; +let policy = RegoPolicy::new(policy_json)?; +let report = result.validate_rego(&policy)?; +``` + +`RegoPolicySet` supports multiple JSON policies for multi-measurement appraisal (one per class_id), matching Intel QAL's full functionality. + +See [Intel's DCAP Appraisal documentation](https://github.com/intel/SGXDataCenterAttestationPrimitives) for the Rego policy JSON format. diff --git a/src/policy/rego.rs b/src/policy/rego.rs index bd457e6..8b2f913 100644 --- a/src/policy/rego.rs +++ b/src/policy/rego.rs @@ -30,10 +30,22 @@ fn tcb_status_to_rego_array(status: TcbStatus) -> serde_json::Value { } } -/// Full collateral time window (expensive to compute, only for Rego). +/// Full collateral time window, aggregated from 8 sources (matching Intel QVL): +/// TcbInfo, QeIdentity, Root CA CRL, PCK CRL, and 4 certificate chains. +/// +/// `SimplePolicy` only needs `earliest_expiration_date` (computed separately +/// from 4 lightweight sources without certificate chain parsing). pub(crate) struct CollateralTimeWindow { + /// `min(issueDate / thisUpdate / notBefore)` across all 8 sources. + /// Rego uses this as `collateral_earliest_issue_date` in measurements. pub earliest_issue_date: u64, + /// `max(issueDate / thisUpdate / notBefore)` across all 8 sources. + /// Rego uses this as `collateral_latest_issue_date` in measurements. pub latest_issue_date: u64, + /// `min(nextUpdate / notAfter)` across all 8 sources (the "weakest link"). + /// Determines when the overall collateral expires. Rego uses this as + /// `collateral_earliest_expiration_date`; also reused by `build_supplemental()` + /// to avoid redundant parsing. pub earliest_expiration_date: u64, } diff --git a/src/python.rs b/src/python.rs index 6173321..0be0cab 100644 --- a/src/python.rs +++ b/src/python.rs @@ -547,7 +547,7 @@ fn py_verify( let verifier = QuoteVerifier::new_prod(crate::verify::ring::backend()); match verifier.verify(quote_bytes, collateral.inner.clone(), now_secs) { Ok(supplemental) => Ok(PyVerifiedReport { - inner: supplemental.into_report(), + inner: supplemental.into_report_unchecked(), }), Err(e) => Err(PyValueError::new_err(format!("Verification failed: {e:?}"))), } @@ -566,7 +566,7 @@ fn py_verify_with_root_ca( let verifier = QuoteVerifier::new(root_ca.to_vec(), crate::verify::ring::backend()); match verifier.verify(quote_bytes, collateral.inner.clone(), now_secs) { Ok(supplemental) => Ok(PyVerifiedReport { - inner: supplemental.into_report(), + inner: supplemental.into_report_unchecked(), }), Err(e) => Err(PyValueError::new_err(format!("Verification failed: {e:?}"))), } diff --git a/src/verify.rs b/src/verify.rs index 5e90d97..db9366a 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -200,16 +200,23 @@ impl QuoteVerificationResult { Ok(self.into_verified_report()) } - /// Convert directly into [`VerifiedReport`] without applying any policy. + /// Convert directly into [`VerifiedReport`] **without applying any policy**. /// - /// Use this only when you have already performed your own validation - /// or intentionally want to skip policy checks. - pub fn into_report(self) -> VerifiedReport { + /// # Warning + /// This skips all policy checks (TCB status, advisory IDs, collateral + /// freshness, platform flags). Use only when you handle validation + /// externally or intentionally accept any verification result. + pub fn into_report_unchecked(self) -> VerifiedReport { self.into_verified_report() } /// Compute earliest_expiration from 4 lightweight sources: - /// TCBInfo nextUpdate, QEIdentity nextUpdate, Root CA CRL nextUpdate, PCK CRL nextUpdate. + /// TCBInfo nextUpdate, QeIdentity nextUpdate, Root CA CRL nextUpdate, PCK CRL nextUpdate. + /// + /// Omits certificate chain `notAfter` dates (which the full 8-source Rego path includes), + /// but this is safe: certificate validity is already enforced during `verify()`, and + /// cert `notAfter` (5–20+ years) never determines the `min()` over CRL/TcbInfo `nextUpdate` + /// (~30 days) in practice. fn compute_earliest_expiration(&self) -> Result { fn parse_rfc3339_ts(s: &str) -> Option { chrono::DateTime::parse_from_rfc3339(s) diff --git a/tests/verify_quote.rs b/tests/verify_quote.rs index 64f1584..a1a86eb 100644 --- a/tests/verify_quote.rs +++ b/tests/verify_quote.rs @@ -18,10 +18,10 @@ pub fn verify( let ring_result = ring_verifier .verify(raw_quote, collateral.clone(), now_secs) - .map(|s| s.into_report()); + .map(|s| s.into_report_unchecked()); let rustcrypto_result = rustcrypto_verifier .verify(raw_quote, collateral.clone(), now_secs) - .map(|s| s.into_report()); + .map(|s| s.into_report_unchecked()); assert_eq!( ring_result.map_err(|e| e.to_string()), @@ -29,7 +29,7 @@ pub fn verify( ); ring_verifier .verify(raw_quote, collateral.clone(), now_secs) - .map(|s| s.into_report()) + .map(|s| s.into_report_unchecked()) } fn now_from_collateral(collateral: &QuoteCollateralV3) -> u64 { From 5ce8c2f6f3ae137395a05c550e399c5db87c26d1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 07:51:03 +0000 Subject: [PATCH 07/33] refactor: unify SupplementalData, impl Policy for Rego, restore flat C FFI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move CollateralTimeWindow fields into SupplementalData (8-source time window) - Add Report to SupplementalData so Policy impls have full context - impl Policy for RegoPolicy and RegoPolicySet (replaces separate eval methods) - Add SimplePolicyConfig (JSON-deserializable) for building SimplePolicy - Python bindings: two-phase API (verify → QVR → validate(policy) → VerifiedReport) - C FFI: keep flat dcap_verify_cb/dcap_verify_with_root_ca_cb (no policy in ABI) - Go bindings unchanged from master --- python-bindings/python/dcap_qvl/__init__.py | 30 +- python-bindings/python/dcap_qvl/_dcap_qvl.pyi | 118 ++++++- python-bindings/tests/test_python_bindings.py | 10 +- src/ffi.rs | 25 +- src/lib.rs | 10 +- src/policy/mod.rs | 19 +- src/policy/rego.rs | 300 ++++++++---------- src/policy/simple.rs | 97 +++++- src/python.rs | 175 +++++++++- src/verify.rs | 195 ++---------- tests/verify_quote.rs | 161 ++++++++-- 11 files changed, 718 insertions(+), 422 deletions(-) diff --git a/python-bindings/python/dcap_qvl/__init__.py b/python-bindings/python/dcap_qvl/__init__.py index edd11d9..0479a89 100644 --- a/python-bindings/python/dcap_qvl/__init__.py +++ b/python-bindings/python/dcap_qvl/__init__.py @@ -4,12 +4,18 @@ This package provides Python bindings for the DCAP (Data Center Attestation Primitives) quote verification library implemented in Rust. +Two-phase verification API (matches Rust): +1. verify(quote, collateral, now_secs) -> QuoteVerificationResult (crypto only) +2. result.validate(policy) -> VerifiedReport (policy checks) + Main classes: - QuoteCollateralV3: Represents quote collateral data -- VerifiedReport: Contains verification results +- QuoteVerificationResult: Intermediate result from crypto verification +- VerifiedReport: Contains verification results after policy validation +- SimplePolicy: Verification policy with builder pattern Main functions: -- verify: Verify a quote with collateral data +- verify: Verify a quote with collateral data (returns QuoteVerificationResult) - get_collateral: Get collateral from PCCS URL - get_collateral_from_pcs: Get collateral from Intel PCS - get_collateral_and_verify: Get collateral and verify quote @@ -22,11 +28,13 @@ from ._dcap_qvl import ( PyQuoteCollateralV3 as QuoteCollateralV3, PyVerifiedReport as VerifiedReport, + PyQuoteVerificationResult as QuoteVerificationResult, PyQuoteHeader as QuoteHeader, PyTdReport10 as TdReport10, PyTdReport15 as TdReport15, PySgxEnclaveReport as SgxEnclaveReport, PyPckExtension as PckExtension, + PySimplePolicy as SimplePolicy, PyQuote as Quote, py_verify as verify, py_verify_with_root_ca as verify_with_root_ca, @@ -92,16 +100,20 @@ async def get_collateral_from_pcs(raw_quote: bytes) -> QuoteCollateralV3: async def get_collateral_and_verify( - raw_quote: bytes, pccs_url: Optional[str] = None -) -> VerifiedReport: - """Get collateral and verify the quote. + raw_quote: bytes, + pccs_url: Optional[str] = None, +) -> QuoteVerificationResult: + """Get collateral and verify the quote (crypto only). + + Returns a QuoteVerificationResult that must be validated with a policy + via .validate(policy) to get a VerifiedReport. Args: raw_quote: Raw quote bytes pccs_url: Optional PCCS URL (defaults to Phala PCCS) Returns: - VerifiedReport: Verification result + QuoteVerificationResult: Use .validate(policy) to get VerifiedReport Raises: ValueError: If quote is invalid or verification fails @@ -112,21 +124,21 @@ async def get_collateral_and_verify( # Get collateral collateral = await get_collateral(url, raw_quote) - # Get current time + # Verify quote (crypto only) now_secs = int(time.time()) - - # Verify quote return verify(raw_quote, collateral, now_secs) __all__ = [ "QuoteCollateralV3", + "QuoteVerificationResult", "VerifiedReport", "QuoteHeader", "TdReport10", "TdReport15", "SgxEnclaveReport", "PckExtension", + "SimplePolicy", "AttestationKeyType", "TeeType", "Quote", diff --git a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi index 2171d7e..2fa5590 100644 --- a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi +++ b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi @@ -313,6 +313,99 @@ class PyPckExtension: ... +class PySimplePolicy: + """Verification policy with builder pattern. + + Use ``SimplePolicy.strict(now_secs)`` to create a strict policy (only UpToDate), + then chain builder methods to relax constraints. + + Example:: + + policy = SimplePolicy.strict(now_secs) \\ + .allow_status("SWHardeningNeeded") \\ + .accept_advisory("INTEL-SA-00334") \\ + .collateral_grace_period(90 * 24 * 3600) + """ + + @staticmethod + def strict(now_secs: int) -> "PySimplePolicy": + """Create a strict policy: only UpToDate, no grace, no advisory tolerance.""" + ... + + def allow_status(self, status: str) -> "PySimplePolicy": + """Allow an additional TCB status (e.g. "SWHardeningNeeded").""" + ... + + def accept_advisory(self, advisory_id: str) -> "PySimplePolicy": + """Accept a specific advisory ID (e.g. "INTEL-SA-00334").""" + ... + + def collateral_grace_period(self, secs: int) -> "PySimplePolicy": + """Set collateral grace period in seconds.""" + ... + + def platform_grace_period(self, secs: int) -> "PySimplePolicy": + """Set platform grace period in seconds.""" + ... + + def min_tcb_eval_data_number(self, min: int) -> "PySimplePolicy": + """Set minimum TCB evaluation data number.""" + ... + + def allow_dynamic_platform(self, allow: bool) -> "PySimplePolicy": + """Set whether dynamic platforms are allowed.""" + ... + + def allow_cached_keys(self, allow: bool) -> "PySimplePolicy": + """Set whether cached keys are allowed.""" + ... + + def allow_smt(self, allow: bool) -> "PySimplePolicy": + """Set whether SMT (hyperthreading) is allowed.""" + ... + + def accepted_sgx_types(self, types: List[int]) -> "PySimplePolicy": + """Set accepted SGX types (e.g. [0, 1, 2]).""" + ... + + +class PyQuoteVerificationResult: + """Intermediate result from crypto verification (phase 1). + + Use ``validate(policy)`` to apply a policy and get a ``PyVerifiedReport``. + Use ``into_report_unchecked()`` to skip policy validation (dangerous). + + The result is consumed on validate/into_report_unchecked — calling twice raises ValueError. + """ + + def validate(self, policy: PySimplePolicy) -> PyVerifiedReport: + """Validate against a policy, returning a VerifiedReport. Consumes the result. + + Args: + policy: Verification policy + + Returns: + PyVerifiedReport containing verification status + + Raises: + ValueError: If result already consumed or policy validation fails + """ + ... + + def into_report_unchecked(self) -> PyVerifiedReport: + """Get VerifiedReport without policy validation. Consumes the result. + + WARNING: Skips all policy checks. Use only when you handle validation externally. + + Returns: + PyVerifiedReport containing verification status + + Raises: + ValueError: If result already consumed + """ + ... + + class PyQuote: """ Represents a parsed SGX or TDX quote. @@ -420,13 +513,13 @@ class PyQuote: def py_verify( raw_quote: bytes, collateral: PyQuoteCollateralV3, now_secs: int -) -> PyVerifiedReport: +) -> PyQuoteVerificationResult: """ - Verify an SGX or TDX quote with the provided collateral data. + Verify an SGX or TDX quote (crypto only, phase 1). - This function performs cryptographic verification of the quote against - the provided collateral information, checking certificates, signatures, - and revocation status. + Performs cryptographic verification of the quote against the provided + collateral. Returns a QuoteVerificationResult that must be validated + with a policy via .validate(policy) to get a VerifiedReport. Args: raw_quote: Raw quote data as bytes (SGX or TDX format) @@ -434,11 +527,10 @@ def py_verify( now_secs: Current timestamp in seconds since Unix epoch for time-based checks Returns: - PyVerifiedReport containing verification status and advisory information + PyQuoteVerificationResult: use .validate(policy) to get VerifiedReport Raises: - ValueError: If verification fails due to invalid data, expired certificates, - revoked keys, or other verification errors + ValueError: If cryptographic verification fails """ ... @@ -446,10 +538,10 @@ def py_verify_with_root_ca( raw_quote: bytes, collateral: PyQuoteCollateralV3, root_ca_der: bytes, - now_secs: int -) -> PyVerifiedReport: + now_secs: int, +) -> PyQuoteVerificationResult: """ - Verify an SGX or TDX quote with the provided collateral data and custom root CA. + Verify an SGX or TDX quote with custom root CA (crypto only, phase 1). Args: raw_quote: Raw quote data as bytes (SGX or TDX format) @@ -458,10 +550,10 @@ def py_verify_with_root_ca( now_secs: Current timestamp in seconds since Unix epoch for time-based checks Returns: - PyVerifiedReport containing verification status and advisory information + PyQuoteVerificationResult: use .validate(policy) to get VerifiedReport Raises: - ValueError: If verification fails + ValueError: If cryptographic verification fails """ ... diff --git a/python-bindings/tests/test_python_bindings.py b/python-bindings/tests/test_python_bindings.py index 07f0973..7eb8d19 100644 --- a/python-bindings/tests/test_python_bindings.py +++ b/python-bindings/tests/test_python_bindings.py @@ -107,9 +107,13 @@ def test_verify_with_sample_data(self): collateral = dcap_qvl.QuoteCollateralV3.from_json(json.dumps(collateral_json)) - # Note: We use a timestamp that might make the test pass - # In a real scenario, you'd use the current time or a known good time - result = dcap_qvl.verify(quote_data, collateral, 1234567890) + # Phase 1: crypto verification + qvr = dcap_qvl.verify(quote_data, collateral, 1234567890) + assert isinstance(qvr, dcap_qvl.QuoteVerificationResult) + + # Phase 2: policy validation + policy = dcap_qvl.SimplePolicy.strict(1234567890) + result = qvr.validate(policy) assert isinstance(result, dcap_qvl.VerifiedReport) assert isinstance(result.status, str) diff --git a/src/ffi.rs b/src/ffi.rs index e9d8087..b0ea81c 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -8,6 +8,7 @@ use serde::Serialize; use crate::intel; use crate::quote::{EnclaveReport, Header, Quote, Report, TDReport10, TDReport15}; +use crate::tcb_info::TcbStatusWithAdvisory; use crate::verify::{self, VerifiedReport}; use crate::QuoteCollateralV3; @@ -196,19 +197,25 @@ struct FfiVerifiedReport { report: FfiReport, #[serde(with = "serde_bytes")] ppid: Vec, - qe_status: crate::tcb_info::TcbStatusWithAdvisory, - platform_status: crate::tcb_info::TcbStatusWithAdvisory, + qe_status: TcbStatusWithAdvisory, + platform_status: TcbStatusWithAdvisory, } impl FfiVerifiedReport { fn from(vr: VerifiedReport) -> Self { + let qe_status = + TcbStatusWithAdvisory::new(vr.qe_tcb_level.tcb_status, vr.qe_tcb_level.advisory_ids); + let platform_status = TcbStatusWithAdvisory::new( + vr.platform_tcb_level.tcb_status, + vr.platform_tcb_level.advisory_ids, + ); Self { status: vr.status, advisory_ids: vr.advisory_ids, report: FfiReport::from_report(&vr.report), ppid: vr.ppid, - qe_status: vr.qe_status, - platform_status: vr.platform_status, + qe_status, + platform_status, } } } @@ -379,8 +386,9 @@ pub unsafe extern "C" fn dcap_verify_cb( } }; - let report = match verify::verify(quote_slice, &collateral, now_secs) { - Ok(r) => r, + let verifier = verify::QuoteVerifier::new_prod(verify::default_crypto::backend()); + let report = match verifier.verify(quote_slice, collateral, now_secs) { + Ok(qvr) => qvr.into_report_unchecked(), Err(e) => return emit_error(format_error(&e), cb, user_data), }; @@ -421,9 +429,8 @@ pub unsafe extern "C" fn dcap_verify_with_root_ca_cb( }; let verifier = verify::QuoteVerifier::new(root_ca.to_vec(), verify::default_crypto::backend()); - - let report = match verifier.verify(quote_slice, &collateral, now_secs) { - Ok(r) => r, + let report = match verifier.verify(quote_slice, collateral, now_secs) { + Ok(qvr) => qvr.into_report_unchecked(), Err(e) => return emit_error(format_error(&e), cb, user_data), }; diff --git a/src/lib.rs b/src/lib.rs index 672d3f9..983ddb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,20 +94,20 @@ mod utils; pub use constants::{CpuSvn, Fmspc, MrEnclave, MrSigner, Svn}; // Re-export commonly used types -pub use qe_identity::{QeIdentity, QeTcb, QeTcbLevel}; -pub use tcb_info::{Tcb, TcbComponents, TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory}; pub use policy::{ - PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, SimplePolicy, SupplementalData, - TcbVerdict, + PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, SimplePolicy, SimplePolicyConfig, + SupplementalData, TcbVerdict, }; +pub use qe_identity::{QeIdentity, QeTcb, QeTcbLevel}; +pub use tcb_info::{Tcb, TcbComponents, TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory}; pub use verify::QuoteVerificationResult; #[cfg(feature = "rego")] pub use policy::{RegoPolicy, RegoPolicySet}; +pub mod policy; pub mod quote; pub mod verify; -pub mod policy; #[cfg(feature = "python")] pub mod python; diff --git a/src/policy/mod.rs b/src/policy/mod.rs index e385f36..6b1ae14 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -3,14 +3,14 @@ use anyhow::Result; use { crate::constants::*, crate::qe_identity::QeTcbLevel, - crate::quote::EnclaveReport, + crate::quote::{EnclaveReport, Report}, crate::tcb_info::{TcbLevel, TcbStatus}, alloc::string::String, alloc::vec::Vec, }; mod simple; -pub use simple::SimplePolicy; +pub use simple::{SimplePolicy, SimplePolicyConfig}; #[cfg(feature = "rego")] pub(crate) mod rego; @@ -76,6 +76,9 @@ impl From> for PckCertFlag { /// - [`tcb`](Self::tcb): Merged TCB verdict /// - [`platform`](Self::platform): Platform-level details from PCK certificate and TCB matching /// - [`qe`](Self::qe): QE (Quoting Enclave) verification results +/// +/// Also includes the collateral time window (8 sources: TCBInfo, QEIdentity, 2 CRLs, +/// 4 certificate chains) and the quote report body. pub struct SupplementalData { /// TEE type: `0x00000000` for SGX, `0x00000081` for TDX. pub tee_type: u32, @@ -85,6 +88,14 @@ pub struct SupplementalData { pub platform: PlatformInfo, /// QE verification details. pub qe: QeInfo, + /// `min(issueDate / thisUpdate / notBefore)` across all 8 collateral sources. + pub earliest_issue_date: u64, + /// `max(issueDate / thisUpdate / notBefore)` across all 8 collateral sources. + pub latest_issue_date: u64, + /// `min(nextUpdate / notAfter)` across all 8 collateral sources (the "weakest link"). + pub earliest_expiration_date: u64, + /// Quote report body (SGX enclave report, TDX TD10/TD15). + pub report: Report, } /// Merged TCB verdict from platform and QE status convergence. @@ -98,10 +109,6 @@ pub struct TcbVerdict { pub advisory_ids: Vec, /// Lower of TCBInfo and QEIdentity `tcbEvaluationDataNumber` values. pub eval_data_number: u32, - /// Earliest expiration from TCBInfo nextUpdate, QEIdentity nextUpdate, - /// and CRL nextUpdate (4 sources). Does **not** include certificate chain - /// notAfter — use full time window via Rego for that. - pub earliest_expiration: u64, } /// Platform-level verification results. diff --git a/src/policy/rego.rs b/src/policy/rego.rs index 8b2f913..073c62d 100644 --- a/src/policy/rego.rs +++ b/src/policy/rego.rs @@ -30,41 +30,21 @@ fn tcb_status_to_rego_array(status: TcbStatus) -> serde_json::Value { } } -/// Full collateral time window, aggregated from 8 sources (matching Intel QVL): -/// TcbInfo, QeIdentity, Root CA CRL, PCK CRL, and 4 certificate chains. -/// -/// `SimplePolicy` only needs `earliest_expiration_date` (computed separately -/// from 4 lightweight sources without certificate chain parsing). -pub(crate) struct CollateralTimeWindow { - /// `min(issueDate / thisUpdate / notBefore)` across all 8 sources. - /// Rego uses this as `collateral_earliest_issue_date` in measurements. - pub earliest_issue_date: u64, - /// `max(issueDate / thisUpdate / notBefore)` across all 8 sources. - /// Rego uses this as `collateral_latest_issue_date` in measurements. - pub latest_issue_date: u64, - /// `min(nextUpdate / notAfter)` across all 8 sources (the "weakest link"). - /// Determines when the overall collateral expires. Rego uses this as - /// `collateral_earliest_expiration_date`; also reused by `build_supplemental()` - /// to avoid redundant parsing. - pub earliest_expiration_date: u64, -} - /// Build common platform fields into a Rego measurement JSON map. fn insert_platform_fields( m: &mut serde_json::Map, data: &SupplementalData, - tw: &CollateralTimeWindow, ) { - // Time fields as RFC3339 strings - let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); + // Time fields as RFC3339 strings (from SupplementalData's collateral time window) + let earliest_issue = unix_to_rfc3339(data.earliest_issue_date); if !earliest_issue.is_empty() { m.insert("earliest_issue_date".into(), json!(earliest_issue)); } - let latest_issue = unix_to_rfc3339(tw.latest_issue_date); + let latest_issue = unix_to_rfc3339(data.latest_issue_date); if !latest_issue.is_empty() { m.insert("latest_issue_date".into(), json!(latest_issue)); } - let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); + let earliest_exp = unix_to_rfc3339(data.earliest_expiration_date); if !earliest_exp.is_empty() { m.insert("earliest_expiration_date".into(), json!(earliest_exp)); } @@ -74,7 +54,10 @@ fn insert_platform_fields( } m.insert("pck_crl_num".into(), json!(data.platform.pck_crl_num)); - m.insert("root_ca_crl_num".into(), json!(data.platform.root_ca_crl_num)); + m.insert( + "root_ca_crl_num".into(), + json!(data.platform.root_ca_crl_num), + ); m.insert("tcb_eval_num".into(), json!(data.tcb.eval_data_number)); m.insert("sgx_type".into(), json!(data.platform.pck.sgx_type)); @@ -101,7 +84,10 @@ fn insert_platform_fields( m.insert("platform_provider_id".into(), json!(provider_id)); } - m.insert("fmspc".into(), json!(hex::encode_upper(data.platform.pck.fmspc))); + m.insert( + "fmspc".into(), + json!(hex::encode_upper(data.platform.pck.fmspc)), + ); m.insert( "root_key_id".into(), json!(hex::encode_upper(data.platform.root_key_id)), @@ -109,16 +95,13 @@ fn insert_platform_fields( } /// Build merged Rego measurement (single-measurement path). -pub(crate) fn build_merged_measurement( - data: &SupplementalData, - tw: &CollateralTimeWindow, -) -> serde_json::Value { +fn build_merged_measurement(data: &SupplementalData) -> serde_json::Value { let mut m = serde_json::Map::new(); m.insert( "tcb_status".into(), tcb_status_to_rego_array(data.tcb.status), ); - insert_platform_fields(&mut m, data, tw); + insert_platform_fields(&mut m, data); if !data.tcb.advisory_ids.is_empty() { m.insert("advisory_ids".into(), json!(data.tcb.advisory_ids)); } @@ -126,16 +109,13 @@ pub(crate) fn build_merged_measurement( } /// Build platform TCB measurement using **unmerged** platform status. -pub(crate) fn build_platform_measurement( - data: &SupplementalData, - tw: &CollateralTimeWindow, -) -> serde_json::Value { +fn build_platform_measurement(data: &SupplementalData) -> serde_json::Value { let mut m = serde_json::Map::new(); m.insert( "tcb_status".into(), tcb_status_to_rego_array(data.platform.tcb_level.tcb_status), ); - insert_platform_fields(&mut m, data, tw); + insert_platform_fields(&mut m, data); if !data.platform.tcb_level.advisory_ids.is_empty() { m.insert( "advisory_ids".into(), @@ -146,10 +126,7 @@ pub(crate) fn build_platform_measurement( } /// Build QE Identity measurement for Rego appraisal (TDX). -pub(crate) fn build_qe_measurement( - data: &SupplementalData, - tw: &CollateralTimeWindow, -) -> serde_json::Value { +fn build_qe_measurement(data: &SupplementalData) -> serde_json::Value { let mut m = serde_json::Map::new(); m.insert( @@ -166,15 +143,15 @@ pub(crate) fn build_qe_measurement( m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); } - let earliest_issue = unix_to_rfc3339(tw.earliest_issue_date); + let earliest_issue = unix_to_rfc3339(data.earliest_issue_date); if !earliest_issue.is_empty() { m.insert("earliest_issue_date".into(), json!(earliest_issue)); } - let latest_issue = unix_to_rfc3339(tw.latest_issue_date); + let latest_issue = unix_to_rfc3339(data.latest_issue_date); if !latest_issue.is_empty() { m.insert("latest_issue_date".into(), json!(latest_issue)); } - let earliest_exp = unix_to_rfc3339(tw.earliest_expiration_date); + let earliest_exp = unix_to_rfc3339(data.earliest_expiration_date); if !earliest_exp.is_empty() { m.insert("earliest_expiration_date".into(), json!(earliest_exp)); } @@ -233,12 +210,13 @@ pub(crate) fn sgx_enclave_measurement(report: &EnclaveReport) -> serde_json::Val ); } if let Some(config_id) = report.reserved3.get(32..96) { - m.insert( - "sgx_configid".into(), - json!(hex::encode_upper(config_id)), - ); + m.insert("sgx_configid".into(), json!(hex::encode_upper(config_id))); } - if let Some(config_svn_bytes) = report.reserved4.get(0..2).and_then(|s| <[u8; 2]>::try_from(s).ok()) { + if let Some(config_svn_bytes) = report + .reserved4 + .get(0..2) + .and_then(|s| <[u8; 2]>::try_from(s).ok()) + { let config_svn = u16::from_le_bytes(config_svn_bytes); m.insert("sgx_configsvn".into(), json!(config_svn)); } @@ -260,14 +238,8 @@ fn td10_measurement(report: &TDReport10) -> serde_json::Value { "tdx_attributes".into(), json!(hex::encode_upper(report.td_attributes)), ); - m.insert( - "tdx_xfam".into(), - json!(hex::encode_upper(report.xfam)), - ); - m.insert( - "tdx_mrtd".into(), - json!(hex::encode_upper(report.mr_td)), - ); + m.insert("tdx_xfam".into(), json!(hex::encode_upper(report.xfam))); + m.insert("tdx_mrtd".into(), json!(hex::encode_upper(report.mr_td))); m.insert( "tdx_mrconfigid".into(), json!(hex::encode_upper(report.mr_config_id)), @@ -280,22 +252,10 @@ fn td10_measurement(report: &TDReport10) -> serde_json::Value { "tdx_mrownerconfig".into(), json!(hex::encode_upper(report.mr_owner_config)), ); - m.insert( - "tdx_rtmr0".into(), - json!(hex::encode_upper(report.rt_mr0)), - ); - m.insert( - "tdx_rtmr1".into(), - json!(hex::encode_upper(report.rt_mr1)), - ); - m.insert( - "tdx_rtmr2".into(), - json!(hex::encode_upper(report.rt_mr2)), - ); - m.insert( - "tdx_rtmr3".into(), - json!(hex::encode_upper(report.rt_mr3)), - ); + m.insert("tdx_rtmr0".into(), json!(hex::encode_upper(report.rt_mr0))); + m.insert("tdx_rtmr1".into(), json!(hex::encode_upper(report.rt_mr1))); + m.insert("tdx_rtmr2".into(), json!(hex::encode_upper(report.rt_mr2))); + m.insert("tdx_rtmr3".into(), json!(hex::encode_upper(report.rt_mr3))); m.insert( "tdx_reportdata".into(), json!(hex::encode_upper(report.report_data)), @@ -403,12 +363,6 @@ impl RegoPolicySet { Ok(Self { engine, policies }) } - - /// Evaluate the Rego engine with the given qvl_result entries. - pub(crate) fn eval_rego(&self, qvl_result: Vec) -> Result<()> { - let policy_refs: Vec<&serde_json::Value> = self.policies.iter().collect(); - eval_rego_engine(&self.engine, &policy_refs, qvl_result) - } } /// Shared Rego evaluation logic used by both `RegoPolicy` and `RegoPolicySet`. @@ -524,14 +478,9 @@ impl RegoPolicy { } } -impl RegoPolicy { - /// Evaluate this single-measurement Rego policy against supplemental data + time window. - pub(crate) fn eval( - &self, - data: &SupplementalData, - tw: &CollateralTimeWindow, - ) -> Result<()> { - let measurement = build_merged_measurement(data, tw); +impl Policy for RegoPolicy { + fn validate(&self, data: &SupplementalData) -> Result<()> { + let measurement = build_merged_measurement(data); let qvl_result = vec![json!({ "environment": { "class_id": &self.class_id }, "measurement": measurement, @@ -540,6 +489,58 @@ impl RegoPolicy { } } +impl Policy for RegoPolicySet { + fn validate(&self, data: &SupplementalData) -> Result<()> { + let qvl_result = to_rego_qvl_result(data); + let policy_refs: Vec<&serde_json::Value> = self.policies.iter().collect(); + eval_rego_engine(&self.engine, &policy_refs, qvl_result) + } +} + +/// Generate Intel-format `qvl_result` array for Rego appraisal from [`SupplementalData`]. +/// +/// SGX quotes produce 2 entries (platform + enclave). +/// TDX quotes produce 3 entries (platform + QE identity + TD). +fn to_rego_qvl_result(data: &SupplementalData) -> Vec { + use crate::quote::Report; + + let mut result = Vec::new(); + + // 1. Platform TCB measurement + let platform_cid = platform_class_id(&data.report, data.tee_type); + result.push(json!({ + "environment": { "class_id": platform_cid }, + "measurement": build_platform_measurement(data), + })); + + // 2. QE Identity measurement (TDX only) + if matches!(data.report, Report::TD10(_) | Report::TD15(_)) { + result.push(json!({ + "environment": { "class_id": "3769258c-75e6-4bc7-8d72-d2b0e224cad2" }, + "measurement": build_qe_measurement(data), + })); + } + + // 3. Tenant measurement (enclave or TD report) + let tenant_cid = tenant_class_id(&data.report); + let mut tenant_m = tenant_measurement(&data.report); + // For SGX enclave, add sgx_ce_attributes from the QE report + if let Report::SgxEnclave(_) = &data.report { + if let Some(obj) = tenant_m.as_object_mut() { + obj.insert( + "sgx_ce_attributes".into(), + json!(hex::encode_upper(data.qe.report.attributes)), + ); + } + } + result.push(json!({ + "environment": { "class_id": tenant_cid }, + "measurement": tenant_m, + })); + + result +} + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { @@ -559,7 +560,6 @@ mod tests { status: tcb_status, advisory_ids: vec![], eval_data_number: 17, - earliest_expiration: 1_703_000_000, }, platform: PlatformInfo { tcb_level: TcbLevel { @@ -613,22 +613,32 @@ mod tests { }, tcb_eval_data_number: 17, }, + report: crate::quote::Report::SgxEnclave(crate::quote::EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0, + reserved1: [0u8; 28], + attributes: [0u8; 16], + mr_enclave: [0u8; 32], + reserved2: [0u8; 32], + mr_signer: [0u8; 32], + reserved3: [0u8; 96], + isv_prod_id: 0, + isv_svn: 0, + reserved4: [0u8; 60], + report_data: [0u8; 64], + }), + earliest_issue_date: 1_690_000_000, + latest_issue_date: 1_690_100_000, + earliest_expiration_date: 1_703_000_000, } } - /// Create a test time window with future dates (Rego uses real wall clock). - fn make_test_time_window() -> CollateralTimeWindow { - CollateralTimeWindow { - earliest_issue_date: 1_900_000_000, - latest_issue_date: 1_900_100_000, - earliest_expiration_date: 2_000_000_000, - } - } - - /// Create test supplemental data with future expiration for Rego. + /// Create test supplemental data with future dates for Rego (uses real wall clock). fn make_rego_supplemental(status: TcbStatus) -> SupplementalData { let mut data = make_test_supplemental(status); - data.tcb.earliest_expiration = 2_000_000_000; + data.earliest_issue_date = 1_900_000_000; + data.latest_issue_date = 1_900_100_000; + data.earliest_expiration_date = 2_000_000_000; data } @@ -647,12 +657,10 @@ mod tests { #[test] fn rego_strict_accepts_up_to_date() { let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); - let json = policy_json( - r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, - ); + let json = + policy_json(r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); + let result = policy.validate(&data); assert!( result.is_ok(), "expected Ok, got: {:?}", @@ -663,12 +671,10 @@ mod tests { #[test] fn rego_strict_rejects_out_of_date() { let data = make_rego_supplemental(OutOfDate); - let tw = make_test_time_window(); - let json = policy_json( - r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, - ); + let json = + policy_json(r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#); let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.eval(&data, &tw).unwrap_err().to_string(); + let err = policy.validate(&data).unwrap_err().to_string(); assert!( err.contains("appraisal failed"), "expected appraisal failure, got: {err}" @@ -678,12 +684,11 @@ mod tests { #[test] fn rego_permissive_accepts_out_of_date() { let data = make_rego_supplemental(OutOfDate); - let tw = make_test_time_window(); let json = policy_json( r#"{"accepted_tcb_status": ["UpToDate", "OutOfDate"], "collateral_grace_period": 0}"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); + let result = policy.validate(&data); assert!( result.is_ok(), "expected Ok, got: {:?}", @@ -695,7 +700,6 @@ mod tests { fn rego_rejects_advisory() { let mut data = make_rego_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00334".into()]; - let tw = make_test_time_window(); let json = policy_json( r#"{ "accepted_tcb_status": ["UpToDate"], @@ -704,7 +708,7 @@ mod tests { }"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.eval(&data, &tw).unwrap_err().to_string(); + let err = policy.validate(&data).unwrap_err().to_string(); assert!( err.contains("appraisal failed"), "expected advisory rejection, got: {err}" @@ -715,7 +719,6 @@ mod tests { fn rego_platform_grace_period_accepts() { let mut data = make_rego_supplemental(OutOfDate); data.platform.tcb_date_tag = 1_690_000_000; - let tw = make_test_time_window(); let json = policy_json( r#"{ "accepted_tcb_status": ["UpToDate", "OutOfDate"], @@ -724,7 +727,7 @@ mod tests { }"#, ); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); + let result = policy.validate(&data); assert!( result.is_ok(), "expected Ok, got: {:?}", @@ -734,17 +737,15 @@ mod tests { #[test] fn rego_expiration_check_rejects_expired_collateral() { - let data = make_rego_supplemental(UpToDate); - let tw = CollateralTimeWindow { - earliest_issue_date: 1_700_000_000, - latest_issue_date: 1_700_100_000, - earliest_expiration_date: 1_703_000_000, - }; - let json = policy_json( - r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#, - ); + let mut data = make_rego_supplemental(UpToDate); + // Override with past dates — collateral expired + data.earliest_issue_date = 1_700_000_000; + data.latest_issue_date = 1_700_100_000; + data.earliest_expiration_date = 1_703_000_000; + let json = + policy_json(r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#); let policy = RegoPolicy::new(&json).unwrap(); - let err = policy.eval(&data, &tw).unwrap_err().to_string(); + let err = policy.validate(&data).unwrap_err().to_string(); assert!( err.contains("appraisal failed"), "expected expiration failure, got: {err}" @@ -753,15 +754,14 @@ mod tests { #[test] fn rego_no_collateral_grace_skips_expiration_check() { - let data = make_rego_supplemental(UpToDate); - let tw = CollateralTimeWindow { - earliest_issue_date: 1_700_000_000, - latest_issue_date: 1_700_100_000, - earliest_expiration_date: 1_703_000_000, - }; + let mut data = make_rego_supplemental(UpToDate); + // Override with past dates — but no grace period in policy means no check + data.earliest_issue_date = 1_700_000_000; + data.latest_issue_date = 1_700_100_000; + data.earliest_expiration_date = 1_703_000_000; let json = policy_json(r#"{"accepted_tcb_status": ["UpToDate"]}"#); let policy = RegoPolicy::new(&json).unwrap(); - let result = policy.eval(&data, &tw); + let result = policy.validate(&data); assert!( result.is_ok(), "expected Ok (no expiration check), got: {:?}", @@ -778,8 +778,7 @@ mod tests { #[test] fn rego_to_measurement_tcb_status_mapping() { let data = make_test_supplemental(ConfigurationAndSWHardeningNeeded); - let tw = make_test_time_window(); - let m = build_merged_measurement(&data, &tw); + let m = build_merged_measurement(&data); let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); assert_eq!(statuses.len(), 3); assert_eq!(statuses[0], "UpToDate"); @@ -791,8 +790,7 @@ mod tests { fn rego_to_measurement_omits_undefined_flags() { let data = make_test_supplemental(UpToDate); assert_eq!(data.platform.pck.dynamic_platform, PckCertFlag::Undefined); - let tw = make_test_time_window(); - let m = build_merged_measurement(&data, &tw); + let m = build_merged_measurement(&data); assert!(m.get("is_dynamic_platform").is_none()); assert!(m.get("cached_keys").is_none()); assert!(m.get("smt_enabled").is_none()); @@ -804,8 +802,7 @@ mod tests { data.platform.pck.dynamic_platform = PckCertFlag::True; data.platform.pck.cached_keys = PckCertFlag::False; data.platform.pck.smt_enabled = PckCertFlag::True; - let tw = make_test_time_window(); - let m = build_merged_measurement(&data, &tw); + let m = build_merged_measurement(&data); assert_eq!(m.get("is_dynamic_platform").unwrap(), true); assert_eq!(m.get("cached_keys").unwrap(), false); assert_eq!(m.get("smt_enabled").unwrap(), true); @@ -816,8 +813,7 @@ mod tests { let mut data = make_test_supplemental(UpToDate); data.platform.tcb_level.tcb_status = OutOfDate; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00001".into()]; - let tw = make_test_time_window(); - let m = build_platform_measurement(&data, &tw); + let m = build_platform_measurement(&data); let statuses = m.get("tcb_status").unwrap().as_array().unwrap(); assert!(statuses.contains(&serde_json::json!("OutOfDate"))); let advisories = m.get("advisory_ids").unwrap().as_array().unwrap(); @@ -827,8 +823,7 @@ mod tests { #[test] fn rego_qe_measurement_fields() { let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); - let m = build_qe_measurement(&data, &tw); + let m = build_qe_measurement(&data); assert!(m.get("tcb_status").is_some()); assert_eq!(m.get("tcb_eval_num").unwrap(), 17); assert!(m.get("root_key_id").is_some()); @@ -876,7 +871,6 @@ mod tests { #[test] fn rego_policy_set_sgx_platform_accepts() { let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); let platform_json = format!( r#"{{ "environment": {{ "class_id": "{SGX_PLATFORM_CLASS_ID}" }}, @@ -884,27 +878,17 @@ mod tests { }}"# ); let policies = RegoPolicySet::new(&[&platform_json]).unwrap(); - let qvl_result = vec![serde_json::json!({ - "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": build_platform_measurement(&data, &tw), - })]; + let result = policies.validate(&data); assert!( - policies.eval_rego(qvl_result).is_ok(), + result.is_ok(), "expected Ok, got: {:?}", - { - let qvl_result2 = vec![serde_json::json!({ - "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": build_platform_measurement(&data, &tw), - })]; - policies.eval_rego(qvl_result2).unwrap_err() - } + result.unwrap_err() ); } #[test] fn rego_policy_set_class_id_mismatch_fails() { let data = make_rego_supplemental(UpToDate); - let tw = make_test_time_window(); let tdx_class_id = "9eec018b-7481-4b1c-8e1a-9f7c0c8c777f"; let policy_json = format!( r#"{{ @@ -913,11 +897,7 @@ mod tests { }}"# ); let policies = RegoPolicySet::new(&[&policy_json]).unwrap(); - let qvl_result = vec![serde_json::json!({ - "environment": { "class_id": SGX_PLATFORM_CLASS_ID }, - "measurement": build_platform_measurement(&data, &tw), - })]; - let err = policies.eval_rego(qvl_result).unwrap_err().to_string(); + let err = policies.validate(&data).unwrap_err().to_string(); assert!( err.contains("appraisal failed"), "expected appraisal failure on class_id mismatch, got: {err}" diff --git a/src/policy/simple.rs b/src/policy/simple.rs index c63b4aa..73264bd 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -1,6 +1,7 @@ use core::time::Duration; use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; use { super::{PckCertFlag, Policy, SupplementalData}, @@ -191,14 +192,13 @@ impl Policy for SimplePolicy { // 3. Collateral expiration: earliest_expiration + grace >= now if data - .tcb - .earliest_expiration + .earliest_expiration_date .saturating_add(self.collateral_grace_period) < self.now { bail!( "Collateral expired: earliest_expiration {} + grace {} < now {}", - data.tcb.earliest_expiration, + data.earliest_expiration_date, self.collateral_grace_period, self.now ); @@ -287,6 +287,79 @@ impl Policy for SimplePolicy { } } +/// JSON-serializable configuration for [`SimplePolicy`]. +/// +/// All fields default to the strict values (zero / empty / false). +/// Pass as JSON from FFI (Go, Python) to configure verification policy. +/// +/// ```json +/// { +/// "allowed_statuses": ["UpToDate", "SWHardeningNeeded"], +/// "accepted_advisories": ["INTEL-SA-00334"], +/// "collateral_grace_period_secs": 2592000, +/// "allow_smt": true +/// } +/// ``` +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SimplePolicyConfig { + #[serde(default)] + pub allowed_statuses: Vec, + #[serde(default)] + pub accepted_advisories: Vec, + #[serde(default)] + pub collateral_grace_period_secs: u64, + #[serde(default)] + pub platform_grace_period_secs: u64, + #[serde(default)] + pub min_tcb_eval_data_number: u32, + #[serde(default)] + pub allow_dynamic_platform: bool, + #[serde(default)] + pub allow_cached_keys: bool, + #[serde(default)] + pub allow_smt: bool, + #[serde(default)] + pub accepted_sgx_types: Option>, +} + +impl SimplePolicyConfig { + /// Build a [`SimplePolicy`] from this config + current timestamp. + /// + /// Default config (all fields zero/empty) produces `SimplePolicy::strict(now)`. + pub fn into_policy(self, now_secs: u64) -> SimplePolicy { + let mut policy = if self.allowed_statuses.is_empty() { + SimplePolicy::strict(now_secs) + } else { + let mut p = SimplePolicy::new_with_statuses(now_secs, 0); + for status in self.allowed_statuses { + p = p.allow_status(status); + } + p + }; + for id in self.accepted_advisories { + policy = policy.accept_advisory(id); + } + if self.collateral_grace_period_secs > 0 { + policy = policy + .collateral_grace_period(Duration::from_secs(self.collateral_grace_period_secs)); + } + if self.platform_grace_period_secs > 0 { + policy = + policy.platform_grace_period(Duration::from_secs(self.platform_grace_period_secs)); + } + if self.min_tcb_eval_data_number > 0 { + policy = policy.min_tcb_eval_data_number(self.min_tcb_eval_data_number); + } + policy = policy.allow_dynamic_platform(self.allow_dynamic_platform); + policy = policy.allow_cached_keys(self.allow_cached_keys); + policy = policy.allow_smt(self.allow_smt); + if let Some(types) = self.accepted_sgx_types { + policy = policy.accepted_sgx_types(&types); + } + policy + } +} + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { @@ -304,7 +377,6 @@ mod tests { status: tcb_status, advisory_ids: vec![], eval_data_number: 17, - earliest_expiration: 1_703_000_000, // ~2023-12-19 }, platform: PlatformInfo { tcb_level: TcbLevel { @@ -358,6 +430,23 @@ mod tests { }, tcb_eval_data_number: 17, }, + report: crate::quote::Report::SgxEnclave(crate::quote::EnclaveReport { + cpu_svn: [0u8; 16], + misc_select: 0, + reserved1: [0u8; 28], + attributes: [0u8; 16], + mr_enclave: [0u8; 32], + reserved2: [0u8; 32], + mr_signer: [0u8; 32], + reserved3: [0u8; 96], + isv_prod_id: 0, + isv_svn: 0, + reserved4: [0u8; 60], + report_data: [0u8; 64], + }), + earliest_issue_date: 1_690_000_000, + latest_issue_date: 1_690_100_000, + earliest_expiration_date: 1_703_000_000, // ~2023-12-19 } } diff --git a/src/python.rs b/src/python.rs index d38088d..692a9cf 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,3 +1,5 @@ +use core::time::Duration; + use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyBytes; @@ -7,7 +9,9 @@ use serde_json; use crate::{ collateral::get_collateral_for_fmspc, intel, + policy::SimplePolicy, quote::{EnclaveReport, Header, Quote, Report, TDReport10, TDReport15}, + tcb_info::TcbStatus, verify::{QuoteVerifier, VerifiedReport}, QuoteCollateralV3, }; @@ -427,6 +431,110 @@ impl PyPckExtension { } } +/// Verification policy with builder pattern. +/// +/// Mirrors the Rust `SimplePolicy` API. Use `SimplePolicy.strict(now_secs)` to create +/// a strict policy (only UpToDate), then chain builder methods to relax constraints. +#[pyclass] +#[derive(Clone)] +pub struct PySimplePolicy { + inner: SimplePolicy, +} + +fn parse_tcb_status(s: &str) -> PyResult { + match s { + "UpToDate" => Ok(TcbStatus::UpToDate), + "SWHardeningNeeded" => Ok(TcbStatus::SWHardeningNeeded), + "ConfigurationNeeded" => Ok(TcbStatus::ConfigurationNeeded), + "ConfigurationAndSWHardeningNeeded" => Ok(TcbStatus::ConfigurationAndSWHardeningNeeded), + "OutOfDate" => Ok(TcbStatus::OutOfDate), + "OutOfDateConfigurationNeeded" => Ok(TcbStatus::OutOfDateConfigurationNeeded), + "Revoked" => Ok(TcbStatus::Revoked), + _ => Err(PyValueError::new_err(format!("Unknown TCB status: {s}"))), + } +} + +#[pymethods] +impl PySimplePolicy { + /// Create a strict policy: only `UpToDate` status, no grace, no advisory tolerance. + #[staticmethod] + fn strict(now_secs: u64) -> Self { + Self { + inner: SimplePolicy::strict(now_secs), + } + } + + /// Allow an additional TCB status (e.g. "SWHardeningNeeded"). + fn allow_status(&self, status: &str) -> PyResult { + let s = parse_tcb_status(status)?; + Ok(Self { + inner: self.inner.clone().allow_status(s), + }) + } + + /// Accept a specific advisory ID (e.g. "INTEL-SA-00334"). + fn accept_advisory(&self, advisory_id: &str) -> Self { + Self { + inner: self.inner.clone().accept_advisory(advisory_id), + } + } + + /// Set collateral grace period in seconds. + fn collateral_grace_period(&self, secs: u64) -> Self { + Self { + inner: self + .inner + .clone() + .collateral_grace_period(Duration::from_secs(secs)), + } + } + + /// Set platform grace period in seconds. + fn platform_grace_period(&self, secs: u64) -> Self { + Self { + inner: self + .inner + .clone() + .platform_grace_period(Duration::from_secs(secs)), + } + } + + /// Set minimum TCB evaluation data number. + fn min_tcb_eval_data_number(&self, min: u32) -> Self { + Self { + inner: self.inner.clone().min_tcb_eval_data_number(min), + } + } + + /// Set whether dynamic platforms are allowed. + fn allow_dynamic_platform(&self, allow: bool) -> Self { + Self { + inner: self.inner.clone().allow_dynamic_platform(allow), + } + } + + /// Set whether cached keys are allowed. + fn allow_cached_keys(&self, allow: bool) -> Self { + Self { + inner: self.inner.clone().allow_cached_keys(allow), + } + } + + /// Set whether SMT (hyperthreading) is allowed. + fn allow_smt(&self, allow: bool) -> Self { + Self { + inner: self.inner.clone().allow_smt(allow), + } + } + + /// Set accepted SGX types (e.g. [0, 1, 2]). + fn accepted_sgx_types(&self, types: Vec) -> Self { + Self { + inner: self.inner.clone().accepted_sgx_types(&types), + } + } +} + #[pyclass] pub struct PyQuote { inner: Quote, @@ -537,20 +645,57 @@ impl PyQuote { } } +/// Result of cryptographic quote verification (phase 1). +/// +/// Use `validate(policy)` to apply a policy and get a `VerifiedReport`. +/// Use `into_report_unchecked()` to skip policy validation (dangerous). +#[pyclass] +pub struct PyQuoteVerificationResult { + inner: Option, +} + +#[pymethods] +impl PyQuoteVerificationResult { + /// Validate against a policy, returning a VerifiedReport. Consumes the result. + fn validate(&mut self, policy: &PySimplePolicy) -> PyResult { + let result = self + .inner + .take() + .ok_or_else(|| PyValueError::new_err("verification result already consumed"))?; + let report = result + .validate(&policy.inner) + .map_err(|e| PyValueError::new_err(format!("Policy validation failed: {e:?}")))?; + Ok(PyVerifiedReport { inner: report }) + } + + /// Get VerifiedReport without policy validation. Consumes the result. + /// + /// WARNING: Skips all policy checks. Use only when you handle validation externally. + fn into_report_unchecked(&mut self) -> PyResult { + let result = self + .inner + .take() + .ok_or_else(|| PyValueError::new_err("verification result already consumed"))?; + Ok(PyVerifiedReport { + inner: result.into_report_unchecked(), + }) + } +} + #[pyfunction] fn py_verify( raw_quote: &Bound<'_, PyBytes>, collateral: &PyQuoteCollateralV3, now_secs: u64, -) -> PyResult { +) -> PyResult { let quote_bytes = raw_quote.as_bytes(); let verifier = QuoteVerifier::new_prod(crate::verify::ring::backend()); - match verifier.verify(quote_bytes, collateral.inner.clone(), now_secs) { - Ok(supplemental) => Ok(PyVerifiedReport { - inner: supplemental.into_report_unchecked(), - }), - Err(e) => Err(PyValueError::new_err(format!("Verification failed: {e:?}"))), - } + let result = verifier + .verify(quote_bytes, collateral.inner.clone(), now_secs) + .map_err(|e| PyValueError::new_err(format!("Verification failed: {e:?}")))?; + Ok(PyQuoteVerificationResult { + inner: Some(result), + }) } #[pyfunction] @@ -559,17 +704,17 @@ fn py_verify_with_root_ca( collateral: &PyQuoteCollateralV3, root_ca_der: &Bound<'_, PyBytes>, now_secs: u64, -) -> PyResult { +) -> PyResult { let quote_bytes = raw_quote.as_bytes(); let root_ca = root_ca_der.as_bytes(); let verifier = QuoteVerifier::new(root_ca.to_vec(), crate::verify::ring::backend()); - match verifier.verify(quote_bytes, collateral.inner.clone(), now_secs) { - Ok(supplemental) => Ok(PyVerifiedReport { - inner: supplemental.into_report_unchecked(), - }), - Err(e) => Err(PyValueError::new_err(format!("Verification failed: {e:?}"))), - } + let result = verifier + .verify(quote_bytes, collateral.inner.clone(), now_secs) + .map_err(|e| PyValueError::new_err(format!("Verification failed: {e:?}")))?; + Ok(PyQuoteVerificationResult { + inner: Some(result), + }) } #[pyfunction] @@ -616,6 +761,8 @@ pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(py_verify, m)?)?; m.add_function(wrap_pyfunction!(py_verify_with_root_ca, m)?)?; diff --git a/src/verify.rs b/src/verify.rs index 17a2d53..51a634c 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -7,7 +7,9 @@ use scale::Decode; use { crate::constants::*, crate::intel, - crate::policy::{PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, SupplementalData, TcbVerdict}, + crate::policy::{ + PckCertFlag, PckIdentity, PlatformInfo, Policy, QeInfo, SupplementalData, TcbVerdict, + }, crate::qe_identity::{QeIdentity, QeTcbLevel}, crate::tcb_info::{TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory}, alloc::string::String, @@ -122,21 +124,27 @@ pub struct QuoteVerificationResult { impl QuoteVerificationResult { /// Build the full [`SupplementalData`] from verification intermediates. /// - /// This is lazy — the expensive parts (root_key_id SHA-384, CRL number extraction, - /// earliest_expiration from 4 sources, tcb_date_tag parsing) are only done here. + /// Computes the collateral time window from all 8 sources (TCBInfo, QEIdentity, + /// 2 CRLs, 4 certificate chains), root_key_id SHA-384, CRL numbers, and tcb_date_tag. pub fn supplemental(&self) -> Result { - let earliest_expiration = self.compute_earliest_expiration()?; - Ok(self.build_supplemental(earliest_expiration)) - } + // Parse collateral JSON for time window computation + let tcb_info: TcbInfo = serde_json::from_str(&self.collateral.tcb_info) + .context("Failed to parse TcbInfo for supplemental")?; + let qe_identity: QeIdentity = serde_json::from_str(&self.collateral.qe_identity) + .context("Failed to parse QeIdentity for supplemental")?; + + // Extract PCK cert chain from collateral for time window + let pck_certs = if let Some(pem_chain) = &self.collateral.pck_certificate_chain { + extract_certs(pem_chain.as_bytes()).unwrap_or_default() + } else { + Vec::new() + }; + + let (earliest_issue_date, latest_issue_date, earliest_expiration_date) = + compute_collateral_time_window(&self.collateral, &pck_certs, &tcb_info, &qe_identity)?; - /// Build `SupplementalData` with a pre-computed earliest_expiration value. - /// - /// Avoids redundant JSON/CRL parsing when the caller already has the value - /// (e.g., from `compute_time_window()` which produces a superset). - fn build_supplemental(&self, earliest_expiration: u64) -> SupplementalData { // root_key_id: SHA-384 of root CA's raw public key bytes let root_key_id = { - // root_ca_der was already validated during verify(), so unwrap is safe let root_cert: x509_cert::Certificate = der::Decode::from_der(&self.root_ca_der).expect("root CA already validated"); let raw_key = root_cert @@ -148,7 +156,8 @@ impl QuoteVerificationResult { }; // CRL numbers - let root_ca_crl_num = crate::utils::extract_crl_number(&self.collateral.root_ca_crl).unwrap_or(0); + let root_ca_crl_num = + crate::utils::extract_crl_number(&self.collateral.root_ca_crl).unwrap_or(0); let pck_crl_num = crate::utils::extract_crl_number(&self.collateral.pck_crl).unwrap_or(0); // tcb_date_tag @@ -157,13 +166,12 @@ impl QuoteVerificationResult { .map(|dt| dt.timestamp() as u64) .unwrap_or(0); - SupplementalData { + Ok(SupplementalData { tee_type: self.tee_type, tcb: TcbVerdict { status: self.tcb_status.clone(), advisory_ids: self.advisory_ids.clone(), eval_data_number: self.tcb_eval_data_number, - earliest_expiration, }, platform: PlatformInfo { tcb_level: self.platform_tcb_level.clone(), @@ -190,14 +198,18 @@ impl QuoteVerificationResult { report: self.qe_report.clone(), tcb_eval_data_number: self.qe_tcb_eval_data_number, }, - } + report: self.report.clone(), + earliest_issue_date, + latest_issue_date, + earliest_expiration_date, + }) } /// Validate against a policy, consuming self into [`VerifiedReport`] on success. pub fn validate(self, policy: &P) -> Result { let supplemental = self.supplemental()?; policy.validate(&supplemental)?; - Ok(self.into_verified_report()) + Ok(self.into_report_unchecked()) } /// Convert directly into [`VerifiedReport`] **without applying any policy**. @@ -207,52 +219,6 @@ impl QuoteVerificationResult { /// freshness, platform flags). Use only when you handle validation /// externally or intentionally accept any verification result. pub fn into_report_unchecked(self) -> VerifiedReport { - self.into_verified_report() - } - - /// Compute earliest_expiration from 4 lightweight sources: - /// TCBInfo nextUpdate, QeIdentity nextUpdate, Root CA CRL nextUpdate, PCK CRL nextUpdate. - /// - /// Omits certificate chain `notAfter` dates (which the full 8-source Rego path includes), - /// but this is safe: certificate validity is already enforced during `verify()`, and - /// cert `notAfter` (5–20+ years) never determines the `min()` over CRL/TcbInfo `nextUpdate` - /// (~30 days) in practice. - fn compute_earliest_expiration(&self) -> Result { - fn parse_rfc3339_ts(s: &str) -> Option { - chrono::DateTime::parse_from_rfc3339(s) - .ok() - .map(|dt| dt.timestamp() as u64) - } - - fn parse_crl_next_update(crl_der: &[u8]) -> Option { - use der::Decode as _; - let crl = x509_cert::crl::CertificateList::from_der(crl_der).ok()?; - crl.tbs_cert_list - .next_update - .map(|t| t.to_unix_duration().as_secs()) - } - - let tcb_info: TcbInfo = serde_json::from_str(&self.collateral.tcb_info) - .context("Failed to parse TcbInfo for earliest_expiration")?; - let qe_identity: QeIdentity = serde_json::from_str(&self.collateral.qe_identity) - .context("Failed to parse QeIdentity for earliest_expiration")?; - - let tcb_next = - parse_rfc3339_ts(&tcb_info.next_update).context("TCBInfo nextUpdate parse")?; - let qe_next = - parse_rfc3339_ts(&qe_identity.next_update).context("QEIdentity nextUpdate parse")?; - - let mut earliest = tcb_next.min(qe_next); - if let Some(next) = parse_crl_next_update(&self.collateral.root_ca_crl) { - earliest = earliest.min(next); - } - if let Some(next) = parse_crl_next_update(&self.collateral.pck_crl) { - earliest = earliest.min(next); - } - Ok(earliest) - } - - fn into_verified_report(self) -> VerifiedReport { VerifiedReport { status: self.tcb_status.to_string(), advisory_ids: self.advisory_ids, @@ -264,108 +230,6 @@ impl QuoteVerificationResult { } } -#[cfg(feature = "rego")] -impl QuoteVerificationResult { - /// Compute the full collateral time window (expensive: ~14 DER parses). - /// - /// Only needed for Rego evaluation. Called lazily, not during `verify()`. - fn compute_time_window(&self) -> Result { - // Re-extract PCK cert chain from owned collateral - let pck_certs = if let Some(pem_chain) = &self.collateral.pck_certificate_chain { - extract_certs(pem_chain.as_bytes()).unwrap_or_default() - } else { - Vec::new() - }; - - // We need TcbInfo and QeIdentity dates — re-parse from JSON in collateral. - // This is acceptable since Rego is already expensive. - let tcb_info: TcbInfo = serde_json::from_str(&self.collateral.tcb_info) - .context("Failed to re-parse TcbInfo for time window")?; - let qe_identity: QeIdentity = serde_json::from_str(&self.collateral.qe_identity) - .context("Failed to re-parse QeIdentity for time window")?; - - let (earliest_issue, latest_issue, earliest_expiration) = - compute_collateral_time_window(&self.collateral, &pck_certs, &tcb_info, &qe_identity)?; - - Ok(crate::policy::rego::CollateralTimeWindow { - earliest_issue_date: earliest_issue, - latest_issue_date: latest_issue, - earliest_expiration_date: earliest_expiration, - }) - } - - /// Generate Intel-format `qvl_result` array for Rego appraisal. - /// - /// SGX quotes produce 2 entries (platform + enclave). - /// TDX quotes produce 3 entries (platform + QE identity + TD). - pub(crate) fn to_rego_qvl_result( - &self, - supplemental: &SupplementalData, - tw: &crate::policy::rego::CollateralTimeWindow, - ) -> Vec { - use crate::policy::rego::{ - build_platform_measurement, build_qe_measurement, platform_class_id, tenant_class_id, - tenant_measurement, - }; - - let mut result = Vec::new(); - - // 1. Platform TCB measurement - let platform_cid = platform_class_id(&self.report, supplemental.tee_type); - result.push(serde_json::json!({ - "environment": { "class_id": platform_cid }, - "measurement": build_platform_measurement(supplemental, tw), - })); - - // 2. QE Identity measurement (TDX only) - if matches!(self.report, Report::TD10(_) | Report::TD15(_)) { - result.push(serde_json::json!({ - "environment": { "class_id": "3769258c-75e6-4bc7-8d72-d2b0e224cad2" }, - "measurement": build_qe_measurement(supplemental, tw), - })); - } - - // 3. Tenant measurement (enclave or TD report) - let tenant_cid = tenant_class_id(&self.report); - let mut tenant_m = tenant_measurement(&self.report); - // For SGX enclave, add sgx_ce_attributes from the QE report - if let Report::SgxEnclave(_) = &self.report { - if let Some(obj) = tenant_m.as_object_mut() { - obj.insert( - "sgx_ce_attributes".into(), - serde_json::json!(hex::encode_upper(supplemental.qe.report.attributes)), - ); - } - } - result.push(serde_json::json!({ - "environment": { "class_id": tenant_cid }, - "measurement": tenant_m, - })); - - result - } - - /// Validate against a [`RegoPolicy`] (single-measurement), consuming self on success. - pub fn validate_rego_single( - self, - policy: &crate::policy::RegoPolicy, - ) -> Result { - let tw = self.compute_time_window()?; - let supplemental = self.build_supplemental(tw.earliest_expiration_date); - policy.eval(&supplemental, &tw)?; - Ok(self.into_verified_report()) - } - - /// Validate against a [`RegoPolicySet`] (multi-measurement), consuming self on success. - pub fn validate_rego(self, policies: &crate::policy::RegoPolicySet) -> Result { - let tw = self.compute_time_window()?; - let supplemental = self.build_supplemental(tw.earliest_expiration_date); - let qvl_result = self.to_rego_qvl_result(&supplemental, &tw); - policies.eval_rego(qvl_result)?; - Ok(self.into_verified_report()) - } -} - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] @@ -1213,7 +1077,6 @@ pub mod rustcrypto { } } - // ============================================================================= // Step 6 & 9: Verify QE Report policy and match QE TCB // ============================================================================= diff --git a/tests/verify_quote.rs b/tests/verify_quote.rs index a1a86eb..5077e56 100644 --- a/tests/verify_quote.rs +++ b/tests/verify_quote.rs @@ -111,7 +111,9 @@ fn sgx_supplemental_data_cross_validation() { let now = now_from_collateral(&collateral); let verifier = QuoteVerifier::new_prod(ring::backend()); - let result = verifier.verify(&raw_quote, collateral.clone(), now).unwrap(); + let result = verifier + .verify(&raw_quote, collateral.clone(), now) + .unwrap(); let s = &result.supplemental().unwrap(); // Parse quote for later use @@ -125,7 +127,7 @@ fn sgx_supplemental_data_cross_validation() { assert_eq!(s.tcb.advisory_ids, ["INTEL-SA-00289", "INTEL-SA-00615"]); // earliest_expiration is computed lazily in supplemental() - assert!(s.tcb.earliest_expiration > 0); + assert!(s.earliest_expiration_date > 0); // ── tcb_date_tag ──────────────────────────────────────────────────── let expected_tcb_date = chrono::DateTime::parse_from_rfc3339(&s.platform.tcb_level.tcb_date) @@ -153,7 +155,10 @@ fn sgx_supplemental_data_cross_validation() { } 0 } - assert_eq!(s.platform.root_ca_crl_num, extract_crl_num(&collateral.root_ca_crl)); + assert_eq!( + s.platform.root_ca_crl_num, + extract_crl_num(&collateral.root_ca_crl) + ); assert_eq!(s.platform.pck_crl_num, extract_crl_num(&collateral.pck_crl)); // ── tcb_eval_data_number ──────────────────────────────────────────── @@ -215,7 +220,7 @@ fn sgx_supplemental_data_cross_validation() { let rc = &rc_result.supplemental().unwrap(); assert_eq!(s.tcb.status, rc.tcb.status); assert_eq!(s.tcb.advisory_ids, rc.tcb.advisory_ids); - assert_eq!(s.tcb.earliest_expiration, rc.tcb.earliest_expiration); + assert_eq!(s.earliest_expiration_date, rc.earliest_expiration_date); assert_eq!(s.platform.tcb_date_tag, rc.platform.tcb_date_tag); assert_eq!(s.platform.pck_crl_num, rc.platform.pck_crl_num); assert_eq!(s.platform.root_ca_crl_num, rc.platform.root_ca_crl_num); @@ -228,8 +233,14 @@ fn sgx_supplemental_data_cross_validation() { assert_eq!(s.platform.pck.fmspc, rc.platform.pck.fmspc); assert_eq!(s.tee_type, rc.tee_type); assert_eq!(s.platform.pck.sgx_type, rc.platform.pck.sgx_type); - assert_eq!(s.platform.pck.platform_instance_id, rc.platform.pck.platform_instance_id); - assert_eq!(s.platform.pck.dynamic_platform, rc.platform.pck.dynamic_platform); + assert_eq!( + s.platform.pck.platform_instance_id, + rc.platform.pck.platform_instance_id + ); + assert_eq!( + s.platform.pck.dynamic_platform, + rc.platform.pck.dynamic_platform + ); assert_eq!(s.platform.pck.cached_keys, rc.platform.pck.cached_keys); assert_eq!(s.platform.pck.smt_enabled, rc.platform.pck.smt_enabled); } @@ -273,25 +284,66 @@ fn print_supplemental_data_comparison() { serde_json::from_slice(include_bytes!("../sample/sgx_quote_collateral.json")).unwrap(); let now = now_from_collateral(&collateral); - let result = verifier.verify(&raw_quote, collateral.clone(), now).unwrap(); + let result = verifier + .verify(&raw_quote, collateral.clone(), now) + .unwrap(); let s = &result.supplemental().unwrap(); println!("{:<40} {:?}", "tcb.status", s.tcb.status); println!("{:<40} {:?}", "tcb.advisory_ids", s.tcb.advisory_ids); - println!("{:<40} {} ({})", "tcb.earliest_expiration", s.tcb.earliest_expiration, ts_to_utc(s.tcb.earliest_expiration)); + println!( + "{:<40} {} ({})", + "tcb.earliest_expiration", + s.earliest_expiration_date, + ts_to_utc(s.earliest_expiration_date) + ); println!("{:<40} {}", "tcb.eval_data_number", s.tcb.eval_data_number); - println!("{:<40} {} ({})", "platform.tcb_date_tag", s.platform.tcb_date_tag, ts_to_utc(s.platform.tcb_date_tag)); + println!( + "{:<40} {} ({})", + "platform.tcb_date_tag", + s.platform.tcb_date_tag, + ts_to_utc(s.platform.tcb_date_tag) + ); println!("{:<40} {}", "platform.pck_crl_num", s.platform.pck_crl_num); - println!("{:<40} {}", "platform.root_ca_crl_num", s.platform.root_ca_crl_num); - println!("{:<40} {}...", "platform.root_key_id", hex::encode(&s.platform.root_key_id[..24])); - println!("{:<40} {}", "platform.pck.fmspc", hex::encode(s.platform.pck.fmspc)); - println!("{:<40} {}", "platform.pck.sgx_type", s.platform.pck.sgx_type); - println!("{:<40} {:?}", "platform.pck.dynamic_platform", s.platform.pck.dynamic_platform); - println!("{:<40} {:?}", "platform.pck.cached_keys", s.platform.pck.cached_keys); - println!("{:<40} {:?}", "platform.pck.smt_enabled", s.platform.pck.smt_enabled); + println!( + "{:<40} {}", + "platform.root_ca_crl_num", s.platform.root_ca_crl_num + ); + println!( + "{:<40} {}...", + "platform.root_key_id", + hex::encode(&s.platform.root_key_id[..24]) + ); + println!( + "{:<40} {}", + "platform.pck.fmspc", + hex::encode(s.platform.pck.fmspc) + ); + println!( + "{:<40} {}", + "platform.pck.sgx_type", s.platform.pck.sgx_type + ); + println!( + "{:<40} {:?}", + "platform.pck.dynamic_platform", s.platform.pck.dynamic_platform + ); + println!( + "{:<40} {:?}", + "platform.pck.cached_keys", s.platform.pck.cached_keys + ); + println!( + "{:<40} {:?}", + "platform.pck.smt_enabled", s.platform.pck.smt_enabled + ); println!("{:<40} 0x{:08X}", "tee_type", s.tee_type); - println!("{:<40} {:?}", "platform.tcb_level.tcb_status", s.platform.tcb_level.tcb_status); - println!("{:<40} {:?}", "qe.tcb_level.tcb_status", s.qe.tcb_level.tcb_status); + println!( + "{:<40} {:?}", + "platform.tcb_level.tcb_status", s.platform.tcb_level.tcb_status + ); + println!( + "{:<40} {:?}", + "qe.tcb_level.tcb_status", s.qe.tcb_level.tcb_status + ); // ═══════════════════════════════════════════════════════════════════ // TDX Quote @@ -305,25 +357,66 @@ fn print_supplemental_data_comparison() { serde_json::from_slice(include_bytes!("../sample/tdx_quote_collateral.json")).unwrap(); let now_tdx = now_from_collateral(&collateral_tdx); - let result_tdx = verifier.verify(raw_quote_tdx, collateral_tdx.clone(), now_tdx).unwrap(); + let result_tdx = verifier + .verify(raw_quote_tdx, collateral_tdx.clone(), now_tdx) + .unwrap(); let t = &result_tdx.supplemental().unwrap(); println!("{:<40} {:?}", "tcb.status", t.tcb.status); println!("{:<40} {:?}", "tcb.advisory_ids", t.tcb.advisory_ids); - println!("{:<40} {} ({})", "tcb.earliest_expiration", t.tcb.earliest_expiration, ts_to_utc(t.tcb.earliest_expiration)); + println!( + "{:<40} {} ({})", + "tcb.earliest_expiration", + t.earliest_expiration_date, + ts_to_utc(t.earliest_expiration_date) + ); println!("{:<40} {}", "tcb.eval_data_number", t.tcb.eval_data_number); - println!("{:<40} {} ({})", "platform.tcb_date_tag", t.platform.tcb_date_tag, ts_to_utc(t.platform.tcb_date_tag)); + println!( + "{:<40} {} ({})", + "platform.tcb_date_tag", + t.platform.tcb_date_tag, + ts_to_utc(t.platform.tcb_date_tag) + ); println!("{:<40} {}", "platform.pck_crl_num", t.platform.pck_crl_num); - println!("{:<40} {}", "platform.root_ca_crl_num", t.platform.root_ca_crl_num); - println!("{:<40} {}...", "platform.root_key_id", hex::encode(&t.platform.root_key_id[..24])); - println!("{:<40} {}", "platform.pck.fmspc", hex::encode(t.platform.pck.fmspc)); - println!("{:<40} {}", "platform.pck.sgx_type", t.platform.pck.sgx_type); - println!("{:<40} {:?}", "platform.pck.dynamic_platform", t.platform.pck.dynamic_platform); - println!("{:<40} {:?}", "platform.pck.cached_keys", t.platform.pck.cached_keys); - println!("{:<40} {:?}", "platform.pck.smt_enabled", t.platform.pck.smt_enabled); + println!( + "{:<40} {}", + "platform.root_ca_crl_num", t.platform.root_ca_crl_num + ); + println!( + "{:<40} {}...", + "platform.root_key_id", + hex::encode(&t.platform.root_key_id[..24]) + ); + println!( + "{:<40} {}", + "platform.pck.fmspc", + hex::encode(t.platform.pck.fmspc) + ); + println!( + "{:<40} {}", + "platform.pck.sgx_type", t.platform.pck.sgx_type + ); + println!( + "{:<40} {:?}", + "platform.pck.dynamic_platform", t.platform.pck.dynamic_platform + ); + println!( + "{:<40} {:?}", + "platform.pck.cached_keys", t.platform.pck.cached_keys + ); + println!( + "{:<40} {:?}", + "platform.pck.smt_enabled", t.platform.pck.smt_enabled + ); println!("{:<40} 0x{:08X}", "tee_type", t.tee_type); - println!("{:<40} {:?}", "platform.tcb_level.tcb_status", t.platform.tcb_level.tcb_status); - println!("{:<40} {:?}", "qe.tcb_level.tcb_status", t.qe.tcb_level.tcb_status); + println!( + "{:<40} {:?}", + "platform.tcb_level.tcb_status", t.platform.tcb_level.tcb_status + ); + println!( + "{:<40} {:?}", + "qe.tcb_level.tcb_status", t.qe.tcb_level.tcb_status + ); } /// Cross-validate TDX supplemental data fields. @@ -346,7 +439,7 @@ fn tdx_supplemental_data_cross_validation() { assert!(s.tcb.advisory_ids.is_empty()); // Fields should be populated (computed lazily in supplemental()) - assert!(s.tcb.earliest_expiration > 0); + assert!(s.earliest_expiration_date > 0); assert!(s.platform.tcb_date_tag > 0); // root_key_id should match SHA-384 of Intel root CA raw public key bytes @@ -365,11 +458,13 @@ fn tdx_supplemental_data_cross_validation() { // Verify ring == rustcrypto for all fields let rc_verifier = QuoteVerifier::new_prod(dcap_qvl::verify::rustcrypto::backend()); - let rc_result = rc_verifier.verify(raw_quote, collateral.clone(), now).unwrap(); + let rc_result = rc_verifier + .verify(raw_quote, collateral.clone(), now) + .unwrap(); let rc = &rc_result.supplemental().unwrap(); assert_eq!(s.tee_type, rc.tee_type); assert_eq!(s.tcb.status, rc.tcb.status); assert_eq!(s.platform.root_key_id, rc.platform.root_key_id); - assert_eq!(s.tcb.earliest_expiration, rc.tcb.earliest_expiration); + assert_eq!(s.earliest_expiration_date, rc.earliest_expiration_date); assert_eq!(s.tcb.eval_data_number, rc.tcb.eval_data_number); } From cf71c1952ef64c8640b1b2b987af38d2cbcac97f Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 09:28:31 +0000 Subject: [PATCH 08/33] =?UTF-8?q?feat:=20full=20Intel=20QAL=20compat=20?= =?UTF-8?q?=E2=80=94=20rand.intn,=20final=5Fappraisal=5Fresult,=20qe=5Fide?= =?UTF-8?q?n=5F*=20dates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Register rand.intn as regorus extension with real RNG (getrandom) and per-evaluation memoization matching OPA semantics (cache key = "{str}-{n}") - Switch Rego query from final_ret to final_appraisal_result (the standard Intel QAL output format with nonce, timestamp, appraised_reports) - Add qe_iden_earliest_issue_date, qe_iden_latest_issue_date, qe_iden_earliest_expiration_date to SupplementalData, computed from QE Identity issuer chain + JSON (sources [5]+[7]), matching Intel's qve_get_collateral_dates() lines 94-96 - QE measurement now uses QE-specific time window instead of global dates --- Cargo.toml | 2 +- src/policy/mod.rs | 6 ++ src/policy/rego.rs | 212 ++++++++++++++++++++++++++++++++++++++++--- src/policy/simple.rs | 3 + src/verify.rs | 63 ++++++++++--- 5 files changed, 259 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d390df9..cc68dfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,7 @@ ring = ["dep:ring", "dcap-qvl-webpki/ring", "_anycrypto"] rustcrypto = ["dep:sha2", "dep:p256", "dep:signature", "dcap-qvl-webpki/rustcrypto", "_anycrypto"] _anycrypto = [] contract = ["getrandom"] -rego = ["dep:regorus", "std", "serde_json"] +rego = ["dep:regorus", "std", "serde_json", "getrandom"] # Enables the dangerous_verify_with_tcb_override() function, allowing TCB checks to be overridden # with custom collateral. Normal verify() is not affected. diff --git a/src/policy/mod.rs b/src/policy/mod.rs index 6b1ae14..5a73756 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -94,6 +94,12 @@ pub struct SupplementalData { pub latest_issue_date: u64, /// `min(nextUpdate / notAfter)` across all 8 collateral sources (the "weakest link"). pub earliest_expiration_date: u64, + /// `min(issueDate / notBefore)` across QE Identity sources (issuer chain + JSON). + pub qe_iden_earliest_issue_date: u64, + /// `max(issueDate / notBefore)` across QE Identity sources (issuer chain + JSON). + pub qe_iden_latest_issue_date: u64, + /// `min(nextUpdate / notAfter)` across QE Identity sources (issuer chain + JSON). + pub qe_iden_earliest_expiration_date: u64, /// Quote report body (SGX enclave report, TDX TD10/TD15). pub report: Report, } diff --git a/src/policy/rego.rs b/src/policy/rego.rs index 073c62d..b9a358d 100644 --- a/src/policy/rego.rs +++ b/src/policy/rego.rs @@ -143,15 +143,15 @@ fn build_qe_measurement(data: &SupplementalData) -> serde_json::Value { m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); } - let earliest_issue = unix_to_rfc3339(data.earliest_issue_date); + let earliest_issue = unix_to_rfc3339(data.qe_iden_earliest_issue_date); if !earliest_issue.is_empty() { m.insert("earliest_issue_date".into(), json!(earliest_issue)); } - let latest_issue = unix_to_rfc3339(data.latest_issue_date); + let latest_issue = unix_to_rfc3339(data.qe_iden_latest_issue_date); if !latest_issue.is_empty() { m.insert("latest_issue_date".into(), json!(latest_issue)); } - let earliest_exp = unix_to_rfc3339(data.earliest_expiration_date); + let earliest_exp = unix_to_rfc3339(data.qe_iden_earliest_expiration_date); if !earliest_exp.is_empty() { m.insert("earliest_expiration_date".into(), json!(earliest_exp)); } @@ -344,6 +344,7 @@ impl RegoPolicySet { /// Create a `RegoPolicySet` with a custom Rego script. pub fn with_rego(policy_jsons: &[&str], rego_source: &str) -> Result { let mut engine = regorus::Engine::new(); + register_rand_intn(&mut engine)?; engine .add_policy("qal_script.rego".into(), rego_source.into()) .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; @@ -365,6 +366,64 @@ impl RegoPolicySet { } } +/// Register `rand.intn` extension on a regorus engine. +/// +/// OPA's `rand.intn(str, n)` returns a random integer in `[0, n)`. +/// The `str` parameter is a **memoization key** (not a PRNG seed): same `(str, n)` pair +/// within one query evaluation always returns the same result. The actual random number +/// comes from a separate RNG, not derived from the string. +/// +/// Cache key is `"{str}-{n}"` matching OPA's implementation. +/// +/// Ref: OPA docs — "For any given argument pair (str, n), the output will be consistent +/// throughout a query evaluation." +/// +/// +/// Ref: OPA source — `key := randIntCachingKey(fmt.Sprintf("%s-%d", strOp, n))` +/// +fn register_rand_intn(engine: &mut regorus::Engine) -> Result<()> { + let mut cache = std::collections::HashMap::::new(); + engine + .add_extension( + "rand.intn".to_string(), + 2, + Box::new(move |params: Vec| { + let seed = params + .first() + .ok_or_else(|| anyhow::anyhow!("rand.intn: missing first argument"))? + .as_string() + .map_err(|_| anyhow::anyhow!("rand.intn: first argument must be string"))? + .to_string(); + + let n = params + .get(1) + .ok_or_else(|| anyhow::anyhow!("rand.intn: missing second argument"))? + .as_i64() + .map_err(|_| anyhow::anyhow!("rand.intn: second argument must be integer"))?; + + if n <= 0 { + return Ok(regorus::Value::from(0i64)); + } + + // Cache key = "{seed}-{n}", matching OPA's `fmt.Sprintf("%s-%d", strOp, n)` + let key = alloc::format!("{seed}-{n}"); + + if let Some(&cached) = cache.get(&key) { + return Ok(regorus::Value::from(cached)); + } + + let mut buf = [0u8; 8]; + getrandom::getrandom(&mut buf) + .map_err(|e| anyhow::anyhow!("rand.intn: RNG failed: {e}"))?; + let random_val = + (u64::from_le_bytes(buf).checked_rem(n as u64).unwrap_or(0)) as i64; + cache.insert(key, random_val); + Ok(regorus::Value::from(random_val)) + }), + ) + .map_err(|e| anyhow::anyhow!("Failed to register rand.intn: {e}")) +} + /// Shared Rego evaluation logic used by both `RegoPolicy` and `RegoPolicySet`. fn eval_rego_engine( engine: ®orus::Engine, @@ -387,26 +446,42 @@ fn eval_rego_engine( .map_err(|e| anyhow::anyhow!("Failed to set Rego input: {e}"))?; let result = engine - .eval_rule("data.dcap.quote.appraisal.final_ret".into()) + .eval_rule("data.dcap.quote.appraisal.final_appraisal_result".into()) .map_err(|e| anyhow::anyhow!("Rego evaluation failed: {e}"))?; let result_json = result .to_json_str() .map_err(|e| anyhow::anyhow!("Failed to convert Rego result: {e}"))?; - match result_json.trim() { - "1" => Ok(()), - "0" => { - let detail = engine - .eval_rule("data.dcap.quote.appraisal.appraisal_result".into()) - .ok() - .and_then(|v| v.to_json_str().ok()); + // final_appraisal_result is a Rego set → JSON array of objects + let result_value: serde_json::Value = serde_json::from_str(&result_json) + .map_err(|e| anyhow::anyhow!("Failed to parse final_appraisal_result JSON: {e}"))?; + + let arr = result_value + .as_array() + .ok_or_else(|| anyhow::anyhow!("final_appraisal_result is not an array"))?; + + let entry = arr + .first() + .ok_or_else(|| anyhow::anyhow!("final_appraisal_result is empty"))?; + + let overall = entry + .get("overall_appraisal_result") + .and_then(|v| v.as_i64()) + .ok_or_else(|| { + anyhow::anyhow!("final_appraisal_result missing overall_appraisal_result") + })?; + + match overall { + 1 => Ok(()), + 0 => { + let detail = entry.get("appraised_reports"); if let Some(detail) = detail { bail!("Rego appraisal failed: {detail}"); } bail!("Rego appraisal failed (result = 0)"); } - "-1" => bail!("No policy matched the report class_id"), + -1 => bail!("No policy matched the report class_id"), other => bail!("Unexpected Rego appraisal result: {other}"), } } @@ -456,6 +531,7 @@ impl RegoPolicy { /// Use this to provide an updated or modified version of `qal_script.rego`. pub fn with_rego(policy_json: &str, rego_source: &str) -> Result { let mut engine = regorus::Engine::new(); + register_rand_intn(&mut engine)?; engine .add_policy("qal_script.rego".into(), rego_source.into()) .map_err(|e| anyhow::anyhow!("Failed to load Rego policy: {e}"))?; @@ -630,6 +706,9 @@ mod tests { earliest_issue_date: 1_690_000_000, latest_issue_date: 1_690_100_000, earliest_expiration_date: 1_703_000_000, + qe_iden_earliest_issue_date: 1_690_000_000, + qe_iden_latest_issue_date: 1_690_100_000, + qe_iden_earliest_expiration_date: 1_703_000_000, } } @@ -639,6 +718,9 @@ mod tests { data.earliest_issue_date = 1_900_000_000; data.latest_issue_date = 1_900_100_000; data.earliest_expiration_date = 2_000_000_000; + data.qe_iden_earliest_issue_date = 1_900_000_000; + data.qe_iden_latest_issue_date = 1_900_100_000; + data.qe_iden_earliest_expiration_date = 2_000_000_000; data } @@ -886,6 +968,112 @@ mod tests { ); } + #[test] + fn rego_final_appraisal_result_has_expected_fields() { + // Verify that eval uses final_appraisal_result (not final_ret) by checking + // the Rego engine can produce the full appraisal output with nonce/timestamp. + let data = make_rego_supplemental(UpToDate); + let json = + policy_json(r#"{"accepted_tcb_status": ["UpToDate"], "collateral_grace_period": 0}"#); + + let mut engine = regorus::Engine::new(); + register_rand_intn(&mut engine).unwrap(); + engine + .add_policy( + "qal_script.rego".into(), + include_str!("../../rego/qal_script.rego").into(), + ) + .unwrap(); + + let policy_value: serde_json::Value = serde_json::from_str(&json).unwrap(); + let measurement = build_merged_measurement(&data); + let class_id = policy_value["environment"]["class_id"].as_str().unwrap(); + let qvl_result = vec![json!({ + "environment": { "class_id": class_id }, + "measurement": measurement, + })]; + let input = json!({ + "qvl_result": qvl_result, + "policies": { "policy_array": [&policy_value] }, + }); + engine + .set_input_json(&serde_json::to_string(&input).unwrap()) + .unwrap(); + + let result = engine + .eval_rule("data.dcap.quote.appraisal.final_appraisal_result".into()) + .unwrap(); + let result_json: serde_json::Value = + serde_json::from_str(&result.to_json_str().unwrap()).unwrap(); + let arr = result_json.as_array().unwrap(); + assert_eq!(arr.len(), 1, "expected exactly one appraisal result"); + let entry = &arr[0]; + assert_eq!(entry["overall_appraisal_result"], 1); + assert!(entry.get("nonce").is_some(), "missing nonce from rand.intn"); + assert!( + entry.get("appraisal_check_date").is_some(), + "missing appraisal_check_date from time.now_ns" + ); + assert!( + entry.get("appraised_reports").is_some(), + "missing appraised_reports" + ); + // nonce should be a non-negative integer < 10^15 + let nonce = entry["nonce"].as_i64().unwrap(); + assert!( + nonce >= 0 && nonce < 1_000_000_000_000_000, + "nonce out of range: {nonce}" + ); + } + + #[test] + fn rego_rand_intn_memoization() { + // Same (seed, n) pair within one engine evaluation → same result. + let mut engine = regorus::Engine::new(); + register_rand_intn(&mut engine).unwrap(); + engine + .add_policy( + "test.rego".into(), + r#"package test + import future.keywords.if + a := rand.intn("memo_test", 1000000000) + b := rand.intn("memo_test", 1000000000) + same if { a == b } + "# + .into(), + ) + .unwrap(); + engine.set_input_json("{}").unwrap(); + + let same = engine + .eval_rule("data.test.same".into()) + .unwrap() + .to_json_str() + .unwrap(); + assert_eq!( + same.trim(), + "true", + "rand.intn memoization failed: same seed should return same value" + ); + } + + #[test] + fn rego_qe_measurement_uses_qe_iden_dates() { + let mut data = make_rego_supplemental(UpToDate); + // Set QE-specific dates different from global dates + data.qe_iden_earliest_issue_date = 1_850_000_000; + data.qe_iden_latest_issue_date = 1_850_100_000; + data.qe_iden_earliest_expiration_date = 1_950_000_000; + let m = build_qe_measurement(&data); + // QE measurement should use qe_iden_* dates, not the global ones + let earliest = m.get("earliest_issue_date").unwrap().as_str().unwrap(); + let expected = "2028-08-16T"; // 1_850_000_000 = 2028-08-16T00:53:20Z + assert!( + earliest.starts_with(expected), + "QE measurement should use qe_iden_earliest_issue_date, got: {earliest}" + ); + } + #[test] fn rego_policy_set_class_id_mismatch_fails() { let data = make_rego_supplemental(UpToDate); diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 73264bd..093fbbd 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -447,6 +447,9 @@ mod tests { earliest_issue_date: 1_690_000_000, latest_issue_date: 1_690_100_000, earliest_expiration_date: 1_703_000_000, // ~2023-12-19 + qe_iden_earliest_issue_date: 1_690_000_000, + qe_iden_latest_issue_date: 1_690_100_000, + qe_iden_earliest_expiration_date: 1_703_000_000, } } diff --git a/src/verify.rs b/src/verify.rs index 51a634c..0d43f61 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -140,13 +140,14 @@ impl QuoteVerificationResult { Vec::new() }; - let (earliest_issue_date, latest_issue_date, earliest_expiration_date) = + let collateral_dates = compute_collateral_time_window(&self.collateral, &pck_certs, &tcb_info, &qe_identity)?; // root_key_id: SHA-384 of root CA's raw public key bytes let root_key_id = { let root_cert: x509_cert::Certificate = - der::Decode::from_der(&self.root_ca_der).expect("root CA already validated"); + der::Decode::from_der(&self.root_ca_der) + .context("root CA already validated but failed to re-parse")?; let raw_key = root_cert .tbs_certificate .subject_public_key_info @@ -169,7 +170,7 @@ impl QuoteVerificationResult { Ok(SupplementalData { tee_type: self.tee_type, tcb: TcbVerdict { - status: self.tcb_status.clone(), + status: self.tcb_status, advisory_ids: self.advisory_ids.clone(), eval_data_number: self.tcb_eval_data_number, }, @@ -195,13 +196,16 @@ impl QuoteVerificationResult { }, qe: QeInfo { tcb_level: self.qe_tcb_level.clone(), - report: self.qe_report.clone(), + report: self.qe_report, tcb_eval_data_number: self.qe_tcb_eval_data_number, }, report: self.report.clone(), - earliest_issue_date, - latest_issue_date, - earliest_expiration_date, + earliest_issue_date: collateral_dates.earliest_issue, + latest_issue_date: collateral_dates.latest_issue, + earliest_expiration_date: collateral_dates.earliest_expiration, + qe_iden_earliest_issue_date: collateral_dates.qe_iden_earliest_issue, + qe_iden_latest_issue_date: collateral_dates.qe_iden_latest_issue, + qe_iden_earliest_expiration_date: collateral_dates.qe_iden_earliest_expiration, }) } @@ -850,9 +854,21 @@ fn verify_impl( }) } +/// Collateral time window dates (8 sources + QE Identity subset). +struct CollateralDates { + earliest_issue: u64, + latest_issue: u64, + earliest_expiration: u64, + /// QE Identity-specific dates (sources \[5\] + \[7\] only). + qe_iden_earliest_issue: u64, + qe_iden_latest_issue: u64, + qe_iden_earliest_expiration: u64, +} + /// Compute the collateral time window: earliest issue, latest issue, earliest expiration. /// /// Matches Intel QVL's `qve_get_collateral_dates()` which considers **8 date sources**: +/// /// 1. Root CA CRL thisUpdate/nextUpdate /// 2. PCK CRL thisUpdate/nextUpdate /// 3. PCK CRL issuer certificate chain notBefore/notAfter @@ -866,7 +882,7 @@ fn compute_collateral_time_window( pck_cert_chain: &[CertificateDer<'_>], tcb_info: &TcbInfo, qe_identity: &QeIdentity, -) -> Result<(u64, u64, u64)> { +) -> Result { fn parse_rfc3339_ts(s: &str) -> Option { chrono::DateTime::parse_from_rfc3339(s) .ok() @@ -969,15 +985,34 @@ fn compute_collateral_time_window( &mut latest_issue, &mut earliest_expiration, )?; - // QEIdentity issuer chain + // QEIdentity issuer chain (source [5]) — also track QE-specific dates + let mut qe_chain_earliest_issue = u64::MAX; + let mut qe_chain_latest_issue = 0u64; + let mut qe_chain_earliest_expiration = u64::MAX; fold_cert_chain_dates( collateral.qe_identity_issuer_chain.as_bytes(), - &mut earliest_issue, - &mut latest_issue, - &mut earliest_expiration, + &mut qe_chain_earliest_issue, + &mut qe_chain_latest_issue, + &mut qe_chain_earliest_expiration, )?; - - Ok((earliest_issue, latest_issue, earliest_expiration)) + // Fold into global window + earliest_issue = earliest_issue.min(qe_chain_earliest_issue); + latest_issue = latest_issue.max(qe_chain_latest_issue); + earliest_expiration = earliest_expiration.min(qe_chain_earliest_expiration); + + // QE Identity-specific window: min/max of source [5] (issuer chain) + source [7] (JSON) + let qe_iden_earliest_issue = qe_chain_earliest_issue.min(qe_issue); + let qe_iden_latest_issue = qe_chain_latest_issue.max(qe_issue); + let qe_iden_earliest_expiration = qe_chain_earliest_expiration.min(qe_next); + + Ok(CollateralDates { + earliest_issue, + latest_issue, + earliest_expiration, + qe_iden_earliest_issue, + qe_iden_latest_issue, + qe_iden_earliest_expiration, + }) } fn validate_sgx_attrs(report: &EnclaveReport) -> Result<()> { From e0c8f43aab7844de3d2050627291ce426cf922ab Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 09:40:37 +0000 Subject: [PATCH 09/33] =?UTF-8?q?docs:=20fix=20ignored=20doc=20tests=20to?= =?UTF-8?q?=20compile=20(ignore=20=E2=86=92=20no=5Frun)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/policy/mod.rs | 5 +++-- src/policy/simple.rs | 4 +++- src/verify.rs | 9 --------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/policy/mod.rs b/src/policy/mod.rs index 5a73756..2eff41f 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -26,12 +26,13 @@ pub use rego::RegoPolicySet; /// that covers all common checks from Intel's Appraisal framework. /// /// For most use cases, [`SimplePolicy`] with its builder methods is sufficient: -/// ```ignore +/// ```no_run /// use dcap_qvl::SimplePolicy; /// use dcap_qvl::TcbStatus; -/// /// use core::time::Duration; /// +/// let now_unix_secs = 1_700_000_000u64; +/// /// let policy = SimplePolicy::strict(now_unix_secs) /// .allow_status(TcbStatus::SWHardeningNeeded) /// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 093fbbd..ef8d1f1 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -19,10 +19,12 @@ use { /// Strict by default: only `UpToDate`, no grace period, no advisory tolerance. /// /// # Example -/// ```ignore +/// ```no_run /// use dcap_qvl::SimplePolicy; /// use dcap_qvl::TcbStatus; /// +/// let now = 1_700_000_000u64; // unix timestamp +/// /// // Strict: only UpToDate, collateral must not be expired /// let policy = SimplePolicy::strict(now); /// diff --git a/src/verify.rs b/src/verify.rs index 0d43f61..ee8257f 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -95,15 +95,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; /// /// [`SupplementalData`] is built lazily via [`supplemental()`](Self::supplemental) — /// the `verify()` call itself does the minimum work (crypto only). -/// -/// ```ignore -/// let result = verifier.verify("e, collateral, now)?; -/// // Inspect supplemental data (lazy — built on first call) -/// let sup = result.supplemental()?; -/// println!("TCB status: {:?}", sup.tcb.status); -/// // Or apply policy directly -/// let report = result.validate(&SimplePolicy::strict(now))?; -/// ``` pub struct QuoteVerificationResult { report: Report, collateral: QuoteCollateralV3, From 65556591a08d679735240732ceba2400153a20d5 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 09:48:19 +0000 Subject: [PATCH 10/33] docs: clean up duplicate SimplePolicy doc comment --- docs/policy.md | 4 ++-- src/policy/simple.rs | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/policy.md b/docs/policy.md index 4f9c09c..425e536 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -145,9 +145,9 @@ let policy_json = r#"{ } }"#; let policy = RegoPolicy::new(policy_json)?; -let report = result.validate_rego(&policy)?; +let report = result.validate(&policy)?; ``` -`RegoPolicySet` supports multiple JSON policies for multi-measurement appraisal (one per class_id), matching Intel QAL's full functionality. +`RegoPolicySet` supports multiple JSON policies for multi-measurement appraisal (one per `class_id`), matching Intel QAL's full functionality. Both `RegoPolicy` and `RegoPolicySet` implement the `Policy` trait, so they work with the standard `validate()` method. See [Intel's DCAP Appraisal documentation](https://github.com/intel/SGXDataCenterAttestationPrimitives) for the Rego policy JSON format. diff --git a/src/policy/simple.rs b/src/policy/simple.rs index ef8d1f1..9434201 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -10,13 +10,11 @@ use { alloc::vec::Vec, }; -/// Status-based verification policy. +/// Built-in verification policy with builder pattern. /// -/// By default, the policy is strict: only `UpToDate` status is accepted. -/// Comprehensive verification policy with builder pattern. -/// -/// Covers all checks from Intel's Appraisal framework (`qal_script.rego`). -/// Strict by default: only `UpToDate`, no grace period, no advisory tolerance. +/// Covers the 9 checks from Intel's Appraisal framework (`qal_script.rego`) +/// without requiring a Rego engine. Strict by default: only `UpToDate`, +/// no grace period, no advisory tolerance. /// /// # Example /// ```no_run From 14bb557a8432521b5a171b32a903c04316aa1a54 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 09:55:35 +0000 Subject: [PATCH 11/33] fix: update cli for two-phase verify API, fix rustfmt --- cli/src/bin/test_case.rs | 20 ++++++++------------ cli/src/main.rs | 4 +--- src/verify.rs | 11 ++++++++--- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/cli/src/bin/test_case.rs b/cli/src/bin/test_case.rs index 31972dc..b638083 100644 --- a/cli/src/bin/test_case.rs +++ b/cli/src/bin/test_case.rs @@ -113,10 +113,12 @@ fn run_verify(quote_file: PathBuf, collateral_file: PathBuf, root_ca_file: Optio }; let ring_result = ring_verifier - .verify("e_bytes, &collateral, now) + .verify("e_bytes, collateral.clone(), now) + .map(|r| r.into_report_unchecked()) .map_err(|e| format!("{e:#}")); let rustcrypto_result = rustcrypto_verifier - .verify("e_bytes, &collateral, now) + .verify("e_bytes, collateral, now) + .map(|r| r.into_report_unchecked()) .map_err(|e| format!("{e:#}")); if ring_result != rustcrypto_result { eprintln!("Verification results differ between ring and rustcrypto"); @@ -125,20 +127,14 @@ fn run_verify(quote_file: PathBuf, collateral_file: PathBuf, root_ca_file: Optio return 1; } - let ring_result1 = ring_verifier.verify("e_bytes, &collateral, now); - match ring_result1 { - Ok(verified_report) => { + match ring_result { + Ok(report) => { println!("Verification successful"); - println!("Status: {:?}", verified_report.status); + println!("Status: {}", report.status); 0 } Err(e) => { - eprintln!("Verification failed: {}", e); - let mut source = e.source(); - while let Some(err) = source { - eprintln!(" Caused by: {}", err); - source = err.source(); - } + eprintln!("Verification failed: {e}"); 1 } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 931fed9..7085811 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,7 +10,6 @@ use dcap_qvl::collateral::{get_collateral, PHALA_PCCS_URL}; use dcap_qvl::intel; use dcap_qvl::quote::Quote; use dcap_qvl::verify::{ring, QuoteVerifier}; -use dcap_qvl::SimplePolicy; use der::Decode; use serde::Serialize; use x509_cert::Certificate; @@ -108,8 +107,7 @@ async fn command_verify_quote(args: VerifyQuoteArgs) -> Result<()> { let result = verifier .verify("e, collateral, now) .context("Failed to verify quote")?; - let report = result - .into_report_unchecked(); + let report = result.into_report_unchecked(); println!( "{}", serde_json::to_string(&report).context("Failed to serialize report")? diff --git a/src/verify.rs b/src/verify.rs index ee8257f..334278f 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -136,9 +136,8 @@ impl QuoteVerificationResult { // root_key_id: SHA-384 of root CA's raw public key bytes let root_key_id = { - let root_cert: x509_cert::Certificate = - der::Decode::from_der(&self.root_ca_der) - .context("root CA already validated but failed to re-parse")?; + let root_cert: x509_cert::Certificate = der::Decode::from_der(&self.root_ca_der) + .context("root CA already validated but failed to re-parse")?; let raw_key = root_cert .tbs_certificate .subject_public_key_info @@ -823,6 +822,12 @@ fn verify_impl( TcbStatusWithAdvisory::new(qe_tcb_level.tcb_status, qe_tcb_level.advisory_ids.clone()); let final_status = platform_status.merge(&qe_status); + // Revoked means the platform's keys are compromised — reject unconditionally, + // regardless of policy. This is a security invariant, not a policy decision. + if final_status.status == TcbStatus::Revoked { + bail!("TCB status is Revoked: platform keys are compromised"); + } + // Validate report attributes (debug mode check, etc.) validate_attrs("e.report)?; From 4a78472023facc8ca83b01eab2fd568a308c0af5 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 10:14:30 +0000 Subject: [PATCH 12/33] fix: update Python test_case.py for two-phase verify API --- python-bindings/test_case.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python-bindings/test_case.py b/python-bindings/test_case.py index a2ed20b..563d63c 100755 --- a/python-bindings/test_case.py +++ b/python-bindings/test_case.py @@ -83,9 +83,10 @@ def cmd_verify(args): # Use production Intel root CA result = dcap_qvl.verify(quote_bytes, collateral_obj, now_secs) - # Verification successful + # Verification successful — get report without policy (test harness) + report = result.into_report_unchecked() print("Verification successful") - print(f"Status: {result.status}") + print(f"Status: {report.status}") return 0 except Exception as e: From 125d242c721ab4cbf4bc4a26b8db3f6a9cff2b65 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 10:40:40 +0000 Subject: [PATCH 13/33] fix: match expected error message for Revoked TCB status --- src/verify.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verify.rs b/src/verify.rs index 334278f..e635f5b 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -825,7 +825,7 @@ fn verify_impl( // Revoked means the platform's keys are compromised — reject unconditionally, // regardless of policy. This is a security invariant, not a policy decision. if final_status.status == TcbStatus::Revoked { - bail!("TCB status is Revoked: platform keys are compromised"); + bail!("TCB status is invalid: Revoked"); } // Validate report attributes (debug mode check, etc.) From 4f0a2713f7d386fe174d2aa8badda47e5d83ef1d Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 10 Mar 2026 11:09:53 +0000 Subject: [PATCH 14/33] fix: restore js_verify and js_verify_with_root_ca WASM bindings The two-phase API refactor removed these functions. Re-add them using QuoteVerifier + into_report_unchecked(), matching the test harness behavior. Add rustcrypto to the js feature since CryptoBackend is now required. --- Cargo.toml | 2 +- src/verify.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index cc68dfb..77a9d03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,7 +111,7 @@ std = [ borsh = ["dep:borsh"] borsh_schema = ["borsh", "borsh/unstable__schema"] report = ["std", "tracing", "futures", "reqwest"] -js = ["getrandom/js", "serde-wasm-bindgen", "wasm-bindgen"] +js = ["getrandom/js", "serde-wasm-bindgen", "wasm-bindgen", "rustcrypto"] python = ["pyo3", "pyo3-async-runtimes", "tokio", "std", "report", "ring"] go = ["std", "ring", "serde_json"] ring = ["dep:ring", "dcap-qvl-webpki/ring", "_anycrypto"] diff --git a/src/verify.rs b/src/verify.rs index e635f5b..c1b6fc3 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -311,6 +311,59 @@ impl QuoteVerifier { } } +#[cfg(feature = "js")] +#[wasm_bindgen] +pub fn js_verify( + raw_quote: JsValue, + quote_collateral: JsValue, + now: u64, +) -> Result { + let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) + .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; + let quote_collateral = serde_wasm_bindgen::from_value::(quote_collateral)?; + + let verifier = QuoteVerifier::new_prod(default_crypto::backend()); + let verified_report = verifier + .verify(&raw_quote, quote_collateral, now) + .map(|r| r.into_report_unchecked()) + .map_err(|e| { + let error_msg = format_error_chain(&e); + serde_wasm_bindgen::to_value(&error_msg) + .unwrap_or_else(|_| JsValue::from_str("Failed to encode Error")) + })?; + + serde_wasm_bindgen::to_value(&verified_report) + .map_err(|_| JsValue::from_str("Failed to encode verified_report")) +} + +#[cfg(feature = "js")] +#[wasm_bindgen] +pub fn js_verify_with_root_ca( + raw_quote: JsValue, + quote_collateral: JsValue, + root_ca_der: JsValue, + now: u64, +) -> Result { + let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) + .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; + let quote_collateral = serde_wasm_bindgen::from_value::(quote_collateral)?; + let root_ca_der: Vec = serde_wasm_bindgen::from_value(root_ca_der) + .map_err(|_| JsValue::from_str("Failed to decode root_ca_der"))?; + + let verifier = QuoteVerifier::new(root_ca_der, default_crypto::backend()); + let verified_report = verifier + .verify(&raw_quote, quote_collateral, now) + .map(|r| r.into_report_unchecked()) + .map_err(|e| { + let error_msg = format_error_chain(&e); + serde_wasm_bindgen::to_value(&error_msg) + .unwrap_or_else(|_| JsValue::from_str("Failed to encode Error")) + })?; + + serde_wasm_bindgen::to_value(&verified_report) + .map_err(|_| JsValue::from_str("Failed to encode verified_report")) +} + #[cfg(feature = "js")] #[wasm_bindgen] pub async fn js_get_collateral(pccs_url: JsValue, raw_quote: JsValue) -> Result { From d31f9e1401d3051883a32f08f21d671fb238cd32 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 02:01:59 +0000 Subject: [PATCH 15/33] fix: separate QE grace from platform grace --- docs/policy.md | 7 +- python-bindings/python/dcap_qvl/_dcap_qvl.pyi | 7 +- src/policy/simple.rs | 156 ++++++++++++++---- src/python.rs | 10 ++ 4 files changed, 147 insertions(+), 33 deletions(-) diff --git a/docs/policy.md b/docs/policy.md index 425e536..cce88e2 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -70,6 +70,7 @@ let policy = SimplePolicy::strict(now) | 2 | **Advisory ID whitelist** | Empty set (reject any) | `.accept_advisory(...)` | | 3 | **Collateral expiration** | `earliest_expiration >= now` | `.collateral_grace_period(Duration)` | | 4 | **Platform TCB freshness** | Only for OutOfDate statuses | `.platform_grace_period(Duration)` | +| 4b | **QE TCB freshness** | Only for QE `OutOfDate` | `.qe_grace_period(Duration)` | | 5 | **Min TCB eval data number** | Skip | `.min_tcb_eval_data_number(n)` | | 6 | **Dynamic platform flag** | Reject `True` | `.allow_dynamic_platform(true)` | | 7 | **Cached keys flag** | Reject `True` | `.allow_cached_keys(true)` | @@ -80,9 +81,11 @@ let policy = SimplePolicy::strict(now) **Collateral grace** (`collateral_grace_period`): Extends the collateral expiration window. If `earliest_expiration + grace >= now`, the quote is accepted. Does **not** skip advisory checks — stale collateral doesn't invalidate advisory data. -**Platform grace** (`platform_grace_period`): For `OutOfDate` / `OutOfDateConfigurationNeeded` statuses, checks `tcb_date_tag + grace >= now`. For pure `OutOfDate`, advisory checks are skipped during the grace window (the advisories are inherent to the out-of-date level). For `OutOfDateConfigurationNeeded`, advisories are still checked (the Configuration advisories are unrelated to the OutOfDate aspect). +**Platform grace** (`platform_grace_period`): Applies only to the **platform** TCB level. For `OutOfDate` / `OutOfDateConfigurationNeeded`, checks `platform.tcb_date_tag + grace >= now`. For pure `OutOfDate`, only the **platform** advisories are skipped during the grace window. For `OutOfDateConfigurationNeeded`, platform advisories are still checked. -The two grace periods are **mutually exclusive** — setting both to non-zero causes a validation error. +**QE grace** (`qe_grace_period`): Applies only to the **QE** TCB level. For QE `OutOfDate`, checks `qe.tcb_level.tcb_date + grace >= now`. QE advisories are skipped only while this QE grace is active. + +`collateral_grace_period` is **mutually exclusive** with the TCB grace windows — setting it together with `platform_grace_period` or `qe_grace_period` causes a validation error. ### Platform Flags (Three-State) diff --git a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi index 2fa5590..66db6ab 100644 --- a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi +++ b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi @@ -324,7 +324,8 @@ class PySimplePolicy: policy = SimplePolicy.strict(now_secs) \\ .allow_status("SWHardeningNeeded") \\ .accept_advisory("INTEL-SA-00334") \\ - .collateral_grace_period(90 * 24 * 3600) + .collateral_grace_period(90 * 24 * 3600) \\ + .qe_grace_period(7 * 24 * 3600) """ @staticmethod @@ -348,6 +349,10 @@ class PySimplePolicy: """Set platform grace period in seconds.""" ... + def qe_grace_period(self, secs: int) -> "PySimplePolicy": + """Set QE grace period in seconds.""" + ... + def min_tcb_eval_data_number(self, min: int) -> "PySimplePolicy": """Set minimum TCB evaluation data number.""" ... diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 9434201..720ed9b 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -41,6 +41,7 @@ pub struct SimplePolicy { now: u64, collateral_grace_period: u64, platform_grace_period: u64, + qe_grace_period: u64, // TCB evaluation min_tcb_eval_data_number: Option, @@ -85,6 +86,7 @@ impl SimplePolicy { now, collateral_grace_period: 0, platform_grace_period: 0, + qe_grace_period: 0, min_tcb_eval_data_number: None, accepted_advisory_ids: Vec::new(), allow_dynamic_platform: false, @@ -125,6 +127,13 @@ impl SimplePolicy { self } + /// Set QE grace period (default: zero). When QE TCB status is `OutOfDate`, + /// accepts quotes where `qe_tcb_level.tcb_date + grace_period >= now`. + pub fn qe_grace_period(mut self, duration: Duration) -> Self { + self.qe_grace_period = duration.as_secs(); + self + } + /// Set minimum TCB evaluation data number. Rejects quotes with /// `tcb_eval_data_number` below this threshold. pub fn min_tcb_eval_data_number(mut self, min: u32) -> Self { @@ -177,6 +186,16 @@ impl SimplePolicy { impl Policy for SimplePolicy { fn validate(&self, data: &SupplementalData) -> Result<()> { + fn within_grace(date_tag: u64, grace_period: u64, now: u64) -> bool { + date_tag.saturating_add(grace_period) >= now + } + + fn advisory_accepted(accepted_advisory_ids: &[String], id: &str) -> bool { + accepted_advisory_ids + .iter() + .any(|a| a.eq_ignore_ascii_case(id)) + } + // 1. TCB status whitelist if !self.is_status_acceptable(data.tcb.status) { bail!( @@ -185,9 +204,13 @@ impl Policy for SimplePolicy { ); } - // 3 & 4. Grace periods (mutually exclusive) - if self.collateral_grace_period > 0 && self.platform_grace_period > 0 { - bail!("collateral_grace_period and platform_grace_period are mutually exclusive"); + // 3 & 4. Grace periods + if self.collateral_grace_period > 0 + && (self.platform_grace_period > 0 || self.qe_grace_period > 0) + { + bail!( + "collateral_grace_period is mutually exclusive with platform_grace_period and qe_grace_period" + ); } // 3. Collateral expiration: earliest_expiration + grace >= now @@ -204,19 +227,18 @@ impl Policy for SimplePolicy { ); } - // 4. Platform TCB freshness: tcb_date_tag + grace >= now - // Only checked when TCB status indicates the platform is out-of-date. - let is_out_of_date = matches!( - data.tcb.status, + // 4. Platform TCB freshness: platform tcb_date_tag + grace >= now. + let platform_is_out_of_date = matches!( + data.platform.tcb_level.tcb_status, TcbStatus::OutOfDate | TcbStatus::OutOfDateConfigurationNeeded ); - if is_out_of_date - && data - .platform - .tcb_date_tag - .saturating_add(self.platform_grace_period) - < self.now - { + let platform_in_grace = platform_is_out_of_date + && within_grace( + data.platform.tcb_date_tag, + self.platform_grace_period, + self.now, + ); + if platform_is_out_of_date && !platform_in_grace { bail!( "Platform TCB too old: tcb_date_tag {} + grace {} < now {}", data.platform.tcb_date_tag, @@ -225,22 +247,40 @@ impl Policy for SimplePolicy { ); } - // Determine if we're within a platform grace window — advisory checks are skipped - // only for pure OutOfDate during platform grace. Collateral grace does NOT skip - // advisories (stale collateral doesn't invalidate advisory data). - // OutOfDateConfigurationNeeded does NOT skip either (Configuration advisories - // are unrelated to the OutOfDate grace window). - let in_platform_grace = - self.platform_grace_period > 0 && data.tcb.status == TcbStatus::OutOfDate; - - // 2. Advisory ID whitelist (skipped only during platform grace for pure OutOfDate) - if !in_platform_grace { - for id in &data.tcb.advisory_ids { - if !self - .accepted_advisory_ids - .iter() - .any(|a| a.eq_ignore_ascii_case(id)) - { + // 4b. QE TCB freshness: QE tcb_date + grace >= now. + let qe_tcb_date_tag = chrono::DateTime::parse_from_rfc3339(&data.qe.tcb_level.tcb_date) + .map_err(|e| anyhow::anyhow!("Failed to parse QE TCB date: {e}"))? + .timestamp() as u64; + let qe_is_out_of_date = data.qe.tcb_level.tcb_status == TcbStatus::OutOfDate; + let qe_in_grace = + qe_is_out_of_date && within_grace(qe_tcb_date_tag, self.qe_grace_period, self.now); + if qe_is_out_of_date && !qe_in_grace { + bail!( + "QE TCB too old: tcb_date_tag {} + grace {} < now {}", + qe_tcb_date_tag, + self.qe_grace_period, + self.now + ); + } + + // 2. Advisory ID whitelist. + // Platform grace skips only platform advisories for pure OutOfDate. + // QE grace skips only QE advisories for pure OutOfDate. + let skip_platform_advisories = + platform_in_grace && data.platform.tcb_level.tcb_status == TcbStatus::OutOfDate; + let skip_qe_advisories = + qe_in_grace && data.qe.tcb_level.tcb_status == TcbStatus::OutOfDate; + + if !skip_platform_advisories { + for id in &data.platform.tcb_level.advisory_ids { + if !advisory_accepted(&self.accepted_advisory_ids, id) { + bail!("Advisory ID {id} is not in the accepted set"); + } + } + } + if !skip_qe_advisories { + for id in &data.qe.tcb_level.advisory_ids { + if !advisory_accepted(&self.accepted_advisory_ids, id) { bail!("Advisory ID {id} is not in the accepted set"); } } @@ -311,6 +351,8 @@ pub struct SimplePolicyConfig { #[serde(default)] pub platform_grace_period_secs: u64, #[serde(default)] + pub qe_grace_period_secs: u64, + #[serde(default)] pub min_tcb_eval_data_number: u32, #[serde(default)] pub allow_dynamic_platform: bool, @@ -347,6 +389,9 @@ impl SimplePolicyConfig { policy = policy.platform_grace_period(Duration::from_secs(self.platform_grace_period_secs)); } + if self.qe_grace_period_secs > 0 { + policy = policy.qe_grace_period(Duration::from_secs(self.qe_grace_period_secs)); + } if self.min_tcb_eval_data_number > 0 { policy = policy.min_tcb_eval_data_number(self.min_tcb_eval_data_number); } @@ -491,6 +536,7 @@ mod tests { fn policy_rejects_unknown_advisory() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = SimplePolicy::strict(1_702_000_000); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("INTEL-SA-00615"), "{err}"); @@ -500,6 +546,7 @@ mod tests { fn policy_accepts_whitelisted_advisory() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = SimplePolicy::strict(1_702_000_000).accept_advisory("INTEL-SA-00615"); assert!(policy.validate(&data).is_ok()); } @@ -508,6 +555,7 @@ mod tests { fn policy_advisory_case_insensitive() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = SimplePolicy::strict(1_702_000_000).accept_advisory("intel-sa-00615"); assert!(policy.validate(&data).is_ok()); } @@ -623,6 +671,16 @@ mod tests { assert!(err.contains("mutually exclusive"), "{err}"); } + #[test] + fn policy_collateral_grace_mutually_exclusive_with_qe_grace() { + let data = make_test_supplemental(UpToDate); + let policy = SimplePolicy::strict(1_702_000_000) + .collateral_grace_period(Duration::from_secs(100)) + .qe_grace_period(Duration::from_secs(100)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("mutually exclusive"), "{err}"); + } + // -- min_tcb_eval_data_number -- #[test] @@ -720,6 +778,7 @@ mod tests { fn policy_advisory_checked_during_collateral_grace() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; // Collateral grace doesn't skip advisory checks — stale collateral // doesn't invalidate advisory data. let policy = SimplePolicy::strict(1_704_000_000) @@ -732,6 +791,7 @@ mod tests { fn policy_advisory_skipped_during_platform_grace() { let mut data = make_test_supplemental(OutOfDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; // now=1_702_000_000, tcb_date_tag=1_690_000_000 (old), // grace=13_000_000 → within grace → advisories skipped let policy = SimplePolicy::strict(1_702_000_000) @@ -746,6 +806,7 @@ mod tests { // because the Configuration advisories are unrelated to the OutOfDate grace. let mut data = make_test_supplemental(OutOfDateConfigurationNeeded); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = SimplePolicy::strict(1_702_000_000) .allow_status(OutOfDateConfigurationNeeded) .platform_grace_period(Duration::from_secs(13_000_000)); @@ -757,9 +818,44 @@ mod tests { fn policy_advisory_checked_without_grace() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; // No grace period → advisories checked normally let policy = SimplePolicy::strict(1_702_000_000); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("INTEL-SA-00615"), "{err}"); } + + #[test] + fn policy_platform_grace_does_not_cover_qe_out_of_date() { + let mut data = make_test_supplemental(OutOfDate); + data.platform.tcb_level.tcb_status = UpToDate; + data.platform.tcb_level.advisory_ids = vec![]; + data.platform.tcb_date_tag = 1_702_000_000; + data.qe.tcb_level.tcb_status = OutOfDate; + data.qe.tcb_level.tcb_date = "2023-07-22T00:00:00Z".to_string(); + data.qe.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + + let policy = SimplePolicy::strict(1_702_000_000) + .allow_status(OutOfDate) + .platform_grace_period(Duration::from_secs(13_000_000)); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("QE TCB too old"), "{err}"); + } + + #[test] + fn policy_qe_grace_accepts_qe_out_of_date() { + let mut data = make_test_supplemental(OutOfDate); + data.platform.tcb_level.tcb_status = UpToDate; + data.platform.tcb_level.advisory_ids = vec![]; + data.qe.tcb_level.tcb_status = OutOfDate; + data.qe.tcb_level.tcb_date = "2023-07-22T00:00:00Z".to_string(); + data.qe.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; + + let policy = SimplePolicy::strict(1_702_000_000) + .allow_status(OutOfDate) + .qe_grace_period(Duration::from_secs(13_000_000)); + assert!(policy.validate(&data).is_ok()); + } } diff --git a/src/python.rs b/src/python.rs index 692a9cf..46e5313 100644 --- a/src/python.rs +++ b/src/python.rs @@ -499,6 +499,16 @@ impl PySimplePolicy { } } + /// Set QE grace period in seconds. + fn qe_grace_period(&self, secs: u64) -> Self { + Self { + inner: self + .inner + .clone() + .qe_grace_period(Duration::from_secs(secs)), + } + } + /// Set minimum TCB evaluation data number. fn min_tcb_eval_data_number(&self, min: u32) -> Self { Self { From 482995f7163d8a75bb968b7a659467053cfee9af Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 02:22:43 +0000 Subject: [PATCH 16/33] fix: preserve verified PCK chain for supplemental data --- src/verify.rs | 19 ++++++++++++------- tests/verify_quote.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/verify.rs b/src/verify.rs index c1b6fc3..d746800 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -98,6 +98,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub struct QuoteVerificationResult { report: Report, collateral: QuoteCollateralV3, + pck_cert_chain_der: Vec>, // -- core verification results (always computed) -- tee_type: u32, tcb_status: TcbStatus, @@ -123,13 +124,11 @@ impl QuoteVerificationResult { .context("Failed to parse TcbInfo for supplemental")?; let qe_identity: QeIdentity = serde_json::from_str(&self.collateral.qe_identity) .context("Failed to parse QeIdentity for supplemental")?; - - // Extract PCK cert chain from collateral for time window - let pck_certs = if let Some(pem_chain) = &self.collateral.pck_certificate_chain { - extract_certs(pem_chain.as_bytes()).unwrap_or_default() - } else { - Vec::new() - }; + let pck_certs: Vec> = self + .pck_cert_chain_der + .iter() + .map(|cert| CertificateDer::from(cert.as_slice())) + .collect(); let collateral_dates = compute_collateral_time_window(&self.collateral, &pck_certs, &tcb_info, &qe_identity)?; @@ -542,6 +541,10 @@ fn verify_pck_cert_chain( }); Ok(PckCertChainResult { + pck_cert_chain_der: certification_certs + .iter() + .map(|cert| cert.as_ref().to_vec()) + .collect(), pck_leaf_der: pck_leaf.as_ref().to_vec(), ppid: pck_ext.ppid, cpu_svn: pck_ext.cpu_svn, @@ -558,6 +561,7 @@ fn verify_pck_cert_chain( /// Result from PCK certificate chain verification struct PckCertChainResult { + pck_cert_chain_der: Vec>, pck_leaf_der: Vec, ppid: Vec, cpu_svn: [u8; 16], @@ -887,6 +891,7 @@ fn verify_impl( Ok(QuoteVerificationResult { report: quote.report, collateral, + pck_cert_chain_der: pck_result.pck_cert_chain_der.clone(), tee_type: quote.header.tee_type, tcb_status: final_status.status, advisory_ids: final_status.advisory_ids, diff --git a/tests/verify_quote.rs b/tests/verify_quote.rs index 5077e56..b585383 100644 --- a/tests/verify_quote.rs +++ b/tests/verify_quote.rs @@ -245,6 +245,50 @@ fn sgx_supplemental_data_cross_validation() { assert_eq!(s.platform.pck.smt_enabled, rc.platform.pck.smt_enabled); } +#[test] +fn supplemental_uses_quote_embedded_pck_chain_when_collateral_omits_it() { + use dcap_qvl::verify::{ring, QuoteVerifier}; + + let raw_quote = include_bytes!("../sample/sgx_quote").to_vec(); + let collateral_without_chain: QuoteCollateralV3 = + serde_json::from_slice(include_bytes!("../sample/sgx_quote_collateral.json")).unwrap(); + assert!(collateral_without_chain.pck_certificate_chain.is_none()); + let now = now_from_collateral(&collateral_without_chain); + + let parsed_quote = Quote::decode(&mut &raw_quote[..]).unwrap(); + let embedded_pem = String::from_utf8_lossy(parsed_quote.raw_cert_chain().unwrap()) + .trim_end_matches('\0') + .to_string(); + + let mut collateral_with_chain = collateral_without_chain.clone(); + collateral_with_chain.pck_certificate_chain = Some(embedded_pem); + + let verifier = QuoteVerifier::new_prod(ring::backend()); + let without_chain = verifier + .verify(&raw_quote, collateral_without_chain, now) + .unwrap() + .supplemental() + .unwrap(); + let with_chain = verifier + .verify(&raw_quote, collateral_with_chain, now) + .unwrap() + .supplemental() + .unwrap(); + + assert_eq!( + without_chain.earliest_issue_date, + with_chain.earliest_issue_date + ); + assert_eq!( + without_chain.latest_issue_date, + with_chain.latest_issue_date + ); + assert_eq!( + without_chain.earliest_expiration_date, + with_chain.earliest_expiration_date + ); +} + #[test] fn could_parse_tdx_quote() { let raw_quote = include_bytes!("../sample/tdx_quote"); From 8fb59a0b74f52903aa26e4a432a9ca0dea26bce7 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 02:23:32 +0000 Subject: [PATCH 17/33] docs: note upstream TODO for platform_provider_id --- src/policy/mod.rs | 10 ++++++++++ src/verify.rs | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/policy/mod.rs b/src/policy/mod.rs index 2eff41f..43aba3d 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -167,5 +167,15 @@ pub struct PckIdentity { /// SMT (hyperthreading) flag. pub smt_enabled: PckCertFlag, /// Platform Provider ID (Platform CA only, for Rego). + /// + /// Note: Intel's upstream DCAP Rego policy checks this field, but the + /// upstream QvE measurement producer currently leaves it unpopulated + /// (`//obj_plat_tcb.AddMember("platform_provider_id", , allocator);`). + /// + /// Upstream references: + /// - Rego check: + /// + /// - QvE TODO: + /// pub platform_provider_id: Option, } diff --git a/src/verify.rs b/src/verify.rs index d746800..90a1542 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -177,6 +177,11 @@ impl QuoteVerificationResult { dynamic_platform: self.pck_ext.dynamic_platform, cached_keys: self.pck_ext.cached_keys, smt_enabled: self.pck_ext.smt_enabled, + // Intel's upstream DCAP Rego policy checks + // `platform_provider_id`, but the upstream QvE producer + // currently leaves it as a TODO when building the platform + // measurement JSON: + // https://github.com/intel/confidential-computing.tee.dcap/blob/main/ae/QvE/qve/qve.cpp platform_provider_id: None, }, root_key_id, From 656626118d6bd51d8860a309b53e9e1a9aa0749f Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 02:38:18 +0000 Subject: [PATCH 18/33] chore: fix all-features clippy warnings --- src/ffi.rs | 15 +++--- src/policy/rego.rs | 6 ++- src/python.rs | 72 +++++++++++++++----------- tests/verify_quote.rs | 7 ++- tests/verify_tcb_override.rs | 97 ++++++++++++++++++++++++------------ 5 files changed, 124 insertions(+), 73 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index b0ea81c..e9e608e 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -302,11 +302,12 @@ pub unsafe extern "C" fn dcap_parse_quote_cb( let quote_type = if parsed.header.is_sgx() { "SGX" } else { "TDX" }; let cert_chain_pem = parsed.raw_cert_chain().ok().map(|raw| { - let mut end = raw.len(); - while end > 0 && raw[end.saturating_sub(1)] == 0 { - end = end.saturating_sub(1); - } - String::from_utf8_lossy(&raw[..end]).into_owned() + let trimmed = raw + .iter() + .rposition(|byte| *byte != 0) + .and_then(|end| raw.get(..=end)) + .unwrap_or(&[]); + String::from_utf8_lossy(trimmed).into_owned() }); // For cert_type 5: extract fmspc/ca from embedded cert chain @@ -548,9 +549,7 @@ pub unsafe extern "C" fn dcap_parse_pck_extension_from_pem_cb( pce_id: ext.pce_id.to_vec(), fmspc: ext.fmspc.to_vec(), sgx_type: ext.sgx_type, - platform_instance_id: ext - .platform_instance_id - .map(|v| serde_bytes::ByteBuf::from(v)), + platform_instance_id: ext.platform_instance_id.map(serde_bytes::ByteBuf::from), raw_extension: ext.raw_extension, }; diff --git a/src/policy/rego.rs b/src/policy/rego.rs index b9a358d..5f7d955 100644 --- a/src/policy/rego.rs +++ b/src/policy/rego.rs @@ -618,7 +618,11 @@ fn to_rego_qvl_result(data: &SupplementalData) -> Vec { } #[cfg(test)] -#[allow(clippy::unwrap_used)] +#[allow( + clippy::unwrap_used, + clippy::indexing_slicing, + clippy::manual_range_contains +)] mod tests { use super::*; use crate::policy::{PckCertFlag, PckIdentity, PlatformInfo, QeInfo, TcbVerdict}; diff --git a/src/python.rs b/src/python.rs index 46e5313..f85cc0b 100644 --- a/src/python.rs +++ b/src/python.rs @@ -2,7 +2,7 @@ use core::time::Duration; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::PyBytes; +use pyo3::types::{PyBytes, PyDict, PyTuple}; use pyo3_async_runtimes::tokio::future_into_py; use serde_json; @@ -25,31 +25,42 @@ pub struct PyQuoteCollateralV3 { #[pymethods] impl PyQuoteCollateralV3 { #[new] - fn new( - pck_crl_issuer_chain: String, - root_ca_crl: Vec, - pck_crl: Vec, - tcb_info_issuer_chain: String, - tcb_info: String, - tcb_info_signature: Vec, - qe_identity_issuer_chain: String, - qe_identity: String, - qe_identity_signature: Vec, - ) -> Self { - Self { + fn new(args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult { + fn get_arg( + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, + index: usize, + name: &str, + ) -> PyResult + where + T: for<'py> pyo3::FromPyObject<'py>, + { + if let Ok(value) = args.get_item(index) { + return value.extract::(); + } + let value = kwargs + .and_then(|kw| kw.get_item(name).transpose()) + .transpose()? + .ok_or_else(|| { + PyValueError::new_err(format!("missing required argument: {name}")) + })?; + value.extract::() + } + + Ok(Self { inner: 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_crl_issuer_chain: get_arg(args, kwargs, 0, "pck_crl_issuer_chain")?, + root_ca_crl: get_arg(args, kwargs, 1, "root_ca_crl")?, + pck_crl: get_arg(args, kwargs, 2, "pck_crl")?, + tcb_info_issuer_chain: get_arg(args, kwargs, 3, "tcb_info_issuer_chain")?, + tcb_info: get_arg(args, kwargs, 4, "tcb_info")?, + tcb_info_signature: get_arg(args, kwargs, 5, "tcb_info_signature")?, + qe_identity_issuer_chain: get_arg(args, kwargs, 6, "qe_identity_issuer_chain")?, + qe_identity: get_arg(args, kwargs, 7, "qe_identity")?, + qe_identity_signature: get_arg(args, kwargs, 8, "qe_identity_signature")?, pck_certificate_chain: None, }, - } + }) } #[getter] @@ -629,11 +640,12 @@ impl PyQuote { Ok(v) => v, Err(_) => return Ok(None), }; - let mut end = raw.len(); - while end > 0 && raw[end - 1] == 0 { - end -= 1; - } - Ok(Some(PyBytes::new(py, &raw[..end]).into())) + let trimmed = raw + .iter() + .rposition(|byte| *byte != 0) + .and_then(|end| raw.get(..=end)) + .unwrap_or(&[]); + Ok(Some(PyBytes::new(py, trimmed).into())) } /// Parse the Intel SGX extension from the leaf PCK certificate. @@ -681,8 +693,8 @@ impl PyQuoteVerificationResult { /// Get VerifiedReport without policy validation. Consumes the result. /// /// WARNING: Skips all policy checks. Use only when you handle validation externally. - fn into_report_unchecked(&mut self) -> PyResult { - let result = self + fn into_report_unchecked(mut slf: PyRefMut<'_, Self>) -> PyResult { + let result = slf .inner .take() .ok_or_else(|| PyValueError::new_err("verification result already consumed"))?; diff --git a/tests/verify_quote.rs b/tests/verify_quote.rs index b585383..8142434 100644 --- a/tests/verify_quote.rs +++ b/tests/verify_quote.rs @@ -1,4 +1,9 @@ -#![allow(clippy::unwrap_used, clippy::expect_used)] +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + clippy::arithmetic_side_effects +)] use dcap_qvl::{quote::Quote, verify::VerifiedReport, QuoteCollateralV3}; use der::Decode as DerDecode; diff --git a/tests/verify_tcb_override.rs b/tests/verify_tcb_override.rs index b4f59b6..7044377 100644 --- a/tests/verify_tcb_override.rs +++ b/tests/verify_tcb_override.rs @@ -1,4 +1,9 @@ -#![allow(clippy::unwrap_used, clippy::expect_used)] +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + clippy::arithmetic_side_effects +)] #[cfg(feature = "danger-allow-tcb-override")] mod tests { @@ -17,14 +22,23 @@ mod tests { collateral: &QuoteCollateralV3, now_secs: u64, ) -> anyhow::Result { - use dcap_qvl::verify::{ring, rustcrypto}; - let ring_result = ring::verify(raw_quote, collateral, now_secs); - let rustcrypto_result = rustcrypto::verify(raw_quote, collateral, now_secs); + use dcap_qvl::verify::{ring, rustcrypto, QuoteVerifier}; + let ring_verifier = QuoteVerifier::new_prod(ring::backend()); + let rustcrypto_verifier = QuoteVerifier::new_prod(rustcrypto::backend()); + + let ring_result = ring_verifier + .verify(raw_quote, collateral.clone(), now_secs) + .map(|result| result.into_report_unchecked()); + let rustcrypto_result = rustcrypto_verifier + .verify(raw_quote, collateral.clone(), now_secs) + .map(|result| result.into_report_unchecked()); assert_eq!( ring_result.map_err(|e| e.to_string()), rustcrypto_result.map_err(|e| e.to_string()) ); - ring::verify(raw_quote, collateral, now_secs) + ring_verifier + .verify(raw_quote, collateral.clone(), now_secs) + .map(|result| result.into_report_unchecked()) } fn dangerous_verify_with_tcb_override( @@ -36,24 +50,38 @@ mod tests { where F: FnOnce(TcbInfo) -> TcbInfo + Copy, { - use dcap_qvl::verify::{ring, rustcrypto}; - let ring_result = ring::dangerous_verify_with_tcb_override( - raw_quote, - collateral, - now_secs, - override_tcb_info, - ); - let rustcrypto_result = rustcrypto::dangerous_verify_with_tcb_override( - raw_quote, - collateral, - now_secs, - override_tcb_info, - ); + use dcap_qvl::verify::{ring, rustcrypto, QuoteVerifier}; + let ring_verifier = QuoteVerifier::new_prod(ring::backend()); + let rustcrypto_verifier = QuoteVerifier::new_prod(rustcrypto::backend()); + + let ring_result = ring_verifier + .dangerous_verify_with_tcb_override( + raw_quote, + collateral.clone(), + now_secs, + override_tcb_info, + ) + .map(|result| result.into_report_unchecked()); + let rustcrypto_result = rustcrypto_verifier + .dangerous_verify_with_tcb_override( + raw_quote, + collateral.clone(), + now_secs, + override_tcb_info, + ) + .map(|result| result.into_report_unchecked()); assert_eq!( ring_result.map_err(|e| e.to_string()), rustcrypto_result.map_err(|e| e.to_string()) ); - ring::dangerous_verify_with_tcb_override(raw_quote, collateral, now_secs, override_tcb_info) + ring_verifier + .dangerous_verify_with_tcb_override( + raw_quote, + collateral.clone(), + now_secs, + override_tcb_info, + ) + .map(|result| result.into_report_unchecked()) } fn force_out_of_date(mut tcb_info: TcbInfo) -> TcbInfo { @@ -135,7 +163,7 @@ mod tests { #[test] fn override_can_change_tcb_result_and_runs_once() { - use dcap_qvl::verify::ring; + use dcap_qvl::verify::{ring, QuoteVerifier}; let raw_quote = include_bytes!("../sample/tdx_quote"); let raw_quote_collateral = include_bytes!("../sample/tdx_quote_collateral.json"); @@ -148,19 +176,22 @@ mod tests { static OVERRIDE_CALLS: AtomicUsize = AtomicUsize::new(0); OVERRIDE_CALLS.store(0, Ordering::SeqCst); - let ring_overridden = ring::dangerous_verify_with_tcb_override( - raw_quote, - "e_collateral, - now, - |mut tcb_info| { - OVERRIDE_CALLS.fetch_add(1, Ordering::SeqCst); - for level in &mut tcb_info.tcb_levels { - level.tcb_status = TcbStatus::OutOfDate; - } - tcb_info - }, - ) - .expect("verify with override"); + let ring_verifier = QuoteVerifier::new_prod(ring::backend()); + let ring_overridden = ring_verifier + .dangerous_verify_with_tcb_override( + raw_quote, + quote_collateral.clone(), + now, + |mut tcb_info| { + OVERRIDE_CALLS.fetch_add(1, Ordering::SeqCst); + for level in &mut tcb_info.tcb_levels { + level.tcb_status = TcbStatus::OutOfDate; + } + tcb_info + }, + ) + .map(|result| result.into_report_unchecked()) + .expect("verify with override"); assert_eq!(OVERRIDE_CALLS.load(Ordering::SeqCst), 1); assert_eq!(ring_overridden.status, "OutOfDate"); From e1e05e566e2bdc8e349233799d1910c9744cabc5 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 02:47:08 +0000 Subject: [PATCH 19/33] fix: accept kwargs in python collateral constructor --- src/python.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python.rs b/src/python.rs index f85cc0b..d053ec4 100644 --- a/src/python.rs +++ b/src/python.rs @@ -25,6 +25,7 @@ pub struct PyQuoteCollateralV3 { #[pymethods] impl PyQuoteCollateralV3 { #[new] + #[pyo3(signature = (*args, **kwargs))] fn new(args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult { fn get_arg( args: &Bound<'_, PyTuple>, From 24153157e884cf9a7f218ad4b423ca95d8d6fa7b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 03:08:36 +0000 Subject: [PATCH 20/33] feat: two-phase WASM API with QuoteVerifier, SimplePolicy, QuoteVerificationResult Replace flat js_verify/js_verify_with_root_ca/js_get_collateral globals with class-based API matching the Rust two-phase pattern: const verifier = new QuoteVerifier(); // or new QuoteVerifier(rootCaDer) const result = verifier.verify(quote, collateral, now); const report = result.validate(policy); // or result.into_report_unchecked() const policy = new SimplePolicy(now) .allow_status("OutOfDate") .allow_smt(true); const collateral = await QuoteVerifier.get_collateral(pccsUrl, quote); Add rustcrypto to js feature since CryptoBackend is now required. --- src/verify.rs | 258 +++++++++++++++++++++++------- tests/js/verify_quote_node.js | 14 +- tests/js/verify_quote_web.js | 12 +- tests/js/verify_quote_web_test.js | 60 +++---- tests/test_case.js | 16 +- 5 files changed, 251 insertions(+), 109 deletions(-) diff --git a/src/verify.rs b/src/verify.rs index 90a1542..a5f7e64 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -315,72 +315,216 @@ impl QuoteVerifier { } } +/// Verification policy builder for JS/WASM. +/// +/// ```js +/// const policy = new SimplePolicy(now) +/// .allow_status("OutOfDate") +/// .collateral_grace_period(7n * 86400n) +/// .allow_smt(true); +/// ``` #[cfg(feature = "js")] -#[wasm_bindgen] -pub fn js_verify( - raw_quote: JsValue, - quote_collateral: JsValue, - now: u64, -) -> Result { - let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) - .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; - let quote_collateral = serde_wasm_bindgen::from_value::(quote_collateral)?; - - let verifier = QuoteVerifier::new_prod(default_crypto::backend()); - let verified_report = verifier - .verify(&raw_quote, quote_collateral, now) - .map(|r| r.into_report_unchecked()) - .map_err(|e| { - let error_msg = format_error_chain(&e); - serde_wasm_bindgen::to_value(&error_msg) - .unwrap_or_else(|_| JsValue::from_str("Failed to encode Error")) - })?; - - serde_wasm_bindgen::to_value(&verified_report) - .map_err(|_| JsValue::from_str("Failed to encode verified_report")) +#[wasm_bindgen(js_name = "SimplePolicy")] +pub struct JsSimplePolicy { + inner: crate::policy::SimplePolicy, +} + +#[cfg(feature = "js")] +fn js_parse_tcb_status(s: &str) -> Result { + match s { + "UpToDate" => Ok(TcbStatus::UpToDate), + "SWHardeningNeeded" => Ok(TcbStatus::SWHardeningNeeded), + "ConfigurationNeeded" => Ok(TcbStatus::ConfigurationNeeded), + "ConfigurationAndSWHardeningNeeded" => Ok(TcbStatus::ConfigurationAndSWHardeningNeeded), + "OutOfDate" => Ok(TcbStatus::OutOfDate), + "OutOfDateConfigurationNeeded" => Ok(TcbStatus::OutOfDateConfigurationNeeded), + "Revoked" => Ok(TcbStatus::Revoked), + _ => Err(JsValue::from_str(&alloc::format!("Unknown TCB status: {s}"))), + } } #[cfg(feature = "js")] #[wasm_bindgen] -pub fn js_verify_with_root_ca( - raw_quote: JsValue, - quote_collateral: JsValue, - root_ca_der: JsValue, - now: u64, -) -> Result { - let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) - .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; - let quote_collateral = serde_wasm_bindgen::from_value::(quote_collateral)?; - let root_ca_der: Vec = serde_wasm_bindgen::from_value(root_ca_der) - .map_err(|_| JsValue::from_str("Failed to decode root_ca_der"))?; - - let verifier = QuoteVerifier::new(root_ca_der, default_crypto::backend()); - let verified_report = verifier - .verify(&raw_quote, quote_collateral, now) - .map(|r| r.into_report_unchecked()) - .map_err(|e| { - let error_msg = format_error_chain(&e); - serde_wasm_bindgen::to_value(&error_msg) - .unwrap_or_else(|_| JsValue::from_str("Failed to encode Error")) - })?; - - serde_wasm_bindgen::to_value(&verified_report) - .map_err(|_| JsValue::from_str("Failed to encode verified_report")) +impl JsSimplePolicy { + /// Create a strict policy: only `UpToDate`, no grace period, no advisory tolerance. + #[wasm_bindgen(constructor)] + pub fn strict(now_secs: u64) -> Self { + Self { + inner: crate::policy::SimplePolicy::strict(now_secs), + } + } + + /// Allow an additional TCB status (e.g. "OutOfDate", "SWHardeningNeeded"). + pub fn allow_status(self, status: &str) -> Result { + let s = js_parse_tcb_status(status)?; + Ok(Self { + inner: self.inner.allow_status(s), + }) + } + + /// Accept a specific advisory ID (e.g. "INTEL-SA-00334"). + pub fn accept_advisory(self, id: &str) -> Self { + Self { + inner: self.inner.accept_advisory(id), + } + } + + /// Set collateral grace period in seconds. + pub fn collateral_grace_period(self, secs: u64) -> Self { + Self { + inner: self + .inner + .collateral_grace_period(Duration::from_secs(secs)), + } + } + + /// Set platform grace period in seconds. + pub fn platform_grace_period(self, secs: u64) -> Self { + Self { + inner: self.inner.platform_grace_period(Duration::from_secs(secs)), + } + } + + /// Set QE grace period in seconds. + pub fn qe_grace_period(self, secs: u64) -> Self { + Self { + inner: self.inner.qe_grace_period(Duration::from_secs(secs)), + } + } + + /// Set minimum TCB evaluation data number. + pub fn min_tcb_eval_data_number(self, min: u32) -> Self { + Self { + inner: self.inner.min_tcb_eval_data_number(min), + } + } + + /// Set whether dynamic platforms are allowed. + pub fn allow_dynamic_platform(self, allow: bool) -> Self { + Self { + inner: self.inner.allow_dynamic_platform(allow), + } + } + + /// Set whether cached keys are allowed. + pub fn allow_cached_keys(self, allow: bool) -> Self { + Self { + inner: self.inner.allow_cached_keys(allow), + } + } + + /// Set whether SMT (hyperthreading) is allowed. + pub fn allow_smt(self, allow: bool) -> Self { + Self { + inner: self.inner.allow_smt(allow), + } + } +} + +/// Result of cryptographic quote verification (phase 1) for JS/WASM. +/// +/// Use `validate(policy)` to apply a [`JsSimplePolicy`] and get a `VerifiedReport`. +/// Use `into_report_unchecked()` to skip policy validation. +#[cfg(feature = "js")] +#[wasm_bindgen(js_name = "QuoteVerificationResult")] +pub struct JsQuoteVerificationResult { + inner: Option, } #[cfg(feature = "js")] #[wasm_bindgen] -pub async fn js_get_collateral(pccs_url: JsValue, raw_quote: JsValue) -> Result { - let pccs_url: String = serde_wasm_bindgen::from_value(pccs_url) - .map_err(|_| JsValue::from_str("Failed to decode pccs_url"))?; - let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) - .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; - - let collateral: QuoteCollateralV3 = crate::collateral::get_collateral(&pccs_url, &raw_quote) - .await - .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; - serde_wasm_bindgen::to_value(&collateral) - .map_err(|_| JsValue::from_str("Failed to encode collateral")) +impl JsQuoteVerificationResult { + /// Validate against a policy, returning a VerifiedReport. Consumes the result. + pub fn validate(&mut self, policy: &JsSimplePolicy) -> Result { + let result = self + .inner + .take() + .ok_or_else(|| JsValue::from_str("verification result already consumed"))?; + let report = result + .validate(&policy.inner) + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + serde_wasm_bindgen::to_value(&report) + .map_err(|_| JsValue::from_str("Failed to encode verified_report")) + } + + /// Get VerifiedReport without policy validation. Consumes the result. + pub fn into_report_unchecked(&mut self) -> Result { + let result = self + .inner + .take() + .ok_or_else(|| JsValue::from_str("verification result already consumed"))?; + serde_wasm_bindgen::to_value(&result.into_report_unchecked()) + .map_err(|_| JsValue::from_str("Failed to encode verified_report")) + } +} + +/// Quote verifier for JS/WASM. +/// +/// ```js +/// const verifier = new QuoteVerifier(); // Intel production root CA +/// const verifier = new QuoteVerifier(rootCaDer); // custom root CA +/// const result = verifier.verify(quote, collateral, now); +/// ``` +#[cfg(feature = "js")] +#[wasm_bindgen(js_name = "QuoteVerifier")] +pub struct JsQuoteVerifier { + inner: QuoteVerifier, +} + +#[cfg(feature = "js")] +#[wasm_bindgen(js_class = "QuoteVerifier")] +impl JsQuoteVerifier { + /// Create a verifier. No argument = Intel production root CA; pass `rootCaDer` for custom. + #[wasm_bindgen(constructor)] + pub fn new(root_ca_der: Option>) -> Self { + let inner = match root_ca_der { + Some(der) => QuoteVerifier::new(der, default_crypto::backend()), + None => QuoteVerifier::new_prod(default_crypto::backend()), + }; + Self { inner } + } + + /// Perform cryptographic verification, returning a [`QuoteVerificationResult`]. + pub fn verify( + &self, + raw_quote: JsValue, + quote_collateral: JsValue, + now: u64, + ) -> Result { + let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) + .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; + let quote_collateral = + serde_wasm_bindgen::from_value::(quote_collateral)?; + + let result = self + .inner + .verify(&raw_quote, quote_collateral, now) + .map_err(|e| { + let error_msg = format_error_chain(&e); + serde_wasm_bindgen::to_value(&error_msg) + .unwrap_or_else(|_| JsValue::from_str("Failed to encode Error")) + })?; + + Ok(JsQuoteVerificationResult { + inner: Some(result), + }) + } + + /// Fetch collateral from a PCCS server. + pub async fn get_collateral( + pccs_url: &str, + raw_quote: JsValue, + ) -> Result { + let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) + .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; + + let collateral: QuoteCollateralV3 = + crate::collateral::get_collateral(pccs_url, &raw_quote) + .await + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + serde_wasm_bindgen::to_value(&collateral) + .map_err(|_| JsValue::from_str("Failed to encode collateral")) + } } // ============================================================================= diff --git a/tests/js/verify_quote_node.js b/tests/js/verify_quote_node.js index bdd9606..11f76b5 100644 --- a/tests/js/verify_quote_node.js +++ b/tests/js/verify_quote_node.js @@ -1,9 +1,6 @@ const fs = require("fs"); const path = require("path"); -const { - js_verify, - js_get_collateral, -} = require("../../pkg/node/dcap-qvl-node"); +const { QuoteVerifier } = require("../../pkg/node/dcap-qvl-node"); // Function to read a file as a Uint8Array function readFileAsUint8Array(filePath) { @@ -22,11 +19,12 @@ const now = BigInt(Math.floor(Date.now() / 1000)); (async () => { try { - // Call the js_verify function let pccs_url = "https://pccs.phala.network/tdx/certification/v4"; - const quoteCollateral = await js_get_collateral(pccs_url, rawQuote); - const result = js_verify(rawQuote, quoteCollateral, now); - console.log("Verification Result:", result); + const quoteCollateral = await QuoteVerifier.get_collateral(pccs_url, rawQuote); + const verifier = new QuoteVerifier(); + const result = verifier.verify(rawQuote, quoteCollateral, now); + const report = result.into_report_unchecked(); + console.log("Verification Result:", report); } catch (error) { console.error("Verification failed:", error); } diff --git a/tests/js/verify_quote_web.js b/tests/js/verify_quote_web.js index a00be0b..a9a1d9a 100644 --- a/tests/js/verify_quote_web.js +++ b/tests/js/verify_quote_web.js @@ -1,4 +1,4 @@ -import init, { js_verify, js_get_collateral } from "/pkg/web/dcap-qvl-web.js"; +import init, { QuoteVerifier } from "/pkg/web/dcap-qvl-web.js"; // Function to fetch a file as a Uint8Array async function fetchFileAsUint8Array(url) { @@ -26,14 +26,16 @@ async function loadFilesAndVerify() { // Get the quote collateral let pccs_url = "https://pccs.phala.network/tdx/certification/v4"; - const quoteCollateral = await js_get_collateral(pccs_url, rawQuote); + const quoteCollateral = await QuoteVerifier.get_collateral(pccs_url, rawQuote); // Current timestamp const now = BigInt(Math.floor(Date.now() / 1000)); - // Call the js_verify function - const result = js_verify(rawQuote, quoteCollateral, now); - console.log("Verification Result:", result); + // Verify + const verifier = new QuoteVerifier(); + const result = verifier.verify(rawQuote, quoteCollateral, now); + const report = result.into_report_unchecked(); + console.log("Verification Result:", report); } catch (error) { console.error("Verification failed:", error); } diff --git a/tests/js/verify_quote_web_test.js b/tests/js/verify_quote_web_test.js index dbf2fff..da89d3f 100644 --- a/tests/js/verify_quote_web_test.js +++ b/tests/js/verify_quote_web_test.js @@ -1,4 +1,4 @@ -import init, { js_verify, js_verify_with_root_ca, js_get_collateral } from "/pkg/web/dcap-qvl-web.js"; +import init, { QuoteVerifier } from "/pkg/web/dcap-qvl-web.js"; const testOutputs = []; let passed = 0; @@ -90,8 +90,9 @@ async function runTests() { const rootCA = await fetchFile('/test_data/certs/root_ca.der'); const now = BigInt(Math.floor(Date.now() / 1000)); - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); - if (!result || !result.status) { + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); + const report = result.into_report_unchecked(); + if (!report || !report.status) { throw new Error('Verification should succeed but got no result'); } }); @@ -103,8 +104,9 @@ async function runTests() { const rootCA = await fetchFile('/test_data/certs/root_ca.der'); const now = BigInt(Math.floor(Date.now() / 1000)); - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); - if (!result || !result.status) { + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); + const report = result.into_report_unchecked(); + if (!report || !report.status) { throw new Error('Verification should succeed but got no result'); } }); @@ -116,8 +118,9 @@ async function runTests() { const rootCA = await fetchFile('/test_data/certs/root_ca.der'); const now = BigInt(Math.floor(Date.now() / 1000)); - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); - if (!result || !result.status) { + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); + const report = result.into_report_unchecked(); + if (!report || !report.status) { throw new Error('Verification should succeed but got no result'); } }); @@ -129,8 +132,9 @@ async function runTests() { const rootCA = await fetchFile('/test_data/certs/root_ca.der'); const now = BigInt(Math.floor(Date.now() / 1000)); - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); - if (!result || !result.status) { + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); + const report = result.into_report_unchecked(); + if (!report || !report.status) { throw new Error('Verification should succeed but got no result'); } }); @@ -146,7 +150,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { // WASM errors might be strings or objects @@ -165,7 +169,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { // WASM errors might be strings or objects @@ -184,7 +188,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -205,7 +209,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { // WASM errors might be strings or objects @@ -224,7 +228,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -245,7 +249,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -263,7 +267,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -284,7 +288,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -305,7 +309,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -323,7 +327,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -344,7 +348,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -365,7 +369,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -386,7 +390,7 @@ async function runTests() { const now = BigInt(Math.floor(Date.now() / 1000)); try { - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); throw new Error('Should have failed but succeeded'); } catch (error) { const errorStr = typeof error === 'string' ? error : (error.message || String(error)); @@ -407,8 +411,9 @@ async function runTests() { const rootCA = await fetchFile("/test_data/certs/root_ca.der"); const now = BigInt(Math.floor(Date.now() / 1000)); - const result = js_verify_with_root_ca(quote, collateral, rootCA, now); - if (!result || !result.status) { + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); + const report = result.into_report_unchecked(); + if (!report || !report.status) { throw new Error( "Verification should succeed for PKS enabled quote" ); @@ -422,14 +427,9 @@ async function runTests() { await runTest('Fetch collateral from PCCS', async () => { const quote = await fetchFile('/sample/tdx_quote'); - // Check if get_collateral function is available in Web WASM - if (typeof js_get_collateral !== 'function') { - throw new Error('js_get_collateral function not available in Web WASM'); - } - // Test with HTTP URL (our mock server runs on HTTP) const mockPccsUrl = 'http://localhost:8765/tdx/certification/v4'; - const result = js_get_collateral(mockPccsUrl, quote); + const result = QuoteVerifier.get_collateral(mockPccsUrl, quote); // The function should return a promise in Web WASM just like in Node.js if (!result || typeof result.then !== 'function') { diff --git a/tests/test_case.js b/tests/test_case.js index 7bad017..4adb62d 100755 --- a/tests/test_case.js +++ b/tests/test_case.js @@ -87,15 +87,13 @@ async function cmdVerify(args) { } try { - let result; - if (rootCaDer) { - result = wasmModule.js_verify_with_root_ca(quoteBytes, collateral, rootCaDer, now); - } else { - result = wasmModule.js_verify(quoteBytes, collateral, now); - } - + const verifier = rootCaDer + ? new wasmModule.QuoteVerifier(rootCaDer) + : new wasmModule.QuoteVerifier(); + const result = verifier.verify(quoteBytes, collateral, now); + const report = result.into_report_unchecked(); console.log("Verification successful"); - console.log(`Status: ${result.status}`); + console.log(`Status: ${report.status}`); process.exit(0); } catch (e) { console.error(`Verification failed: ${e}`); @@ -136,7 +134,7 @@ async function cmdGetCollateral(args) { } try { - const result = await wasmModule.js_get_collateral(pccsUrl, quoteBytes); + const result = await wasmModule.QuoteVerifier.get_collateral(pccsUrl, quoteBytes); if (!result || !result.tcb_info_issuer_chain) { console.error("Error: Collateral missing required fields"); From 1a3d87f55cdef1e2314be8945def9ef7b6e243f5 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 03:20:31 +0000 Subject: [PATCH 21/33] fix: update remaining JS/TS files to QuoteVerifier API, add accepted_sgx_types - Fix get_collateral_web.js, esbuild/main.ts, vite/main.ts to use QuoteVerifier class instead of deleted js_verify/js_get_collateral - Add accepted_sgx_types() to JsSimplePolicy (parity with Python) - Change example files to use validate(policy) as default path instead of into_report_unchecked() - Fix rustfmt formatting in get_collateral and js_parse_tcb_status --- src/verify.rs | 23 ++++++++++++++--------- tests/esbuild/src/main.ts | 12 +++++++----- tests/js/get_collateral_node.js | 4 ++-- tests/js/get_collateral_web.js | 5 ++--- tests/js/verify_quote_node.js | 5 +++-- tests/js/verify_quote_web.js | 5 +++-- tests/vite/src/main.ts | 16 +++++++--------- 7 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/verify.rs b/src/verify.rs index a5f7e64..ae89eb5 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -339,7 +339,9 @@ fn js_parse_tcb_status(s: &str) -> Result { "OutOfDate" => Ok(TcbStatus::OutOfDate), "OutOfDateConfigurationNeeded" => Ok(TcbStatus::OutOfDateConfigurationNeeded), "Revoked" => Ok(TcbStatus::Revoked), - _ => Err(JsValue::from_str(&alloc::format!("Unknown TCB status: {s}"))), + _ => Err(JsValue::from_str(&alloc::format!( + "Unknown TCB status: {s}" + ))), } } @@ -419,6 +421,13 @@ impl JsSimplePolicy { inner: self.inner.allow_smt(allow), } } + + /// Set accepted SGX types (e.g. [0, 1, 2]). + pub fn accepted_sgx_types(self, types: Vec) -> Self { + Self { + inner: self.inner.accepted_sgx_types(&types), + } + } } /// Result of cryptographic quote verification (phase 1) for JS/WASM. @@ -511,17 +520,13 @@ impl JsQuoteVerifier { } /// Fetch collateral from a PCCS server. - pub async fn get_collateral( - pccs_url: &str, - raw_quote: JsValue, - ) -> Result { + pub async fn get_collateral(pccs_url: &str, raw_quote: JsValue) -> Result { let raw_quote: Vec = serde_wasm_bindgen::from_value(raw_quote) .map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?; - let collateral: QuoteCollateralV3 = - crate::collateral::get_collateral(pccs_url, &raw_quote) - .await - .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + let collateral: QuoteCollateralV3 = crate::collateral::get_collateral(pccs_url, &raw_quote) + .await + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; serde_wasm_bindgen::to_value(&collateral) .map_err(|_| JsValue::from_str("Failed to encode collateral")) } diff --git a/tests/esbuild/src/main.ts b/tests/esbuild/src/main.ts index 5218350..a13dc24 100644 --- a/tests/esbuild/src/main.ts +++ b/tests/esbuild/src/main.ts @@ -1,4 +1,4 @@ -import init, { js_verify, js_get_collateral } from "@phala/dcap-qvl-web"; +import init, { QuoteVerifier, SimplePolicy } from "@phala/dcap-qvl-web"; import wasm from "@phala/dcap-qvl-web/dcap-qvl-web_bg.wasm"; const PCCS_URL = "https://pccs.phala.network/tdx/certification/v4"; @@ -14,12 +14,14 @@ async function fetchQuoteAsUint8Array(url: string): Promise { init(wasm).then(() => { console.log("Phala DCAP QVL initialized!"); - // You can now use js_verify, js_get_collateral, etc. fetchQuoteAsUint8Array("/sample/tdx_quote").then(async (rawQuote) => { - const quoteCollateral = await js_get_collateral(PCCS_URL, rawQuote); + const quoteCollateral = await QuoteVerifier.get_collateral(PCCS_URL, rawQuote); const now = BigInt(Math.floor(Date.now() / 1000)); - const result = js_verify(rawQuote, quoteCollateral, now); - console.log("Verification Result:", result); + const verifier = new QuoteVerifier(); + const result = verifier.verify(rawQuote, quoteCollateral, now); + const policy = new SimplePolicy(now); + const report = result.validate(policy); + console.log("Verification Result:", report); }); }).catch((error: unknown) => { console.error("Error:", error); diff --git a/tests/js/get_collateral_node.js b/tests/js/get_collateral_node.js index 394e156..41ec405 100644 --- a/tests/js/get_collateral_node.js +++ b/tests/js/get_collateral_node.js @@ -1,6 +1,6 @@ const fs = require("fs"); const path = require("path"); -const { js_get_collateral } = require("../../pkg/node/dcap-qvl-node"); +const { QuoteVerifier } = require("../../pkg/node/dcap-qvl-node"); // Function to read a file as a Uint8Array function readFileAsUint8Array(filePath) { @@ -16,7 +16,7 @@ const rawQuote = readFileAsUint8Array(rawQuotePath); try { // Call the js_get_collateral function for TDX quote let pccs_url = "https://pccs.phala.network/tdx/certification/v4"; - const result = await js_get_collateral(pccs_url, rawQuote); + const result = await QuoteVerifier.get_collateral(pccs_url, rawQuote); console.log("Collateral Result:", result); } catch (error) { console.error("Get collateral failed:", error); diff --git a/tests/js/get_collateral_web.js b/tests/js/get_collateral_web.js index 3a1d52b..8f865fb 100644 --- a/tests/js/get_collateral_web.js +++ b/tests/js/get_collateral_web.js @@ -1,4 +1,4 @@ -import init, { js_get_collateral } from "/pkg/web/dcap-qvl-web.js"; +import init, { QuoteVerifier } from "/pkg/web/dcap-qvl-web.js"; // Function to fetch a file as a Uint8Array async function fetchFileAsUint8Array(url) { @@ -18,9 +18,8 @@ async function getCollateral() { const rawQuote = await fetchFileAsUint8Array(rawQuoteUrl); - // Call the js_get_collateral function for TDX quote let pccs_url = "https://pccs.phala.network/tdx/certification/v4"; - const result = await js_get_collateral(pccs_url, rawQuote); + const result = await QuoteVerifier.get_collateral(pccs_url, rawQuote); console.log("Collateral Result:", result); } catch (error) { console.error("Get collateral failed:", error); diff --git a/tests/js/verify_quote_node.js b/tests/js/verify_quote_node.js index 11f76b5..65811d2 100644 --- a/tests/js/verify_quote_node.js +++ b/tests/js/verify_quote_node.js @@ -1,6 +1,6 @@ const fs = require("fs"); const path = require("path"); -const { QuoteVerifier } = require("../../pkg/node/dcap-qvl-node"); +const { QuoteVerifier, SimplePolicy } = require("../../pkg/node/dcap-qvl-node"); // Function to read a file as a Uint8Array function readFileAsUint8Array(filePath) { @@ -23,7 +23,8 @@ const now = BigInt(Math.floor(Date.now() / 1000)); const quoteCollateral = await QuoteVerifier.get_collateral(pccs_url, rawQuote); const verifier = new QuoteVerifier(); const result = verifier.verify(rawQuote, quoteCollateral, now); - const report = result.into_report_unchecked(); + const policy = new SimplePolicy(now); + const report = result.validate(policy); console.log("Verification Result:", report); } catch (error) { console.error("Verification failed:", error); diff --git a/tests/js/verify_quote_web.js b/tests/js/verify_quote_web.js index a9a1d9a..a5c1a75 100644 --- a/tests/js/verify_quote_web.js +++ b/tests/js/verify_quote_web.js @@ -1,4 +1,4 @@ -import init, { QuoteVerifier } from "/pkg/web/dcap-qvl-web.js"; +import init, { QuoteVerifier, SimplePolicy } from "/pkg/web/dcap-qvl-web.js"; // Function to fetch a file as a Uint8Array async function fetchFileAsUint8Array(url) { @@ -34,7 +34,8 @@ async function loadFilesAndVerify() { // Verify const verifier = new QuoteVerifier(); const result = verifier.verify(rawQuote, quoteCollateral, now); - const report = result.into_report_unchecked(); + const policy = new SimplePolicy(now); + const report = result.validate(policy); console.log("Verification Result:", report); } catch (error) { console.error("Verification failed:", error); diff --git a/tests/vite/src/main.ts b/tests/vite/src/main.ts index a1322dd..92b73dc 100644 --- a/tests/vite/src/main.ts +++ b/tests/vite/src/main.ts @@ -1,6 +1,6 @@ import "./style.css"; -import init, { js_verify, js_get_collateral } from "@phala/dcap-qvl-web"; +import init, { QuoteVerifier, SimplePolicy } from "@phala/dcap-qvl-web"; import wasm from "@phala/dcap-qvl-web/dcap-qvl-web_bg.wasm"; const PCCS_URL = "https://pccs.phala.network/tdx/certification/v4"; @@ -16,16 +16,14 @@ async function fetchQuoteAsUint8Array(url: string): Promise { init(wasm).then(() => { console.log("Phala DCAP QVL initialized!"); - // You can now use js_verify, js_get_collateral, etc. fetchQuoteAsUint8Array("/sample/tdx_quote").then(async (rawQuote) => { - const quoteCollateral = await js_get_collateral(PCCS_URL, rawQuote); - - // Current timestamp + const quoteCollateral = await QuoteVerifier.get_collateral(PCCS_URL, rawQuote); const now = BigInt(Math.floor(Date.now() / 1000)); - - // Call the js_verify function - const result = js_verify(rawQuote, quoteCollateral, now); - console.log("Verification Result:", result); + const verifier = new QuoteVerifier(); + const result = verifier.verify(rawQuote, quoteCollateral, now); + const policy = new SimplePolicy(now); + const report = result.validate(policy); + console.log("Verification Result:", report); }); }).catch((error: unknown) => { console.error("Error:", error); From f3f349c9815ffb161f60279cd1f320b19f99f1d6 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 03:26:26 +0000 Subject: [PATCH 22/33] fix: add js_class annotations for SimplePolicy and QuoteVerificationResult Without js_class matching the js_name on the struct, wasm_bindgen doesn't attach methods to the renamed JS class. --- src/verify.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/verify.rs b/src/verify.rs index ae89eb5..d1dedca 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -346,7 +346,7 @@ fn js_parse_tcb_status(s: &str) -> Result { } #[cfg(feature = "js")] -#[wasm_bindgen] +#[wasm_bindgen(js_class = "SimplePolicy")] impl JsSimplePolicy { /// Create a strict policy: only `UpToDate`, no grace period, no advisory tolerance. #[wasm_bindgen(constructor)] @@ -441,7 +441,7 @@ pub struct JsQuoteVerificationResult { } #[cfg(feature = "js")] -#[wasm_bindgen] +#[wasm_bindgen(js_class = "QuoteVerificationResult")] impl JsQuoteVerificationResult { /// Validate against a policy, returning a VerifiedReport. Consumes the result. pub fn validate(&mut self, policy: &JsSimplePolicy) -> Result { From 5bb08f7e6a6cbcb1f5e91412ed4f11f6baa58f5c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 07:33:01 +0000 Subject: [PATCH 23/33] fix: check grace period mutual exclusivity before data-dependent checks Move the collateral_grace_period vs platform/qe_grace_period mutual exclusivity check to the top of SimplePolicy::validate(), before any data-dependent checks run. Previously it was checked after the TCB status check, meaning a misconfigured policy would run partial validation before failing on the config error. --- src/policy/simple.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 720ed9b..0150213 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -196,15 +196,7 @@ impl Policy for SimplePolicy { .any(|a| a.eq_ignore_ascii_case(id)) } - // 1. TCB status whitelist - if !self.is_status_acceptable(data.tcb.status) { - bail!( - "TCB status {:?} is not acceptable by policy", - data.tcb.status - ); - } - - // 3 & 4. Grace periods + // 0. Sanity: grace periods are mutually exclusive (policy misconfiguration) if self.collateral_grace_period > 0 && (self.platform_grace_period > 0 || self.qe_grace_period > 0) { @@ -213,6 +205,14 @@ impl Policy for SimplePolicy { ); } + // 1. TCB status whitelist + if !self.is_status_acceptable(data.tcb.status) { + bail!( + "TCB status {:?} is not acceptable by policy", + data.tcb.status + ); + } + // 3. Collateral expiration: earliest_expiration + grace >= now if data .earliest_expiration_date From a01b28420416b9075f6e96eea178d10453797d13 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 07:34:04 +0000 Subject: [PATCH 24/33] fix: match OPA semantics for rand.intn with n <= 0 OPA's builtinRandIntn returns 0 for n==0 and uses abs(n) for negative values. Our implementation was returning 0 for all n<=0, diverging from OPA's behavior for negative n. --- src/policy/rego.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/policy/rego.rs b/src/policy/rego.rs index 5f7d955..8ab0bd1 100644 --- a/src/policy/rego.rs +++ b/src/policy/rego.rs @@ -401,11 +401,15 @@ fn register_rand_intn(engine: &mut regorus::Engine) -> Result<()> { .as_i64() .map_err(|_| anyhow::anyhow!("rand.intn: second argument must be integer"))?; - if n <= 0 { + if n == 0 { return Ok(regorus::Value::from(0i64)); } + // OPA uses abs(n) for negative values + let n = n.unsigned_abs(); + // Cache key = "{seed}-{n}", matching OPA's `fmt.Sprintf("%s-%d", strOp, n)` + // Note: OPA caches with abs'd n, so "-5" and "5" share the same key. let key = alloc::format!("{seed}-{n}"); if let Some(&cached) = cache.get(&key) { @@ -415,8 +419,7 @@ fn register_rand_intn(engine: &mut regorus::Engine) -> Result<()> { let mut buf = [0u8; 8]; getrandom::getrandom(&mut buf) .map_err(|e| anyhow::anyhow!("rand.intn: RNG failed: {e}"))?; - let random_val = - (u64::from_le_bytes(buf).checked_rem(n as u64).unwrap_or(0)) as i64; + let random_val = (u64::from_le_bytes(buf).checked_rem(n).unwrap_or(0)) as i64; cache.insert(key, random_val); Ok(regorus::Value::from(random_val)) }), From 0b3977e2d736be0c248f94b3da7e6b1b44053a2d Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 08:04:06 +0000 Subject: [PATCH 25/33] fix: remove grace period mutual exclusivity constraint The collateral_grace_period, platform_grace_period, and qe_grace_period are orthogonal checks (time-based vs version-based). Intel's Rego script enforces mutual exclusivity as a simplification, but it's not a logical necessity. Allow all three to be set independently. --- src/policy/simple.rs | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 0150213..6b7f525 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -110,8 +110,6 @@ impl SimplePolicy { /// Set collateral grace period (default: zero). Accepts quotes where /// `earliest_expiration_date + grace_period >= now`. - /// - /// Must be zero if [`platform_grace_period`](Self::platform_grace_period) is non-zero. pub fn collateral_grace_period(mut self, duration: Duration) -> Self { self.collateral_grace_period = duration.as_secs(); self @@ -120,8 +118,6 @@ impl SimplePolicy { /// Set platform grace period (default: zero). When TCB status is /// OutOfDate or OutOfDateConfigurationNeeded, accepts quotes where /// `tcb_level_date_tag + grace_period >= now`. Skipped for UpToDate/ConfigNeeded/SWHardening. - /// - /// Must be zero if [`collateral_grace_period`](Self::collateral_grace_period) is non-zero. pub fn platform_grace_period(mut self, duration: Duration) -> Self { self.platform_grace_period = duration.as_secs(); self @@ -196,15 +192,6 @@ impl Policy for SimplePolicy { .any(|a| a.eq_ignore_ascii_case(id)) } - // 0. Sanity: grace periods are mutually exclusive (policy misconfiguration) - if self.collateral_grace_period > 0 - && (self.platform_grace_period > 0 || self.qe_grace_period > 0) - { - bail!( - "collateral_grace_period is mutually exclusive with platform_grace_period and qe_grace_period" - ); - } - // 1. TCB status whitelist if !self.is_status_acceptable(data.tcb.status) { bail!( @@ -661,26 +648,6 @@ mod tests { assert!(err.contains("Platform TCB too old"), "{err}"); } - #[test] - fn policy_grace_periods_mutually_exclusive() { - let data = make_test_supplemental(UpToDate); - let policy = SimplePolicy::strict(1_702_000_000) - .collateral_grace_period(Duration::from_secs(100)) - .platform_grace_period(Duration::from_secs(100)); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("mutually exclusive"), "{err}"); - } - - #[test] - fn policy_collateral_grace_mutually_exclusive_with_qe_grace() { - let data = make_test_supplemental(UpToDate); - let policy = SimplePolicy::strict(1_702_000_000) - .collateral_grace_period(Duration::from_secs(100)) - .qe_grace_period(Duration::from_secs(100)); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("mutually exclusive"), "{err}"); - } - // -- min_tcb_eval_data_number -- #[test] From e6b8b1688a4359bbd8cae7a6fb6773147814dca9 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 11:23:54 +0000 Subject: [PATCH 26/33] feat: expose Rego policies to python and wasm bindings --- Cargo.toml | 4 +- python-bindings/python/dcap_qvl/__init__.py | 6 ++ python-bindings/python/dcap_qvl/_dcap_qvl.pyi | 32 ++++++- src/python.rs | 83 ++++++++++++++++++- src/verify.rs | 81 ++++++++++++++++++ 5 files changed, 199 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 77a9d03..a33621f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,8 +111,8 @@ std = [ borsh = ["dep:borsh"] borsh_schema = ["borsh", "borsh/unstable__schema"] report = ["std", "tracing", "futures", "reqwest"] -js = ["getrandom/js", "serde-wasm-bindgen", "wasm-bindgen", "rustcrypto"] -python = ["pyo3", "pyo3-async-runtimes", "tokio", "std", "report", "ring"] +js = ["getrandom/js", "serde-wasm-bindgen", "wasm-bindgen", "rustcrypto", "rego"] +python = ["pyo3", "pyo3-async-runtimes", "tokio", "std", "report", "ring", "rego"] go = ["std", "ring", "serde_json"] ring = ["dep:ring", "dcap-qvl-webpki/ring", "_anycrypto"] rustcrypto = ["dep:sha2", "dep:p256", "dep:signature", "dcap-qvl-webpki/rustcrypto", "_anycrypto"] diff --git a/python-bindings/python/dcap_qvl/__init__.py b/python-bindings/python/dcap_qvl/__init__.py index 0479a89..293c051 100644 --- a/python-bindings/python/dcap_qvl/__init__.py +++ b/python-bindings/python/dcap_qvl/__init__.py @@ -13,6 +13,8 @@ - QuoteVerificationResult: Intermediate result from crypto verification - VerifiedReport: Contains verification results after policy validation - SimplePolicy: Verification policy with builder pattern +- RegoPolicy: Intel QAL-compatible Rego policy +- RegoPolicySet: Intel QAL-compatible multi-policy set Main functions: - verify: Verify a quote with collateral data (returns QuoteVerificationResult) @@ -35,6 +37,8 @@ PySgxEnclaveReport as SgxEnclaveReport, PyPckExtension as PckExtension, PySimplePolicy as SimplePolicy, + PyRegoPolicy as RegoPolicy, + PyRegoPolicySet as RegoPolicySet, PyQuote as Quote, py_verify as verify, py_verify_with_root_ca as verify_with_root_ca, @@ -139,6 +143,8 @@ async def get_collateral_and_verify( "SgxEnclaveReport", "PckExtension", "SimplePolicy", + "RegoPolicy", + "RegoPolicySet", "AttestationKeyType", "TeeType", "Quote", diff --git a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi index 66db6ab..50c8eaa 100644 --- a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi +++ b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi @@ -374,6 +374,34 @@ class PySimplePolicy: ... +class PyRegoPolicy: + """Intel QAL-compatible Rego policy.""" + + def __init__(self, policy_json: str) -> None: + """Create a Rego policy from Intel-format JSON.""" + ... + + @staticmethod + def with_rego(policy_json: str, rego_source: str) -> "PyRegoPolicy": + """Create a Rego policy with a custom Rego script.""" + ... + + +class PyRegoPolicySet: + """Intel QAL-compatible multi-measurement Rego policy set.""" + + def __init__(self, policy_jsons: List[str]) -> None: + """Create a Rego policy set from multiple Intel-format JSON policies.""" + ... + + @staticmethod + def with_rego( + policy_jsons: List[str], rego_source: str + ) -> "PyRegoPolicySet": + """Create a Rego policy set with a custom Rego script.""" + ... + + class PyQuoteVerificationResult: """Intermediate result from crypto verification (phase 1). @@ -383,7 +411,9 @@ class PyQuoteVerificationResult: The result is consumed on validate/into_report_unchecked — calling twice raises ValueError. """ - def validate(self, policy: PySimplePolicy) -> PyVerifiedReport: + def validate( + self, policy: Union[PySimplePolicy, PyRegoPolicy, PyRegoPolicySet] + ) -> PyVerifiedReport: """Validate against a policy, returning a VerifiedReport. Consumes the result. Args: diff --git a/src/python.rs b/src/python.rs index d053ec4..4d30313 100644 --- a/src/python.rs +++ b/src/python.rs @@ -6,6 +6,8 @@ use pyo3::types::{PyBytes, PyDict, PyTuple}; use pyo3_async_runtimes::tokio::future_into_py; use serde_json; +#[cfg(feature = "rego")] +use crate::policy::{RegoPolicy, RegoPolicySet}; use crate::{ collateral::get_collateral_for_fmspc, intel, @@ -557,6 +559,56 @@ impl PySimplePolicy { } } +#[cfg(feature = "rego")] +#[pyclass] +pub struct PyRegoPolicy { + inner: RegoPolicy, +} + +#[cfg(feature = "rego")] +#[pymethods] +impl PyRegoPolicy { + #[new] + fn new(policy_json: &str) -> PyResult { + let inner = RegoPolicy::new(policy_json) + .map_err(|e| PyValueError::new_err(format!("Invalid Rego policy: {e}")))?; + Ok(Self { inner }) + } + + #[staticmethod] + fn with_rego(policy_json: &str, rego_source: &str) -> PyResult { + let inner = RegoPolicy::with_rego(policy_json, rego_source) + .map_err(|e| PyValueError::new_err(format!("Invalid Rego policy: {e}")))?; + Ok(Self { inner }) + } +} + +#[cfg(feature = "rego")] +#[pyclass] +pub struct PyRegoPolicySet { + inner: RegoPolicySet, +} + +#[cfg(feature = "rego")] +#[pymethods] +impl PyRegoPolicySet { + #[new] + fn new(policy_jsons: Vec) -> PyResult { + let policy_refs: Vec<&str> = policy_jsons.iter().map(String::as_str).collect(); + let inner = RegoPolicySet::new(&policy_refs) + .map_err(|e| PyValueError::new_err(format!("Invalid Rego policy set: {e}")))?; + Ok(Self { inner }) + } + + #[staticmethod] + fn with_rego(policy_jsons: Vec, rego_source: &str) -> PyResult { + let policy_refs: Vec<&str> = policy_jsons.iter().map(String::as_str).collect(); + let inner = RegoPolicySet::with_rego(&policy_refs, rego_source) + .map_err(|e| PyValueError::new_err(format!("Invalid Rego policy set: {e}")))?; + Ok(Self { inner }) + } +} + #[pyclass] pub struct PyQuote { inner: Quote, @@ -680,14 +732,33 @@ pub struct PyQuoteVerificationResult { #[pymethods] impl PyQuoteVerificationResult { /// Validate against a policy, returning a VerifiedReport. Consumes the result. - fn validate(&mut self, policy: &PySimplePolicy) -> PyResult { + fn validate<'py>(&mut self, policy: &Bound<'py, PyAny>) -> PyResult { let result = self .inner .take() .ok_or_else(|| PyValueError::new_err("verification result already consumed"))?; - let report = result - .validate(&policy.inner) - .map_err(|e| PyValueError::new_err(format!("Policy validation failed: {e:?}")))?; + + let report = if let Ok(policy) = policy.extract::>() { + result.validate(&policy.inner) + } else { + #[cfg(feature = "rego")] + { + if let Ok(policy) = policy.extract::>() { + result.validate(&policy.inner) + } else if let Ok(policy) = policy.extract::>() { + result.validate(&policy.inner) + } else { + return Err(PyValueError::new_err( + "policy must be SimplePolicy, RegoPolicy, or RegoPolicySet", + )); + } + } + #[cfg(not(feature = "rego"))] + { + return Err(PyValueError::new_err("policy must be SimplePolicy")); + } + } + .map_err(|e| PyValueError::new_err(format!("Policy validation failed: {e:?}")))?; Ok(PyVerifiedReport { inner: report }) } @@ -785,6 +856,10 @@ pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + #[cfg(feature = "rego")] + m.add_class::()?; + #[cfg(feature = "rego")] + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(py_verify, m)?)?; diff --git a/src/verify.rs b/src/verify.rs index d1dedca..f04ae4e 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -430,6 +430,59 @@ impl JsSimplePolicy { } } +/// Intel QAL-compatible Rego policy for JS/WASM. +#[cfg(all(feature = "js", feature = "rego"))] +#[wasm_bindgen(js_name = "RegoPolicy")] +pub struct JsRegoPolicy { + inner: crate::policy::RegoPolicy, +} + +#[cfg(all(feature = "js", feature = "rego"))] +#[wasm_bindgen(js_class = "RegoPolicy")] +impl JsRegoPolicy { + #[wasm_bindgen(constructor)] + pub fn new(policy_json: &str) -> Result { + let inner = crate::policy::RegoPolicy::new(policy_json) + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + Ok(Self { inner }) + } + + pub fn with_rego(policy_json: &str, rego_source: &str) -> Result { + let inner = crate::policy::RegoPolicy::with_rego(policy_json, rego_source) + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + Ok(JsRegoPolicy { inner }) + } +} + +/// Multi-measurement Intel QAL-compatible Rego policy set for JS/WASM. +#[cfg(all(feature = "js", feature = "rego"))] +#[wasm_bindgen(js_name = "RegoPolicySet")] +pub struct JsRegoPolicySet { + inner: crate::policy::RegoPolicySet, +} + +#[cfg(all(feature = "js", feature = "rego"))] +#[wasm_bindgen(js_class = "RegoPolicySet")] +impl JsRegoPolicySet { + #[wasm_bindgen(constructor)] + pub fn new(policy_jsons: Vec) -> Result { + let policy_refs: Vec<&str> = policy_jsons.iter().map(String::as_str).collect(); + let inner = crate::policy::RegoPolicySet::new(&policy_refs) + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + Ok(Self { inner }) + } + + pub fn with_rego( + policy_jsons: Vec, + rego_source: &str, + ) -> Result { + let policy_refs: Vec<&str> = policy_jsons.iter().map(String::as_str).collect(); + let inner = crate::policy::RegoPolicySet::with_rego(&policy_refs, rego_source) + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + Ok(JsRegoPolicySet { inner }) + } +} + /// Result of cryptographic quote verification (phase 1) for JS/WASM. /// /// Use `validate(policy)` to apply a [`JsSimplePolicy`] and get a `VerifiedReport`. @@ -456,6 +509,34 @@ impl JsQuoteVerificationResult { .map_err(|_| JsValue::from_str("Failed to encode verified_report")) } + /// Validate against a Rego policy, returning a VerifiedReport. Consumes the result. + #[cfg(feature = "rego")] + pub fn validate_rego(&mut self, policy: &JsRegoPolicy) -> Result { + let result = self + .inner + .take() + .ok_or_else(|| JsValue::from_str("verification result already consumed"))?; + let report = result + .validate(&policy.inner) + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + serde_wasm_bindgen::to_value(&report) + .map_err(|_| JsValue::from_str("Failed to encode verified_report")) + } + + /// Validate against a Rego policy set, returning a VerifiedReport. Consumes the result. + #[cfg(feature = "rego")] + pub fn validate_rego_set(&mut self, policy: &JsRegoPolicySet) -> Result { + let result = self + .inner + .take() + .ok_or_else(|| JsValue::from_str("verification result already consumed"))?; + let report = result + .validate(&policy.inner) + .map_err(|e| JsValue::from_str(&format_error_chain(&e)))?; + serde_wasm_bindgen::to_value(&report) + .map_err(|_| JsValue::from_str("Failed to encode verified_report")) + } + /// Get VerifiedReport without policy validation. Consumes the result. pub fn into_report_unchecked(&mut self) -> Result { let result = self From a329512a84aa664fea40140b14521a84e75fb3b4 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 11 Mar 2026 11:23:54 +0000 Subject: [PATCH 27/33] test: add Rego binding examples and coverage --- README.md | 47 ++++++- docs/policy.md | 44 ++++++- python-bindings/tests/test_python_bindings.py | 118 ++++++++++++++++++ tests/js/README.md | 1 + tests/js/verify_quote_rego_node.js | 46 +++++++ tests/js/verify_quote_web_test.js | 48 ++++++- 6 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 tests/js/verify_quote_rego_node.js diff --git a/README.md b/README.md index ae097ff..a2d90b4 100644 --- a/README.md +++ b/README.md @@ -140,18 +140,61 @@ make test_python_versions ```python import asyncio +import time import dcap_qvl async def main(): quote_data = open("quote.bin", "rb").read() - # Get collateral and verify in one step (defaults to Phala PCCS) + # Get collateral and perform crypto verification (defaults to Phala PCCS) result = await dcap_qvl.get_collateral_and_verify(quote_data) - print(f"Status: {result.status}") + + # Validate with SimplePolicy + now = int(time.time()) + policy = dcap_qvl.SimplePolicy.strict(now) + report = result.validate(policy) + print(f"Status: {report.status}") asyncio.run(main()) ``` +You can also validate with Intel QAL-compatible Rego policies: + +```python +policy_json = r'''{ + "environment": { + "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570" + }, + "reference": { + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0 + } +}''' + +rego_policy = dcap_qvl.RegoPolicy(policy_json) +report = result.validate(rego_policy) +``` + +And from JS/WASM: + +```js +import init, { QuoteVerifier, SimplePolicy, RegoPolicy } from "@phala/dcap-qvl-web"; + +await init(); + +const collateral = await QuoteVerifier.get_collateral(pccsUrl, quoteBytes); +const verifier = new QuoteVerifier(); +const now = BigInt(Math.floor(Date.now() / 1000)); +const result = verifier.verify(quoteBytes, collateral, now); + +const simplePolicy = new SimplePolicy(now); +const report1 = result.validate(simplePolicy); + +const regoPolicy = new RegoPolicy(policyJson); +const result2 = verifier.verify(quoteBytes, collateral, now); +const report2 = result2.validate_rego(regoPolicy); +``` + See [python-bindings/](python-bindings/) for complete documentation, examples, and testing information. # License diff --git a/docs/policy.md b/docs/policy.md index cce88e2..af7021f 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -142,8 +142,11 @@ Runs Intel's official `qal_script.rego` via the `regorus` Rego interpreter. Acce use dcap_qvl::RegoPolicy; let policy_json = r#"{ - "policy": { - "tcb_status": ["UpToDate", "SWHardeningNeeded"], + "environment": { + "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570" + }, + "reference": { + "accepted_tcb_status": ["UpToDate", "SWHardeningNeeded"], "collateral_grace_period": 7776000 } }"#; @@ -153,4 +156,41 @@ let report = result.validate(&policy)?; `RegoPolicySet` supports multiple JSON policies for multi-measurement appraisal (one per `class_id`), matching Intel QAL's full functionality. Both `RegoPolicy` and `RegoPolicySet` implement the `Policy` trait, so they work with the standard `validate()` method. +### Python + +```python +import dcap_qvl + +policy_json = r'''{ + "environment": { + "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570" + }, + "reference": { + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0 + } +}''' + +policy = dcap_qvl.RegoPolicy(policy_json) +report = result.validate(policy) +``` + +### JS / WASM + +```js +import init, { QuoteVerifier, RegoPolicy, RegoPolicySet } from "@phala/dcap-qvl-web"; + +await init(); + +const verifier = new QuoteVerifier(); +const result = verifier.verify(quoteBytes, collateral, now); + +const policy = new RegoPolicy(policyJson); +const report = result.validate_rego(policy); + +const policySet = new RegoPolicySet([platformPolicyJson, tenantPolicyJson]); +const result2 = verifier.verify(quoteBytes, collateral, now); +const report2 = result2.validate_rego_set(policySet); +``` + See [Intel's DCAP Appraisal documentation](https://github.com/intel/SGXDataCenterAttestationPrimitives) for the Rego policy JSON format. diff --git a/python-bindings/tests/test_python_bindings.py b/python-bindings/tests/test_python_bindings.py index 7eb8d19..3087c2b 100644 --- a/python-bindings/tests/test_python_bindings.py +++ b/python-bindings/tests/test_python_bindings.py @@ -83,6 +83,58 @@ def test_verify_with_invalid_quote(self): dcap_qvl.verify(invalid_quote, collateral, 1234567890) +class TestRegoPolicies: + """Test Rego policy bindings.""" + + def test_rego_policy_constructor(self): + """Test creating a RegoPolicy from valid JSON.""" + policy_json = json.dumps( + { + "environment": { + "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570", + }, + "reference": { + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0, + }, + } + ) + + policy = dcap_qvl.RegoPolicy(policy_json) + assert isinstance(policy, dcap_qvl.RegoPolicy) + + def test_rego_policy_set_constructor(self): + """Test creating a RegoPolicySet from valid JSON policies.""" + policy_json = json.dumps( + { + "environment": { + "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570", + }, + "reference": { + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0, + }, + } + ) + + policies = dcap_qvl.RegoPolicySet([policy_json]) + assert isinstance(policies, dcap_qvl.RegoPolicySet) + + def test_rego_policy_missing_class_id(self): + """Test that missing class_id is rejected.""" + policy_json = json.dumps( + { + "reference": { + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0, + }, + } + ) + + with pytest.raises(ValueError): + dcap_qvl.RegoPolicy(policy_json) + + @pytest.mark.skipif( os.getenv("DCAP_QVL_RUN_SAMPLE_VERIFY") != "1", reason="Sample verify is an integration test. Set DCAP_QVL_RUN_SAMPLE_VERIFY=1 to run.", @@ -118,3 +170,69 @@ def test_verify_with_sample_data(self): assert isinstance(result, dcap_qvl.VerifiedReport) assert isinstance(result.status, str) assert isinstance(result.advisory_ids, list) + + def test_validate_with_rego_policy(self): + """Test validation with RegoPolicy using sample SGX quote.""" + if not Path("sample/sgx_quote").exists() or not Path( + "sample/sgx_quote_collateral.json" + ).exists(): + pytest.skip("Sample files not available") + + with open("sample/sgx_quote", "rb") as f: + quote_data = f.read() + + with open("sample/sgx_quote_collateral.json", "r") as f: + collateral_json = json.load(f) + + collateral = dcap_qvl.QuoteCollateralV3.from_json(json.dumps(collateral_json)) + qvr = dcap_qvl.verify(quote_data, collateral, 1234567890) + + policy_json = json.dumps( + { + "environment": { + "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570", + }, + "reference": { + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0, + }, + } + ) + policy = dcap_qvl.RegoPolicy(policy_json) + result = qvr.validate(policy) + + assert isinstance(result, dcap_qvl.VerifiedReport) + assert isinstance(result.status, str) + + def test_validate_with_rego_policy_set(self): + """Test validation with RegoPolicySet using sample SGX quote.""" + if not Path("sample/sgx_quote").exists() or not Path( + "sample/sgx_quote_collateral.json" + ).exists(): + pytest.skip("Sample files not available") + + with open("sample/sgx_quote", "rb") as f: + quote_data = f.read() + + with open("sample/sgx_quote_collateral.json", "r") as f: + collateral_json = json.load(f) + + collateral = dcap_qvl.QuoteCollateralV3.from_json(json.dumps(collateral_json)) + qvr = dcap_qvl.verify(quote_data, collateral, 1234567890) + + policy_json = json.dumps( + { + "environment": { + "class_id": "3123ec35-8d38-4ea5-87a5-d6c48b567570", + }, + "reference": { + "accepted_tcb_status": ["UpToDate"], + "collateral_grace_period": 0, + }, + } + ) + policy = dcap_qvl.RegoPolicySet([policy_json]) + result = qvr.validate(policy) + + assert isinstance(result, dcap_qvl.VerifiedReport) + assert isinstance(result.status, str) diff --git a/tests/js/README.md b/tests/js/README.md index 640118b..80bdcb9 100644 --- a/tests/js/README.md +++ b/tests/js/README.md @@ -29,6 +29,7 @@ See [TEST_WEB.md](TEST_WEB.md) for detailed web testing documentation. ```bash cd tests/js node verify_quote_node.js +node verify_quote_rego_node.js ``` ### Verify Quote in Web Browser diff --git a/tests/js/verify_quote_rego_node.js b/tests/js/verify_quote_rego_node.js new file mode 100644 index 0000000..c1127c7 --- /dev/null +++ b/tests/js/verify_quote_rego_node.js @@ -0,0 +1,46 @@ +const fs = require("fs"); +const path = require("path"); +const { + QuoteVerifier, + RegoPolicy, + RegoPolicySet, +} = require("../../pkg/node/dcap-qvl-node"); + +function readFileAsUint8Array(filePath) { + const data = fs.readFileSync(filePath); + return new Uint8Array(data); +} + +const rawQuotePath = path.join(__dirname, "../../sample", "sgx_quote"); +const rawQuote = readFileAsUint8Array(rawQuotePath); +const now = BigInt(Math.floor(Date.now() / 1000)); + +const platformPolicyJson = JSON.stringify({ + environment: { + class_id: "3123ec35-8d38-4ea5-87a5-d6c48b567570", + }, + reference: { + accepted_tcb_status: ["UpToDate"], + collateral_grace_period: 0, + }, +}); + +(async () => { + try { + const pccsUrl = "https://pccs.phala.network/sgx/certification/v4"; + const collateral = await QuoteVerifier.get_collateral(pccsUrl, rawQuote); + const verifier = new QuoteVerifier(); + + const regoReport = verifier + .verify(rawQuote, collateral, now) + .validate_rego(new RegoPolicy(platformPolicyJson)); + console.log("RegoPolicy report:", regoReport); + + const regoSetReport = verifier + .verify(rawQuote, collateral, now) + .validate_rego_set(new RegoPolicySet([platformPolicyJson])); + console.log("RegoPolicySet report:", regoSetReport); + } catch (error) { + console.error("Rego verification failed:", error); + } +})(); diff --git a/tests/js/verify_quote_web_test.js b/tests/js/verify_quote_web_test.js index da89d3f..39eed4b 100644 --- a/tests/js/verify_quote_web_test.js +++ b/tests/js/verify_quote_web_test.js @@ -1,4 +1,4 @@ -import init, { QuoteVerifier } from "/pkg/web/dcap-qvl-web.js"; +import init, { QuoteVerifier, RegoPolicy, RegoPolicySet } from "/pkg/web/dcap-qvl-web.js"; const testOutputs = []; let passed = 0; @@ -139,6 +139,52 @@ async function runTests() { } }); + await runTest('RegoPolicy validates valid SGX v3 quote', async () => { + const quote = await fetchFile('/test_data/samples/valid_sgx_v3/quote.bin'); + const collateral = await fetchJSON('/test_data/samples/valid_sgx_v3/collateral.json'); + const rootCA = await fetchFile('/test_data/certs/root_ca.der'); + const now = BigInt(Math.floor(Date.now() / 1000)); + + const policyJson = JSON.stringify({ + environment: { + class_id: '3123ec35-8d38-4ea5-87a5-d6c48b567570', + }, + reference: { + accepted_tcb_status: ['UpToDate'], + collateral_grace_period: 0, + }, + }); + + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); + const report = result.validate_rego(new RegoPolicy(policyJson)); + if (!report || !report.status) { + throw new Error('RegoPolicy validation should succeed but got no report'); + } + }); + + await runTest('RegoPolicySet validates valid SGX v3 quote', async () => { + const quote = await fetchFile('/test_data/samples/valid_sgx_v3/quote.bin'); + const collateral = await fetchJSON('/test_data/samples/valid_sgx_v3/collateral.json'); + const rootCA = await fetchFile('/test_data/certs/root_ca.der'); + const now = BigInt(Math.floor(Date.now() / 1000)); + + const platformPolicyJson = JSON.stringify({ + environment: { + class_id: '3123ec35-8d38-4ea5-87a5-d6c48b567570', + }, + reference: { + accepted_tcb_status: ['UpToDate'], + collateral_grace_period: 0, + }, + }); + + const result = new QuoteVerifier(rootCA).verify(quote, collateral, now); + const report = result.validate_rego_set(new RegoPolicySet([platformPolicyJson])); + if (!report || !report.status) { + throw new Error('RegoPolicySet validation should succeed but got no report'); + } + }); + log(''); log('━━━ Decode Errors ━━━'); From d9f9dde99c0fee9ee731931871e6a72a369a8823 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 12 Mar 2026 04:13:16 +0000 Subject: [PATCH 28/33] refactor: switch SimplePolicy advisories to blacklist semantics --- README.md | 3 +- docs/policy.md | 13 +- python-bindings/python/dcap_qvl/_dcap_qvl.pyi | 12 +- src/policy/mod.rs | 2 +- src/policy/simple.rs | 130 +++++++++--------- src/python.rs | 15 +- src/verify.rs | 15 +- 7 files changed, 107 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index a2d90b4..835bceb 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,8 @@ let policy = SimplePolicy::strict(now); let policy = SimplePolicy::strict(now) .allow_status(TcbStatus::OutOfDate) .collateral_grace_period(Duration::from_secs(30 * 24 * 3600)) - .accept_advisory("INTEL-SA-00334"); + .reject_advisory("INTEL-SA-00334") + .reject_advisories(&["INTEL-SA-00615", "INTEL-SA-00809"]); ``` For custom validation logic, implement the `Policy` trait directly. diff --git a/docs/policy.md b/docs/policy.md index af7021f..dfee5aa 100644 --- a/docs/policy.md +++ b/docs/policy.md @@ -22,7 +22,7 @@ verify() ──► QuoteVerificationResult ──► validate(policy) ─ ## SimplePolicy -The built-in policy with 9 checks from Intel's Appraisal framework. Strict by default — only `UpToDate` status, no grace period, no advisory tolerance. +The built-in policy with 9 checks from Intel's Appraisal framework. Strict by default — only `UpToDate` status, no grace period, no advisory blacklist. ### Basic Usage @@ -47,9 +47,10 @@ let policy = SimplePolicy::strict(now) // Accept additional TCB statuses .allow_status(TcbStatus::SWHardeningNeeded) .allow_status(TcbStatus::ConfigurationNeeded) - // Accept specific advisory IDs (case-insensitive) - .accept_advisory("INTEL-SA-00334") - .accept_advisory("INTEL-SA-00615") + // Reject specific advisory IDs (case-insensitive) + .reject_advisory("INTEL-SA-00334") + .reject_advisory("INTEL-SA-00615") + .reject_advisories(&["INTEL-SA-00809", "INTEL-SA-00820"]) // Collateral freshness: accept expired collateral within grace window .collateral_grace_period(Duration::from_secs(30 * 24 * 3600)) // 30 days // Minimum TCB evaluation data number @@ -67,7 +68,7 @@ let policy = SimplePolicy::strict(now) | # | Check | Default | Builder | |---|-------|---------|---------| | 1 | **TCB status whitelist** | Only `UpToDate` | `.allow_status(...)` | -| 2 | **Advisory ID whitelist** | Empty set (reject any) | `.accept_advisory(...)` | +| 2 | **Advisory ID blacklist** | Empty set (allow all) | `.reject_advisory(...)` | | 3 | **Collateral expiration** | `earliest_expiration >= now` | `.collateral_grace_period(Duration)` | | 4 | **Platform TCB freshness** | Only for OutOfDate statuses | `.platform_grace_period(Duration)` | | 4b | **QE TCB freshness** | Only for QE `OutOfDate` | `.qe_grace_period(Duration)` | @@ -79,7 +80,7 @@ let policy = SimplePolicy::strict(now) ### Grace Period Behavior -**Collateral grace** (`collateral_grace_period`): Extends the collateral expiration window. If `earliest_expiration + grace >= now`, the quote is accepted. Does **not** skip advisory checks — stale collateral doesn't invalidate advisory data. +**Collateral grace** (`collateral_grace_period`): Extends the collateral expiration window. If `earliest_expiration + grace >= now`, the quote is accepted. **Platform grace** (`platform_grace_period`): Applies only to the **platform** TCB level. For `OutOfDate` / `OutOfDateConfigurationNeeded`, checks `platform.tcb_date_tag + grace >= now`. For pure `OutOfDate`, only the **platform** advisories are skipped during the grace window. For `OutOfDateConfigurationNeeded`, platform advisories are still checked. diff --git a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi index 50c8eaa..83717e3 100644 --- a/python-bindings/python/dcap_qvl/_dcap_qvl.pyi +++ b/python-bindings/python/dcap_qvl/_dcap_qvl.pyi @@ -323,22 +323,26 @@ class PySimplePolicy: policy = SimplePolicy.strict(now_secs) \\ .allow_status("SWHardeningNeeded") \\ - .accept_advisory("INTEL-SA-00334") \\ + .reject_advisory("INTEL-SA-00334") \\ .collateral_grace_period(90 * 24 * 3600) \\ .qe_grace_period(7 * 24 * 3600) """ @staticmethod def strict(now_secs: int) -> "PySimplePolicy": - """Create a strict policy: only UpToDate, no grace, no advisory tolerance.""" + """Create a strict policy: only UpToDate, no grace, no advisory blacklist.""" ... def allow_status(self, status: str) -> "PySimplePolicy": """Allow an additional TCB status (e.g. "SWHardeningNeeded").""" ... - def accept_advisory(self, advisory_id: str) -> "PySimplePolicy": - """Accept a specific advisory ID (e.g. "INTEL-SA-00334").""" + def reject_advisory(self, advisory_id: str) -> "PySimplePolicy": + """Reject a specific advisory ID (e.g. "INTEL-SA-00334").""" + ... + + def reject_advisories(self, advisory_ids: List[str]) -> "PySimplePolicy": + """Reject multiple advisory IDs at once.""" ... def collateral_grace_period(self, secs: int) -> "PySimplePolicy": diff --git a/src/policy/mod.rs b/src/policy/mod.rs index 43aba3d..d6ea6d4 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -36,7 +36,7 @@ pub use rego::RegoPolicySet; /// let policy = SimplePolicy::strict(now_unix_secs) /// .allow_status(TcbStatus::SWHardeningNeeded) /// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) -/// .accept_advisory("INTEL-SA-00334"); +/// .reject_advisory("INTEL-SA-00334"); /// ``` /// /// Implement this trait directly only for logic that [`SimplePolicy`] cannot express. diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 6b7f525..7b4a0b7 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -14,7 +14,7 @@ use { /// /// Covers the 9 checks from Intel's Appraisal framework (`qal_script.rego`) /// without requiring a Rego engine. Strict by default: only `UpToDate`, -/// no grace period, no advisory tolerance. +/// no grace period, no advisory blacklist. /// /// # Example /// ```no_run @@ -31,7 +31,7 @@ use { /// let policy = SimplePolicy::strict(now) /// .allow_status(TcbStatus::SWHardeningNeeded) /// .collateral_grace_period(Duration::from_secs(90 * 24 * 3600)) -/// .accept_advisory("INTEL-SA-00334"); +/// .reject_advisory("INTEL-SA-00334"); /// ``` #[derive(Clone, Debug)] pub struct SimplePolicy { @@ -46,8 +46,8 @@ pub struct SimplePolicy { // TCB evaluation min_tcb_eval_data_number: Option, - // Advisory whitelist (all advisories in quote must be in this set) - accepted_advisory_ids: Vec, + // Advisory blacklist (quote is rejected if any advisory is in this set) + rejected_advisory_ids: Vec, // Platform flags (default false = reject if True) allow_dynamic_platform: bool, @@ -88,7 +88,7 @@ impl SimplePolicy { platform_grace_period: 0, qe_grace_period: 0, min_tcb_eval_data_number: None, - accepted_advisory_ids: Vec::new(), + rejected_advisory_ids: Vec::new(), allow_dynamic_platform: false, allow_cached_keys: false, allow_smt: false, @@ -97,7 +97,7 @@ impl SimplePolicy { } /// Create a strict policy: only `UpToDate` status is accepted, - /// no grace period, no advisory tolerance. + /// no grace period, no advisory blacklist. pub fn strict(now_secs: u64) -> Self { Self::new_with_statuses(now_secs, Self::UP_TO_DATE) } @@ -137,11 +137,18 @@ impl SimplePolicy { self } - /// Accept a specific advisory ID. All advisories in the quote must be in - /// the accepted set or validation fails. By default the set is empty, - /// rejecting any quote with advisories. - pub fn accept_advisory(mut self, id: impl Into) -> Self { - self.accepted_advisory_ids.push(id.into()); + /// Reject a specific advisory ID. Quotes containing any advisory in the + /// rejected set fail validation. By default the set is empty, allowing all + /// advisory IDs. + pub fn reject_advisory(mut self, id: impl Into) -> Self { + self.rejected_advisory_ids.push(id.into()); + self + } + + /// Reject multiple advisory IDs at once. + pub fn reject_advisories(mut self, ids: &[impl AsRef]) -> Self { + self.rejected_advisory_ids + .extend(ids.iter().map(|id| id.as_ref().to_string())); self } @@ -186,8 +193,8 @@ impl Policy for SimplePolicy { date_tag.saturating_add(grace_period) >= now } - fn advisory_accepted(accepted_advisory_ids: &[String], id: &str) -> bool { - accepted_advisory_ids + fn advisory_rejected(rejected_advisory_ids: &[String], id: &str) -> bool { + rejected_advisory_ids .iter() .any(|a| a.eq_ignore_ascii_case(id)) } @@ -250,26 +257,15 @@ impl Policy for SimplePolicy { ); } - // 2. Advisory ID whitelist. - // Platform grace skips only platform advisories for pure OutOfDate. - // QE grace skips only QE advisories for pure OutOfDate. - let skip_platform_advisories = - platform_in_grace && data.platform.tcb_level.tcb_status == TcbStatus::OutOfDate; - let skip_qe_advisories = - qe_in_grace && data.qe.tcb_level.tcb_status == TcbStatus::OutOfDate; - - if !skip_platform_advisories { - for id in &data.platform.tcb_level.advisory_ids { - if !advisory_accepted(&self.accepted_advisory_ids, id) { - bail!("Advisory ID {id} is not in the accepted set"); - } + // 2. Advisory ID blacklist. + for id in &data.platform.tcb_level.advisory_ids { + if advisory_rejected(&self.rejected_advisory_ids, id) { + bail!("Advisory ID {id} is rejected by policy"); } } - if !skip_qe_advisories { - for id in &data.qe.tcb_level.advisory_ids { - if !advisory_accepted(&self.accepted_advisory_ids, id) { - bail!("Advisory ID {id} is not in the accepted set"); - } + for id in &data.qe.tcb_level.advisory_ids { + if advisory_rejected(&self.rejected_advisory_ids, id) { + bail!("Advisory ID {id} is rejected by policy"); } } @@ -322,7 +318,7 @@ impl Policy for SimplePolicy { /// ```json /// { /// "allowed_statuses": ["UpToDate", "SWHardeningNeeded"], -/// "accepted_advisories": ["INTEL-SA-00334"], +/// "rejected_advisory_ids": ["INTEL-SA-00334"], /// "collateral_grace_period_secs": 2592000, /// "allow_smt": true /// } @@ -332,7 +328,7 @@ pub struct SimplePolicyConfig { #[serde(default)] pub allowed_statuses: Vec, #[serde(default)] - pub accepted_advisories: Vec, + pub rejected_advisory_ids: Vec, #[serde(default)] pub collateral_grace_period_secs: u64, #[serde(default)] @@ -365,8 +361,8 @@ impl SimplePolicyConfig { } p }; - for id in self.accepted_advisories { - policy = policy.accept_advisory(id); + for id in self.rejected_advisory_ids { + policy = policy.reject_advisory(id); } if self.collateral_grace_period_secs > 0 { policy = policy @@ -517,34 +513,46 @@ mod tests { assert!(policy.validate(&data).is_ok()); } - // -- Advisory ID whitelist -- + // -- Advisory ID blacklist -- #[test] - fn policy_rejects_unknown_advisory() { + fn policy_allows_advisory_when_not_blacklisted() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = SimplePolicy::strict(1_702_000_000); - let err = policy.validate(&data).unwrap_err().to_string(); - assert!(err.contains("INTEL-SA-00615"), "{err}"); + assert!(policy.validate(&data).is_ok()); } #[test] - fn policy_accepts_whitelisted_advisory() { + fn policy_rejects_blacklisted_advisory() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - let policy = SimplePolicy::strict(1_702_000_000).accept_advisory("INTEL-SA-00615"); - assert!(policy.validate(&data).is_ok()); + let policy = SimplePolicy::strict(1_702_000_000).reject_advisory("INTEL-SA-00615"); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); } #[test] - fn policy_advisory_case_insensitive() { + fn policy_advisory_blacklist_case_insensitive() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - let policy = SimplePolicy::strict(1_702_000_000).accept_advisory("intel-sa-00615"); - assert!(policy.validate(&data).is_ok()); + let policy = SimplePolicy::strict(1_702_000_000).reject_advisory("intel-sa-00615"); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); + } + + #[test] + fn policy_reject_advisories_batch() { + let mut data = make_test_supplemental(UpToDate); + data.tcb.advisory_ids = vec!["INTEL-SA-00820".to_string()]; + data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00820".to_string()]; + let policy = SimplePolicy::strict(1_702_000_000) + .reject_advisories(&["INTEL-SA-00615", "INTEL-SA-00820"]); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00820"), "{err}"); } #[test] @@ -552,7 +560,6 @@ mod tests { let data = make_test_supplemental(UpToDate); assert!(data.tcb.advisory_ids.is_empty()); let policy = SimplePolicy::strict(1_702_000_000); - // Empty advisory list in quote → nothing to check against whitelist → passes assert!(policy.validate(&data).is_ok()); } @@ -739,55 +746,52 @@ mod tests { assert!(policy.validate(&data).is_ok()); } - // -- Advisory skipped during grace -- + // -- Advisory blacklist during grace -- #[test] - fn policy_advisory_checked_during_collateral_grace() { + fn policy_blacklist_checked_during_collateral_grace() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - // Collateral grace doesn't skip advisory checks — stale collateral - // doesn't invalidate advisory data. let policy = SimplePolicy::strict(1_704_000_000) - .collateral_grace_period(Duration::from_secs(2_000_000)); + .collateral_grace_period(Duration::from_secs(2_000_000)) + .reject_advisory("INTEL-SA-00615"); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("INTEL-SA-00615"), "{err}"); } #[test] - fn policy_advisory_skipped_during_platform_grace() { + fn policy_blacklist_checked_during_platform_grace() { let mut data = make_test_supplemental(OutOfDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - // now=1_702_000_000, tcb_date_tag=1_690_000_000 (old), - // grace=13_000_000 → within grace → advisories skipped let policy = SimplePolicy::strict(1_702_000_000) .allow_status(OutOfDate) - .platform_grace_period(Duration::from_secs(13_000_000)); - assert!(policy.validate(&data).is_ok()); + .platform_grace_period(Duration::from_secs(13_000_000)) + .reject_advisory("INTEL-SA-00615"); + let err = policy.validate(&data).unwrap_err().to_string(); + assert!(err.contains("INTEL-SA-00615"), "{err}"); } #[test] - fn policy_advisory_not_skipped_for_out_of_date_config_needed() { - // OutOfDateConfigurationNeeded should NOT skip advisory checks during grace, - // because the Configuration advisories are unrelated to the OutOfDate grace. + fn policy_blacklist_checked_for_out_of_date_config_needed() { let mut data = make_test_supplemental(OutOfDateConfigurationNeeded); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; let policy = SimplePolicy::strict(1_702_000_000) .allow_status(OutOfDateConfigurationNeeded) - .platform_grace_period(Duration::from_secs(13_000_000)); + .platform_grace_period(Duration::from_secs(13_000_000)) + .reject_advisory("INTEL-SA-00615"); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("INTEL-SA-00615"), "{err}"); } #[test] - fn policy_advisory_checked_without_grace() { + fn policy_blacklist_checked_without_grace() { let mut data = make_test_supplemental(UpToDate); data.tcb.advisory_ids = vec!["INTEL-SA-00615".to_string()]; data.platform.tcb_level.advisory_ids = vec!["INTEL-SA-00615".to_string()]; - // No grace period → advisories checked normally - let policy = SimplePolicy::strict(1_702_000_000); + let policy = SimplePolicy::strict(1_702_000_000).reject_advisory("INTEL-SA-00615"); let err = policy.validate(&data).unwrap_err().to_string(); assert!(err.contains("INTEL-SA-00615"), "{err}"); } diff --git a/src/python.rs b/src/python.rs index 4d30313..91e6411 100644 --- a/src/python.rs +++ b/src/python.rs @@ -470,7 +470,7 @@ fn parse_tcb_status(s: &str) -> PyResult { #[pymethods] impl PySimplePolicy { - /// Create a strict policy: only `UpToDate` status, no grace, no advisory tolerance. + /// Create a strict policy: only `UpToDate` status, no grace, no advisory blacklist. #[staticmethod] fn strict(now_secs: u64) -> Self { Self { @@ -486,10 +486,17 @@ impl PySimplePolicy { }) } - /// Accept a specific advisory ID (e.g. "INTEL-SA-00334"). - fn accept_advisory(&self, advisory_id: &str) -> Self { + /// Reject a specific advisory ID (e.g. "INTEL-SA-00334"). + fn reject_advisory(&self, advisory_id: &str) -> Self { Self { - inner: self.inner.clone().accept_advisory(advisory_id), + inner: self.inner.clone().reject_advisory(advisory_id), + } + } + + /// Reject multiple advisory IDs at once. + fn reject_advisories(&self, advisory_ids: Vec) -> Self { + Self { + inner: self.inner.clone().reject_advisories(&advisory_ids), } } diff --git a/src/verify.rs b/src/verify.rs index f04ae4e..4402356 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -348,7 +348,7 @@ fn js_parse_tcb_status(s: &str) -> Result { #[cfg(feature = "js")] #[wasm_bindgen(js_class = "SimplePolicy")] impl JsSimplePolicy { - /// Create a strict policy: only `UpToDate`, no grace period, no advisory tolerance. + /// Create a strict policy: only `UpToDate`, no grace period, no advisory blacklist. #[wasm_bindgen(constructor)] pub fn strict(now_secs: u64) -> Self { Self { @@ -364,10 +364,17 @@ impl JsSimplePolicy { }) } - /// Accept a specific advisory ID (e.g. "INTEL-SA-00334"). - pub fn accept_advisory(self, id: &str) -> Self { + /// Reject a specific advisory ID (e.g. "INTEL-SA-00334"). + pub fn reject_advisory(self, id: &str) -> Self { Self { - inner: self.inner.accept_advisory(id), + inner: self.inner.reject_advisory(id), + } + } + + /// Reject multiple advisory IDs at once. + pub fn reject_advisories(self, ids: Vec) -> Self { + Self { + inner: self.inner.reject_advisories(&ids), } } From 7b9baf1fca20f2ead8d6bd1efb2476d0f1e394fb Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 12 Mar 2026 07:42:45 +0000 Subject: [PATCH 29/33] feat: derive QuoteVerificationResult and nested types --- src/lib.rs | 4 +++- src/policy/mod.rs | 11 ++++++++++- src/utils.rs | 26 ++++++++++++++++++++++++++ src/verify.rs | 41 +++++++++++++++++++++++++++-------------- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 983ddb5..c2be080 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,9 @@ use borsh::BorshSchema; #[cfg(feature = "borsh")] use borsh::{BorshDeserialize, BorshSerialize}; -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive( + Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, +)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] pub struct QuoteCollateralV3 { diff --git a/src/policy/mod.rs b/src/policy/mod.rs index d6ea6d4..63583be 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use serde::{Deserialize, Serialize}; use { crate::constants::*, @@ -9,6 +10,11 @@ use { alloc::vec::Vec, }; +#[cfg(feature = "borsh_schema")] +use borsh::BorshSchema; +#[cfg(feature = "borsh")] +use borsh::{BorshDeserialize, BorshSerialize}; + mod simple; pub use simple::{SimplePolicy, SimplePolicyConfig}; @@ -51,7 +57,10 @@ pub trait Policy { /// /// These flags are only present in PCK certificates issued by the **Platform CA**. /// For Processor CA certificates, the value is [`Undefined`](PckCertFlag::Undefined). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] +#[cfg_attr(feature = "borsh", borsh(use_discriminant = true))] pub enum PckCertFlag { /// The flag is explicitly false (ASN.1 BOOLEAN FALSE). False = 0, diff --git a/src/utils.rs b/src/utils.rs index 9c07931..c0f8836 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -104,6 +104,32 @@ pub(crate) fn extract_raw_certs(cert_chain: &[u8]) -> Result>> { .collect()) } +pub(crate) mod serde_vec_bytes { + use alloc::vec::Vec; + use serde::ser::SerializeSeq; + use serde::{Deserialize, Deserializer, Serializer}; + use serde_bytes::{ByteBuf, Bytes}; + + pub fn serialize(value: &[Vec], serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(Some(value.len()))?; + for item in value { + seq.serialize_element(Bytes::new(item))?; + } + seq.end() + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let value = Vec::::deserialize(deserializer)?; + Ok(value.into_iter().map(ByteBuf::into_vec).collect()) + } +} + pub fn extract_certs<'a>(cert_chain: &'a [u8]) -> Result>> { let mut certs = Vec::>::new(); diff --git a/src/verify.rs b/src/verify.rs index 4402356..47cb3f3 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -95,9 +95,13 @@ use borsh::{BorshDeserialize, BorshSerialize}; /// /// [`SupplementalData`] is built lazily via [`supplemental()`](Self::supplemental) — /// the `verify()` call itself does the minimum work (crypto only). +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] pub struct QuoteVerificationResult { report: Report, collateral: QuoteCollateralV3, + #[serde(with = "crate::utils::serde_vec_bytes")] pck_cert_chain_der: Vec>, // -- core verification results (always computed) -- tee_type: u32, @@ -109,8 +113,8 @@ pub struct QuoteVerificationResult { qe_report: EnclaveReport, tcb_eval_data_number: u32, qe_tcb_eval_data_number: u32, - root_ca_der: Vec, - sha384: fn(&[u8]) -> [u8; 48], + #[serde(with = "serde_bytes")] + root_key_id: [u8; 48], } impl QuoteVerificationResult { @@ -134,16 +138,7 @@ impl QuoteVerificationResult { compute_collateral_time_window(&self.collateral, &pck_certs, &tcb_info, &qe_identity)?; // root_key_id: SHA-384 of root CA's raw public key bytes - let root_key_id = { - let root_cert: x509_cert::Certificate = der::Decode::from_der(&self.root_ca_der) - .context("root CA already validated but failed to re-parse")?; - let raw_key = root_cert - .tbs_certificate - .subject_public_key_info - .subject_public_key - .raw_bytes(); - (self.sha384)(raw_key) - }; + let root_key_id = self.root_key_id; // CRL numbers let root_ca_crl_num = @@ -802,12 +797,20 @@ fn verify_pck_cert_chain( } /// Result from PCK certificate chain verification +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] struct PckCertChainResult { + #[serde(with = "crate::utils::serde_vec_bytes")] pck_cert_chain_der: Vec>, + #[serde(with = "serde_bytes")] pck_leaf_der: Vec, + #[serde(with = "serde_bytes")] ppid: Vec, + #[serde(with = "serde_bytes")] cpu_svn: [u8; 16], pce_svn: u16, + #[serde(with = "serde_bytes")] fmspc: [u8; 6], pce_id: u16, sgx_type: u8, @@ -1127,6 +1130,17 @@ fn verify_impl( bail!("TCB status is invalid: Revoked"); } + let root_key_id = { + let root_cert: x509_cert::Certificate = + der::Decode::from_der(root_ca_der).context("Failed to parse root CA certificate")?; + let raw_key = root_cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(); + (backend.sha384)(raw_key) + }; + // Validate report attributes (debug mode check, etc.) validate_attrs("e.report)?; @@ -1145,8 +1159,7 @@ fn verify_impl( .tcb_evaluation_data_number .min(qe_identity.tcb_evaluation_data_number), qe_tcb_eval_data_number: qe_identity.tcb_evaluation_data_number, - root_ca_der: root_ca_der.to_vec(), - sha384: backend.sha384, + root_key_id, }) } From ebd1f0d36cb6a8364936826f041380251f6574b5 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 12 Mar 2026 07:58:28 +0000 Subject: [PATCH 30/33] fix: reject pre-epoch RFC3339 timestamps --- src/policy/rego.rs | 23 +++++++++++----------- src/policy/simple.rs | 6 +++--- src/utils.rs | 8 ++++++++ src/verify.rs | 46 ++++++++++++++++++-------------------------- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/policy/rego.rs b/src/policy/rego.rs index 8ab0bd1..c2e5456 100644 --- a/src/policy/rego.rs +++ b/src/policy/rego.rs @@ -1,6 +1,7 @@ use super::*; use serde_json::json; +use crate::utils::parse_rfc3339_unix_secs; use anyhow::{bail, Result}; /// Convert a unix timestamp (seconds) to an RFC3339 string. @@ -126,7 +127,7 @@ fn build_platform_measurement(data: &SupplementalData) -> serde_json::Value { } /// Build QE Identity measurement for Rego appraisal (TDX). -fn build_qe_measurement(data: &SupplementalData) -> serde_json::Value { +fn build_qe_measurement(data: &SupplementalData) -> Result { let mut m = serde_json::Map::new(); m.insert( @@ -134,10 +135,8 @@ fn build_qe_measurement(data: &SupplementalData) -> serde_json::Value { tcb_status_to_rego_array(data.qe.tcb_level.tcb_status), ); - let qe_tcb_date = chrono::DateTime::parse_from_rfc3339(&data.qe.tcb_level.tcb_date) - .ok() - .map(|dt| dt.timestamp() as u64) - .unwrap_or(0); + let qe_tcb_date = parse_rfc3339_unix_secs(&data.qe.tcb_level.tcb_date) + .map_err(|e| anyhow::anyhow!("Failed to parse QE TCB date: {e}"))?; let qe_date_str = unix_to_rfc3339(qe_tcb_date); if !qe_date_str.is_empty() { m.insert("tcb_level_date_tag".into(), json!(qe_date_str)); @@ -162,7 +161,7 @@ fn build_qe_measurement(data: &SupplementalData) -> serde_json::Value { json!(hex::encode_upper(data.platform.root_key_id)), ); - serde_json::Value::Object(m) + Ok(serde_json::Value::Object(m)) } // ── Tenant measurement helpers ───────────────────────────────────────── @@ -570,7 +569,7 @@ impl Policy for RegoPolicy { impl Policy for RegoPolicySet { fn validate(&self, data: &SupplementalData) -> Result<()> { - let qvl_result = to_rego_qvl_result(data); + let qvl_result = to_rego_qvl_result(data)?; let policy_refs: Vec<&serde_json::Value> = self.policies.iter().collect(); eval_rego_engine(&self.engine, &policy_refs, qvl_result) } @@ -580,7 +579,7 @@ impl Policy for RegoPolicySet { /// /// SGX quotes produce 2 entries (platform + enclave). /// TDX quotes produce 3 entries (platform + QE identity + TD). -fn to_rego_qvl_result(data: &SupplementalData) -> Vec { +fn to_rego_qvl_result(data: &SupplementalData) -> Result> { use crate::quote::Report; let mut result = Vec::new(); @@ -596,7 +595,7 @@ fn to_rego_qvl_result(data: &SupplementalData) -> Vec { if matches!(data.report, Report::TD10(_) | Report::TD15(_)) { result.push(json!({ "environment": { "class_id": "3769258c-75e6-4bc7-8d72-d2b0e224cad2" }, - "measurement": build_qe_measurement(data), + "measurement": build_qe_measurement(data)?, })); } @@ -617,7 +616,7 @@ fn to_rego_qvl_result(data: &SupplementalData) -> Vec { "measurement": tenant_m, })); - result + Ok(result) } #[cfg(test)] @@ -912,7 +911,7 @@ mod tests { #[test] fn rego_qe_measurement_fields() { let data = make_rego_supplemental(UpToDate); - let m = build_qe_measurement(&data); + let m = build_qe_measurement(&data).unwrap(); assert!(m.get("tcb_status").is_some()); assert_eq!(m.get("tcb_eval_num").unwrap(), 17); assert!(m.get("root_key_id").is_some()); @@ -1071,7 +1070,7 @@ mod tests { data.qe_iden_earliest_issue_date = 1_850_000_000; data.qe_iden_latest_issue_date = 1_850_100_000; data.qe_iden_earliest_expiration_date = 1_950_000_000; - let m = build_qe_measurement(&data); + let m = build_qe_measurement(&data).unwrap(); // QE measurement should use qe_iden_* dates, not the global ones let earliest = m.get("earliest_issue_date").unwrap().as_str().unwrap(); let expected = "2028-08-16T"; // 1_850_000_000 = 2028-08-16T00:53:20Z diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 7b4a0b7..3f42c8c 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use { super::{PckCertFlag, Policy, SupplementalData}, crate::tcb_info::TcbStatus, + crate::utils::parse_rfc3339_unix_secs, alloc::string::String, alloc::vec::Vec, }; @@ -242,9 +243,8 @@ impl Policy for SimplePolicy { } // 4b. QE TCB freshness: QE tcb_date + grace >= now. - let qe_tcb_date_tag = chrono::DateTime::parse_from_rfc3339(&data.qe.tcb_level.tcb_date) - .map_err(|e| anyhow::anyhow!("Failed to parse QE TCB date: {e}"))? - .timestamp() as u64; + let qe_tcb_date_tag = parse_rfc3339_unix_secs(&data.qe.tcb_level.tcb_date) + .map_err(|e| anyhow::anyhow!("Failed to parse QE TCB date: {e}"))?; let qe_is_out_of_date = data.qe.tcb_level.tcb_status == TcbStatus::OutOfDate; let qe_in_grace = qe_is_out_of_date && within_grace(qe_tcb_date_tag, self.qe_grace_period, self.now); diff --git a/src/utils.rs b/src/utils.rs index c0f8836..626807e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -104,6 +104,14 @@ pub(crate) fn extract_raw_certs(cert_chain: &[u8]) -> Result>> { .collect()) } +pub(crate) fn parse_rfc3339_unix_secs(value: &str) -> Result { + chrono::DateTime::parse_from_rfc3339(value) + .context("Failed to parse RFC3339 datetime")? + .timestamp() + .try_into() + .context("RFC3339 datetime is before Unix epoch") +} + pub(crate) mod serde_vec_bytes { use alloc::vec::Vec; use serde::ser::SerializeSeq; diff --git a/src/verify.rs b/src/verify.rs index 47cb3f3..16fc69a 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -24,7 +24,9 @@ pub(crate) use self::ring as default_crypto; pub(crate) use self::rustcrypto as default_crypto; use crate::{ quote::{Report, TDAttributes}, - utils::{encode_as_der, extract_certs, parse_crls, verify_certificate_chain}, + utils::{ + encode_as_der, extract_certs, parse_crls, parse_rfc3339_unix_secs, verify_certificate_chain, + }, }; use crate::{ quote::{TDReport10, TDReport15}, @@ -146,10 +148,8 @@ impl QuoteVerificationResult { let pck_crl_num = crate::utils::extract_crl_number(&self.collateral.pck_crl).unwrap_or(0); // tcb_date_tag - let tcb_date_tag = chrono::DateTime::parse_from_rfc3339(&self.platform_tcb_level.tcb_date) - .ok() - .map(|dt| dt.timestamp() as u64) - .unwrap_or(0); + let tcb_date_tag = parse_rfc3339_unix_secs(&self.platform_tcb_level.tcb_date) + .context("Failed to parse platform TCB date")?; Ok(SupplementalData { tee_type: self.tee_type, @@ -632,16 +632,14 @@ fn verify_tcb_info_signature( .context("Failed to decode TcbInfo")?; // Check validity window - let issue_date = chrono::DateTime::parse_from_rfc3339(&tcb_info.issue_date) - .ok() + let issue_date = parse_rfc3339_unix_secs(&tcb_info.issue_date) .context("Failed to parse TCB Info issue date")?; - let next_update = chrono::DateTime::parse_from_rfc3339(&tcb_info.next_update) - .ok() + let next_update = parse_rfc3339_unix_secs(&tcb_info.next_update) .context("Failed to parse TCB Info next update")?; - if now.as_secs() < issue_date.timestamp() as u64 { + if now.as_secs() < issue_date { bail!("TCBInfo issue date is in the future"); } - if now.as_secs() > next_update.timestamp() as u64 { + if now.as_secs() > next_update { bail!("TCBInfo expired"); } @@ -687,16 +685,14 @@ fn verify_qe_identity_signature( .context("Failed to decode QeIdentity")?; // Check validity window - let issue_date = chrono::DateTime::parse_from_rfc3339(&qe_identity.issue_date) - .ok() + let issue_date = parse_rfc3339_unix_secs(&qe_identity.issue_date) .context("Failed to parse QE Identity issue date")?; - let next_update = chrono::DateTime::parse_from_rfc3339(&qe_identity.next_update) - .ok() + let next_update = parse_rfc3339_unix_secs(&qe_identity.next_update) .context("Failed to parse QE Identity next update")?; - if now.as_secs() < issue_date.timestamp() as u64 { + if now.as_secs() < issue_date { bail!("QE Identity issue date is in the future"); } - if now.as_secs() > next_update.timestamp() as u64 { + if now.as_secs() > next_update { bail!("QE Identity expired"); } @@ -1192,12 +1188,6 @@ fn compute_collateral_time_window( tcb_info: &TcbInfo, qe_identity: &QeIdentity, ) -> Result { - fn parse_rfc3339_ts(s: &str) -> Option { - chrono::DateTime::parse_from_rfc3339(s) - .ok() - .map(|dt| dt.timestamp() as u64) - } - fn parse_crl_dates(crl_der: &[u8]) -> Result<(u64, Option)> { use der::Decode as _; let crl = x509_cert::crl::CertificateList::from_der(crl_der) @@ -1251,12 +1241,14 @@ fn compute_collateral_time_window( } // TCBInfo dates (already parsed upstream) - let tcb_issue = parse_rfc3339_ts(&tcb_info.issue_date).context("TCBInfo issueDate")?; - let tcb_next = parse_rfc3339_ts(&tcb_info.next_update).context("TCBInfo nextUpdate")?; + let tcb_issue = parse_rfc3339_unix_secs(&tcb_info.issue_date).context("TCBInfo issueDate")?; + let tcb_next = parse_rfc3339_unix_secs(&tcb_info.next_update).context("TCBInfo nextUpdate")?; // QEIdentity dates (already parsed upstream) - let qe_issue = parse_rfc3339_ts(&qe_identity.issue_date).context("QEIdentity issueDate")?; - let qe_next = parse_rfc3339_ts(&qe_identity.next_update).context("QEIdentity nextUpdate")?; + let qe_issue = + parse_rfc3339_unix_secs(&qe_identity.issue_date).context("QEIdentity issueDate")?; + let qe_next = + parse_rfc3339_unix_secs(&qe_identity.next_update).context("QEIdentity nextUpdate")?; let mut earliest_issue = tcb_issue.min(qe_issue); let mut latest_issue = tcb_issue.max(qe_issue); From 73246f7355bb5caa8773db4fe99a9578b58b64a0 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 12 Mar 2026 09:55:28 +0000 Subject: [PATCH 31/33] feat: add strict mode to CLI verify --- cli/README.md | 6 ++++++ cli/src/main.rs | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cli/README.md b/cli/README.md index 8b9e197..a38f2e4 100644 --- a/cli/README.md +++ b/cli/README.md @@ -9,3 +9,9 @@ git clone https://github.com/Phala-Network/dcap-qvl.git cd dcap-qvl/cli cargo run -- decode-quote --hex ../sample/tdx-quote.hex | jq . ``` + +Strict verification: + +```sh +cargo run -- verify --strict --hex ../sample/tdx-quote.hex +``` diff --git a/cli/src/main.rs b/cli/src/main.rs index 7085811..f5e2aa3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,6 +10,7 @@ use dcap_qvl::collateral::{get_collateral, PHALA_PCCS_URL}; use dcap_qvl::intel; use dcap_qvl::quote::Quote; use dcap_qvl::verify::{ring, QuoteVerifier}; +use dcap_qvl::SimplePolicy; use der::Decode; use serde::Serialize; use x509_cert::Certificate; @@ -48,6 +49,9 @@ struct VerifyQuoteArgs { /// Indicate the quote file is in hex format #[arg(long)] hex: bool, + /// Apply SimplePolicy::strict(now) after cryptographic verification + #[arg(long)] + strict: bool, /// The quote file quote_file: PathBuf, } @@ -107,12 +111,22 @@ async fn command_verify_quote(args: VerifyQuoteArgs) -> Result<()> { let result = verifier .verify("e, collateral, now) .context("Failed to verify quote")?; - let report = result.into_report_unchecked(); + let report = if args.strict { + result + .validate(&SimplePolicy::strict(now)) + .context("Strict policy validation failed")? + } else { + result.into_report_unchecked() + }; println!( "{}", serde_json::to_string(&report).context("Failed to serialize report")? ); - eprintln!("Quote verified"); + if args.strict { + eprintln!("Quote verified under strict policy"); + } else { + eprintln!("Quote verified"); + } Ok(()) } From 4c2e3b36c8faec5c4adad303d73de9d6f8aeda0c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 12 Mar 2026 10:26:25 +0000 Subject: [PATCH 32/33] feat: add ppid() accessor to QuoteVerificationResult Lightweight accessor that avoids cloning the entire QVR or calling into_report_unchecked() just to read the PPID. --- src/verify.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/verify.rs b/src/verify.rs index 16fc69a..0ae0643 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -205,6 +205,11 @@ impl QuoteVerificationResult { Ok(self.into_report_unchecked()) } + /// The Platform Provisioning ID (PPID) extracted from the PCK certificate. + pub fn ppid(&self) -> &[u8] { + &self.pck_ext.ppid + } + /// Convert directly into [`VerifiedReport`] **without applying any policy**. /// /// # Warning From e508e1c4b6359d732fdbdd1dd55733354c55e09d Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 20 Mar 2026 01:17:17 +0000 Subject: [PATCH 33/33] refactor: preserve PCK pce_id as raw bytes --- src/policy/mod.rs | 4 ++-- src/policy/rego.rs | 2 +- src/policy/simple.rs | 2 +- src/verify.rs | 13 +++++-------- tests/verify_quote.rs | 6 +----- 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/policy/mod.rs b/src/policy/mod.rs index 63583be..0a4b1dd 100644 --- a/src/policy/mod.rs +++ b/src/policy/mod.rs @@ -161,8 +161,8 @@ pub struct PckIdentity { pub cpu_svn: CpuSvn, /// PCE ISV Security Version Number. pub pce_svn: Svn, - /// PCE ID. - pub pce_id: u16, + /// PCE ID (raw value from the PCK certificate SGX extension). + pub pce_id: Vec, /// FMSPC (6 bytes). pub fmspc: Fmspc, /// SGX type: 0=Standard, 1=Scalable, 2=ScalableWithIntegrity. diff --git a/src/policy/rego.rs b/src/policy/rego.rs index c2e5456..88451ef 100644 --- a/src/policy/rego.rs +++ b/src/policy/rego.rs @@ -659,7 +659,7 @@ mod tests { ppid: vec![0u8; 16], cpu_svn: [0u8; 16], pce_svn: 13, - pce_id: 0, + pce_id: vec![0u8; 2], fmspc: [0u8; 6], sgx_type: 0, platform_instance_id: None, diff --git a/src/policy/simple.rs b/src/policy/simple.rs index 3f42c8c..6c6cc0e 100644 --- a/src/policy/simple.rs +++ b/src/policy/simple.rs @@ -422,7 +422,7 @@ mod tests { ppid: vec![0u8; 16], cpu_svn: [0u8; 16], pce_svn: 13, - pce_id: 0, + pce_id: vec![0u8; 2], fmspc: [0u8; 6], sgx_type: 0, platform_instance_id: None, diff --git a/src/verify.rs b/src/verify.rs index 0ae0643..8dca45c 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -165,7 +165,7 @@ impl QuoteVerificationResult { ppid: self.pck_ext.ppid.clone(), cpu_svn: self.pck_ext.cpu_svn, pce_svn: self.pck_ext.pce_svn, - pce_id: self.pck_ext.pce_id, + pce_id: self.pck_ext.pce_id.clone(), fmspc: self.pck_ext.fmspc, sgx_type: self.pck_ext.sgx_type, platform_instance_id: self.pck_ext.platform_instance_id, @@ -765,12 +765,8 @@ fn verify_pck_cert_chain( // Extract Intel extension data from PCK cert (parsed once) let pck_ext = intel::parse_pck_extension(pck_leaf)?; - // Convert pce_id bytes to u16 (big-endian) - let pce_id = match pck_ext.pce_id.as_slice() { - [hi, lo] => u16::from_be_bytes([*hi, *lo]), - [lo] => u16::from(*lo), - _ => 0, - }; + // Preserve pce_id as the raw value from the PCK cert SGX extension. + let pce_id = pck_ext.pce_id.clone(); // Convert platform_instance_id to fixed-size array let platform_instance_id = pck_ext.platform_instance_id.as_ref().and_then(|v| { @@ -813,7 +809,8 @@ struct PckCertChainResult { pce_svn: u16, #[serde(with = "serde_bytes")] fmspc: [u8; 6], - pce_id: u16, + #[serde(with = "serde_bytes")] + pce_id: Vec, sgx_type: u8, platform_instance_id: Option<[u8; 16]>, dynamic_platform: PckCertFlag, diff --git a/tests/verify_quote.rs b/tests/verify_quote.rs index 8142434..896cb77 100644 --- a/tests/verify_quote.rs +++ b/tests/verify_quote.rs @@ -198,11 +198,7 @@ fn sgx_supplemental_data_cross_validation() { assert_eq!(s.platform.pck.ppid, pck_ext.ppid); assert_eq!(s.platform.pck.sgx_type, pck_ext.sgx_type as u8); - let expected_pce_id = match pck_ext.pce_id.len() { - 2 => u16::from_be_bytes([pck_ext.pce_id[0], pck_ext.pce_id[1]]), - 1 => u16::from(pck_ext.pce_id[0]), - _ => 0, - }; + let expected_pce_id = pck_ext.pce_id.clone(); assert_eq!(s.platform.pck.pce_id, expected_pce_id); // ── TEE type ────────────────────────────────────────────────────────