From dc85df4f35cc48299a9e4a69c9f91fc917807f0d Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Wed, 10 Sep 2025 16:39:24 +0100 Subject: [PATCH] Allow unassigned values for labels The previous code would reject the use of numeric label values that do not correspond to an IANA `enum` and which are outside of any defined private range. This made it hard to deal with as-yet-unassigned label values from internet drafts, which are not yet definitive. So add an `Unassigned(i64)` variant to cover these values. This also means that the `UnregisteredIana[NonPrivate]Value` errors can no longer be emitted. Fixes #112 --- CHANGELOG.md | 4 +++ examples/signature.rs | 3 +- src/common/mod.rs | 78 +++++++++++++++++++++++++------------------ src/common/tests.rs | 12 +++---- src/context/tests.rs | 23 +++++++------ src/cwt/tests.rs | 4 ++- src/header/tests.rs | 34 +++++++++++-------- src/key/tests.rs | 65 +++++++++++++++++++++++------------- 8 files changed, 135 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f863ddc..0d08e7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.4.0 - TBD +- Breaking change: add `Unassigned(i64)` variant to `RegisteredLabel` and + `RegisteredLabelWithPrivateRange`, to allow use of values that are not yet IANA-assigned. +- Breaking change: remove `CoseError::UnregisteredIanaValue` and + `CoseError::UnregisteredIanaNonPrivateValue` variants. - Breaking change: alter type of `crit` field in `Header` to support private-use labels (in accordance with [9052 ยง3.1](https://datatracker.ietf.org/doc/html/rfc9052#name-common-cose-header-paramete)). diff --git a/examples/signature.rs b/examples/signature.rs index b30cd4e..3344dfa 100644 --- a/examples/signature.rs +++ b/examples/signature.rs @@ -45,10 +45,11 @@ fn main() -> Result<(), CoseError> { let aad = b"this is additional data"; // Build a `CoseSign1` object. - let protected = coset::HeaderBuilder::new() + let mut protected = coset::HeaderBuilder::new() .algorithm(iana::Algorithm::ES256) .key_id(b"11".to_vec()) .build(); + protected.alg = Some(coset::Algorithm::PrivateUse(-49)); let sign1 = coset::CoseSign1Builder::new() .protected(protected) .payload(pt.to_vec()) diff --git a/src/common/mod.rs b/src/common/mod.rs index b2c6b78..c311a4f 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -48,10 +48,6 @@ pub enum CoseError { OutOfRangeIntegerValue, /// Unexpected CBOR item encountered (got, want). UnexpectedItem(&'static str, &'static str), - /// Unrecognized value in IANA-controlled range (with no private range). - UnregisteredIanaValue, - /// Unrecognized value in neither IANA-controlled range nor private range. - UnregisteredIanaNonPrivateValue, } /// Crate-specific Result type @@ -107,10 +103,6 @@ impl CoseError { CoseError::ExtraneousData => write!(f, "extraneous data in CBOR input"), CoseError::OutOfRangeIntegerValue => write!(f, "out of range integer value"), CoseError::UnexpectedItem(got, want) => write!(f, "got {got}, expected {want}"), - CoseError::UnregisteredIanaValue => write!(f, "expected recognized IANA value"), - CoseError::UnregisteredIanaNonPrivateValue => { - write!(f, "expected value in IANA or private use range") - } } } } @@ -291,6 +283,7 @@ impl AsCborValue for Label { /// where the allowed integer values are governed by IANA. #[derive(Clone, Debug, Eq, PartialEq)] pub enum RegisteredLabel { + Unassigned(i64), Assigned(T), Text(String), } @@ -306,16 +299,25 @@ impl CborSerializable for RegisteredLabel {} /// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. impl Ord for RegisteredLabel { fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (RegisteredLabel::Assigned(i1), RegisteredLabel::Assigned(i2)) => { - Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64())) + let left: i64 = match self { + RegisteredLabel::Assigned(i1) => i1.to_i64(), + RegisteredLabel::Unassigned(i1) => *i1, + RegisteredLabel::Text(t1) => { + return match other { + RegisteredLabel::Assigned(_i2) => Ordering::Greater, + RegisteredLabel::Unassigned(_i2) => Ordering::Greater, + RegisteredLabel::Text(t2) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)), + }; } - (RegisteredLabel::Assigned(_i1), RegisteredLabel::Text(_t2)) => Ordering::Less, - (RegisteredLabel::Text(_t1), RegisteredLabel::Assigned(_i2)) => Ordering::Greater, - (RegisteredLabel::Text(t1), RegisteredLabel::Text(t2)) => { - t1.len().cmp(&t2.len()).then(t1.cmp(t2)) - } - } + }; + // The `self`/`left` value is an integer if we reach here. + + let right: i64 = match other { + RegisteredLabel::Assigned(i2) => i2.to_i64(), + RegisteredLabel::Unassigned(i2) => *i2, + RegisteredLabel::Text(_t2) => return Ordering::Less, + }; + Label::Int(left).cmp(&Label::Int(right)) } } @@ -329,10 +331,11 @@ impl AsCborValue for RegisteredLabel { fn from_cbor_value(value: Value) -> Result { match value { Value::Integer(i) => { - if let Some(a) = T::from_i64(i.try_into()?) { + let i: i64 = i.try_into()?; + if let Some(a) = T::from_i64(i) { Ok(RegisteredLabel::Assigned(a)) } else { - Err(CoseError::UnregisteredIanaValue) + Ok(RegisteredLabel::Unassigned(i)) } } Value::Text(t) => Ok(RegisteredLabel::Text(t)), @@ -342,6 +345,7 @@ impl AsCborValue for RegisteredLabel { fn to_cbor_value(self) -> Result { Ok(match self { + RegisteredLabel::Unassigned(e) => Value::from(e), RegisteredLabel::Assigned(e) => Value::from(e.to_i64()), RegisteredLabel::Text(t) => Value::Text(t), }) @@ -354,6 +358,7 @@ impl AsCborValue for RegisteredLabel { #[derive(Clone, Debug, Eq, PartialEq)] pub enum RegisteredLabelWithPrivate { PrivateUse(i64), + Unassigned(i64), Assigned(T), Text(String), } @@ -369,18 +374,26 @@ impl CborSerializable for RegisteredLabelWithPriv /// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. impl Ord for RegisteredLabelWithPrivate { fn cmp(&self, other: &Self) -> Ordering { - use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text}; - match (self, other) { - (Assigned(i1), Assigned(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64())), - (Assigned(i1), PrivateUse(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(*i2)), - (PrivateUse(i1), Assigned(i2)) => Label::Int(*i1).cmp(&Label::Int(i2.to_i64())), - (PrivateUse(i1), PrivateUse(i2)) => Label::Int(*i1).cmp(&Label::Int(*i2)), - (Assigned(_i1), Text(_t2)) => Ordering::Less, - (PrivateUse(_i1), Text(_t2)) => Ordering::Less, - (Text(_t1), Assigned(_i2)) => Ordering::Greater, - (Text(_t1), PrivateUse(_i2)) => Ordering::Greater, - (Text(t1), Text(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)), - } + use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text, Unassigned}; + let left: i64 = match self { + Assigned(i1) => i1.to_i64(), + Unassigned(i1) | PrivateUse(i1) => *i1, + Text(t1) => { + return match other { + Assigned(_i2) => Ordering::Greater, + Unassigned(_i2) | PrivateUse(_i2) => Ordering::Greater, + Text(t2) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)), + }; + } + }; + // The `self`/`left` value is an integer if we reach here. + + let right: i64 = match other { + Assigned(i2) => i2.to_i64(), + Unassigned(i2) | PrivateUse(i2) => *i2, + Text(_t2) => return Ordering::Less, + }; + Label::Int(left).cmp(&Label::Int(right)) } } @@ -400,7 +413,7 @@ impl AsCborValue for RegisteredLabelWithPrivate Ok(RegisteredLabelWithPrivate::Text(t)), @@ -410,6 +423,7 @@ impl AsCborValue for RegisteredLabelWithPrivate Result { Ok(match self { RegisteredLabelWithPrivate::PrivateUse(i) => Value::from(i), + RegisteredLabelWithPrivate::Unassigned(i) => Value::from(i), RegisteredLabelWithPrivate::Assigned(i) => Value::from(i.to_i64()), RegisteredLabelWithPrivate::Text(t) => Value::Text(t), }) diff --git a/src/common/tests.rs b/src/common/tests.rs index 8dbda7d..411316a 100644 --- a/src/common/tests.rs +++ b/src/common/tests.rs @@ -186,6 +186,8 @@ fn test_registered_label_encode() { (RegisteredLabel::Assigned(iana::Algorithm::A192GCM), "02"), (RegisteredLabel::Assigned(iana::Algorithm::EdDSA), "27"), (RegisteredLabel::Text("abc".to_owned()), "63616263"), + (RegisteredLabel::Unassigned(9), "09"), + (RegisteredLabel::Unassigned(-20_000), "394e1f"), ]; for (i, (label, label_data)) in tests.iter().enumerate() { @@ -247,8 +249,6 @@ fn test_registered_label_decode_fail() { let tests = [ ("43010203", "expected int/tstr"), ("", "decode CBOR failure: Io(EndOfFile"), - ("09", "expected recognized IANA value"), - ("394e1f", "expected recognized IANA value"), ]; for (label_data, err_msg) in tests.iter() { let data = hex::decode(label_data).unwrap(); @@ -266,7 +266,7 @@ iana_registry! { impl WithPrivateRange for TestPrivateLabel { fn is_private(i: i64) -> bool { - i > 10 || i < 1000 + !(-50_000..=10).contains(&i) } } @@ -286,6 +286,8 @@ fn test_registered_label_with_private_encode() { "3a0001116f", ), (RegisteredLabelWithPrivate::PrivateUse(11), "0b"), + (RegisteredLabelWithPrivate::Unassigned(9), "09"), + (RegisteredLabelWithPrivate::Unassigned(-20_000), "394e1f"), ]; for (i, (label, label_data)) in tests.iter().enumerate() { @@ -293,7 +295,7 @@ fn test_registered_label_with_private_encode() { assert_eq!(*label_data, hex::encode(&got), "case {i}"); let got = RegisteredLabelWithPrivate::from_slice(&got).unwrap(); - assert_eq!(*label, got); + assert_eq!(*label, got, "case {i}: {label_data}"); } } @@ -353,8 +355,6 @@ fn test_registered_label_with_private_decode_fail() { let tests = [ ("43010203", "expected int/tstr"), ("", "decode CBOR failure: Io(EndOfFile"), - ("09", "expected value in IANA or private use range"), - ("394e1f", "expected value in IANA or private use range"), ]; for (label_data, err_msg) in tests.iter() { let data = hex::decode(label_data).unwrap(); diff --git a/src/context/tests.rs b/src/context/tests.rs index 1515ab6..9f0d76c 100644 --- a/src/context/tests.rs +++ b/src/context/tests.rs @@ -196,6 +196,19 @@ fn test_context_encode() { "41", "03", // 1-bstr ), ), + ( + CoseKdfContext { + algorithm_id: Algorithm::Unassigned(8), + ..CoseKdfContext::default() + }, + concat!( + "84", // 4-tuple + "08", // int : unassigned value + "83", "f6f6f6", // 3-tuple: [nil, nil, nil] + "83", "f6f6f6", // 3-tuple: [nil, nil, nil] + "82", "0040", // 2-tuple: [0, 0-bstr] + ), + ), ]; for (i, (key, key_data)) in tests.iter().enumerate() { let got = key.clone().to_vec().unwrap(); @@ -238,16 +251,6 @@ fn test_context_decode_fail() { ), "decode CBOR failure: Io(EndOfFile", ), - ( - concat!( - "84", // 4-tuple - "08", // int : unassigned value - "83", "f6f6f6", // 3-tuple: [nil, nil, nil] - "83", "f6f6f6", // 3-tuple: [nil, nil, nil] - "82", "0040", // 2-tuple: [0, 0-bstr] - ), - "expected value in IANA or private use range", - ), ( concat!( "84", // 4-tuple diff --git a/src/cwt/tests.rs b/src/cwt/tests.rs index 6f8619b..fb02768 100644 --- a/src/cwt/tests.rs +++ b/src/cwt/tests.rs @@ -108,7 +108,9 @@ fn test_cwt_encode() { let got = claims.clone().to_vec().unwrap(); assert_eq!(*claims_data, hex::encode(&got), "case {i}"); - let got = ClaimsSet::from_slice(&got).unwrap(); + let got = ClaimsSet::from_slice(&got).unwrap_or_else(|e| { + panic!("deserialize failed on case {}, {}: {:?}", i, claims_data, e) + }); assert_eq!(*claims, got); } } diff --git a/src/header/tests.rs b/src/header/tests.rs index 7ceb1d4..28c69dd 100644 --- a/src/header/tests.rs +++ b/src/header/tests.rs @@ -160,6 +160,26 @@ fn test_header_encode() { "3a00010000", // crit => 1-arr [-65537] ), ), + ( + Header { + content_type: Some(ContentType::Unassigned(1542)), + ..Default::default() + }, + concat!( + "a1", // 1-map + "03", "19", "0606", // 3 (content-type) => unassigned value 1542 + ), + ), + ( + Header { + alg: Some(Algorithm::Unassigned(8)), + ..Default::default() + }, + concat!( + "a1", // 1-map + "01", "08", // 1 (alg) => unassigned value 8 + ), + ), ]; for (i, (header, header_data)) in tests.iter().enumerate() { let got = header.clone().to_vec().unwrap(); @@ -213,13 +233,6 @@ fn test_header_decode_fail() { ), "extraneous data in CBOR input", ), - ( - concat!( - "a1", // 1-map - "01", "08", // 1 (alg) => invalid value - ), - "expected value in IANA or private use range", - ), ( concat!( "a1", // 1-map @@ -255,13 +268,6 @@ fn test_header_decode_fail() { ), "expected int/tstr", ), - ( - concat!( - "a1", // 1-map - "03", "19", "0606", // 3 (content-type) => invalid value 1542 - ), - "expected recognized IANA value", - ), ( concat!( "a1", // 1-map diff --git a/src/key/tests.rs b/src/key/tests.rs index 9ba84ed..5b90025 100644 --- a/src/key/tests.rs +++ b/src/key/tests.rs @@ -205,6 +205,47 @@ fn test_cose_key_encode() { "22", "f4" // -3 (y) => false ), ), + ( + CoseKey { + kty: KeyType::Unassigned(17), + key_id: vec![1, 2, 3], + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "11", // 1 (kty) => 17 (unassigned) + "02", "43", "010203" // 2 (kid) => 3-bstr + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + key_ops: vec![ + KeyOperation::Assigned(iana::KeyOperation::Encrypt), + KeyOperation::Unassigned(11), + ] + .into_iter() + .collect(), + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "04", "82", "03", "0b", // 4 (key_ops) => 3-tuple [3, 11] + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Unassigned(0x99)), + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "1899", // 3 (alg) => 0x99 + ), + ), ]; for (i, (key, key_data)) in tests.iter().enumerate() { let got = key.clone().to_vec().unwrap(); @@ -456,14 +497,6 @@ fn test_cose_key_decode_fail() { ), "expected map", ), - ( - concat!( - "a2", // 2-map - "01", "11", // 1 (kty) => invalid value - "02", "43", "010203" // 2 (kid) => 3-bstr - ), - "expected recognized IANA value", - ), ( concat!( "a2", // 2-map @@ -495,14 +528,6 @@ fn test_cose_key_decode_fail() { ), "expected bstr", ), - ( - concat!( - "a2", // 2-map - "01", "01", // 1 (kty) => OKP - "03", "1899", // 3 (alg) => 0x99 - ), - "expected value in IANA or private use range", - ), ( concat!( "a2", // 2-map @@ -535,14 +560,6 @@ fn test_cose_key_decode_fail() { ), "expected non-empty array", ), - ( - concat!( - "a2", // 2-map - "01", "01", // 1 (kty) => OKP - "04", "82", "03", "0b", // 4 (key_ops) => 3-tuple [3,11] - ), - "expected recognized IANA value", - ), ( concat!( "a2", // 2-map