Files
cli/vendor/rcgen/src/string.rs

667 lines
18 KiB
Rust
Raw Normal View History

//! 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<Self, Error> {
input.to_string().try_into()
}
}
impl TryFrom<String> 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<Self, Self::Error> {
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<Self, Self::Err> {
s.try_into()
}
}
impl AsRef<str> 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<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()
}
}
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<Self, Error> {
input.to_string().try_into()
}
}
impl TryFrom<String> 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<Self, Error> {
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<Self, Self::Err> {
s.try_into()
}
}
impl AsRef<str> 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<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()
}
}
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<Self, Error> {
input.to_string().try_into()
}
}
impl TryFrom<String> 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<Self, Error> {
// 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<Self, Self::Err> {
s.try_into()
}
}
impl AsRef<str> 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<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()
}
}
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<u8>);
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-16BEencoded vector `vec` into a `BmpString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data.
pub fn from_utf16be(vec: Vec<u8>) -> Result<Self, Error> {
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<Self, Self::Error> {
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<String> 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<Self, Self::Error> {
value.as_str().try_into()
}
}
impl FromStr for BmpString {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<u8>);
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-32BEencoded vector `vec` into a `UniversalString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data.
pub fn from_utf32be(vec: Vec<u8>) -> Result<UniversalString, Error> {
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<Self, Self::Error> {
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<String> 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<Self, Self::Error> {
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);
}
}