diff --git a/Cargo.lock b/Cargo.lock index ad8eaf090..3abef14ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,6 +290,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "const-oid" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -414,6 +420,17 @@ dependencies = [ "log", ] +[[package]] +name = "facet-asn1" +version = "0.1.0" +dependencies = [ + "const-oid", + "facet", + "facet-core", + "facet-reflect", + "facet-testhelpers 0.17.5", +] + [[package]] name = "facet-bench" version = "0.23.0" @@ -436,6 +453,7 @@ dependencies = [ "bytes", "camino", "chrono", + "const-oid", "eyre", "facet-testhelpers 0.17.5", "impls", diff --git a/Cargo.toml b/Cargo.toml index 7f0ef2676..d8df5a180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ # # formats / ecosystem "facet-args", + "facet-asn1", "facet-csv", "facet-json", "facet-msgpack", diff --git a/facet-asn1/CHANGELOG.md b/facet-asn1/CHANGELOG.md new file mode 100644 index 000000000..6cab0ca76 --- /dev/null +++ b/facet-asn1/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- added facet-asn1 +- added support for OCTET STRING and OBJECT IDENTIFIER diff --git a/facet-asn1/Cargo.toml b/facet-asn1/Cargo.toml new file mode 100644 index 000000000..1a600a0b3 --- /dev/null +++ b/facet-asn1/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "facet-asn1" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "ASN.1 serialization and deserialization for Facet types" +keywords = ["asn1", "serialization", "deserialization", "reflection", "facet"] +categories = ["encoding", "parsing", "data-structures"] + +[features] +# TODO features for different encoding rules +std = ["alloc", "facet-core/std", "facet-reflect/std"] +alloc = ["facet-core/alloc", "facet-reflect/alloc"] +default = ["std", "const-oid"] +const-oid = ["facet-core/const-oid", "dep:const-oid"] + +[dependencies] +facet-core = { version = "0.27.12", path = "../facet-core", default-features = false } +facet-reflect = { version = "0.27.12", path = "../facet-reflect", default-features = false } +const-oid = { version = "0.10.1", optional = true } + +[dev-dependencies] +facet = { path = "../facet" } +facet-testhelpers = { path = "../facet-testhelpers" } +const-oid = { version = "0.10.1", features = ["db"] } \ No newline at end of file diff --git a/facet-asn1/README.md b/facet-asn1/README.md new file mode 100644 index 000000000..8f726d43c --- /dev/null +++ b/facet-asn1/README.md @@ -0,0 +1,104 @@ +

+ + + + + Facet logo - a reflection library for Rust + +

+ +[![Coverage Status](https://coveralls.io/repos/github/facet-rs/facet/badge.svg?branch=main)](https://coveralls.io/github/facet-rs/facet?branch=main) +[![free of syn](https://img.shields.io/badge/free%20of-syn-hotpink)](https://github.com/fasterthanlime/free-of-syn) +[![crates.io](https://img.shields.io/crates/v/facet-asn1.svg)](https://crates.io/crates/facet-asn1) +[![documentation](https://docs.rs/facet-asn1/badge.svg)](https://docs.rs/facet-asn1) +[![MIT/Apache-2.0 licensed](https://img.shields.io/crates/l/facet-asn1.svg)](./LICENSE) +[![Discord](https://img.shields.io/discord/1379550208551026748?logo=discord&label=discord)](https://discord.gg/JhD7CwCJ8F) + +_Logo by [Misiasart](https://misiasart.com/)_ + +Thanks to all individual and corporate sponsors, without whom this work could not exist: + +

+ + +Ko-fi + + + + +GitHub Sponsors + + + + +Patreon + + + + +Zed + + + + +Depot + +

+ +# facet-asn1 + +A `#![no_std]` ASN.1 serializer and deserializer based on facet + +Currently supports Distinguished Encoding Rules (DER) only + +## Basic Types + +| ASN.1 Type | Rust | +|-------------------|----------------------------------------------------------------------| +| BOOLEAN | `bool` | +| INTEGER | `i8`, `i16`, `i32`, or `i64` | +| OCTET STRING | `Vec` | +| NULL | Any unit struct | +| OBJECT IDENTIFIER | `const_oid::ObjectIdentifier` (with the `const-oid` feature enabled) | +| REAL | `f32` or `f64` | +| UTF8String | `String` | +| CHOICE | `enum` | +| SEQUENCE | `struct` | + +## Other ASN.1 Types + +Newtype structs using the `facet::Shape::type_tag` property can be used to create other basic types without any content validation: + +```rust +#[derive(Debug, Clone, Facet, PartialEq, Eq)] +#[facet(type_tag = "IA5String", transparent)] +struct IA5String(String); +``` + +## Context Specific Type Tags + +You can also set context specific BER/DER tags to a given number. Implicit tags must be set as transparent. + +```rust +// ImplicitString ::= [5] IMPLICIT UTF8String +#[derive(Debug, Facet, PartialEq, Eq)] +#[facet(type_tag = "5", transparent)] +struct ImplicitString(String); + +// ExplciitString ::= [5] EXPLICIT UTF8String +#[derive(Debug, Facet, PartialEq, Eq)] +#[facet(type_tag = "5")] +struct ExplicitString(String); +``` + +The tag classes `UNIVERSAL`, `APPLICATION`, and `PRIVATE` are also supported in `type_tag`s for greater flexibility. + + +## License + +Licensed under either of: + +- Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/facet-rs/facet/blob/main/LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](https://github.com/facet-rs/facet/blob/main/LICENSE-MIT) or ) + +at your option. diff --git a/facet-asn1/README.md.in b/facet-asn1/README.md.in new file mode 100644 index 000000000..aaeb8678b --- /dev/null +++ b/facet-asn1/README.md.in @@ -0,0 +1,48 @@ +# facet-asn1 + +A `#![no_std]` ASN.1 serializer and deserializer based on facet + +Currently supports Distinguished Encoding Rules (DER) only + +## Basic Types + +| ASN.1 Type | Rust | +|-------------------|----------------------------------------------------------------------| +| BOOLEAN | `bool` | +| INTEGER | `i8`, `i16`, `i32`, or `i64` | +| OCTET STRING | `Vec` | +| NULL | Any unit struct | +| OBJECT IDENTIFIER | `const_oid::ObjectIdentifier` (with the `const-oid` feature enabled) | +| REAL | `f32` or `f64` | +| UTF8String | `String` | +| CHOICE | `enum` | +| SEQUENCE | `struct` | + +## Other ASN.1 Types + +Newtype structs using the `facet::Shape::type_tag` property can be used to create other basic types without any content validation: + +```rust +#[derive(Debug, Clone, Facet, PartialEq, Eq)] +#[facet(type_tag = "IA5String", transparent)] +struct IA5String(String); +``` + +## Context Specific Type Tags + +You can also set context specific BER/DER tags to a given number. Implicit tags must be set as transparent. + +```rust +// ImplicitString ::= [5] IMPLICIT UTF8String +#[derive(Debug, Facet, PartialEq, Eq)] +#[facet(type_tag = "5", transparent)] +struct ImplicitString(String); + +// ExplciitString ::= [5] EXPLICIT UTF8String +#[derive(Debug, Facet, PartialEq, Eq)] +#[facet(type_tag = "5")] +struct ExplicitString(String); +``` + +The tag classes `UNIVERSAL`, `APPLICATION`, and `PRIVATE` are also supported in `type_tag`s for greater flexibility. + diff --git a/facet-asn1/src/lib.rs b/facet-asn1/src/lib.rs new file mode 100644 index 000000000..49c3cce46 --- /dev/null +++ b/facet-asn1/src/lib.rs @@ -0,0 +1,1015 @@ +#![no_std] +#![warn(missing_docs)] +#![forbid(unsafe_code)] +#![doc = include_str!("../README.md")] + +#[macro_use] +extern crate alloc; + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use core::{f64, num::FpCategory, str::FromStr}; + +#[cfg(feature = "const-oid")] +use const_oid::ObjectIdentifier; + +use facet_core::{ + Def, Facet, NumberBits, ScalarAffinity, Shape, ShapeAttribute, StructKind, Type, UserType, +}; +use facet_reflect::{HasFields, HeapValue, Partial, Peek}; + +mod tag; + +const ASN1_TYPE_TAG_BOOLEAN: u8 = 0x01; +const ASN1_TYPE_TAG_INTEGER: u8 = 0x02; +const ASN1_TYPE_TAG_OCTET_STRING: u8 = 0x04; +const ASN1_TYPE_TAG_NULL: u8 = 0x05; +const ASN1_TYPE_TAG_OBJECT_IDENTIFIER: u8 = 0x06; +const ASN1_TYPE_TAG_REAL: u8 = 0x09; +const ASN1_TYPE_TAG_UTF8STRING: u8 = 0x0C; +const ASN1_TYPE_TAG_SEQUENCE: u8 = 0x10; + +const ASN1_FORM_CONSTRUCTED: u8 = 0b1 << 5; + +const ASN1_REAL_INFINITY: u8 = 0b01000000; +const ASN1_REAL_NEG_INFINITY: u8 = 0b01000001; +const ASN1_REAL_NAN: u8 = 0b01000010; +const ASN1_REAL_NEG_ZERO: u8 = 0b01000011; + +const F64_MANTISSA_MASK: u64 = 0b1111111111111111111111111111111111111111111111111111; + +/// `no_std` compatible Write trait used by the ASN.1 serializer. +pub trait Asn1Write { + /// Write all these bytes to the writer. + fn write(&mut self, buf: &[u8]); + + /// If the writer supports it, reserve space for `len` additional bytes. + fn reserve(&mut self, additional: usize); +} + +impl Asn1Write for &mut Vec { + fn write(&mut self, buf: &[u8]) { + self.extend(buf) + } + + fn reserve(&mut self, additional: usize) { + Vec::reserve(self, additional) + } +} + +impl Asn1Write for Vec { + fn write(&mut self, buf: &[u8]) { + self.extend(buf) + } + + fn reserve(&mut self, additional: usize) { + Vec::reserve(self, additional) + } +} + +#[derive(Debug)] +#[non_exhaustive] +/// Errors when serializing to an ASN.1 format +pub enum Asn1SerError { + /// Invalid type tag + TypeTag(tag::Asn1TagError), + /// Unsupported shape + UnsupportedShape, + /// Enum unit variant discriminant too large + InvalidDiscriminant(Option), +} + +impl core::fmt::Display for Asn1SerError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Asn1SerError::TypeTag(error) => write!(f, "Bad type_tag: {}", error), + Asn1SerError::UnsupportedShape => write!(f, "Unsupported shape"), + Asn1SerError::InvalidDiscriminant(d) => { + if let Some(d) = d { + write!(f, "Enum variant discriminant invalid: {}", d) + } else { + write!(f, "Enum variant discriminant invalid") + } + } + } + } +} + +impl core::error::Error for Asn1SerError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Asn1SerError::TypeTag(error) => Some(error), + _ => None, + } + } +} + +/// Serialize a Facet type to ASN.1 DER bytes +pub fn to_vec_der<'f, F: Facet<'f>>(value: &'f F) -> Result, Asn1SerError> { + let mut buffer = Vec::new(); + let peek = Peek::new(value); + let mut serializer = DerSerializer { + writer: &mut buffer, + }; + serialize_der_recursive(peek, &mut serializer, None)?; + Ok(buffer) +} + +struct DerSerializer<'w, W: Asn1Write> { + writer: &'w mut W, +} + +impl<'w, W: Asn1Write> DerSerializer<'w, W> { + fn serialize_tlv(&mut self, tag: u8, value: &[u8]) { + if value.len() < 128 { + self.writer.write(&[tag, value.len() as u8]); + } else { + let len_bytes_len = core::cmp::max(value.len() / 256, 1); + let len_bytes = value.len().to_be_bytes(); + self.writer.write(&[tag, len_bytes_len as u8]); + self.writer + .write(&len_bytes[len_bytes.len() - len_bytes_len..]); + } + self.writer.write(value); + } + + fn serialize_i64(&mut self, tag: u8, value: i64) { + let bytes = value.to_be_bytes(); + let mut leading_zeroes = 0; + for window in bytes.windows(2) { + let byte = window[0] as i8; + let bit = window[1] as i8 >> 7; + if byte ^ bit == 0 { + leading_zeroes += 1; + } else { + break; + } + } + self.serialize_tlv(tag, &bytes[leading_zeroes..]) + } + + fn serialize_f64(&mut self, tag: u8, value: f64) { + match value.classify() { + FpCategory::Nan => self.serialize_tlv(tag, &[ASN1_REAL_NAN]), + FpCategory::Infinite => { + if value.is_sign_positive() { + self.serialize_tlv(tag, &[ASN1_REAL_INFINITY]) + } else { + self.serialize_tlv(tag, &[ASN1_REAL_NEG_INFINITY]) + } + } + FpCategory::Zero | FpCategory::Subnormal => { + // Subnormals cannot be represented in DER and are rounded to zero + if value.is_sign_positive() { + self.serialize_unit(tag) + } else { + self.serialize_tlv(tag, &[ASN1_REAL_NEG_ZERO]) + } + } + FpCategory::Normal => { + let sign_negative = value.is_sign_negative(); + let bits = value.to_bits(); + // The exponent is always 11 bits in f64, so we can fit it inside an i16. + let mut exponent = ((bits >> 52) & 0b11111111111) as i16 - 1023; + let mut mantissa = bits & F64_MANTISSA_MASK | (0b1 << 52); + let mut normalization_factor = 52; + while mantissa & 0b1 == 0 { + mantissa >>= 1; + normalization_factor -= 1; + } + exponent -= normalization_factor; + let mantissa_bytes = mantissa.to_be_bytes(); + let mut leading_zero_bytes = 0; + for byte in mantissa_bytes { + if byte == 0 { + leading_zero_bytes += 1; + } else { + break; + } + } + let exponent_bytes = exponent.to_be_bytes(); + // If the exponent can be represented as an i8, then we must do so. + let short_exp = exponent_bytes[0] == 0 || exponent_bytes[0] == 0xFF; + let len = 2 + (!short_exp as usize) + mantissa_bytes.len() - leading_zero_bytes; + // This identifying byte contains the encoding method, as well as the sign and + // exponent length. + let structure_byte = 0b10000000 | ((sign_negative as u8) << 6) | (!short_exp as u8); + self.writer.write(&[tag, len as u8, structure_byte]); + if short_exp { + self.writer.write(&[exponent_bytes[1]]); + } else { + self.writer.write(&exponent_bytes); + } + self.writer.write(&mantissa_bytes[leading_zero_bytes..]); + } + } + } + + fn serialize_bool(&mut self, tag: u8, value: bool) { + let byte = if value { 0xFF } else { 0x00 }; + self.serialize_tlv(tag, &[byte]) + } + + fn serialize_str(&mut self, tag: u8, value: &str) { + self.serialize_tlv(tag, value.as_bytes()) + } + + fn serialize_unit(&mut self, tag: u8) { + self.serialize_tlv(tag, &[]) + } +} + +fn serialize_der_recursive<'shape, 'w, W: Asn1Write>( + pv: Peek<'shape, '_, '_>, + serializer: &'w mut DerSerializer<'w, W>, + wrapper_tag: Option, +) -> Result<(), Asn1SerError> { + let shape = pv.shape(); + let type_tag = shape + .type_tag + .map(tag::Asn1TypeTag::from_str) + .transpose() + .map_err(Asn1SerError::TypeTag)? + .map(|t| t.ber()); + let tag = wrapper_tag.or(type_tag); + match (shape.def, shape.ty) { + (Def::Scalar(sd), _) => match sd.affinity { + ScalarAffinity::Boolean(_) => { + serializer.serialize_bool( + tag.unwrap_or(ASN1_TYPE_TAG_BOOLEAN), + *pv.get::().unwrap(), + ); + Ok(()) + } + ScalarAffinity::Number(na) => match na.bits { + NumberBits::Integer { .. } => { + let value = if shape.is_type::() { + *pv.get::().unwrap() as i64 + } else if shape.is_type::() { + *pv.get::().unwrap() as i64 + } else if shape.is_type::() { + *pv.get::().unwrap() as i64 + } else if shape.is_type::() { + *pv.get::().unwrap() + } else { + return Err(Asn1SerError::UnsupportedShape); + }; + serializer.serialize_i64(tag.unwrap_or(ASN1_TYPE_TAG_INTEGER), value); + Ok(()) + } + NumberBits::Float { .. } => { + let value = if shape.is_type::() { + *pv.get::().unwrap() as f64 + } else if shape.is_type::() { + *pv.get::().unwrap() + } else { + return Err(Asn1SerError::UnsupportedShape); + }; + serializer.serialize_f64(tag.unwrap_or(ASN1_TYPE_TAG_REAL), value); + Ok(()) + } + _ => Err(Asn1SerError::UnsupportedShape), + }, + ScalarAffinity::String(_) => { + let value = pv.get::().unwrap(); + serializer.serialize_str(tag.unwrap_or(ASN1_TYPE_TAG_UTF8STRING), value); + Ok(()) + } + #[cfg(feature = "const-oid")] + ScalarAffinity::OID(_) => { + let value = pv.get::().unwrap(); + serializer.serialize_tlv( + tag.unwrap_or(ASN1_TYPE_TAG_OBJECT_IDENTIFIER), + value.as_bytes(), + ); + Ok(()) + } + _ => Err(Asn1SerError::UnsupportedShape), + }, + (Def::List(ld), _) => { + if ld.t().is_type::() && shape.is_type::>() { + serializer.serialize_tlv( + tag.unwrap_or(ASN1_TYPE_TAG_OCTET_STRING), + pv.get::>().unwrap(), + ); + } else { + let pv = pv.into_list().unwrap(); + let mut value = Vec::new(); + for pv in pv.iter() { + let mut inner_serializer = DerSerializer { writer: &mut value }; + serialize_der_recursive(pv, &mut inner_serializer, None)?; + } + serializer.serialize_tlv( + tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE | ASN1_FORM_CONSTRUCTED), + &value, + ); + } + Ok(()) + } + (Def::Option(_), _) => { + let pv = pv.into_option().unwrap(); + if let Some(pv) = pv.value() { + serialize_der_recursive(pv, serializer, tag)?; + } + Ok(()) + } + (_, Type::User(ut)) => match ut { + UserType::Struct(st) => match st.kind { + StructKind::Unit => { + serializer.serialize_unit(tag.unwrap_or(ASN1_TYPE_TAG_NULL)); + Ok(()) + } + StructKind::TupleStruct + if st.fields.len() == 1 + && shape.attributes.contains(&ShapeAttribute::Transparent) => + { + let inner = pv.into_struct().unwrap().field(0).unwrap(); + serialize_der_recursive(inner, serializer, tag) + } + StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => { + let pv = pv.into_struct().unwrap(); + let mut value = Vec::new(); + for (_, pv) in pv.fields() { + let mut inner_serializer = DerSerializer { writer: &mut value }; + serialize_der_recursive(pv, &mut inner_serializer, None)?; + } + serializer.serialize_tlv( + tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE) | ASN1_FORM_CONSTRUCTED, + &value, + ); + Ok(()) + } + _ => Err(Asn1SerError::UnsupportedShape), + }, + UserType::Enum(_) => { + let pv = pv.into_enum().unwrap(); + let v = pv.active_variant().unwrap(); + let discriminant = v.discriminant; + match v.data.kind { + StructKind::Unit => { + if discriminant + .is_some_and(|discriminant| !(0..128).contains(&discriminant)) + { + return Err(Asn1SerError::InvalidDiscriminant(discriminant)); + } + let tag = (discriminant.unwrap_or(ASN1_TYPE_TAG_NULL as i64) as u8) + | tag::ASN1_CLASS_CONTEXT_SPECIFIC; + serializer.serialize_unit(tag); + Ok(()) + } + StructKind::TupleStruct if pv.fields().count() == 1 => { + let inner = pv.innermost_peek(); + serialize_der_recursive(inner, serializer, None) + } + StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => { + if discriminant + .is_some_and(|discriminant| !(0..128).contains(&discriminant)) + { + return Err(Asn1SerError::InvalidDiscriminant(discriminant)); + } + let tag = (discriminant + .unwrap_or((ASN1_TYPE_TAG_SEQUENCE | ASN1_FORM_CONSTRUCTED) as i64) + as u8) + | tag::ASN1_CLASS_CONTEXT_SPECIFIC; + let mut value = Vec::new(); + for (_, pv) in pv.fields() { + let mut inner_serializer = DerSerializer { writer: &mut value }; + serialize_der_recursive(pv, &mut inner_serializer, None)?; + } + serializer.serialize_tlv(tag, &value); + Ok(()) + } + _ => Err(Asn1SerError::UnsupportedShape), + } + } + _ => Err(Asn1SerError::UnsupportedShape), + }, + _ => Err(Asn1SerError::UnsupportedShape), + } +} + +/// Errors when deserializing from ASN.1 BER or DER bytes +#[derive(Debug)] +pub enum Asn1DeserError { + /// Invalid type tag + TypeTag(tag::Asn1TagError), + /// Unsupported shape + UnsupportedShape, + /// Tag couldn't be matched to a struct, field, or enum variant + UnknownTag { + /// Tag value + tag: u8, + /// Position of this error in bytes + position: usize, + }, + /// Unexpected length for type + LengthMismatch { + /// Length value + len: usize, + /// Expected length value + expected_len: usize, + /// Position of this error in bytes + position: usize, + }, + /// Invalid boolean + InvalidBool { + /// Position of this error in bytes + position: usize, + }, + /// Invalid real + InvalidReal { + /// Position of this error in bytes + position: usize, + }, + /// Invalid string + InvalidString { + /// Position of this error in bytes + position: usize, + /// Underlying UTF-8 error + source: core::str::Utf8Error, + }, + #[cfg(feature = "const-oid")] + /// Invalid OID + InvalidOid { + /// Position of this error in bytes + position: usize, + /// Underlying const-oid error + source: const_oid::Error, + }, + /// Sequence length didn't match content length + SequenceSizeMismatch { + /// Position of the end of the sequence in bytes + sequence_end: usize, + /// Position of the end of the sequence content + content_end: usize, + }, +} + +impl core::fmt::Display for Asn1DeserError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Asn1DeserError::TypeTag(error) => write!(f, "Bad type_tag: {}", error), + Asn1DeserError::UnsupportedShape => write!(f, "Unsupported shape"), + Asn1DeserError::UnknownTag { tag, position } => { + write!(f, "Unknown tag {} at byte {}", tag, position) + } + Asn1DeserError::LengthMismatch { + len, + expected_len, + position, + } => { + write!( + f, + "Unexpected length {} for type at byte {}, expected {}", + len, position, expected_len + ) + } + Asn1DeserError::InvalidBool { position } => { + write!(f, "Invalid value for boolean at byte {}", position) + } + Asn1DeserError::InvalidReal { position } => { + write!(f, "Invalid value for real at byte {}", position) + } + Asn1DeserError::InvalidString { position, .. } => { + write!(f, "Invalid string at byte {}", position) + } + #[cfg(feature = "const-oid")] + Asn1DeserError::InvalidOid { position, .. } => { + write!(f, "Invalid OID at byte {}", position) + } + Asn1DeserError::SequenceSizeMismatch { + sequence_end, + content_end, + } => { + write!( + f, + "Sequence ending at byte {} didn't match content ending at byte {}", + sequence_end, content_end + ) + } + } + } +} + +impl core::error::Error for Asn1DeserError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Asn1DeserError::TypeTag(source) => Some(source), + Asn1DeserError::InvalidString { source, .. } => Some(source), + #[cfg(feature = "const-oid")] + Asn1DeserError::InvalidOid { source, .. } => Some(source), + _ => None, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum EncodingRules { + // Basic, + // Canonical, + Distinguished, +} + +#[derive(Debug, PartialEq)] +enum PopReason { + TopLevel, + ObjectVal, + ListVal { end: usize }, + Some, + Object { end: usize }, +} + +#[derive(Debug)] +enum DeserializeTask { + Value { with_tag: Option }, + Field(usize), + Pop(PopReason), +} + +struct Asn1DeserializerStack<'input> { + _rules: EncodingRules, + input: &'input [u8], + pos: usize, + stack: Vec, +} + +/// Get the BER/DER tag for a given shape +/// +/// Returns `None` for CHOICE/Enum +fn ber_tag_for_shape(shape: &Shape<'_>) -> Result, Asn1DeserError> { + let type_tag = shape + .type_tag + .map(|t| tag::Asn1TypeTag::from_str(t).map_err(Asn1DeserError::TypeTag)) + .transpose()? + .map(|t| t.ber()); + match (shape.def, shape.ty) { + (Def::Scalar(sd), _) => match sd.affinity { + ScalarAffinity::Boolean(_) => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_BOOLEAN))), + ScalarAffinity::Number(na) => match na.bits { + NumberBits::Integer { .. } => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_INTEGER))), + NumberBits::Float { .. } => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_REAL))), + _ => Err(Asn1DeserError::UnsupportedShape), + }, + ScalarAffinity::String(_) => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_UTF8STRING))), + ScalarAffinity::OID(_) => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_OBJECT_IDENTIFIER))), + _ => Err(Asn1DeserError::UnsupportedShape), + }, + (Def::List(ld), _) => { + if ld.t().is_type::() && shape.is_type::>() { + Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_OCTET_STRING))) + } else { + Ok(Some( + type_tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE) | ASN1_FORM_CONSTRUCTED, + )) + } + } + (Def::Option(od), _) => Ok(type_tag.or(ber_tag_for_shape(od.t)?)), + (_, Type::User(ut)) => match ut { + UserType::Struct(st) => match st.kind { + StructKind::Unit => Ok(Some(type_tag.unwrap_or(ASN1_TYPE_TAG_NULL))), + StructKind::TupleStruct + if st.fields.len() == 1 + && shape.attributes.contains(&ShapeAttribute::Transparent) => + { + Ok(type_tag.or(ber_tag_for_shape(st.fields[0].shape)?)) + } + StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => Ok(Some( + type_tag.unwrap_or(ASN1_TYPE_TAG_SEQUENCE) | ASN1_FORM_CONSTRUCTED, + )), + _ => Err(Asn1DeserError::UnsupportedShape), + }, + UserType::Enum(_) => { + // Enum variants are matched against their discriminant or inner types + Ok(None) + } + _ => Err(Asn1DeserError::UnsupportedShape), + }, + _ => Err(Asn1DeserError::UnsupportedShape), + } +} + +impl<'shape, 'input> Asn1DeserializerStack<'input> { + fn next_tl(&mut self, expected_tag: u8) -> Result { + let tag = self.input[self.pos]; + if tag != expected_tag { + return Err(Asn1DeserError::UnknownTag { + tag, + position: self.pos, + }); + } + let len = self.input[self.pos + 1] as usize; + self.pos += 2; + let len = if len < 128 { + len + } else { + let len_len = len - 128; + self.pos += len_len; + let len_bytes = &self.input[(self.pos - len_len)..self.pos]; + len_bytes.iter().fold(0usize, |mut acc, x| { + acc <<= 8; + acc += *x as usize; + acc + }) + }; + Ok(len) + } + + fn next_tlv(&mut self, expected_tag: u8) -> Result<&'input [u8], Asn1DeserError> { + let len = self.next_tl(expected_tag)?; + self.pos += len; + Ok(&self.input[(self.pos - len)..self.pos]) + } + + fn next_bool(&mut self, tag: u8) -> Result { + let bytes = self.next_tlv(tag)?; + match *bytes { + [0x00] => Ok(false), + [0xFF] => Ok(true), + [_] => Err(Asn1DeserError::InvalidBool { position: self.pos }), + _ => Err(Asn1DeserError::LengthMismatch { + len: bytes.len(), + expected_len: 1, + position: self.pos - bytes.len(), + }), + } + } + + fn next_int(&mut self, tag: u8) -> Result { + let bytes = self.next_tlv(tag)?; + Ok(bytes[1..].iter().fold(bytes[0] as i8 as i64, |mut acc, x| { + acc <<= 8; + acc |= *x as i64; + acc + })) + } + + fn next_float(&mut self, tag: u8) -> Result { + let bytes = self.next_tlv(tag)?; + Ok(if bytes.is_empty() { + 0.0f64 + } else { + match bytes[0] { + ASN1_REAL_INFINITY => f64::INFINITY, + ASN1_REAL_NEG_INFINITY => f64::NEG_INFINITY, + ASN1_REAL_NAN => f64::NAN, + ASN1_REAL_NEG_ZERO => -0.0f64, + struct_byte => { + if struct_byte & 0b10111100 != 0b10000000 { + return Err(Asn1DeserError::InvalidReal { + position: self.pos - bytes.len(), + }); + } + let sign_negative = (struct_byte >> 6 & 0b1) > 0; + let exponent_len = ((struct_byte & 0b11) + 1) as usize; + if bytes.len() < exponent_len + 2 { + return Err(Asn1DeserError::LengthMismatch { + len: bytes.len(), + expected_len: exponent_len + 2, + position: self.pos - bytes.len(), + }); + } + if exponent_len > 1 && matches!(bytes[1], 0x00 | 0xFF) { + return Err(Asn1DeserError::InvalidReal { + position: self.pos - bytes.len(), + }); + } + if bytes.len() > 2 + exponent_len + && matches!(bytes[1 + exponent_len], 0x00 | 0xFF) + { + return Err(Asn1DeserError::InvalidReal { + position: self.pos - bytes.len(), + }); + } + let mut exponent = bytes[2..1 + exponent_len].iter().fold( + bytes[1] as i8 as i64, + |mut acc, x| { + acc <<= 8; + acc |= *x as u64 as i64; + acc + }, + ); + if exponent > 1023 { + if sign_negative { + f64::NEG_INFINITY + } else { + f64::INFINITY + } + } else { + let mut mantissa = + bytes[1 + exponent_len..] + .iter() + .take(7) + .fold(0, |mut acc, x| { + acc <<= 8; + acc |= *x as u64; + acc + }); + let mut normalization_factor = 52; + while mantissa & (0b1 << 52) == 0 && normalization_factor > 0 { + mantissa <<= 1; + normalization_factor -= 1; + } + exponent += normalization_factor + 1023; + f64::from_bits( + (sign_negative as u64) << 63 + | ((exponent as u64) & 0b11111111111) << 52 + | (mantissa & F64_MANTISSA_MASK), + ) + } + } + } + }) + } + + fn next<'f>( + &mut self, + mut wip: Partial<'f, 'shape>, + with_tag: Option, + ) -> Result, Asn1DeserError> { + let shape = wip.shape(); + let tag_for_shape = with_tag.or(ber_tag_for_shape(shape)?); + match (shape.def, shape.ty) { + (Def::Scalar(sd), _) => match sd.affinity { + ScalarAffinity::Boolean(_) => { + wip.set(self.next_bool(tag_for_shape.unwrap())?).unwrap(); + Ok(wip) + } + ScalarAffinity::Number(na) => match na.bits { + NumberBits::Integer { .. } => { + let number = self.next_int(tag_for_shape.unwrap())?; + if shape.is_type::() { + wip.set(number as i8).unwrap(); + } else if shape.is_type::() { + wip.set(number as i16).unwrap(); + } else if shape.is_type::() { + wip.set(number as i32).unwrap(); + } else if shape.is_type::() { + wip.set(number).unwrap(); + } + Ok(wip) + } + NumberBits::Float { .. } => { + let value = self.next_float(tag_for_shape.unwrap())?; + if shape.is_type::() { + wip.set(value as f32).unwrap(); + } else if shape.is_type::() { + wip.set(value).unwrap(); + } + Ok(wip) + } + _ => Err(Asn1DeserError::UnsupportedShape), + }, + ScalarAffinity::String(_) => { + let bytes = self.next_tlv(tag_for_shape.unwrap())?; + let value = core::str::from_utf8(bytes).map_err(|source| { + Asn1DeserError::InvalidString { + position: self.pos, + source, + } + })?; + wip.set(value.to_string()).unwrap(); + Ok(wip) + } + #[cfg(feature = "const-oid")] + ScalarAffinity::OID(_) => { + let bytes = self.next_tlv(tag_for_shape.unwrap())?; + let value = ObjectIdentifier::from_bytes(bytes).map_err(|source| { + Asn1DeserError::InvalidOid { + position: self.pos, + source, + } + })?; + wip.set(value).unwrap(); + Ok(wip) + } + _ => Err(Asn1DeserError::UnsupportedShape), + }, + (Def::List(_), _) => { + if shape.is_type::>() { + let bytes = self.next_tlv(tag_for_shape.unwrap())?; + wip.set(bytes.to_vec()).unwrap(); + } else { + let len = self.next_tl(tag_for_shape.unwrap())?; + self.stack.push(DeserializeTask::Pop(PopReason::ListVal { + end: self.pos + len, + })); + self.stack.push(DeserializeTask::Value { with_tag: None }); + } + Ok(wip) + } + (Def::Option(od), _) => { + if self.pos == self.input.len() { + wip.set_default().unwrap(); + return Ok(wip); + } + let tag = self.input[self.pos]; + match tag_for_shape { + Some(t) if t == tag => { + wip.begin_some().unwrap(); + self.stack.push(DeserializeTask::Pop(PopReason::Some)); + self.stack + .push(DeserializeTask::Value { with_tag: Some(t) }); + } + Some(_) => { + wip.set_default().unwrap(); + } + None => { + if let Type::User(UserType::Enum(et)) = od.t.ty { + for v in et.variants { + if let Some(variant_tag) = match v.data.kind { + StructKind::Tuple if v.data.fields.len() == 1 => { + ber_tag_for_shape(v.data.fields[0].shape)? + } + StructKind::Unit + | StructKind::TupleStruct + | StructKind::Struct + | StructKind::Tuple => v.discriminant.map(|discriminant| { + discriminant as u8 | tag::ASN1_CLASS_CONTEXT_SPECIFIC + }), + _ => return Err(Asn1DeserError::UnsupportedShape), + } { + if tag == variant_tag { + wip.begin_some().unwrap(); + self.stack.push(DeserializeTask::Pop(PopReason::Some)); + self.stack.push(DeserializeTask::Value { with_tag: None }); + break; + } + } + } + wip.set_default().unwrap(); + } else { + wip.set_default().unwrap(); + } + } + } + Ok(wip) + } + (_, Type::User(ut)) => match ut { + UserType::Struct(st) => match st.kind { + StructKind::Unit => { + let len = self.next_tl(tag_for_shape.unwrap())?; + if len != 0 { + Err(Asn1DeserError::LengthMismatch { + len, + expected_len: 0, + position: self.pos, + }) + } else { + Ok(wip) + } + } + StructKind::TupleStruct + if st.fields.len() == 1 + && shape.attributes.contains(&ShapeAttribute::Transparent) => + { + wip.begin_nth_field(0).unwrap(); + self.stack.push(DeserializeTask::Pop(PopReason::ObjectVal)); + self.stack.push(DeserializeTask::Value { + with_tag: tag_for_shape, + }); + Ok(wip) + } + StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => { + let len = self.next_tl(tag_for_shape.unwrap())?; + self.stack.push(DeserializeTask::Pop(PopReason::Object { + end: self.pos + len, + })); + for i in (0..st.fields.len()).rev() { + self.stack.push(DeserializeTask::Field(i)); + } + Ok(wip) + } + _ => Err(Asn1DeserError::UnsupportedShape), + }, + UserType::Enum(et) => { + let tag = self.input[self.pos]; + for (i, v) in et.variants.iter().enumerate() { + match v.data.kind { + StructKind::Unit => { + if let Some(discriminant) = v.discriminant { + let expected_tag = + discriminant as u8 | tag::ASN1_CLASS_CONTEXT_SPECIFIC; + if tag == expected_tag { + wip.select_nth_variant(i).unwrap(); + let len = self.next_tl(tag)?; + if len != 0 { + return Err(Asn1DeserError::LengthMismatch { + len, + expected_len: 0, + position: self.pos, + }); + } else { + return Ok(wip); + } + } + } + } + StructKind::Tuple if v.data.fields.len() == 1 => { + let inner_tag = ber_tag_for_shape(v.data.fields[0].shape)?; + if inner_tag.is_some_and(|vtag| vtag == tag) { + wip.select_nth_variant(i).unwrap(); + self.stack.push(DeserializeTask::Pop(PopReason::ObjectVal)); + self.stack.push(DeserializeTask::Value { with_tag: None }); + } + } + StructKind::TupleStruct | StructKind::Struct | StructKind::Tuple => { + if let Some(discriminant) = v.discriminant { + let expected_tag = + discriminant as u8 | tag::ASN1_CLASS_CONTEXT_SPECIFIC; + if tag == expected_tag { + wip.select_nth_variant(i).unwrap(); + let len = self.next_tl(tag)?; + self.stack.push(DeserializeTask::Pop(PopReason::Object { + end: self.pos + len, + })); + for i in (0..v.data.fields.len()).rev() { + self.stack.push(DeserializeTask::Field(i)); + } + return Ok(wip); + } + } + } + _ => return Err(Asn1DeserError::UnsupportedShape), + } + } + Err(Asn1DeserError::UnknownTag { + tag, + position: self.pos, + }) + } + _ => Err(Asn1DeserError::UnsupportedShape), + }, + _ => Err(Asn1DeserError::UnsupportedShape), + } + } +} + +/// Deserialize an ASN.1 DER slice given some some [`Partial`] into a [`HeapValue`] +pub fn deserialize_der_wip<'facet, 'shape>( + input: &[u8], + mut wip: Partial<'facet, 'shape>, +) -> Result, Asn1DeserError> { + let mut runner = Asn1DeserializerStack { + _rules: EncodingRules::Distinguished, + input, + pos: 0, + stack: vec![ + DeserializeTask::Pop(PopReason::TopLevel), + DeserializeTask::Value { with_tag: None }, + ], + }; + + loop { + match runner.stack.pop() { + Some(DeserializeTask::Pop(reason)) => match reason { + PopReason::TopLevel => { + return Ok(wip.build().unwrap()); + } + PopReason::Object { end } => { + if runner.pos != end { + return Err(Asn1DeserError::SequenceSizeMismatch { + sequence_end: end, + content_end: runner.pos, + }); + } + } + PopReason::ListVal { end } => { + if runner.pos < end { + runner + .stack + .push(DeserializeTask::Pop(PopReason::ListVal { end })); + runner.stack.push(DeserializeTask::Value { with_tag: None }); + } else if runner.pos > end { + return Err(Asn1DeserError::SequenceSizeMismatch { + sequence_end: end, + content_end: runner.pos, + }); + } + } + _ => { + wip.end().unwrap(); + } + }, + Some(DeserializeTask::Value { with_tag }) => { + wip = runner.next(wip, with_tag)?; + } + Some(DeserializeTask::Field(index)) => { + runner + .stack + .push(DeserializeTask::Pop(PopReason::ObjectVal)); + runner.stack.push(DeserializeTask::Value { with_tag: None }); + wip.begin_nth_field(index).unwrap(); + } + None => unreachable!("Instruction stack is empty"), + } + } +} + +/// Deserialize a slice of ASN.1 DER bytes into a Facet type +pub fn deserialize_der<'f, F: facet_core::Facet<'f>>(input: &[u8]) -> Result { + let v = deserialize_der_wip(input, Partial::alloc_shape(F::SHAPE).unwrap())?; + let f: F = v.materialize().unwrap(); + Ok(f) +} diff --git a/facet-asn1/src/tag.rs b/facet-asn1/src/tag.rs new file mode 100644 index 000000000..fdb04c9b2 --- /dev/null +++ b/facet-asn1/src/tag.rs @@ -0,0 +1,334 @@ +//! Parsing of ASN.1 type tags (referred to simply as "tags" in the spec). +//! In ASN.1, these are always u8 in size. +//! +//! These are parsed from the `#[facet(type_tag = ...)]` paramter in Facet types. +//! Keywords UNIVERSAL, APPLICATION, and PRIVATE are supported, but specifying the +//! raw tag from 0-255 is also fine. + +use core::{num::ParseIntError, str::FromStr}; + +const ASN1_CLASS_UNIVERSAL: u8 = 0b00 << 6; +const ASN1_CLASS_APPLICATION: u8 = 0b01 << 6; +pub(crate) const ASN1_CLASS_CONTEXT_SPECIFIC: u8 = 0b10 << 6; +const ASN1_CLASS_PRIVATE: u8 = 0b11 << 6; + +#[derive(Debug)] +pub enum Asn1TagError { + Unknown, + Int(ParseIntError), +} + +impl core::fmt::Display for Asn1TagError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Asn1TagError::Unknown => write!(f, "Type tag couldn't be parsed into a BER/DER tag"), + Asn1TagError::Int(_) => write!(f, "Raw type tag couldn't be parsed into a u8"), + } + } +} + +impl core::error::Error for Asn1TagError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Asn1TagError::Int(parse_int_error) => Some(parse_int_error), + _ => None, + } + } +} + +enum Asn1TypeClass { + Universal, + Application, + ContextSpecific, + Private, +} + +#[allow(non_camel_case_types)] +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, PartialEq, Eq)] +enum Asn1Type { + BOOLEAN, + INTEGER, + BIT_STRING, + OCTET_STRING, + NULL, + OBJECT_IDENTIFIER, + ObjectDescriptor, + EXTERNAL, + REAL, + ENUMERATED, + EMBEDDED_PDV, + UTF8String, + RELATIVE_OID, + TIME, + SEQUENCE, + SET, + NumericString, + PrintableString, + TeletexString, + T61String, + VideotexString, + IA5String, + UTCTime, + GeneralizedTime, + GraphicString, + VisibleString, + GeneralString, + UniversalString, + CHARACTER_STRING, + BMPString, + DATE, + TIME_OF_DAY, + DATE_TIME, + DURATION, + Raw(u8), +} + +pub struct Asn1TypeTag { + class: Asn1TypeClass, + tag: Asn1Type, +} + +impl FromStr for Asn1TypeTag { + type Err = Asn1TagError; + + fn from_str(s: &str) -> core::result::Result { + let mut class: Option = None; + let mut tag: Option = None; + let mut words = s.split_whitespace(); + while let Some(word) = words.next() { + match word { + "UNIVERSAL" => { + class = Some(Asn1TypeClass::Universal); + } + "APPLICATION" => { + class = Some(Asn1TypeClass::Application); + } + "CONTEXT" => { + if words.next() == Some("SPECIFIC") { + class = Some(Asn1TypeClass::ContextSpecific); + } else { + return Err(Asn1TagError::Unknown); + } + } + "PRIVATE" => { + class = Some(Asn1TypeClass::Private); + } + "BOOLEAN" => { + tag = Some(Asn1Type::BOOLEAN); + break; + } + "INTEGER" => { + tag = Some(Asn1Type::INTEGER); + break; + } + "BIT" => { + if words.next() == Some("STRING") { + tag = Some(Asn1Type::BIT_STRING); + break; + } else { + return Err(Asn1TagError::Unknown); + } + } + "OCTET" => { + if words.next() == Some("STRING") { + tag = Some(Asn1Type::OCTET_STRING); + break; + } else { + return Err(Asn1TagError::Unknown); + } + } + "NULL" => { + tag = Some(Asn1Type::NULL); + break; + } + "OBJECT" => { + if words.next() == Some("IDENTIFIER") { + tag = Some(Asn1Type::OBJECT_IDENTIFIER); + break; + } else { + return Err(Asn1TagError::Unknown); + } + } + "ObjectDescriptor" => { + tag = Some(Asn1Type::ObjectDescriptor); + break; + } + "EXTERNAL" => { + tag = Some(Asn1Type::EXTERNAL); + break; + } + "REAL" => { + tag = Some(Asn1Type::REAL); + break; + } + "ENUMERATED" => { + tag = Some(Asn1Type::ENUMERATED); + break; + } + "EMBEDDED" => { + if words.next() == Some("PDV") { + tag = Some(Asn1Type::EMBEDDED_PDV); + break; + } else { + return Err(Asn1TagError::Unknown); + } + } + "UTF8String" => { + tag = Some(Asn1Type::UTF8String); + break; + } + "RELATIVE-OID" => { + tag = Some(Asn1Type::RELATIVE_OID); + break; + } + "TIME" => { + tag = Some(Asn1Type::TIME); + break; + } + "SEQUENCE" => { + tag = Some(Asn1Type::SEQUENCE); + break; + } + "SET" => { + tag = Some(Asn1Type::SET); + break; + } + "NumericString" => { + tag = Some(Asn1Type::NumericString); + break; + } + "PrintableString" => { + tag = Some(Asn1Type::PrintableString); + break; + } + "TeletexString" => { + tag = Some(Asn1Type::TeletexString); + break; + } + "T61String" => { + tag = Some(Asn1Type::T61String); + break; + } + "VideotexString" => { + tag = Some(Asn1Type::VideotexString); + break; + } + "IA5String" => { + tag = Some(Asn1Type::IA5String); + break; + } + "UTCTime" => { + tag = Some(Asn1Type::UTCTime); + break; + } + "GeneralizedTime" => { + tag = Some(Asn1Type::GeneralizedTime); + break; + } + "GraphicString" => { + tag = Some(Asn1Type::GraphicString); + break; + } + "VisibleString" => { + tag = Some(Asn1Type::VisibleString); + break; + } + "GeneralString" => { + tag = Some(Asn1Type::GeneralString); + break; + } + "UniversalString" => { + tag = Some(Asn1Type::UniversalString); + break; + } + "CHARACTER STRING" => { + tag = Some(Asn1Type::CHARACTER_STRING); + break; + } + "BMPString" => { + tag = Some(Asn1Type::BMPString); + break; + } + "DATE" => { + tag = Some(Asn1Type::DATE); + break; + } + "TIME-OF-DAY" => { + tag = Some(Asn1Type::TIME_OF_DAY); + break; + } + "DATE-TIME" => { + tag = Some(Asn1Type::DATE_TIME); + break; + } + "DURATION" => { + tag = Some(Asn1Type::DURATION); + break; + } + raw => { + if class.is_none() { + class = Some(Asn1TypeClass::ContextSpecific); + } + let raw = u8::from_str(raw).map_err(Asn1TagError::Int)?; + tag = Some(Asn1Type::Raw(raw)); + break; + } + } + } + let class = class.unwrap_or(Asn1TypeClass::Universal); + if let Some(tag) = tag { + Ok(Self { class, tag }) + } else { + Err(Asn1TagError::Unknown) + } + } +} + +impl Asn1TypeTag { + pub(crate) fn ber(&self) -> u8 { + let class = match self.class { + Asn1TypeClass::Universal => ASN1_CLASS_UNIVERSAL, + Asn1TypeClass::Application => ASN1_CLASS_APPLICATION, + Asn1TypeClass::ContextSpecific => ASN1_CLASS_CONTEXT_SPECIFIC, + Asn1TypeClass::Private => ASN1_CLASS_PRIVATE, + }; + let value = match self.tag { + Asn1Type::BOOLEAN => 1, + Asn1Type::INTEGER => 2, + Asn1Type::BIT_STRING => 3, + Asn1Type::OCTET_STRING => 4, + Asn1Type::NULL => 5, + Asn1Type::OBJECT_IDENTIFIER => 6, + Asn1Type::ObjectDescriptor => 7, + Asn1Type::EXTERNAL => 8, + Asn1Type::REAL => 9, + Asn1Type::ENUMERATED => 10, + Asn1Type::EMBEDDED_PDV => 11, + Asn1Type::UTF8String => 12, + Asn1Type::RELATIVE_OID => 13, + Asn1Type::TIME => 14, + Asn1Type::SEQUENCE => 16, + Asn1Type::SET => 17, + Asn1Type::NumericString => 18, + Asn1Type::PrintableString => 19, + Asn1Type::TeletexString | Asn1Type::T61String => 20, + Asn1Type::VideotexString => 21, + Asn1Type::IA5String => 22, + Asn1Type::UTCTime => 23, + Asn1Type::GeneralizedTime => 24, + Asn1Type::GraphicString => 25, + Asn1Type::VisibleString => 26, + Asn1Type::GeneralString => 27, + Asn1Type::UniversalString => 28, + Asn1Type::CHARACTER_STRING => 29, + Asn1Type::BMPString => 30, + Asn1Type::DATE => 31, + Asn1Type::TIME_OF_DAY => 32, + Asn1Type::DATE_TIME => 33, + Asn1Type::DURATION => 34, + Asn1Type::Raw(r) => r, + }; + value | class + } +} diff --git a/facet-asn1/tests/mod.rs b/facet-asn1/tests/mod.rs new file mode 100644 index 000000000..7e90b4e67 --- /dev/null +++ b/facet-asn1/tests/mod.rs @@ -0,0 +1,382 @@ +use facet::Facet; +use facet_asn1::{deserialize_der, to_vec_der}; +use facet_testhelpers::test; + +#[derive(Debug, Clone, Facet, PartialEq, Eq)] +#[facet(type_tag = "IA5String", transparent)] +struct IA5String(String); + +impl From for IA5String { + fn from(value: String) -> Self { + Self(value) + } +} + +// FooQuestion ::= SEQUENCE { +// trackingNumber INTEGER, +// question IA5String +// } +#[derive(Debug, Facet, PartialEq, Eq)] +struct FooQuestion { + tracking_number: i8, + question: IA5String, +} + +#[test] +fn test_deserialize_foo_question() { + let der: [u8; 21] = [ + 0x30, 0x13, 0x02, 0x01, 0x05, 0x16, 0x0e, 0x41, 0x6e, 0x79, 0x62, 0x6f, 0x64, 0x79, 0x20, + 0x74, 0x68, 0x65, 0x72, 0x65, 0x3f, + ]; + let question: FooQuestion = deserialize_der(&der).unwrap(); + assert_eq!( + question, + FooQuestion { + tracking_number: 5, + question: String::from("Anybody there?").into(), + } + ); +} + +#[test] +fn test_serialize_foo_question() { + let question = FooQuestion { + tracking_number: 5, + question: String::from("Anybody there?").into(), + }; + let der = to_vec_der(&question).unwrap(); + let expected_der: [u8; 21] = [ + 0x30, 0x13, 0x02, 0x01, 0x05, 0x16, 0x0e, 0x41, 0x6e, 0x79, 0x62, 0x6f, 0x64, 0x79, 0x20, + 0x74, 0x68, 0x65, 0x72, 0x65, 0x3f, + ]; + assert_eq!(&expected_der[..], &der[..]); +} + +#[derive(Debug, Facet, Clone, Copy, PartialEq, Eq)] +#[facet(type_tag = "0", transparent)] +struct OptionalIntZero(Option); + +impl From> for OptionalIntZero { + fn from(value: Option) -> Self { + Self(value) + } +} + +#[derive(Debug, Facet, Clone, Copy, PartialEq, Eq)] +#[facet(type_tag = "1", transparent)] +struct OptionalIntOne(Option); + +impl From> for OptionalIntOne { + fn from(value: Option) -> Self { + Self(value) + } +} + +// Point ::= SEQUENCE { +// x [0] INTEGER OPTIONAL, +// y [1] INTEGER OPTIONAL, +// } +#[derive(Debug, Facet, PartialEq, Eq)] +struct Point { + x: OptionalIntZero, + y: OptionalIntOne, +} + +#[test] +fn test_deserialize_point_x() { + let der: [u8; 5] = [0x30, 0x03, 0x80, 0x01, 0x09]; + let point: Point = deserialize_der(&der).unwrap(); + assert_eq!( + point, + Point { + x: Some(9).into(), + y: None.into(), + } + ); +} + +#[test] +fn test_deserialize_point_y() { + let der: [u8; 5] = [0x30, 0x03, 0x81, 0x01, 0x09]; + let point: Point = deserialize_der(&der).unwrap(); + assert_eq!( + point, + Point { + x: None.into(), + y: Some(9).into(), + } + ); +} + +#[test] +fn test_deserialize_point_x_y() { + let der: [u8; 8] = [0x30, 0x06, 0x80, 0x01, 0x09, 0x81, 0x01, 0x09]; + let point: Point = deserialize_der(&der).unwrap(); + assert_eq!( + point, + Point { + x: Some(9).into(), + y: Some(9).into(), + } + ); +} + +#[test] +fn test_serialize_point_x() { + let point = Point { + x: Some(9).into(), + y: None.into(), + }; + let der = to_vec_der(&point).unwrap(); + let expected_der = [0x30, 0x03, 0x80, 0x01, 0x09]; + assert_eq!(&expected_der[..], &der[..]); +} + +#[test] +fn test_serialize_point_y() { + let point = Point { + x: None.into(), + y: Some(9).into(), + }; + let der = to_vec_der(&point).unwrap(); + let expected_der = [0x30, 0x03, 0x81, 0x01, 0x09]; + assert_eq!(&expected_der[..], &der[..]); +} + +#[test] +fn test_serialize_point_x_y() { + let point = Point { + x: Some(9).into(), + y: Some(9).into(), + }; + let der = to_vec_der(&point).unwrap(); + let expected_der: [u8; 8] = [0x30, 0x06, 0x80, 0x01, 0x09, 0x81, 0x01, 0x09]; + assert_eq!(&expected_der[..], &der[..]); +} + +// ImplicitString ::= [5] IMPLICIT UTF8String +#[derive(Debug, Facet, PartialEq, Eq)] +#[facet(type_tag = "5", transparent)] +struct ImplicitString(String); + +// ExplciitString ::= [5] EXPLICIT UTF8String +#[derive(Debug, Facet, PartialEq, Eq)] +#[facet(type_tag = "5")] +struct ExplicitString(String); + +#[test] +fn test_deserialize_implicit_string() { + let der: [u8; 4] = [0x85, 0x02, 0x68, 0x69]; + let implicit_string: ImplicitString = deserialize_der(&der).unwrap(); + assert_eq!(implicit_string, ImplicitString(String::from("hi"))); +} + +#[test] +fn test_serialize_implicit_string() { + let implicit_string = ImplicitString("hi".to_owned()); + let der = to_vec_der(&implicit_string).unwrap(); + let expected_der: [u8; 4] = [0x85, 0x02, 0x68, 0x69]; + assert_eq!(&expected_der[..], &der[..]); +} + +#[test] +fn test_deserialize_explicit_string() { + let der: [u8; 6] = [0xA5, 0x04, 0x0C, 0x02, 0x68, 0x69]; + let explicit_string: ExplicitString = deserialize_der(&der).unwrap(); + assert_eq!(explicit_string, ExplicitString(String::from("hi"))); +} + +#[test] +fn test_serialize_explicit_string() { + let explicit_string = ExplicitString("hi".to_owned()); + let der = to_vec_der(&explicit_string).unwrap(); + let expected_der: [u8; 6] = [0xA5, 0x04, 0x0C, 0x02, 0x68, 0x69]; + assert_eq!(&expected_der[..], &der[..]); +} + +// NullStruct ::= NULL +#[derive(Debug, Facet, PartialEq, Eq)] +struct NullStruct; + +#[test] +fn test_deserialize_null() { + let der: [u8; 2] = [0x05, 0x00]; + let null_struct: NullStruct = deserialize_der(&der).unwrap(); + assert_eq!(null_struct, NullStruct); +} + +#[test] +fn test_serialize_null() { + let null_struct = NullStruct; + let der = to_vec_der(&null_struct).unwrap(); + let expected_der: [u8; 2] = [0x05, 0x00]; + assert_eq!(&expected_der[..], &der[..]); +} + +#[test] +fn test_deserialize_octet_string() { + let der: [u8; 6] = [0x04, 0x04, 0x00, 0x01, 0x02, 0x03]; + let octet_string: Vec = deserialize_der(&der).unwrap(); + assert_eq!(octet_string, vec![0x00, 0x01, 0x02, 0x03]); +} + +#[test] +fn test_serialize_octet_string() { + let octet_string: Vec = vec![0x00, 0x01, 0x02, 0x03]; + let der = to_vec_der(&octet_string).unwrap(); + let expected_der: [u8; 6] = [0x04, 0x04, 0x00, 0x01, 0x02, 0x03]; + assert_eq!(&expected_der[..], &der); +} + +#[test] +fn test_deserialize_object_identifier() { + let der: [u8; 5] = [0x06, 0x03, 0x55, 0x04, 0x31]; + let oid: const_oid::ObjectIdentifier = deserialize_der(&der).unwrap(); + assert_eq!(oid, const_oid::db::rfc4519::DISTINGUISHED_NAME); +} + +#[test] +fn test_serialize_object_identifier() { + let oid = const_oid::db::rfc4519::DISTINGUISHED_NAME; + let der = to_vec_der(&oid).unwrap(); + let expected_der: [u8; 5] = [0x06, 0x03, 0x55, 0x04, 0x31]; + assert_eq!(&expected_der[..], &der); +} + +// Division ::= CHOICE { +// r-and-d [1] IMPLICIT SEQUENCE { +// labID INTEGER, +// currentProject IA5String, +// } +// unassigned [2] IMPLICIT NULL +// } +#[derive(Debug, Facet, PartialEq, Eq)] +#[repr(u8)] +enum Division { + RAndD { + lab_id: i64, + current_project: IA5String, + } = 1, + Unassigned = 2, +} + +#[test] +fn test_deserialize_choice_sequence() { + let der: [u8; 11] = [ + 0x81, 0x09, 0x02, 0x01, 0x30, 0x16, 0x04, 0x44, 0x58, 0x2D, 0x37, + ]; + let division: Division = deserialize_der(&der).unwrap(); + assert_eq!( + division, + Division::RAndD { + lab_id: 48, + current_project: String::from("DX-7").into(), + } + ); +} + +#[test] +fn test_serialize_choice_sequence() { + let division = Division::RAndD { + lab_id: 48, + current_project: String::from("DX-7").into(), + }; + let der = to_vec_der(&division).unwrap(); + let expected_der: [u8; 11] = [ + 0x81, 0x09, 0x02, 0x01, 0x30, 0x16, 0x04, 0x44, 0x58, 0x2D, 0x37, + ]; + assert_eq!(&expected_der[..], &der[..]); +} + +#[test] +fn test_deserialize_choice_null() { + let der: [u8; 2] = [0x82, 0x00]; + let division: Division = deserialize_der(&der).unwrap(); + assert_eq!(division, Division::Unassigned); +} + +#[test] +fn test_serialize_choice_null() { + let division = Division::Unassigned; + let der = to_vec_der(&division).unwrap(); + let expected_der: [u8; 2] = [0x82, 0x00]; + assert_eq!(&expected_der[..], &der[..]); +} + +const INTEGER_TESTS: [(i64, &[u8]); 6] = [ + (0, &[0x02, 0x01, 0x00]), + (127, &[0x02, 0x01, 0x7F]), + (128, &[0x02, 0x02, 0x00, 0x80]), + (256, &[0x02, 0x02, 0x01, 0x00]), + (-128, &[0x02, 0x01, 0x80]), + (-129, &[0x02, 0x02, 0xFF, 0x7F]), +]; + +#[test] +fn test_deserialize_integer_values() { + for (value, der) in INTEGER_TESTS { + let i: i64 = deserialize_der(der).unwrap(); + assert_eq!(i, value); + } +} + +#[test] +fn test_serialize_integer_values() { + for (i, expected_der) in INTEGER_TESTS { + let der = to_vec_der(&i).unwrap(); + assert_eq!(expected_der, &der[..]); + } +} + +const FLOAT_TESTS: [(f64, &[u8]); 15] = [ + (0.0, &[0x09, 0x00]), + (f64::INFINITY, &[0x09, 0x01, 0b01000000]), + (f64::NEG_INFINITY, &[0x09, 0x01, 0b01000001]), + (f64::NAN, &[0x09, 0x01, 0b01000010]), + (-0.0, &[0x09, 0x01, 0b01000011]), + (1.0, &[0x09, 0x03, 0b10000000, 0x00, 0x01]), + (2.0, &[0x09, 0x03, 0b10000000, 0x01, 0x01]), + (8192.0, &[0x09, 0x03, 0b10000000, 0x0D, 0x01]), + ( + f64::from_bits(0x4FF0000000000000), + &[0x09, 0x04, 0b10000001, 0x01, 0x00, 0x01], + ), + (3.0, &[0x09, 0x03, 0b10000000, 0x00, 0x03]), + (1.99609375, &[0x09, 0x04, 0b10000000, 0xF8, 0x01, 0xFF]), + (15.96875, &[0x09, 0x04, 0b10000000, 0xFB, 0x01, 0xFF]), + (f64::from_bits(0b1), &[0x09, 0x00]), + ( + f64::from_bits(0xC00FFFFFFFFFFFFF), + &[ + 0x09, 0x09, 0b11000000, 0xCD, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ], + ), + ( + f64::from_bits(0x7FE0000000000001), + &[ + 0x09, 0x0A, 0b10000001, 0x03, 0xCB, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ], + ), +]; + +#[test] +fn test_deserialize_real_values() { + for (value, der) in FLOAT_TESTS { + let i: f64 = deserialize_der(der).unwrap(); + if i.is_nan() && value.is_nan() { + continue; + } + if value.is_subnormal() && i == 0.0 { + continue; + } + assert_eq!(i, value); + } +} + +#[test] +fn test_serialize_real_values() { + for (i, expected_der) in FLOAT_TESTS { + let der = to_vec_der(&i).unwrap(); + assert_eq!(expected_der, &der[..]); + } +} diff --git a/facet-core/CHANGELOG.md b/facet-core/CHANGELOG.md index 034eef59f..e05fab400 100644 --- a/facet-core/CHANGELOG.md +++ b/facet-core/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Add support for `const_oid::ObjectIdentifier` and `ScalarAffinity::OID` + ## [0.27.14](https://github.com/facet-rs/facet/compare/facet-core-v0.27.13...facet-core-v0.27.14) - 2025-06-17 ### Other diff --git a/facet-core/Cargo.toml b/facet-core/Cargo.toml index c25f6cec7..79bc1def9 100644 --- a/facet-core/Cargo.toml +++ b/facet-core/Cargo.toml @@ -33,6 +33,8 @@ url = ["alloc", "dep:url"] jiff02 = ["alloc", "dep:jiff"] # Provide Facet trait implementations for bytes::Bytes bytes = ["alloc", "dep:bytes"] +# Provide Facet trait implementations for const_oid::ObjectIdentifier +const-oid = ["alloc", "dep:const-oid"] # Provide Facet trait implementations for tuples up to size 12. Without it, # Facet is only implemented for tuples up to size 4. @@ -59,6 +61,7 @@ chrono = { version = "0.4", optional = true, default-features = false, features ] } jiff = { version = "0.2.13", optional = true } bytes = { version = "1.10.1", optional = true, default-features = false } +const-oid = { version = "0.10.1", optional = true } [dev-dependencies] eyre = "0.6.12" diff --git a/facet-core/src/impls_const_oid.rs b/facet-core/src/impls_const_oid.rs new file mode 100644 index 000000000..1c5e08796 --- /dev/null +++ b/facet-core/src/impls_const_oid.rs @@ -0,0 +1,84 @@ +use const_oid::ObjectIdentifier; + +use crate::{ + Def, Facet, ParseError, PtrConst, PtrMut, PtrUninit, ScalarAffinity, ScalarDef, Shape, + TryFromError, TryIntoInnerError, Type, UserType, ValueVTable, value_vtable, +}; + +unsafe impl<'a, const L: usize> Facet<'a> for ObjectIdentifier { + const VTABLE: &'static ValueVTable = &const { + unsafe fn try_from<'shape, 'dst>( + src_ptr: PtrConst<'_>, + src_shape: &'shape Shape, + dst: PtrUninit<'dst>, + ) -> Result, TryFromError<'shape>> { + if src_shape.id == ::SHAPE.id { + let s = unsafe { src_ptr.read::() }; + return match ObjectIdentifier::new(&s) { + Ok(oid) => Ok(unsafe { dst.put(oid) }), + Err(_) => Err(TryFromError::UnsupportedSourceShape { + src_shape, + expected: &[<&[u8] as Facet>::SHAPE, ::SHAPE], + }), + }; + } + if src_shape.id == <&[u8] as Facet>::SHAPE.id { + let b = unsafe { src_ptr.read::<&[u8]>() }; + return match ObjectIdentifier::from_bytes(b) { + Ok(oid) => Ok(unsafe { dst.put(oid) }), + Err(_) => Err(TryFromError::UnsupportedSourceShape { + src_shape, + expected: &[<&[u8] as Facet>::SHAPE, ::SHAPE], + }), + }; + } + Err(TryFromError::UnsupportedSourceShape { + src_shape, + expected: &[<&[u8] as Facet>::SHAPE, ::SHAPE], + }) + } + + unsafe fn try_into_inner<'dst>( + src_ptr: PtrMut<'_>, + dst: PtrUninit<'dst>, + ) -> Result, TryIntoInnerError> { + let oid = unsafe { src_ptr.read::() }; + Ok(unsafe { dst.put(oid.as_bytes()) }) + } + + let mut vtable = value_vtable!(ObjectIdentifier, |f, _opts| write!( + f, + "{}", + Self::SHAPE.type_identifier + )); + { + let vtable = vtable.sized_mut().unwrap(); + vtable.parse = || { + Some(|s, target| match ObjectIdentifier::new(s) { + Ok(oid) => Ok(unsafe { target.put(oid) }), + Err(_) => Err(ParseError::Generic("OID parsing failed")), + }) + }; + vtable.try_from = || Some(try_from); + vtable.try_into_inner = || Some(try_into_inner); + } + vtable + }; + + const SHAPE: &'static Shape<'static> = &const { + fn inner_shape() -> &'static Shape<'static> { + <&[u8] as Facet>::SHAPE + } + + Shape::builder_for_sized::() + .type_identifier("ObjectIdentifier") + .ty(Type::User(UserType::Opaque)) + .def(Def::Scalar( + ScalarDef::builder() + .affinity(&const { ScalarAffinity::oid().build() }) + .build(), + )) + .inner(inner_shape::) + .build() + }; +} diff --git a/facet-core/src/lib.rs b/facet-core/src/lib.rs index 771fb0d1a..3fa6b8c96 100644 --- a/facet-core/src/lib.rs +++ b/facet-core/src/lib.rs @@ -59,6 +59,9 @@ mod impls_url; #[cfg(feature = "jiff02")] mod impls_jiff; +#[cfg(feature = "const-oid")] +mod impls_const_oid; + // Const type Id mod typeid; pub use typeid::*; diff --git a/facet-core/src/types/def/mod.rs b/facet-core/src/types/def/mod.rs index 662dc6288..b9d282c23 100644 --- a/facet-core/src/types/def/mod.rs +++ b/facet-core/src/types/def/mod.rs @@ -96,6 +96,7 @@ impl<'shape> core::fmt::Debug for Def<'shape> { crate::ScalarAffinity::Url(_) => "Url", crate::ScalarAffinity::UUID(_) => "UUID", crate::ScalarAffinity::ULID(_) => "ULID", + crate::ScalarAffinity::OID(_) => "OID", crate::ScalarAffinity::Time(_) => "Time", crate::ScalarAffinity::Opaque(_) => "Opaque", crate::ScalarAffinity::Other(_) => "Other", diff --git a/facet-core/src/types/def/scalar.rs b/facet-core/src/types/def/scalar.rs index ad1983cc4..192e770cf 100644 --- a/facet-core/src/types/def/scalar.rs +++ b/facet-core/src/types/def/scalar.rs @@ -70,6 +70,8 @@ pub enum ScalarAffinity<'shape> { UUID(UuidAffinity), /// ULID or ULID-like identifier, containing 16 bytes of information ULID(UlidAffinity), + /// OID or OID-like identifier, containing 39 bytes of information + OID(OidAffinity), /// Timestamp or Datetime-like scalar affinity Time(TimeAffinity<'shape>), /// Something you're not supposed to look inside of @@ -133,6 +135,11 @@ impl<'shape> ScalarAffinity<'shape> { UlidAffinityBuilder::new() } + /// Returns a OidAffinityBuilder + pub const fn oid() -> OidAffinityBuilder { + OidAffinityBuilder::new() + } + /// Returns an TimeAffinityBuilder pub const fn time() -> TimeAffinityBuilder<'shape> { TimeAffinityBuilder::new() @@ -795,6 +802,36 @@ impl UlidAffinityBuilder { } } +/// Definition for OID and OID-like scalar affinities +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr(C)] +#[non_exhaustive] +pub struct OidAffinity {} + +impl OidAffinity { + /// Returns a builder for UuidAffinity + pub const fn builder() -> OidAffinityBuilder { + OidAffinityBuilder::new() + } +} + +/// Builder for UlidAffinity +#[repr(C)] +pub struct OidAffinityBuilder {} + +impl OidAffinityBuilder { + /// Creates a new UlidAffinityBuilder + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + OidAffinityBuilder {} + } + + /// Builds the ScalarAffinity + pub const fn build(self) -> ScalarAffinity<'static> { + ScalarAffinity::OID(OidAffinity {}) + } +} + /// Definition for Datetime/Timestamp scalar affinities #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[repr(C)]