From 4d7c099ff15ad86177ecb43e6f8e67c05de59fa4 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Thu, 12 Mar 2026 18:09:21 +0100 Subject: [PATCH] Implement ASN.1 DER parser for X.509 and PKCS#1 support DerParser reads TLV structures from DER-encoded byte slices. Supports all primitive types needed for X.509 certificates: BOOLEAN, INTEGER, BIT STRING, OCTET STRING, NULL, OID, UTF8String, PrintableString, IA5String, UTCTime, GeneralizedTime. Constructed types: SEQUENCE, SET. Context-specific tags with explicit/implicit tagging. Includes well-known OID constants for RSA, ECDSA, SHA-2, X.509 extensions, and X.500 attribute types. OID encode/decode with dotted notation. Rejects non-DER encodings (indefinite length, non-minimal integers/lengths). 45 tests covering all types, DER validation, real certificate fragments, and edge cases. Co-Authored-By: Claude Opus 4.6 --- crates/crypto/src/asn1.rs | 1285 +++++++++++++++++++++++++++++++++++++ crates/crypto/src/lib.rs | 1 + 2 files changed, 1286 insertions(+) create mode 100644 crates/crypto/src/asn1.rs diff --git a/crates/crypto/src/asn1.rs b/crates/crypto/src/asn1.rs new file mode 100644 index 0000000..b69ac10 --- /dev/null +++ b/crates/crypto/src/asn1.rs @@ -0,0 +1,1285 @@ +//! ASN.1 DER (Distinguished Encoding Rules) parser. +//! +//! Parses TLV (Tag-Length-Value) structures from DER-encoded byte slices. +//! Supports all types needed for X.509 certificate and PKCS#1 key parsing. + +use core::fmt; + +// --------------------------------------------------------------------------- +// Error type +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Asn1Error { + /// Not enough data to read the tag/length/value. + Truncated, + /// Indefinite-length encoding is forbidden in DER. + IndefiniteLength, + /// Length encoding is not minimal (DER violation). + NonMinimalLength, + /// Integer encoding is not minimal (leading zero or negative without sign byte). + NonMinimalInteger, + /// Unexpected tag encountered. + UnexpectedTag { expected: u8, got: u8 }, + /// Trailing data after a complete parse. + TrailingData, + /// Invalid OID encoding. + InvalidOid, + /// Invalid boolean value in DER (must be 0x00 or 0xFF). + InvalidBoolean, + /// Invalid UTF-8 in string value. + InvalidUtf8, + /// Invalid time string format. + InvalidTime, + /// Bit string unused bits > 7 or non-zero unused bits with empty content. + InvalidBitString, + /// Content length exceeds available data. + LengthOverflow, +} + +impl fmt::Display for Asn1Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Truncated => write!(f, "truncated data"), + Self::IndefiniteLength => write!(f, "indefinite length not allowed in DER"), + Self::NonMinimalLength => write!(f, "non-minimal length encoding"), + Self::NonMinimalInteger => write!(f, "non-minimal integer encoding"), + Self::UnexpectedTag { expected, got } => { + write!( + f, + "unexpected tag: expected 0x{expected:02x}, got 0x{got:02x}" + ) + } + Self::TrailingData => write!(f, "trailing data after value"), + Self::InvalidOid => write!(f, "invalid OID encoding"), + Self::InvalidBoolean => write!(f, "invalid DER boolean"), + Self::InvalidUtf8 => write!(f, "invalid UTF-8"), + Self::InvalidTime => write!(f, "invalid time string"), + Self::InvalidBitString => write!(f, "invalid bit string"), + Self::LengthOverflow => write!(f, "length overflow"), + } + } +} + +pub type Result = core::result::Result; + +// --------------------------------------------------------------------------- +// Tag constants +// --------------------------------------------------------------------------- + +// Tag class bits (bits 7-6) +pub const TAG_CLASS_UNIVERSAL: u8 = 0x00; +pub const TAG_CLASS_APPLICATION: u8 = 0x40; +pub const TAG_CLASS_CONTEXT: u8 = 0x80; +pub const TAG_CLASS_PRIVATE: u8 = 0xC0; + +// Constructed bit (bit 5) +pub const TAG_CONSTRUCTED: u8 = 0x20; + +// Universal type tags +pub const TAG_BOOLEAN: u8 = 0x01; +pub const TAG_INTEGER: u8 = 0x02; +pub const TAG_BIT_STRING: u8 = 0x03; +pub const TAG_OCTET_STRING: u8 = 0x04; +pub const TAG_NULL: u8 = 0x05; +pub const TAG_OID: u8 = 0x06; +pub const TAG_UTF8_STRING: u8 = 0x0C; +pub const TAG_SEQUENCE: u8 = 0x30; // constructed +pub const TAG_SET: u8 = 0x31; // constructed +pub const TAG_PRINTABLE_STRING: u8 = 0x13; +pub const TAG_IA5_STRING: u8 = 0x16; +pub const TAG_UTC_TIME: u8 = 0x17; +pub const TAG_GENERALIZED_TIME: u8 = 0x18; + +// --------------------------------------------------------------------------- +// OID — Object Identifier +// --------------------------------------------------------------------------- + +/// An ASN.1 Object Identifier, stored as raw DER-encoded bytes. +#[derive(Clone, PartialEq, Eq)] +pub struct Oid<'a> { + /// The raw DER-encoded OID value bytes (without tag and length). + bytes: &'a [u8], +} + +impl<'a> Oid<'a> { + /// Create an OID from raw DER-encoded bytes. + pub fn from_der_bytes(bytes: &'a [u8]) -> Self { + Self { bytes } + } + + /// Return the raw DER-encoded bytes. + pub fn as_bytes(&self) -> &[u8] { + self.bytes + } + + /// Decode to a vector of arc components (e.g., [1, 2, 840, 113549, ...]). + pub fn components(&self) -> Result> { + if self.bytes.is_empty() { + return Err(Asn1Error::InvalidOid); + } + + let mut out = Vec::new(); + let first = self.bytes[0]; + out.push((first / 40) as u32); + out.push((first % 40) as u32); + + let mut i = 1; + while i < self.bytes.len() { + let mut value: u32 = 0; + loop { + if i >= self.bytes.len() { + return Err(Asn1Error::InvalidOid); + } + let byte = self.bytes[i]; + i += 1; + value = value + .checked_shl(7) + .and_then(|v| v.checked_add((byte & 0x7F) as u32)) + .ok_or(Asn1Error::InvalidOid)?; + if byte & 0x80 == 0 { + break; + } + } + out.push(value); + } + + Ok(out) + } + + /// Encode from dotted-notation components. + pub fn encode_components(components: &[u32]) -> Result> { + if components.len() < 2 { + return Err(Asn1Error::InvalidOid); + } + if components[0] > 2 || (components[0] < 2 && components[1] >= 40) { + return Err(Asn1Error::InvalidOid); + } + + let mut out = Vec::new(); + let first = (components[0] * 40 + components[1]) as u8; + out.push(first); + + for &c in &components[2..] { + encode_base128(&mut out, c); + } + + Ok(out) + } + + /// Compare against a known OID given as a byte slice of DER-encoded value. + pub fn matches(&self, other: &[u8]) -> bool { + self.bytes == other + } +} + +impl fmt::Debug for Oid<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.components() { + Ok(comps) => { + let s: Vec = comps.iter().map(|c| c.to_string()).collect(); + write!(f, "OID({})", s.join(".")) + } + Err(_) => write!(f, "OID()"), + } + } +} + +impl fmt::Display for Oid<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.components() { + Ok(comps) => { + let s: Vec = comps.iter().map(|c| c.to_string()).collect(); + write!(f, "{}", s.join(".")) + } + Err(_) => write!(f, ""), + } + } +} + +fn encode_base128(out: &mut Vec, mut value: u32) { + if value == 0 { + out.push(0); + return; + } + // Find out how many bytes we need + let mut tmp = Vec::new(); + while value > 0 { + tmp.push((value & 0x7F) as u8); + value >>= 7; + } + tmp.reverse(); + for (i, byte) in tmp.iter().enumerate() { + if i < tmp.len() - 1 { + out.push(byte | 0x80); + } else { + out.push(*byte); + } + } +} + +// --------------------------------------------------------------------------- +// Well-known OIDs (DER-encoded value bytes) +// --------------------------------------------------------------------------- + +/// sha256WithRSAEncryption (1.2.840.113549.1.1.11) +pub const OID_SHA256_WITH_RSA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B]; + +/// sha384WithRSAEncryption (1.2.840.113549.1.1.12) +pub const OID_SHA384_WITH_RSA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0C]; + +/// sha512WithRSAEncryption (1.2.840.113549.1.1.13) +pub const OID_SHA512_WITH_RSA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0D]; + +/// rsaEncryption (1.2.840.113549.1.1.1) +pub const OID_RSA_ENCRYPTION: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]; + +/// ecPublicKey (1.2.840.10045.2.1) +pub const OID_EC_PUBLIC_KEY: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; + +/// prime256v1 / secp256r1 (1.2.840.10045.3.1.7) +pub const OID_PRIME256V1: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; + +/// secp384r1 (1.3.132.0.34) +pub const OID_SECP384R1: &[u8] = &[0x2B, 0x81, 0x04, 0x00, 0x22]; + +/// ecdsaWithSHA256 (1.2.840.10045.4.3.2) +pub const OID_ECDSA_WITH_SHA256: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]; + +/// ecdsaWithSHA384 (1.2.840.10045.4.3.3) +pub const OID_ECDSA_WITH_SHA384: &[u8] = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03]; + +/// sha256 (2.16.840.1.101.3.4.2.1) +pub const OID_SHA256: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]; + +/// sha384 (2.16.840.1.101.3.4.2.2) +pub const OID_SHA384: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02]; + +/// sha512 (2.16.840.1.101.3.4.2.3) +pub const OID_SHA512: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]; + +/// id-ce-subjectKeyIdentifier (2.5.29.14) +pub const OID_SUBJECT_KEY_IDENTIFIER: &[u8] = &[0x55, 0x1D, 0x0E]; + +/// id-ce-keyUsage (2.5.29.15) +pub const OID_KEY_USAGE: &[u8] = &[0x55, 0x1D, 0x0F]; + +/// id-ce-subjectAltName (2.5.29.17) +pub const OID_SUBJECT_ALT_NAME: &[u8] = &[0x55, 0x1D, 0x11]; + +/// id-ce-basicConstraints (2.5.29.19) +pub const OID_BASIC_CONSTRAINTS: &[u8] = &[0x55, 0x1D, 0x13]; + +/// id-ce-authorityKeyIdentifier (2.5.29.35) +pub const OID_AUTHORITY_KEY_IDENTIFIER: &[u8] = &[0x55, 0x1D, 0x23]; + +/// id-at-commonName (2.5.4.3) +pub const OID_COMMON_NAME: &[u8] = &[0x55, 0x04, 0x03]; + +/// id-at-countryName (2.5.4.6) +pub const OID_COUNTRY_NAME: &[u8] = &[0x55, 0x04, 0x06]; + +/// id-at-organizationName (2.5.4.10) +pub const OID_ORGANIZATION_NAME: &[u8] = &[0x55, 0x04, 0x0A]; + +/// id-at-organizationalUnitName (2.5.4.11) +pub const OID_ORGANIZATIONAL_UNIT_NAME: &[u8] = &[0x55, 0x04, 0x0B]; + +// --------------------------------------------------------------------------- +// TLV item — a parsed tag-length-value triple +// --------------------------------------------------------------------------- + +/// A single parsed TLV (Tag-Length-Value) item. +#[derive(Debug, Clone)] +pub struct Tlv<'a> { + /// The raw tag byte. + pub tag: u8, + /// The value bytes (not including tag and length encoding). + pub value: &'a [u8], + /// The full TLV encoding (tag + length + value). Useful for signature verification. + pub raw: &'a [u8], +} + +impl<'a> Tlv<'a> { + /// True if this is a constructed (SEQUENCE, SET, context-specific constructed) type. + pub fn is_constructed(&self) -> bool { + self.tag & TAG_CONSTRUCTED != 0 + } + + /// Return the tag class (UNIVERSAL, APPLICATION, CONTEXT, PRIVATE). + pub fn tag_class(&self) -> u8 { + self.tag & 0xC0 + } + + /// Return the tag number (bits 4-0). + pub fn tag_number(&self) -> u8 { + self.tag & 0x1F + } + + /// True if this is a context-specific tag (class bits = 10). + pub fn is_context_specific(&self) -> bool { + self.tag_class() == TAG_CLASS_CONTEXT + } + + /// Return context-specific tag number, or None if not context-specific. + pub fn context_tag(&self) -> Option { + if self.is_context_specific() { + Some(self.tag_number()) + } else { + None + } + } + + /// Parse the value as a SEQUENCE of TLV items. + pub fn sequence_items(&self) -> Result>> { + let mut parser = DerParser::new(self.value); + let mut items = Vec::new(); + while parser.has_more() { + items.push(parser.read_tlv()?); + } + Ok(items) + } + + /// Parse the value as a DER INTEGER and return the bytes (may have leading zero for sign). + pub fn as_integer(&self) -> Result<&'a [u8]> { + if self.tag != TAG_INTEGER { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_INTEGER, + got: self.tag, + }); + } + validate_der_integer(self.value)?; + Ok(self.value) + } + + /// Parse the value as a positive DER INTEGER, stripping any leading zero byte. + pub fn as_positive_integer(&self) -> Result<&'a [u8]> { + let bytes = self.as_integer()?; + if bytes.first() == Some(&0) && bytes.len() > 1 { + Ok(&bytes[1..]) + } else { + Ok(bytes) + } + } + + /// Parse the value as a BOOLEAN. + pub fn as_boolean(&self) -> Result { + if self.tag != TAG_BOOLEAN { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_BOOLEAN, + got: self.tag, + }); + } + if self.value.len() != 1 { + return Err(Asn1Error::InvalidBoolean); + } + match self.value[0] { + 0x00 => Ok(false), + 0xFF => Ok(true), + _ => Err(Asn1Error::InvalidBoolean), + } + } + + /// Parse the value as a NULL (must be empty). + pub fn as_null(&self) -> Result<()> { + if self.tag != TAG_NULL { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_NULL, + got: self.tag, + }); + } + if !self.value.is_empty() { + return Err(Asn1Error::TrailingData); + } + Ok(()) + } + + /// Parse the value as an OID. + pub fn as_oid(&self) -> Result> { + if self.tag != TAG_OID { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_OID, + got: self.tag, + }); + } + if self.value.is_empty() { + return Err(Asn1Error::InvalidOid); + } + Ok(Oid::from_der_bytes(self.value)) + } + + /// Parse the value as a BIT STRING. Returns (unused_bits, data). + pub fn as_bit_string(&self) -> Result<(u8, &'a [u8])> { + if self.tag != TAG_BIT_STRING { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_BIT_STRING, + got: self.tag, + }); + } + if self.value.is_empty() { + return Err(Asn1Error::InvalidBitString); + } + let unused = self.value[0]; + if unused > 7 { + return Err(Asn1Error::InvalidBitString); + } + let data = &self.value[1..]; + if data.is_empty() && unused != 0 { + return Err(Asn1Error::InvalidBitString); + } + // DER: unused bits in the last byte must be zero + if unused > 0 && !data.is_empty() { + let mask = (1u8 << unused) - 1; + if data[data.len() - 1] & mask != 0 { + return Err(Asn1Error::InvalidBitString); + } + } + Ok((unused, data)) + } + + /// Parse the value as an OCTET STRING. + pub fn as_octet_string(&self) -> Result<&'a [u8]> { + if self.tag != TAG_OCTET_STRING { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_OCTET_STRING, + got: self.tag, + }); + } + Ok(self.value) + } + + /// Parse the value as a UTF8String. + pub fn as_utf8_string(&self) -> Result<&'a str> { + if self.tag != TAG_UTF8_STRING { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_UTF8_STRING, + got: self.tag, + }); + } + core::str::from_utf8(self.value).map_err(|_| Asn1Error::InvalidUtf8) + } + + /// Parse the value as a PrintableString (ASCII subset). + pub fn as_printable_string(&self) -> Result<&'a str> { + if self.tag != TAG_PRINTABLE_STRING { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_PRINTABLE_STRING, + got: self.tag, + }); + } + core::str::from_utf8(self.value).map_err(|_| Asn1Error::InvalidUtf8) + } + + /// Parse the value as an IA5String (ASCII). + pub fn as_ia5_string(&self) -> Result<&'a str> { + if self.tag != TAG_IA5_STRING { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_IA5_STRING, + got: self.tag, + }); + } + core::str::from_utf8(self.value).map_err(|_| Asn1Error::InvalidUtf8) + } + + /// Parse as any string type (UTF8String, PrintableString, IA5String). + pub fn as_string(&self) -> Result<&'a str> { + match self.tag { + TAG_UTF8_STRING | TAG_PRINTABLE_STRING | TAG_IA5_STRING => { + core::str::from_utf8(self.value).map_err(|_| Asn1Error::InvalidUtf8) + } + _ => Err(Asn1Error::UnexpectedTag { + expected: TAG_UTF8_STRING, + got: self.tag, + }), + } + } + + /// Parse the value as UTCTime (YYMMDDHHMMSSZ). + pub fn as_utc_time(&self) -> Result<&'a str> { + if self.tag != TAG_UTC_TIME { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_UTC_TIME, + got: self.tag, + }); + } + let s = core::str::from_utf8(self.value).map_err(|_| Asn1Error::InvalidTime)?; + // Basic validation: should be 13 chars ending in 'Z' (YYMMDDHHMMSSZ) + if s.len() != 13 || !s.ends_with('Z') { + return Err(Asn1Error::InvalidTime); + } + Ok(s) + } + + /// Parse the value as GeneralizedTime (YYYYMMDDHHMMSSZ). + pub fn as_generalized_time(&self) -> Result<&'a str> { + if self.tag != TAG_GENERALIZED_TIME { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_GENERALIZED_TIME, + got: self.tag, + }); + } + let s = core::str::from_utf8(self.value).map_err(|_| Asn1Error::InvalidTime)?; + // Basic validation: should be 15 chars ending in 'Z' (YYYYMMDDHHMMSSZ) + if s.len() != 15 || !s.ends_with('Z') { + return Err(Asn1Error::InvalidTime); + } + Ok(s) + } +} + +// --------------------------------------------------------------------------- +// DER integer validation +// --------------------------------------------------------------------------- + +fn validate_der_integer(bytes: &[u8]) -> Result<()> { + if bytes.is_empty() { + return Err(Asn1Error::NonMinimalInteger); + } + // Check for non-minimal positive: leading 0x00 followed by a byte < 0x80 + if bytes.len() > 1 && bytes[0] == 0x00 && bytes[1] & 0x80 == 0 { + return Err(Asn1Error::NonMinimalInteger); + } + // Check for non-minimal negative: leading 0xFF followed by a byte >= 0x80 + if bytes.len() > 1 && bytes[0] == 0xFF && bytes[1] & 0x80 != 0 { + return Err(Asn1Error::NonMinimalInteger); + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// DER parser +// --------------------------------------------------------------------------- + +/// A streaming DER parser over a byte slice. +pub struct DerParser<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> DerParser<'a> { + /// Create a new parser over the given data. + pub fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + /// True if there is more data to parse. + pub fn has_more(&self) -> bool { + self.pos < self.data.len() + } + + /// Remaining unparsed bytes. + pub fn remaining(&self) -> &'a [u8] { + &self.data[self.pos..] + } + + /// Current position in the input. + pub fn position(&self) -> usize { + self.pos + } + + /// Read the next TLV item. + pub fn read_tlv(&mut self) -> Result> { + let start = self.pos; + + // Read tag + let tag = self.read_byte()?; + + // We only support single-byte tags (tag number < 31). + // Multi-byte tags (tag number == 0x1F) are rare in X.509. + if tag & 0x1F == 0x1F { + // Long-form tag — skip continuation bytes + // For simplicity, we don't decode these but we need to consume them. + while self.pos < self.data.len() { + let b = self.read_byte()?; + if b & 0x80 == 0 { + break; + } + } + // This is a simplification; we store the first tag byte only. + } + + // Read length + let length = self.read_length()?; + + // Read value + if self.pos + length > self.data.len() { + return Err(Asn1Error::Truncated); + } + let value = &self.data[self.pos..self.pos + length]; + self.pos += length; + let raw = &self.data[start..self.pos]; + + Ok(Tlv { tag, value, raw }) + } + + /// Read a TLV and expect a specific tag. + pub fn read_expect(&mut self, expected_tag: u8) -> Result> { + let tlv = self.read_tlv()?; + if tlv.tag != expected_tag { + return Err(Asn1Error::UnexpectedTag { + expected: expected_tag, + got: tlv.tag, + }); + } + Ok(tlv) + } + + /// Read an optional context-specific tagged value. Returns None if the next + /// tag doesn't match. + pub fn read_optional_context( + &mut self, + tag_number: u8, + constructed: bool, + ) -> Result>> { + if !self.has_more() { + return Ok(None); + } + let expected = + TAG_CLASS_CONTEXT | if constructed { TAG_CONSTRUCTED } else { 0 } | tag_number; + if self.peek_tag()? != expected { + return Ok(None); + } + Ok(Some(self.read_tlv()?)) + } + + /// Peek at the next tag byte without consuming it. + pub fn peek_tag(&self) -> Result { + if self.pos >= self.data.len() { + return Err(Asn1Error::Truncated); + } + Ok(self.data[self.pos]) + } + + /// Skip the next TLV item. + pub fn skip(&mut self) -> Result<()> { + self.read_tlv()?; + Ok(()) + } + + /// Read all remaining TLV items. + pub fn read_all(&mut self) -> Result>> { + let mut items = Vec::new(); + while self.has_more() { + items.push(self.read_tlv()?); + } + Ok(items) + } + + // --- Internal helpers --- + + fn read_byte(&mut self) -> Result { + if self.pos >= self.data.len() { + return Err(Asn1Error::Truncated); + } + let b = self.data[self.pos]; + self.pos += 1; + Ok(b) + } + + fn read_length(&mut self) -> Result { + let first = self.read_byte()?; + if first == 0x80 { + // Indefinite length — forbidden in DER + return Err(Asn1Error::IndefiniteLength); + } + if first & 0x80 == 0 { + // Short form: length is the byte itself + return Ok(first as usize); + } + + // Long form: first byte tells how many length bytes follow + let num_bytes = (first & 0x7F) as usize; + if num_bytes == 0 { + return Err(Asn1Error::IndefiniteLength); + } + if num_bytes > 4 { + // We don't support lengths > 4 bytes (4 GB should be more than enough) + return Err(Asn1Error::LengthOverflow); + } + + let mut length: usize = 0; + for i in 0..num_bytes { + let b = self.read_byte()? as usize; + // DER minimality: first byte must not be 0 in multi-byte encoding + if i == 0 && b == 0 { + return Err(Asn1Error::NonMinimalLength); + } + length = length + .checked_shl(8) + .and_then(|v| v.checked_add(b)) + .ok_or(Asn1Error::LengthOverflow)?; + } + + // DER minimality: if the length fits in short form (< 128), must use short form + if length < 128 { + return Err(Asn1Error::NonMinimalLength); + } + // DER minimality: if the length fits in fewer bytes, must use fewer bytes + if num_bytes > 1 { + let min_bytes = if length < 0x100 { + 1 + } else if length < 0x10000 { + 2 + } else if length < 0x1000000 { + 3 + } else { + 4 + }; + if num_bytes > min_bytes { + return Err(Asn1Error::NonMinimalLength); + } + } + + Ok(length) + } +} + +// --------------------------------------------------------------------------- +// Convenience: parse a single top-level TLV from a byte slice +// --------------------------------------------------------------------------- + +/// Parse a single DER-encoded TLV from the data, returning an error if there +/// is trailing data. +pub fn parse_one(data: &[u8]) -> Result> { + let mut parser = DerParser::new(data); + let tlv = parser.read_tlv()?; + if parser.has_more() { + return Err(Asn1Error::TrailingData); + } + Ok(tlv) +} + +/// Parse a DER-encoded SEQUENCE and return its inner TLV items. +pub fn parse_sequence(data: &[u8]) -> Result>> { + let outer = parse_one(data)?; + if outer.tag != TAG_SEQUENCE { + return Err(Asn1Error::UnexpectedTag { + expected: TAG_SEQUENCE, + got: outer.tag, + }); + } + outer.sequence_items() +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // --- Basic TLV parsing --- + + #[test] + fn parse_null() { + let data = [0x05, 0x00]; // NULL + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.tag, TAG_NULL); + assert!(tlv.value.is_empty()); + tlv.as_null().unwrap(); + } + + #[test] + fn parse_boolean_true() { + let data = [0x01, 0x01, 0xFF]; // BOOLEAN TRUE + let tlv = parse_one(&data).unwrap(); + assert!(tlv.as_boolean().unwrap()); + } + + #[test] + fn parse_boolean_false() { + let data = [0x01, 0x01, 0x00]; // BOOLEAN FALSE + let tlv = parse_one(&data).unwrap(); + assert!(!tlv.as_boolean().unwrap()); + } + + #[test] + fn reject_invalid_boolean() { + let data = [0x01, 0x01, 0x01]; // Not 0x00 or 0xFF + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_boolean().unwrap_err(), Asn1Error::InvalidBoolean); + } + + #[test] + fn parse_integer_positive() { + // INTEGER 127 + let data = [0x02, 0x01, 0x7F]; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_integer().unwrap(), &[0x7F]); + } + + #[test] + fn parse_integer_with_leading_zero() { + // INTEGER 128 (needs leading 0x00 to stay positive) + let data = [0x02, 0x02, 0x00, 0x80]; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_integer().unwrap(), &[0x00, 0x80]); + assert_eq!(tlv.as_positive_integer().unwrap(), &[0x80]); + } + + #[test] + fn reject_non_minimal_integer() { + // Non-minimal: leading 0x00 before a byte < 0x80 + let data = [0x02, 0x02, 0x00, 0x7F]; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_integer().unwrap_err(), Asn1Error::NonMinimalInteger); + } + + #[test] + fn parse_octet_string() { + let data = [0x04, 0x03, 0x01, 0x02, 0x03]; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_octet_string().unwrap(), &[0x01, 0x02, 0x03]); + } + + #[test] + fn parse_bit_string() { + // BIT STRING: 0 unused bits, data = [0xAB, 0xCD] + let data = [0x03, 0x03, 0x00, 0xAB, 0xCD]; + let tlv = parse_one(&data).unwrap(); + let (unused, bits) = tlv.as_bit_string().unwrap(); + assert_eq!(unused, 0); + assert_eq!(bits, &[0xAB, 0xCD]); + } + + #[test] + fn parse_bit_string_with_unused_bits() { + // BIT STRING: 4 unused bits, last byte has low 4 bits = 0 + let data = [0x03, 0x02, 0x04, 0xF0]; + let tlv = parse_one(&data).unwrap(); + let (unused, bits) = tlv.as_bit_string().unwrap(); + assert_eq!(unused, 4); + assert_eq!(bits, &[0xF0]); + } + + #[test] + fn reject_invalid_bit_string_nonzero_unused() { + // BIT STRING: 4 unused bits but low 4 bits are non-zero + let data = [0x03, 0x02, 0x04, 0xF1]; + let tlv = parse_one(&data).unwrap(); + assert_eq!( + tlv.as_bit_string().unwrap_err(), + Asn1Error::InvalidBitString + ); + } + + // --- OID tests --- + + #[test] + fn parse_oid_rsa_encryption() { + // OID 1.2.840.113549.1.1.1 (rsaEncryption) + let data = [ + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, + ]; + let tlv = parse_one(&data).unwrap(); + let oid = tlv.as_oid().unwrap(); + assert!(oid.matches(OID_RSA_ENCRYPTION)); + let comps = oid.components().unwrap(); + assert_eq!(comps, vec![1, 2, 840, 113549, 1, 1, 1]); + } + + #[test] + fn parse_oid_sha256_with_rsa() { + let data = [ + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, + ]; + let tlv = parse_one(&data).unwrap(); + let oid = tlv.as_oid().unwrap(); + assert!(oid.matches(OID_SHA256_WITH_RSA)); + assert_eq!(oid.components().unwrap(), vec![1, 2, 840, 113549, 1, 1, 11]); + } + + #[test] + fn parse_oid_prime256v1() { + let data = [0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; + let tlv = parse_one(&data).unwrap(); + let oid = tlv.as_oid().unwrap(); + assert!(oid.matches(OID_PRIME256V1)); + assert_eq!(oid.components().unwrap(), vec![1, 2, 840, 10045, 3, 1, 7]); + } + + #[test] + fn oid_display() { + let oid = Oid::from_der_bytes(OID_RSA_ENCRYPTION); + assert_eq!(format!("{}", oid), "1.2.840.113549.1.1.1"); + } + + #[test] + fn oid_encode_decode_roundtrip() { + let components = [1u32, 2, 840, 113549, 1, 1, 1]; + let encoded = Oid::encode_components(&components).unwrap(); + assert_eq!(encoded.as_slice(), OID_RSA_ENCRYPTION); + let oid = Oid::from_der_bytes(&encoded); + assert_eq!(oid.components().unwrap(), components); + } + + #[test] + fn oid_encode_simple() { + // 2.5.4.3 (id-at-commonName) + let components = [2u32, 5, 4, 3]; + let encoded = Oid::encode_components(&components).unwrap(); + assert_eq!(encoded.as_slice(), OID_COMMON_NAME); + } + + // --- String tests --- + + #[test] + fn parse_utf8_string() { + let data = [0x0C, 0x05, b'H', b'e', b'l', b'l', b'o']; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_utf8_string().unwrap(), "Hello"); + } + + #[test] + fn parse_printable_string() { + let data = [0x13, 0x02, b'U', b'S']; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_printable_string().unwrap(), "US"); + } + + #[test] + fn parse_ia5_string() { + let data = [0x16, 0x03, b'c', b'o', b'm']; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_ia5_string().unwrap(), "com"); + } + + // --- Time tests --- + + #[test] + fn parse_utc_time() { + // "230101120000Z" + let time_str = b"230101120000Z"; + let mut data = vec![TAG_UTC_TIME, time_str.len() as u8]; + data.extend_from_slice(time_str); + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_utc_time().unwrap(), "230101120000Z"); + } + + #[test] + fn parse_generalized_time() { + // "20230101120000Z" + let time_str = b"20230101120000Z"; + let mut data = vec![TAG_GENERALIZED_TIME, time_str.len() as u8]; + data.extend_from_slice(time_str); + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.as_generalized_time().unwrap(), "20230101120000Z"); + } + + // --- SEQUENCE tests --- + + #[test] + fn parse_sequence_of_integers() { + // SEQUENCE { INTEGER 1, INTEGER 2 } + #[rustfmt::skip] + let data = [ + 0x30, 0x06, // SEQUENCE, length 6 + 0x02, 0x01, 0x01, // INTEGER 1 + 0x02, 0x01, 0x02, // INTEGER 2 + ]; + let items = parse_sequence(&data).unwrap(); + assert_eq!(items.len(), 2); + assert_eq!(items[0].as_integer().unwrap(), &[0x01]); + assert_eq!(items[1].as_integer().unwrap(), &[0x02]); + } + + #[test] + fn parse_nested_sequence() { + // SEQUENCE { SEQUENCE { INTEGER 42 } } + #[rustfmt::skip] + let data = [ + 0x30, 0x05, // outer SEQUENCE + 0x30, 0x03, // inner SEQUENCE + 0x02, 0x01, 0x2A, // INTEGER 42 + ]; + let outer = parse_sequence(&data).unwrap(); + assert_eq!(outer.len(), 1); + let inner = outer[0].sequence_items().unwrap(); + assert_eq!(inner.len(), 1); + assert_eq!(inner[0].as_integer().unwrap(), &[0x2A]); + } + + // --- Context-specific tags --- + + #[test] + fn parse_context_specific_explicit() { + // [0] EXPLICIT { INTEGER 3 } + #[rustfmt::skip] + let data = [ + 0xA0, 0x03, // context-specific [0] constructed + 0x02, 0x01, 0x03, // INTEGER 3 + ]; + let tlv = parse_one(&data).unwrap(); + assert!(tlv.is_context_specific()); + assert_eq!(tlv.context_tag(), Some(0)); + assert!(tlv.is_constructed()); + + // Parse inner content + let mut inner = DerParser::new(tlv.value); + let int_tlv = inner.read_tlv().unwrap(); + assert_eq!(int_tlv.as_integer().unwrap(), &[0x03]); + } + + #[test] + fn parse_context_specific_implicit() { + // [1] IMPLICIT OCTET STRING (primitive) + let data = [0x81, 0x02, 0xAB, 0xCD]; + let tlv = parse_one(&data).unwrap(); + assert!(tlv.is_context_specific()); + assert_eq!(tlv.context_tag(), Some(1)); + assert!(!tlv.is_constructed()); + assert_eq!(tlv.value, &[0xAB, 0xCD]); + } + + #[test] + fn read_optional_context_present() { + #[rustfmt::skip] + let data = [ + 0xA0, 0x03, // [0] constructed + 0x02, 0x01, 0x03, // INTEGER 3 + ]; + let mut parser = DerParser::new(&data); + let opt = parser.read_optional_context(0, true).unwrap(); + assert!(opt.is_some()); + assert_eq!(opt.unwrap().context_tag(), Some(0)); + } + + #[test] + fn read_optional_context_absent() { + let data = [0x02, 0x01, 0x42]; // INTEGER, not [0] + let mut parser = DerParser::new(&data); + let opt = parser.read_optional_context(0, true).unwrap(); + assert!(opt.is_none()); + // Parser should not have advanced + assert_eq!(parser.position(), 0); + } + + // --- Long-form length --- + + #[test] + fn parse_long_form_length() { + // OCTET STRING with 200 bytes (needs 2-byte length encoding: 0x81, 0xC8) + let mut data = vec![0x04, 0x81, 0xC8]; + data.extend_from_slice(&[0xAA; 200]); + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.value.len(), 200); + } + + #[test] + fn parse_two_byte_long_form_length() { + // OCTET STRING with 256 bytes (needs: 0x82, 0x01, 0x00) + let mut data = vec![0x04, 0x82, 0x01, 0x00]; + data.extend_from_slice(&[0xBB; 256]); + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.value.len(), 256); + } + + // --- DER violation rejection --- + + #[test] + fn reject_indefinite_length() { + let data = [0x30, 0x80, 0x00, 0x00]; // SEQUENCE with indefinite length + assert_eq!(parse_one(&data).unwrap_err(), Asn1Error::IndefiniteLength); + } + + #[test] + fn reject_non_minimal_length() { + // OCTET STRING, length 5 encoded as 0x81, 0x05 (should be just 0x05) + let data = [0x04, 0x81, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]; + assert_eq!(parse_one(&data).unwrap_err(), Asn1Error::NonMinimalLength); + } + + #[test] + fn reject_trailing_data() { + let data = [0x05, 0x00, 0xFF]; // NULL + trailing byte + assert_eq!(parse_one(&data).unwrap_err(), Asn1Error::TrailingData); + } + + #[test] + fn reject_truncated() { + let data = [0x04, 0x05, 0x01, 0x02]; // OCTET STRING claims 5 bytes but only 2 given + assert_eq!(parse_one(&data).unwrap_err(), Asn1Error::Truncated); + } + + // --- Real certificate fragment --- + + #[test] + fn parse_algorithm_identifier() { + // AlgorithmIdentifier { algorithm: sha256WithRSA, parameters: NULL } + #[rustfmt::skip] + let data = [ + 0x30, 0x0D, // SEQUENCE + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, // OID + 0x05, 0x00, // NULL + ]; + let items = parse_sequence(&data).unwrap(); + assert_eq!(items.len(), 2); + let oid = items[0].as_oid().unwrap(); + assert!(oid.matches(OID_SHA256_WITH_RSA)); + items[1].as_null().unwrap(); + } + + #[test] + fn parse_rdn_sequence_fragment() { + // A single RelativeDistinguishedName: CN = "Test" + #[rustfmt::skip] + let data = [ + 0x30, 0x13, // SEQUENCE (Name) + 0x31, 0x11, // SET (RDN) + 0x30, 0x0F, // SEQUENCE (AttributeTypeAndValue) + 0x06, 0x03, 0x55, 0x04, 0x03, // OID: id-at-commonName + 0x0C, 0x08, b'T', b'e', b's', b't', b' ', b'C', b'A', b'0', // UTF8String "Test CA0" + ]; + let name_items = parse_sequence(&data).unwrap(); + assert_eq!(name_items.len(), 1); // One RDN + + let rdn = name_items[0].sequence_items().unwrap(); + assert_eq!(rdn.len(), 1); // One attribute + + let atv = rdn[0].sequence_items().unwrap(); + assert_eq!(atv.len(), 2); + + let oid = atv[0].as_oid().unwrap(); + assert!(oid.matches(OID_COMMON_NAME)); + assert_eq!(atv[1].as_utf8_string().unwrap(), "Test CA0"); + } + + // --- Raw field access --- + + #[test] + fn raw_includes_full_encoding() { + let data = [0x02, 0x01, 0x42]; // INTEGER 66 + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.raw, &data); + assert_eq!(tlv.value, &[0x42]); + } + + // --- DerParser sequential reads --- + + #[test] + fn parser_sequential_reads() { + #[rustfmt::skip] + let data = [ + 0x02, 0x01, 0x01, // INTEGER 1 + 0x02, 0x01, 0x02, // INTEGER 2 + 0x05, 0x00, // NULL + ]; + let mut parser = DerParser::new(&data); + let a = parser.read_expect(TAG_INTEGER).unwrap(); + assert_eq!(a.as_integer().unwrap(), &[0x01]); + let b = parser.read_expect(TAG_INTEGER).unwrap(); + assert_eq!(b.as_integer().unwrap(), &[0x02]); + let c = parser.read_expect(TAG_NULL).unwrap(); + c.as_null().unwrap(); + assert!(!parser.has_more()); + } + + #[test] + fn parser_read_expect_wrong_tag() { + let data = [0x05, 0x00]; // NULL + let mut parser = DerParser::new(&data); + let err = parser.read_expect(TAG_INTEGER).unwrap_err(); + assert_eq!( + err, + Asn1Error::UnexpectedTag { + expected: TAG_INTEGER, + got: TAG_NULL, + } + ); + } + + // --- X.509 version field (context-specific [0] EXPLICIT INTEGER) --- + + #[test] + fn parse_x509_version_field() { + // [0] EXPLICIT { INTEGER 2 } — X.509 v3 + #[rustfmt::skip] + let data = [ + 0xA0, 0x03, // [0] constructed + 0x02, 0x01, 0x02, // INTEGER 2 + ]; + let tlv = parse_one(&data).unwrap(); + assert!(tlv.is_context_specific()); + assert!(tlv.is_constructed()); + assert_eq!(tlv.context_tag(), Some(0)); + + let mut inner = DerParser::new(tlv.value); + let version = inner.read_expect(TAG_INTEGER).unwrap(); + assert_eq!(version.as_integer().unwrap(), &[0x02]); + } + + // --- BasicConstraints extension value --- + + #[test] + fn parse_basic_constraints() { + // SEQUENCE { BOOLEAN TRUE, INTEGER 0 } + #[rustfmt::skip] + let data = [ + 0x30, 0x06, // SEQUENCE + 0x01, 0x01, 0xFF, // BOOLEAN TRUE (cA) + 0x02, 0x01, 0x00, // INTEGER 0 (pathLenConstraint) + ]; + let items = parse_sequence(&data).unwrap(); + assert_eq!(items.len(), 2); + assert!(items[0].as_boolean().unwrap()); + assert_eq!(items[1].as_integer().unwrap(), &[0x00]); + } + + // --- Large OID component values --- + + #[test] + fn parse_oid_large_components() { + // 2.16.840.1.101.3.4.2.1 (sha256) + let data = [ + 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, + ]; + let tlv = parse_one(&data).unwrap(); + let oid = tlv.as_oid().unwrap(); + assert!(oid.matches(OID_SHA256)); + assert_eq!( + oid.components().unwrap(), + vec![2, 16, 840, 1, 101, 3, 4, 2, 1] + ); + } + + // --- SET tag --- + + #[test] + fn parse_set() { + // SET { INTEGER 1, INTEGER 2 } + #[rustfmt::skip] + let data = [ + 0x31, 0x06, // SET + 0x02, 0x01, 0x01, // INTEGER 1 + 0x02, 0x01, 0x02, // INTEGER 2 + ]; + let tlv = parse_one(&data).unwrap(); + assert_eq!(tlv.tag, TAG_SET); + assert!(tlv.is_constructed()); + let items = tlv.sequence_items().unwrap(); + assert_eq!(items.len(), 2); + } + + // --- read_all --- + + #[test] + fn read_all_items() { + #[rustfmt::skip] + let data = [ + 0x02, 0x01, 0x01, + 0x02, 0x01, 0x02, + 0x02, 0x01, 0x03, + ]; + let mut parser = DerParser::new(&data); + let items = parser.read_all().unwrap(); + assert_eq!(items.len(), 3); + } + + // --- Empty data --- + + #[test] + fn empty_data_returns_truncated() { + assert_eq!(parse_one(&[]).unwrap_err(), Asn1Error::Truncated); + } +} diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 9bc468a..3a0f67c 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -1,6 +1,7 @@ //! Pure Rust cryptography — AES-GCM, ChaCha20-Poly1305, SHA-2, X25519, RSA, X.509, ASN.1. pub mod aes_gcm; +pub mod asn1; pub mod chacha20_poly1305; pub mod hkdf; pub mod hmac; -- 2.51.2