From 2eed11214e90fa52becbad8380376be6ff8a9543 Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Fri, 12 Jan 2024 10:32:13 +0000 Subject: [PATCH] Support CoseKey field ordering (#85) --- CHANGELOG.md | 7 +++ Cargo.lock | 2 +- Cargo.toml | 2 +- src/common/mod.rs | 31 +++++++++++ src/common/tests.rs | 66 ++++++++++++++++++++++ src/key/mod.rs | 22 +++++++- src/key/tests.rs | 132 +++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 258 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01d470..0a8d5fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 0.3.6 - TBD + +- Helpers for ordering of fields in a `COSE_Key`: + - Add `Label::cmp_canonical()` for RFC 7049 canonical ordering. + - Add `CborOrdering` enum to specify ordering. + - Add `CoseKey::canonicalize()` method to order fields. + ## 0.3.5 - 2023-09-29 - Add helper methods to create and verify detached signatures: diff --git a/Cargo.lock b/Cargo.lock index 8063a90..c7efb02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "coset" -version = "0.3.5" +version = "0.3.6" dependencies = [ "ciborium", "ciborium-io", diff --git a/Cargo.toml b/Cargo.toml index 1e35011..94529ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coset" -version = "0.3.5" +version = "0.3.6" authors = ["David Drysdale ", "Paul Crowley "] edition = "2018" license = "Apache-2.0" diff --git a/src/common/mod.rs b/src/common/mod.rs index b94ec31..336db50 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -240,6 +240,37 @@ impl PartialOrd for Label { } } +impl Label { + /// Alternative ordering for `Label`, using the canonical ordering criteria from RFC 7049 + /// section 3.9 (where the primary sorting criterion is the length of the encoded form), rather + /// than the ordering given by RFC 8949 section 4.2.1 (lexicographic ordering of encoded form). + /// + /// # Panics + /// + /// Panics if either `Label` fails to serialize. + pub fn cmp_canonical(&self, other: &Self) -> Ordering { + let encoded_self = self.clone().to_vec().unwrap(); /* safe: documented */ + let encoded_other = other.clone().to_vec().unwrap(); /* safe: documented */ + if encoded_self.len() != encoded_other.len() { + // Shorter encoding sorts first. + encoded_self.len().cmp(&encoded_other.len()) + } else { + // Both encode to the same length, sort lexicographically on encoded form. + encoded_self.cmp(&encoded_other) + } + } +} + +/// Indicate which ordering should be applied to CBOR values. +pub enum CborOrdering { + /// Order values lexicographically, as per RFC 8949 section 4.2.1 (Core Deterministic Encoding + /// Requirements) + Lexicographic, + /// Order values by encoded length, then by lexicographic ordering of encoded form, as per RFC + /// 7049 section 3.9 (Canonical CBOR) / RFC 8949 section 4.2.3 (Length-First Map Key Ordering). + LengthFirstLexicographic, +} + impl AsCborValue for Label { fn from_cbor_value(value: Value) -> Result { match value { diff --git a/src/common/tests.rs b/src/common/tests.rs index e7b59aa..39ef41c 100644 --- a/src/common/tests.rs +++ b/src/common/tests.rs @@ -56,6 +56,7 @@ fn test_label_sort() { let pairs = vec![ (Label::Int(0x1234), Label::Text("a".to_owned())), (Label::Int(0x1234), Label::Text("ab".to_owned())), + (Label::Int(0x12345678), Label::Text("ab".to_owned())), (Label::Int(0), Label::Text("ab".to_owned())), (Label::Int(-1), Label::Text("ab".to_owned())), (Label::Int(0), Label::Int(10)), @@ -99,6 +100,71 @@ fn test_label_sort() { } } +#[test] +fn test_label_canonical_sort() { + // Pairs of `Label`s with the "smaller" first, as per RFC7049 "canonical" ordering. + let pairs = vec![ + (Label::Text("a".to_owned()), Label::Int(0x1234)), // different than above + (Label::Int(0x1234), Label::Text("ab".to_owned())), + (Label::Text("ab".to_owned()), Label::Int(0x12345678)), // different than above + (Label::Int(0), Label::Text("ab".to_owned())), + (Label::Int(-1), Label::Text("ab".to_owned())), + (Label::Int(0), Label::Int(10)), + (Label::Int(0), Label::Int(-10)), + (Label::Int(10), Label::Int(-1)), + (Label::Int(-1), Label::Int(-2)), + (Label::Int(0x12), Label::Int(0x1234)), + (Label::Int(0x99), Label::Int(0x1234)), + (Label::Int(0x1234), Label::Int(0x1235)), + (Label::Text("a".to_owned()), Label::Text("ab".to_owned())), + (Label::Text("aa".to_owned()), Label::Text("ab".to_owned())), + ]; + for (left, right) in pairs.into_iter() { + let value_cmp = left.cmp_canonical(&right); + + let left_data = left.clone().to_vec().unwrap(); + let right_data = right.clone().to_vec().unwrap(); + + let len_cmp = left_data.len().cmp(&right_data.len()); + let data_cmp = left_data.cmp(&right_data); + let reverse_cmp = right.cmp_canonical(&left); + let equal_cmp = left.cmp_canonical(&left); + + assert_eq!( + value_cmp, + Ordering::Less, + "{:?} (encoded: {}) < {:?} (encoded: {})", + left, + hex::encode(&left_data), + right, + hex::encode(&right_data) + ); + if len_cmp != Ordering::Equal { + assert_eq!( + len_cmp, + Ordering::Less, + "{:?}={} < {:?}={} by len", + left, + hex::encode(&left_data), + right, + hex::encode(&right_data) + ); + } else { + assert_eq!( + data_cmp, + Ordering::Less, + "{:?}={} < {:?}={} by data", + left, + hex::encode(&left_data), + right, + hex::encode(&right_data) + ); + } + assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left); + assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left); + } +} + #[test] fn test_label_decode_fail() { let tests = [ diff --git a/src/key/mod.rs b/src/key/mod.rs index 07ee7a7..0df7d31 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -18,7 +18,7 @@ use crate::{ cbor::value::Value, - common::AsCborValue, + common::{AsCborValue, CborOrdering}, iana, iana::EnumI64, util::{to_cbor_array, ValueTryAs}, @@ -88,6 +88,26 @@ pub struct CoseKey { pub params: Vec<(Label, Value)>, } +impl CoseKey { + /// Re-order the contents of the key so that the contents will be emitted in one of the standard + /// CBOR sorted orders. + pub fn canonicalize(&mut self, ordering: CborOrdering) { + // The keys that are represented as named fields CBOR-encode as single bytes 0x01 - 0x05, + // which sort before any other CBOR values (other than 0x00) in either sorting scheme: + // - In length-first sorting, a single byte sorts before anything multi-byte and 1-5 sorts + // before any other value. + // - In encoded-lexicographic sorting, there are no valid CBOR-encoded single values that + // start with a byte in the range 0x01 - 0x05 other than the values 1-5. + // So we only need to sort the `params`. + match ordering { + CborOrdering::Lexicographic => self.params.sort_by(|l, r| l.0.cmp(&r.0)), + CborOrdering::LengthFirstLexicographic => { + self.params.sort_by(|l, r| l.0.cmp_canonical(&r.0)) + } + } + } +} + impl crate::CborSerializable for CoseKey {} const KTY: Label = Label::Int(iana::KeyParameter::Kty as i64); diff --git a/src/key/tests.rs b/src/key/tests.rs index ada3673..ccbf20f 100644 --- a/src/key/tests.rs +++ b/src/key/tests.rs @@ -15,7 +15,7 @@ //////////////////////////////////////////////////////////////////////////////// use super::*; -use crate::{cbor::value::Value, iana, util::expect_err, CborSerializable}; +use crate::{cbor::value::Value, iana, util::expect_err, CborOrdering, CborSerializable}; use alloc::{borrow::ToOwned, string::ToString, vec}; #[test] @@ -742,3 +742,133 @@ fn test_key_builder_core_param_panic() { .param(1, Value::Null) .build(); } + +#[test] +fn test_key_canonicalize() { + struct TestCase { + key_data: &'static str, // hex + rfc7049_key: CoseKey, + rfc8949_key: CoseKey, + rfc7049_data: Option<&'static str>, // hex, `None` indicates same as `key_data` + rfc8949_data: Option<&'static str>, // hex, `None` indicates same as `key_data` + } + let tests = [ + TestCase { + key_data: concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "26", // 3 (alg) => -7 + ), + rfc7049_key: CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)), + ..Default::default() + }, + rfc8949_key: CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)), + ..Default::default() + }, + rfc7049_data: None, + rfc8949_data: None, + }, + TestCase { + key_data: concat!( + "a2", // 2-map + "03", "26", // 3 (alg) => -7 + "01", "01", // 1 (kty) => OKP + ), + rfc7049_key: CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)), + ..Default::default() + }, + rfc8949_key: CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)), + ..Default::default() + }, + rfc7049_data: Some(concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "26", // 3 (alg) => -7 + )), + rfc8949_data: Some(concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "26", // 3 (alg) => -7 + )), + }, + TestCase { + key_data: concat!( + "a4", // 4-map + "03", "26", // 3 (alg) => -7 + "1904d2", "01", // 1234 => 1 + "01", "01", // 1 (kty) => OKP + "6161", "01", // "a" => 1 + ), + // "a" encodes shorter than 1234, so appears first + rfc7049_key: CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)), + params: vec![ + (Label::Text("a".to_string()), Value::Integer(1.into())), + (Label::Int(1234), Value::Integer(1.into())), + ], + ..Default::default() + }, + // 1234 encodes with leading byte 0x19, so appears before a tstr + rfc8949_key: CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)), + params: vec![ + (Label::Int(1234), Value::Integer(1.into())), + (Label::Text("a".to_string()), Value::Integer(1.into())), + ], + ..Default::default() + }, + rfc7049_data: Some(concat!( + "a4", // 4-map + "01", "01", // 1 (kty) => OKP + "03", "26", // 3 (alg) => -7 + "6161", "01", // "a" => 1 + "1904d2", "01", // 1234 => 1 + )), + rfc8949_data: Some(concat!( + "a4", // 4-map + "01", "01", // 1 (kty) => OKP + "03", "26", // 3 (alg) => -7 + "1904d2", "01", // 1234 => 1 + "6161", "01", // "a" => 1 + )), + }, + ]; + for testcase in tests { + let key_data = hex::decode(testcase.key_data).unwrap(); + let mut key = CoseKey::from_slice(&key_data) + .unwrap_or_else(|e| panic!("Failed to deserialize {}: {e:?}", testcase.key_data)); + + // Canonicalize according to RFC 7049. + key.canonicalize(CborOrdering::LengthFirstLexicographic); + assert_eq!( + key, testcase.rfc7049_key, + "Mismatch for {}", + testcase.key_data + ); + let got = testcase.rfc7049_key.to_vec().unwrap(); + let want = testcase.rfc7049_data.unwrap_or(testcase.key_data); + assert_eq!(hex::encode(got), want, "Mismatch for {}", testcase.key_data); + + // Canonicalize according to RFC 8949. + key.canonicalize(CborOrdering::Lexicographic); + assert_eq!( + key, testcase.rfc8949_key, + "Mismatch for {}", + testcase.key_data + ); + + let got = testcase.rfc8949_key.to_vec().unwrap(); + let want = testcase.rfc8949_data.unwrap_or(testcase.key_data); + assert_eq!(hex::encode(got), want, "Mismatch for {}", testcase.key_data); + } +}