diff --git a/catalyst-gateway-crates/c509-certificate/Cargo.toml b/catalyst-gateway-crates/c509-certificate/Cargo.toml index f596dff281f..d4fd5ba4ec9 100644 --- a/catalyst-gateway-crates/c509-certificate/Cargo.toml +++ b/catalyst-gateway-crates/c509-certificate/Cargo.toml @@ -57,6 +57,7 @@ bimap = "0.6.3" once_cell = "1.19.0" strum = "0.26.3" strum_macros = "0.26.3" +regex = "1.10.5" [package.metadata.cargo-machete] ignored = ["strum"] diff --git a/catalyst-gateway-crates/c509-certificate/src/c509_attributes/attribute.rs b/catalyst-gateway-crates/c509-certificate/src/c509_attributes/attribute.rs new file mode 100644 index 00000000000..232bacb31ad --- /dev/null +++ b/catalyst-gateway-crates/c509-certificate/src/c509_attributes/attribute.rs @@ -0,0 +1,219 @@ +//! C509 Attribute +//! +//! ```cddl +//! Attribute = ( attributeType: int, attributeValue: text ) // +//! ( attributeType: ~oid, attributeValue: bytes ) // +//! ( attributeType: pen, attributeValue: bytes ) +//! ``` +//! +//! For more information about Attribute, +//! visit [C509 Certificate](https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/) + +use asn1_rs::Oid; +use minicbor::{encode::Write, Decode, Decoder, Encode, Encoder}; + +use super::data::{get_oid_from_int, ATTRIBUTES_LOOKUP}; +use crate::c509_oid::{C509oid, C509oidRegistered}; + +/// A struct of C509 `Attribute` +#[derive(Debug, Clone, PartialEq)] +pub struct Attribute { + /// A registered OID of C509 `Attribute`. + registered_oid: C509oidRegistered, + /// A flag to indicate whether the value can have multiple value. + multi_value: bool, + /// A value of C509 `Attribute` can be a vector of text or bytes. + value: Vec, +} + +impl Attribute { + /// Create a new instance of `Attribute`. + #[must_use] + pub fn new(oid: Oid<'static>) -> Self { + Self { + registered_oid: C509oidRegistered::new(oid, ATTRIBUTES_LOOKUP.get_int_to_oid_table()), + multi_value: false, + value: Vec::new(), + } + } + + /// Add a value to `Attribute`. + pub fn add_value(&mut self, value: AttributeValue) { + self.value.push(value); + } + + /// Get the registered OID of `Attribute`. + pub(crate) fn get_registered_oid(&self) -> &C509oidRegistered { + &self.registered_oid + } + + /// Get the value of `Attribute`. + pub(crate) fn get_value(&self) -> &Vec { + &self.value + } + + /// Set whether `Attribute` can be PEN encoded. + pub(crate) fn set_pen_supported(self) -> Self { + Self { + registered_oid: self.registered_oid.pen_encoded(), + multi_value: self.multi_value, + value: self.value, + } + } + + /// Set whether `Attribute` can have multiple value. + pub(crate) fn set_multi_value(mut self) -> Self { + self.multi_value = true; + self + } +} + +impl Encode<()> for Attribute { + fn encode( + &self, e: &mut Encoder, ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + // Encode CBOR int if available + if let Some(&oid) = self + .registered_oid + .get_table() + .get_map() + .get_by_right(&self.registered_oid.get_c509_oid().get_oid()) + { + e.i16(oid)?; + } else { + // Encode unwrapped CBOR OID or CBOR PEN + self.registered_oid.get_c509_oid().encode(e, ctx)?; + } + + // Check if the attribute value is empty + if self.value.is_empty() { + return Err(minicbor::encode::Error::message("Attribute value is empty")); + } + + // If multi-value attributes, encode it as array + if self.multi_value { + e.array(self.value.len() as u64)?; + } + + // Encode each value in the attribute + for value in &self.value { + value.encode(e, ctx)?; + } + + Ok(()) + } +} + +impl Decode<'_, ()> for Attribute { + fn decode(d: &mut Decoder<'_>, ctx: &mut ()) -> Result { + // Handle CBOR int + let mut attr = if d.datatype()? == minicbor::data::Type::U8 { + let i = d.i16()?; + let oid = get_oid_from_int(i).map_err(minicbor::decode::Error::message)?; + Attribute::new(oid.clone()) + } else { + // Handle unwrapped CBOR OID or CBOR PEN + let c509_oid: C509oid = d.decode()?; + Attribute::new(c509_oid.get_oid()) + }; + + // Handle attribute value + if d.datatype()? == minicbor::data::Type::Array { + // When multi-value attribute + let len = d.array()?.ok_or_else(|| { + minicbor::decode::Error::message("Failed to get array length for attribute value") + })?; + + if len == 0 { + return Err(minicbor::decode::Error::message("Attribute value is empty")); + } + + for _ in 0..len { + attr.add_value(AttributeValue::decode(d, ctx)?); + } + attr = attr.set_multi_value(); + } else { + let value = AttributeValue::decode(d, ctx)?; + attr.add_value(value); + } + Ok(attr) + } +} + +// ------------------AttributeValue---------------------- + +/// An enum of possible value types for `Attribute`. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum AttributeValue { + /// A text string. + Text(String), + /// A byte vector. + Bytes(Vec), +} + +impl Encode<()> for AttributeValue { + fn encode( + &self, e: &mut Encoder, _ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + match self { + AttributeValue::Text(text) => e.str(text)?, + AttributeValue::Bytes(bytes) => e.bytes(bytes)?, + }; + Ok(()) + } +} + +impl Decode<'_, ()> for AttributeValue { + fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result { + match d.datatype()? { + minicbor::data::Type::String => Ok(AttributeValue::Text(d.str()?.to_string())), + minicbor::data::Type::Bytes => Ok(AttributeValue::Bytes(d.bytes()?.to_vec())), + _ => { + Err(minicbor::decode::Error::message( + "Invalid AttributeValue, value should be either String or Bytes", + )) + }, + } + } +} + +// ------------------Test---------------------- + +#[cfg(test)] +mod test_attribute { + use asn1_rs::oid; + + use super::*; + + #[test] + fn encode_decode_attribute_int() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + let mut attribute = Attribute::new(oid!(1.2.840 .113549 .1 .9 .1)); + attribute.add_value(AttributeValue::Text("example@example.com".to_string())); + attribute + .encode(&mut encoder, &mut ()) + .expect("Failed to encode Attribute"); + // Email Address example@example.com: 0x00736578616d706c65406578616d706c652e636f6d + assert_eq!( + hex::encode(buffer.clone()), + "00736578616d706c65406578616d706c652e636f6d" + ); + + let mut decoder = Decoder::new(&buffer); + let attribute_decoded = + Attribute::decode(&mut decoder, &mut ()).expect("Failed to decode Attribute"); + assert_eq!(attribute_decoded, attribute); + } + + #[test] + fn empty_attribute_value() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + let attribute = Attribute::new(oid!(1.2.840 .113549 .1 .9 .1)); + attribute + .encode(&mut encoder, &mut ()) + .expect_err("Failed to encode Attribute"); + } +} diff --git a/catalyst-gateway-crates/c509-certificate/src/c509_attributes/data.rs b/catalyst-gateway-crates/c509-certificate/src/c509_attributes/data.rs new file mode 100644 index 00000000000..b89fb429cb4 --- /dev/null +++ b/catalyst-gateway-crates/c509-certificate/src/c509_attributes/data.rs @@ -0,0 +1,88 @@ +//! Attribute data provides a necessary information for encoding and decoding of C509 +//! Attribute. See [C509 Certificate](https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/) +//! Section 9.3 C509 Attributes Registry for more information. + +use anyhow::Error; +use asn1_rs::{oid, Oid}; +use once_cell::sync::Lazy; + +use crate::tables::IntegerToOidTable; + +/// Type of `Attribute` data +/// Int | OID | Name +type AttributeDataTuple = (i16, Oid<'static>, &'static str); + +/// `Attribute` data table +#[rustfmt::skip] +const ATTRIBUTE_DATA: [AttributeDataTuple; 30] = [ + // Int | OID | Name + (0, oid!(1.2.840.113549.1.9.1), "Email Address"), + (1, oid!(2.5.4.3), "Common Name"), + (2, oid!(2.5.4.4), "Surname"), + (3, oid!(2.5.4.5), "Serial Number"), + (4, oid!(2.5.4.6), "Country"), + (5, oid!(2.5.4.7), "Locality"), + (6, oid!(2.5.4.8), "State or Province"), + (7, oid!(2.5.4.9), "Street Address"), + (8, oid!(2.5.4.10), "Organization"), + (9, oid!(2.5.4.11), "Organizational Unit"), + (10, oid!(2.5.4.12), "Title"), + (11, oid!(2.5.4.15), "Business Category"), + (12, oid!(2.5.4.17), "Postal Code"), + (13, oid!(2.5.4.42), "Given Name"), + (14, oid!(2.5.4.43), "Initials"), + (15, oid!(2.5.4.44), "Generation Qualifier"), + (16, oid!(2.5.4.46), "DN Qualifier"), + (17, oid!(2.5.4.65), "Pseudonym"), + (18, oid!(2.5.4.97), "Organization Identifier"), + (19, oid!(1.3.6.1.4.1.311.60.2.1.1), "Inc. Locality"), + (20, oid!(1.3.6.1.4.1.311.60.2.1.2), "Inc. State or Province"), + (21, oid!(1.3.6.1.4.1.311.60.2.1.3), "Inc. Country"), + (22, oid!(0.9.2342.19200300.100.1.25), "Domain Component"), + (23, oid!(2.5.4.16), "Postal Address"), + (24, oid!(2.5.4.41), "Name"), + (25, oid!(2.5.4.20), "Telephone Number"), + (26, oid!(2.5.4.54), "Directory Management Domain Name"), + (27, oid!(0.9.2342.19200300.100.1.1), "userid"), + (28, oid!(1.2.840.113549.1.9.2), "Unstructured Name"), + (29, oid!(1.2.840.113549.1.9.8), "Unstructured Address"), +]; + +/// A struct of data that contains lookup tables for `Attribute`. +pub(crate) struct AttributeData { + /// A table of integer to OID, provide a bidirectional lookup. + int_to_oid_table: IntegerToOidTable, +} + +impl AttributeData { + /// Get the `IntegerToOidTable`. + pub(crate) fn get_int_to_oid_table(&self) -> &IntegerToOidTable { + &self.int_to_oid_table + } +} + +/// Define static lookup for attributes table +static ATTRIBUTES_TABLES: Lazy = Lazy::new(|| { + let mut int_to_oid_table = IntegerToOidTable::new(); + + for data in ATTRIBUTE_DATA { + int_to_oid_table.add(data.0, data.1); + } + + AttributeData { int_to_oid_table } +}); + +/// Static reference to the `AttributeData` lookup table. +pub(crate) static ATTRIBUTES_LOOKUP: &Lazy = &ATTRIBUTES_TABLES; + +/// Get the OID from the int value. +pub(crate) fn get_oid_from_int(i: i16) -> Result, Error> { + ATTRIBUTES_TABLES + .get_int_to_oid_table() + .get_map() + .get_by_left(&i) + .ok_or(Error::msg(format!( + "OID int not found in the attribute registry table given {i}" + ))) + .cloned() +} diff --git a/catalyst-gateway-crates/c509-certificate/src/c509_attributes/mod.rs b/catalyst-gateway-crates/c509-certificate/src/c509_attributes/mod.rs new file mode 100644 index 00000000000..3808c5a3500 --- /dev/null +++ b/catalyst-gateway-crates/c509-certificate/src/c509_attributes/mod.rs @@ -0,0 +1,127 @@ +//! C509 `Attributes` containing `Attribute` +//! +//! ```cddl +//! Attributes = ( attributeType: int, attributeValue: [+text] ) // +//! ( attributeType: ~oid, attributeValue: [+bytes] ) +//! +//! Use case: +//! ```cddl +//! SubjectDirectoryAttributes = [+Attributes] +//! ``` +//! +//! For more information about `Atributes`, +//! visit [C509 Certificate](https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/) + +use attribute::Attribute; +use minicbor::{encode::Write, Decode, Decoder, Encode, Encoder}; + +pub mod attribute; +mod data; + +/// A struct of C509 `Attributes` containing a vector of `Attribute`. +#[derive(Debug, Clone, PartialEq)] +pub struct Attributes(Vec); + +impl Default for Attributes { + fn default() -> Self { + Self::new() + } +} + +impl Attributes { + /// Create a new instance of `Attributes` as empty vector. + #[must_use] + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Add an `Attribute` to the `Attributes`. + /// and set `Attribute` value to support multiple value. + pub fn add_attr(&mut self, attribute: Attribute) { + self.0.push(attribute.set_multi_value()); + } +} + +impl Encode<()> for Attributes { + fn encode( + &self, e: &mut Encoder, ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + if self.0.is_empty() { + return Err(minicbor::encode::Error::message( + "Attributes should not be empty", + )); + } + e.array(self.0.len() as u64)?; + for attribute in &self.0 { + attribute.encode(e, ctx)?; + } + Ok(()) + } +} + +impl Decode<'_, ()> for Attributes { + fn decode(d: &mut Decoder<'_>, _ctx: &mut ()) -> Result { + let len = d + .array()? + .ok_or_else(|| minicbor::decode::Error::message("Failed to get array length"))?; + if len == 0 { + return Err(minicbor::decode::Error::message("Attributes is empty")); + } + + let mut attributes = Attributes::new(); + + for _ in 0..len { + let attribute = Attribute::decode(d, &mut ())?; + attributes.add_attr(attribute); + } + + Ok(attributes) + } +} + +// ------------------Test---------------------- + +#[cfg(test)] +mod test_attributes { + use asn1_rs::oid; + use attribute::AttributeValue; + + use super::*; + + #[test] + fn encode_decode_attributes_int() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + let mut attr = Attribute::new(oid!(1.2.840 .113549 .1 .9 .1)); + attr.add_value(AttributeValue::Text("example@example.com".to_string())); + attr.add_value(AttributeValue::Text("example@example.com".to_string())); + let mut attributes = Attributes::new(); + attributes.add_attr(attr); + attributes + .encode(&mut encoder, &mut ()) + .expect("Failed to encode Attributes"); + // 1 Attribute value (array len 1): 0x81 + // Email Address: 0x00 + // Attribute value (array len 2): 0x82 + // example@example.com: 0x736578616d706c65406578616d706c652e636f6d + assert_eq!( + hex::encode(buffer.clone()), + "810082736578616d706c65406578616d706c652e636f6d736578616d706c65406578616d706c652e636f6d" + ); + + let mut decoder = Decoder::new(&buffer); + let attribute_decoded = + Attributes::decode(&mut decoder, &mut ()).expect("Failed to decode Attributes"); + assert_eq!(attribute_decoded, attributes); + } + + #[test] + fn empty_attributes() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + let attributes = Attributes::new(); + attributes + .encode(&mut encoder, &mut ()) + .expect_err("Failed to encode Attributes"); + } +} diff --git a/catalyst-gateway-crates/c509-certificate/src/c509_general_names/data.rs b/catalyst-gateway-crates/c509-certificate/src/c509_general_names/data.rs index 72cc7e906fd..71ff7649492 100644 --- a/catalyst-gateway-crates/c509-certificate/src/c509_general_names/data.rs +++ b/catalyst-gateway-crates/c509-certificate/src/c509_general_names/data.rs @@ -32,7 +32,7 @@ const GENERAL_NAME_DATA: [GeneralNameDataTuple; 10] = [ (0, Gntr::OtherName, Gnvt::OtherNameHWModuleName), (1, Gntr::Rfc822Name, Gnvt::Text), (2, Gntr::DNSName, Gnvt::Text), - (4, Gntr::DirectoryName, Gnvt::Unsupported), + (4, Gntr::DirectoryName, Gnvt::Name), (6, Gntr::UniformResourceIdentifier, Gnvt::Text), (7, Gntr::IPAddress, Gnvt::Bytes), (8, Gntr::RegisteredID, Gnvt::Oid), diff --git a/catalyst-gateway-crates/c509-certificate/src/c509_general_names/general_name.rs b/catalyst-gateway-crates/c509-certificate/src/c509_general_names/general_name.rs index 56da450b1b5..b1b7928b3e4 100644 --- a/catalyst-gateway-crates/c509-certificate/src/c509_general_names/general_name.rs +++ b/catalyst-gateway-crates/c509-certificate/src/c509_general_names/general_name.rs @@ -12,7 +12,7 @@ use super::{ data::{get_gn_from_int, get_gn_value_type_from_int, get_int_from_gn}, other_name_hw_module::OtherNameHardwareModuleName, }; -use crate::c509_oid::C509oid; +use crate::{c509_name::Name, c509_oid::C509oid}; /// A struct represents a `GeneralName`. /// ```cddl @@ -99,7 +99,7 @@ pub enum GeneralNameTypeRegistry { /// A dNSName. DNSName, /// A directoryName. - DirectoryName, // Name + DirectoryName, /// A uniformResourceIdentifier. UniformResourceIdentifier, /// An iPAddress. @@ -112,7 +112,7 @@ pub enum GeneralNameTypeRegistry { /// An enum of possible value types for `GeneralName`. #[allow(clippy::module_name_repetitions)] -#[derive(Debug, PartialEq, Clone, Eq, Hash, EnumDiscriminants)] +#[derive(Debug, PartialEq, Clone, EnumDiscriminants)] #[strum_discriminants(name(GeneralNameValueType))] pub enum GeneralNameValue { /// A text string. @@ -123,6 +123,8 @@ pub enum GeneralNameValue { Bytes(Vec), /// An OID Oid(C509oid), + /// Name + Name(Name), /// An unsupported value. Unsupported, } @@ -156,6 +158,9 @@ impl Encode<()> for GeneralNameValue { GeneralNameValue::OtherNameHWModuleName(value) => { value.encode(e, ctx)?; }, + GeneralNameValue::Name(value) => { + Name::encode(value, e, ctx)?; + }, GeneralNameValue::Unsupported => { return Err(minicbor::encode::Error::message( "Cannot encode unsupported GeneralName value", @@ -186,6 +191,10 @@ where C: GeneralNameValueTrait + Debug let value = OtherNameHardwareModuleName::decode(d, &mut ())?; Ok(GeneralNameValue::OtherNameHWModuleName(value)) }, + GeneralNameValueType::Name => { + let value = Name::decode(d, &mut ())?; + Ok(GeneralNameValue::Name(value)) + }, GeneralNameValueType::Unsupported => { Err(minicbor::decode::Error::message( "Cannot decode Unsupported GeneralName value", diff --git a/catalyst-gateway-crates/c509-certificate/src/c509_name/mod.rs b/catalyst-gateway-crates/c509-certificate/src/c509_name/mod.rs new file mode 100644 index 00000000000..9c4f18540e8 --- /dev/null +++ b/catalyst-gateway-crates/c509-certificate/src/c509_name/mod.rs @@ -0,0 +1,484 @@ +//! C509 type Name +//! +//! ```cddl +//! Name = [ * RelativeDistinguishedName ] / text / bytes +//! RelativeDistinguishedName = Attribute / [ 2* Attribute ] +//! Attribute = ( attributeType: int, attributeValue: text ) // +//! ( attributeType: ~oid, attributeValue: bytes ) // +//! ( attributeType: pen, attributeValue: bytes ) +//! ``` +//! +//! For more information about Name, +//! visit [C509 Certificate](https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/) + +// cspell: words rdns + +mod rdn; +use asn1_rs::{oid, Oid}; +use minicbor::{encode::Write, Decode, Decoder, Encode, Encoder}; +use rdn::RelativeDistinguishedName; +use regex::Regex; + +use crate::c509_attributes::attribute::{Attribute, AttributeValue}; + +/// OID of `CommonName` attribute. +const COMMON_NAME_OID: Oid<'static> = oid!(2.5.4 .3); +/// EUI-64 prefix. +const EUI64_PREFIX: u8 = 0x01; +/// Hex prefix. +const HEX_PREFIX: u8 = 0x00; +/// Total length of CBOR byte for EUI-64. +const EUI64_LEN: usize = 9; +/// Total length of CBOR byte for EUI-64 mapped from a 48-bit MAC address. +const EUI64_MAC_LEN: usize = 7; + +// ------------------Name---------------------- + +/// A struct of C509 Name with `NameValue`. +#[derive(Debug, Clone, PartialEq)] +pub struct Name(NameValue); + +impl Name { + /// Create a new instance of `Name` its value. + #[must_use] + pub fn new(value: NameValue) -> Self { + Self(value) + } +} + +impl Encode<()> for Name { + fn encode( + &self, e: &mut Encoder, ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, ctx) + } +} + +impl Decode<'_, ()> for Name { + fn decode(d: &mut Decoder<'_>, ctx: &mut ()) -> Result { + NameValue::decode(d, ctx).map(Name::new) + } +} + +// ------------------NameValue---------------------- + +/// An enum of possible value types for `Name`. +#[derive(Debug, Clone, PartialEq)] +pub enum NameValue { + /// A relative distinguished name. + RelativeDistinguishedName(RelativeDistinguishedName), + /// A text. + Text(String), + /// bytes. + Bytes(Vec), +} + +impl Encode<()> for NameValue { + fn encode( + &self, e: &mut Encoder, ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + match self { + NameValue::RelativeDistinguishedName(rdn) => { + let attr = rdn.get_attributes(); + let attr_first = attr.first().ok_or(minicbor::encode::Error::message( + "Cannot get the first Attribute", + ))?; + // If Name contains a single Attribute of type CommonName + if attr.len() == 1 + && attr_first.get_registered_oid().get_c509_oid().get_oid() == COMMON_NAME_OID + { + // Get the value of the attribute + let cn_value = + attr_first + .get_value() + .first() + .ok_or(minicbor::encode::Error::message( + "Cannot get the first Attribute value", + ))?; + + encode_cn_value(e, cn_value)?; + } else { + rdn.encode(e, ctx)?; + } + }, + NameValue::Text(text) => { + e.str(text)?; + }, + NameValue::Bytes(bytes) => { + e.bytes(bytes)?; + }, + } + Ok(()) + } +} + +impl Decode<'_, ()> for NameValue { + fn decode(d: &mut Decoder<'_>, ctx: &mut ()) -> Result { + match d.datatype()? { + minicbor::data::Type::Array => { + Ok(NameValue::RelativeDistinguishedName( + RelativeDistinguishedName::decode(d, ctx)?, + )) + }, + // If Name is a text string, the attribute is a CommonName + minicbor::data::Type::String => Ok(create_rdn_with_cn_attr(d.str()?.to_string())), + minicbor::data::Type::Bytes => decode_bytes(d), + _ => { + Err(minicbor::decode::Error::message( + "Name must be an array, text or bytes", + )) + }, + } + } +} + +/// Encode common name value. +fn encode_cn_value( + e: &mut Encoder, cn_value: &AttributeValue, +) -> Result<(), minicbor::encode::Error> { + let hex_regex = Regex::new(r"^[0-9a-f]+$").map_err(minicbor::encode::Error::message)?; + let eui64_regex = + Regex::new(r"^([0-9A-F]{2}-){7}[0-9A-F]{2}$").map_err(minicbor::encode::Error::message)?; + let mac_eui64_regex = Regex::new(r"^([0-9A-F]{2}-){3}FF-FE-([0-9A-F]{2}-){2}[0-9A-F]{2}$") + .map_err(minicbor::encode::Error::message)?; + + match cn_value { + AttributeValue::Text(s) => { + // If the text string has an even length ≥ 2 and contains only the + // symbols '0'–'9' or 'a'–'f', it is encoded as a CBOR byte + // string, prefixed with an initial byte set to '00' + if hex_regex.is_match(s) && s.len() % 2 == 0 { + let decoded_bytes = hex::decode(s).map_err(minicbor::encode::Error::message)?; + e.bytes(&[&[HEX_PREFIX], &decoded_bytes[..]].concat())?; + + // An EUI-64 mapped from a 48-bit MAC address (i.e., of the form + // "HH-HH-HH-FF-FE-HH-HH-HH) is encoded as a CBOR byte string prefixed with an + // initial byte set to '01', for a total length of 7. + } else if mac_eui64_regex.is_match(s) { + let clean_name = s.replace('-', ""); + let decoded_bytes = + hex::decode(clean_name).map_err(minicbor::encode::Error::message)?; + let chunk2 = decoded_bytes + .get(..3) + .ok_or(minicbor::encode::Error::message( + "Failed to get MAC EUI-64 bytes index 0 to 2", + ))?; + let chunk3 = decoded_bytes + .get(5..) + .ok_or(minicbor::encode::Error::message( + "Failed to get MAC EUI-64 bytes index 5 to 6", + ))?; + e.bytes(&[&[EUI64_PREFIX], chunk2, chunk3].concat())?; + + // an EUI-64 of the form "HH-HH-HH-HH-HH-HH-HH-HH" where 'H' + // is one of the symbols '0'–'9' or 'A'–'F' it is encoded as a + // CBOR byte string prefixed with an initial byte set to '01', for a total + // length of 9. + } else if eui64_regex.is_match(s) { + let clean_name = s.replace('-', ""); + let decoded_bytes = + hex::decode(clean_name).map_err(minicbor::encode::Error::message)?; + e.bytes(&[&[EUI64_PREFIX], &decoded_bytes[..]].concat())?; + } else { + e.str(s)?; + } + }, + AttributeValue::Bytes(_) => { + return Err(minicbor::encode::Error::message( + "CommonName attribute value must be a text string", + )); + }, + } + Ok(()) +} + +/// Format EUI bytes. +fn formatted_eui_bytes(data: &[u8]) -> String { + data.iter() + .map(|b| format!("{b:02X}")) + .collect::>() + .join("-") +} + +/// Decode bytes. +fn decode_bytes(d: &mut Decoder<'_>) -> Result { + let bytes = d.bytes()?; + + let first_i = bytes.first().ok_or(minicbor::decode::Error::message( + "Failed to get the first index of bytes", + ))?; + + // Bytes prefix + match *first_i { + // 0x00 for hex + HEX_PREFIX => decode_hex_cn_bytes(bytes), + // 0x01 for EUI + EUI64_PREFIX => decode_eui_cn_bytes(bytes), + _ => Ok(NameValue::Bytes(bytes.to_vec())), + } +} + +/// Decode common name hex bytes. +fn decode_hex_cn_bytes(bytes: &[u8]) -> Result { + let text = hex::encode(bytes.get(1..).ok_or(minicbor::decode::Error::message( + "Failed to get hex bytes index", + ))?); + Ok(create_rdn_with_cn_attr(text)) +} + +/// Decode common name EUI-64 bytes. +fn decode_eui_cn_bytes(bytes: &[u8]) -> Result { + // Check the length of the bytes to determine what EUI type it is + match bytes.len() { + // EUI-64 mapped from a 48-bit MAC address + EUI64_MAC_LEN => { + let chunk1 = bytes.get(1..4).ok_or(minicbor::decode::Error::message( + "Failed to get EUI-64 bytes index 1 to 3", + ))?; + let chunk4 = bytes.get(4..).ok_or(minicbor::decode::Error::message( + "Failed to get EUI-64 bytes index 4 to 7", + ))?; + // Turn it into HH-HH-HH-FF-FE-HH-HH-HH + let data = [chunk1, &[0xFF], &[0xFE], chunk4].concat(); + let text = formatted_eui_bytes(&data); + Ok(create_rdn_with_cn_attr(text)) + }, + // EUI-64 + EUI64_LEN => { + let text = formatted_eui_bytes(bytes.get(1..).ok_or( + minicbor::decode::Error::message("Failed to get EUI-64 bytes index"), + )?); + Ok(create_rdn_with_cn_attr(text)) + }, + _ => { + Err(minicbor::decode::Error::message( + "EUI-64 or MAC address must be 7 or 9 bytes", + )) + }, + } +} + +/// Create a relative distinguished name with attribute common name from string. +fn create_rdn_with_cn_attr(text: String) -> NameValue { + let mut attr = Attribute::new(COMMON_NAME_OID); + attr.add_value(AttributeValue::Text(text)); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + NameValue::RelativeDistinguishedName(rdn) +} + +// ------------------Test---------------------- + +#[cfg(test)] +mod test_name { + use super::*; + use crate::c509_attributes::attribute::Attribute; + + #[test] + fn encode_decode_type_name_cn() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(2.5.4 .3)); + attr.add_value(AttributeValue::Text("RFC test CA".to_string())); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + + // Test data from https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/ + // A.1.1. Example C509 Certificate Encoding + assert_eq!(hex::encode(buffer.clone()), "6b5246432074657374204341"); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } + + #[test] + fn encode_decode_type_name_hex() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(2.5.4 .3)); + attr.add_value(AttributeValue::Text("000123abcd".to_string())); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + + // Bytes of length 6: 0x46 + // Prefix of CommonName hex: 0x00 + // Bytes 000123abcd: 0x000123abcd + assert_eq!(hex::encode(buffer.clone()), "4600000123abcd"); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } + + #[test] + fn encode_decode_type_name_hex_cap() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(2.5.4 .3)); + attr.add_value(AttributeValue::Text("000123ABCD".to_string())); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + + // String of len 10: 0x6a + // String 000123abcd: 30303031323341424344 + assert_eq!(hex::encode(buffer.clone()), "6a30303031323341424344"); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } + + #[test] + fn encode_decode_type_name_cn_eui_mac() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(2.5.4 .3)); + attr.add_value(AttributeValue::Text("01-23-45-FF-FE-67-89-AB".to_string())); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + + // Test data from https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/ + // A.1. Example RFC 7925 profiled X.509 Certificate + assert_eq!(hex::encode(buffer.clone()), "47010123456789ab"); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } + + #[test] + fn encode_decode_type_name_cn_eui_mac_un_cap() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(2.5.4 .3)); + attr.add_value(AttributeValue::Text("01-23-45-ff-fe-67-89-AB".to_string())); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + + // String of len 23: 0x77 + // "01-23-45-ff-fe-67-89-AB": 0x7730312d32332d34352d66662d66652d36372d38392d4142 + assert_eq!( + hex::encode(buffer.clone()), + "7730312d32332d34352d66662d66652d36372d38392d4142" + ); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } + + #[test] + fn encode_decode_type_name_cn_eui() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(2.5.4 .3)); + attr.add_value(AttributeValue::Text("01-23-45-67-89-AB-00-01".to_string())); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + + assert_eq!(hex::encode(buffer.clone()), "49010123456789ab0001"); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } + + #[test] + fn encode_decode_type_name_cn_eui_un_cap() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(2.5.4 .3)); + attr.add_value(AttributeValue::Text("01-23-45-67-89-ab-00-01".to_string())); + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + + // String of len 23: 0x77 + // "01-23-45-67-89-ab-00-01": 0x7730312d32332d34352d36372d38392d61622d30302d3031 + assert_eq!( + hex::encode(buffer.clone()), + "7730312d32332d34352d36372d38392d61622d30302d3031" + ); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } + + #[test] + fn encode_decode_type_name_rdns() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + // Test data from https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/ + // A.2. Example IEEE 802.1AR profiled X.509 Certificate + // Issuer: C=US, ST=CA, O=Example Inc, OU=certification, CN=802.1AR CA + let mut attr1 = Attribute::new(oid!(2.5.4 .6)); + attr1.add_value(AttributeValue::Text("US".to_string())); + let mut attr2 = Attribute::new(oid!(2.5.4 .8)); + attr2.add_value(AttributeValue::Text("CA".to_string())); + let mut attr3 = Attribute::new(oid!(2.5.4 .10)); + attr3.add_value(AttributeValue::Text("Example Inc".to_string())); + let mut attr4 = Attribute::new(oid!(2.5.4 .11)); + attr4.add_value(AttributeValue::Text("certification".to_string())); + let mut attr5 = Attribute::new(oid!(2.5.4 .3)); + attr5.add_value(AttributeValue::Text("802.1AR CA".to_string())); + + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr1); + rdn.add_attr(attr2); + rdn.add_attr(attr3); + rdn.add_attr(attr4); + rdn.add_attr(attr5); + + let name = Name::new(NameValue::RelativeDistinguishedName(rdn)); + + name.encode(&mut encoder, &mut ()) + .expect("Failed to encode Name"); + assert_eq!( + hex::encode(buffer.clone()), + "8a0462555306624341086b4578616d706c6520496e63096d63657274696669636174696f6e016a3830322e314152204341" + ); + + let mut decoder = Decoder::new(&buffer); + let name_decoded = Name::decode(&mut decoder, &mut ()).expect("Failed to decode Name"); + assert_eq!(name_decoded, name); + } +} diff --git a/catalyst-gateway-crates/c509-certificate/src/c509_name/rdn.rs b/catalyst-gateway-crates/c509-certificate/src/c509_name/rdn.rs new file mode 100644 index 00000000000..3c63c3bf4aa --- /dev/null +++ b/catalyst-gateway-crates/c509-certificate/src/c509_name/rdn.rs @@ -0,0 +1,169 @@ +//! C509 Relative Distinguished Name +//! +//! For more information about `RelativeDistinguishedName`, +//! visit [C509 Certificate](https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/) + +// cspell: words rdns + +use minicbor::{encode::Write, Decode, Decoder, Encode, Encoder}; + +use crate::c509_attributes::attribute::Attribute; + +/// A struct represents a Relative Distinguished Name containing vector of `Attribute`. +/// +/// ```cddl +/// RelativeDistinguishedName = Attribute / [ 2* Attribute ] +/// ``` +#[derive(Debug, Clone, PartialEq)] +pub struct RelativeDistinguishedName(Vec); + +impl Default for RelativeDistinguishedName { + fn default() -> Self { + Self::new() + } +} + +impl RelativeDistinguishedName { + /// Create a new instance of `RelativeDistinguishedName` as empty vector. + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Add an `Attribute` to the `RelativeDistinguishedName`. + pub fn add_attr(&mut self, attribute: Attribute) { + // RelativeDistinguishedName support pen encoding + self.0.push(attribute.set_pen_supported()); + } + + /// Get the a vector of `Attribute`. + pub fn get_attributes(&self) -> &Vec { + &self.0 + } +} + +impl Encode<()> for RelativeDistinguishedName { + // ```cddl + // RelativeDistinguishedName = Attribute / [ 2* Attribute ] + // ``` + fn encode( + &self, e: &mut Encoder, ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + // Should contain >= 1 attribute + if self.0.is_empty() { + return Err(minicbor::encode::Error::message( + "RelativeDistinguishedName should not be empty", + )); + } + + if self.0.len() == 1 { + self.0.first().encode(e, ctx)?; + } else { + // The attribute type should be included in array too + e.array(self.0.len() as u64 * 2)?; + for attr in &self.0 { + attr.encode(e, ctx)?; + } + } + Ok(()) + } +} + +impl Decode<'_, ()> for RelativeDistinguishedName { + fn decode(d: &mut Decoder<'_>, ctx: &mut ()) -> Result { + let mut rdn = RelativeDistinguishedName::new(); + + match d.datatype()? { + minicbor::data::Type::Array => { + let len = d.array()?.ok_or(minicbor::decode::Error::message( + "Failed to get array length for relative distinguished name", + ))?; + // Should contain >= 1 attribute + if len == 0 { + return Err(minicbor::decode::Error::message( + "RelativeDistinguishedName should not be empty", + )); + } + // The attribute type is included in an array, so divide by 2 + for _ in 0..len / 2 { + rdn.add_attr(Attribute::decode(d, ctx)?); + } + }, + _ => rdn.add_attr(Attribute::decode(d, ctx)?), + } + Ok(rdn) + } +} + +// -------------------Test---------------------- + +#[cfg(test)] +mod test_relative_distinguished_name { + + use asn1_rs::oid; + + use super::*; + use crate::c509_attributes::attribute::AttributeValue; + + #[test] + fn encode_decode_rdn() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr = Attribute::new(oid!(1.2.840 .113549 .1 .9 .1)); + attr.add_value(AttributeValue::Text("example@example.com".to_string())); + + let mut rdn = RelativeDistinguishedName::new(); + rdn.add_attr(attr); + rdn.encode(&mut encoder, &mut ()) + .expect("Failed to encode RDN"); + // Email Address: 0x00 + // "example@example.como": 736578616d706c65406578616d706c652e636f6d + assert_eq!( + hex::encode(buffer.clone()), + "00736578616d706c65406578616d706c652e636f6d" + ); + + let mut decoder = Decoder::new(&buffer); + let rdn_decoded = RelativeDistinguishedName::decode(&mut decoder, &mut ()) + .expect("Failed to decode RelativeDistinguishedName"); + assert_eq!(rdn_decoded, rdn); + } + + #[test] + fn encode_decode_rdns() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + + let mut attr1 = Attribute::new(oid!(1.2.840 .113549 .1 .9 .1)); + attr1.add_value(AttributeValue::Text("example@example.com".to_string())); + let mut attr2 = Attribute::new(oid!(2.5.4 .3)); + attr2.add_value(AttributeValue::Text("example".to_string())); + + let mut rdns = RelativeDistinguishedName::new(); + rdns.add_attr(attr1); + rdns.add_attr(attr2); + + rdns.encode(&mut encoder, &mut ()) + .expect("Failed to encode RDN"); + // Array of 2 attributes: 0x84 + // Email Address example@example.com: 0x00736578616d706c65406578616d706c652e636f6d + // Common Name example: 0x01676578616d706c65 + assert_eq!( + hex::encode(buffer.clone()), + "8400736578616d706c65406578616d706c652e636f6d01676578616d706c65" + ); + let mut decoder = Decoder::new(&buffer); + let rdn_decoded = RelativeDistinguishedName::decode(&mut decoder, &mut ()) + .expect("Failed to decode RelativeDistinguishedName"); + assert_eq!(rdn_decoded, rdns); + } + + #[test] + fn empty_rdn() { + let mut buffer = Vec::new(); + let mut encoder = Encoder::new(&mut buffer); + let rdn = RelativeDistinguishedName::new(); + rdn.encode(&mut encoder, &mut ()) + .expect_err("Failed to encode RDN"); + } +} diff --git a/catalyst-gateway-crates/c509-certificate/src/lib.rs b/catalyst-gateway-crates/c509-certificate/src/lib.rs index 066482a57c4..41a76e0be34 100644 --- a/catalyst-gateway-crates/c509-certificate/src/lib.rs +++ b/catalyst-gateway-crates/c509-certificate/src/lib.rs @@ -6,8 +6,10 @@ //! Please refer to the [C509 Certificate](https://datatracker.ietf.org/doc/draft-ietf-cose-cbor-encoded-cert/09/) for more information. use wasm_bindgen::prelude::wasm_bindgen; +pub mod c509_attributes; pub mod c509_extensions; pub mod c509_general_names; +pub mod c509_name; pub mod c509_oid; mod tables;