use crate::cbor;
use crate::cbor::value::{Tagged, Value};
use crate::nostd_prelude::*;
use crate::profile::{Profile, ProfileRegistry};
use crate::types::corim::ProfileChoice;
use crate::types::signed::{
CORIM_CONTENT_TYPE, COSE_HEADER_ALG, COSE_HEADER_CONTENT_TYPE, COSE_HEADER_CORIM_META,
COSE_HEADER_CWT_CLAIMS, COSE_HEADER_KID, COSE_HEADER_PAYLOAD_HASH_ALG,
COSE_HEADER_PAYLOAD_LOCATION, COSE_HEADER_PAYLOAD_PREIMAGE_CT, COSE_HEADER_X5BAG,
COSE_HEADER_X5CHAIN, COSE_HEADER_X5T, COSE_HEADER_X5U,
};
use crate::types::tags::{
CLASS_KEY_CLASS_ID, CLASS_KEY_INDEX, CLASS_KEY_LAYER, CLASS_KEY_MODEL, CLASS_KEY_VENDOR,
COMID_KEY_ENTITIES, COMID_KEY_LANGUAGE, COMID_KEY_LINKED_TAGS, COMID_KEY_TAG_IDENTITY,
COMID_KEY_TRIPLES, CORIM_KEY_DEPENDENT_RIMS, CORIM_KEY_ENTITIES, CORIM_KEY_ID,
CORIM_KEY_PROFILE, CORIM_KEY_RIM_VALIDITY, CORIM_KEY_TAGS, ENV_KEY_CLASS, ENV_KEY_GROUP,
ENV_KEY_INSTANCE, MEAS_KEY_AUTHORIZED_BY, MEAS_KEY_MKEY, MEAS_KEY_MVAL, MVAL_KEY_CRYPTOKEYS,
MVAL_KEY_DIGESTS, MVAL_KEY_FLAGS, MVAL_KEY_INTEGRITY_REGISTERS, MVAL_KEY_INT_RANGE,
MVAL_KEY_IP_ADDR, MVAL_KEY_MAC_ADDR, MVAL_KEY_NAME, MVAL_KEY_RAW_VALUE,
MVAL_KEY_RAW_VALUE_MASK_DEPRECATED, MVAL_KEY_SERIAL_NUMBER, MVAL_KEY_SVN, MVAL_KEY_UEID,
MVAL_KEY_UUID, MVAL_KEY_VERSION, TAG_COMID, TAG_CORIM, TAG_COSWID, TAG_COTL, TAG_LEGACY_SIGNED,
TAG_LEGACY_TOP, TAG_OID, TAG_SIGNED_CORIM, TAG_UUID, TRIPLES_KEY_ATTEST_KEY,
TRIPLES_KEY_COND_ENDORSEMENT, TRIPLES_KEY_COND_ENDORSEMENT_SERIES, TRIPLES_KEY_COSWID,
TRIPLES_KEY_DEPENDENCY, TRIPLES_KEY_ENDORSED, TRIPLES_KEY_IDENTITY, TRIPLES_KEY_MEMBERSHIP,
TRIPLES_KEY_REFERENCE,
};
use core::fmt;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Info,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Error => f.write_str("error"),
Severity::Warning => f.write_str("warn "),
Severity::Info => f.write_str("ok "),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DecodeIssue {
pub(crate) severity: Severity,
pub(crate) path: String,
pub(crate) message: String,
pub(crate) hint: Option<&'static str>,
}
impl DecodeIssue {
pub fn severity(&self) -> Severity {
self.severity
}
pub fn path(&self) -> &str {
&self.path
}
pub fn message(&self) -> &str {
&self.message
}
pub fn hint(&self) -> Option<&'static str> {
self.hint
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DecodeReport {
pub(crate) issues: Vec<DecodeIssue>,
pub(crate) envelope: EnvelopeKind,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EnvelopeKind {
#[default]
Unknown,
Signed,
Unsigned,
}
impl DecodeReport {
pub fn issues(&self) -> &[DecodeIssue] {
&self.issues
}
pub fn envelope(&self) -> EnvelopeKind {
self.envelope
}
pub fn error_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == Severity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.issues
.iter()
.filter(|i| i.severity == Severity::Warning)
.count()
}
}
impl fmt::Display for DecodeReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = match self.envelope {
EnvelopeKind::Signed => "signed CoRIM (tag 18)",
EnvelopeKind::Unsigned => "unsigned CoRIM (tag 501)",
EnvelopeKind::Unknown => "unrecognized envelope",
};
writeln!(f, "Diagnose: {}", kind)?;
for issue in &self.issues {
writeln!(f, " [{}] {}", issue.severity, issue.path)?;
writeln!(f, " {}", issue.message)?;
if let Some(hint) = issue.hint {
writeln!(f, " hint: {}", hint)?;
}
}
writeln!(
f,
"Summary: {} error(s), {} warning(s)",
self.error_count(),
self.warning_count()
)?;
Ok(())
}
}
struct Inspector<'a> {
report: DecodeReport,
profiles: &'a ProfileRegistry,
current_profile: Option<&'a dyn Profile>,
}
impl<'a> Inspector<'a> {
fn new(profiles: &'a ProfileRegistry) -> Self {
Self {
report: DecodeReport::default(),
profiles,
current_profile: None,
}
}
fn err(&mut self, path: impl Into<String>, msg: impl Into<String>) {
self.report.issues.push(DecodeIssue {
severity: Severity::Error,
path: path.into(),
message: msg.into(),
hint: None,
});
}
fn err_hint(&mut self, path: impl Into<String>, msg: impl Into<String>, hint: &'static str) {
self.report.issues.push(DecodeIssue {
severity: Severity::Error,
path: path.into(),
message: msg.into(),
hint: Some(hint),
});
}
fn warn(&mut self, path: impl Into<String>, msg: impl Into<String>) {
self.report.issues.push(DecodeIssue {
severity: Severity::Warning,
path: path.into(),
message: msg.into(),
hint: None,
});
}
fn info(&mut self, path: impl Into<String>, msg: impl Into<String>) {
self.report.issues.push(DecodeIssue {
severity: Severity::Info,
path: path.into(),
message: msg.into(),
hint: None,
});
}
}
fn value_kind(v: &Value) -> &'static str {
match v {
Value::Integer(_) => "integer",
Value::Bytes(_) => "bytes",
Value::Text(_) => "text",
Value::Array(_) => "array",
Value::Map(_) => "map",
Value::Tag(_, _) => "tag",
Value::Bool(_) => "bool",
Value::Null => "null",
Value::Float(_) => "float",
}
}
pub fn inspect(bytes: &[u8], profiles: &ProfileRegistry) -> DecodeReport {
let mut ins = Inspector::new(profiles);
if bytes.is_empty() {
ins.err("$", "input is empty");
return ins.report;
}
let top: Value = match cbor::decode::<Value>(bytes) {
Ok(v) => v,
Err(e) => {
ins.err("$", format!("not valid CBOR: {}", e));
return ins.report;
}
};
match top {
Value::Tag(TAG_LEGACY_TOP, inner) | Value::Tag(TAG_LEGACY_SIGNED, inner) => {
let outer_tag = match cbor::decode::<Value>(bytes).ok() {
Some(Value::Tag(t, _)) => t,
_ => 0, };
ins.warn(
"$",
format!(
"found legacy outer tag #6.{} — dropped from IETF draft-10 (PR #337, Jan 2025); \
still emitted by the TCG Endorsement spec and some real-world producers (e.g. NVIDIA). \
The library accepts these on decode; encode always uses draft-10 tags.",
outer_tag
),
);
inspect_top_value(&mut ins, *inner);
}
other => inspect_top_value(&mut ins, other),
}
ins.report
}
fn inspect_top_value(ins: &mut Inspector<'_>, top: Value) {
match top {
Value::Tag(TAG_SIGNED_CORIM, inner) => {
ins.report.envelope = EnvelopeKind::Signed;
ins.info(
"$",
format!("recognized CBOR tag {} (signed-corim)", TAG_SIGNED_CORIM),
);
inspect_cose_sign1(ins, *inner);
}
Value::Tag(TAG_CORIM, inner) => {
ins.report.envelope = EnvelopeKind::Unsigned;
ins.info(
"$",
format!(
"recognized CBOR tag {} (tagged-unsigned-corim-map)",
TAG_CORIM
),
);
inspect_corim_map(ins, "$", *inner);
}
Value::Tag(TAG_LEGACY_TOP, inner) | Value::Tag(TAG_LEGACY_SIGNED, inner) => {
inspect_top_value(ins, *inner);
}
Value::Tag(t, _) => {
ins.err(
"$",
format!(
"expected CBOR tag {} (unsigned-corim) or {} (signed-corim), found tag {}",
TAG_CORIM, TAG_SIGNED_CORIM, t
),
);
}
other => {
ins.err(
"$",
format!(
"expected a CBOR-tagged value (#6.{} or #6.{}), found bare {}",
TAG_CORIM,
TAG_SIGNED_CORIM,
value_kind(&other)
),
);
}
}
}
fn inspect_cose_sign1(ins: &mut Inspector<'_>, v: Value) {
let arr = match v {
Value::Array(a) => a,
other => {
ins.err_hint(
"$",
format!(
"COSE_Sign1 must be a 4-element array, found {}",
value_kind(&other)
),
"RFC 9052 §4: COSE_Sign1 = [protected, unprotected, payload, signature]",
);
return;
}
};
if arr.len() != 4 {
ins.err_hint(
"$",
format!("COSE_Sign1 array has {} element(s), expected 4", arr.len()),
"RFC 9052 §4: COSE_Sign1 = [protected, unprotected, payload, signature]",
);
}
let mut it = arr.into_iter();
if let Some(protected) = it.next() {
inspect_cose_protected(ins, protected);
}
if let Some(unprotected) = it.next() {
inspect_cose_unprotected(ins, unprotected);
}
if let Some(payload) = it.next() {
inspect_cose_payload(ins, payload);
}
if let Some(signature) = it.next() {
inspect_cose_signature(ins, signature);
}
}
fn inspect_cose_protected(ins: &mut Inspector<'_>, v: Value) {
let bytes = match v {
Value::Bytes(b) => b,
other => {
ins.err_hint(
"$.protected",
format!(
"protected header must be a byte string (bstr .cbor protected-corim-header-map), found {}",
value_kind(&other)
),
"RFC 9052 §4: protected MUST be `bstr .cbor header_map`",
);
return;
}
};
if bytes.is_empty() {
ins.err(
"$.protected",
"protected header byte string is empty (must encode a non-empty CBOR map)",
);
return;
}
let inner: Value = match cbor::decode::<Value>(&bytes) {
Ok(v) => v,
Err(e) => {
ins.err(
"$.protected",
format!("inner CBOR of protected header is not valid: {}", e),
);
return;
}
};
inspect_protected_header_map(ins, inner);
}
fn inspect_cose_unprotected(ins: &mut Inspector<'_>, v: Value) {
match v {
Value::Map(_) => {
ins.info(
"$.unprotected",
"unprotected header is a CBOR map (contents not inspected)",
);
}
other => ins.err(
"$.unprotected",
format!(
"unprotected header must be a CBOR map, found {}",
value_kind(&other)
),
),
}
}
fn inspect_cose_payload(ins: &mut Inspector<'_>, v: Value) {
match v {
Value::Null => {
ins.info(
"$.payload",
"payload is nil (detached or hash-envelope mode)",
);
}
Value::Bytes(b) => {
if b.is_empty() {
ins.warn("$.payload", "payload byte string is empty");
return;
}
match cbor::decode::<Value>(&b) {
Ok(Value::Tag(TAG_CORIM, inner)) => {
ins.info(
"$.payload",
format!(
"payload decodes as #6.{}(unsigned-corim-map) ({} bytes)",
TAG_CORIM,
b.len()
),
);
inspect_corim_map(ins, "$.payload", *inner);
}
Ok(Value::Tag(t, _)) => {
ins.err_hint(
"$.payload",
format!(
"payload CBOR is tagged #6.{}; expected #6.{} (tagged-unsigned-corim-map)",
t, TAG_CORIM
),
"If this is a hash-envelope, payload should be raw digest bytes (no CBOR wrapping)",
);
}
Ok(Value::Map(m)) => {
ins.warn(
"$.payload",
format!(
"payload is a bare CBOR map (not #6.{}-tagged) — TCG-style untagged corim-map. \
The library accepts this on decode; encoders always emit the tag.",
TAG_CORIM
),
);
inspect_corim_map(ins, "$.payload", Value::Map(m));
}
Ok(other) => {
ins.warn(
"$.payload",
format!(
"payload bytes parse as bare CBOR {} (not tagged); could be a hash-envelope digest or malformed payload",
value_kind(&other)
),
);
}
Err(_) => {
ins.info(
"$.payload",
format!(
"payload is {} bytes that do not parse as CBOR; treated as hash-envelope digest",
b.len()
),
);
}
}
}
other => ins.err(
"$.payload",
format!(
"payload must be a byte string or nil, found {}",
value_kind(&other)
),
),
}
}
fn inspect_cose_signature(ins: &mut Inspector<'_>, v: Value) {
match v {
Value::Bytes(b) => {
if b.is_empty() {
ins.err("$.signature", "signature byte string is empty");
} else {
ins.info("$.signature", format!("signature is {} bytes", b.len()));
}
}
other => ins.err(
"$.signature",
format!(
"signature must be a byte string, found {}",
value_kind(&other)
),
),
}
}
fn inspect_protected_header_map(ins: &mut Inspector<'_>, v: Value) {
let map = match v {
Value::Map(m) => m,
other => {
ins.err(
"$.protected",
format!(
"protected header inner CBOR must be a map, found {}",
value_kind(&other)
),
);
return;
}
};
let mut have_alg = false;
let mut have_corim_meta = false;
let mut have_cwt_claims = false;
let mut have_content_type = false;
let mut have_payload_preimage_ct = false;
let mut have_cwt_iss_flat = false;
for (k, val) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(k) => k,
Err(_) => {
ins.warn(
"$.protected",
format!("integer header key {} is out of i64 range; skipped", n),
);
continue;
}
},
other => {
ins.warn(
"$.protected",
format!(
"non-integer header key ({}); RFC 9052 expects int/tstr labels",
value_kind(other)
),
);
continue;
}
};
let path = format!("$.protected.{}", key);
match key {
COSE_HEADER_ALG => {
have_alg = true;
if !matches!(val, Value::Integer(_)) {
ins.err(
path,
format!(
"alg (key 1) must be int (COSE algorithm registry), found {}",
value_kind(&val)
),
);
}
}
COSE_HEADER_CONTENT_TYPE => match val {
Value::Text(t) => {
have_content_type = true;
if t != CORIM_CONTENT_TYPE {
ins.warn(
path,
format!(
"content-type (key 3) is {:?}, expected {:?}",
t, CORIM_CONTENT_TYPE
),
);
}
}
Value::Integer(_) => {
have_content_type = true;
ins.warn(
path,
"content-type (key 3) is an integer (CoAP content-format); CoRIM §4.2.1 requires the tstr form",
);
}
_ => ins.err(
path,
format!(
"content-type (key 3) must be tstr, found {}",
value_kind(&val)
),
),
},
COSE_HEADER_KID => match val {
Value::Bytes(_) => {
ins.warn(
path,
"kid (key 4) appears in the protected header; RFC 9052 §3.1 puts kid in unprotected",
);
}
Value::Text(_) => {
have_cwt_iss_flat = true;
ins.info(
path,
"key 4 is a tstr — interpreted as CWT iss claim placed flat in protected header",
);
}
_ => ins.err(
path,
format!(
"key 4 must be bstr (kid) or tstr (CWT iss), found {}",
value_kind(&val)
),
),
},
COSE_HEADER_CORIM_META => match val {
Value::Bytes(b) => {
have_corim_meta = true;
match cbor::decode::<Value>(&b) {
Ok(Value::Map(_)) => ins.info(
path,
format!(
"corim-meta (key 8) decoded as bstr .cbor map ({} bytes)",
b.len()
),
),
Ok(other) => ins.err(
path,
format!(
"corim-meta (key 8) byte-string contents are CBOR {}, expected map",
value_kind(&other)
),
),
Err(e) => ins.err(
path,
format!("corim-meta (key 8) inner CBOR did not parse: {}", e),
),
}
}
Value::Map(_) => {
ins.err_hint(
path,
"corim-meta (key 8) is a bare CBOR map, but the spec requires `bstr .cbor corim-meta-map` (a byte string wrapping the CBOR-encoded map)",
"Producer should encode the corim-meta-map to bytes, then store those bytes as a CBOR byte string at key 8",
);
}
_ => ins.err(
path,
format!(
"corim-meta (key 8) must be a byte string wrapping a CBOR map, found {}",
value_kind(&val)
),
),
},
COSE_HEADER_CWT_CLAIMS => match val {
Value::Map(_) => {
have_cwt_claims = true;
ins.info(path, "CWT-Claims (key 15) is a CBOR map");
}
_ => ins.err(
path,
format!(
"CWT-Claims (key 15) must be a map, found {}",
value_kind(&val)
),
),
},
COSE_HEADER_PAYLOAD_HASH_ALG => {
if !matches!(val, Value::Integer(_)) {
ins.err(
path,
format!(
"payload_hash_alg (key 258) must be int, found {}",
value_kind(&val)
),
);
}
}
COSE_HEADER_PAYLOAD_PREIMAGE_CT => match val {
Value::Text(_) => have_payload_preimage_ct = true,
_ => ins.err(
path,
format!(
"payload_preimage_content_type (key 259) must be tstr, found {}",
value_kind(&val)
),
),
},
COSE_HEADER_PAYLOAD_LOCATION => {
if !matches!(val, Value::Text(_)) {
ins.err(
path,
format!(
"payload_location (key 260) must be tstr, found {}",
value_kind(&val)
),
);
}
}
COSE_HEADER_X5BAG | COSE_HEADER_X5CHAIN => {
if !matches!(val, Value::Bytes(_) | Value::Array(_)) {
ins.err(
path,
format!(
"x5bag/x5chain (key {}) must be bstr or array of bstr per RFC 9360, found {}",
key,
value_kind(&val)
),
);
}
}
COSE_HEADER_X5T => {
if !matches!(val, Value::Array(_)) {
ins.err(
path,
format!(
"x5t (key 34) must be a [hashAlg, hashValue] array, found {}",
value_kind(&val)
),
);
}
}
COSE_HEADER_X5U => {
if !matches!(val, Value::Text(_)) {
ins.err(
path,
format!(
"x5u (key 35) must be tstr (URI), found {}",
value_kind(&val)
),
);
}
}
_ => {
ins.info(
path,
format!("unrecognized header key {} (passed through as extra)", key),
);
}
}
}
if !have_alg {
ins.err("$.protected", "missing required key 1 (alg)");
}
let inline_mode = have_content_type;
let hash_envelope_mode = have_payload_preimage_ct;
if !inline_mode && !hash_envelope_mode {
ins.err_hint(
"$.protected",
"missing both content-type (key 3, inline mode) and payload_preimage_content_type (key 259, hash-envelope mode)",
"draft-ietf-rats-corim-10 §4.2.1 requires exactly one of these",
);
} else if inline_mode && hash_envelope_mode {
ins.warn(
"$.protected",
"both content-type (key 3) and payload_preimage_content_type (key 259) are present; pick one mode",
);
}
if !have_corim_meta && !have_cwt_claims && !have_cwt_iss_flat {
ins.err_hint(
"$.protected",
"meta-group violation: at least one of corim-meta (key 8) or CWT-Claims (key 15) must be present",
"draft-ietf-rats-corim-10 §4.2.1 meta-group: ((corim-meta-identity, ?cwt-claims-identity) // cwt-claims-identity)",
);
}
}
fn inspect_corim_map(ins: &mut Inspector<'_>, base_path: &str, v: Value) {
let map = match v {
Value::Map(m) => m,
other => {
ins.err(
base_path,
format!(
"tagged-unsigned-corim-map inner value must be a map, found {}",
value_kind(&other)
),
);
return;
}
};
let mut have_id = false;
let mut have_tags = false;
let mut tags_value: Option<Value> = None;
for (k, val) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(v) => v,
Err(_) => {
ins.warn(
base_path,
format!("corim-map key {} out of i64 range; skipped", n),
);
continue;
}
},
other => {
ins.warn(
base_path,
format!(
"corim-map has non-integer key ({}); ignored",
value_kind(other)
),
);
continue;
}
};
let path = format!("{}.{}", base_path, key);
match key {
CORIM_KEY_ID => {
have_id = true;
match val {
Value::Text(_) => {}
Value::Bytes(b) if b.len() == 16 => {} Value::Tag(TAG_UUID, inner) => match *inner {
Value::Bytes(b) if b.len() == 16 => {}
other => ins.err(
path,
format!(
"id (key 0) tagged-uuid inner must be 16-byte bstr, found {} of len {}",
value_kind(&other),
if let Value::Bytes(ref b) = other { b.len() } else { 0 }
),
),
},
other => ins.err(
path,
format!(
"id (key 0) must be tstr or (tagged-)uuid-type, found {}",
value_kind(&other)
),
),
}
}
CORIM_KEY_TAGS => {
have_tags = true;
tags_value = Some(val);
}
CORIM_KEY_DEPENDENT_RIMS => {
if !matches!(val, Value::Array(_)) {
ins.err(
path,
format!(
"dependent-rims (key 2) must be array of corim-locator-map, found {}",
value_kind(&val)
),
);
}
}
CORIM_KEY_PROFILE => match val {
Value::Text(ref s) => {
let id = ProfileChoice::Uri(s.clone());
if let Some(p) = ins.profiles.get(&id) {
ins.current_profile = Some(p);
ins.info(
path.clone(),
format!("profile (key 3) URI matched registered profile: {}", s),
);
}
}
Value::Tag(TAG_OID, ref inner) => {
if let Value::Bytes(ref b) = **inner {
let id = ProfileChoice::Oid(b.clone());
if let Some(p) = ins.profiles.get(&id) {
ins.current_profile = Some(p);
ins.info(
path.clone(),
format!(
"profile (key 3) OID matched registered profile ({} bytes)",
b.len()
),
);
}
} else {
ins.err(
path.clone(),
format!(
"profile (key 3) tagged-oid inner must be bstr, found {}",
value_kind(inner)
),
);
}
}
_ => ins.err(
path,
format!(
"profile (key 3) must be uri (tstr) or tagged-oid-type, found {}",
value_kind(&val)
),
),
},
CORIM_KEY_RIM_VALIDITY => {
if !matches!(val, Value::Map(_)) {
ins.err(
path,
format!(
"rim-validity (key 4) must be a validity-map, found {}",
value_kind(&val)
),
);
}
}
CORIM_KEY_ENTITIES => {
if !matches!(val, Value::Array(_)) {
ins.err(
path,
format!(
"entities (key 5) must be array of corim-entity-map, found {}",
value_kind(&val)
),
);
}
}
_ => {
ins.info(
path,
format!("unrecognized corim-map key {} (extension)", key),
);
}
}
}
if !have_id {
ins.err(base_path, "missing required key 0 (id)");
}
if !have_tags {
ins.err(base_path, "missing required key 1 (tags)");
}
if let Some(v) = tags_value {
inspect_tags_array(ins, &format!("{}.1", base_path), v);
}
}
fn inspect_tags_array(ins: &mut Inspector<'_>, base_path: &str, v: Value) {
let arr = match v {
Value::Array(a) => a,
other => {
ins.err(
base_path,
format!(
"tags (key 1) must be a non-empty array, found {}",
value_kind(&other)
),
);
return;
}
};
if arr.is_empty() {
ins.err(
base_path,
"tags array is empty (CDDL requires at least one)",
);
return;
}
for (i, tag) in arr.into_iter().enumerate() {
let path = format!("{}[{}]", base_path, i);
match tag {
Value::Tag(TAG_COMID, inner) => match *inner {
Value::Bytes(b) => {
ins.info(
path.clone(),
format!(
"tagged-concise-mid-tag (#6.{}), {} bytes inner CBOR",
TAG_COMID,
b.len()
),
);
inspect_comid_bytes(ins, &path, &b);
}
other => ins.err(
path,
format!(
"#6.{} (CoMID) inner must be bstr .cbor concise-mid-tag, found {}",
TAG_COMID,
value_kind(&other)
),
),
},
Value::Tag(TAG_COSWID, inner) => match *inner {
Value::Bytes(b) => ins.info(
path,
format!(
"tagged-concise-swid-tag (#6.{}), {} bytes inner CBOR",
TAG_COSWID,
b.len()
),
),
other => ins.err(
path,
format!(
"#6.{} (CoSWID) inner must be bstr .cbor concise-swid-tag, found {}",
TAG_COSWID,
value_kind(&other)
),
),
},
Value::Tag(TAG_COTL, inner) => match *inner {
Value::Bytes(b) => ins.info(
path,
format!(
"tagged-concise-tl-tag (#6.{}), {} bytes inner CBOR",
TAG_COTL,
b.len()
),
),
other => ins.err(
path,
format!(
"#6.{} (CoTL) inner must be bstr .cbor concise-tl-tag, found {}",
TAG_COTL,
value_kind(&other)
),
),
},
Value::Tag(t, _) => ins.warn(
path,
format!(
"tag #6.{} is not a recognized CoRIM tag type ({}/{}/{} expected)",
t, TAG_COSWID, TAG_COMID, TAG_COTL
),
),
Value::Bytes(b) => {
ins.warn(
path.clone(),
format!(
"tags[] entry is a bare bstr ({} bytes), not the spec-required \
#6.{}/{}/{}-tagged form. The library accepts this as a TCG-style interop \
relaxation and routes it through `compat::decode_comid_from_tcg_bstr`.",
b.len(),
TAG_COSWID,
TAG_COMID,
TAG_COTL
),
);
inspect_comid_bytes(ins, &path, &b);
}
other => ins.err(
path,
format!(
"tags[] entry must be a CBOR-tagged item or a bare bstr, found {}",
value_kind(&other)
),
),
}
}
}
fn inspect_comid_bytes(ins: &mut Inspector<'_>, base_path: &str, bytes: &[u8]) {
let val: Value = match cbor::decode::<Value>(bytes) {
Ok(v) => v,
Err(e) => {
ins.err(base_path, format!("CoMID inner CBOR is not valid: {}", e));
return;
}
};
let map_val = match val {
Value::Tag(TAG_COMID, inner) => {
ins.warn(
base_path,
format!(
"CoMID inner is double-wrapped: #6.{} around the map (TCG-style)",
TAG_COMID
),
);
*inner
}
other => other,
};
let map = match map_val {
Value::Map(m) => m,
other => {
ins.err(
base_path,
format!(
"concise-mid-tag must be a map, found {}",
value_kind(&other)
),
);
return;
}
};
let mut have_tag_identity = false;
let mut have_triples = false;
let mut triples_value: Option<Value> = None;
for (k, v) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(v) => v,
Err(_) => {
ins.warn(
base_path,
format!("concise-mid-tag key {} out of i64 range; skipped", n),
);
continue;
}
},
other => {
ins.warn(
base_path,
format!(
"concise-mid-tag has non-integer key ({}); ignored",
value_kind(other)
),
);
continue;
}
};
let path = format!("{}.{}", base_path, key);
match key {
COMID_KEY_LANGUAGE => {
if !matches!(v, Value::Text(_)) {
ins.err(
path,
format!("language (key 0) must be tstr, found {}", value_kind(&v)),
);
}
}
COMID_KEY_TAG_IDENTITY => {
have_tag_identity = true;
if !matches!(v, Value::Map(_)) {
ins.err(
path,
format!(
"tag-identity (key 1) must be a tag-identity-map, found {}",
value_kind(&v)
),
);
}
}
COMID_KEY_ENTITIES => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!(
"entities (key 2) must be array of comid-entity-map, found {}",
value_kind(&v)
),
);
}
}
COMID_KEY_LINKED_TAGS => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!(
"linked-tags (key 3) must be array of linked-tag-map, found {}",
value_kind(&v)
),
);
}
}
COMID_KEY_TRIPLES => {
have_triples = true;
triples_value = Some(v);
}
_ => {
ins.info(
path,
format!("unrecognized concise-mid-tag key {} (extension)", key),
);
}
}
}
if !have_tag_identity {
ins.err(base_path, "missing required key 1 (tag-identity)");
}
if !have_triples {
ins.err(base_path, "missing required key 4 (triples)");
}
if let Some(v) = triples_value {
inspect_triples_map(ins, &format!("{}.4", base_path), v);
}
}
fn triple_kind_label(key: i64) -> &'static str {
match key {
TRIPLES_KEY_REFERENCE => "reference-triples",
TRIPLES_KEY_ENDORSED => "endorsed-triples",
TRIPLES_KEY_IDENTITY => "identity-triples",
TRIPLES_KEY_ATTEST_KEY => "attest-key-triples",
TRIPLES_KEY_DEPENDENCY => "dependency-triples",
TRIPLES_KEY_MEMBERSHIP => "membership-triples",
TRIPLES_KEY_COSWID => "coswid-triples",
TRIPLES_KEY_COND_ENDORSEMENT_SERIES => "conditional-endorsement-series-triples",
TRIPLES_KEY_COND_ENDORSEMENT => "conditional-endorsement-triples",
_ => "unknown-triples",
}
}
fn inspect_triples_map(ins: &mut Inspector<'_>, base_path: &str, v: Value) {
let map = match v {
Value::Map(m) => m,
other => {
ins.err(
base_path,
format!(
"triples (key 4) must be a triples-map, found {}",
value_kind(&other)
),
);
return;
}
};
let mut had_any = false;
for (k, v) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(v) => v,
Err(_) => {
ins.warn(
base_path,
format!("triples-map key {} out of i64 range; skipped", n),
);
continue;
}
},
other => {
ins.warn(
base_path,
format!(
"triples-map has non-integer key ({}); ignored",
value_kind(other)
),
);
continue;
}
};
had_any = true;
let path = format!("{}.{}", base_path, key);
match key {
TRIPLES_KEY_REFERENCE
| TRIPLES_KEY_ENDORSED
| TRIPLES_KEY_IDENTITY
| TRIPLES_KEY_ATTEST_KEY => {
inspect_env_measurements_triples(ins, &path, v, triple_kind_label(key));
}
TRIPLES_KEY_DEPENDENCY | TRIPLES_KEY_MEMBERSHIP => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!(
"{} (key {}) must be array, found {}",
triple_kind_label(key),
key,
value_kind(&v)
),
);
}
}
TRIPLES_KEY_COSWID => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!(
"coswid-triples (key 6) must be array, found {}",
value_kind(&v)
),
);
}
}
TRIPLES_KEY_COND_ENDORSEMENT_SERIES | TRIPLES_KEY_COND_ENDORSEMENT => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!(
"{} (key {}) must be array, found {}",
triple_kind_label(key),
key,
value_kind(&v)
),
);
}
}
_ => {
ins.info(
path,
format!("unrecognized triples-map key {} (extension)", key),
);
}
}
}
if !had_any {
ins.err(base_path, "triples-map must contain at least one entry");
}
}
fn inspect_env_measurements_triples(
ins: &mut Inspector<'_>,
base_path: &str,
v: Value,
kind: &str,
) {
let arr = match v {
Value::Array(a) => a,
other => {
ins.err(
base_path,
format!(
"{} must be a non-empty array of triple-records, found {}",
kind,
value_kind(&other)
),
);
return;
}
};
if arr.is_empty() {
ins.err(base_path, format!("{} array is empty", kind));
return;
}
for (i, triple) in arr.into_iter().enumerate() {
let tpath = format!("{}[{}]", base_path, i);
let pair = match triple {
Value::Array(a) => a,
other => {
ins.err(
tpath,
format!(
"{} record must be a 2-element array [env, [+ meas]], found {}",
kind,
value_kind(&other)
),
);
continue;
}
};
if pair.len() != 2 {
ins.err(
tpath.clone(),
format!(
"{} record must be a 2-element array [env, [+ meas]], found {} elements",
kind,
pair.len()
),
);
continue;
}
let mut it = pair.into_iter();
let env = it.next().expect("len checked == 2");
let meas = it.next().expect("len checked == 2");
inspect_environment_map(ins, &format!("{}.env", tpath), env);
let meas_path = format!("{}.measurements", tpath);
match meas {
Value::Array(ms) => {
if ms.is_empty() {
ins.err(meas_path, "measurements list is empty");
} else {
for (j, m) in ms.into_iter().enumerate() {
inspect_measurement_map(ins, &format!("{}[{}]", meas_path, j), m);
}
}
}
other => ins.err(
meas_path,
format!(
"measurements must be array of measurement-map, found {}",
value_kind(&other)
),
),
}
}
}
fn inspect_environment_map(ins: &mut Inspector<'_>, base_path: &str, v: Value) {
let map = match v {
Value::Map(m) => m,
other => {
ins.err(
base_path,
format!(
"environment-map must be a map, found {}",
value_kind(&other)
),
);
return;
}
};
let mut had_any = false;
for (k, v) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(v) => v,
Err(_) => continue,
},
_ => continue,
};
had_any = true;
let path = format!("{}.{}", base_path, key);
match key {
ENV_KEY_CLASS => inspect_class_map(ins, &path, v),
ENV_KEY_INSTANCE | ENV_KEY_GROUP => {
}
_ => ins.info(
path,
format!("unrecognized environment-map key {} (extension)", key),
),
}
}
if !had_any {
ins.err(
base_path,
"environment-map must have at least one of class/instance/group",
);
}
}
fn inspect_class_map(ins: &mut Inspector<'_>, base_path: &str, v: Value) {
let map = match v {
Value::Map(m) => m,
other => {
ins.err(
base_path,
format!("class-map must be a map, found {}", value_kind(&other)),
);
return;
}
};
let mut had_any = false;
for (k, v) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(v) => v,
Err(_) => continue,
},
_ => continue,
};
had_any = true;
let path = format!("{}.{}", base_path, key);
match key {
CLASS_KEY_CLASS_ID => {}
CLASS_KEY_VENDOR | CLASS_KEY_MODEL => {
if !matches!(v, Value::Text(_)) {
ins.err(
path,
format!(
"class-map key {} (vendor/model) must be tstr, found {}",
key,
value_kind(&v)
),
);
}
}
CLASS_KEY_LAYER | CLASS_KEY_INDEX => {
if !matches!(v, Value::Integer(_)) {
ins.err(
path,
format!(
"class-map key {} (layer/index) must be uint, found {}",
key,
value_kind(&v)
),
);
}
}
_ => ins.info(
path,
format!("unrecognized class-map key {} (extension)", key),
),
}
}
if !had_any {
ins.err(base_path, "class-map must not be empty");
}
}
fn inspect_measurement_map(ins: &mut Inspector<'_>, base_path: &str, v: Value) {
let map = match v {
Value::Map(m) => m,
other => {
ins.err(
base_path,
format!(
"measurement-map must be a map, found {}",
value_kind(&other)
),
);
return;
}
};
let mut have_mval = false;
for (k, v) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(v) => v,
Err(_) => continue,
},
_ => continue,
};
let path = format!("{}.{}", base_path, key);
match key {
MEAS_KEY_MKEY => {
}
MEAS_KEY_MVAL => {
have_mval = true;
inspect_measurement_values_map(ins, &path, v);
}
MEAS_KEY_AUTHORIZED_BY => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!(
"authorized-by (key 2) must be array of $crypto-key-type-choice, found {}",
value_kind(&v)
),
);
}
}
_ => ins.info(
path,
format!("unrecognized measurement-map key {} (extension)", key),
),
}
}
if !have_mval {
ins.err(base_path, "missing required key 1 (mval)");
}
}
fn inspect_measurement_values_map(ins: &mut Inspector<'_>, base_path: &str, v: Value) {
let map = match v {
Value::Map(m) => m,
other => {
ins.err(
base_path,
format!(
"measurement-values-map must be a map, found {}",
value_kind(&other)
),
);
return;
}
};
let mut had_any = false;
for (k, v) in map {
let key = match &k {
Value::Integer(n) => match i64::try_from(*n) {
Ok(v) => v,
Err(_) => {
ins.warn(
base_path,
format!("mval key {} out of i64 range; skipped", n),
);
continue;
}
},
other => {
ins.warn(
base_path,
format!("mval has non-integer key ({}); ignored", value_kind(other)),
);
continue;
}
};
had_any = true;
let path = format!("{}{{{}}}", base_path, key);
match key {
MVAL_KEY_VERSION => {
if !matches!(v, Value::Map(_)) {
ins.err(
path,
format!(
"version (key 0) must be a version-map, found {}",
value_kind(&v)
),
);
}
}
MVAL_KEY_SVN => {
if !matches!(v, Value::Integer(_) | Value::Tag(_, _)) {
ins.err(
path,
format!(
"svn (key 1) must be uint or tagged-svn/min-svn, found {}",
value_kind(&v)
),
);
}
}
MVAL_KEY_DIGESTS => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!(
"digests (key 2) must be array of digest, found {}",
value_kind(&v)
),
);
}
}
MVAL_KEY_FLAGS => {
if !matches!(v, Value::Map(_)) {
ins.err(
path,
format!("flags (key 3) must be flags-map, found {}", value_kind(&v)),
);
}
}
MVAL_KEY_RAW_VALUE => {
if !matches!(v, Value::Bytes(_) | Value::Tag(_, _)) {
ins.err(
path,
format!(
"raw-value (key 4) must be bstr or tagged-raw-value, found {}",
value_kind(&v)
),
);
}
}
MVAL_KEY_RAW_VALUE_MASK_DEPRECATED => {
ins.warn(
path,
"raw-value-mask (key 5) is deprecated — use tagged-masked-raw-value instead",
);
}
MVAL_KEY_MAC_ADDR | MVAL_KEY_IP_ADDR => {
if !matches!(v, Value::Bytes(_)) {
ins.err(
path,
format!(
"{} (key {}) must be bstr, found {}",
if key == MVAL_KEY_MAC_ADDR {
"mac-addr"
} else {
"ip-addr"
},
key,
value_kind(&v)
),
);
}
}
MVAL_KEY_SERIAL_NUMBER | MVAL_KEY_NAME => {
if !matches!(v, Value::Text(_)) {
ins.err(
path,
format!(
"{} (key {}) must be tstr, found {}",
if key == MVAL_KEY_SERIAL_NUMBER {
"serial-number"
} else {
"name"
},
key,
value_kind(&v)
),
);
}
}
MVAL_KEY_UEID => {
if !matches!(v, Value::Bytes(_)) {
ins.err(
path,
format!(
"ueid (key 9) must be bstr (7-33 bytes), found {}",
value_kind(&v)
),
);
}
}
MVAL_KEY_UUID => {
let ok = match &v {
Value::Bytes(b) => b.len() == 16,
Value::Tag(TAG_UUID, inner) => {
matches!(inner.as_ref(), Value::Bytes(b) if b.len() == 16)
}
_ => false,
};
if !ok {
ins.err(
path,
format!(
"uuid (key 10) must be 16-byte bstr or #6.{}(16-byte bstr), found {}",
TAG_UUID,
value_kind(&v)
),
);
}
}
MVAL_KEY_CRYPTOKEYS => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!("cryptokeys (key 13) must be array of $crypto-key-type-choice, found {}", value_kind(&v)),
);
}
}
MVAL_KEY_INTEGRITY_REGISTERS => {
if !matches!(v, Value::Map(_)) {
ins.err(
path,
format!(
"integrity-registers (key 14) must be a map, found {}",
value_kind(&v)
),
);
}
}
MVAL_KEY_INT_RANGE => {
if !matches!(v, Value::Array(_)) {
ins.err(
path,
format!("int-range (key 15) must be array, found {}", value_kind(&v)),
);
}
}
_ => {
let label = ins
.current_profile
.and_then(|p| p.diagnose_mval_entry(key, &v));
match label {
Some(s) => ins.info(path, s),
None => ins.info(path, format!("extension key {}", key)),
}
}
}
}
if !had_any {
ins.err(
base_path,
"measurement-values-map must have at least one entry",
);
}
}
#[allow(dead_code)]
fn _unused_tagged_marker(_t: Tagged<()>) {}
#[cfg(test)]
mod tests {
use super::*;
use crate::cbor::encode;
fn inspect(bytes: &[u8]) -> DecodeReport {
super::inspect(bytes, &ProfileRegistry::new())
}
fn minimal_valid_comid_map() -> Value {
let env = Value::Map(vec![(
Value::Integer(0), Value::Map(vec![(
Value::Integer(1), Value::Text("ACME".into()),
)]),
)]);
let meas = Value::Map(vec![(
Value::Integer(1), Value::Map(vec![(
Value::Integer(11), Value::Text("widget".into()),
)]),
)]);
let ref_triples = Value::Array(vec![Value::Array(vec![env, Value::Array(vec![meas])])]);
Value::Map(vec![
(
Value::Integer(1), Value::Map(vec![(Value::Integer(0), Value::Text("test-tag".into()))]),
),
(
Value::Integer(4), Value::Map(vec![(Value::Integer(0), ref_triples)]),
),
])
}
fn empty_report_has_envelope(bytes: &[u8], kind: EnvelopeKind) {
let r = inspect(bytes);
assert_eq!(r.envelope, kind);
}
#[test]
fn empty_input_reports_error() {
let r = inspect(&[]);
assert_eq!(r.envelope, EnvelopeKind::Unknown);
assert!(r.error_count() >= 1);
}
#[test]
fn unknown_top_tag_reports_error() {
let bytes = encode(&Tagged::new(999u64, Value::Integer(0))).unwrap();
let r = inspect(&bytes);
assert_eq!(r.envelope, EnvelopeKind::Unknown);
assert!(r.issues.iter().any(|i| i.severity == Severity::Error));
}
#[test]
fn signed_envelope_recognized_even_when_payload_missing_inner_decode() {
let protected_inner = Value::Map(vec![(
Value::Integer(3),
Value::Text(CORIM_CONTENT_TYPE.into()),
)]);
let protected_bytes = encode(&protected_inner).unwrap();
let arr = Value::Array(vec![
Value::Bytes(protected_bytes),
Value::Map(vec![]),
Value::Null,
Value::Bytes(vec![0x55; 64]),
]);
let bytes = encode(&Tagged::new(TAG_SIGNED_CORIM, arr)).unwrap();
empty_report_has_envelope(&bytes, EnvelopeKind::Signed);
let r = inspect(&bytes);
assert!(r
.issues
.iter()
.any(|i| i.severity == Severity::Error && i.message.contains("alg")));
assert!(r
.issues
.iter()
.any(|i| i.severity == Severity::Error && i.message.contains("meta-group")));
}
#[test]
fn corim_meta_as_bare_map_is_flagged_with_hint() {
let protected_inner = Value::Map(vec![
(Value::Integer(1), Value::Integer(-35)),
(Value::Integer(3), Value::Text(CORIM_CONTENT_TYPE.into())),
(
Value::Integer(8),
Value::Map(vec![(
Value::Integer(0),
Value::Map(vec![(Value::Integer(0), Value::Text("Intel".into()))]),
)]),
),
]);
let protected_bytes = encode(&protected_inner).unwrap();
let arr = Value::Array(vec![
Value::Bytes(protected_bytes),
Value::Map(vec![]),
Value::Null,
Value::Bytes(vec![0x00; 32]),
]);
let bytes = encode(&Tagged::new(TAG_SIGNED_CORIM, arr)).unwrap();
let r = inspect(&bytes);
let bad = r
.issues
.iter()
.find(|i| i.path == "$.protected.8" && i.severity == Severity::Error)
.expect("expected an Error at $.protected.8");
assert!(bad.message.contains("bare CBOR map"));
assert!(bad.hint.is_some());
}
#[test]
fn unsigned_corim_with_one_comid_tag_reports_no_errors() {
let comid_bytes = encode(&minimal_valid_comid_map()).unwrap();
let corim_inner = Value::Map(vec![
(Value::Integer(0), Value::Text("my-id".into())),
(
Value::Integer(1),
Value::Array(vec![Value::Tag(
TAG_COMID,
Box::new(Value::Bytes(comid_bytes)),
)]),
),
]);
let bytes = encode(&Tagged::new(TAG_CORIM, corim_inner)).unwrap();
let r = inspect(&bytes);
assert_eq!(r.envelope, EnvelopeKind::Unsigned);
assert_eq!(r.error_count(), 0, "issues: {:#?}", r.issues);
}
#[test]
fn cose_sign1_wrong_arity_is_reported_but_decoding_continues() {
let arr = Value::Array(vec![Value::Bytes(vec![]), Value::Map(vec![])]);
let bytes = encode(&Tagged::new(TAG_SIGNED_CORIM, arr)).unwrap();
let r = inspect(&bytes);
assert!(r
.issues
.iter()
.any(|i| i.message.contains("4") && i.severity == Severity::Error));
assert!(r.issues.iter().any(|i| i.path == "$.protected"));
}
#[test]
fn legacy_500_wrapper_warns_and_recurses() {
let comid_bytes = encode(&minimal_valid_comid_map()).unwrap();
let corim_inner = Value::Map(vec![
(Value::Integer(0), Value::Text("my-id".into())),
(
Value::Integer(1),
Value::Array(vec![Value::Tag(
TAG_COMID,
Box::new(Value::Bytes(comid_bytes)),
)]),
),
]);
let corim_tagged = Value::Tag(TAG_CORIM, Box::new(corim_inner));
let bytes = encode(&Tagged::new(TAG_LEGACY_TOP, corim_tagged)).unwrap();
let r = inspect(&bytes);
assert_eq!(r.envelope, EnvelopeKind::Unsigned);
let warned = r
.issues
.iter()
.find(|i| i.severity == Severity::Warning && i.message.contains("legacy"))
.expect("expected a legacy-tag warning");
assert!(warned.message.contains("500"));
assert_eq!(r.error_count(), 0, "issues: {:#?}", r.issues);
}
#[test]
fn nested_500_502_18_envelope_is_recognized_as_signed() {
let protected_inner = Value::Map(vec![
(Value::Integer(1), Value::Integer(-7)),
(Value::Integer(3), Value::Text(CORIM_CONTENT_TYPE.into())),
(
Value::Integer(8),
Value::Bytes(encode(&Value::Map(vec![])).unwrap()),
),
]);
let protected_bytes = encode(&protected_inner).unwrap();
let cose = Value::Array(vec![
Value::Bytes(protected_bytes),
Value::Map(vec![]),
Value::Null,
Value::Bytes(vec![0xAA; 64]),
]);
let cose_tagged = Value::Tag(TAG_SIGNED_CORIM, Box::new(cose));
let inner502 = Value::Tag(TAG_LEGACY_SIGNED, Box::new(cose_tagged));
let bytes = encode(&Tagged::new(TAG_LEGACY_TOP, inner502)).unwrap();
let r = inspect(&bytes);
assert_eq!(r.envelope, EnvelopeKind::Signed);
assert!(r
.issues
.iter()
.any(|i| i.severity == Severity::Warning && i.message.contains("legacy")));
}
}