From c01b562ddde37c18ac5e2a92b71c3de54d3b7fdf Mon Sep 17 00:00:00 2001
From: Nick Santana <nick@mobilecoin.com>
Date: Wed, 3 May 2023 08:59:42 -0700
Subject: [PATCH] Add RFC5380 `DistinguishedName` behavior

RFC5280 section
[4.2.1.4](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.4),
calls out specific behavior on how issuer and subject names should be
compared. It calls these fields `DistinguishedName`. This change
implements most of this behavior with the exception of prohibited
unicode code points and ignoring bidi code points.
---
 verifier/Cargo.toml          |   4 +-
 verifier/src/x509.rs         |   2 +
 verifier/src/x509/certs.rs   |   5 +
 verifier/src/x509/chain.rs   |  21 +-
 verifier/src/x509/error.rs   |   2 +
 verifier/src/x509/name.rs    | 391 +++++++++++++++++++++++++++++++++++
 verifier/src/x509/rfc4518.rs | 153 ++++++++++++++
 7 files changed, 574 insertions(+), 4 deletions(-)
 create mode 100644 verifier/src/x509/name.rs
 create mode 100644 verifier/src/x509/rfc4518.rs

diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml
index fef4c20..f1169db 100644
--- a/verifier/Cargo.toml
+++ b/verifier/Cargo.toml
@@ -14,9 +14,10 @@ repository = { workspace = true }
 rust-version = { workspace = true }
 
 [features]
-alloc = ["pem-rfc7468/alloc", "dep:const-oid", "dep:p256", "dep:x509-cert", "dep:rsa"]
+alloc = ["pem-rfc7468/alloc", "dep:const-oid", "dep:p256", "dep:x509-cert", "dep:rsa", "dep:caseless", "dep:unicode-normalization"]
 
 [dependencies]
+caseless = { version = "0.2.1", default-features = false, optional = true }
 const-oid = { version = "0.9.2", default-features = false, optional = true }
 displaydoc = { version = "0.2.1", default-features = false }
 mc-sgx-core-types = "0.6.0"
@@ -24,6 +25,7 @@ p256 = { version = "0.13.0", default-features = false, features = ["ecdsa"], opt
 pem-rfc7468 = { version = "0.7.0", default-features = false, optional = true }
 rsa = { version = "0.9.0", default-features = false, features = ["sha2"], optional = true }
 subtle = { version = "2.4.0", default-features = false }
+unicode-normalization = { version = "0.1.22", default-features = false, optional = true }
 x509-cert = { version = "0.2.0", default-features = false, optional = true }
 
 [dev-dependencies]
diff --git a/verifier/src/x509.rs b/verifier/src/x509.rs
index b0032ed..f607985 100644
--- a/verifier/src/x509.rs
+++ b/verifier/src/x509.rs
@@ -8,6 +8,8 @@ mod certs;
 mod chain;
 mod crl;
 mod error;
+mod name;
+mod rfc4518;
 
 pub use algorithm::{PublicKey, Signature};
 pub use certs::{UnverifiedCertificate, VerifiedCertificate};
diff --git a/verifier/src/x509/certs.rs b/verifier/src/x509/certs.rs
index 0f925b0..cf7532b 100644
--- a/verifier/src/x509/certs.rs
+++ b/verifier/src/x509/certs.rs
@@ -155,6 +155,11 @@ impl VerifiedCertificate {
         &self.key
     }
 
+    /// Get the issuer of the certificate
+    pub fn issuer(&self) -> &Name {
+        &self.certificate.tbs_certificate.issuer
+    }
+
     /// Get the subject name of the certificate
     pub fn subject_name(&self) -> &Name {
         &self.certificate.tbs_certificate.subject
diff --git a/verifier/src/x509/chain.rs b/verifier/src/x509/chain.rs
index 0f06271..17e22d2 100644
--- a/verifier/src/x509/chain.rs
+++ b/verifier/src/x509/chain.rs
@@ -9,6 +9,7 @@ use super::{Error, Result};
 use crate::x509::algorithm::PublicKey;
 use crate::x509::certs::VerifiedCertificate;
 use crate::x509::crl::UnverifiedCrl;
+use crate::x509::name::DistinguishedName;
 use alloc::vec::Vec;
 use core::time::Duration;
 
@@ -56,6 +57,7 @@ impl CertificateChain {
         for cert in &self.certificates {
             let key = signing_cert.public_key();
             let verified_cert = cert.verify(key, unix_time)?;
+            verify_name_chain(&verified_cert, &signing_cert)?;
             verify_certificate_not_revoked(&verified_cert, &signing_cert, crls, unix_time)?;
             signing_cert = verified_cert;
         }
@@ -105,15 +107,28 @@ impl TryFrom<&[&[u8]]> for CertificateChain {
     }
 }
 
+/// Ensures the issuer matches the subject as specified in
+/// https://datatracker.ietf.org/doc/html/rfc5280#section-7.1
+fn verify_name_chain(cert: &VerifiedCertificate, ca: &VerifiedCertificate) -> Result<()> {
+    let issuer = DistinguishedName::from(cert.issuer());
+    let subject = DistinguishedName::from(ca.subject_name());
+
+    if issuer == subject {
+        Ok(())
+    } else {
+        Err(Error::NameChaining)
+    }
+}
+
 fn verify_certificate_not_revoked(
     cert: &VerifiedCertificate,
-    trust_anchor: &VerifiedCertificate,
+    ca: &VerifiedCertificate,
     crls: &[UnverifiedCrl],
     unix_time: Duration,
 ) -> Result<()> {
     for crl in crls {
-        if crl.issuer() == trust_anchor.subject_name() {
-            let verified_crl = crl.verify(trust_anchor.public_key(), unix_time)?;
+        if crl.issuer() == ca.subject_name() {
+            let verified_crl = crl.verify(ca.public_key(), unix_time)?;
             if verified_crl.is_cert_revoked(cert.serial_number()) {
                 return Err(Error::CertificateRevoked);
             }
diff --git a/verifier/src/x509/error.rs b/verifier/src/x509/error.rs
index bb3450a..4d7f12d 100644
--- a/verifier/src/x509/error.rs
+++ b/verifier/src/x509/error.rs
@@ -28,6 +28,8 @@ pub enum Error {
     CrlNotYetValid,
     /// Certificate revocation list missing next update time
     CrlMissingNextUpdate,
+    /// Issuer name does not match CAs subject name
+    NameChaining,
 }
 
 impl From<x509_cert::der::Error> for Error {
diff --git a/verifier/src/x509/name.rs b/verifier/src/x509/name.rs
new file mode 100644
index 0000000..b70a93e
--- /dev/null
+++ b/verifier/src/x509/name.rs
@@ -0,0 +1,391 @@
+// Copyright (c) 2023 The MobileCoin Foundation
+
+//! X509 distinguished name as defined in sections
+//! [4.1.2.4](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.4) and
+//! [4.1.2.6](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of
+//! [RFC5280](https://datatracker.ietf.org/doc/html/rfc5280)
+//!
+//! Issuer and subject structure from
+//! [4.1.2.4](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.4):
+//!
+//! ```ignore
+//!     Name ::= RDNSequence
+//!     RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
+//!     RelativeDistinguishedName ::= SET OF AttributeTypeAndValue
+//!     AttributeTypeAndValue ::= SEQUENCE {
+//!         AttributeType,
+//!         AttributeValue
+//!     }
+//!     AttributeType ::= OBJECT IDENTIFIER
+//!     AttributeValue ::= DirectoryString
+//!
+//!     DirectoryString ::= CHOICE {
+//!         TeletexString (Unsupported in this implementation).
+//!         PrintableString
+//!         UniversalString (Unsupported in this implementation)
+//!         UTF8String
+//!         BMPString (Unsupported in this implementation)
+//!         IA5String (See note below)
+//!     }
+//! ```
+//!
+//! Note: IA5String is is not called out in the BNF explanation of
+//!   [4.1.2.4](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.4), but if
+//!   one reads X they will see:
+//!
+//! > In addition, implementations of this specification MUST be prepared
+//! > to receive the domainComponent attribute, as defined in
+//! > [RFC4519](https://www.rfc-editor.org/rfc/rfc4519).
+//!
+//! [RFC4519](https://www.rfc-editor.org/rfc/rfc4519) says says that `Ia5String`
+//! will be used for the domain component.
+//!
+//! The [`DistinguishedName`] corresponds to the `Name` in the above hierarchy.
+//! RFC5280 calls this distinguished name when used as the issuer or subject of
+//! a certificate.
+//!
+//! The [`DirectoryString`] corresponds to the `DirectoryString` in the above
+//! hierarchy.
+
+use super::rfc4518::Rfc4518String;
+use x509_cert::attr::AttributeValue;
+use x509_cert::der::asn1::{Ia5StringRef, PrintableStringRef, Utf8StringRef};
+use x509_cert::der::ErrorKind::TagUnknown;
+use x509_cert::der::{Length, Tag, Tagged};
+use x509_cert::name::Name;
+
+#[derive(Debug)]
+pub struct DistinguishedName<'a>(&'a Name);
+
+impl<'a> From<&'a Name> for DistinguishedName<'a> {
+    fn from(name: &'a Name) -> Self {
+        Self(name)
+    }
+}
+
+/// Does `DistinguishedName` comparison as defined in
+/// https://tools.ietf.org/html/rfc5280#section-7.1
+impl<'a> PartialEq for DistinguishedName<'a> {
+    fn eq(&self, other: &Self) -> bool {
+        let name_1 = self.0;
+        let name_2 = other.0;
+
+        if name_1.0.len() != name_2.0.len() {
+            return false;
+        }
+        for (rdn_1, rdn_2) in name_1.0.iter().zip(name_2.0.iter()) {
+            if rdn_1.0.len() != rdn_2.0.len() {
+                return false;
+            }
+            for (attr_1, attr_2) in rdn_1.0.iter().zip(rdn_2.0.iter()) {
+                if attr_1.oid != attr_2.oid {
+                    return false;
+                }
+
+                let value_1 = match DirectoryString::try_from(&attr_1.value) {
+                    Ok(value) => value,
+                    Err(_) => return false,
+                };
+                let value_2 = match DirectoryString::try_from(&attr_2.value) {
+                    Ok(value) => value,
+                    Err(_) => return false,
+                };
+
+                if value_1 != value_2 {
+                    return false;
+                }
+            }
+        }
+        true
+    }
+}
+
+#[derive(Debug)]
+enum DirectoryString<'a> {
+    Printable(PrintableStringRef<'a>),
+    Utf8(Utf8StringRef<'a>),
+    Ia5(Ia5StringRef<'a>),
+}
+
+/// Compares a `DirectoryString` as defined in
+/// https://tools.ietf.org/html/rfc5280#section-7.1
+impl<'a> PartialEq for DirectoryString<'a> {
+    fn eq(&self, other: &Self) -> bool {
+        let string_1 = Rfc4518String::from(self);
+        let string_2 = Rfc4518String::from(other);
+
+        let fold_1 = caseless::default_case_fold_str((&string_1).into());
+        let fold_2 = caseless::default_case_fold_str((&string_2).into());
+
+        fold_1 == fold_2
+    }
+}
+
+impl<'a> TryFrom<&'a AttributeValue> for DirectoryString<'a> {
+    type Error = x509_cert::der::Error;
+
+    fn try_from(value: &'a AttributeValue) -> Result<Self, Self::Error> {
+        match value.tag() {
+            Tag::PrintableString => Ok(DirectoryString::Printable(PrintableStringRef::try_from(
+                value,
+            )?)),
+            Tag::Utf8String => Ok(DirectoryString::Utf8(Utf8StringRef::try_from(value)?)),
+            Tag::Ia5String => Ok(DirectoryString::Ia5(Ia5StringRef::try_from(value)?)),
+            tag => Err(Self::Error::new(
+                TagUnknown { byte: tag.octet() },
+                Length::from(0u8),
+            )),
+        }
+    }
+}
+
+impl<'a> From<&DirectoryString<'a>> for &'a str {
+    fn from(value: &DirectoryString<'a>) -> &'a str {
+        match value {
+            DirectoryString::Printable(s) => s.as_str(),
+            DirectoryString::Utf8(s) => s.as_str(),
+            DirectoryString::Ia5(s) => s.as_str(),
+        }
+    }
+}
+
+impl<'a> From<&DirectoryString<'a>> for Rfc4518String {
+    fn from(value: &DirectoryString<'a>) -> Rfc4518String {
+        let string: &str = value.into();
+        Rfc4518String::from(string)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    extern crate alloc;
+
+    use super::*;
+    use alloc::vec;
+    use const_oid::db::rfc4519::ORGANIZATION_NAME;
+    use core::str::FromStr;
+    use rsa::pkcs8::der::asn1::SetOfVec;
+    use x509_cert::attr::AttributeTypeAndValue;
+    use x509_cert::der::asn1::TeletexStringRef;
+    use x509_cert::name::RelativeDistinguishedName;
+    use yare::parameterized;
+
+    #[parameterized(
+        same_1 = {"Hello", "Hello"},
+        same_2 = {"World", "World"},
+        case_ignore_first = {"Title", "title"},
+        case_ignore_all = {"ALL UPPER", "all upper"},
+        space_compression = {"Hello World", "Hello  World"},
+    )]
+    fn compare_printable_strings(str_1: &str, str_2: &str) {
+        let string_1 = PrintableStringRef::new(str_1).expect("Failed to create PrintableStringRef");
+        let value_1 = AttributeValue::from(string_1);
+        let directory_string_1 =
+            DirectoryString::try_from(&value_1).expect("Failed to convert to directory string");
+
+        let string_2 = PrintableStringRef::new(str_2).expect("Failed to create PrintableStringRef");
+        let value_2 = AttributeValue::from(string_2);
+        let directory_string_2 =
+            DirectoryString::try_from(&value_2).expect("Failed to convert to directory string");
+
+        assert_eq!(directory_string_1, directory_string_2);
+    }
+
+    // Code points for case folding taken from
+    // https://unicode.org/Public/UNIDATA/CaseFolding.txt
+    // A good reference for seeing the characters is
+    // https://www.compart.com/en/unicode/U+03A1
+    // Change the `U+03A1` as appropriate in the url
+    #[parameterized(
+        same = {"Sure", "Sure"},
+        case_fold_micro = {"Μ","µ"}, // U+039C, U+00B5
+        case_fold_rho = {"Ρ", "ρ"}, // U+03A1, U+03C1
+        case_fold_adlam_sha = {"\u{1E921}", "\u{1E943}"}, // not visible in most IDEs
+    )]
+    fn compare_utf8_strings(str_1: &str, str_2: &str) {
+        let string_1 = Utf8StringRef::new(str_1).expect("Failed to create Utf8StringRef");
+        let value_1 = AttributeValue::from(string_1);
+        let directory_string_1 =
+            DirectoryString::try_from(&value_1).expect("Failed to convert to directory string");
+
+        let string_2 = Utf8StringRef::new(str_2).expect("Failed to create Utf8StringRef");
+        let value_2 = AttributeValue::from(string_2);
+        let directory_string_2 =
+            DirectoryString::try_from(&value_2).expect("Failed to convert to directory string");
+
+        assert_eq!(directory_string_1, directory_string_2);
+    }
+
+    #[parameterized(
+        same = {"string with @", "string with @"},
+        case_fold = {"Capitalized Ampersand &", "capitalized ampersand &"},
+        space_compression = {"A          big        distance   from     @", "A big distance from @"},
+    )]
+    fn compare_ia5_strings(str_1: &str, str_2: &str) {
+        let string_1 = Ia5StringRef::new(str_1).expect("Failed to create Ia5StringRef");
+        let value_1 = AttributeValue::from(string_1);
+        let directory_string_1 =
+            DirectoryString::try_from(&value_1).expect("Failed to convert to directory string");
+
+        let string_2 = Ia5StringRef::new(str_2).expect("Failed to create Ia5StringRef");
+        let value_2 = AttributeValue::from(string_2);
+        let directory_string_2 =
+            DirectoryString::try_from(&value_2).expect("Failed to convert to directory string");
+
+        assert_eq!(directory_string_1, directory_string_2);
+    }
+
+    #[test]
+    fn unsupported_directory_string_type() {
+        let teletex_string =
+            TeletexStringRef::new("Hello").expect("Failed to create TeletexStringRef");
+        let attribute_value = AttributeValue::from(teletex_string);
+        let byte = Tag::TeletexString.octet();
+        assert_eq!(
+            DirectoryString::try_from(&attribute_value),
+            Err(x509_cert::der::Error::new(
+                TagUnknown { byte },
+                Length::from(0u8)
+            ))
+        );
+    }
+
+    #[parameterized(
+    name_1 = {"C=US,O=Test Certificates 2011,CN=Trust Anchor"},
+    name_2 = {"C=US,O=Test Certificates 2011,CN=Good CA"},
+    name_3 = {"C=US,O=Test Certificates 2011,CN=Valid EE Certificate Test1"},
+    multiple_first_rdns = {"C=US+C=CA+C=UK,O=Test Certificates 2011,CN=Trust Anchor"},
+    multiple_middle_rdns = {"C=US,O=Test Certificates 2011+O=More Stuff,CN=Trust Anchor"},
+    multiple_last_rdns = {"C=US,O=Test Certificates 2011,CN=Trust Anchor+CN=You Know It"},
+    )]
+    fn matched_distinguished_names(name: &str) {
+        let name_1 = Name::from_str(name).expect("Failed to parse name");
+        let name_2 = name_1.clone();
+        assert_eq!(
+            DistinguishedName::from(&name_1),
+            DistinguishedName::from(&name_2)
+        );
+    }
+
+    #[parameterized(
+    first = {"C=US,O=Test Certificates 2011,CN=Trust Anchor", "C=IS,O=Test Certificates 2011,CN=Trust Anchor"},
+    middle = {"C=US,O=Test Certificates 2011,CN=Good CA", "C=US,O=Test Certificate 2011,CN=Good CA"},
+    last = {"C=US,O=Test Certificates 2011,CN=Valid EE Certificate Test1", "C=US,O=Test Certificates 2011,CN=Invalid EE Certificate Test1"},
+    different_lengths = {"C=US,O=Test Certificates 2011", "C=US,O=Test Certificates 2011,CN=Valid EE Certificate Test1"},
+    different_rdn_lengths = {"C=US+C=CA+C=UK", "C=US+C=CA"},
+    different_oids = {"C=US", "CN=US"},
+    )]
+    fn mismatched_distinguished_names(name_1: &str, name_2: &str) {
+        let name_1 = Name::from_str(name_1).expect("Failed to parse name");
+        let name_2 = Name::from_str(name_2).expect("Failed to parse name");
+        assert_ne!(
+            DistinguishedName::from(&name_1),
+            DistinguishedName::from(&name_2)
+        );
+    }
+
+    #[test]
+    fn distinguished_name_build_up() {
+        // This test builds up a `DistinguishedName` type manually and compares
+        // to show that subsequent test cases fail due to unsupported string
+        // types and not a failure to build up the `DistinguishedName`.
+        let common_message = "Hello";
+        let oid = ORGANIZATION_NAME;
+
+        let string_1 =
+            PrintableStringRef::new(common_message).expect("Failed to create PrintableStringRef");
+        let attribute_type_value_1 = AttributeTypeAndValue {
+            oid,
+            value: AttributeValue::from(string_1),
+        };
+        let rdn_1 = RelativeDistinguishedName::from(
+            SetOfVec::try_from([attribute_type_value_1])
+                .expect("Failed to build `RelativeDistinguishedName`"),
+        );
+        let name_1 = Name::from(vec![rdn_1]);
+
+        let string_2 =
+            PrintableStringRef::new(common_message).expect("Failed to create PrintableStringRef");
+        let attribute_type_value_2 = AttributeTypeAndValue {
+            oid,
+            value: AttributeValue::from(string_2),
+        };
+        let rdn_2 = RelativeDistinguishedName::from(
+            SetOfVec::try_from([attribute_type_value_2])
+                .expect("Failed to build `RelativeDistinguishedName`"),
+        );
+        let name_2 = Name::from(vec![rdn_2]);
+        assert_eq!(
+            DistinguishedName::from(&name_1),
+            DistinguishedName::from(&name_2)
+        );
+    }
+
+    #[test]
+    fn teletxstring_as_first_distinguished_name_fails() {
+        let common_message = "Hello";
+        let oid = ORGANIZATION_NAME;
+
+        let string_1 =
+            TeletexStringRef::new(common_message).expect("Failed to create TeletexStringRef");
+        let attribute_type_value_1 = AttributeTypeAndValue {
+            oid,
+            value: AttributeValue::from(string_1),
+        };
+        let rdn_1 = RelativeDistinguishedName::from(
+            SetOfVec::try_from([attribute_type_value_1])
+                .expect("Failed to build `RelativeDistinguishedName`"),
+        );
+        let name_1 = Name::from(vec![rdn_1]);
+
+        let string_2 =
+            PrintableStringRef::new(common_message).expect("Failed to create PrintableStringRef");
+        let attribute_type_value_2 = AttributeTypeAndValue {
+            oid,
+            value: AttributeValue::from(string_2),
+        };
+        let rdn_2 = RelativeDistinguishedName::from(
+            SetOfVec::try_from([attribute_type_value_2])
+                .expect("Failed to build `RelativeDistinguishedName`"),
+        );
+        let name_2 = Name::from(vec![rdn_2]);
+        assert_ne!(
+            DistinguishedName::from(&name_1),
+            DistinguishedName::from(&name_2)
+        );
+    }
+
+    #[test]
+    fn teletxstring_as_second_distinguished_name_fails() {
+        let common_message = "Hello";
+        let oid = ORGANIZATION_NAME;
+
+        let string_1 =
+            PrintableStringRef::new(common_message).expect("Failed to create PrintableStringRef");
+        let attribute_type_value_1 = AttributeTypeAndValue {
+            oid,
+            value: AttributeValue::from(string_1),
+        };
+        let rdn_1 = RelativeDistinguishedName::from(
+            SetOfVec::try_from([attribute_type_value_1])
+                .expect("Failed to build `RelativeDistinguishedName`"),
+        );
+        let name_1 = Name::from(vec![rdn_1]);
+
+        let string_2 =
+            TeletexStringRef::new(common_message).expect("Failed to create TeletexStringRef");
+        let attribute_type_value_2 = AttributeTypeAndValue {
+            oid,
+            value: AttributeValue::from(string_2),
+        };
+        let rdn_2 = RelativeDistinguishedName::from(
+            SetOfVec::try_from([attribute_type_value_2])
+                .expect("Failed to build `RelativeDistinguishedName`"),
+        );
+        let name_2 = Name::from(vec![rdn_2]);
+        assert_ne!(
+            DistinguishedName::from(&name_1),
+            DistinguishedName::from(&name_2)
+        );
+    }
+}
diff --git a/verifier/src/x509/rfc4518.rs b/verifier/src/x509/rfc4518.rs
new file mode 100644
index 0000000..a0b2c66
--- /dev/null
+++ b/verifier/src/x509/rfc4518.rs
@@ -0,0 +1,153 @@
+// Copyright (c) 2023 The MobileCoin Foundation
+
+//! An implementation of https://www.rfc-editor.org/rfc/rfc4518 string preparation.
+//!
+//! A good document on normal forms, https://unicode.org/reports/tr15/#Norm_Forms
+extern crate alloc;
+
+use alloc::string::String;
+use unicode_normalization::UnicodeNormalization;
+
+/// An RFC4518 prepared String
+#[derive(Debug, PartialEq)]
+pub struct Rfc4518String {
+    inner: String,
+}
+
+impl From<&str> for Rfc4518String {
+    fn from(string: &str) -> Self {
+        let normalized = string
+            .chars()
+            .filter_map(rfc_4518_filter_map)
+            .nfkc()
+            .collect::<String>();
+        let inner = space_compression(&normalized);
+        Self { inner }
+    }
+}
+
+impl<'a> From<&'a Rfc4518String> for &'a str {
+    fn from(value: &'a Rfc4518String) -> &'a str {
+        &value.inner
+    }
+}
+
+/// Perform step 2 of the string preparation,
+/// https://www.rfc-editor.org/rfc/rfc4518#section-2.2
+///
+/// Note RFC4518 calls this step map, but it also _filters_ out certain
+/// characters, thus the name deviation.
+fn rfc_4518_filter_map(c: char) -> Option<char> {
+    // per https://doc.rust-lang.org/std/primitive.char.html#method.is_whitespace
+    // uses to the same values in
+    // https://www.rfc-editor.org/rfc/rfc4518#section-2.2 that map to space.
+    if c.is_whitespace() {
+        Some(' ')
+    } else {
+        rfc_4518_filter(c)
+    }
+}
+
+/// Filter out characters from
+/// https://www.rfc-editor.org/rfc/rfc4518#section-2.2 that map to nothing.
+///
+/// Note it says:
+///
+///   VARIATION SELECTORs (U+180B-180D, FF00-FE0F)
+///
+/// However that should be `FE00-FE0F` as per
+/// https://www.rfc-editor.org/rfc/rfc3454#appendix-B.1
+///
+fn rfc_4518_filter(c: char) -> Option<char> {
+    match c {
+        '\u{0000}'..='\u{0008}'
+        | '\u{000E}'..='\u{001F}'
+        | '\u{007F}'..='\u{0084}'
+        | '\u{0086}'..='\u{009F}'
+        | '\u{00AD}'
+        | '\u{034F}'
+        | '\u{06DD}'
+        | '\u{070F}'
+        | '\u{1806}'
+        | '\u{180B}'..='\u{180E}'
+        | '\u{200B}'..='\u{200F}'
+        | '\u{202A}'..='\u{202E}'
+        | '\u{2060}'..='\u{2063}'
+        | '\u{206A}'..='\u{206F}'
+        | '\u{FE00}'..='\u{FE0F}'
+        | '\u{FEFF}'
+        | '\u{FFF9}'..='\u{FFFC}'
+        | '\u{1D173}'..='\u{1D17A}'
+        | '\u{E0001}'
+        | '\u{E0020}'..='\u{E0074}' => None,
+        c => Some(c),
+    }
+}
+
+/// Compress spaces as defined in https://www.rfc-editor.org/rfc/rfc4518#section-2.6.1
+///
+/// The resultant string will have:
+/// - One leading space
+/// - One trailing space.
+/// - Any consecutive intermediate spaces will be converted to two spaces. i.e.
+///   4 consecutive spaces will be 2, but also 1 lone space will be converted
+///   to 2 spaces.
+fn space_compression(s: &str) -> String {
+    // Strings are either empty => <space><space>
+    // or they have a leading space so always start with a space
+    let mut result = String::from(" ");
+
+    let mut last_char = ' ';
+
+    for c in s.trim_end().chars() {
+        if c == ' ' {
+            if last_char == ' ' {
+                continue;
+            } else {
+                // An extra space for the two spaces specified in
+                // https://www.rfc-editor.org/rfc/rfc4518#section-2.6.1
+                result.push(' ');
+            }
+        }
+        result.push(c);
+        last_char = c;
+    }
+
+    // Strings are either empty => <space><space>
+    // or they have a trailing space so always end with a space
+    result.push(' ');
+    result
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use yare::parameterized;
+
+    #[parameterized(
+        empty = {"", "  "},
+        mulitple_spaces = {"      ", "  "},
+        leading_and_trailing_space = {"Hello", " Hello "},
+        intermediate_space = {"Hello world", " Hello  world "},
+        foo_space_bar_space_space = {"foo bar  ", " foo  bar "},
+        so_many_spaces = {"       What     it      is?      ", " What  it  is? "},
+    )]
+    fn insignificant_space_handling(input: &str, expected: &str) {
+        let result = space_compression(input);
+        assert_eq!(&result, expected);
+    }
+
+    #[parameterized(
+        empty = {"", "  "},
+        ignored_control_character_0000 = {"Hello \u{0000}world", " Hello  world "},
+        ignored_control_character_0704 = {"Hello\u{070F}world", " Helloworld "},
+        whitespace_is_the_same_as_space = {"\n\t\n\tHello\nworld\n\t\n\t\n", " Hello  world "},
+        normalizing_nfkc_fi = {"fi", " fi "}, // U+FB01, U+0066 U+0069
+        normalizing_nfkc_25 = {"2⁵", " 25 "}, // U+0032 U+2075, U+0032 U+0035
+        normalizing_nfkc_tel = {"℡", " TEL "}, // U+2121, U+0054 U+0045 U+004C
+    )]
+    fn to_rfc4518_string(input: &str, expected: &str) {
+        let result = Rfc4518String::from(input);
+        assert_eq!(&result.inner, expected);
+    }
+}