Skip to content

Commit

Permalink
Support CoseKey field ordering (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddrysdale authored Jan 12, 2024
1 parent 8678c9d commit 2eed112
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 4 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "coset"
version = "0.3.5"
version = "0.3.6"
authors = ["David Drysdale <[email protected]>", "Paul Crowley <[email protected]>"]
edition = "2018"
license = "Apache-2.0"
Expand Down
31 changes: 31 additions & 0 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
match value {
Expand Down
66 changes: 66 additions & 0 deletions src/common/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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 = [
Expand Down
22 changes: 21 additions & 1 deletion src/key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use crate::{
cbor::value::Value,
common::AsCborValue,
common::{AsCborValue, CborOrdering},
iana,
iana::EnumI64,
util::{to_cbor_array, ValueTryAs},
Expand Down Expand Up @@ -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);
Expand Down
132 changes: 131 additions & 1 deletion src/key/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 2eed112

Please sign in to comment.