diff --git a/Cargo.toml b/Cargo.toml index 0d88e613..7c31f44b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ include = [ "src/name/dns_name.rs", "src/name/ip_address.rs", "src/name/verify.rs", + "src/name/name.rs", "src/signed_data.rs", "src/time.rs", "src/trust_anchor.rs", diff --git a/src/end_entity.rs b/src/end_entity.rs index 1161c305..3674c901 100644 --- a/src/end_entity.rs +++ b/src/end_entity.rs @@ -13,7 +13,7 @@ // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. use crate::{ - cert, name, signed_data, verify_cert, DnsNameRef, Error, SignatureAlgorithm, + cert, name, signed_data, verify_cert, DnsNameRef, Error, SignatureAlgorithm, SubjectNameRef, TLSClientTrustAnchors, TLSServerTrustAnchors, Time, }; use core::convert::TryFrom; @@ -27,6 +27,9 @@ use core::convert::TryFrom; /// certificate is currently valid *for use by a TLS server*. /// * `EndEntityCert.verify_is_valid_for_dns_name`: Verify that the server's /// certificate is valid for the host that is being connected to. +/// * `EndEntityCert.verify_is_valid_for_subject_name`: Verify that the server's +/// certificate is valid for the host or IP address that is being connected to. +/// /// * `EndEntityCert.verify_signature`: Verify that the signature of server's /// `ServerKeyExchange` message is valid for the server's certificate. /// @@ -148,6 +151,14 @@ impl<'a> EndEntityCert<'a> { name::verify_cert_dns_name(self, dns_name) } + /// Verifies that the certificate is valid for the given Subject Name. + pub fn verify_is_valid_for_subject_name( + &self, + subject_name: SubjectNameRef, + ) -> Result<(), Error> { + name::verify_cert_subject_name(self, subject_name) + } + /// Verifies the signature `signature` of message `msg` using the /// certificate's public key. /// diff --git a/src/lib.rs b/src/lib.rs index a70afe25..b9b4b363 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,7 +49,10 @@ mod verify_cert; pub use { end_entity::EndEntityCert, error::Error, - name::{DnsNameRef, InvalidDnsNameError}, + name::{ + ip_address::AddrParseError, ip_address::IpAddrRef, DnsNameRef, InvalidDnsNameError, + InvalidSubjectNameError, SubjectNameRef, + }, signed_data::{ SignatureAlgorithm, ECDSA_P256_SHA256, ECDSA_P256_SHA384, ECDSA_P384_SHA256, ECDSA_P384_SHA384, ED25519, @@ -60,7 +63,7 @@ pub use { #[cfg(feature = "alloc")] pub use { - name::DnsName, + name::{ip_address::IpAddr, DnsName}, signed_data::{ RSA_PKCS1_2048_8192_SHA256, RSA_PKCS1_2048_8192_SHA384, RSA_PKCS1_2048_8192_SHA512, RSA_PKCS1_3072_8192_SHA384, RSA_PSS_2048_8192_SHA256_LEGACY_KEY, diff --git a/src/name.rs b/src/name.rs index 040a8133..61c78651 100644 --- a/src/name.rs +++ b/src/name.rs @@ -1,4 +1,4 @@ -// Copyright 2015-2020 Brian Smith. +// Copyright 2022 Rafael Fernández López. // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -19,7 +19,11 @@ pub use dns_name::{DnsNameRef, InvalidDnsNameError}; #[cfg(feature = "alloc")] pub use dns_name::DnsName; -mod ip_address; +#[allow(clippy::module_inception)] +mod name; +pub use name::{InvalidSubjectNameError, SubjectNameRef}; + +pub mod ip_address; mod verify; -pub(super) use verify::{check_name_constraints, verify_cert_dns_name}; +pub(super) use verify::{check_name_constraints, verify_cert_dns_name, verify_cert_subject_name}; diff --git a/src/name/dns_name.rs b/src/name/dns_name.rs index e4f18f20..348d9a0f 100644 --- a/src/name/dns_name.rs +++ b/src/name/dns_name.rs @@ -77,7 +77,7 @@ impl From> for DnsName { /// /// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2 #[derive(Clone, Copy)] -pub struct DnsNameRef<'a>(&'a [u8]); +pub struct DnsNameRef<'a>(pub(crate) &'a [u8]); impl AsRef<[u8]> for DnsNameRef<'_> { #[inline] @@ -139,6 +139,13 @@ impl core::fmt::Debug for DnsNameRef<'_> { } } +#[cfg(not(feature = "alloc"))] +impl core::fmt::Debug for DnsNameRef<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + f.debug_tuple("DnsNameRef").field(&self.0).finish() + } +} + impl<'a> From> for &'a str { fn from(DnsNameRef(d): DnsNameRef<'a>) -> Self { // The unwrap won't fail because DnsNameRefs are guaranteed to be ASCII diff --git a/src/name/ip_address.rs b/src/name/ip_address.rs index 1eedf169..f5ac5ced 100644 --- a/src/name/ip_address.rs +++ b/src/name/ip_address.rs @@ -12,8 +12,217 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +use core::{convert::TryInto, fmt::Write}; + use crate::Error; +#[cfg(feature = "alloc")] +use alloc::string::String; + +const VALID_IP_BY_CONSTRUCTION: &str = "IP address is a valid string by construction"; + +/// Either a IPv4 or IPv6 address, plus its owned string representation +#[cfg(feature = "alloc")] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum IpAddr { + /// An IPv4 address and its owned string representation + V4(String, [u8; 4]), + /// An IPv6 address and its owned string representation + V6(String, [u8; 16]), +} + +#[cfg(feature = "alloc")] +impl AsRef for IpAddr { + fn as_ref(&self) -> &str { + match self { + IpAddr::V4(ip_address, _) | IpAddr::V6(ip_address, _) => ip_address.as_str(), + } + } +} + +/// Either a IPv4 or IPv6 address, plus its borrowed string representation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IpAddrRef<'a> { + /// An IPv4 address and its borrowed string representation + V4(&'a [u8], [u8; 4]), + /// An IPv6 address and its borrowed string representation + V6(&'a [u8], [u8; 16]), +} + +#[cfg(feature = "alloc")] +impl<'a> From> for IpAddr { + fn from(ip_address: IpAddrRef<'a>) -> IpAddr { + match ip_address { + IpAddrRef::V4(ip_address, ip_address_octets) => IpAddr::V4( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + ip_address_octets, + ), + IpAddrRef::V6(ip_address, ip_address_octets) => IpAddr::V6( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + ip_address_octets, + ), + } + } +} + +#[cfg(feature = "alloc")] +impl<'a> From<&'a IpAddr> for IpAddrRef<'a> { + fn from(ip_address: &'a IpAddr) -> IpAddrRef<'a> { + match ip_address { + IpAddr::V4(ip_address, ip_address_octets) => { + IpAddrRef::V4(ip_address.as_bytes(), *ip_address_octets) + } + IpAddr::V6(ip_address, ip_address_octets) => { + IpAddrRef::V6(ip_address.as_bytes(), *ip_address_octets) + } + } + } +} + +/// An error indicating that an `IpAddrRef` could not built because +/// the input could not be parsed as an IP address. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AddrParseError; + +impl core::fmt::Display for AddrParseError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Requires the `std` feature. +#[cfg(feature = "std")] +impl ::std::error::Error for AddrParseError {} + +impl<'a> IpAddrRef<'a> { + /// Constructs an `IpAddrRef` from the given input if the input is + /// a valid IPv4 or IPv6 address. + pub fn try_from_ascii(ip_address: &'a [u8]) -> Result { + if let Ok(ip_address) = parse_ipv4_address(ip_address) { + Ok(ip_address) + } else if let Ok(ip_address) = parse_ipv6_address(ip_address) { + Ok(ip_address) + } else { + Err(AddrParseError) + } + } + + /// Constructs an `IpAddrRef` from the given input if the input is a + /// valid IP address. + pub fn try_from_ascii_str(ip_address: &'a str) -> Result { + Self::try_from_ascii(ip_address.as_bytes()) + } + + /// Constructs an `IpAddr` from this `IpAddrRef` + /// + /// Requires the `alloc` feature. + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> IpAddr { + match self { + IpAddrRef::V4(ip_address, ip_address_octets) => IpAddr::V4( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + *ip_address_octets, + ), + IpAddrRef::V6(ip_address, ip_address_octets) => IpAddr::V6( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + *ip_address_octets, + ), + } + } +} + +#[cfg(feature = "std")] +fn ipv6_to_uncompressed_string(octets: [u8; 16]) -> String { + let mut result = String::with_capacity(39); + for i in 0..7 { + result + .write_fmt(format_args!( + "{:02x?}{:02x?}:", + octets[i * 2], + octets[(i * 2) + 1] + )) + .expect("unexpected error while formatting IPv6 address"); + } + result + .write_fmt(format_args!("{:02x?}{:02x?}", octets[14], octets[15])) + .expect("unexpected error while formatting IPv6 address"); + + result +} + +#[cfg(feature = "std")] +impl From for IpAddr { + fn from(ip_address: std::net::IpAddr) -> IpAddr { + match ip_address { + std::net::IpAddr::V4(ip_address) => { + IpAddr::V4(ip_address.to_string(), ip_address.octets()) + } + std::net::IpAddr::V6(ip_address) => IpAddr::V6( + // We cannot rely on the Display implementation of + // std::net::Ipv6Addr given that it might return + // compressed IPv6 addresses if the address can be + // expressed in such form. However, given we don't + // support the IPv6 compressed form, we should not + // generate such format either when converting from a + // type that supports it. + ipv6_to_uncompressed_string(ip_address.octets()), + ip_address.octets(), + ), + } + } +} + +impl<'a> From> for &'a str { + fn from(ip_address: IpAddrRef<'a>) -> &'a str { + match ip_address { + IpAddrRef::V4(ip_address, _) | IpAddrRef::V6(ip_address, _) => { + core::str::from_utf8(ip_address).expect(VALID_IP_BY_CONSTRUCTION) + } + } + } +} + +impl<'a> From> for &'a [u8] { + fn from(ip_address: IpAddrRef<'a>) -> &'a [u8] { + match ip_address { + IpAddrRef::V4(ip_address, _) | IpAddrRef::V6(ip_address, _) => ip_address, + } + } +} + +// https://tools.ietf.org/html/rfc5280#section-4.2.1.6 says: +// When the subjectAltName extension contains an iPAddress, the address +// MUST be stored in the octet string in "network byte order", as +// specified in [RFC791]. The least significant bit (LSB) of each octet +// is the LSB of the corresponding byte in the network address. For IP +// version 4, as specified in [RFC791], the octet string MUST contain +// exactly four octets. For IP version 6, as specified in +// [RFC2460], the octet string MUST contain exactly sixteen octets. +pub(super) fn presented_id_matches_reference_id( + presented_id: untrusted::Input, + reference_id: untrusted::Input, +) -> Result { + match (presented_id.len(), reference_id.len()) { + (4, 4) => (), + (16, 16) => (), + _ => { + return Ok(false); + } + }; + + let mut presented_ip_address = untrusted::Reader::new(presented_id); + let mut reference_ip_address = untrusted::Reader::new(reference_id); + while !presented_ip_address.at_end() { + let presented_ip_address_byte = presented_ip_address.read_byte().unwrap(); + let reference_ip_address_byte = reference_ip_address.read_byte().unwrap(); + if presented_ip_address_byte != reference_ip_address_byte { + return Ok(false); + } + } + + Ok(true) +} + // https://tools.ietf.org/html/rfc5280#section-4.2.1.10 says: // // For IPv4 addresses, the iPAddress field of GeneralName MUST contain @@ -62,3 +271,1026 @@ pub(super) fn presented_id_matches_constraint( Ok(true) } + +pub(crate) fn parse_ipv4_address(ip_address_: &[u8]) -> Result { + let mut ip_address = untrusted::Reader::new(untrusted::Input::from(ip_address_)); + let mut is_first_byte = true; + let mut current_octet: [u8; 3] = [0, 0, 0]; + let mut current_size = 0; + let mut dot_count = 0; + + let mut octet = 0; + let mut octets: [u8; 4] = [0, 0, 0, 0]; + + // Returns a u32 so it's possible to identify (and error) when + // provided textual octets > 255, not representable by u8. + fn radix10_to_octet(textual_octets: &[u8]) -> u32 { + let mut result: u32 = 0; + for digit in textual_octets.iter() { + result *= 10; + result += u32::from(*digit); + } + result + } + + loop { + match ip_address.read_byte() { + Ok(b'.') => { + if is_first_byte { + // IPv4 address cannot start with a dot. + return Err(AddrParseError); + } + if ip_address.at_end() { + // IPv4 address cannot end with a dot. + return Err(AddrParseError); + } + if dot_count == 3 { + // IPv4 address cannot have more than three dots. + return Err(AddrParseError); + } + dot_count += 1; + if current_size == 0 { + // IPv4 address cannot contain two dots in a row. + return Err(AddrParseError); + } + let current_raw_octet = radix10_to_octet(¤t_octet[..current_size]); + if current_raw_octet > 255 { + // No octet can be greater than 255. + return Err(AddrParseError); + } + octets[octet] = + TryInto::::try_into(current_raw_octet).expect("invalid character"); + octet += 1; + // We move on to the next textual octet. + current_octet = [0, 0, 0]; + current_size = 0; + } + Ok(number @ b'0'..=b'9') => { + if number == b'0' + && current_size == 0 + && !ip_address.peek(b'.') + && !ip_address.at_end() + { + // No octet can start with 0 if a dot does not follow and if we are not at the end. + return Err(AddrParseError); + } + if current_size >= current_octet.len() { + // More than 3 octets in a triple + return Err(AddrParseError); + } + current_octet[current_size] = number - b'0'; + current_size += 1; + } + _ => { + return Err(AddrParseError); + } + } + is_first_byte = false; + + if ip_address.at_end() { + let last_octet = radix10_to_octet(¤t_octet[..current_size]); + if current_size > 0 && last_octet > 255 { + // No octet can be greater than 255. + return Err(AddrParseError); + } + octets[octet] = TryInto::::try_into(last_octet).expect("invalid character"); + break; + } + } + if dot_count != 3 { + return Err(AddrParseError); + } + Ok(IpAddrRef::V4(ip_address_, octets)) +} + +pub(crate) fn parse_ipv6_address(ip_address_: &[u8]) -> Result { + // Compressed addresses are not supported. Also, IPv4-mapped IPv6 + // addresses are not supported. This makes 8 groups of 4 + // hexadecimal characters + 7 colons. + if ip_address_.len() != 39 { + return Err(AddrParseError); + } + + let mut ip_address = untrusted::Reader::new(untrusted::Input::from(ip_address_)); + let mut is_first_byte = true; + let mut current_textual_block_size = 0; + let mut colon_count = 0; + + let mut octet = 0; + let mut previous_character = None; + let mut octets: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + loop { + match ip_address.read_byte() { + Ok(b':') => { + if is_first_byte { + // Uncompressed IPv6 address cannot start with a colon. + return Err(AddrParseError); + } + if ip_address.at_end() { + // Uncompressed IPv6 address cannot end with a colon. + return Err(AddrParseError); + } + if colon_count == 7 { + // IPv6 address cannot have more than seven colons. + return Err(AddrParseError); + } + colon_count += 1; + if current_textual_block_size == 0 { + // Uncompressed IPv6 address cannot contain two colons in a row. + return Err(AddrParseError); + } + if current_textual_block_size != 4 { + // Compressed IPv6 addresses are not supported. + return Err(AddrParseError); + } + // We move on to the next textual block. + current_textual_block_size = 0; + previous_character = None; + } + Ok(character @ b'0'..=b'9') + | Ok(character @ b'a'..=b'f') + | Ok(character @ b'A'..=b'F') => { + if current_textual_block_size == 4 { + // Blocks cannot contain more than 4 hexadecimal characters. + return Err(AddrParseError); + } + if let Some(previous_character_) = previous_character { + octets[octet] = (TryInto::::try_into( + TryInto::::try_into( + (TryInto::::try_into(previous_character_) + .expect("invalid character")) + .to_digit(16) + // Safe to unwrap because we know character is within hexadecimal bounds ([0-9a-f]) + .unwrap(), + ) + .expect("invalid character"), + ) + .expect("invalid character") + << 4) + | (TryInto::::try_into( + TryInto::::try_into(character) + .expect("invalid character") + .to_digit(16) + // Safe to unwrap because we know character is within hexadecimal bounds ([0-9a-f]) + .unwrap(), + ) + .expect("invalid character")); + previous_character = None; + octet += 1; + } else { + previous_character = Some(character); + } + current_textual_block_size += 1; + } + _ => { + return Err(AddrParseError); + } + } + is_first_byte = false; + + if ip_address.at_end() { + break; + } + } + if colon_count != 7 { + return Err(AddrParseError); + } + Ok(IpAddrRef::V6(ip_address_, octets)) +} + +#[cfg(test)] +mod tests { + use super::*; + + const fn ipv4_address( + ip_address: &[u8], + octets: [u8; 4], + ) -> (&[u8], Result) { + (ip_address, Ok(IpAddrRef::V4(ip_address, octets))) + } + + const IPV4_ADDRESSES: &[(&[u8], Result)] = &[ + // Valid IPv4 addresses + ipv4_address(b"0.0.0.0", [0, 0, 0, 0]), + ipv4_address(b"1.1.1.1", [1, 1, 1, 1]), + ipv4_address(b"205.0.0.0", [205, 0, 0, 0]), + ipv4_address(b"0.205.0.0", [0, 205, 0, 0]), + ipv4_address(b"0.0.205.0", [0, 0, 205, 0]), + ipv4_address(b"0.0.0.205", [0, 0, 0, 205]), + ipv4_address(b"0.0.0.20", [0, 0, 0, 20]), + // Invalid IPv4 addresses + (b"", Err(AddrParseError)), + (b"...", Err(AddrParseError)), + (b".0.0.0.0", Err(AddrParseError)), + (b"0.0.0.0.", Err(AddrParseError)), + (b"0.0.0", Err(AddrParseError)), + (b"0.0.0.", Err(AddrParseError)), + (b"256.0.0.0", Err(AddrParseError)), + (b"0.256.0.0", Err(AddrParseError)), + (b"0.0.256.0", Err(AddrParseError)), + (b"0.0.0.256", Err(AddrParseError)), + (b"1..1.1.1", Err(AddrParseError)), + (b"1.1..1.1", Err(AddrParseError)), + (b"1.1.1..1", Err(AddrParseError)), + (b"025.0.0.0", Err(AddrParseError)), + (b"0.025.0.0", Err(AddrParseError)), + (b"0.0.025.0", Err(AddrParseError)), + (b"0.0.0.025", Err(AddrParseError)), + (b"1234.0.0.0", Err(AddrParseError)), + (b"0.1234.0.0", Err(AddrParseError)), + (b"0.0.1234.0", Err(AddrParseError)), + (b"0.0.0.1234", Err(AddrParseError)), + ]; + + #[test] + fn parse_ipv4_address_test() { + for &(ip_address, expected_result) in IPV4_ADDRESSES { + assert_eq!(parse_ipv4_address(ip_address), expected_result,); + } + } + + const fn ipv6_address( + ip_address: &[u8], + octets: [u8; 16], + ) -> (&[u8], Result) { + (ip_address, Ok(IpAddrRef::V6(ip_address, octets))) + } + + const IPV6_ADDRESSES: &[(&[u8], Result)] = &[ + // Valid IPv6 addresses + ipv6_address( + b"2a05:d018:076c:b685:e8ab:afd3:af51:3aed", + [ + 0x2a, 0x05, 0xd0, 0x18, 0x07, 0x6c, 0xb6, 0x85, 0xe8, 0xab, 0xaf, 0xd3, 0xaf, 0x51, + 0x3a, 0xed, + ], + ), + ipv6_address( + b"2A05:D018:076C:B685:E8AB:AFD3:AF51:3AED", + [ + 0x2a, 0x05, 0xd0, 0x18, 0x07, 0x6c, 0xb6, 0x85, 0xe8, 0xab, 0xaf, 0xd3, 0xaf, 0x51, + 0x3a, 0xed, + ], + ), + ipv6_address( + b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + ), + ipv6_address( + b"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + ), + ipv6_address( + b"FFFF:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + ), + // Invalid IPv6 addresses + // Missing octets on uncompressed addresses. The unmatching letter has the violation + ( + b"aaa:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:aaa:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:aaa:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:aaa:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:aaa:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:aaa:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:ffff:aaa:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:aaa", + Err(AddrParseError), + ), + // Wrong hexadecimal characters on different positions + ( + b"ffgf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:gfff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:fffg:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffgf:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:gfff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:fgff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:ffff:ffgf:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:ffff:ffgf:fffg", + Err(AddrParseError), + ), + // Wrong colons on uncompressed addresses + ( + b":ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff::ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff::ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff::ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff::ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff::ffff:ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:ffff::ffff:ffff", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:ffff:ffff::ffff", + Err(AddrParseError), + ), + // More colons than allowed + ( + b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:", + Err(AddrParseError), + ), + ( + b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError), + ), + // v Invalid UTF-8 encoding + ( + b"\xc3\x28a05:d018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError), + ), + // v Invalid hexadecimal + ( + b"ga05:d018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError), + ), + // Cannot start with colon + ( + b":a05:d018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError), + ), + // Cannot end with colon + ( + b"2a05:d018:076c:b685:e8ab:afd3:af51:3ae:", + Err(AddrParseError), + ), + // Cannot have more than seven colons + ( + b"2a05:d018:076c:b685:e8ab:afd3:af51:3a::", + Err(AddrParseError), + ), + // Cannot contain two colons in a row + ( + b"2a05::018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError), + ), + // v Textual block size is longer + ( + b"2a056:d018:076c:b685:e8ab:afd3:af51:3ae", + Err(AddrParseError), + ), + // v Textual block size is shorter + ( + b"2a0:d018:076c:b685:e8ab:afd3:af51:3aed ", + Err(AddrParseError), + ), + // Shorter IPv6 address + (b"d018:076c:b685:e8ab:afd3:af51:3aed", Err(AddrParseError)), + // Longer IPv6 address + ( + b"2a05:d018:076c:b685:e8ab:afd3:af51:3aed3aed", + Err(AddrParseError), + ), + // These are valid IPv6 addresses, but we don't support compressed addresses + (b"0:0:0:0:0:0:0:1", Err(AddrParseError)), + ( + b"2a05:d018:76c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError), + ), + ]; + + #[test] + fn parse_ipv6_address_test() { + for &(ip_address, expected_result) in IPV6_ADDRESSES { + assert_eq!(parse_ipv6_address(ip_address), expected_result,); + } + } + + #[test] + fn try_from_ascii_ip_address_test() { + const IP_ADDRESSES: &[(&[u8], Result)] = &[ + // Valid IPv4 addresses + ( + b"127.0.0.1", + Ok(IpAddrRef::V4(b"127.0.0.1", [127, 0, 0, 1])), + ), + // Invalid IPv4 addresses + ( + // Ends with a dot; misses one octet + b"127.0.0.", + Err(AddrParseError), + ), + // Valid IPv6 addresses + ( + b"0000:0000:0000:0000:0000:0000:0000:0001", + Ok(IpAddrRef::V6( + b"0000:0000:0000:0000:0000:0000:0000:0001", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + )), + ), + // Invalid IPv6 addresses + ( + // IPv6 addresses in compressed form are not supported + b"0:0:0:0:0:0:0:1", + Err(AddrParseError), + ), + // Something else + ( + // A hostname + b"example.com", + Err(AddrParseError), + ), + ]; + for &(ip_address, expected_result) in IP_ADDRESSES { + assert_eq!(IpAddrRef::try_from_ascii(ip_address), expected_result) + } + } + + #[test] + fn try_from_ascii_str_ip_address_test() { + const IP_ADDRESSES: &[(&str, Result)] = &[ + // Valid IPv4 addresses + ("127.0.0.1", Ok(IpAddrRef::V4(b"127.0.0.1", [127, 0, 0, 1]))), + // Invalid IPv4 addresses + ( + // Ends with a dot; misses one octet + "127.0.0.", + Err(AddrParseError), + ), + // Valid IPv6 addresses + ( + "0000:0000:0000:0000:0000:0000:0000:0001", + Ok(IpAddrRef::V6( + b"0000:0000:0000:0000:0000:0000:0000:0001", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + )), + ), + // Invalid IPv6 addresses + ( + // IPv6 addresses in compressed form are not supported + "0:0:0:0:0:0:0:1", + Err(AddrParseError), + ), + // Something else + ( + // A hostname + "example.com", + Err(AddrParseError), + ), + ]; + for &(ip_address, expected_result) in IP_ADDRESSES { + assert_eq!(IpAddrRef::try_from_ascii_str(ip_address), expected_result) + } + } + + #[test] + fn str_from_ip_address_ref_test() { + let ip_addresses = vec![ + // IPv4 addresses + (IpAddrRef::V4(b"127.0.0.1", [127, 0, 0, 1]), "127.0.0.1"), + // IPv6 addresses + ( + IpAddrRef::V6( + b"0000:0000:0000:0000:0000:0000:0000:0001", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ), + "0000:0000:0000:0000:0000:0000:0000:0001", + ), + ]; + for (ip_address, expected_ip_address) in ip_addresses { + assert_eq!(Into::<&str>::into(ip_address), expected_ip_address,) + } + } + + #[test] + fn u8_array_from_ip_address_ref_test() { + let ip_addresses = vec![ + // IPv4 addresses + (IpAddrRef::V4(b"127.0.0.1", [127, 0, 0, 1]), "127.0.0.1"), + // IPv6 addresses + ( + IpAddrRef::V6( + b"0000:0000:0000:0000:0000:0000:0000:0001", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ), + "0000:0000:0000:0000:0000:0000:0000:0001", + ), + ]; + for (ip_address, expected_ip_address) in ip_addresses { + assert_eq!( + Into::<&[u8]>::into(ip_address), + expected_ip_address.as_bytes() + ) + } + } + + #[test] + fn presented_id_matches_constraint_ipv4_test() { + let names_and_constraints = vec![ + ( + // 192.0.2.0 matches constraint 192.0.2.0/24 + [0xC0, 0x00, 0x02, 0x00], + [0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00], + Ok(true), + ), + ( + // 192.0.2.1 matches constraint 192.0.2.0/24 + [0xC0, 0x00, 0x02, 0x01], + [0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00], + Ok(true), + ), + ( + // 192.0.2.255 matches constraint 192.0.2.0/24 + [0xC0, 0x00, 0x02, 0xFF], + [0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00], + Ok(true), + ), + ( + // 192.0.1.255 does not match constraint 192.0.2.0/24 + [0xC0, 0x00, 0x01, 0xFF], + [0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00], + Ok(false), + ), + ( + // 192.0.3.0 does not match constraint 192.0.2.0/24 + [0xC0, 0x00, 0x03, 0x00], + [0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00], + Ok(false), + ), + ]; + for (name, constraint, match_result) in names_and_constraints { + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&name), + untrusted::Input::from(&constraint), + ), + match_result + ) + } + + // Invalid name length (shorter) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[0xC0, 0x00, 0x02]), + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00]), + ), + Err(Error::BadDER), + ); + + // Invalid name length (longer) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0x00]), + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00]), + ), + Err(Error::BadDER), + ); + + // Unmatching constraint size (shorter) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]), + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF]), + ), + Err(Error::BadDER), + ); + + // Unmatching constraint size (longer) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]), + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00]), + ), + Err(Error::BadDER), + ); + + // Unmatching constraint size (IPv6 constraint for IPv4 address) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00]), + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + ), + Ok(false), + ); + } + + #[test] + fn presented_id_matches_constraint_ipv6_test() { + let names_and_constraints = vec![ + ( + // 2001:0DB8:ABCD:0012:0000:0000:0000:0000 matches constraint + // 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64 + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ], + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + Ok(true), + ), + ( + // 2001:0DB8:ABCD:0012:0000:0000:0000:0001 matches constraint + // 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64 + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, + ], + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + Ok(true), + ), + ( + // 2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF matches constraint + // 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64 + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, + ], + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + Ok(true), + ), + ( + // 2001:0DB8:ABCD:0011:0000:0000:0000:0000 does not match constraint + // 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64 + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ], + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + Ok(false), + ), + ( + // 2001:0DB8:ABCD:0013:0000:0000:0000:0000 does not match constraint + // 2001:0DB8:ABCD:0012:0000:0000:0000:0000/64 + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ], + [ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + Ok(false), + ), + ]; + for (name, constraint, match_result) in names_and_constraints { + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&name), + untrusted::Input::from(&constraint), + ), + match_result + ) + } + + // Invalid name length (shorter) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00 + ]), + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + ), + Err(Error::BadDER), + ); + + // Invalid name length (longer) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]), + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + ), + Err(Error::BadDER), + ); + + // Unmatching constraint size (shorter) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ]), + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + ), + Err(Error::BadDER), + ); + + // Unmatching constraint size (longer) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ]), + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + ), + Err(Error::BadDER), + ); + + // Unmatching constraint size (IPv4 constraint for IPv6 address) + assert_eq!( + presented_id_matches_constraint( + untrusted::Input::from(&[ + 0x20, 0x01, 0x0D, 0xB8, 0xAB, 0xCD, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ]), + untrusted::Input::from(&[0xC0, 0x00, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0x00]), + ), + Ok(false), + ); + } + + #[test] + fn test_presented_id_matches_reference_id() { + assert_eq!( + presented_id_matches_reference_id( + untrusted::Input::from(&[]), + untrusted::Input::from(&[]) + ), + Ok(false), + ); + + assert_eq!( + presented_id_matches_reference_id( + untrusted::Input::from(&[0x01]), + untrusted::Input::from(&[]) + ), + Ok(false), + ); + + assert_eq!( + presented_id_matches_reference_id( + untrusted::Input::from(&[]), + untrusted::Input::from(&[0x01]) + ), + Ok(false), + ); + + assert_eq!( + presented_id_matches_reference_id( + untrusted::Input::from(&[1, 2, 3, 4]), + untrusted::Input::from(&[1, 2, 3, 4]) + ), + Ok(true), + ); + + assert_eq!( + presented_id_matches_reference_id( + untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), + untrusted::Input::from(&[1, 2, 3, 4]) + ), + Ok(false), + ); + + assert_eq!( + presented_id_matches_reference_id( + untrusted::Input::from(&[1, 2, 3, 4]), + untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + ), + Ok(false), + ); + + assert_eq!( + presented_id_matches_reference_id( + untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), + untrusted::Input::from(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + ), + Ok(true), + ); + } +} + +#[cfg(all(test, feature = "alloc"))] +mod alloc_tests { + use super::*; + + #[test] + fn as_ref_ip_address_test() { + assert_eq!( + IpAddr::V4(String::from("127.0.0.1"), [127, 0, 0, 1]).as_ref(), + "127.0.0.1", + ); + assert_eq!( + IpAddr::V6( + String::from("0000:0000:0000:0000:0000:0000:0000:0001"), + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + ) + .as_ref(), + "0000:0000:0000:0000:0000:0000:0000:0001", + ); + } + + #[test] + fn from_ip_address_ref_for_ip_address_test() { + { + let (ip_address, ip_address_octets) = ("127.0.0.1", [127, 0, 0, 1]); + assert_eq!( + IpAddr::from(IpAddrRef::V4(ip_address.as_bytes(), ip_address_octets)), + IpAddr::V4(String::from(ip_address), ip_address_octets), + ) + } + { + let (ip_address, ip_address_octets) = ( + "0000:0000:0000:0000:0000:0000:0000:0001", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ); + assert_eq!( + IpAddr::from(IpAddrRef::V6(ip_address.as_bytes(), ip_address_octets)), + IpAddr::V6(String::from(ip_address), ip_address_octets), + ) + } + } + + #[test] + fn from_ip_address_for_ip_address_ref_test() { + { + let ip_address = IpAddr::V4(String::from("127.0.0.1"), [127, 0, 0, 1]); + assert_eq!( + IpAddrRef::from(&ip_address), + IpAddrRef::V4(b"127.0.0.1", [127, 0, 0, 1]), + ) + } + { + let ip_address = IpAddr::V6( + String::from("0000:0000:0000:0000:0000:0000:0000:0001"), + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ); + assert_eq!( + IpAddrRef::from(&ip_address), + IpAddrRef::V6( + b"0000:0000:0000:0000:0000:0000:0000:0001", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + ), + ) + } + } + + #[test] + fn display_invalid_ip_address_error_test() { + assert_eq!(AddrParseError.to_string(), String::from("AddrParseError"),) + } + + #[test] + fn ip_address_ref_to_owned_test() { + { + assert_eq!( + IpAddrRef::V4(b"127.0.0.1", [127, 0, 0, 1]).to_owned(), + IpAddr::V4(String::from("127.0.0.1"), [127, 0, 0, 1]), + ) + } + { + assert_eq!( + IpAddrRef::V6( + b"0000:0000:0000:0000:0000:0000:0000:0001", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ) + .to_owned(), + IpAddr::V6( + String::from("0000:0000:0000:0000:0000:0000:0000:0001"), + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ), + ) + } + } + + #[test] + fn ip_address_from_std_net_ipaddr_test() { + let ip_addresses = vec![ + ( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + IpAddr::V4(String::from("127.0.0.1"), [127, 0, 0, 1]), + ), + ( + std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + IpAddr::V6( + String::from("0000:0000:0000:0000:0000:0000:0000:0001"), + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + ), + ), + ]; + for (ip_address, expected_ip_address) in ip_addresses { + assert_eq!(IpAddr::from(ip_address), expected_ip_address,) + } + } + + #[test] + fn ipv6_to_uncompressed_string_test() { + let ip_addresses = vec![ + ( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + String::from("0000:0000:0000:0000:0000:0000:0000:0001"), + ), + ( + [ + 0x2a, 0x05, 0xd0, 0x18, 0x07, 0x6c, 0xb6, 0x84, 0x8e, 0x48, 0x47, 0xc9, 0x84, + 0xaa, 0xb3, 0x4d, + ], + String::from("2a05:d018:076c:b684:8e48:47c9:84aa:b34d"), + ), + ]; + for (ip_address_octets, expected_result) in ip_addresses { + assert_eq!( + ipv6_to_uncompressed_string(ip_address_octets), + expected_result, + ) + } + } +} diff --git a/src/name/name.rs b/src/name/name.rs new file mode 100644 index 00000000..3f34edfa --- /dev/null +++ b/src/name/name.rs @@ -0,0 +1,95 @@ +// Copyright 2015-2020 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::DnsNameRef; + +use super::ip_address::{self, IpAddrRef}; + +/// A DNS name or IP address, which borrows its text representation. +#[derive(Debug, Clone, Copy)] +pub enum SubjectNameRef<'a> { + /// A valid DNS name + DnsName(DnsNameRef<'a>), + + /// A valid IP address + IpAddress(IpAddrRef<'a>), +} + +/// An error indicating that a `SubjectNameRef` could not built +/// because the input is not a syntactically-valid DNS Name or IP +/// address. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct InvalidSubjectNameError; + +impl<'a> SubjectNameRef<'a> { + /// Attempts to decode an encodingless string as either an IPv4 address, IPv6 address or + /// DNS name; in that order. In practice this space is non-overlapping because + /// DNS name components are separated by periods but cannot be wholly numeric (so cannot + /// overlap with a valid IPv4 address), and IPv6 addresses are separated by colons but + /// cannot contain periods. + /// + /// The IPv6 address encoding supported here is extremely simplified; it does not support + /// compression, all leading zeroes must be present in each 16-bit word, etc. Generally + /// this is not suitable as a parse for human-provided addresses for this reason. Instead: + /// consider parsing these with `std::net::IpAddr` and then using + /// `IpAddr::from`. + pub fn try_from_ascii(subject_name: &'a [u8]) -> Result { + if let Ok(ip_address) = ip_address::parse_ipv4_address(subject_name) { + return Ok(SubjectNameRef::IpAddress(ip_address)); + } else if let Ok(ip_address) = ip_address::parse_ipv6_address(subject_name) { + return Ok(SubjectNameRef::IpAddress(ip_address)); + } else { + Ok(SubjectNameRef::DnsName( + DnsNameRef::try_from_ascii(subject_name).map_err(|_| InvalidSubjectNameError)?, + )) + } + } + + /// Constructs a `SubjectNameRef` from the given input if the + /// input is a syntactically-valid DNS name or IP address. + pub fn try_from_ascii_str(subject_name: &'a str) -> Result { + Self::try_from_ascii(subject_name.as_bytes()) + } +} + +impl<'a> From> for SubjectNameRef<'a> { + fn from(dns_name: DnsNameRef<'a>) -> SubjectNameRef { + SubjectNameRef::DnsName(DnsNameRef(dns_name.0)) + } +} + +impl<'a> From> for SubjectNameRef<'a> { + fn from(dns_name: IpAddrRef<'a>) -> SubjectNameRef { + match dns_name { + IpAddrRef::V4(ip_address, ip_address_octets) => { + SubjectNameRef::IpAddress(IpAddrRef::V4(ip_address, ip_address_octets)) + } + IpAddrRef::V6(ip_address, ip_address_octets) => { + SubjectNameRef::IpAddress(IpAddrRef::V6(ip_address, ip_address_octets)) + } + } + } +} + +impl AsRef<[u8]> for SubjectNameRef<'_> { + #[inline] + fn as_ref(&self) -> &[u8] { + match self { + SubjectNameRef::DnsName(dns_name) => dns_name.0, + SubjectNameRef::IpAddress(ip_address) => match ip_address { + IpAddrRef::V4(ip_address, _) | IpAddrRef::V6(ip_address, _) => ip_address, + }, + } + } +} diff --git a/src/name/verify.rs b/src/name/verify.rs index 30e428ac..dbbb38b8 100644 --- a/src/name/verify.rs +++ b/src/name/verify.rs @@ -14,7 +14,8 @@ use super::{ dns_name::{self, DnsNameRef}, - ip_address, + ip_address::{self, IpAddrRef}, + name::SubjectNameRef, }; use crate::{ cert::{Cert, EndEntityOrCa}, @@ -28,7 +29,7 @@ pub fn verify_cert_dns_name( let cert = cert.inner(); let dns_name = untrusted::Input::from(dns_name.as_ref()); iterate_names( - cert.subject, + Some(cert.subject), cert.subject_alt_name, Err(Error::CertNotValidForName), &|name| { @@ -44,6 +45,41 @@ pub fn verify_cert_dns_name( ) } +pub fn verify_cert_subject_name( + cert: &crate::EndEntityCert, + subject_name: SubjectNameRef, +) -> Result<(), Error> { + let ip_address = match subject_name { + SubjectNameRef::DnsName(dns_name) => return verify_cert_dns_name(cert, dns_name), + SubjectNameRef::IpAddress(IpAddrRef::V4(_, ref ip_address_octets)) => { + untrusted::Input::from(ip_address_octets) + } + SubjectNameRef::IpAddress(IpAddrRef::V6(_, ref ip_address_octets)) => { + untrusted::Input::from(ip_address_octets) + } + }; + + iterate_names( + // IP addresses are not compared against the subject field; + // only against Subject Alternative Names. + None, + cert.inner().subject_alt_name, + Err(Error::CertNotValidForName), + &|name| { + if let GeneralName::IpAddress(presented_id) = name { + match ip_address::presented_id_matches_reference_id(presented_id, ip_address) { + Ok(true) => return NameIteration::Stop(Ok(())), + Ok(false) => (), + Err(_) => { + return NameIteration::Stop(Err(Error::BadDER)); + } + } + } + NameIteration::KeepGoing + }, + ) +} + // https://tools.ietf.org/html/rfc5280#section-4.2.1.10 pub fn check_name_constraints( input: Option<&mut untrusted::Reader>, @@ -74,9 +110,18 @@ pub fn check_name_constraints( let mut child = subordinate_certs; loop { - iterate_names(child.subject, child.subject_alt_name, Ok(()), &|name| { - check_presented_id_conforms_to_constraints(name, permitted_subtrees, excluded_subtrees) - })?; + iterate_names( + Some(child.subject), + child.subject_alt_name, + Ok(()), + &|name| { + check_presented_id_conforms_to_constraints( + name, + permitted_subtrees, + excluded_subtrees, + ) + }, + )?; child = match child.ee_or_ca { EndEntityOrCa::Ca(child_cert) => child_cert, @@ -239,36 +284,37 @@ enum NameIteration { } fn iterate_names( - subject: untrusted::Input, + subject: Option, subject_alt_name: Option, result_if_never_stopped_early: Result<(), Error>, f: &dyn Fn(GeneralName) -> NameIteration, ) -> Result<(), Error> { - match subject_alt_name { - Some(subject_alt_name) => { - let mut subject_alt_name = untrusted::Reader::new(subject_alt_name); - // https://bugzilla.mozilla.org/show_bug.cgi?id=1143085: An empty - // subjectAltName is not legal, but some certificates have an empty - // subjectAltName. Since we don't support CN-IDs, the certificate - // will be rejected either way, but checking `at_end` before - // attempting to parse the first entry allows us to return a better - // error code. - while !subject_alt_name.at_end() { - let name = general_name(&mut subject_alt_name)?; - match f(name) { - NameIteration::Stop(result) => { - return result; - } - NameIteration::KeepGoing => (), + if let Some(subject_alt_name) = subject_alt_name { + let mut subject_alt_name = untrusted::Reader::new(subject_alt_name); + // https://bugzilla.mozilla.org/show_bug.cgi?id=1143085: An empty + // subjectAltName is not legal, but some certificates have an empty + // subjectAltName. Since we don't support CN-IDs, the certificate + // will be rejected either way, but checking `at_end` before + // attempting to parse the first entry allows us to return a better + // error code. + while !subject_alt_name.at_end() { + let name = general_name(&mut subject_alt_name)?; + match f(name) { + NameIteration::Stop(result) => { + return result; } + NameIteration::KeepGoing => (), } } - None => (), } - match f(GeneralName::DirectoryName(subject)) { - NameIteration::Stop(result) => result, - NameIteration::KeepGoing => result_if_never_stopped_early, + if let Some(subject) = subject { + match f(GeneralName::DirectoryName(subject)) { + NameIteration::Stop(result) => result, + NameIteration::KeepGoing => result_if_never_stopped_early, + } + } else { + result_if_never_stopped_early } } diff --git a/tests/cloudflare_dns/ca.der b/tests/cloudflare_dns/ca.der new file mode 100644 index 00000000..2f1e5523 Binary files /dev/null and b/tests/cloudflare_dns/ca.der differ diff --git a/tests/cloudflare_dns/ee.der b/tests/cloudflare_dns/ee.der new file mode 100644 index 00000000..afa7087f Binary files /dev/null and b/tests/cloudflare_dns/ee.der differ diff --git a/tests/cloudflare_dns/inter.der b/tests/cloudflare_dns/inter.der new file mode 100644 index 00000000..7d8413a3 Binary files /dev/null and b/tests/cloudflare_dns/inter.der differ diff --git a/tests/integration.rs b/tests/integration.rs index 0f3f4ce1..0ae7bea8 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -53,6 +53,59 @@ pub fn netflix() { ); } +/* This is notable because it is a popular use of IP address subjectAltNames. */ +#[cfg(feature = "alloc")] +#[test] +pub fn cloudflare_dns() { + let ee: &[u8] = include_bytes!("cloudflare_dns/ee.der"); + let inter = include_bytes!("cloudflare_dns/inter.der"); + let ca = include_bytes!("cloudflare_dns/ca.der"); + + let anchors = vec![webpki::TrustAnchor::try_from_cert_der(ca).unwrap()]; + let anchors = webpki::TLSServerTrustAnchors(&anchors); + + #[allow(clippy::unreadable_literal)] + let time = webpki::Time::from_seconds_since_unix_epoch(1663495771); + + let cert = webpki::EndEntityCert::try_from(ee).unwrap(); + assert_eq!( + Ok(()), + cert.verify_is_valid_tls_server_cert(ALL_SIGALGS, &anchors, &[inter], time) + ); + + let check_name = |name: &str| { + let dns_name_ref = webpki::DnsNameRef::try_from_ascii_str(name).unwrap(); + assert_eq!(Ok(()), cert.verify_is_valid_for_dns_name(dns_name_ref)); + let subject_name_ref = webpki::SubjectNameRef::from(dns_name_ref); + assert_eq!( + Ok(()), + cert.verify_is_valid_for_subject_name(subject_name_ref) + ); + println!("{:?} ok as name", name); + }; + + let check_addr = |addr: &str| { + let subject_name_ref = webpki::SubjectNameRef::try_from_ascii(addr.as_bytes()).unwrap(); + assert_eq!( + Ok(()), + cert.verify_is_valid_for_subject_name(subject_name_ref) + ); + println!("{:?} ok as address", addr); + }; + + check_name("cloudflare-dns.com"); + check_name("wildcard.cloudflare-dns.com"); + check_name("one.one.one.one"); + check_addr("1.1.1.1"); + check_addr("1.0.0.1"); + check_addr("162.159.36.1"); + check_addr("162.159.46.1"); + check_addr("2606:4700:4700:0000:0000:0000:0000:1111"); + check_addr("2606:4700:4700:0000:0000:0000:0000:1001"); + check_addr("2606:4700:4700:0000:0000:0000:0000:0064"); + check_addr("2606:4700:4700:0000:0000:0000:0000:6400"); +} + #[test] pub fn ed25519() { let ee: &[u8] = include_bytes!("ed25519/ee.der");