Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion corim-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <N>".
Expand All @@ -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"]
4 changes: 4 additions & 0 deletions corim-cli/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions corim-cli/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
51 changes: 51 additions & 0 deletions corim-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Comment thread
Copilot marked this conversation as resolved.
registry
}

Expand Down Expand Up @@ -360,6 +364,11 @@ 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
// known-good baseline and exit on the conformance result.
if let Some(baseline_path) = &cli.baseline {
Expand Down Expand Up @@ -418,6 +427,48 @@ 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<String>,
) {
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;
};
Comment thread
Copilot marked this conversation as resolved.

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 for {profile_name}"
));
}
}
}
}

fn validate_profile_attest_key_triples(
profile: &(dyn corim::profile::Profile + Send + Sync),
comids: &[corim::types::comid::ComidTag],
errors: &mut Vec<String>,
) {
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:
Expand Down
170 changes: 168 additions & 2 deletions corim-cli/tests/validate_json_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -97,6 +99,107 @@ 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<MeasurementMap>) -> Vec<u8> {
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()
}

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<CryptoKey>) -> Vec<u8> {
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);
Expand Down Expand Up @@ -151,6 +254,69 @@ 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")
&& s.contains("tag:arm.com,2025:endorsements/cca_platform#1.0.0"))),
"expected profile-specific validation error, got: {v}"
);
}

#[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).
Expand Down
4 changes: 4 additions & 0 deletions corim/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions corim/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -157,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::{AttestKeyTriple, ReferenceTriple};

/// First-party Intel CoRIM profile (`draft-cds-rats-intel-corim-profile`).
///
Expand Down Expand Up @@ -185,6 +187,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 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;

// ---------------------------------------------------------------------------
// MatchContext
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -288,6 +302,50 @@ pub trait Profile {
None
}

/// 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 — 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
}

/// 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.
///
/// 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
Expand Down
Loading
Loading