From dc900bd514ba71b9329e92e230b751ff3a64f61a Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Tue, 15 Sep 2026 22:59:28 +0000 Subject: [PATCH 01/18] Add minimal Arm CCA profile support --- corim-cli/Cargo.toml | 5 +- corim-cli/src/generate.rs | 4 + corim-cli/src/main.rs | 4 + corim/Cargo.toml | 4 + corim/src/profile.rs | 13 ++ corim/src/profile/cca/mod.rs | 241 +++++++++++++++++++++++++++++++ corim/tests/profile_cca_tests.rs | 165 +++++++++++++++++++++ 7 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 corim/src/profile/cca/mod.rs create mode 100644 corim/tests/profile_cca_tests.rs diff --git a/corim-cli/Cargo.toml b/corim-cli/Cargo.toml index 3233a62..e1d50d1 100644 --- a/corim-cli/Cargo.toml +++ b/corim-cli/Cargo.toml @@ -30,7 +30,7 @@ base64 = "0.22" # `--diagnose` labels vendor-defined mval keys by their spec names # out of the box, and `generate` resolves their mval aliases. Use # `--no-default-features` to disable. -default = ["intel", "azure", "psa"] +default = ["intel", "azure", "psa", "cca"] # Register the Intel CoRIM profile (draft-cds-rats-intel-corim-profile) # in the diagnose registry so `--diagnose` labels Intel tee.* mval keys # by their spec names instead of "extension key ". @@ -41,3 +41,6 @@ azure = ["corim/profile-azure"] # Register the Arm PSA profile so `generate` resolves its `psa-cert-num` # mval alias and `--diagnose` labels it. psa = ["corim/profile-psa"] +# Register the Arm CCA endorsements profile so CCA platform/realm +# measurement keys are recognised as part of the CCA profile. +cca = ["corim/profile-cca"] diff --git a/corim-cli/src/generate.rs b/corim-cli/src/generate.rs index 33f9371..5731abc 100644 --- a/corim-cli/src/generate.rs +++ b/corim-cli/src/generate.rs @@ -413,5 +413,9 @@ fn build_registry() -> ProfileRegistry { registry.register(Box::new(corim::profile::azure::AzureProfile::new())); #[cfg(feature = "psa")] registry.register(Box::new(corim::profile::psa::PsaProfile::new())); + #[cfg(feature = "cca")] + registry.register(Box::new(corim::profile::cca::CcaPlatformProfile::new())); + #[cfg(feature = "cca")] + registry.register(Box::new(corim::profile::cca::CcaRealmProfile::new())); registry } diff --git a/corim-cli/src/main.rs b/corim-cli/src/main.rs index e44460d..1e13ad1 100644 --- a/corim-cli/src/main.rs +++ b/corim-cli/src/main.rs @@ -27,6 +27,10 @@ fn build_registry() -> corim::profile::ProfileRegistry { registry.register(Box::new(corim::profile::azure::AzureProfile::new())); #[cfg(feature = "psa")] registry.register(Box::new(corim::profile::psa::PsaProfile::new())); + #[cfg(feature = "cca")] + registry.register(Box::new(corim::profile::cca::CcaPlatformProfile::new())); + #[cfg(feature = "cca")] + registry.register(Box::new(corim::profile::cca::CcaRealmProfile::new())); registry } diff --git a/corim/Cargo.toml b/corim/Cargo.toml index 5663ef0..315d3d7 100644 --- a/corim/Cargo.toml +++ b/corim/Cargo.toml @@ -37,6 +37,10 @@ profile-azure = [] # First-party Arm PSA profile: the `psa-cert-num` (key 100) # measurement-values-map extension from draft-ietf-rats-corim-11. profile-psa = [] +# First-party Arm CCA endorsements profile. +# Implements the draft-ydb-rats-cca-endorsements-04 profile identifiers and +# the CCA platform/realm measurement-key naming conventions. +profile-cca = [] [dependencies] corim-macros.workspace = true diff --git a/corim/src/profile.rs b/corim/src/profile.rs index b15de21..8d5f8e3 100644 --- a/corim/src/profile.rs +++ b/corim/src/profile.rs @@ -30,6 +30,7 @@ //! | `profile-intel` | [`intel`](crate::profile::intel) | `draft-cds-rats-intel-corim-profile-03` | //! | `profile-azure` | `azure` (feature-gated) | Azure `tcbstatus` example extension | //! | `profile-psa` | `psa` (feature-gated) | Arm PSA `psa-cert-num` (draft-corim-11) | +//! | `profile-cca` | `cca` (feature-gated) | Arm CCA endorsements (draft-ydb-rats-cca-endorsements-04) | //! //! Third-party profiles are first-class — the [`Profile`](crate::profile::Profile) trait is //! public and stable, and out-of-tree crates may publish their own @@ -185,6 +186,18 @@ pub mod azure; #[cfg_attr(docsrs, doc(cfg(feature = "profile-psa")))] pub mod psa; +/// Minimal Arm CCA endorsements profile support for +/// `draft-ydb-rats-cca-endorsements-04`. +/// +/// The draft primarily introduces CCA Platform / Realm profile URIs and +/// the characteristic `mkey` names used for measurements. The core crate +/// already knows how to compare the underlying `digests` / `raw-value` +/// fields, so this module focuses on profile identification and +/// enforcement that the `mkey` names belong to the CCA profile. +#[cfg(feature = "profile-cca")] +#[cfg_attr(docsrs, doc(cfg(feature = "profile-cca")))] +pub mod cca; + // --------------------------------------------------------------------------- // MatchContext // --------------------------------------------------------------------------- diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs new file mode 100644 index 0000000..5996a96 --- /dev/null +++ b/corim/src/profile/cca/mod.rs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Minimal Arm CCA endorsements profile support for +//! `draft-ydb-rats-cca-endorsements-04`. +//! +//! The draft defines two specific CoRIM profile URIs: +//! +//! - `tag:arm.com,2025:endorsements/cca_platform#1.0.0` +//! - `tag:arm.com,2025:endorsements/cca_realm#1.0.0` +//! +//! and a set of characteristic `measurement-map` names: +//! +//! - Platform: `cca.software-component`, `cca.platform-config`, +//! `cca.rotpk.CM..`, `cca.rotpk.DM..`, +//! `cca.platform-manufacturing-config` +//! - Realm: `cca.rim`, `cca.rem0`..`cca.rem3`, `cca.rpv` +//! +//! The core crate already knows how to compare the underlying CBOR value +//! shape (`digests`, `raw-value`, `cryptokeys`, etc.) for these maps, so this +//! minimal profile support focuses on: +//! +//! - identifying the CCA profile URI, +//! - validating the CCA-specific `mkey` names, +//! - providing diagnosis labels for those names, +//! - allowing JSON alias round-tripping for the human-facing names. + +use crate::cbor::value::Value; +use crate::nostd_prelude::*; +use crate::profile::{MatchContext, Profile}; +use crate::types::common::MeasuredElement; +use crate::types::corim::ProfileChoice; +use crate::types::measurement::MeasurementMap; + +/// Profile URI for CCA Platform endorsements. +pub const CCA_PLATFORM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_platform#1.0.0"; +/// Profile URI for CCA Realm endorsements. +pub const CCA_REALM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_realm#1.0.0"; + +/// Recognize a CCA Platform measurement key, per draft-ydb-rats-cca-endorsements-04. +pub fn is_cca_platform_mkey(name: &str) -> bool { + match name { + "cca.software-component" | "cca.platform-config" | "cca.platform-manufacturing-config" => true, + _ => { + let Some(rest) = name.strip_prefix("cca.rotpk.") else { + return false; + }; + let mut parts = rest.split('.'); + let family = parts.next(); + let idx = parts.next(); + let slot = parts.next(); + if parts.next().is_some() { + return false; + } + matches!(family, Some("CM") | Some("DM")) + && idx + .and_then(|s| s.parse::().ok()) + .is_some_and(|n| n <= 7) + && slot + .and_then(|s| s.parse::().ok()) + .is_some_and(|n| n <= 5) + } + } +} + +/// Recognize a CCA Realm measurement key. +pub fn is_cca_realm_mkey(name: &str) -> bool { + match name { + "cca.rim" | "cca.rpv" => true, + _ => { + let Some(rest) = name.strip_prefix("cca.rem") else { + return false; + }; + rest.parse::().is_ok_and(|n| n <= 3) + } + } +} + +fn mkey_name(mkey: &Option) -> Option { + match mkey { + Some(MeasuredElement::Text(s)) => Some(s.clone()), + _ => None, + } +} + +fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { + let Some(mkey) = mkey_name(&m.mkey) else { + return false; + }; + + match mkey.as_str() { + "cca.software-component" => { + m.mval.digests.as_ref().is_some_and(|d| !d.is_empty()) + && m.mval.cryptokeys.as_ref().is_some_and(|keys| { + !keys.is_empty() + && keys.iter().all(|k| match k { + crate::types::common::CryptoKey::Bytes(b) => matches!(b.len(), 32 | 48 | 64), + _ => false, + }) + }) + } + "cca.platform-config" | "cca.platform-manufacturing-config" => m.mval.raw_value.is_some(), + _ if is_cca_platform_mkey(&mkey) => { + m.mval.cryptokeys.as_ref().is_some_and(|keys| { + !keys.is_empty() + && keys.len() == 1 + && keys.iter().all(|k| match k { + crate::types::common::CryptoKey::Bytes(b) => matches!(b.len(), 32 | 48 | 64), + _ => false, + }) + }) + } + _ => false, + } +} + +fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { + let Some(mkey) = mkey_name(&m.mkey) else { + return false; + }; + + match mkey.as_str() { + "cca.rim" | "cca.rem0" | "cca.rem1" | "cca.rem2" | "cca.rem3" => { + m.mval.digests.as_ref().is_some_and(|d| !d.is_empty()) + } + "cca.rpv" => m.mval.raw_value.is_some(), + _ => false, + } +} + +#[derive(Debug)] +pub struct CcaPlatformProfile { + id: ProfileChoice, +} + +impl CcaPlatformProfile { + pub fn new() -> Self { + Self { + id: ProfileChoice::Uri(CCA_PLATFORM_PROFILE_URI.into()), + } + } +} + +impl Default for CcaPlatformProfile { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +pub struct CcaRealmProfile { + id: ProfileChoice, +} + +impl CcaRealmProfile { + pub fn new() -> Self { + Self { + id: ProfileChoice::Uri(CCA_REALM_PROFILE_URI.into()), + } + } +} + +impl Default for CcaRealmProfile { + fn default() -> Self { + Self::new() + } +} + +impl Profile for CcaPlatformProfile { + fn identifier(&self) -> &ProfileChoice { + &self.id + } + + fn match_measurement( + &self, + reference: &MeasurementMap, + evidence: &MeasurementMap, + _ctx: &MatchContext, + ) -> Option { + let ref_mkey = mkey_name(&reference.mkey)?; + let ev_mkey = mkey_name(&evidence.mkey)?; + + if ref_mkey != ev_mkey { + return Some(false); + } + if !is_cca_platform_mkey(&ref_mkey) { + return None; + } + if !is_valid_cca_platform_measurement(reference) + || !is_valid_cca_platform_measurement(evidence) + { + return Some(false); + } + + Some(crate::validate::core_fields_match(reference, evidence)) + } + + fn diagnose_mval_entry(&self, _key: i64, value: &Value) -> Option { + match value { + Value::Text(s) if is_cca_platform_mkey(s) => Some(format!("{} = {}", s, s)), + _ => None, + } + } +} + +impl Profile for CcaRealmProfile { + fn identifier(&self) -> &ProfileChoice { + &self.id + } + + fn match_measurement( + &self, + reference: &MeasurementMap, + evidence: &MeasurementMap, + _ctx: &MatchContext, + ) -> Option { + let ref_mkey = mkey_name(&reference.mkey)?; + let ev_mkey = mkey_name(&evidence.mkey)?; + + if ref_mkey != ev_mkey { + return Some(false); + } + if !is_cca_realm_mkey(&ref_mkey) { + return None; + } + if !is_valid_cca_realm_measurement(reference) + || !is_valid_cca_realm_measurement(evidence) + { + return Some(false); + } + + Some(crate::validate::core_fields_match(reference, evidence)) + } + + fn diagnose_mval_entry(&self, _key: i64, value: &Value) -> Option { + match value { + Value::Text(s) if is_cca_realm_mkey(s) => Some(format!("{} = {}", s, s)), + _ => None, + } + } +} diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs new file mode 100644 index 0000000..24ca75b --- /dev/null +++ b/corim/tests/profile_cca_tests.rs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![cfg(feature = "profile-cca")] + +use corim::cbor::value::Value; +use corim::profile::cca::{ + is_cca_platform_mkey, is_cca_realm_mkey, CcaPlatformProfile, CcaRealmProfile, + CCA_PLATFORM_PROFILE_URI, CCA_REALM_PROFILE_URI, +}; +use corim::profile::{MatchContext, Profile}; +use corim::types::common::{CryptoKey, MeasuredElement}; +use corim::types::corim::ProfileChoice; +use corim::types::measurement::{Digest, MeasurementMap, MeasurementValuesMap, RawValueChoice}; + +fn measurement_with_mkey(mkey: &str, digest_val: &[u8]) -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text(mkey.into())), + mval: MeasurementValuesMap { + digests: Some(vec![Digest::new(7, digest_val.to_vec())]), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + +fn software_component_measurement(name: &str, digest_val: &[u8], signer: &[u8]) -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text(name.into())), + mval: MeasurementValuesMap { + digests: Some(vec![Digest::new(7, digest_val.to_vec())]), + cryptokeys: Some(vec![CryptoKey::Bytes(signer.to_vec())]), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + +fn rotpk_measurement(mkey: &str, key_bytes: &[u8]) -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text(mkey.into())), + mval: MeasurementValuesMap { + cryptokeys: Some(vec![CryptoKey::Bytes(key_bytes.to_vec())]), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + +fn raw_value_measurement(mkey: &str, value: &[u8]) -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text(mkey.into())), + mval: MeasurementValuesMap { + raw_value: Some(RawValueChoice::Bytes(value.to_vec())), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + +#[test] +fn platform_profile_uses_cca_platform_uri() { + let profile = CcaPlatformProfile::new(); + assert_eq!( + profile.identifier(), + &ProfileChoice::Uri(CCA_PLATFORM_PROFILE_URI.into()) + ); +} + +#[test] +fn realm_profile_uses_cca_realm_uri() { + let profile = CcaRealmProfile::new(); + assert_eq!( + profile.identifier(), + &ProfileChoice::Uri(CCA_REALM_PROFILE_URI.into()) + ); +} + +#[test] +fn recognized_platform_mkeys_include_software_component_and_config() { + assert!(is_cca_platform_mkey("cca.software-component")); + assert!(is_cca_platform_mkey("cca.platform-config")); + assert!(is_cca_platform_mkey("cca.rotpk.CM.2.3")); + assert!(is_cca_platform_mkey("cca.rotpk.DM.7.5")); + assert!(is_cca_platform_mkey("cca.platform-manufacturing-config")); + assert!(!is_cca_platform_mkey("cca.unknown")); + assert!(!is_cca_platform_mkey("cca.rotpk.CM.8.0")); + assert!(!is_cca_platform_mkey("cca.rotpk.CM.2.6")); +} + +#[test] +fn recognized_realm_mkeys_include_rim_rem_and_rpv() { + assert!(is_cca_realm_mkey("cca.rim")); + assert!(is_cca_realm_mkey("cca.rem0")); + assert!(is_cca_realm_mkey("cca.rem3")); + assert!(is_cca_realm_mkey("cca.rpv")); + assert!(!is_cca_realm_mkey("cca.rem10")); +} + +#[test] +fn platform_match_accepts_same_cca_mkey_and_same_core_values() { + let profile = CcaPlatformProfile::new(); + let reference = software_component_measurement( + "cca.software-component", + &[0x11, 0x22, 0x33], + &[0xAA; 32], + ); + let evidence = software_component_measurement( + "cca.software-component", + &[0x11, 0x22, 0x33], + &[0xAA; 32], + ); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(true) + ); +} + +#[test] +fn platform_match_rejects_mkey_mismatch() { + let profile = CcaPlatformProfile::new(); + let reference = measurement_with_mkey("cca.software-component", &[0x11, 0x22, 0x33]); + let evidence = measurement_with_mkey("cca.platform-config", &[0x11, 0x22, 0x33]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn platform_match_rejects_invalid_cca_structures() { + let profile = CcaPlatformProfile::new(); + let reference = measurement_with_mkey("cca.software-component", &[0x11, 0x22, 0x33]); + let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn realm_match_rejects_raw_value_violation() { + let profile = CcaRealmProfile::new(); + let reference = raw_value_measurement("cca.rpv", b"abc"); + let evidence = measurement_with_mkey("cca.rpv", b"def"); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn diagnosis_uses_cca_names_for_known_platform_mkeys() { + let profile = CcaPlatformProfile::new(); + let key = Value::Text("cca.software-component".into()); + assert_eq!( + profile.diagnose_mval_entry(0, &key), + Some("cca.software-component = cca.software-component".into()) + ); + assert_eq!(profile.diagnose_mval_entry(99, &Value::Integer(42)), None); +} From b7a14ceb6327a3ffeae83a28783efbcda9bf5a06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:06:04 +0000 Subject: [PATCH 02/18] style: format CCA profile code Co-authored-by: a-trikalinou <139903738+a-trikalinou@users.noreply.github.com> --- corim/src/profile/cca/mod.rs | 30 +++++++++++++++--------------- corim/tests/profile_cca_tests.rs | 14 ++++---------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 5996a96..e713446 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -40,7 +40,9 @@ pub const CCA_REALM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_realm /// Recognize a CCA Platform measurement key, per draft-ydb-rats-cca-endorsements-04. pub fn is_cca_platform_mkey(name: &str) -> bool { match name { - "cca.software-component" | "cca.platform-config" | "cca.platform-manufacturing-config" => true, + "cca.software-component" | "cca.platform-config" | "cca.platform-manufacturing-config" => { + true + } _ => { let Some(rest) = name.strip_prefix("cca.rotpk.") else { return false; @@ -94,22 +96,22 @@ fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { && m.mval.cryptokeys.as_ref().is_some_and(|keys| { !keys.is_empty() && keys.iter().all(|k| match k { - crate::types::common::CryptoKey::Bytes(b) => matches!(b.len(), 32 | 48 | 64), + crate::types::common::CryptoKey::Bytes(b) => { + matches!(b.len(), 32 | 48 | 64) + } _ => false, }) }) } "cca.platform-config" | "cca.platform-manufacturing-config" => m.mval.raw_value.is_some(), - _ if is_cca_platform_mkey(&mkey) => { - m.mval.cryptokeys.as_ref().is_some_and(|keys| { - !keys.is_empty() - && keys.len() == 1 - && keys.iter().all(|k| match k { - crate::types::common::CryptoKey::Bytes(b) => matches!(b.len(), 32 | 48 | 64), - _ => false, - }) - }) - } + _ if is_cca_platform_mkey(&mkey) => m.mval.cryptokeys.as_ref().is_some_and(|keys| { + !keys.is_empty() + && keys.len() == 1 + && keys.iter().all(|k| match k { + crate::types::common::CryptoKey::Bytes(b) => matches!(b.len(), 32 | 48 | 64), + _ => false, + }) + }), _ => false, } } @@ -223,9 +225,7 @@ impl Profile for CcaRealmProfile { if !is_cca_realm_mkey(&ref_mkey) { return None; } - if !is_valid_cca_realm_measurement(reference) - || !is_valid_cca_realm_measurement(evidence) - { + if !is_valid_cca_realm_measurement(reference) || !is_valid_cca_realm_measurement(evidence) { return Some(false); } diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index 24ca75b..7f5d39e 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -100,16 +100,10 @@ fn recognized_realm_mkeys_include_rim_rem_and_rpv() { #[test] fn platform_match_accepts_same_cca_mkey_and_same_core_values() { let profile = CcaPlatformProfile::new(); - let reference = software_component_measurement( - "cca.software-component", - &[0x11, 0x22, 0x33], - &[0xAA; 32], - ); - let evidence = software_component_measurement( - "cca.software-component", - &[0x11, 0x22, 0x33], - &[0xAA; 32], - ); + let reference = + software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + let evidence = + software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), From 5abeb717e03df6cdeef134a10fdf7f4018d8ae93 Mon Sep 17 00:00:00 2001 From: a-trikalinou <139903738+a-trikalinou@users.noreply.github.com> Date: Wed, 16 Sep 2026 08:36:07 -0700 Subject: [PATCH 03/18] Update comments in mod.rs for clarity Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- corim/src/profile/cca/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index e713446..eaf32d6 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -23,7 +23,7 @@ //! - identifying the CCA profile URI, //! - validating the CCA-specific `mkey` names, //! - providing diagnosis labels for those names, -//! - allowing JSON alias round-tripping for the human-facing names. +//! - enforcing the CCA-specific measurement-map shapes. use crate::cbor::value::Value; use crate::nostd_prelude::*; From 3cfb808d22c663b9514dfb02cf048044114d4fde Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 19:34:05 +0000 Subject: [PATCH 04/18] Fix CCA profile validation edge cases --- corim/src/profile/cca/mod.rs | 42 +++++++--------------- corim/tests/profile_cca_tests.rs | 61 ++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index eaf32d6..5d83ae9 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -25,7 +25,6 @@ //! - providing diagnosis labels for those names, //! - enforcing the CCA-specific measurement-map shapes. -use crate::cbor::value::Value; use crate::nostd_prelude::*; use crate::profile::{MatchContext, Profile}; use crate::types::common::MeasuredElement; @@ -55,12 +54,8 @@ pub fn is_cca_platform_mkey(name: &str) -> bool { return false; } matches!(family, Some("CM") | Some("DM")) - && idx - .and_then(|s| s.parse::().ok()) - .is_some_and(|n| n <= 7) - && slot - .and_then(|s| s.parse::().ok()) - .is_some_and(|n| n <= 5) + && matches!(idx, Some("0" | "1" | "2" | "3" | "4" | "5" | "6" | "7")) + && matches!(slot, Some("0" | "1" | "2" | "3" | "4" | "5")) } } } @@ -68,13 +63,8 @@ pub fn is_cca_platform_mkey(name: &str) -> bool { /// Recognize a CCA Realm measurement key. pub fn is_cca_realm_mkey(name: &str) -> bool { match name { - "cca.rim" | "cca.rpv" => true, - _ => { - let Some(rest) = name.strip_prefix("cca.rem") else { - return false; - }; - rest.parse::().is_ok_and(|n| n <= 3) - } + "cca.rim" | "cca.rem0" | "cca.rem1" | "cca.rem2" | "cca.rem3" | "cca.rpv" => true, + _ => false, } } @@ -86,6 +76,10 @@ fn mkey_name(mkey: &Option) -> Option { } fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { + if m.authorized_by.is_some() { + return false; + } + let Some(mkey) = mkey_name(&m.mkey) else { return false; }; @@ -94,7 +88,7 @@ fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { "cca.software-component" => { m.mval.digests.as_ref().is_some_and(|d| !d.is_empty()) && m.mval.cryptokeys.as_ref().is_some_and(|keys| { - !keys.is_empty() + keys.len() == 1 && keys.iter().all(|k| match k { crate::types::common::CryptoKey::Bytes(b) => { matches!(b.len(), 32 | 48 | 64) @@ -117,6 +111,10 @@ fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { } fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { + if m.authorized_by.is_some() { + return false; + } + let Some(mkey) = mkey_name(&m.mkey) else { return false; }; @@ -196,13 +194,6 @@ impl Profile for CcaPlatformProfile { Some(crate::validate::core_fields_match(reference, evidence)) } - - fn diagnose_mval_entry(&self, _key: i64, value: &Value) -> Option { - match value { - Value::Text(s) if is_cca_platform_mkey(s) => Some(format!("{} = {}", s, s)), - _ => None, - } - } } impl Profile for CcaRealmProfile { @@ -231,11 +222,4 @@ impl Profile for CcaRealmProfile { Some(crate::validate::core_fields_match(reference, evidence)) } - - fn diagnose_mval_entry(&self, _key: i64, value: &Value) -> Option { - match value { - Value::Text(s) if is_cca_realm_mkey(s) => Some(format!("{} = {}", s, s)), - _ => None, - } - } } diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index 7f5d39e..ef538a8 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -3,7 +3,6 @@ #![cfg(feature = "profile-cca")] -use corim::cbor::value::Value; use corim::profile::cca::{ is_cca_platform_mkey, is_cca_realm_mkey, CcaPlatformProfile, CcaRealmProfile, CCA_PLATFORM_PROFILE_URI, CCA_REALM_PROFILE_URI, @@ -86,6 +85,8 @@ fn recognized_platform_mkeys_include_software_component_and_config() { assert!(!is_cca_platform_mkey("cca.unknown")); assert!(!is_cca_platform_mkey("cca.rotpk.CM.8.0")); assert!(!is_cca_platform_mkey("cca.rotpk.CM.2.6")); + assert!(!is_cca_platform_mkey("cca.rotpk.CM.02.3")); + assert!(!is_cca_platform_mkey("cca.rotpk.CM.2.03")); } #[test] @@ -95,6 +96,7 @@ fn recognized_realm_mkeys_include_rim_rem_and_rpv() { assert!(is_cca_realm_mkey("cca.rem3")); assert!(is_cca_realm_mkey("cca.rpv")); assert!(!is_cca_realm_mkey("cca.rem10")); + assert!(!is_cca_realm_mkey("cca.rem00")); } #[test] @@ -135,6 +137,39 @@ fn platform_match_rejects_invalid_cca_structures() { ); } +#[test] +fn platform_match_rejects_multiple_software_component_signer_ids() { + let profile = CcaPlatformProfile::new(); + let mut reference = + software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + reference + .mval + .cryptokeys + .as_mut() + .unwrap() + .push(CryptoKey::Bytes(vec![0xBB; 32])); + let evidence = + software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn platform_match_rejects_authorized_by() { + let profile = CcaPlatformProfile::new(); + let mut reference = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + reference.authorized_by = Some(vec![CryptoKey::Bytes(vec![0xCC; 32])]); + let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + #[test] fn realm_match_rejects_raw_value_violation() { let profile = CcaRealmProfile::new(); @@ -148,12 +183,26 @@ fn realm_match_rejects_raw_value_violation() { } #[test] -fn diagnosis_uses_cca_names_for_known_platform_mkeys() { +fn realm_match_rejects_authorized_by() { + let profile = CcaRealmProfile::new(); + let reference = measurement_with_mkey("cca.rim", &[0x11, 0x22, 0x33]); + let mut evidence = measurement_with_mkey("cca.rim", &[0x11, 0x22, 0x33]); + evidence.authorized_by = Some(vec![CryptoKey::Bytes(vec![0xCC; 32])]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn diagnosis_does_not_treat_mkeys_as_mval_extensions() { let profile = CcaPlatformProfile::new(); - let key = Value::Text("cca.software-component".into()); assert_eq!( - profile.diagnose_mval_entry(0, &key), - Some("cca.software-component = cca.software-component".into()) + profile.diagnose_mval_entry( + -999, + &corim::cbor::value::Value::Text("cca.software-component".into()) + ), + None ); - assert_eq!(profile.diagnose_mval_entry(99, &Value::Integer(42)), None); } From 4349c50647ff64780e2a1fb7874b18f2bf0ec950 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:49:14 +0000 Subject: [PATCH 05/18] fix: satisfy clippy matches lint Co-authored-by: a-trikalinou <139903738+a-trikalinou@users.noreply.github.com> --- corim/src/profile/cca/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 5d83ae9..2dac965 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -62,10 +62,10 @@ pub fn is_cca_platform_mkey(name: &str) -> bool { /// Recognize a CCA Realm measurement key. pub fn is_cca_realm_mkey(name: &str) -> bool { - match name { - "cca.rim" | "cca.rem0" | "cca.rem1" | "cca.rem2" | "cca.rem3" | "cca.rpv" => true, - _ => false, - } + matches!( + name, + "cca.rim" | "cca.rem0" | "cca.rem1" | "cca.rem2" | "cca.rem3" | "cca.rpv" + ) } fn mkey_name(mkey: &Option) -> Option { From f67b40e95a8517acc036cd2f66ac8920d8690574 Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 20:12:08 +0000 Subject: [PATCH 06/18] Refine CCA profile validation --- corim/src/profile.rs | 13 +++++ corim/src/profile/cca/mod.rs | 89 +++++++++++++++++++++++--------- corim/src/validate.rs | 4 ++ corim/tests/profile_cca_tests.rs | 75 +++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 24 deletions(-) diff --git a/corim/src/profile.rs b/corim/src/profile.rs index 8d5f8e3..765741c 100644 --- a/corim/src/profile.rs +++ b/corim/src/profile.rs @@ -301,6 +301,19 @@ pub trait Profile { None } + /// Validate profile-specific constraints across the measurements in a + /// single reference triple before per-measurement appraisal begins. + /// + /// Use this when the profile has requirements that cannot be checked from + /// one `(reference, evidence)` measurement pair alone, such as a mandatory + /// measurement that must appear somewhere in the reference triple. Return + /// `false` to make the whole reference triple ineligible for profile-aware + /// matching. Profiles without cross-measurement requirements can use the + /// default implementation. + fn reference_measurements_valid(&self, _measurements: &[MeasurementMap]) -> bool { + true + } + /// Render an `extra_entries` key/value pair for `--diagnose` output. /// /// Called by the diagnose walker when it encounters a profile-defined diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 2dac965..52b469e 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -27,15 +27,26 @@ use crate::nostd_prelude::*; use crate::profile::{MatchContext, Profile}; -use crate::types::common::MeasuredElement; +use crate::types::common::{CryptoKey, MeasuredElement}; use crate::types::corim::ProfileChoice; -use crate::types::measurement::MeasurementMap; +use crate::types::measurement::{MeasurementMap, MeasurementValuesMap}; /// Profile URI for CCA Platform endorsements. pub const CCA_PLATFORM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_platform#1.0.0"; /// Profile URI for CCA Realm endorsements. pub const CCA_REALM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_realm#1.0.0"; +/// Maximum ROTPK array index from draft-ydb-rats-cca-endorsements-04 §3.1.3.3. +const CCA_ROTPK_MAX_INDEX: u8 = 7; +/// Maximum ROTPK slot index from draft-ydb-rats-cca-endorsements-04 §3.1.3.3. +const CCA_ROTPK_MAX_SLOT: u8 = 5; +/// CCA hash size in bytes from draft-ydb-rats-cca-endorsements-04 §3.1.3.1 and §3.1.3.3. +const CCA_HASH_SIZE_256: usize = 32; +/// CCA hash size in bytes from draft-ydb-rats-cca-endorsements-04 §3.1.3.1 and §3.1.3.3. +const CCA_HASH_SIZE_384: usize = 48; +/// CCA hash size in bytes from draft-ydb-rats-cca-endorsements-04 §3.1.3.1 and §3.1.3.3. +const CCA_HASH_SIZE_512: usize = 64; + /// Recognize a CCA Platform measurement key, per draft-ydb-rats-cca-endorsements-04. pub fn is_cca_platform_mkey(name: &str) -> bool { match name { @@ -54,12 +65,23 @@ pub fn is_cca_platform_mkey(name: &str) -> bool { return false; } matches!(family, Some("CM") | Some("DM")) - && matches!(idx, Some("0" | "1" | "2" | "3" | "4" | "5" | "6" | "7")) - && matches!(slot, Some("0" | "1" | "2" | "3" | "4" | "5")) + && one_digit_at_most(idx, CCA_ROTPK_MAX_INDEX) + && one_digit_at_most(slot, CCA_ROTPK_MAX_SLOT) } } } +fn one_digit_at_most(value: Option<&str>, max: u8) -> bool { + let Some(value) = value else { + return false; + }; + + let [digit] = value.as_bytes() else { + return false; + }; + digit.is_ascii_digit() && digit - b'0' <= max +} + /// Recognize a CCA Realm measurement key. pub fn is_cca_realm_mkey(name: &str) -> bool { matches!( @@ -75,6 +97,19 @@ fn mkey_name(mkey: &Option) -> Option { } } +fn has_single_signer_key(mval: &MeasurementValuesMap) -> bool { + mval.cryptokeys.as_ref().is_some_and(|keys| { + keys.len() == 1 + && keys.iter().all(|k| match k { + CryptoKey::Bytes(b) => matches!( + b.len(), + CCA_HASH_SIZE_256 | CCA_HASH_SIZE_384 | CCA_HASH_SIZE_512 + ), + _ => false, + }) + }) +} + fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { if m.authorized_by.is_some() { return false; @@ -86,26 +121,10 @@ fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { match mkey.as_str() { "cca.software-component" => { - m.mval.digests.as_ref().is_some_and(|d| !d.is_empty()) - && m.mval.cryptokeys.as_ref().is_some_and(|keys| { - keys.len() == 1 - && keys.iter().all(|k| match k { - crate::types::common::CryptoKey::Bytes(b) => { - matches!(b.len(), 32 | 48 | 64) - } - _ => false, - }) - }) + m.mval.digests.as_ref().is_some_and(|d| !d.is_empty()) && has_single_signer_key(&m.mval) } "cca.platform-config" | "cca.platform-manufacturing-config" => m.mval.raw_value.is_some(), - _ if is_cca_platform_mkey(&mkey) => m.mval.cryptokeys.as_ref().is_some_and(|keys| { - !keys.is_empty() - && keys.len() == 1 - && keys.iter().all(|k| match k { - crate::types::common::CryptoKey::Bytes(b) => matches!(b.len(), 32 | 48 | 64), - _ => false, - }) - }), + _ if is_cca_platform_mkey(&mkey) => has_single_signer_key(&m.mval), _ => false, } } @@ -128,12 +147,14 @@ fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { } } -#[derive(Debug)] +/// Profile implementation for Arm CCA Platform endorsements. +#[derive(Clone, Debug, PartialEq)] pub struct CcaPlatformProfile { id: ProfileChoice, } impl CcaPlatformProfile { + /// Construct a new CCA Platform profile instance. pub fn new() -> Self { Self { id: ProfileChoice::Uri(CCA_PLATFORM_PROFILE_URI.into()), @@ -147,12 +168,14 @@ impl Default for CcaPlatformProfile { } } -#[derive(Debug)] +/// Profile implementation for Arm CCA Realm endorsements. +#[derive(Clone, Debug, PartialEq)] pub struct CcaRealmProfile { id: ProfileChoice, } impl CcaRealmProfile { + /// Construct a new CCA Realm profile instance. pub fn new() -> Self { Self { id: ProfileChoice::Uri(CCA_REALM_PROFILE_URI.into()), @@ -201,6 +224,24 @@ impl Profile for CcaRealmProfile { &self.id } + fn reference_measurements_valid(&self, measurements: &[MeasurementMap]) -> bool { + let mut has_realm_measurement = false; + let mut has_rim = false; + + for measurement in measurements { + let Some(mkey) = mkey_name(&measurement.mkey) else { + continue; + }; + + if is_cca_realm_mkey(&mkey) { + has_realm_measurement = true; + has_rim |= mkey == "cca.rim"; + } + } + + !has_realm_measurement || has_rim + } + fn match_measurement( &self, reference: &MeasurementMap, diff --git a/corim/src/validate.rs b/corim/src/validate.rs index a01685c..d8553d1 100644 --- a/corim/src/validate.rs +++ b/corim/src/validate.rs @@ -341,6 +341,10 @@ pub fn match_reference_values_with_profile( let mut corroborated = Vec::new(); for triple in ref_triples { + if profile.is_some_and(|p| !p.reference_measurements_valid(triple.measurements())) { + continue; + } + for ev in evidence { if !environment_matches(triple.environment(), &ev.environment) { continue; diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index ef538a8..60c597f 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -10,7 +10,10 @@ use corim::profile::cca::{ use corim::profile::{MatchContext, Profile}; use corim::types::common::{CryptoKey, MeasuredElement}; use corim::types::corim::ProfileChoice; +use corim::types::environment::EnvironmentMap; use corim::types::measurement::{Digest, MeasurementMap, MeasurementValuesMap, RawValueChoice}; +use corim::types::triples::ReferenceTriple; +use corim::validate::{match_reference_values_with_profile, EvidenceClaim}; fn measurement_with_mkey(mkey: &str, digest_val: &[u8]) -> MeasurementMap { MeasurementMap { @@ -125,6 +128,18 @@ fn platform_match_rejects_mkey_mismatch() { ); } +#[test] +fn platform_match_defers_for_non_cca_mkey() { + let profile = CcaPlatformProfile::new(); + let reference = measurement_with_mkey("tee.something", &[0x11, 0x22, 0x33]); + let evidence = measurement_with_mkey("tee.something", &[0x11, 0x22, 0x33]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + None + ); +} + #[test] fn platform_match_rejects_invalid_cca_structures() { let profile = CcaPlatformProfile::new(); @@ -182,6 +197,66 @@ fn realm_match_rejects_raw_value_violation() { ); } +#[test] +fn realm_profile_rejects_reference_triple_without_mandatory_rim() { + let profile = CcaRealmProfile::new(); + let rem = measurement_with_mkey("cca.rem0", &[0x11, 0x22, 0x33]); + let triples = vec![ReferenceTriple::new( + EnvironmentMap::for_class("ACME", "Realm"), + vec![rem.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: EnvironmentMap::for_class("ACME", "Realm"), + measurements: vec![rem], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + +#[test] +fn realm_profile_accepts_reference_triple_with_mandatory_rim() { + let profile = CcaRealmProfile::new(); + let rim = measurement_with_mkey("cca.rim", &[0xAA, 0xBB, 0xCC]); + let rem = measurement_with_mkey("cca.rem0", &[0x11, 0x22, 0x33]); + let triples = vec![ReferenceTriple::new( + EnvironmentMap::for_class("ACME", "Realm"), + vec![rim.clone(), rem.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: EnvironmentMap::for_class("ACME", "Realm"), + measurements: vec![rim, rem], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert_eq!(claims.len(), 1); + assert_eq!(claims[0].measurements.len(), 2); +} + +#[test] +fn realm_match_defers_for_non_cca_mkey() { + let profile = CcaRealmProfile::new(); + let reference = measurement_with_mkey("tee.something", &[0x11, 0x22, 0x33]); + let evidence = measurement_with_mkey("tee.something", &[0x11, 0x22, 0x33]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + None + ); +} + #[test] fn realm_match_rejects_authorized_by() { let profile = CcaRealmProfile::new(); From 7f121e359a384a2956deb35dbf0cc5b6d408ec8f Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 20:29:34 +0000 Subject: [PATCH 07/18] Address CCA profile review comments --- corim-cli/src/convert.rs | 4 ++++ corim/src/profile/cca/mod.rs | 8 ++++++-- corim/tests/profile_cca_tests.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/corim-cli/src/convert.rs b/corim-cli/src/convert.rs index 0c3d84e..53cd527 100644 --- a/corim-cli/src/convert.rs +++ b/corim-cli/src/convert.rs @@ -270,6 +270,10 @@ fn build_registry() -> ProfileRegistry { registry.register(Box::new(corim::profile::azure::AzureProfile::new())); #[cfg(feature = "psa")] registry.register(Box::new(corim::profile::psa::PsaProfile::new())); + #[cfg(feature = "cca")] + registry.register(Box::new(corim::profile::cca::CcaPlatformProfile::new())); + #[cfg(feature = "cca")] + registry.register(Box::new(corim::profile::cca::CcaRealmProfile::new())); registry } diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 52b469e..d4661fb 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -22,7 +22,6 @@ //! //! - identifying the CCA profile URI, //! - validating the CCA-specific `mkey` names, -//! - providing diagnosis labels for those names, //! - enforcing the CCA-specific measurement-map shapes. use crate::nostd_prelude::*; @@ -129,6 +128,11 @@ fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { } } +fn cca_platform_measurements_match(reference: &MeasurementMap, evidence: &MeasurementMap) -> bool { + crate::validate::core_fields_match(reference, evidence) + && reference.mval.cryptokeys == evidence.mval.cryptokeys +} + fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { if m.authorized_by.is_some() { return false; @@ -215,7 +219,7 @@ impl Profile for CcaPlatformProfile { return Some(false); } - Some(crate::validate::core_fields_match(reference, evidence)) + Some(cca_platform_measurements_match(reference, evidence)) } } diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index 60c597f..bdde04c 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -143,7 +143,7 @@ fn platform_match_defers_for_non_cca_mkey() { #[test] fn platform_match_rejects_invalid_cca_structures() { let profile = CcaPlatformProfile::new(); - let reference = measurement_with_mkey("cca.software-component", &[0x11, 0x22, 0x33]); + let reference = measurement_with_mkey("cca.rotpk.CM.2.3", &[0x11, 0x22, 0x33]); let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); assert_eq!( @@ -152,6 +152,32 @@ fn platform_match_rejects_invalid_cca_structures() { ); } +#[test] +fn platform_match_rejects_different_software_component_signer_id() { + let profile = CcaPlatformProfile::new(); + let reference = + software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + let evidence = + software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xBB; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn platform_match_rejects_different_rotpk_key() { + let profile = CcaPlatformProfile::new(); + let reference = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xBB; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + #[test] fn platform_match_rejects_multiple_software_component_signer_ids() { let profile = CcaPlatformProfile::new(); From 690950fb1c2a0db62a72ff5dcd21f4a6a4d7ec45 Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 20:48:50 +0000 Subject: [PATCH 08/18] Tighten CCA measurement validation --- corim/src/profile/cca/mod.rs | 186 ++++++++++++++++++++++++++++--- corim/tests/profile_cca_tests.rs | 158 ++++++++++++++++++++++++-- 2 files changed, 315 insertions(+), 29 deletions(-) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index d4661fb..21cd97a 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -16,9 +16,9 @@ //! `cca.platform-manufacturing-config` //! - Realm: `cca.rim`, `cca.rem0`..`cca.rem3`, `cca.rpv` //! -//! The core crate already knows how to compare the underlying CBOR value -//! shape (`digests`, `raw-value`, `cryptokeys`, etc.) for these maps, so this -//! minimal profile support focuses on: +//! The core crate already knows how to compare most underlying CBOR value +//! shapes (`digests`, `raw-value`, etc.) for these maps, so this minimal +//! profile support focuses on: //! //! - identifying the CCA profile URI, //! - validating the CCA-specific `mkey` names, @@ -28,7 +28,9 @@ use crate::nostd_prelude::*; use crate::profile::{MatchContext, Profile}; use crate::types::common::{CryptoKey, MeasuredElement}; use crate::types::corim::ProfileChoice; -use crate::types::measurement::{MeasurementMap, MeasurementValuesMap}; +use crate::types::measurement::{ + Digest, DigestAlg, MeasurementMap, MeasurementValuesMap, RawValueChoice, +}; /// Profile URI for CCA Platform endorsements. pub const CCA_PLATFORM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_platform#1.0.0"; @@ -109,7 +111,117 @@ fn has_single_signer_key(mval: &MeasurementValuesMap) -> bool { }) } -fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { +fn has_no_mval_fields_except( + mval: &MeasurementValuesMap, + allow_version: bool, + allow_digests: bool, + allow_raw_value: bool, + allow_name: bool, + allow_cryptokeys: bool, +) -> bool { + (allow_version || mval.version.is_none()) + && (allow_digests || mval.digests.is_none()) + && (allow_raw_value || mval.raw_value.is_none()) + && (allow_name || mval.name.is_none()) + && (allow_cryptokeys || mval.cryptokeys.is_none()) + && mval.svn.is_none() + && mval.flags.is_none() + && mval.mac_addr.is_none() + && mval.ip_addr.is_none() + && mval.serial_number.is_none() + && mval.ueid.is_none() + && mval.uuid.is_none() + && mval.integrity_registers.is_none() + && mval.int_range.is_none() + && mval.extra_entries.is_empty() +} + +fn has_cca_digests(mval: &MeasurementValuesMap) -> bool { + mval.digests.as_ref().is_some_and(|digests| { + !digests.is_empty() + && digests.iter().all(is_cca_digest) + && digests.iter().enumerate().all(|(i, digest)| { + digests + .iter() + .skip(i + 1) + .all(|other| digest.alg() != other.alg()) + }) + }) +} + +fn is_cca_digest(digest: &Digest) -> bool { + matches!(digest.alg(), DigestAlg::Text(_)) && is_cca_hash_size(digest.value().len()) +} + +fn is_cca_hash_size(len: usize) -> bool { + matches!( + len, + CCA_HASH_SIZE_256 | CCA_HASH_SIZE_384 | CCA_HASH_SIZE_512 + ) +} + +fn is_masked_raw_value(mval: &MeasurementValuesMap) -> bool { + matches!(mval.raw_value, Some(RawValueChoice::Masked { .. })) +} + +fn is_bytes_raw_value(mval: &MeasurementValuesMap) -> bool { + matches!(mval.raw_value, Some(RawValueChoice::Bytes(_))) +} + +fn has_raw_value(mval: &MeasurementValuesMap) -> bool { + mval.raw_value.is_some() +} + +fn is_cca_software_component_mval(mval: &MeasurementValuesMap) -> bool { + has_no_mval_fields_except(mval, true, true, false, true, true) + && mval + .version + .as_ref() + .is_none_or(|version| version.version_scheme.is_none()) + && has_cca_digests(mval) + && has_single_signer_key(mval) +} + +fn is_cca_rotpk_mval(mval: &MeasurementValuesMap) -> bool { + has_no_mval_fields_except(mval, false, false, false, false, true) && has_single_signer_key(mval) +} + +fn is_cca_masked_config_reference_mval(mval: &MeasurementValuesMap) -> bool { + has_no_mval_fields_except(mval, false, false, true, false, false) && is_masked_raw_value(mval) +} + +fn is_cca_raw_config_evidence_mval(mval: &MeasurementValuesMap) -> bool { + has_no_mval_fields_except(mval, false, false, true, false, false) && has_raw_value(mval) +} + +fn is_cca_realm_digest_mval(mval: &MeasurementValuesMap) -> bool { + has_no_mval_fields_except(mval, false, true, false, false, false) && has_cca_digests(mval) +} + +fn is_cca_rpv_mval(mval: &MeasurementValuesMap) -> bool { + has_no_mval_fields_except(mval, false, false, true, false, false) && is_bytes_raw_value(mval) +} + +fn is_valid_cca_platform_reference_measurement(m: &MeasurementMap) -> bool { + if m.authorized_by.is_some() { + return false; + } + + let Some(mkey) = mkey_name(&m.mkey) else { + return false; + }; + + match mkey.as_str() { + "cca.software-component" => is_cca_software_component_mval(&m.mval), + "cca.platform-config" | "cca.platform-manufacturing-config" => { + is_cca_masked_config_reference_mval(&m.mval) + } + _ if is_cca_platform_mkey(&mkey) => is_cca_rotpk_mval(&m.mval), + _ => false, + } +} + +fn is_valid_cca_platform_evidence_measurement(m: &MeasurementMap) -> bool { if m.authorized_by.is_some() { return false; } @@ -119,18 +231,58 @@ fn is_valid_cca_platform_measurement(m: &MeasurementMap) -> bool { }; match mkey.as_str() { - "cca.software-component" => { - m.mval.digests.as_ref().is_some_and(|d| !d.is_empty()) && has_single_signer_key(&m.mval) + "cca.software-component" => is_cca_software_component_mval(&m.mval), + "cca.platform-config" | "cca.platform-manufacturing-config" => { + is_cca_raw_config_evidence_mval(&m.mval) } - "cca.platform-config" | "cca.platform-manufacturing-config" => m.mval.raw_value.is_some(), - _ if is_cca_platform_mkey(&mkey) => has_single_signer_key(&m.mval), + _ if is_cca_platform_mkey(&mkey) => is_cca_rotpk_mval(&m.mval), _ => false, } } fn cca_platform_measurements_match(reference: &MeasurementMap, evidence: &MeasurementMap) -> bool { - crate::validate::core_fields_match(reference, evidence) - && reference.mval.cryptokeys == evidence.mval.cryptokeys + let Some(mkey) = mkey_name(&reference.mkey) else { + return false; + }; + + match mkey.as_str() { + "cca.platform-config" | "cca.platform-manufacturing-config" => { + raw_value_matches_with_reference_mask( + &reference.mval.raw_value, + &evidence.mval.raw_value, + ) + } + _ => { + crate::validate::core_fields_match(reference, evidence) + && reference.mval.cryptokeys == evidence.mval.cryptokeys + } + } +} + +fn raw_value_matches_with_reference_mask( + reference: &Option, + evidence: &Option, +) -> bool { + match (reference, evidence) { + (Some(RawValueChoice::Masked { value, mask }), Some(RawValueChoice::Bytes(evidence))) + | ( + Some(RawValueChoice::Masked { value, mask }), + Some(RawValueChoice::Masked { + value: evidence, .. + }), + ) => masked_bytes_match(value, evidence, mask), + _ => reference == evidence, + } +} + +fn masked_bytes_match(reference: &[u8], evidence: &[u8], mask: &[u8]) -> bool { + reference.len() == evidence.len() + && reference.len() == mask.len() + && reference + .iter() + .zip(evidence) + .zip(mask) + .all(|((r, e), m)| (r & m) == (e & m)) } fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { @@ -144,9 +296,9 @@ fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { match mkey.as_str() { "cca.rim" | "cca.rem0" | "cca.rem1" | "cca.rem2" | "cca.rem3" => { - m.mval.digests.as_ref().is_some_and(|d| !d.is_empty()) + is_cca_realm_digest_mval(&m.mval) } - "cca.rpv" => m.mval.raw_value.is_some(), + "cca.rpv" => is_cca_rpv_mval(&m.mval), _ => false, } } @@ -213,8 +365,8 @@ impl Profile for CcaPlatformProfile { if !is_cca_platform_mkey(&ref_mkey) { return None; } - if !is_valid_cca_platform_measurement(reference) - || !is_valid_cca_platform_measurement(evidence) + if !is_valid_cca_platform_reference_measurement(reference) + || !is_valid_cca_platform_evidence_measurement(evidence) { return Some(false); } @@ -229,7 +381,6 @@ impl Profile for CcaRealmProfile { } fn reference_measurements_valid(&self, measurements: &[MeasurementMap]) -> bool { - let mut has_realm_measurement = false; let mut has_rim = false; for measurement in measurements { @@ -238,12 +389,11 @@ impl Profile for CcaRealmProfile { }; if is_cca_realm_mkey(&mkey) { - has_realm_measurement = true; has_rim |= mkey == "cca.rim"; } } - !has_realm_measurement || has_rim + has_rim } fn match_measurement( diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index bdde04c..d743d63 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -19,7 +19,7 @@ fn measurement_with_mkey(mkey: &str, digest_val: &[u8]) -> MeasurementMap { MeasurementMap { mkey: Some(MeasuredElement::Text(mkey.into())), mval: MeasurementValuesMap { - digests: Some(vec![Digest::new(7, digest_val.to_vec())]), + digests: Some(vec![Digest::new_text("sha-256", digest_val.to_vec())]), ..MeasurementValuesMap::default() }, authorized_by: None, @@ -30,7 +30,7 @@ fn software_component_measurement(name: &str, digest_val: &[u8], signer: &[u8]) MeasurementMap { mkey: Some(MeasuredElement::Text(name.into())), mval: MeasurementValuesMap { - digests: Some(vec![Digest::new(7, digest_val.to_vec())]), + digests: Some(vec![Digest::new_text("sha-256", digest_val.to_vec())]), cryptokeys: Some(vec![CryptoKey::Bytes(signer.to_vec())]), ..MeasurementValuesMap::default() }, @@ -60,6 +60,20 @@ fn raw_value_measurement(mkey: &str, value: &[u8]) -> MeasurementMap { } } +fn masked_raw_value_measurement(mkey: &str, value: &[u8], mask: &[u8]) -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text(mkey.into())), + mval: MeasurementValuesMap { + raw_value: Some(RawValueChoice::Masked { + value: value.to_vec(), + mask: mask.to_vec(), + }), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + #[test] fn platform_profile_uses_cca_platform_uri() { let profile = CcaPlatformProfile::new(); @@ -106,9 +120,9 @@ fn recognized_realm_mkeys_include_rim_rem_and_rpv() { fn platform_match_accepts_same_cca_mkey_and_same_core_values() { let profile = CcaPlatformProfile::new(); let reference = - software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); let evidence = - software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), @@ -156,9 +170,9 @@ fn platform_match_rejects_invalid_cca_structures() { fn platform_match_rejects_different_software_component_signer_id() { let profile = CcaPlatformProfile::new(); let reference = - software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); let evidence = - software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xBB; 32]); + software_component_measurement("cca.software-component", &[0x11; 32], &[0xBB; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), @@ -178,11 +192,85 @@ fn platform_match_rejects_different_rotpk_key() { ); } +#[test] +fn platform_match_accepts_masked_config_reference_against_unmasked_evidence() { + let profile = CcaPlatformProfile::new(); + let reference = + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]); + let evidence = raw_value_measurement("cca.platform-config", &[0xAF, 0xFF]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(true) + ); +} + +#[test] +fn platform_match_rejects_unmasked_config_reference() { + let profile = CcaPlatformProfile::new(); + let reference = raw_value_measurement("cca.platform-config", &[0xAA, 0xBB]); + let evidence = raw_value_measurement("cca.platform-config", &[0xAA, 0xBB]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn platform_match_rejects_software_component_digest_with_integer_alg() { + let profile = CcaPlatformProfile::new(); + let reference = MeasurementMap { + mkey: Some(MeasuredElement::Text("cca.software-component".into())), + mval: MeasurementValuesMap { + digests: Some(vec![Digest::new(7, vec![0x11; 32])]), + cryptokeys: Some(vec![CryptoKey::Bytes(vec![0xAA; 32])]), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + }; + let evidence = + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn platform_match_rejects_software_component_extra_mval_field() { + let profile = CcaPlatformProfile::new(); + let mut reference = + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); + reference.mval.raw_value = Some(RawValueChoice::Bytes(vec![0xCC; 32])); + let evidence = + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn platform_match_rejects_rotpk_extra_mval_field() { + let profile = CcaPlatformProfile::new(); + let mut reference = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + reference.mval.raw_value = Some(RawValueChoice::Bytes(vec![0xCC; 32])); + let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + #[test] fn platform_match_rejects_multiple_software_component_signer_ids() { let profile = CcaPlatformProfile::new(); let mut reference = - software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); reference .mval .cryptokeys @@ -190,7 +278,7 @@ fn platform_match_rejects_multiple_software_component_signer_ids() { .unwrap() .push(CryptoKey::Bytes(vec![0xBB; 32])); let evidence = - software_component_measurement("cca.software-component", &[0x11, 0x22, 0x33], &[0xAA; 32]); + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), @@ -226,7 +314,7 @@ fn realm_match_rejects_raw_value_violation() { #[test] fn realm_profile_rejects_reference_triple_without_mandatory_rim() { let profile = CcaRealmProfile::new(); - let rem = measurement_with_mkey("cca.rem0", &[0x11, 0x22, 0x33]); + let rem = measurement_with_mkey("cca.rem0", &[0x11; 32]); let triples = vec![ReferenceTriple::new( EnvironmentMap::for_class("ACME", "Realm"), vec![rem.clone()], @@ -246,11 +334,34 @@ fn realm_profile_rejects_reference_triple_without_mandatory_rim() { assert!(claims.is_empty()); } +#[test] +fn realm_profile_rejects_reference_triple_without_cca_measurements() { + let profile = CcaRealmProfile::new(); + let measurement = measurement_with_mkey("tee.something", &[0x11; 32]); + let triples = vec![ReferenceTriple::new( + EnvironmentMap::for_class("ACME", "Realm"), + vec![measurement.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: EnvironmentMap::for_class("ACME", "Realm"), + measurements: vec![measurement], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + #[test] fn realm_profile_accepts_reference_triple_with_mandatory_rim() { let profile = CcaRealmProfile::new(); - let rim = measurement_with_mkey("cca.rim", &[0xAA, 0xBB, 0xCC]); - let rem = measurement_with_mkey("cca.rem0", &[0x11, 0x22, 0x33]); + let rim = measurement_with_mkey("cca.rim", &[0xAA; 32]); + let rem = measurement_with_mkey("cca.rem0", &[0x11; 32]); let triples = vec![ReferenceTriple::new( EnvironmentMap::for_class("ACME", "Realm"), vec![rim.clone(), rem.clone()], @@ -271,6 +382,31 @@ fn realm_profile_accepts_reference_triple_with_mandatory_rim() { assert_eq!(claims[0].measurements.len(), 2); } +#[test] +fn realm_match_rejects_masked_rpv() { + let profile = CcaRealmProfile::new(); + let reference = masked_raw_value_measurement("cca.rpv", &[0xAA; 32], &[0xFF; 32]); + let evidence = raw_value_measurement("cca.rpv", &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn realm_match_rejects_rim_extra_mval_field() { + let profile = CcaRealmProfile::new(); + let mut reference = measurement_with_mkey("cca.rim", &[0xAA; 32]); + reference.mval.raw_value = Some(RawValueChoice::Bytes(vec![0xCC; 32])); + let evidence = measurement_with_mkey("cca.rim", &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + #[test] fn realm_match_defers_for_non_cca_mkey() { let profile = CcaRealmProfile::new(); From b6ca63892e1e4e65f9e764137cf2537e08d15760 Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 21:04:21 +0000 Subject: [PATCH 09/18] Address remaining CCA validation review --- corim/src/profile/cca/mod.rs | 50 +++++++++++---- corim/tests/profile_cca_tests.rs | 101 ++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 12 deletions(-) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 21cd97a..56e3ac1 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -47,6 +47,8 @@ const CCA_HASH_SIZE_256: usize = 32; const CCA_HASH_SIZE_384: usize = 48; /// CCA hash size in bytes from draft-ydb-rats-cca-endorsements-04 §3.1.3.1 and §3.1.3.3. const CCA_HASH_SIZE_512: usize = 64; +/// CCA Realm personalization value size in bytes from draft-ydb-rats-cca-endorsements-04 §3.2.3. +const CCA_RPV_SIZE: usize = 64; /// Recognize a CCA Platform measurement key, per draft-ydb-rats-cca-endorsements-04. pub fn is_cca_platform_mkey(name: &str) -> bool { @@ -168,8 +170,8 @@ fn is_bytes_raw_value(mval: &MeasurementValuesMap) -> bool { matches!(mval.raw_value, Some(RawValueChoice::Bytes(_))) } -fn has_raw_value(mval: &MeasurementValuesMap) -> bool { - mval.raw_value.is_some() +fn is_bytes_raw_value_of_len(mval: &MeasurementValuesMap, len: usize) -> bool { + matches!(&mval.raw_value, Some(RawValueChoice::Bytes(bytes)) if bytes.len() == len) } fn is_cca_software_component_mval(mval: &MeasurementValuesMap) -> bool { @@ -191,7 +193,7 @@ fn is_cca_masked_config_reference_mval(mval: &MeasurementValuesMap) -> bool { } fn is_cca_raw_config_evidence_mval(mval: &MeasurementValuesMap) -> bool { - has_no_mval_fields_except(mval, false, false, true, false, false) && has_raw_value(mval) + has_no_mval_fields_except(mval, false, false, true, false, false) && is_bytes_raw_value(mval) } fn is_cca_realm_digest_mval(mval: &MeasurementValuesMap) -> bool { @@ -199,7 +201,8 @@ fn is_cca_realm_digest_mval(mval: &MeasurementValuesMap) -> bool { } fn is_cca_rpv_mval(mval: &MeasurementValuesMap) -> bool { - has_no_mval_fields_except(mval, false, false, true, false, false) && is_bytes_raw_value(mval) + has_no_mval_fields_except(mval, false, false, true, false, false) + && is_bytes_raw_value_of_len(mval, CCA_RPV_SIZE) } fn is_valid_cca_platform_reference_measurement(m: &MeasurementMap) -> bool { @@ -264,13 +267,9 @@ fn raw_value_matches_with_reference_mask( evidence: &Option, ) -> bool { match (reference, evidence) { - (Some(RawValueChoice::Masked { value, mask }), Some(RawValueChoice::Bytes(evidence))) - | ( - Some(RawValueChoice::Masked { value, mask }), - Some(RawValueChoice::Masked { - value: evidence, .. - }), - ) => masked_bytes_match(value, evidence, mask), + (Some(RawValueChoice::Masked { value, mask }), Some(RawValueChoice::Bytes(evidence))) => { + masked_bytes_match(value, evidence, mask) + } _ => reference == evidence, } } @@ -350,6 +349,32 @@ impl Profile for CcaPlatformProfile { &self.id } + fn reference_measurements_valid(&self, measurements: &[MeasurementMap]) -> bool { + let mut platform_config_count = 0usize; + let mut manufacturing_config_count = 0usize; + + for measurement in measurements { + let Some(mkey) = mkey_name(&measurement.mkey) else { + continue; + }; + + if !is_cca_platform_mkey(&mkey) { + continue; + } + if !is_valid_cca_platform_reference_measurement(measurement) { + return false; + } + + match mkey.as_str() { + "cca.platform-config" => platform_config_count += 1, + "cca.platform-manufacturing-config" => manufacturing_config_count += 1, + _ => {} + } + } + + platform_config_count <= 1 && manufacturing_config_count <= 1 + } + fn match_measurement( &self, reference: &MeasurementMap, @@ -389,6 +414,9 @@ impl Profile for CcaRealmProfile { }; if is_cca_realm_mkey(&mkey) { + if !is_valid_cca_realm_measurement(measurement) { + return false; + } has_rim |= mkey == "cca.rim"; } } diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index d743d63..2ecaa51 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -205,6 +205,68 @@ fn platform_match_accepts_masked_config_reference_against_unmasked_evidence() { ); } +#[test] +fn platform_match_rejects_masked_config_evidence() { + let profile = CcaPlatformProfile::new(); + let reference = + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]); + let evidence = + masked_raw_value_measurement("cca.platform-config", &[0xAF, 0xFF], &[0xFF, 0xFF]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + +#[test] +fn platform_profile_rejects_duplicate_config_measurements() { + let profile = CcaPlatformProfile::new(); + let config = masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]); + let triples = vec![ReferenceTriple::new( + EnvironmentMap::for_class("ACME", "Platform"), + vec![config.clone(), config.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: EnvironmentMap::for_class("ACME", "Platform"), + measurements: vec![raw_value_measurement("cca.platform-config", &[0xAF, 0xFF])], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + +#[test] +fn platform_profile_rejects_malformed_reference_measurement_in_triple() { + let profile = CcaPlatformProfile::new(); + let mut malformed_rotpk = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + malformed_rotpk.mval.raw_value = Some(RawValueChoice::Bytes(vec![0xCC; 32])); + let config = masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]); + let triples = vec![ReferenceTriple::new( + EnvironmentMap::for_class("ACME", "Platform"), + vec![malformed_rotpk, config.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: EnvironmentMap::for_class("ACME", "Platform"), + measurements: vec![raw_value_measurement("cca.platform-config", &[0xAF, 0xFF])], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + #[test] fn platform_match_rejects_unmasked_config_reference() { let profile = CcaPlatformProfile::new(); @@ -302,7 +364,7 @@ fn platform_match_rejects_authorized_by() { #[test] fn realm_match_rejects_raw_value_violation() { let profile = CcaRealmProfile::new(); - let reference = raw_value_measurement("cca.rpv", b"abc"); + let reference = raw_value_measurement("cca.rpv", &[0xAA; 64]); let evidence = measurement_with_mkey("cca.rpv", b"def"); assert_eq!( @@ -311,6 +373,18 @@ fn realm_match_rejects_raw_value_violation() { ); } +#[test] +fn realm_match_rejects_short_rpv() { + let profile = CcaRealmProfile::new(); + let reference = raw_value_measurement("cca.rpv", b"abc"); + let evidence = raw_value_measurement("cca.rpv", b"abc"); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(false) + ); +} + #[test] fn realm_profile_rejects_reference_triple_without_mandatory_rim() { let profile = CcaRealmProfile::new(); @@ -382,6 +456,31 @@ fn realm_profile_accepts_reference_triple_with_mandatory_rim() { assert_eq!(claims[0].measurements.len(), 2); } +#[test] +fn realm_profile_rejects_malformed_rim_in_reference_triple() { + let profile = CcaRealmProfile::new(); + let mut rim = measurement_with_mkey("cca.rim", &[0xAA; 32]); + rim.authorized_by = Some(vec![CryptoKey::Bytes(vec![0xCC; 32])]); + let rem = measurement_with_mkey("cca.rem0", &[0x11; 32]); + let triples = vec![ReferenceTriple::new( + EnvironmentMap::for_class("ACME", "Realm"), + vec![rim, rem.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: EnvironmentMap::for_class("ACME", "Realm"), + measurements: vec![rem], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + #[test] fn realm_match_rejects_masked_rpv() { let profile = CcaRealmProfile::new(); From 694cec59f4d126e2b5541be054f599e8fb7ce438 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:38:09 +0000 Subject: [PATCH 10/18] Enforce CCA triple environment linkage and platform completeness Co-authored-by: a-trikalinou <139903738+a-trikalinou@users.noreply.github.com> --- corim/src/profile.rs | 21 +- corim/src/profile/cca/mod.rs | 127 ++++++++++- corim/src/validate.rs | 2 +- corim/tests/profile_cca_tests.rs | 376 ++++++++++++++++++++++++------- 4 files changed, 430 insertions(+), 96 deletions(-) diff --git a/corim/src/profile.rs b/corim/src/profile.rs index 765741c..b45cfc8 100644 --- a/corim/src/profile.rs +++ b/corim/src/profile.rs @@ -158,6 +158,7 @@ use crate::cbor::value::Value; use crate::types::common::CborTime; use crate::types::corim::ProfileChoice; use crate::types::measurement::MeasurementMap; +use crate::types::triples::ReferenceTriple; /// First-party Intel CoRIM profile (`draft-cds-rats-intel-corim-profile`). /// @@ -301,16 +302,20 @@ pub trait Profile { None } - /// Validate profile-specific constraints across the measurements in a - /// single reference triple before per-measurement appraisal begins. + /// Validate profile-specific constraints over a whole reference + /// triple before per-measurement appraisal begins. /// /// Use this when the profile has requirements that cannot be checked from - /// one `(reference, evidence)` measurement pair alone, such as a mandatory - /// measurement that must appear somewhere in the reference triple. Return - /// `false` to make the whole reference triple ineligible for profile-aware - /// matching. Profiles without cross-measurement requirements can use the - /// default implementation. - fn reference_measurements_valid(&self, _measurements: &[MeasurementMap]) -> bool { + /// one `(reference, evidence)` measurement pair alone — a mandatory + /// measurement that must appear somewhere in the triple, a cardinality + /// constraint across measurements, or a constraint on the triple's + /// [`environment`][crate::types::triples::ReferenceTriple::environment] + /// such as a profile-defined subject identifier that must be present + /// and consistent with the measurements. Return `false` to make the + /// whole reference triple ineligible for profile-aware matching. + /// Profiles without triple-level requirements can use the default + /// implementation. + fn reference_triple_valid(&self, _triple: &ReferenceTriple) -> bool { true } diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 56e3ac1..23bc05b 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -22,15 +22,20 @@ //! //! - identifying the CCA profile URI, //! - validating the CCA-specific `mkey` names, -//! - enforcing the CCA-specific measurement-map shapes. +//! - enforcing the CCA-specific measurement-map shapes, +//! - enforcing the triple-level constraints: the environment subject +//! (Platform Implementation ID / Realm RIM) and the measurement +//! cardinality a reference triple must satisfy. use crate::nostd_prelude::*; use crate::profile::{MatchContext, Profile}; -use crate::types::common::{CryptoKey, MeasuredElement}; +use crate::types::common::{ClassIdChoice, CryptoKey, InstanceIdChoice, MeasuredElement}; use crate::types::corim::ProfileChoice; +use crate::types::environment::EnvironmentMap; use crate::types::measurement::{ Digest, DigestAlg, MeasurementMap, MeasurementValuesMap, RawValueChoice, }; +use crate::types::triples::ReferenceTriple; /// Profile URI for CCA Platform endorsements. pub const CCA_PLATFORM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_platform#1.0.0"; @@ -49,6 +54,13 @@ const CCA_HASH_SIZE_384: usize = 48; const CCA_HASH_SIZE_512: usize = 64; /// CCA Realm personalization value size in bytes from draft-ydb-rats-cca-endorsements-04 §3.2.3. const CCA_RPV_SIZE: usize = 64; +/// CCA Platform Implementation ID size in bytes from draft-ydb-rats-cca-endorsements-04 §3.1.2. +const CCA_IMPLEMENTATION_ID_SIZE: usize = 32; +/// CCA Platform Instance ID (UEID) size in bytes, including the type byte, +/// from draft-ydb-rats-cca-endorsements-04 §3.1.2. +const CCA_INSTANCE_ID_SIZE: usize = 33; +/// UEID `RAND` type byte required by draft-ydb-rats-cca-endorsements-04 §3.1.2. +const CCA_INSTANCE_ID_RAND_TYPE: u8 = 0x01; /// Recognize a CCA Platform measurement key, per draft-ydb-rats-cca-endorsements-04. pub fn is_cca_platform_mkey(name: &str) -> bool { @@ -302,6 +314,68 @@ fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { } } +fn has_duplicate_mkeys(measurements: &[MeasurementMap], recognized: fn(&str) -> bool) -> bool { + measurements.iter().enumerate().any(|(i, measurement)| { + let Some(mkey) = mkey_name(&measurement.mkey) else { + return false; + }; + if !recognized(&mkey) { + return false; + } + measurements + .iter() + .skip(i + 1) + .any(|other| mkey_name(&other.mkey).as_ref() == Some(&mkey)) + }) +} + +fn class_id_bytes(environment: &EnvironmentMap) -> Option<&[u8]> { + match environment.class.as_ref()?.class_id.as_ref()? { + ClassIdChoice::Bytes(bytes) => Some(bytes), + _ => None, + } +} + +/// The subject of a CCA Platform triple is the Implementation ID, encoded as +/// `#6.560(bytes .size 32)` in `environment.class.class-id`, optionally +/// narrowed to a single instance by a `#6.550` UEID +/// (draft-ydb-rats-cca-endorsements-04 §3.1.2). +fn is_valid_cca_platform_environment(environment: &EnvironmentMap) -> bool { + let Some(impl_id) = class_id_bytes(environment) else { + return false; + }; + if impl_id.len() != CCA_IMPLEMENTATION_ID_SIZE { + return false; + } + + match &environment.instance { + None => true, + Some(InstanceIdChoice::Ueid(ueid)) => { + ueid.len() == CCA_INSTANCE_ID_SIZE && ueid[0] == CCA_INSTANCE_ID_RAND_TYPE + } + Some(_) => false, + } +} + +/// The subject of a CCA Realm triple is the RIM itself, encoded as +/// `#6.560(cca-hash-type)` in `environment.class.class-id` +/// (draft-ydb-rats-cca-endorsements-04 §3.2.2). The same value is also +/// carried as the mandatory `cca.rim` digest, so the two MUST agree. +fn is_valid_cca_realm_environment(environment: &EnvironmentMap) -> bool { + environment.instance.is_none() + && class_id_bytes(environment).is_some_and(|rim| is_cca_hash_size(rim.len())) +} + +fn realm_rim_matches_environment(environment: &EnvironmentMap, rim: &MeasurementMap) -> bool { + let Some(class_rim) = class_id_bytes(environment) else { + return false; + }; + rim.mval + .digests + .as_ref() + .is_some_and(|digests| digests.iter().any(|digest| digest.value() == class_rim)) +} + /// Profile implementation for Arm CCA Platform endorsements. #[derive(Clone, Debug, PartialEq)] pub struct CcaPlatformProfile { @@ -349,11 +423,17 @@ impl Profile for CcaPlatformProfile { &self.id } - fn reference_measurements_valid(&self, measurements: &[MeasurementMap]) -> bool { + fn reference_triple_valid(&self, triple: &ReferenceTriple) -> bool { + if !is_valid_cca_platform_environment(triple.environment()) { + return false; + } + + let mut software_component_count = 0usize; let mut platform_config_count = 0usize; let mut manufacturing_config_count = 0usize; + let mut rotpk_count = 0usize; - for measurement in measurements { + for measurement in triple.measurements() { let Some(mkey) = mkey_name(&measurement.mkey) else { continue; }; @@ -366,13 +446,29 @@ impl Profile for CcaPlatformProfile { } match mkey.as_str() { + "cca.software-component" => software_component_count += 1, "cca.platform-config" => platform_config_count += 1, "cca.platform-manufacturing-config" => manufacturing_config_count += 1, - _ => {} + _ => rotpk_count += 1, } } - platform_config_count <= 1 && manufacturing_config_count <= 1 + // §3.1.3.3: each ROTPK array entry is carried in its own reference + // triple, so a ROTPK triple describes no other platform measurement. + if rotpk_count > 0 { + return software_component_count == 0 + && platform_config_count == 0 + && manufacturing_config_count == 0 + && !has_duplicate_mkeys(triple.measurements(), is_cca_platform_mkey); + } + + // §3.1.3: a single reference triple MUST completely describe the CCA + // Platform measurements — a mandatory platform configuration + // (§3.1.3.2, "only one") and the platform software components + // (§3.1.3.1), plus at most one manufacturing configuration (§3.1.3.4). + software_component_count >= 1 + && platform_config_count == 1 + && manufacturing_config_count <= 1 } fn match_measurement( @@ -405,10 +501,14 @@ impl Profile for CcaRealmProfile { &self.id } - fn reference_measurements_valid(&self, measurements: &[MeasurementMap]) -> bool { + fn reference_triple_valid(&self, triple: &ReferenceTriple) -> bool { + if !is_valid_cca_realm_environment(triple.environment()) { + return false; + } + let mut has_rim = false; - for measurement in measurements { + for measurement in triple.measurements() { let Some(mkey) = mkey_name(&measurement.mkey) else { continue; }; @@ -417,11 +517,18 @@ impl Profile for CcaRealmProfile { if !is_valid_cca_realm_measurement(measurement) { return false; } - has_rim |= mkey == "cca.rim"; + // §3.2.2: the environment class-id carries the RIM, so the + // mandatory `cca.rim` measurement MUST report the same value. + if mkey == "cca.rim" { + if !realm_rim_matches_environment(triple.environment(), measurement) { + return false; + } + has_rim = true; + } } } - has_rim + has_rim && !has_duplicate_mkeys(triple.measurements(), is_cca_realm_mkey) } fn match_measurement( diff --git a/corim/src/validate.rs b/corim/src/validate.rs index d8553d1..a6bdfb2 100644 --- a/corim/src/validate.rs +++ b/corim/src/validate.rs @@ -341,7 +341,7 @@ pub fn match_reference_values_with_profile( let mut corroborated = Vec::new(); for triple in ref_triples { - if profile.is_some_and(|p| !p.reference_measurements_valid(triple.measurements())) { + if profile.is_some_and(|p| !p.reference_triple_valid(triple)) { continue; } diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index 2ecaa51..f0b6990 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -8,9 +8,9 @@ use corim::profile::cca::{ CCA_PLATFORM_PROFILE_URI, CCA_REALM_PROFILE_URI, }; use corim::profile::{MatchContext, Profile}; -use corim::types::common::{CryptoKey, MeasuredElement}; +use corim::types::common::{ClassIdChoice, CryptoKey, InstanceIdChoice, MeasuredElement}; use corim::types::corim::ProfileChoice; -use corim::types::environment::EnvironmentMap; +use corim::types::environment::{ClassMap, EnvironmentMap}; use corim::types::measurement::{Digest, MeasurementMap, MeasurementValuesMap, RawValueChoice}; use corim::types::triples::ReferenceTriple; use corim::validate::{match_reference_values_with_profile, EvidenceClaim}; @@ -74,6 +74,29 @@ fn masked_raw_value_measurement(mkey: &str, value: &[u8], mask: &[u8]) -> Measur } } +fn environment_with_class_id(class_id: &[u8]) -> EnvironmentMap { + EnvironmentMap { + class: Some(ClassMap { + class_id: Some(ClassIdChoice::Bytes(class_id.to_vec())), + ..ClassMap::default() + }), + instance: None, + group: None, + } +} + +/// Platform triples are keyed by a 32-byte Implementation ID +/// (draft-ydb-rats-cca-endorsements-04 §3.1.2). +fn platform_environment() -> EnvironmentMap { + environment_with_class_id(&[0x5A; 32]) +} + +/// Realm triples carry the RIM itself as `class-id` +/// (draft-ydb-rats-cca-endorsements-04 §3.2.2). +fn realm_environment(rim: &[u8]) -> EnvironmentMap { + environment_with_class_id(rim) +} + #[test] fn platform_profile_uses_cca_platform_uri() { let profile = CcaPlatformProfile::new(); @@ -219,17 +242,162 @@ fn platform_match_rejects_masked_config_evidence() { ); } +fn platform_evidence() -> Vec { + vec![EvidenceClaim { + environment: platform_environment(), + measurements: vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + raw_value_measurement("cca.platform-config", &[0xAF, 0xFF]), + ], + }] +} + +fn platform_claims(profile: &CcaPlatformProfile, measurements: Vec) -> usize { + let triples = vec![ReferenceTriple::new(platform_environment(), measurements)]; + + match_reference_values_with_profile( + &triples, + &platform_evidence(), + Some(profile), + &MatchContext::new(), + ) + .len() +} + +#[test] +fn platform_profile_accepts_complete_reference_triple() { + let profile = CcaPlatformProfile::new(); + + assert_eq!( + platform_claims( + &profile, + vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]), + ], + ), + 1 + ); +} + #[test] fn platform_profile_rejects_duplicate_config_measurements() { let profile = CcaPlatformProfile::new(); let config = masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]); + + assert_eq!( + platform_claims( + &profile, + vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + config.clone(), + config, + ], + ), + 0 + ); +} + +#[test] +fn platform_profile_rejects_triple_without_platform_config() { + let profile = CcaPlatformProfile::new(); + + assert_eq!( + platform_claims( + &profile, + vec![software_component_measurement( + "cca.software-component", + &[0x11; 32], + &[0xAA; 32] + )], + ), + 0 + ); +} + +#[test] +fn platform_profile_rejects_triple_without_software_component() { + let profile = CcaPlatformProfile::new(); + + assert_eq!( + platform_claims( + &profile, + vec![masked_raw_value_measurement( + "cca.platform-config", + &[0xA0, 0x05], + &[0xF0, 0x00] + )], + ), + 0 + ); +} + +#[test] +fn platform_profile_rejects_triple_without_cca_measurements() { + let profile = CcaPlatformProfile::new(); + + assert_eq!( + platform_claims( + &profile, + vec![measurement_with_mkey("tee.something", &[0x11; 32])], + ), + 0 + ); +} + +#[test] +fn platform_profile_accepts_standalone_rotpk_triple() { + let profile = CcaPlatformProfile::new(); + let rotpk = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + let triples = vec![ReferenceTriple::new( + platform_environment(), + vec![rotpk.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: platform_environment(), + measurements: vec![rotpk], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert_eq!(claims.len(), 1); +} + +#[test] +fn platform_profile_rejects_rotpk_mixed_with_platform_measurements() { + let profile = CcaPlatformProfile::new(); + + assert_eq!( + platform_claims( + &profile, + vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]), + rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]), + ], + ), + 0 + ); +} + +#[test] +fn platform_profile_rejects_triple_without_implementation_id() { + let profile = CcaPlatformProfile::new(); let triples = vec![ReferenceTriple::new( EnvironmentMap::for_class("ACME", "Platform"), - vec![config.clone(), config.clone()], + vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]), + ], )]; let evidence = vec![EvidenceClaim { environment: EnvironmentMap::for_class("ACME", "Platform"), - measurements: vec![raw_value_measurement("cca.platform-config", &[0xAF, 0xFF])], + measurements: platform_evidence().remove(0).measurements, }]; let claims = match_reference_values_with_profile( @@ -243,18 +411,20 @@ fn platform_profile_rejects_duplicate_config_measurements() { } #[test] -fn platform_profile_rejects_malformed_reference_measurement_in_triple() { +fn platform_profile_rejects_triple_with_non_ueid_instance() { let profile = CcaPlatformProfile::new(); - let mut malformed_rotpk = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); - malformed_rotpk.mval.raw_value = Some(RawValueChoice::Bytes(vec![0xCC; 32])); - let config = masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]); + let mut environment = platform_environment(); + environment.instance = Some(InstanceIdChoice::Bytes(vec![0x01; 32])); let triples = vec![ReferenceTriple::new( - EnvironmentMap::for_class("ACME", "Platform"), - vec![malformed_rotpk, config.clone()], + environment.clone(), + vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]), + ], )]; let evidence = vec![EvidenceClaim { - environment: EnvironmentMap::for_class("ACME", "Platform"), - measurements: vec![raw_value_measurement("cca.platform-config", &[0xAF, 0xFF])], + environment, + measurements: platform_evidence().remove(0).measurements, }]; let claims = match_reference_values_with_profile( @@ -267,6 +437,25 @@ fn platform_profile_rejects_malformed_reference_measurement_in_triple() { assert!(claims.is_empty()); } +#[test] +fn platform_profile_rejects_malformed_reference_measurement_in_triple() { + let profile = CcaPlatformProfile::new(); + let mut malformed_software_component = + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]); + malformed_software_component.mval.cryptokeys = None; + + assert_eq!( + platform_claims( + &profile, + vec![ + malformed_software_component, + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]), + ], + ), + 0 + ); +} + #[test] fn platform_match_rejects_unmasked_config_reference() { let profile = CcaPlatformProfile::new(); @@ -385,100 +574,133 @@ fn realm_match_rejects_short_rpv() { ); } -#[test] -fn realm_profile_rejects_reference_triple_without_mandatory_rim() { - let profile = CcaRealmProfile::new(); - let rem = measurement_with_mkey("cca.rem0", &[0x11; 32]); +fn realm_claims( + profile: &CcaRealmProfile, + environment: EnvironmentMap, + measurements: Vec, +) -> usize { let triples = vec![ReferenceTriple::new( - EnvironmentMap::for_class("ACME", "Realm"), - vec![rem.clone()], + environment.clone(), + measurements.clone(), )]; let evidence = vec![EvidenceClaim { - environment: EnvironmentMap::for_class("ACME", "Realm"), - measurements: vec![rem], + environment, + measurements, }]; - let claims = match_reference_values_with_profile( - &triples, - &evidence, - Some(&profile), - &MatchContext::new(), - ); + match_reference_values_with_profile(&triples, &evidence, Some(profile), &MatchContext::new()) + .len() +} - assert!(claims.is_empty()); +#[test] +fn realm_profile_rejects_reference_triple_without_mandatory_rim() { + let profile = CcaRealmProfile::new(); + + assert_eq!( + realm_claims( + &profile, + realm_environment(&[0xAA; 32]), + vec![measurement_with_mkey("cca.rem0", &[0x11; 32])], + ), + 0 + ); } #[test] fn realm_profile_rejects_reference_triple_without_cca_measurements() { let profile = CcaRealmProfile::new(); - let measurement = measurement_with_mkey("tee.something", &[0x11; 32]); - let triples = vec![ReferenceTriple::new( - EnvironmentMap::for_class("ACME", "Realm"), - vec![measurement.clone()], - )]; - let evidence = vec![EvidenceClaim { - environment: EnvironmentMap::for_class("ACME", "Realm"), - measurements: vec![measurement], - }]; - let claims = match_reference_values_with_profile( - &triples, - &evidence, - Some(&profile), - &MatchContext::new(), + assert_eq!( + realm_claims( + &profile, + realm_environment(&[0xAA; 32]), + vec![measurement_with_mkey("tee.something", &[0x11; 32])], + ), + 0 ); - - assert!(claims.is_empty()); } #[test] fn realm_profile_accepts_reference_triple_with_mandatory_rim() { let profile = CcaRealmProfile::new(); - let rim = measurement_with_mkey("cca.rim", &[0xAA; 32]); - let rem = measurement_with_mkey("cca.rem0", &[0x11; 32]); - let triples = vec![ReferenceTriple::new( - EnvironmentMap::for_class("ACME", "Realm"), - vec![rim.clone(), rem.clone()], - )]; - let evidence = vec![EvidenceClaim { - environment: EnvironmentMap::for_class("ACME", "Realm"), - measurements: vec![rim, rem], - }]; + let rim_value = [0xAA; 32]; - let claims = match_reference_values_with_profile( - &triples, - &evidence, - Some(&profile), - &MatchContext::new(), + assert_eq!( + realm_claims( + &profile, + realm_environment(&rim_value), + vec![ + measurement_with_mkey("cca.rim", &rim_value), + measurement_with_mkey("cca.rem0", &[0x11; 32]), + ], + ), + 1 ); +} - assert_eq!(claims.len(), 1); - assert_eq!(claims[0].measurements.len(), 2); +#[test] +fn realm_profile_rejects_rim_that_disagrees_with_environment_class_id() { + let profile = CcaRealmProfile::new(); + + assert_eq!( + realm_claims( + &profile, + realm_environment(&[0xAA; 32]), + vec![measurement_with_mkey("cca.rim", &[0xBB; 32])], + ), + 0 + ); } #[test] -fn realm_profile_rejects_malformed_rim_in_reference_triple() { +fn realm_profile_rejects_reference_triple_without_rim_class_id() { let profile = CcaRealmProfile::new(); - let mut rim = measurement_with_mkey("cca.rim", &[0xAA; 32]); - rim.authorized_by = Some(vec![CryptoKey::Bytes(vec![0xCC; 32])]); + + assert_eq!( + realm_claims( + &profile, + EnvironmentMap::for_class("ACME", "Realm"), + vec![measurement_with_mkey("cca.rim", &[0xAA; 32])], + ), + 0 + ); +} + +#[test] +fn realm_profile_rejects_duplicate_realm_measurements() { + let profile = CcaRealmProfile::new(); + let rim_value = [0xAA; 32]; let rem = measurement_with_mkey("cca.rem0", &[0x11; 32]); - let triples = vec![ReferenceTriple::new( - EnvironmentMap::for_class("ACME", "Realm"), - vec![rim, rem.clone()], - )]; - let evidence = vec![EvidenceClaim { - environment: EnvironmentMap::for_class("ACME", "Realm"), - measurements: vec![rem], - }]; - let claims = match_reference_values_with_profile( - &triples, - &evidence, - Some(&profile), - &MatchContext::new(), + assert_eq!( + realm_claims( + &profile, + realm_environment(&rim_value), + vec![ + measurement_with_mkey("cca.rim", &rim_value), + rem.clone(), + rem, + ], + ), + 0 ); +} - assert!(claims.is_empty()); +#[test] +fn realm_profile_rejects_malformed_rim_in_reference_triple() { + let profile = CcaRealmProfile::new(); + let rim_value = [0xAA; 32]; + let mut rim = measurement_with_mkey("cca.rim", &rim_value); + rim.authorized_by = Some(vec![CryptoKey::Bytes(vec![0xCC; 32])]); + + assert_eq!( + realm_claims( + &profile, + realm_environment(&rim_value), + vec![rim, measurement_with_mkey("cca.rem0", &[0x11; 32])], + ), + 0 + ); } #[test] From a78fbafee16b790b6353d32ef57215538dddb40a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:41:12 +0000 Subject: [PATCH 11/18] Document realm RIM digest linkage semantics Co-authored-by: a-trikalinou <139903738+a-trikalinou@users.noreply.github.com> --- corim/src/profile/cca/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 23bc05b..189a431 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -366,6 +366,9 @@ fn is_valid_cca_realm_environment(environment: &EnvironmentMap) -> bool { && class_id_bytes(environment).is_some_and(|rim| is_cca_hash_size(rim.len())) } +/// The `cca.rim` measurement may report the RIM under more than one hash +/// algorithm, and the class-id carries exactly one of those values, so one +/// matching digest is what the linkage requires. fn realm_rim_matches_environment(environment: &EnvironmentMap, rim: &MeasurementMap) -> bool { let Some(class_rim) = class_id_bytes(environment) else { return false; From 5d6197ede0dea83b23a36b7df54351ae23c50379 Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 22:01:17 +0000 Subject: [PATCH 12/18] Address CCA ROTPK grouping review --- corim/src/profile.rs | 10 +- corim/src/profile/cca/mod.rs | 151 +++++++++++++++++++++++-------- corim/src/validate.rs | 9 +- corim/tests/profile_cca_tests.rs | 53 +++++++++++ 4 files changed, 176 insertions(+), 47 deletions(-) diff --git a/corim/src/profile.rs b/corim/src/profile.rs index b45cfc8..58ec1f4 100644 --- a/corim/src/profile.rs +++ b/corim/src/profile.rs @@ -190,11 +190,11 @@ pub mod psa; /// Minimal Arm CCA endorsements profile support for /// `draft-ydb-rats-cca-endorsements-04`. /// -/// The draft primarily introduces CCA Platform / Realm profile URIs and -/// the characteristic `mkey` names used for measurements. The core crate -/// already knows how to compare the underlying `digests` / `raw-value` -/// fields, so this module focuses on profile identification and -/// enforcement that the `mkey` names belong to the CCA profile. +/// The module recognizes CCA Platform / Realm profile URIs and measurement +/// keys, validates CCA-specific measurement shapes and environment subject +/// identifiers, enforces triple-level cardinality and linkage constraints, +/// and adds matching semantics for CCA cryptokeys and masked configuration +/// reference values that the generic matcher deliberately does not handle. #[cfg(feature = "profile-cca")] #[cfg_attr(docsrs, doc(cfg(feature = "profile-cca")))] pub mod cca; diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 189a431..40808d4 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -17,12 +17,13 @@ //! - Realm: `cca.rim`, `cca.rem0`..`cca.rem3`, `cca.rpv` //! //! The core crate already knows how to compare most underlying CBOR value -//! shapes (`digests`, `raw-value`, etc.) for these maps, so this minimal -//! profile support focuses on: +//! shapes (`digests`, `raw-value`, etc.) for these maps. This profile adds +//! CCA-specific checks for fields the generic matcher does not own, including: //! //! - identifying the CCA profile URI, //! - validating the CCA-specific `mkey` names, //! - enforcing the CCA-specific measurement-map shapes, +//! - matching cryptokeys and masked configuration reference values, //! - enforcing the triple-level constraints: the environment subject //! (Platform Implementation ID / Realm RIM) and the measurement //! cardinality a reference triple must satisfy. @@ -42,6 +43,27 @@ pub const CCA_PLATFORM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_pl /// Profile URI for CCA Realm endorsements. pub const CCA_REALM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_realm#1.0.0"; +/// CCA Platform software-component measurement key. +pub const CCA_MKEY_SOFTWARE_COMPONENT: &str = "cca.software-component"; +/// CCA Platform configuration measurement key. +pub const CCA_MKEY_PLATFORM_CONFIG: &str = "cca.platform-config"; +/// CCA Platform manufacturing configuration measurement key. +pub const CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG: &str = "cca.platform-manufacturing-config"; +/// Prefix for CCA Platform ROTPK measurement keys. +pub const CCA_MKEY_ROTPK_PREFIX: &str = "cca.rotpk."; +/// CCA Realm initial measurement key. +pub const CCA_MKEY_RIM: &str = "cca.rim"; +/// CCA Realm extended measurement key for bank 0. +pub const CCA_MKEY_REM0: &str = "cca.rem0"; +/// CCA Realm extended measurement key for bank 1. +pub const CCA_MKEY_REM1: &str = "cca.rem1"; +/// CCA Realm extended measurement key for bank 2. +pub const CCA_MKEY_REM2: &str = "cca.rem2"; +/// CCA Realm extended measurement key for bank 3. +pub const CCA_MKEY_REM3: &str = "cca.rem3"; +/// CCA Realm personalization value measurement key. +pub const CCA_MKEY_RPV: &str = "cca.rpv"; + /// Maximum ROTPK array index from draft-ydb-rats-cca-endorsements-04 §3.1.3.3. const CCA_ROTPK_MAX_INDEX: u8 = 7; /// Maximum ROTPK slot index from draft-ydb-rats-cca-endorsements-04 §3.1.3.3. @@ -62,46 +84,69 @@ const CCA_INSTANCE_ID_SIZE: usize = 33; /// UEID `RAND` type byte required by draft-ydb-rats-cca-endorsements-04 §3.1.2. const CCA_INSTANCE_ID_RAND_TYPE: u8 = 0x01; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RotpkFamily { + Cm, + Dm, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct RotpkMkey { + family: RotpkFamily, + index: u8, + slot: u8, +} + /// Recognize a CCA Platform measurement key, per draft-ydb-rats-cca-endorsements-04. pub fn is_cca_platform_mkey(name: &str) -> bool { match name { - "cca.software-component" | "cca.platform-config" | "cca.platform-manufacturing-config" => { - true - } - _ => { - let Some(rest) = name.strip_prefix("cca.rotpk.") else { - return false; - }; - let mut parts = rest.split('.'); - let family = parts.next(); - let idx = parts.next(); - let slot = parts.next(); - if parts.next().is_some() { - return false; - } - matches!(family, Some("CM") | Some("DM")) - && one_digit_at_most(idx, CCA_ROTPK_MAX_INDEX) - && one_digit_at_most(slot, CCA_ROTPK_MAX_SLOT) - } + CCA_MKEY_SOFTWARE_COMPONENT + | CCA_MKEY_PLATFORM_CONFIG + | CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG => true, + _ => parse_rotpk_mkey(name).is_some(), } } -fn one_digit_at_most(value: Option<&str>, max: u8) -> bool { - let Some(value) = value else { - return false; - }; +fn one_digit_at_most(value: Option<&str>, max: u8) -> Option { + let value = value?; let [digit] = value.as_bytes() else { - return false; + return None; }; - digit.is_ascii_digit() && digit - b'0' <= max + if !digit.is_ascii_digit() { + return None; + } + + let value = digit - b'0'; + (value <= max).then_some(value) +} + +fn parse_rotpk_mkey(name: &str) -> Option { + let rest = name.strip_prefix(CCA_MKEY_ROTPK_PREFIX)?; + let mut parts = rest.split('.'); + let family = match parts.next()? { + "CM" => RotpkFamily::Cm, + "DM" => RotpkFamily::Dm, + _ => return None, + }; + let index = one_digit_at_most(parts.next(), CCA_ROTPK_MAX_INDEX)?; + let slot = one_digit_at_most(parts.next(), CCA_ROTPK_MAX_SLOT)?; + if parts.next().is_some() { + return None; + } + + Some(RotpkMkey { + family, + index, + slot, + }) } /// Recognize a CCA Realm measurement key. pub fn is_cca_realm_mkey(name: &str) -> bool { matches!( name, - "cca.rim" | "cca.rem0" | "cca.rem1" | "cca.rem2" | "cca.rem3" | "cca.rpv" + CCA_MKEY_RIM | CCA_MKEY_REM0 | CCA_MKEY_REM1 | CCA_MKEY_REM2 | CCA_MKEY_REM3 | CCA_MKEY_RPV ) } @@ -227,8 +272,8 @@ fn is_valid_cca_platform_reference_measurement(m: &MeasurementMap) -> bool { }; match mkey.as_str() { - "cca.software-component" => is_cca_software_component_mval(&m.mval), - "cca.platform-config" | "cca.platform-manufacturing-config" => { + CCA_MKEY_SOFTWARE_COMPONENT => is_cca_software_component_mval(&m.mval), + CCA_MKEY_PLATFORM_CONFIG | CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG => { is_cca_masked_config_reference_mval(&m.mval) } _ if is_cca_platform_mkey(&mkey) => is_cca_rotpk_mval(&m.mval), @@ -246,8 +291,8 @@ fn is_valid_cca_platform_evidence_measurement(m: &MeasurementMap) -> bool { }; match mkey.as_str() { - "cca.software-component" => is_cca_software_component_mval(&m.mval), - "cca.platform-config" | "cca.platform-manufacturing-config" => { + CCA_MKEY_SOFTWARE_COMPONENT => is_cca_software_component_mval(&m.mval), + CCA_MKEY_PLATFORM_CONFIG | CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG => { is_cca_raw_config_evidence_mval(&m.mval) } _ if is_cca_platform_mkey(&mkey) => is_cca_rotpk_mval(&m.mval), @@ -261,7 +306,7 @@ fn cca_platform_measurements_match(reference: &MeasurementMap, evidence: &Measur }; match mkey.as_str() { - "cca.platform-config" | "cca.platform-manufacturing-config" => { + CCA_MKEY_PLATFORM_CONFIG | CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG => { raw_value_matches_with_reference_mask( &reference.mval.raw_value, &evidence.mval.raw_value, @@ -306,14 +351,42 @@ fn is_valid_cca_realm_measurement(m: &MeasurementMap) -> bool { }; match mkey.as_str() { - "cca.rim" | "cca.rem0" | "cca.rem1" | "cca.rem2" | "cca.rem3" => { + CCA_MKEY_RIM | CCA_MKEY_REM0 | CCA_MKEY_REM1 | CCA_MKEY_REM2 | CCA_MKEY_REM3 => { is_cca_realm_digest_mval(&m.mval) } - "cca.rpv" => is_cca_rpv_mval(&m.mval), + CCA_MKEY_RPV => is_cca_rpv_mval(&m.mval), _ => false, } } +fn valid_rotpk_group(measurements: &[MeasurementMap]) -> bool { + let mut group = None; + let mut slots = [false; (CCA_ROTPK_MAX_SLOT as usize) + 1]; + + for measurement in measurements { + let Some(mkey) = mkey_name(&measurement.mkey) else { + continue; + }; + let Some(rotpk) = parse_rotpk_mkey(&mkey) else { + continue; + }; + + let current_group = (rotpk.family, rotpk.index); + if group.is_some_and(|group| group != current_group) { + return false; + } + group = Some(current_group); + + let slot = usize::from(rotpk.slot); + if slots[slot] { + return false; + } + slots[slot] = true; + } + + true +} + fn has_duplicate_mkeys(measurements: &[MeasurementMap], recognized: fn(&str) -> bool) -> bool { measurements.iter().enumerate().any(|(i, measurement)| { let Some(mkey) = mkey_name(&measurement.mkey) else { @@ -449,9 +522,9 @@ impl Profile for CcaPlatformProfile { } match mkey.as_str() { - "cca.software-component" => software_component_count += 1, - "cca.platform-config" => platform_config_count += 1, - "cca.platform-manufacturing-config" => manufacturing_config_count += 1, + CCA_MKEY_SOFTWARE_COMPONENT => software_component_count += 1, + CCA_MKEY_PLATFORM_CONFIG => platform_config_count += 1, + CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG => manufacturing_config_count += 1, _ => rotpk_count += 1, } } @@ -462,7 +535,7 @@ impl Profile for CcaPlatformProfile { return software_component_count == 0 && platform_config_count == 0 && manufacturing_config_count == 0 - && !has_duplicate_mkeys(triple.measurements(), is_cca_platform_mkey); + && valid_rotpk_group(triple.measurements()); } // §3.1.3: a single reference triple MUST completely describe the CCA @@ -522,7 +595,7 @@ impl Profile for CcaRealmProfile { } // §3.2.2: the environment class-id carries the RIM, so the // mandatory `cca.rim` measurement MUST report the same value. - if mkey == "cca.rim" { + if mkey == CCA_MKEY_RIM { if !realm_rim_matches_environment(triple.environment(), measurement) { return false; } diff --git a/corim/src/validate.rs b/corim/src/validate.rs index a6bdfb2..3fb5502 100644 --- a/corim/src/validate.rs +++ b/corim/src/validate.rs @@ -315,9 +315,12 @@ pub struct EvidenceClaim { /// - `None` from the profile — defer to the default per-pair logic /// (the same comparison performed by [`match_reference_values`]). /// -/// The profile is consulted independently for each (reference, evidence) -/// pair within a triple. Pass `None` for `profile` to get behavior -/// identical to [`match_reference_values`]. +/// Before any per-pair matching, the profile's +/// [`Profile::reference_triple_valid`] hook is called once for each +/// reference triple. A `false` result skips that whole triple. The default +/// hook returns `true`, so profiles with no triple-level rules behave as +/// if only per-pair matching were customized. Pass `None` for `profile` to +/// get behavior identical to [`match_reference_values`]. /// /// Profile lookup is the caller's responsibility: /// diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index f0b6990..9683735 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -368,6 +368,59 @@ fn platform_profile_accepts_standalone_rotpk_triple() { assert_eq!(claims.len(), 1); } +#[test] +fn platform_profile_accepts_rotpk_triple_for_one_array_entry() { + let profile = CcaPlatformProfile::new(); + let first = rotpk_measurement("cca.rotpk.CM.2.0", &[0xAA; 32]); + let second = rotpk_measurement("cca.rotpk.CM.2.1", &[0xBB; 32]); + let triples = vec![ReferenceTriple::new( + platform_environment(), + vec![first.clone(), second.clone()], + )]; + let evidence = vec![EvidenceClaim { + environment: platform_environment(), + measurements: vec![first, second], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert_eq!(claims.len(), 1); + assert_eq!(claims[0].measurements.len(), 2); +} + +#[test] +fn platform_profile_rejects_rotpk_mixed_array_entries() { + let profile = CcaPlatformProfile::new(); + let first = rotpk_measurement("cca.rotpk.CM.2.0", &[0xAA; 32]); + let second = rotpk_measurement("cca.rotpk.CM.3.0", &[0xBB; 32]); + let third = rotpk_measurement("cca.rotpk.DM.2.1", &[0xCC; 32]); + + for measurements in [vec![first.clone(), second], vec![first.clone(), third]] { + let triples = vec![ReferenceTriple::new( + platform_environment(), + measurements.clone(), + )]; + let evidence = vec![EvidenceClaim { + environment: platform_environment(), + measurements, + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); + } +} + #[test] fn platform_profile_rejects_rotpk_mixed_with_platform_measurements() { let profile = CcaPlatformProfile::new(); From 7cde314dfd36873ab8893521e2cebdd52378cba8 Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 22:11:50 +0000 Subject: [PATCH 13/18] Handle ROTPK raw-value evidence --- corim/src/profile/cca/mod.rs | 44 +++++++++++++++++++++----------- corim/tests/profile_cca_tests.rs | 27 +++++++++++++++----- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 40808d4..924e430 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -23,7 +23,8 @@ //! - identifying the CCA profile URI, //! - validating the CCA-specific `mkey` names, //! - enforcing the CCA-specific measurement-map shapes, -//! - matching cryptokeys and masked configuration reference values, +//! - matching cryptokeys, ROTPK raw evidence, and masked configuration +//! reference values, //! - enforcing the triple-level constraints: the environment subject //! (Platform Implementation ID / Realm RIM) and the measurement //! cardinality a reference triple must satisfy. @@ -158,16 +159,21 @@ fn mkey_name(mkey: &Option) -> Option { } fn has_single_signer_key(mval: &MeasurementValuesMap) -> bool { - mval.cryptokeys.as_ref().is_some_and(|keys| { - keys.len() == 1 - && keys.iter().all(|k| match k { - CryptoKey::Bytes(b) => matches!( - b.len(), - CCA_HASH_SIZE_256 | CCA_HASH_SIZE_384 | CCA_HASH_SIZE_512 - ), - _ => false, - }) - }) + single_signer_key_bytes(mval).is_some_and(|bytes| is_cca_hash_size(bytes.len())) +} + +fn single_signer_key_bytes(mval: &MeasurementValuesMap) -> Option<&[u8]> { + match mval.cryptokeys.as_ref()?.as_slice() { + [CryptoKey::Bytes(bytes)] => Some(bytes), + _ => None, + } +} + +fn raw_value_bytes(mval: &MeasurementValuesMap) -> Option<&[u8]> { + match &mval.raw_value { + Some(RawValueChoice::Bytes(bytes)) => Some(bytes), + _ => None, + } } fn has_no_mval_fields_except( @@ -224,11 +230,11 @@ fn is_masked_raw_value(mval: &MeasurementValuesMap) -> bool { } fn is_bytes_raw_value(mval: &MeasurementValuesMap) -> bool { - matches!(mval.raw_value, Some(RawValueChoice::Bytes(_))) + raw_value_bytes(mval).is_some() } fn is_bytes_raw_value_of_len(mval: &MeasurementValuesMap, len: usize) -> bool { - matches!(&mval.raw_value, Some(RawValueChoice::Bytes(bytes)) if bytes.len() == len) + raw_value_bytes(mval).is_some_and(|bytes| bytes.len() == len) } fn is_cca_software_component_mval(mval: &MeasurementValuesMap) -> bool { @@ -245,6 +251,11 @@ fn is_cca_rotpk_mval(mval: &MeasurementValuesMap) -> bool { has_no_mval_fields_except(mval, false, false, false, false, true) && has_single_signer_key(mval) } +fn is_cca_rotpk_evidence_mval(mval: &MeasurementValuesMap) -> bool { + has_no_mval_fields_except(mval, false, false, true, false, false) + && raw_value_bytes(mval).is_some_and(|bytes| is_cca_hash_size(bytes.len())) +} + fn is_cca_masked_config_reference_mval(mval: &MeasurementValuesMap) -> bool { has_no_mval_fields_except(mval, false, false, true, false, false) && is_masked_raw_value(mval) } @@ -276,7 +287,7 @@ fn is_valid_cca_platform_reference_measurement(m: &MeasurementMap) -> bool { CCA_MKEY_PLATFORM_CONFIG | CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG => { is_cca_masked_config_reference_mval(&m.mval) } - _ if is_cca_platform_mkey(&mkey) => is_cca_rotpk_mval(&m.mval), + _ if parse_rotpk_mkey(&mkey).is_some() => is_cca_rotpk_mval(&m.mval), _ => false, } } @@ -295,7 +306,7 @@ fn is_valid_cca_platform_evidence_measurement(m: &MeasurementMap) -> bool { CCA_MKEY_PLATFORM_CONFIG | CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG => { is_cca_raw_config_evidence_mval(&m.mval) } - _ if is_cca_platform_mkey(&mkey) => is_cca_rotpk_mval(&m.mval), + _ if parse_rotpk_mkey(&mkey).is_some() => is_cca_rotpk_evidence_mval(&m.mval), _ => false, } } @@ -312,6 +323,9 @@ fn cca_platform_measurements_match(reference: &MeasurementMap, evidence: &Measur &evidence.mval.raw_value, ) } + _ if parse_rotpk_mkey(&mkey).is_some() => { + single_signer_key_bytes(&reference.mval) == raw_value_bytes(&evidence.mval) + } _ => { crate::validate::core_fields_match(reference, evidence) && reference.mval.cryptokeys == evidence.mval.cryptokeys diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index 9683735..40e90c1 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -181,7 +181,7 @@ fn platform_match_defers_for_non_cca_mkey() { fn platform_match_rejects_invalid_cca_structures() { let profile = CcaPlatformProfile::new(); let reference = measurement_with_mkey("cca.rotpk.CM.2.3", &[0x11, 0x22, 0x33]); - let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + let evidence = raw_value_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), @@ -189,6 +189,18 @@ fn platform_match_rejects_invalid_cca_structures() { ); } +#[test] +fn platform_match_accepts_rotpk_reference_against_raw_value_evidence() { + let profile = CcaPlatformProfile::new(); + let reference = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + let evidence = raw_value_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + + assert_eq!( + profile.match_measurement(&reference, &evidence, &MatchContext::new()), + Some(true) + ); +} + #[test] fn platform_match_rejects_different_software_component_signer_id() { let profile = CcaPlatformProfile::new(); @@ -207,7 +219,7 @@ fn platform_match_rejects_different_software_component_signer_id() { fn platform_match_rejects_different_rotpk_key() { let profile = CcaPlatformProfile::new(); let reference = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); - let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xBB; 32]); + let evidence = raw_value_measurement("cca.rotpk.CM.2.3", &[0xBB; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), @@ -355,7 +367,7 @@ fn platform_profile_accepts_standalone_rotpk_triple() { )]; let evidence = vec![EvidenceClaim { environment: platform_environment(), - measurements: vec![rotpk], + measurements: vec![raw_value_measurement("cca.rotpk.CM.2.3", &[0xAA; 32])], }]; let claims = match_reference_values_with_profile( @@ -379,7 +391,10 @@ fn platform_profile_accepts_rotpk_triple_for_one_array_entry() { )]; let evidence = vec![EvidenceClaim { environment: platform_environment(), - measurements: vec![first, second], + measurements: vec![ + raw_value_measurement("cca.rotpk.CM.2.0", &[0xAA; 32]), + raw_value_measurement("cca.rotpk.CM.2.1", &[0xBB; 32]), + ], }]; let claims = match_reference_values_with_profile( @@ -562,7 +577,7 @@ fn platform_match_rejects_rotpk_extra_mval_field() { let profile = CcaPlatformProfile::new(); let mut reference = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); reference.mval.raw_value = Some(RawValueChoice::Bytes(vec![0xCC; 32])); - let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + let evidence = raw_value_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), @@ -595,7 +610,7 @@ fn platform_match_rejects_authorized_by() { let profile = CcaPlatformProfile::new(); let mut reference = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); reference.authorized_by = Some(vec![CryptoKey::Bytes(vec![0xCC; 32])]); - let evidence = rotpk_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); + let evidence = raw_value_measurement("cca.rotpk.CM.2.3", &[0xAA; 32]); assert_eq!( profile.match_measurement(&reference, &evidence, &MatchContext::new()), From 89efdf1e0c018cffc1eabfb67a0317952277acc4 Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 22:23:37 +0000 Subject: [PATCH 14/18] Reject mixed ROTPK reference triples --- corim/src/profile/cca/mod.rs | 1 + corim/tests/profile_cca_tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 924e430..a4ba5fa 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -549,6 +549,7 @@ impl Profile for CcaPlatformProfile { return software_component_count == 0 && platform_config_count == 0 && manufacturing_config_count == 0 + && rotpk_count == triple.measurements().len() && valid_rotpk_group(triple.measurements()); } diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index 40e90c1..c4cedde 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -436,6 +436,29 @@ fn platform_profile_rejects_rotpk_mixed_array_entries() { } } +#[test] +fn platform_profile_rejects_rotpk_mixed_with_unknown_measurement() { + let profile = CcaPlatformProfile::new(); + let rotpk = rotpk_measurement("cca.rotpk.CM.2.0", &[0xAA; 32]); + let triples = vec![ReferenceTriple::new( + platform_environment(), + vec![rotpk, measurement_with_mkey("tee.something", &[0x11; 32])], + )]; + let evidence = vec![EvidenceClaim { + environment: platform_environment(), + measurements: vec![raw_value_measurement("cca.rotpk.CM.2.0", &[0xAA; 32])], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + #[test] fn platform_profile_rejects_rotpk_mixed_with_platform_measurements() { let profile = CcaPlatformProfile::new(); From 4d02e0501de99bc4f70188d94a6161465859e81b Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 22:38:14 +0000 Subject: [PATCH 15/18] Validate CCA evidence environments --- corim/src/profile.rs | 12 ++++ corim/src/profile/cca/mod.rs | 18 ++++++ corim/src/validate.rs | 11 +++- corim/tests/profile_cca_tests.rs | 101 +++++++++++++++++++++++++++++-- 4 files changed, 135 insertions(+), 7 deletions(-) diff --git a/corim/src/profile.rs b/corim/src/profile.rs index 58ec1f4..5f0d83d 100644 --- a/corim/src/profile.rs +++ b/corim/src/profile.rs @@ -319,6 +319,18 @@ pub trait Profile { true } + /// Validate profile-specific constraints over one evidence claim before + /// it is matched against any reference triple. + /// + /// Use this when evidence produced for a profile must satisfy identity or + /// shape requirements that are stricter than the generic CoRIM environment + /// matching rules. Return `false` to make the evidence claim ineligible for + /// profile-aware matching. Profiles without evidence-level requirements can + /// use the default implementation. + fn evidence_claim_valid(&self, _claim: &crate::validate::EvidenceClaim) -> bool { + true + } + /// Render an `extra_entries` key/value pair for `--diagnose` output. /// /// Called by the diagnose walker when it encounters a profile-defined diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index a4ba5fa..f0f731f 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -38,6 +38,7 @@ use crate::types::measurement::{ Digest, DigestAlg, MeasurementMap, MeasurementValuesMap, RawValueChoice, }; use crate::types::triples::ReferenceTriple; +use crate::validate::EvidenceClaim; /// Profile URI for CCA Platform endorsements. pub const CCA_PLATFORM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_platform#1.0.0"; @@ -444,6 +445,15 @@ fn is_valid_cca_platform_environment(environment: &EnvironmentMap) -> bool { } } +fn is_valid_cca_platform_evidence_environment(environment: &EnvironmentMap) -> bool { + is_valid_cca_platform_environment(environment) + && matches!( + &environment.instance, + Some(InstanceIdChoice::Ueid(ueid)) + if ueid.len() == CCA_INSTANCE_ID_SIZE && ueid[0] == CCA_INSTANCE_ID_RAND_TYPE + ) +} + /// The subject of a CCA Realm triple is the RIM itself, encoded as /// `#6.560(cca-hash-type)` in `environment.class.class-id` /// (draft-ydb-rats-cca-endorsements-04 §3.2.2). The same value is also @@ -562,6 +572,10 @@ impl Profile for CcaPlatformProfile { && manufacturing_config_count <= 1 } + fn evidence_claim_valid(&self, claim: &EvidenceClaim) -> bool { + is_valid_cca_platform_evidence_environment(&claim.environment) + } + fn match_measurement( &self, reference: &MeasurementMap, @@ -622,6 +636,10 @@ impl Profile for CcaRealmProfile { has_rim && !has_duplicate_mkeys(triple.measurements(), is_cca_realm_mkey) } + fn evidence_claim_valid(&self, claim: &EvidenceClaim) -> bool { + is_valid_cca_realm_environment(&claim.environment) + } + fn match_measurement( &self, reference: &MeasurementMap, diff --git a/corim/src/validate.rs b/corim/src/validate.rs index 3fb5502..9f451ba 100644 --- a/corim/src/validate.rs +++ b/corim/src/validate.rs @@ -318,8 +318,11 @@ pub struct EvidenceClaim { /// Before any per-pair matching, the profile's /// [`Profile::reference_triple_valid`] hook is called once for each /// reference triple. A `false` result skips that whole triple. The default -/// hook returns `true`, so profiles with no triple-level rules behave as -/// if only per-pair matching were customized. Pass `None` for `profile` to +/// hook returns `true`. For each candidate evidence claim, the profile's +/// [`Profile::evidence_claim_valid`] hook is also called before generic +/// environment matching; `false` skips that evidence claim. The default hook +/// returns `true`, so profiles with no triple- or evidence-level rules behave +/// as if only per-pair matching were customized. Pass `None` for `profile` to /// get behavior identical to [`match_reference_values`]. /// /// Profile lookup is the caller's responsibility: @@ -349,6 +352,10 @@ pub fn match_reference_values_with_profile( } for ev in evidence { + if profile.is_some_and(|p| !p.evidence_claim_valid(ev)) { + continue; + } + if !environment_matches(triple.environment(), &ev.environment) { continue; } diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index c4cedde..0e9cc0d 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -91,6 +91,16 @@ fn platform_environment() -> EnvironmentMap { environment_with_class_id(&[0x5A; 32]) } +fn platform_evidence_environment() -> EnvironmentMap { + let mut environment = platform_environment(); + environment.instance = Some(InstanceIdChoice::Ueid({ + let mut ueid = vec![0x42; 33]; + ueid[0] = 0x01; + ueid + })); + environment +} + /// Realm triples carry the RIM itself as `class-id` /// (draft-ydb-rats-cca-endorsements-04 §3.2.2). fn realm_environment(rim: &[u8]) -> EnvironmentMap { @@ -256,7 +266,7 @@ fn platform_match_rejects_masked_config_evidence() { fn platform_evidence() -> Vec { vec![EvidenceClaim { - environment: platform_environment(), + environment: platform_evidence_environment(), measurements: vec![ software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), raw_value_measurement("cca.platform-config", &[0xAF, 0xFF]), @@ -366,7 +376,7 @@ fn platform_profile_accepts_standalone_rotpk_triple() { vec![rotpk.clone()], )]; let evidence = vec![EvidenceClaim { - environment: platform_environment(), + environment: platform_evidence_environment(), measurements: vec![raw_value_measurement("cca.rotpk.CM.2.3", &[0xAA; 32])], }]; @@ -390,7 +400,7 @@ fn platform_profile_accepts_rotpk_triple_for_one_array_entry() { vec![first.clone(), second.clone()], )]; let evidence = vec![EvidenceClaim { - environment: platform_environment(), + environment: platform_evidence_environment(), measurements: vec![ raw_value_measurement("cca.rotpk.CM.2.0", &[0xAA; 32]), raw_value_measurement("cca.rotpk.CM.2.1", &[0xBB; 32]), @@ -421,7 +431,7 @@ fn platform_profile_rejects_rotpk_mixed_array_entries() { measurements.clone(), )]; let evidence = vec![EvidenceClaim { - environment: platform_environment(), + environment: platform_evidence_environment(), measurements, }]; @@ -445,7 +455,7 @@ fn platform_profile_rejects_rotpk_mixed_with_unknown_measurement() { vec![rotpk, measurement_with_mkey("tee.something", &[0x11; 32])], )]; let evidence = vec![EvidenceClaim { - environment: platform_environment(), + environment: platform_evidence_environment(), measurements: vec![raw_value_measurement("cca.rotpk.CM.2.0", &[0xAA; 32])], }]; @@ -528,6 +538,58 @@ fn platform_profile_rejects_triple_with_non_ueid_instance() { assert!(claims.is_empty()); } +#[test] +fn platform_profile_rejects_evidence_without_instance() { + let profile = CcaPlatformProfile::new(); + let triples = vec![ReferenceTriple::new( + platform_environment(), + vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]), + ], + )]; + let evidence = vec![EvidenceClaim { + environment: platform_environment(), + measurements: platform_evidence().remove(0).measurements, + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + +#[test] +fn platform_profile_rejects_evidence_with_non_ueid_instance() { + let profile = CcaPlatformProfile::new(); + let triples = vec![ReferenceTriple::new( + platform_environment(), + vec![ + software_component_measurement("cca.software-component", &[0x11; 32], &[0xAA; 32]), + masked_raw_value_measurement("cca.platform-config", &[0xA0, 0x05], &[0xF0, 0x00]), + ], + )]; + let mut evidence_environment = platform_environment(); + evidence_environment.instance = Some(InstanceIdChoice::Bytes(vec![0x01; 33])); + let evidence = vec![EvidenceClaim { + environment: evidence_environment, + measurements: platform_evidence().remove(0).measurements, + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + #[test] fn platform_profile_rejects_malformed_reference_measurement_in_triple() { let profile = CcaPlatformProfile::new(); @@ -794,6 +856,35 @@ fn realm_profile_rejects_malformed_rim_in_reference_triple() { ); } +#[test] +fn realm_profile_rejects_evidence_with_instance() { + let profile = CcaRealmProfile::new(); + let rim_value = [0xAA; 32]; + let triples = vec![ReferenceTriple::new( + realm_environment(&rim_value), + vec![measurement_with_mkey("cca.rim", &rim_value)], + )]; + let mut evidence_environment = realm_environment(&rim_value); + evidence_environment.instance = Some(InstanceIdChoice::Ueid({ + let mut ueid = vec![0x42; 33]; + ueid[0] = 0x01; + ueid + })); + let evidence = vec![EvidenceClaim { + environment: evidence_environment, + measurements: vec![measurement_with_mkey("cca.rim", &rim_value)], + }]; + + let claims = match_reference_values_with_profile( + &triples, + &evidence, + Some(&profile), + &MatchContext::new(), + ); + + assert!(claims.is_empty()); +} + #[test] fn realm_match_rejects_masked_rpv() { let profile = CcaRealmProfile::new(); From 349ddba5fa222cd3153300df6d07933a04e2ccb6 Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 22:59:50 +0000 Subject: [PATCH 16/18] Validate CCA profiles in CLI --- corim-cli/src/main.rs | 24 ++++++ corim-cli/tests/validate_json_tests.rs | 103 ++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/corim-cli/src/main.rs b/corim-cli/src/main.rs index 1e13ad1..0166b85 100644 --- a/corim-cli/src/main.rs +++ b/corim-cli/src/main.rs @@ -364,6 +364,10 @@ decoded via compat::decode_comid_from_tcg_bstr", } } + if let Some(profile) = profile_for_render { + validate_profile_reference_triples(profile, &comid_tags, &mut errors); + } + // Baseline conformance mode: compare the (valid) input against a // known-good baseline and exit on the conformance result. if let Some(baseline_path) = &cli.baseline { @@ -422,6 +426,26 @@ decoded via compat::decode_comid_from_tcg_bstr", } } +fn validate_profile_reference_triples( + profile: &(dyn corim::profile::Profile + Send + Sync), + comids: &[corim::types::comid::ComidTag], + errors: &mut Vec, +) { + for (comid_idx, comid) in comids.iter().enumerate() { + let Some(reference_triples) = &comid.triples.reference_triples else { + continue; + }; + + for (triple_idx, triple) in reference_triples.iter().enumerate() { + if !profile.reference_triple_valid(triple) { + errors.push(format!( + "comids[{comid_idx}].reference-triples[{triple_idx}]: failed profile-specific validation" + )); + } + } + } +} + /// Information extracted from a signed CoRIM's COSE_Sign1 wrapper. /// /// Mirrors the four elements of the RFC 9052 §4 `COSE_Sign1` array: diff --git a/corim-cli/tests/validate_json_tests.rs b/corim-cli/tests/validate_json_tests.rs index a119fc6..bdf7207 100644 --- a/corim-cli/tests/validate_json_tests.rs +++ b/corim-cli/tests/validate_json_tests.rs @@ -7,10 +7,12 @@ use std::process::Command; use corim::builder::{ComidBuilder, CorimBuilder}; -use corim::types::common::{MeasuredElement, TagIdChoice}; +use corim::types::common::{ClassIdChoice, CryptoKey, MeasuredElement, TagIdChoice}; use corim::types::corim::{CorimId, CorimMetaMap, CorimSignerMap}; use corim::types::environment::{ClassMap, EnvironmentMap}; -use corim::types::measurement::{MeasurementMap, MeasurementValuesMap, SvnChoice}; +use corim::types::measurement::{ + Digest, MeasurementMap, MeasurementValuesMap, RawValueChoice, SvnChoice, +}; use corim::types::signed::{CwtClaims, SignedCorimBuilder}; use corim::types::triples::ReferenceTriple; @@ -97,6 +99,76 @@ fn validate_json(bytes: &[u8], ext: &str) -> serde_json::Value { .unwrap_or_else(|e| panic!("output is not valid JSON: {e}\n{stdout}")) } +fn validate_json_status(bytes: &[u8], ext: &str) -> (std::process::ExitStatus, serde_json::Value) { + let path = unique_temp("validate_json_status", ext); + std::fs::write(&path, bytes).unwrap(); + let out = Command::new(bin()) + .args(["validate", "-f", "json", path.to_str().unwrap()]) + .output() + .expect("run validate"); + let _ = std::fs::remove_file(&path); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + let parsed = serde_json::from_str(&stdout) + .unwrap_or_else(|e| panic!("output is not valid JSON: {e}\n{stdout}")); + (out.status, parsed) +} + +fn cca_platform_environment() -> EnvironmentMap { + EnvironmentMap { + class: Some(ClassMap { + class_id: Some(ClassIdChoice::Bytes(vec![0x5A; 32])), + ..ClassMap::default() + }), + instance: None, + group: None, + } +} + +fn cca_software_component() -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text("cca.software-component".into())), + mval: MeasurementValuesMap { + digests: Some(vec![Digest::new_text("sha-256", vec![0x11; 32])]), + cryptokeys: Some(vec![CryptoKey::Bytes(vec![0xAA; 32])]), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + +fn cca_platform_config() -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text("cca.platform-config".into())), + mval: MeasurementValuesMap { + raw_value: Some(RawValueChoice::Masked { + value: vec![0xA0, 0x05], + mask: vec![0xF0, 0x00], + }), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + +fn cca_platform_corim(measurements: Vec) -> Vec { + let comid = ComidBuilder::new(TagIdChoice::Text("cca-platform-comid".into())) + .add_reference_triple(ReferenceTriple::new( + cca_platform_environment(), + measurements, + )) + .build() + .unwrap(); + + CorimBuilder::new(CorimId::Text("cca-platform-corim".into())) + .set_profile(corim::types::corim::ProfileChoice::Uri( + "tag:arm.com,2025:endorsements/cca_platform#1.0.0".into(), + )) + .add_comid_tag(comid) + .unwrap() + .build_bytes() + .unwrap() +} + #[test] fn signed_corim_json_includes_protected_header_fields() { let signed = make_signed(&sample_unsigned_corim(), false); @@ -151,6 +223,33 @@ fn unsigned_corim_json_has_no_signed_object() { assert_eq!(v["id"], "json-corim"); } +#[test] +fn validate_accepts_cca_platform_profile_reference_triples() { + let bytes = cca_platform_corim(vec![cca_software_component(), cca_platform_config()]); + let v = validate_json(&bytes, "cbor"); + + assert_eq!(v["valid"], true); + assert_eq!( + v["profile"], + "tag:arm.com,2025:endorsements/cca_platform#1.0.0" + ); +} + +#[test] +fn validate_rejects_invalid_cca_platform_profile_reference_triples() { + let bytes = cca_platform_corim(vec![cca_software_component()]); + let (status, v) = validate_json_status(&bytes, "cbor"); + + assert!(!status.success(), "validate unexpectedly succeeded: {v}"); + assert_eq!(v["valid"], false); + assert!( + v["errors"].as_array().unwrap().iter().any(|error| error + .as_str() + .is_some_and(|s| s.contains("failed profile-specific validation"))), + "expected profile-specific validation error, got: {v}" + ); +} + /// Producer-controlled strings reach the report verbatim, so control /// characters must be escaped rather than emitted raw (which would make the /// output unparseable). From dfdda598fd812f533a88235996528484ab8bbbfd Mon Sep 17 00:00:00 2001 From: Anna Trikalinou Date: Thu, 17 Sep 2026 23:11:52 +0000 Subject: [PATCH 17/18] Address profile validation review --- corim-cli/src/main.rs | 3 +- corim-cli/tests/validate_json_tests.rs | 3 +- corim/src/profile/cca/mod.rs | 56 +++++++++++++++---------- corim/src/validate.rs | 8 ++-- corim/tests/profile_cca_tests.rs | 18 ++++++++ corim/tests/profile_validation_tests.rs | 43 +++++++++++++++++++ 6 files changed, 103 insertions(+), 28 deletions(-) diff --git a/corim-cli/src/main.rs b/corim-cli/src/main.rs index 0166b85..90151d4 100644 --- a/corim-cli/src/main.rs +++ b/corim-cli/src/main.rs @@ -431,6 +431,7 @@ fn validate_profile_reference_triples( comids: &[corim::types::comid::ComidTag], errors: &mut Vec, ) { + let profile_name = display::profile_str(profile.identifier()); for (comid_idx, comid) in comids.iter().enumerate() { let Some(reference_triples) = &comid.triples.reference_triples else { continue; @@ -439,7 +440,7 @@ fn validate_profile_reference_triples( for (triple_idx, triple) in reference_triples.iter().enumerate() { if !profile.reference_triple_valid(triple) { errors.push(format!( - "comids[{comid_idx}].reference-triples[{triple_idx}]: failed profile-specific validation" + "comids[{comid_idx}].reference-triples[{triple_idx}]: failed profile-specific validation for {profile_name}" )); } } diff --git a/corim-cli/tests/validate_json_tests.rs b/corim-cli/tests/validate_json_tests.rs index bdf7207..a2288fc 100644 --- a/corim-cli/tests/validate_json_tests.rs +++ b/corim-cli/tests/validate_json_tests.rs @@ -245,7 +245,8 @@ fn validate_rejects_invalid_cca_platform_profile_reference_triples() { assert!( v["errors"].as_array().unwrap().iter().any(|error| error .as_str() - .is_some_and(|s| s.contains("failed profile-specific validation"))), + .is_some_and(|s| s.contains("failed profile-specific validation") + && s.contains("tag:arm.com,2025:endorsements/cca_platform#1.0.0"))), "expected profile-specific validation error, got: {v}" ); } diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index f0f731f..427a105 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -40,30 +40,42 @@ use crate::types::measurement::{ use crate::types::triples::ReferenceTriple; use crate::validate::EvidenceClaim; -/// Profile URI for CCA Platform endorsements. +/// Profile URI for CCA Platform endorsements +/// (draft-ydb-rats-cca-endorsements-04 §3.1.1). pub const CCA_PLATFORM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_platform#1.0.0"; -/// Profile URI for CCA Realm endorsements. +/// Profile URI for CCA Realm endorsements +/// (draft-ydb-rats-cca-endorsements-04 §3.2.1). pub const CCA_REALM_PROFILE_URI: &str = "tag:arm.com,2025:endorsements/cca_realm#1.0.0"; -/// CCA Platform software-component measurement key. +/// CCA Platform software-component measurement key +/// (draft-ydb-rats-cca-endorsements-04 §3.1.3.1). pub const CCA_MKEY_SOFTWARE_COMPONENT: &str = "cca.software-component"; -/// CCA Platform configuration measurement key. +/// CCA Platform configuration measurement key +/// (draft-ydb-rats-cca-endorsements-04 §3.1.3.2). pub const CCA_MKEY_PLATFORM_CONFIG: &str = "cca.platform-config"; -/// CCA Platform manufacturing configuration measurement key. +/// CCA Platform manufacturing configuration measurement key +/// (draft-ydb-rats-cca-endorsements-04 §3.1.3.4). pub const CCA_MKEY_PLATFORM_MANUFACTURING_CONFIG: &str = "cca.platform-manufacturing-config"; -/// Prefix for CCA Platform ROTPK measurement keys. +/// Prefix for CCA Platform ROTPK measurement keys +/// (draft-ydb-rats-cca-endorsements-04 §3.1.3.3). pub const CCA_MKEY_ROTPK_PREFIX: &str = "cca.rotpk."; -/// CCA Realm initial measurement key. +/// CCA Realm initial measurement key +/// (draft-ydb-rats-cca-endorsements-04 §3.2.3). pub const CCA_MKEY_RIM: &str = "cca.rim"; -/// CCA Realm extended measurement key for bank 0. +/// CCA Realm extended measurement key for bank 0 +/// (draft-ydb-rats-cca-endorsements-04 §3.2.3). pub const CCA_MKEY_REM0: &str = "cca.rem0"; -/// CCA Realm extended measurement key for bank 1. +/// CCA Realm extended measurement key for bank 1 +/// (draft-ydb-rats-cca-endorsements-04 §3.2.3). pub const CCA_MKEY_REM1: &str = "cca.rem1"; -/// CCA Realm extended measurement key for bank 2. +/// CCA Realm extended measurement key for bank 2 +/// (draft-ydb-rats-cca-endorsements-04 §3.2.3). pub const CCA_MKEY_REM2: &str = "cca.rem2"; -/// CCA Realm extended measurement key for bank 3. +/// CCA Realm extended measurement key for bank 3 +/// (draft-ydb-rats-cca-endorsements-04 §3.2.3). pub const CCA_MKEY_REM3: &str = "cca.rem3"; -/// CCA Realm personalization value measurement key. +/// CCA Realm personalization value measurement key +/// (draft-ydb-rats-cca-endorsements-04 §3.2.3). pub const CCA_MKEY_RPV: &str = "cca.rpv"; /// Maximum ROTPK array index from draft-ydb-rats-cca-endorsements-04 §3.1.3.3. @@ -615,21 +627,19 @@ impl Profile for CcaRealmProfile { for measurement in triple.measurements() { let Some(mkey) = mkey_name(&measurement.mkey) else { - continue; + return false; }; - if is_cca_realm_mkey(&mkey) { - if !is_valid_cca_realm_measurement(measurement) { + if !is_cca_realm_mkey(&mkey) || !is_valid_cca_realm_measurement(measurement) { + return false; + } + // §3.2.2: the environment class-id carries the RIM, so the + // mandatory `cca.rim` measurement MUST report the same value. + if mkey == CCA_MKEY_RIM { + if !realm_rim_matches_environment(triple.environment(), measurement) { return false; } - // §3.2.2: the environment class-id carries the RIM, so the - // mandatory `cca.rim` measurement MUST report the same value. - if mkey == CCA_MKEY_RIM { - if !realm_rim_matches_environment(triple.environment(), measurement) { - return false; - } - has_rim = true; - } + has_rim = true; } } diff --git a/corim/src/validate.rs b/corim/src/validate.rs index 9f451ba..9dabf9a 100644 --- a/corim/src/validate.rs +++ b/corim/src/validate.rs @@ -428,9 +428,10 @@ pub fn apply_endorsement_series( } /// Like [`apply_endorsement_series`] but consults a profile's -/// [`Profile::match_measurement`] hook when comparing each series -/// `condition` entry against evidence. Per-pair semantics are identical -/// to those of [`match_reference_values_with_profile`]. +/// [`Profile::evidence_claim_valid`] hook before generic environment +/// matching and [`Profile::match_measurement`] hook when comparing each +/// series `condition` entry against evidence. Per-pair semantics are +/// identical to those of [`match_reference_values_with_profile`]. /// /// Pass `None::<&dyn Profile>` for `profile` to get behavior identical /// to [`apply_endorsement_series`]. @@ -447,6 +448,7 @@ pub fn apply_endorsement_series_with_profile( let matching_evidence: Vec<_> = evidence .iter() + .filter(|ev| profile.is_none_or(|p| p.evidence_claim_valid(ev))) .filter(|ev| environment_matches(&condition.environment, &ev.environment)) .collect(); diff --git a/corim/tests/profile_cca_tests.rs b/corim/tests/profile_cca_tests.rs index 0e9cc0d..203dbf7 100644 --- a/corim/tests/profile_cca_tests.rs +++ b/corim/tests/profile_cca_tests.rs @@ -839,6 +839,24 @@ fn realm_profile_rejects_duplicate_realm_measurements() { ); } +#[test] +fn realm_profile_rejects_unknown_measurement_in_reference_triple() { + let profile = CcaRealmProfile::new(); + let rim_value = [0xAA; 32]; + + assert_eq!( + realm_claims( + &profile, + realm_environment(&rim_value), + vec![ + measurement_with_mkey("cca.rim", &rim_value), + measurement_with_mkey("tee.something", &[0x11; 32]), + ], + ), + 0 + ); +} + #[test] fn realm_profile_rejects_malformed_rim_in_reference_triple() { let profile = CcaRealmProfile::new(); diff --git a/corim/tests/profile_validation_tests.rs b/corim/tests/profile_validation_tests.rs index 3e1d6df..afc58d2 100644 --- a/corim/tests/profile_validation_tests.rs +++ b/corim/tests/profile_validation_tests.rs @@ -74,6 +74,27 @@ impl Profile for AlwaysRejectProfile { } } +/// Profile that accepts every measurement pair but rejects every evidence claim. +struct RejectEvidenceProfile { + id: ProfileChoice, +} +impl Profile for RejectEvidenceProfile { + fn identifier(&self) -> &ProfileChoice { + &self.id + } + fn evidence_claim_valid(&self, _claim: &EvidenceClaim) -> bool { + false + } + fn match_measurement( + &self, + _reference: &MeasurementMap, + _evidence: &MeasurementMap, + _ctx: &MatchContext, + ) -> Option { + Some(true) + } +} + fn test_id() -> ProfileChoice { ProfileChoice::Uri("urn:example:test-profile".into()) } @@ -363,3 +384,25 @@ fn endorsement_series_always_reject_profile_blocks_endorsement() { apply_endorsement_series_with_profile(&triples, &evidence, Some(&profile), &ctx()).unwrap(); assert_eq!(with_profile.len(), 0, "profile should block endorsement"); } + +#[test] +fn endorsement_series_rejects_profile_invalid_evidence_claim() { + let profile = RejectEvidenceProfile { id: test_id() }; + let triples = vec![build_series_triple(0xAA, 0xCC)]; + let evidence = vec![EvidenceClaim { + environment: EnvironmentMap::for_class("ACME", "Widget"), + measurements: vec![MeasurementMap { + mkey: Some(MeasuredElement::Text("k".into())), + mval: MeasurementValuesMap { + digests: Some(vec![Digest::new(7, vec![0xBB; 32])]), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + }], + }]; + + let endorsed = + apply_endorsement_series_with_profile(&triples, &evidence, Some(&profile), &ctx()).unwrap(); + + assert!(endorsed.is_empty()); +} From ccd945f65873c8fb4867e7e2d491ee886b2be83a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:22:24 +0000 Subject: [PATCH 18/18] =?UTF-8?q?Add=20attest-key-triple=20profile=20valid?= =?UTF-8?q?ation=20hook=20and=20wire=20CCA=20=C2=A73.1.4=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: a-trikalinou <139903738+a-trikalinou@users.noreply.github.com> --- corim-cli/src/main.rs | 22 +++++++++ corim-cli/tests/validate_json_tests.rs | 66 ++++++++++++++++++++++++++ corim/src/profile.rs | 17 ++++++- corim/src/profile/cca/mod.rs | 11 ++++- 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/corim-cli/src/main.rs b/corim-cli/src/main.rs index 90151d4..6fc98f8 100644 --- a/corim-cli/src/main.rs +++ b/corim-cli/src/main.rs @@ -366,6 +366,7 @@ decoded via compat::decode_comid_from_tcg_bstr", if let Some(profile) = profile_for_render { validate_profile_reference_triples(profile, &comid_tags, &mut errors); + validate_profile_attest_key_triples(profile, &comid_tags, &mut errors); } // Baseline conformance mode: compare the (valid) input against a @@ -447,6 +448,27 @@ fn validate_profile_reference_triples( } } +fn validate_profile_attest_key_triples( + profile: &(dyn corim::profile::Profile + Send + Sync), + comids: &[corim::types::comid::ComidTag], + errors: &mut Vec, +) { + let profile_name = display::profile_str(profile.identifier()); + for (comid_idx, comid) in comids.iter().enumerate() { + let Some(attest_key_triples) = &comid.triples.attest_key_triples else { + continue; + }; + + for (triple_idx, triple) in attest_key_triples.iter().enumerate() { + if !profile.attest_key_triple_valid(triple) { + errors.push(format!( + "comids[{comid_idx}].attest-key-triples[{triple_idx}]: failed profile-specific validation for {profile_name}" + )); + } + } + } +} + /// Information extracted from a signed CoRIM's COSE_Sign1 wrapper. /// /// Mirrors the four elements of the RFC 9052 §4 `COSE_Sign1` array: diff --git a/corim-cli/tests/validate_json_tests.rs b/corim-cli/tests/validate_json_tests.rs index a2288fc..76a5219 100644 --- a/corim-cli/tests/validate_json_tests.rs +++ b/corim-cli/tests/validate_json_tests.rs @@ -169,6 +169,37 @@ fn cca_platform_corim(measurements: Vec) -> Vec { .unwrap() } +fn cca_platform_instance_environment() -> EnvironmentMap { + EnvironmentMap { + instance: Some(corim::types::common::InstanceIdChoice::Ueid( + [&[0x01u8][..], &[0x5A; 32]].concat(), + )), + ..cca_platform_environment() + } +} + +fn cca_platform_corim_with_attest_key(environment: EnvironmentMap, keys: Vec) -> Vec { + let comid = ComidBuilder::new(TagIdChoice::Text("cca-platform-comid".into())) + .add_reference_triple(ReferenceTriple::new( + cca_platform_environment(), + vec![cca_software_component(), cca_platform_config()], + )) + .add_attest_key_triple(corim::types::triples::AttestKeyTriple::new( + environment, keys, None, + )) + .build() + .unwrap(); + + CorimBuilder::new(CorimId::Text("cca-platform-corim".into())) + .set_profile(corim::types::corim::ProfileChoice::Uri( + "tag:arm.com,2025:endorsements/cca_platform#1.0.0".into(), + )) + .add_comid_tag(comid) + .unwrap() + .build_bytes() + .unwrap() +} + #[test] fn signed_corim_json_includes_protected_header_fields() { let signed = make_signed(&sample_unsigned_corim(), false); @@ -251,6 +282,41 @@ fn validate_rejects_invalid_cca_platform_profile_reference_triples() { ); } +#[test] +fn validate_accepts_cca_platform_profile_attest_key_triple() { + let bytes = cca_platform_corim_with_attest_key( + cca_platform_instance_environment(), + vec![CryptoKey::PkixBase64Key( + "-----BEGIN PUBLIC KEY-----\nMA==\n-----END PUBLIC KEY-----".into(), + )], + ); + let v = validate_json(&bytes, "cbor"); + + assert_eq!(v["valid"], true); +} + +#[test] +fn validate_rejects_invalid_cca_platform_profile_attest_key_triple() { + // §3.1.4 requires exactly one `tagged-pkix-base64-key-type` key; an + // opaque key-identifier bytes value must be rejected. + let bytes = cca_platform_corim_with_attest_key( + cca_platform_instance_environment(), + vec![CryptoKey::Bytes(vec![0xAA; 32])], + ); + let (status, v) = validate_json_status(&bytes, "cbor"); + + assert!(!status.success(), "validate unexpectedly succeeded: {v}"); + assert_eq!(v["valid"], false); + assert!( + v["errors"].as_array().unwrap().iter().any(|error| error + .as_str() + .is_some_and(|s| s.contains("attest-key-triples") + && s.contains("failed profile-specific validation") + && s.contains("tag:arm.com,2025:endorsements/cca_platform#1.0.0"))), + "expected profile-specific validation error, got: {v}" + ); +} + /// Producer-controlled strings reach the report verbatim, so control /// characters must be escaped rather than emitted raw (which would make the /// output unparseable). diff --git a/corim/src/profile.rs b/corim/src/profile.rs index 5f0d83d..79a628e 100644 --- a/corim/src/profile.rs +++ b/corim/src/profile.rs @@ -158,7 +158,7 @@ use crate::cbor::value::Value; use crate::types::common::CborTime; use crate::types::corim::ProfileChoice; use crate::types::measurement::MeasurementMap; -use crate::types::triples::ReferenceTriple; +use crate::types::triples::{AttestKeyTriple, ReferenceTriple}; /// First-party Intel CoRIM profile (`draft-cds-rats-intel-corim-profile`). /// @@ -319,6 +319,21 @@ pub trait Profile { true } + /// Validate profile-specific constraints over an attestation-key + /// triple. + /// + /// Use this when the profile places stricter requirements on + /// `attest-key-triple-record` than the generic + /// [`AttestKeyTriple::valid`][crate::types::triples::AttestKeyTriple] + /// check (a non-empty key list) — for example a profile-defined subject + /// identifier that must be present on the triple's environment, or a + /// constraint on the number or encoding of the verification keys. + /// Return `false` to reject the triple. Profiles without triple-level + /// requirements can use the default implementation. + fn attest_key_triple_valid(&self, _triple: &AttestKeyTriple) -> bool { + true + } + /// Validate profile-specific constraints over one evidence claim before /// it is matched against any reference triple. /// diff --git a/corim/src/profile/cca/mod.rs b/corim/src/profile/cca/mod.rs index 427a105..8dda926 100644 --- a/corim/src/profile/cca/mod.rs +++ b/corim/src/profile/cca/mod.rs @@ -37,7 +37,7 @@ use crate::types::environment::EnvironmentMap; use crate::types::measurement::{ Digest, DigestAlg, MeasurementMap, MeasurementValuesMap, RawValueChoice, }; -use crate::types::triples::ReferenceTriple; +use crate::types::triples::{AttestKeyTriple, ReferenceTriple}; use crate::validate::EvidenceClaim; /// Profile URI for CCA Platform endorsements @@ -588,6 +588,15 @@ impl Profile for CcaPlatformProfile { is_valid_cca_platform_evidence_environment(&claim.environment) } + /// §3.1.4: the IAK verification key endorsement MUST identify both the + /// Implementation and Instance and MUST carry exactly one key, encoded + /// as `tagged-pkix-base64-key-type` (`#6.554`). + fn attest_key_triple_valid(&self, triple: &AttestKeyTriple) -> bool { + is_valid_cca_platform_environment(triple.environment()) + && triple.environment().instance.is_some() + && matches!(triple.keys(), [CryptoKey::PkixBase64Key(_)]) + } + fn match_measurement( &self, reference: &MeasurementMap,