From 16cdcac33e3c53685c85cdbd099ee5699053dcdf Mon Sep 17 00:00:00 2001 From: Andrew Scull Date: Wed, 17 Dec 2025 11:45:38 +0000 Subject: [PATCH] Convert EC2 keys to/from SEC1 octet strings Add helper functions to build a CoseKey from a SEC1 octet string and to encode an EC2 CoseKey to a SEC1 octet string. --- src/key/mod.rs | 105 ++++++++++++++++++ src/key/tests.rs | 284 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 389 insertions(+) diff --git a/src/key/mod.rs b/src/key/mod.rs index 0df7d31..9c515a1 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -88,6 +88,52 @@ pub struct CoseKey { pub params: Vec<(Label, Value)>, } +const SEC1_COMPRESSED_SIGN_0: u8 = 0x02; +const SEC1_COMPRESSED_SIGN_1: u8 = 0x03; +const SEC1_UNCOMPRESSED: u8 = 0x04; + +/// The error type returned when a [`CoseKey`] can't be converted to a SEC1 octet string. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ToSec1OctetStringError { + /// The [`CoseKey`] is not an elliptic curve [`iana::KeyType::EC2`] key type. + NotEcKey, + /// The X or Y coordinate is not present in the key parameters. + MissingCoordinate, + /// The X or Y coordinate is an invalid CBOR type. + InvalidCoordinateType, + /// The X and Y coordinates are not the same length. + UnequalCoordinateLength, +} + +#[cfg(feature = "std")] +impl std::error::Error for ToSec1OctetStringError {} + +impl core::fmt::Display for ToSec1OctetStringError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ToSec1OctetStringError::NotEcKey => write!(f, "not an EC key"), + ToSec1OctetStringError::MissingCoordinate => write!(f, "missing coordinate"), + ToSec1OctetStringError::InvalidCoordinateType => write!(f, "invalid coordinate type"), + ToSec1OctetStringError::UnequalCoordinateLength => { + write!(f, "unequal coordinate lengths") + } + } + } +} + +/// The error type returned when a SEC1 octet string is malformed. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ParseSec1OctetStringError; + +#[cfg(feature = "std")] +impl std::error::Error for ParseSec1OctetStringError {} + +impl core::fmt::Display for ParseSec1OctetStringError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "ParseSec1OctetStringError") + } +} + impl CoseKey { /// Re-order the contents of the key so that the contents will be emitted in one of the standard /// CBOR sorted orders. @@ -106,6 +152,45 @@ impl CoseKey { } } } + + /// Converts an EC2 key to a SEC1 octet string representing the public key point. The SEC1 octet + /// string is compatible with the ANSI X9.62 point format. + /// + /// Minimal validation is performed, notably: + /// - must be an EC2 key type + /// - first instance of X and Y parameters are used + /// - X and Y must be the same length + /// - the absolute length of X and Y are not checked + /// - the curve and algorithm are not considered + /// + /// The caller is responsible for any stricter validation. + pub fn to_sec1_octet_string(&self) -> Result, ToSec1OctetStringError> { + if self.kty != KeyType::Assigned(iana::KeyType::EC2) { + return Err(ToSec1OctetStringError::NotEcKey); + } + let x_param = self + .params + .iter() + .find(|(k, _)| k == &Label::Int(iana::Ec2KeyParameter::X as i64)) + .ok_or(ToSec1OctetStringError::MissingCoordinate)?; + let y_param = self + .params + .iter() + .find(|(k, _)| k == &Label::Int(iana::Ec2KeyParameter::Y as i64)) + .ok_or(ToSec1OctetStringError::MissingCoordinate)?; + let x = x_param + .1 + .as_bytes() + .ok_or(ToSec1OctetStringError::InvalidCoordinateType)? + .as_slice(); + match &y_param.1 { + Value::Bool(false) => Ok([&[SEC1_COMPRESSED_SIGN_0], x].concat()), + Value::Bool(true) => Ok([&[SEC1_COMPRESSED_SIGN_1], x].concat()), + Value::Bytes(y) if x.len() == y.len() => Ok([&[SEC1_UNCOMPRESSED], x, y].concat()), + Value::Bytes(_) => Err(ToSec1OctetStringError::UnequalCoordinateLength), + _ => Err(ToSec1OctetStringError::InvalidCoordinateType), + } + } } impl crate::CborSerializable for CoseKey {} @@ -243,6 +328,26 @@ impl CoseKeyBuilder { }) } + /// Constructor for an elliptic curve public key specified by a SEC1 octet string representing + /// the public key point. The SEC1 octet string is compatible with ANSI X9.62 point format. The + /// caller is responsible for validating the SEC1 point and setting the correct curve for the + /// key. The leading octet must be `0x02`, `0x03`, or `0x04`. + pub fn new_ec2_pub_key_sec1_octet_string( + curve: iana::EllipticCurve, + sec1: &[u8], + ) -> Result { + let (first, rest) = sec1.split_first().ok_or(ParseSec1OctetStringError)?; + match *first { + SEC1_COMPRESSED_SIGN_0 => Ok(Self::new_ec2_pub_key_y_sign(curve, rest.to_vec(), false)), + SEC1_COMPRESSED_SIGN_1 => Ok(Self::new_ec2_pub_key_y_sign(curve, rest.to_vec(), true)), + SEC1_UNCOMPRESSED if rest.len() % 2 == 0 => { + let (x, y) = rest.split_at(rest.len() / 2); + Ok(Self::new_ec2_pub_key(curve, x.to_vec(), y.to_vec())) + } + _ => Err(ParseSec1OctetStringError), + } + } + /// Constructor for an elliptic curve private key specified by `d`, together with public `x` and /// `y` coordinates. pub fn new_ec2_priv_key( diff --git a/src/key/tests.rs b/src/key/tests.rs index e5ae55e..2957aff 100644 --- a/src/key/tests.rs +++ b/src/key/tests.rs @@ -205,6 +205,51 @@ fn test_cose_key_encode() { "22", "f4" // -3 (y) => false ), ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("03333333").unwrap(), + ) + .unwrap() + .build(), + concat!( + "a4", // 3-map + "01", "02", // 1 (kty) => 2 (EC2) + "20", "01", // -1 (crv) => 1 (P_256) + "21", "43", "333333", // -2 (x) => 2-bstr + "22", "f5", // -3 (y) => true + ), + ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("0201020304").unwrap(), + ) + .unwrap() + .build(), + concat!( + "a4", // 3-map + "01", "02", // 1 (kty) => 2 (EC2) + "20", "01", // -1 (crv) => 1 (P_256) + "21", "44", "01020304", // -2 (x) => 2-bstr + "22", "f4", // -3 (y) => false + ), + ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("0412345678").unwrap(), + ) + .unwrap() + .build(), + concat!( + "a4", // 3-map + "01", "02", // 1 (kty) => 2 (EC2) + "20", "01", // -1 (crv) => 1 (P_256) + "21", "42", "1234", // -2 (x) => 2-bstr + "22", "42", "5678", // -3 (y) => 2-bstr + ), + ), ]; for (i, (key, key_data)) in tests.iter().enumerate() { let got = key.clone().to_vec().unwrap(); @@ -227,6 +272,39 @@ fn test_cose_key_encode() { assert_eq!(got, keyset); } +#[test] +fn test_new_ec2_pub_key_sec1_octet_string_errors() { + let tests = [ + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("00").unwrap(), + ) + .unwrap_err(), + ParseSec1OctetStringError, + ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("06c2b3a98ffe82").unwrap(), + ) + .unwrap_err(), + ParseSec1OctetStringError, + ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("04112233445566778899").unwrap(), + ) + .unwrap_err(), + ParseSec1OctetStringError, + ), + ]; + for (i, (got, want)) in tests.iter().enumerate() { + assert_eq!(got, want, "case {i}"); + } +} + #[test] fn test_rfc8152_public_cose_key_decode() { // Public keys from RFC8152 section 6.7.1. @@ -872,3 +950,209 @@ fn test_key_canonicalize() { assert_eq!(hex::encode(got), want, "Mismatch for {}", testcase.key_data); } } + +#[test] +fn test_key_to_sec1_octet_string() { + let tests = [ + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("12").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bytes(hex::decode("34").unwrap()), + ), + ], + ..Default::default() + }, + Ok("041234"), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("13").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bytes(hex::decode("37").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("5555").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bytes(hex::decode("8888").unwrap()), + ), + ], + ..Default::default() + }, + Ok("041337"), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("aabbccdd").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bool(false), + ), + ], + ..Default::default() + }, + Ok("02aabbccdd"), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("123456789abcde").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bool(true), + ), + ], + ..Default::default() + }, + Ok("03123456789abcde"), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("111133").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bytes(hex::decode("22222244").unwrap()), + ), + ], + ..Default::default() + }, + Err(ToSec1OctetStringError::UnequalCoordinateLength), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bytes(hex::decode("34").unwrap()), + )], + ..Default::default() + }, + Err(ToSec1OctetStringError::MissingCoordinate), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("12").unwrap()), + )], + ..Default::default() + }, + Err(ToSec1OctetStringError::MissingCoordinate), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("12").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bytes(hex::decode("34").unwrap()), + ), + ], + ..Default::default() + }, + Err(ToSec1OctetStringError::NotEcKey), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Bytes(hex::decode("12").unwrap()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Text("34".to_string()), + ), + ], + ..Default::default() + }, + Err(ToSec1OctetStringError::InvalidCoordinateType), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::X as i64), + Value::Integer(0x12.into()), + ), + ( + Label::Int(iana::Ec2KeyParameter::Y as i64), + Value::Bytes(hex::decode("34").unwrap()), + ), + ], + ..Default::default() + }, + Err(ToSec1OctetStringError::InvalidCoordinateType), + ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("02aa11bb22cc33dd44").unwrap(), + ) + .unwrap() + .build(), + Ok("02aa11bb22cc33dd44"), + ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("0379f82de731b98a27").unwrap(), + ) + .unwrap() + .build(), + Ok("0379f82de731b98a27"), + ), + ( + CoseKeyBuilder::new_ec2_pub_key_sec1_octet_string( + iana::EllipticCurve::P_256, + &hex::decode("0412345678").unwrap(), + ) + .unwrap() + .build(), + Ok("0412345678"), + ), + ]; + for (i, (key, want)) in tests.iter().enumerate() { + assert_eq!( + key.to_sec1_octet_string().map(hex::encode), + want.map(str::to_string), + "case {i}" + ); + } +}