//! ASN.1 string types use std::fmt; use std::str::FromStr; use crate::{Error, InvalidAsn1String}; /// ASN.1 `PrintableString` type. /// /// Supports a subset of the ASCII printable characters (described below). /// /// For the full ASCII character set, use /// [`Ia5String`][`crate::Ia5String`]. /// /// # Examples /// /// You can create a `PrintableString` from [a literal string][`&str`] with [`PrintableString::try_from`]: /// /// ``` /// use rcgen::string::PrintableString; /// let hello = PrintableString::try_from("hello").unwrap(); /// ``` /// /// # Supported characters /// /// PrintableString is a subset of the [ASCII printable characters]. /// For instance, `'@'` is a printable character as per ASCII but can't be part of [ASN.1's `PrintableString`]. /// /// The following ASCII characters/ranges are supported: /// /// - `A..Z` /// - `a..z` /// - `0..9` /// - "` `" (i.e. space) /// - `\` /// - `(` /// - `)` /// - `+` /// - `,` /// - `-` /// - `.` /// - `/` /// - `:` /// - `=` /// - `?` /// /// [ASCII printable characters]: https://en.wikipedia.org/wiki/ASCII#Printable_characters /// [ASN.1's `PrintableString`]: https://en.wikipedia.org/wiki/PrintableString #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct PrintableString(String); impl PrintableString { /// Extracts a string slice containing the entire `PrintableString`. pub fn as_str(&self) -> &str { &self.0 } } impl TryFrom<&str> for PrintableString { type Error = Error; /// Converts a `&str` to a [`PrintableString`]. /// /// Any character not in the [`PrintableString`] charset will be rejected. /// See [`PrintableString`] documentation for more information. /// /// The result is allocated on the heap. fn try_from(input: &str) -> Result { input.to_string().try_into() } } impl TryFrom for PrintableString { type Error = Error; /// Converts a [`String`][`std::string::String`] into a [`PrintableString`] /// /// Any character not in the [`PrintableString`] charset will be rejected. /// See [`PrintableString`] documentation for more information. /// /// This conversion does not allocate or copy memory. fn try_from(value: String) -> Result { for &c in value.as_bytes() { match c { b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b' ' | b'\'' | b'(' | b')' | b'+' | b',' | b'-' | b'.' | b'/' | b':' | b'=' | b'?' => (), _ => { return Err(Error::InvalidAsn1String( InvalidAsn1String::PrintableString(value), )) }, } } Ok(Self(value)) } } impl FromStr for PrintableString { type Err = Error; fn from_str(s: &str) -> Result { s.try_into() } } impl AsRef for PrintableString { fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for PrintableString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self.as_str(), f) } } impl PartialEq for PrintableString { fn eq(&self, other: &str) -> bool { self.as_str() == other } } impl PartialEq for PrintableString { fn eq(&self, other: &String) -> bool { self.as_str() == other.as_str() } } impl PartialEq<&str> for PrintableString { fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialEq<&String> for PrintableString { fn eq(&self, other: &&String) -> bool { self.as_str() == other.as_str() } } /// ASN.1 `IA5String` type. /// /// # Examples /// /// You can create a `Ia5String` from [a literal string][`&str`] with [`Ia5String::try_from`]: /// /// ``` /// use rcgen::string::Ia5String; /// let hello = Ia5String::try_from("hello").unwrap(); /// ``` /// /// # Supported characters /// /// Supports the [International Alphabet No. 5 (IA5)] character encoding, i.e. /// the 128 characters of the ASCII alphabet. (Note: IA5 is now /// technically known as the International Reference Alphabet or IRA as /// specified in the ITU-T's T.50 recommendation). /// /// For UTF-8, use [`String`][`std::string::String`]. /// /// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_(standard) #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Ia5String(String); impl Ia5String { /// Extracts a string slice containing the entire `Ia5String`. pub fn as_str(&self) -> &str { &self.0 } } impl TryFrom<&str> for Ia5String { type Error = Error; /// Converts a `&str` to a [`Ia5String`]. /// /// Any character not in the [`Ia5String`] charset will be rejected. /// See [`Ia5String`] documentation for more information. /// /// The result is allocated on the heap. fn try_from(input: &str) -> Result { input.to_string().try_into() } } impl TryFrom for Ia5String { type Error = Error; /// Converts a [`String`][`std::string::String`] into a [`Ia5String`] /// /// Any character not in the [`Ia5String`] charset will be rejected. /// See [`Ia5String`] documentation for more information. fn try_from(input: String) -> Result { if !input.is_ascii() { return Err(Error::InvalidAsn1String(InvalidAsn1String::Ia5String( input, ))); } Ok(Self(input)) } } impl FromStr for Ia5String { type Err = Error; fn from_str(s: &str) -> Result { s.try_into() } } impl AsRef for Ia5String { fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for Ia5String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self.as_str(), f) } } impl PartialEq for Ia5String { fn eq(&self, other: &str) -> bool { self.as_str() == other } } impl PartialEq for Ia5String { fn eq(&self, other: &String) -> bool { self.as_str() == other.as_str() } } impl PartialEq<&str> for Ia5String { fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialEq<&String> for Ia5String { fn eq(&self, other: &&String) -> bool { self.as_str() == other.as_str() } } /// ASN.1 `TeletexString` type. /// /// # Examples /// /// You can create a `TeletexString` from [a literal string][`&str`] with [`TeletexString::try_from`]: /// /// ``` /// use rcgen::string::TeletexString; /// let hello = TeletexString::try_from("hello").unwrap(); /// ``` /// /// # Supported characters /// /// The standard defines a complex character set allowed in this type. However, quoting the ASN.1 /// [mailing list], "a sizable volume of software in the world treats TeletexString (T61String) as a /// simple 8-bit string with mostly Windows Latin 1 (superset of iso-8859-1) encoding". /// /// `TeletexString` is included for backward compatibility, [RFC 5280] say it /// SHOULD NOT be used for certificates for new subjects. /// /// [mailing list]: https://www.mail-archive.com/asn1@asn1.org/msg00460.html /// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25 #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct TeletexString(String); impl TeletexString { /// Extracts a string slice containing the entire `TeletexString`. pub fn as_str(&self) -> &str { &self.0 } /// Returns a byte slice of this `TeletexString`’s contents. pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } } impl TryFrom<&str> for TeletexString { type Error = Error; /// Converts a `&str` to a [`TeletexString`]. /// /// Any character not in the [`TeletexString`] charset will be rejected. /// See [`TeletexString`] documentation for more information. /// /// The result is allocated on the heap. fn try_from(input: &str) -> Result { input.to_string().try_into() } } impl TryFrom for TeletexString { type Error = Error; /// Converts a [`String`][`std::string::String`] into a [`TeletexString`] /// /// Any character not in the [`TeletexString`] charset will be rejected. /// See [`TeletexString`] documentation for more information. /// /// This conversion does not allocate or copy memory. fn try_from(input: String) -> Result { // Check all bytes are visible if !input.as_bytes().iter().all(|b| (0x20..=0x7f).contains(b)) { return Err(Error::InvalidAsn1String(InvalidAsn1String::TeletexString( input, ))); } Ok(Self(input)) } } impl FromStr for TeletexString { type Err = Error; fn from_str(s: &str) -> Result { s.try_into() } } impl AsRef for TeletexString { fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for TeletexString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self.as_str(), f) } } impl PartialEq for TeletexString { fn eq(&self, other: &str) -> bool { self.as_str() == other } } impl PartialEq for TeletexString { fn eq(&self, other: &String) -> bool { self.as_str() == other.as_str() } } impl PartialEq<&str> for TeletexString { fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialEq<&String> for TeletexString { fn eq(&self, other: &&String) -> bool { self.as_str() == other.as_str() } } /// ASN.1 `BMPString` type. /// /// # Examples /// /// You can create a `BmpString` from [a literal string][`&str`] with [`BmpString::try_from`]: /// /// ``` /// use rcgen::string::BmpString; /// let hello = BmpString::try_from("hello").unwrap(); /// ``` /// /// # Supported characters /// /// Encodes Basic Multilingual Plane (BMP) subset of Unicode (ISO 10646), /// a.k.a. UCS-2. /// /// Bytes are encoded as UTF-16 big-endian. /// /// `BMPString` is included for backward compatibility, [RFC 5280] say it /// SHOULD NOT be used for certificates for new subjects. /// /// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25 #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct BmpString(Vec); impl BmpString { /// Returns a byte slice of this `BmpString`'s contents. /// /// The inverse of this method is [`from_utf16be`]. /// /// [`from_utf16be`]: BmpString::from_utf16be /// /// # Examples /// /// ``` /// use rcgen::string::BmpString; /// let s = BmpString::try_from("hello").unwrap(); /// /// assert_eq!(&[0, 104, 0, 101, 0, 108, 0, 108, 0, 111], s.as_bytes()); /// ``` pub fn as_bytes(&self) -> &[u8] { &self.0 } /// Decode a UTF-16BE–encoded vector `vec` into a `BmpString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data. pub fn from_utf16be(vec: Vec) -> Result { if vec.len() % 2 != 0 { return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString( "Invalid UTF-16 encoding".to_string(), ))); } // FIXME: Update this when `array_chunks` is stabilized. for maybe_char in char::decode_utf16( vec.chunks_exact(2) .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])), ) { // We check we only use the BMP subset of Unicode (the first 65 536 code points) match maybe_char { // Character is in the Basic Multilingual Plane Ok(c) if (c as u64) < u64::from(u16::MAX) => (), // Characters outside Basic Multilingual Plane or unpaired surrogates _ => { return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString( "Invalid UTF-16 encoding".to_string(), ))); }, } } Ok(Self(vec.to_vec())) } } impl TryFrom<&str> for BmpString { type Error = Error; /// Converts a `&str` to a [`BmpString`]. /// /// Any character not in the [`BmpString`] charset will be rejected. /// See [`BmpString`] documentation for more information. /// /// The result is allocated on the heap. fn try_from(value: &str) -> Result { let capacity = value.len().checked_mul(2).ok_or_else(|| { Error::InvalidAsn1String(InvalidAsn1String::BmpString(value.to_string())) })?; let mut bytes = Vec::with_capacity(capacity); for code_point in value.encode_utf16() { bytes.extend(code_point.to_be_bytes()); } BmpString::from_utf16be(bytes) } } impl TryFrom for BmpString { type Error = Error; /// Converts a [`String`][`std::string::String`] into a [`BmpString`] /// /// Any character not in the [`BmpString`] charset will be rejected. /// See [`BmpString`] documentation for more information. /// /// Parsing a `BmpString` allocates memory since the UTF-8 to UTF-16 conversion requires a memory allocation. fn try_from(value: String) -> Result { value.as_str().try_into() } } impl FromStr for BmpString { type Err = Error; fn from_str(s: &str) -> Result { s.try_into() } } /// ASN.1 `UniversalString` type. /// /// # Examples /// /// You can create a `UniversalString` from [a literal string][`&str`] with [`UniversalString::try_from`]: /// /// ``` /// use rcgen::string::UniversalString; /// let hello = UniversalString::try_from("hello").unwrap(); /// ``` /// /// # Supported characters /// /// The characters which can appear in the `UniversalString` type are any of the characters allowed by /// ISO/IEC 10646 (Unicode). /// /// Bytes are encoded like UTF-32 big-endian. /// /// `UniversalString` is included for backward compatibility, [RFC 5280] say it /// SHOULD NOT be used for certificates for new subjects. /// /// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25 #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct UniversalString(Vec); impl UniversalString { /// Returns a byte slice of this `UniversalString`'s contents. /// /// The inverse of this method is [`from_utf32be`]. /// /// [`from_utf32be`]: UniversalString::from_utf32be /// /// # Examples /// /// ``` /// use rcgen::string::UniversalString; /// let s = UniversalString::try_from("hello").unwrap(); /// /// assert_eq!(&[0, 0, 0, 104, 0, 0, 0, 101, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 111], s.as_bytes()); /// ``` pub fn as_bytes(&self) -> &[u8] { &self.0 } /// Decode a UTF-32BE–encoded vector `vec` into a `UniversalString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data. pub fn from_utf32be(vec: Vec) -> Result { if vec.len() % 4 != 0 { return Err(Error::InvalidAsn1String( InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()), )); } // FIXME: Update this when `array_chunks` is stabilized. for maybe_char in vec .chunks_exact(4) .map(|chunk| u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) { if core::char::from_u32(maybe_char).is_none() { return Err(Error::InvalidAsn1String( InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()), )); } } Ok(Self(vec)) } } impl TryFrom<&str> for UniversalString { type Error = Error; /// Converts a `&str` to a [`UniversalString`]. /// /// Any character not in the [`UniversalString`] charset will be rejected. /// See [`UniversalString`] documentation for more information. /// /// The result is allocated on the heap. fn try_from(value: &str) -> Result { let capacity = value.len().checked_mul(4).ok_or_else(|| { Error::InvalidAsn1String(InvalidAsn1String::UniversalString(value.to_string())) })?; let mut bytes = Vec::with_capacity(capacity); // A `char` is any ‘Unicode code point’ other than a surrogate code point. // The code units for UTF-32 correspond exactly to Unicode code points. // (https://www.unicode.org/reports/tr19/tr19-9.html#Introduction) // So any `char` is a valid UTF-32, we just cast it to perform the conversion. for char in value.chars().map(|char| char as u32) { bytes.extend(char.to_be_bytes()) } UniversalString::from_utf32be(bytes) } } impl TryFrom for UniversalString { type Error = Error; /// Converts a [`String`][`std::string::String`] into a [`UniversalString`] /// /// Any character not in the [`UniversalString`] charset will be rejected. /// See [`UniversalString`] documentation for more information. /// /// Parsing a `UniversalString` allocates memory since the UTF-8 to UTF-32 conversion requires a memory allocation. fn try_from(value: String) -> Result { value.as_str().try_into() } } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use crate::{BmpString, Ia5String, PrintableString, TeletexString, UniversalString}; #[test] fn printable_string() { const EXAMPLE_UTF8: &str = "CertificateTemplate"; let printable_string = PrintableString::try_from(EXAMPLE_UTF8).unwrap(); assert_eq!(printable_string, EXAMPLE_UTF8); assert!(PrintableString::try_from("@").is_err()); assert!(PrintableString::try_from("*").is_err()); } #[test] fn ia5_string() { const EXAMPLE_UTF8: &str = "CertificateTemplate"; let ia5_string = Ia5String::try_from(EXAMPLE_UTF8).unwrap(); assert_eq!(ia5_string, EXAMPLE_UTF8); assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok()); assert!(Ia5String::try_from(String::from('\u{8F}')).is_err()); } #[test] fn teletext_string() { const EXAMPLE_UTF8: &str = "CertificateTemplate"; let teletext_string = TeletexString::try_from(EXAMPLE_UTF8).unwrap(); assert_eq!(teletext_string, EXAMPLE_UTF8); assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok()); assert!(Ia5String::try_from(String::from('\u{8F}')).is_err()); } #[test] fn bmp_string() { const EXPECTED_BYTES: &[u8] = &[ 0x00, 0x43, 0x00, 0x65, 0x00, 0x72, 0x00, 0x74, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69, 0x00, 0x63, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x54, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, ]; const EXAMPLE_UTF8: &str = "CertificateTemplate"; let bmp_string = BmpString::try_from(EXAMPLE_UTF8).unwrap(); assert_eq!(bmp_string.as_bytes(), EXPECTED_BYTES); assert!(BmpString::try_from(String::from('\u{FFFE}')).is_ok()); assert!(BmpString::try_from(String::from('\u{FFFF}')).is_err()); } #[test] fn universal_string() { const EXPECTED_BYTES: &[u8] = &[ 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x65, ]; const EXAMPLE_UTF8: &str = "CertificateTemplate"; let universal_string = UniversalString::try_from(EXAMPLE_UTF8).unwrap(); assert_eq!(universal_string.as_bytes(), EXPECTED_BYTES); } }