From ccc6426354cfd5adb3aa1f1742b83c4fcdfdc66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Fern=C3=A1ndez=20L=C3=B3pez?= Date: Tue, 26 Apr 2022 19:12:59 +0300 Subject: [PATCH 1/7] Implement IP address validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `IpAddressRef`, `DnsNameOrIpRef` and the owned type `IpAddress`. Introduce a new public function `verify_is_valid_for_dns_name_or_ip` that validates a given host name or IP address against a certificate. IP addresses are only compared against Subject Alternative Names. It's possible to convert the already existing types `DnsNameRef` and `IpAddressRef` into a `DnsNameOrIpRef` for better ergonomics when calling to `verify_cert_dns_name_or_ip`. The behavior of `verify_cert_dns_name` has not been altered, and works in the same way as it has done until now, so that if `webpki` gets bumped as a dependency, it won't start accepting certificates that would have been rejected until now without notice. Neither `IpAddressRef`, `DnsNameOrIpRef` nor `IpAddress` can be instantiated directly. They must be instantiated through the `try_from_ascii` and `try_from_ascii_str` public functions. This ensures that instances of these types are correct by construction. IPv6 addresses are only validated and supported in their uncompressed form. Signed-off-by: Rafael Fernández López --- src/end_entity.rs | 13 +- src/lib.rs | 7 +- src/name.rs | 8 +- src/name/dns_name.rs | 9 +- src/name/ip_address.rs | 482 +++++++++++++++++++++++++++++++++++++++++ src/name/name.rs | 107 +++++++++ src/name/verify.rs | 77 ++++++- 7 files changed, 688 insertions(+), 15 deletions(-) create mode 100644 src/name/name.rs diff --git a/src/end_entity.rs b/src/end_entity.rs index 1161c305..7c879016 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, DnsNameOrIpRef, DnsNameRef, Error, SignatureAlgorithm, 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_dns_name_or_ip`: 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 DNS host name or IP address. + pub fn verify_is_valid_for_dns_name_or_ip( + &self, + dns_name_or_ip: DnsNameOrIpRef, + ) -> Result<(), Error> { + name::verify_cert_dns_name_or_ip(self, dns_name_or_ip) + } + /// 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..17f01716 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::InvalidIpAddressError, ip_address::IpAddressRef, DnsNameOrIpRef, DnsNameRef, + InvalidDnsNameError, InvalidDnsNameOrIpError, + }, 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::IpAddress, 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..c49babd6 100644 --- a/src/name.rs +++ b/src/name.rs @@ -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::{DnsNameOrIpRef, InvalidDnsNameOrIpError}; + +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_dns_name_or_ip}; 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..7658922d 100644 --- a/src/name/ip_address.rs +++ b/src/name/ip_address.rs @@ -14,6 +14,236 @@ 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 IpAddress { + /// An ipv4 address and its owned string representation + IpV4Address(String, [u8; 4]), + /// An ipv6 address and its owned string representation + IpV6Address(String, [u8; 16]), +} + +#[cfg(feature = "alloc")] +impl AsRef for IpAddress { + fn as_ref(&self) -> &str { + match self { + IpAddress::IpV4Address(ip_address, _) | IpAddress::IpV6Address(ip_address, _) => { + ip_address.as_str() + } + } + } +} + +/// Either a ipv4 or ipv6 address, plus its borrowed string representation +#[derive(Debug, Clone, Copy)] +pub enum IpAddressRef<'a> { + /// An ipv4 address and its borrowed string representation + IpV4AddressRef(&'a [u8], [u8; 4]), + /// An ipv6 address and its borrowed string representation + IpV6AddressRef(&'a [u8], [u8; 16]), +} + +#[cfg(feature = "alloc")] +impl<'a> From> for IpAddress { + fn from(ip_address: IpAddressRef<'a>) -> IpAddress { + match ip_address { + IpAddressRef::IpV4AddressRef(ip_address, ip_address_octets) => IpAddress::IpV4Address( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + ip_address_octets, + ), + IpAddressRef::IpV6AddressRef(ip_address, ip_address_octets) => IpAddress::IpV6Address( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + ip_address_octets, + ), + } + } +} + +// Returns the octets that correspond to the provided IPv4 address. +// +// This function can only be called on IPv4 addresses that have +// already been validated with `is_valid_ipv4_address`. +pub(crate) fn ipv4_octets(ip_address: &[u8]) -> Result<[u8; 4], InvalidIpAddressError> { + let mut result: [u8; 4] = [0, 0, 0, 0]; + for (i, textual_octet) in ip_address + .split(|textual_octet| *textual_octet == b'.') + .enumerate() + { + result[i] = str::parse::( + core::str::from_utf8(textual_octet).map_err(|_| InvalidIpAddressError)?, + ) + .map_err(|_| InvalidIpAddressError)?; + } + Ok(result) +} + +// Returns the octets that correspond to the provided IPv6 address. +// +// This function can only be called on uncompressed IPv6 addresses +// that have already been validated with `is_valid_ipv6_address`. +pub(crate) fn ipv6_octets(ip_address: &[u8]) -> Result<[u8; 16], InvalidIpAddressError> { + let mut result: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i, textual_block) in ip_address + .split(|textual_block| *textual_block == b':') + .enumerate() + { + let octets = u16::from_str_radix( + core::str::from_utf8(textual_block).map_err(|_| InvalidIpAddressError)?, + 16, + ) + .map_err(|_| InvalidIpAddressError)? + .to_be_bytes(); + + result[2 * i] = octets[0]; + result[(2 * i) + 1] = octets[1]; + } + Ok(result) +} + +#[cfg(feature = "alloc")] +impl<'a> From<&'a IpAddress> for IpAddressRef<'a> { + fn from(ip_address: &'a IpAddress) -> IpAddressRef<'a> { + match ip_address { + IpAddress::IpV4Address(ip_address, ip_address_octets) => { + IpAddressRef::IpV4AddressRef(ip_address.as_bytes(), *ip_address_octets) + } + IpAddress::IpV6Address(ip_address, ip_address_octets) => { + IpAddressRef::IpV6AddressRef(ip_address.as_bytes(), *ip_address_octets) + } + } + } +} + +/// An error indicating that an `IpAddressRef` could not built because the input +/// is not a valid IP address. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct InvalidIpAddressError; + +impl core::fmt::Display for InvalidIpAddressError { + 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 InvalidIpAddressError {} + +impl<'a> IpAddressRef<'a> { + /// Constructs an `IpAddressRef` 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 is_valid_ipv4_address(untrusted::Input::from(ip_address)) { + Ok(IpAddressRef::IpV4AddressRef( + ip_address, + ipv4_octets(ip_address)?, + )) + } else if is_valid_ipv6_address(untrusted::Input::from(ip_address)) { + Ok(IpAddressRef::IpV6AddressRef( + ip_address, + ipv6_octets(ip_address)?, + )) + } else { + Err(InvalidIpAddressError) + } + } + + /// Constructs an `IpAddressRef` 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 `IpAddress` from this `IpAddressRef` + /// + /// Requires the `alloc` feature. + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> IpAddress { + match self { + IpAddressRef::IpV4AddressRef(ip_address, ip_address_octets) => IpAddress::IpV4Address( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + *ip_address_octets, + ), + IpAddressRef::IpV6AddressRef(ip_address, ip_address_octets) => IpAddress::IpV6Address( + String::from_utf8(ip_address.to_vec()).expect(VALID_IP_BY_CONSTRUCTION), + *ip_address_octets, + ), + } + } +} + +#[cfg(feature = "std")] +impl From for IpAddress { + fn from(ip_address: std::net::IpAddr) -> IpAddress { + match ip_address { + std::net::IpAddr::V4(ip_address) => { + IpAddress::IpV4Address(ip_address.to_string(), ip_address.octets()) + } + std::net::IpAddr::V6(ip_address) => { + IpAddress::IpV6Address(ip_address.to_string(), ip_address.octets()) + } + } + } +} + +impl<'a> From> for &'a str { + fn from(ip_address: IpAddressRef<'a>) -> &'a str { + match ip_address { + IpAddressRef::IpV4AddressRef(ip_address, _) + | IpAddressRef::IpV6AddressRef(ip_address, _) => { + core::str::from_utf8(ip_address).expect(VALID_IP_BY_CONSTRUCTION) + } + } + } +} + +impl<'a> From> for &'a [u8] { + fn from(ip_address: IpAddressRef<'a>) -> &'a [u8] { + match ip_address { + IpAddressRef::IpV4AddressRef(ip_address, _) + | IpAddressRef::IpV6AddressRef(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 { + if presented_id.len() != reference_id.len() { + return Ok(false); + } + + let mut presented_ip_address = untrusted::Reader::new(presented_id); + let mut reference_ip_address = untrusted::Reader::new(reference_id); + loop { + 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); + } + if presented_ip_address.at_end() { + break; + } + } + + 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 +292,255 @@ pub(super) fn presented_id_matches_constraint( Ok(true) } + +pub(crate) fn is_valid_ipv4_address(ip_address: untrusted::Input) -> bool { + let mut ip_address = untrusted::Reader::new(ip_address); + let mut is_first_byte = true; + let mut current_textual_octet: [u8; 3] = [0, 0, 0]; + let mut current_textual_octet_size = 0; + let mut dot_count = 0; + + loop { + // Returns a u32 so it's possible to identify (and error) when + // provided textual octets > 255, not representable by u8. + fn textual_octets_to_octet(textual_octets: [u8; 3], textual_octet_size: usize) -> u32 { + let mut result: u32 = 0; + for (i, textual_octet) in textual_octets.iter().rev().enumerate() { + if i >= textual_octet_size { + break; + } + if let Some(digit) = char::to_digit(*textual_octet as char, 10) { + result += digit * 10_u32.pow(i as u32); + } + } + result + } + + match ip_address.read_byte() { + Ok(b'.') => { + if is_first_byte { + // IPv4 address cannot start with a dot. + return false; + } + if ip_address.at_end() { + // IPv4 address cannot end with a dot. + return false; + } + if dot_count == 3 { + // IPv4 address cannot have more than three dots. + return false; + } + dot_count += 1; + if current_textual_octet_size == 0 { + // IPv4 address cannot contain two dots in a row. + return false; + } + if textual_octets_to_octet(current_textual_octet, current_textual_octet_size) > 255 + { + // No octet can be greater than 255. + return false; + } + // We move on to the next textual octet. + current_textual_octet = [0, 0, 0]; + current_textual_octet_size = 0; + } + Ok(number @ b'0'..=b'9') => { + if number == b'0' + && current_textual_octet_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 false; + } + current_textual_octet[current_textual_octet_size] = u8::from_be(number); + current_textual_octet_size += 1; + } + _ => { + return false; + } + } + is_first_byte = false; + + if ip_address.at_end() { + if current_textual_octet_size > 0 + && textual_octets_to_octet(current_textual_octet, current_textual_octet_size) > 255 + { + // No octet can be greater than 255. + return false; + } + break; + } + } + dot_count == 3 +} + +pub(crate) fn is_valid_ipv6_address(ip_address: untrusted::Input) -> bool { + // 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 false; + } + + let mut ip_address = untrusted::Reader::new(ip_address); + let mut is_first_byte = true; + let mut current_textual_block_size = 0; + let mut colon_count = 0; + loop { + match ip_address.read_byte() { + Ok(b':') => { + if is_first_byte { + // Uncompressed IPv6 address cannot start with a colon. + return false; + } + if ip_address.at_end() { + // Uncompressed IPv6 address cannot end with a colon. + return false; + } + if colon_count == 7 { + // IPv6 address cannot have more than seven colons. + return false; + } + colon_count += 1; + if current_textual_block_size == 0 { + // Uncompressed IPv6 address cannot contain two colons in a row. + return false; + } + if current_textual_block_size != 4 { + // Compressed IPv6 addresses are not supported. + return false; + } + // We move on to the next textual block. + current_textual_block_size = 0; + } + Ok(b'0'..=b'9') | Ok(b'a'..=b'f') => { + if current_textual_block_size == 4 { + // Blocks cannot contain more than 4 hexadecimal characters. + return false; + } + current_textual_block_size += 1; + } + _ => { + return false; + } + } + is_first_byte = false; + + if ip_address.at_end() { + break; + } + } + colon_count == 7 +} + +#[cfg(test)] +mod tests { + use super::*; + + const IPV4_ADDRESSES_VALIDITY: &[(&[u8], bool)] = &[ + // Valid IPv4 addresses + (b"0.0.0.0", true), + (b"127.0.0.1", true), + (b"1.1.1.1", true), + (b"255.255.255.255", true), + (b"205.0.0.0", true), + (b"0.205.0.0", true), + (b"0.0.205.0", true), + (b"0.0.0.205", true), + (b"0.0.0.20", true), + // Invalid IPv4 addresses + (b"", false), + (b"...", false), + (b".0.0.0.0", false), + (b"0.0.0.0.", false), + (b"256.0.0.0", false), + (b"0.256.0.0", false), + (b"0.0.256.0", false), + (b"0.0.0.256", false), + (b"1..1.1.1", false), + (b"1.1..1.1", false), + (b"1.1.1..1", false), + (b"025.0.0.0", false), + (b"0.025.0.0", false), + (b"0.0.025.0", false), + (b"0.0.0.025", false), + ]; + + #[test] + fn is_valid_ipv4_address_test() { + for &(ip_address, expected_result) in IPV4_ADDRESSES_VALIDITY { + assert_eq!( + is_valid_ipv4_address(untrusted::Input::from(ip_address)), + expected_result + ); + } + } + + #[test] + fn ipv4_octets_test() { + assert_eq!(ipv4_octets(b"0.0.0.0"), Ok([0, 0, 0, 0])); + assert_eq!(ipv4_octets(b"54.155.246.232"), Ok([54, 155, 246, 232])); + } + + const IPV6_ADDRESSES_VALIDITY: &[(&[u8], bool)] = &[ + // Valid IPv6 addresses + (b"2a05:d018:076c:b685:e8ab:afd3:af51:3aed", true), + (b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true), + // Invalid IPv6 addresses + + // Missing octets on uncompressed addresses. The unmatching letter has the violation + (b"aaa:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:aaa:ffff:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:aaa:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:aaa:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:aaa:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:aaa:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:ffff:aaa:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:aaa", false), + // Wrong hexadecimal characters on different positions + (b"ffgf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:gfff:ffff:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:fffg:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffgf:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:gfff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:fgff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:ffff:ffgf:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:ffff:ffgf:fffg", false), + // Wrong colons on uncompressed addresses + (b":ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff::ffff:ffff:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff::ffff:ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff::ffff:ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff::ffff:ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff::ffff:ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:ffff::ffff:ffff", false), + (b"ffff:ffff:ffff:ffff:ffff:ffff:ffff::ffff", false), + // More colons than allowed + (b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:", false), + (b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false), + // This is a valid IPv6 address, but we don't support compressed addresses + (b"2a05:d018:76c:b685:e8ab:afd3:af51:3aed", false), + ]; + + #[test] + fn is_valid_ipv6_address_test() { + for &(ip_address, expected_result) in IPV6_ADDRESSES_VALIDITY { + assert_eq!( + is_valid_ipv6_address(untrusted::Input::from(ip_address)), + expected_result + ); + } + } + + #[test] + fn ipv6_octets_test() { + assert_eq!( + ipv6_octets(b"2a05:d018:076c:b684:8e48:47c9:84aa:b34d"), + Ok([ + 0x2a, 0x05, 0xd0, 0x18, 0x07, 0x6c, 0xb6, 0x84, 0x8e, 0x48, 0x47, 0xc9, 0x84, 0xaa, + 0xb3, 0x4d + ]) + ); + } +} diff --git a/src/name/name.rs b/src/name/name.rs new file mode 100644 index 00000000..027dba55 --- /dev/null +++ b/src/name/name.rs @@ -0,0 +1,107 @@ +// 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, IpAddressRef}; + +/// A DNS name or IP address, which borrows its text representation. +#[derive(Debug, Clone, Copy)] +pub enum DnsNameOrIpRef<'a> { + /// A valid DNS name + DnsName(DnsNameRef<'a>), + + /// A valid IP address + IpAddress(IpAddressRef<'a>), +} + +/// An error indicating that a `DnsNameOrIpRef` could not built because the input +/// is not a syntactically-valid DNS Name or IP address. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct InvalidDnsNameOrIpError; + +impl<'a> DnsNameOrIpRef<'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 + /// `IpAddress::from`. + pub fn try_from_ascii(dns_name_or_ip: &'a [u8]) -> Result { + if ip_address::is_valid_ipv4_address(untrusted::Input::from(dns_name_or_ip)) { + return Ok(DnsNameOrIpRef::IpAddress(IpAddressRef::IpV4AddressRef( + dns_name_or_ip, + ip_address::ipv4_octets(dns_name_or_ip).map_err(|_| InvalidDnsNameOrIpError)?, + ))); + } + if ip_address::is_valid_ipv6_address(untrusted::Input::from(dns_name_or_ip)) { + return Ok(DnsNameOrIpRef::IpAddress(IpAddressRef::IpV6AddressRef( + dns_name_or_ip, + ip_address::ipv6_octets(dns_name_or_ip).map_err(|_| InvalidDnsNameOrIpError)?, + ))); + } + Ok(DnsNameOrIpRef::DnsName( + DnsNameRef::try_from_ascii(dns_name_or_ip).map_err(|_| InvalidDnsNameOrIpError)?, + )) + } + + /// Constructs a `DnsNameOrIpRef` from the given input if the input is a + /// syntactically-valid DNS name or IP address. + pub fn try_from_ascii_str(dns_name_or_ip: &'a str) -> Result { + Self::try_from_ascii(dns_name_or_ip.as_bytes()) + } +} + +impl<'a> From> for DnsNameOrIpRef<'a> { + fn from(dns_name: DnsNameRef<'a>) -> DnsNameOrIpRef { + DnsNameOrIpRef::DnsName(DnsNameRef(dns_name.0)) + } +} + +impl<'a> From> for DnsNameOrIpRef<'a> { + fn from(dns_name: IpAddressRef<'a>) -> DnsNameOrIpRef { + match dns_name { + IpAddressRef::IpV4AddressRef(ip_address, ip_address_octets) => { + DnsNameOrIpRef::IpAddress(IpAddressRef::IpV4AddressRef( + ip_address, + ip_address_octets, + )) + } + IpAddressRef::IpV6AddressRef(ip_address, ip_address_octets) => { + DnsNameOrIpRef::IpAddress(IpAddressRef::IpV6AddressRef( + ip_address, + ip_address_octets, + )) + } + } + } +} + +impl AsRef<[u8]> for DnsNameOrIpRef<'_> { + #[inline] + fn as_ref(&self) -> &[u8] { + match self { + DnsNameOrIpRef::DnsName(dns_name) => dns_name.0, + DnsNameOrIpRef::IpAddress(ip_address) => match ip_address { + IpAddressRef::IpV4AddressRef(ip_address, _) + | IpAddressRef::IpV6AddressRef(ip_address, _) => ip_address, + }, + } + } +} diff --git a/src/name/verify.rs b/src/name/verify.rs index 749a9ea6..aefc0922 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, IpAddressRef}, + name::DnsNameOrIpRef, }; 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| { @@ -51,6 +52,51 @@ pub fn verify_cert_dns_name( ) } +pub fn verify_cert_dns_name_or_ip( + cert: &crate::EndEntityCert, + dns_name_or_ip: DnsNameOrIpRef, +) -> Result<(), Error> { + match dns_name_or_ip { + DnsNameOrIpRef::DnsName(dns_name) => verify_cert_dns_name(cert, dns_name), + DnsNameOrIpRef::IpAddress(ip_address) => { + let ip_address = match ip_address { + IpAddressRef::IpV4AddressRef(_, ref ip_address_octets) => { + untrusted::Input::from(ip_address_octets) + } + IpAddressRef::IpV6AddressRef(_, 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| { + #[allow(clippy::single_match)] + match name { + GeneralName::IpAddress(presented_id) => { + 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>, @@ -81,9 +127,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, @@ -246,7 +301,7 @@ 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, @@ -273,9 +328,13 @@ fn iterate_names( 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 } } From 6477d82c0505680c40c67e7386da16894f178d22 Mon Sep 17 00:00:00 2001 From: Joseph Birr-Pixton Date: Sun, 18 Sep 2022 11:38:35 +0100 Subject: [PATCH 2/7] Fix panic in ipv4 validation current_textual_octet is [u8; 3] but it was indexed by an unbounded count of octets if they matched 1..9. --- src/name/ip_address.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/name/ip_address.rs b/src/name/ip_address.rs index 7658922d..de27af9f 100644 --- a/src/name/ip_address.rs +++ b/src/name/ip_address.rs @@ -353,6 +353,10 @@ pub(crate) fn is_valid_ipv4_address(ip_address: untrusted::Input) -> bool { // No octet can start with 0 if a dot does not follow and if we are not at the end. return false; } + if current_textual_octet_size >= current_textual_octet.len() { + // More than 3 octets in a triple + return false; + } current_textual_octet[current_textual_octet_size] = u8::from_be(number); current_textual_octet_size += 1; } @@ -465,6 +469,10 @@ mod tests { (b"0.025.0.0", false), (b"0.0.025.0", false), (b"0.0.0.025", false), + (b"1234.0.0.0", false), + (b"0.1234.0.0", false), + (b"0.0.1234.0", false), + (b"0.0.0.1234", false), ]; #[test] From 7bb2899f87036d43c6d89d799e5c6c388f7edef1 Mon Sep 17 00:00:00 2001 From: Joseph Birr-Pixton Date: Sun, 18 Sep 2022 11:53:25 +0100 Subject: [PATCH 3/7] ipv6: allow upper case hex rfc5952 says both are allowed. --- src/name/ip_address.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/name/ip_address.rs b/src/name/ip_address.rs index de27af9f..c02e934d 100644 --- a/src/name/ip_address.rs +++ b/src/name/ip_address.rs @@ -418,7 +418,7 @@ pub(crate) fn is_valid_ipv6_address(ip_address: untrusted::Input) -> bool { // We move on to the next textual block. current_textual_block_size = 0; } - Ok(b'0'..=b'9') | Ok(b'a'..=b'f') => { + Ok(b'0'..=b'9') | Ok(b'a'..=b'f') | Ok(b'A'..=b'F') => { if current_textual_block_size == 4 { // Blocks cannot contain more than 4 hexadecimal characters. return false; @@ -495,6 +495,7 @@ mod tests { // Valid IPv6 addresses (b"2a05:d018:076c:b685:e8ab:afd3:af51:3aed", true), (b"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true), + (b"FFFF:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true), // both case hex allowed // Invalid IPv6 addresses // Missing octets on uncompressed addresses. The unmatching letter has the violation From f285cd2251a5d0b01d5a923f9d41b1b3bff14211 Mon Sep 17 00:00:00 2001 From: Joseph Birr-Pixton Date: Sun, 18 Sep 2022 12:06:45 +0100 Subject: [PATCH 4/7] Add basic tests for ipv4/ipv6 SANs --- tests/cloudflare_dns/ca.der | Bin 0 -> 947 bytes tests/cloudflare_dns/ee.der | Bin 0 -> 1533 bytes tests/cloudflare_dns/inter.der | Bin 0 -> 1051 bytes tests/integration.rs | 53 +++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 tests/cloudflare_dns/ca.der create mode 100644 tests/cloudflare_dns/ee.der create mode 100644 tests/cloudflare_dns/inter.der diff --git a/tests/cloudflare_dns/ca.der b/tests/cloudflare_dns/ca.der new file mode 100644 index 0000000000000000000000000000000000000000..2f1e5523af47d44e35ea27be89e25a663ea7f99f GIT binary patch literal 947 zcmXqLVqS01#58>YGZP~dlK_YHgRlusZW|YtW}S?jc-+f?myJ`a&7D}zCz zA-4f18*?ZNn=n&ou%W1dFo?q?%;S=op6Q%gRHERSmux6$APy4d78WQkFV{-}N+koO z^pf*)4HXRJKvK-Y;&2`AIr&M6ISN7f`6UX@js|k#yar~5hK2?Z5GBrQ4CKMN^l(BG zqY|>m8Ce;an;7{S44N3Zn3@dd`IJs!)JE3#aq8T|A z&M;11?q{$)_`CCwOos5ek9w8vwzD}fG;*FhC*8dN{#olnw@25~jV;t*_#-WJ|60ik06Z{$usNcN*7A>Z>YdMz&_;#V8wj@Ao_KA}aTB!TxfA z{eKw0wccFeA`oG;vQ*;g#YOwvAE{@3nPvRLlOv(~O8ge9mi-G?dHK}Zo%fH=DT`a+ zZ9Z|{9i?Zv=R~CXvQGayyICYf{CKqHJfr=e-u=4F?Q~`Jc1NRIJXhZTXuP_+H^P3a z^b1doMFqDES?+${B=*UN%a47$hjQ?3! zfC;G0fFHyc2Ju-9n1PgmEJ%QlMT|v+`Cfo&*hQb+X&)9pl9oO^eUt6QPy>08v@(l? zfmj1}1*qu{7!8aJr{|cj@(>o{`gPZ3j(^aGFZb86e66hHSga!P$6LGQfbPz#$Nzmf zC?9cpNA8@elIJrtR|{@^T(Dr_S)Y!XlY1r>JX*~!vv2as|H)UMuqRwzynk*q=f|sk zkAuJTCI7NtetS#W{iL*O7xNr@Kk06LRqi!^Zp0()$O8__Z*?`AT67HFks3>ujWo?Cxj#WeU z^4IG{MPAb-4oIsket75QefOHJTU0n$4c_SP<=*eQKVav| c&#z}N3avfSacqW3d@q;Hv6ok;PR+ds0Q5RJkHs&Ff$v38FCwN zvN4CUun9AT1{;bR2!l9W!aOdS>6y-{MI{QJdC7(b26`Z2W?^->aEMQ^f=6XiQD%yQ ztFyC0u!p0ug^7ZZfsuiNv!kJboH(zMp`n41sR0;70l7vXu92~+rKx3OfHzluFFF8NgAQI>~cCgQx7@Y(*Mjl|cG0-$nhk1aBQA`@@*z)pnR6Cplf(;s5VJaCJStSirR7ieh1bqW;QH~?H&!i1HTHN5Fxk}XOg#9W zfq{jY!Nb6fNs-|+_a5K3AI~g~I4HgH)80)h#8#`k+U)cD-{Z1PkALr(IsvHU8&_iU z3E>qHM^~3BPYe=%&%oHm%iqu$sD1lNoA#AJPKHv3z{MNB{$;G36PCNfyrQ3HN!1x? z_0!+ybvVu9{dc9C^(oZiUto*f3|yEL>JKP(SzMW6e;~Q=`qb%K=8BHZ%R;7h9AMUy zF4kI>_=8D-{ZWwHtK*)bcjD7$C$(~|y71OE%a?QRWoaFOzZ|jezA%(AIG#AQ@xGFq zm%y=wR-8`laeju~(#)@4dmqpEFd=Cg+c&7SJHXbu8@Mtlq}-bNPTypkMr8gvZm}go zi{|eC&XB*dF=fs*KZcKM1uB5v?Pt2$J+msB<7{jBxrvWXbfz3xa(8Nn$+Y5se|=w$L!^u43-F>ot+heJsgcKOcaa^j0}L*8XA-t2(YmO z{d&%piII&}yOD)Ki8+aZ<)HMjSC6$Y{X9#s*>l zg*=olE&J_v@P;n?KR3j Date: Sun, 18 Sep 2022 12:33:42 +0100 Subject: [PATCH 5/7] textual_octets_to_octet: simplify and satisfy clippy Seems better to convert from ascii to radix-10 at the time that is known, rather than doing that validation twice (and skipping a digit as an error handling strategy). --- src/name/ip_address.rs | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/name/ip_address.rs b/src/name/ip_address.rs index c02e934d..617d7339 100644 --- a/src/name/ip_address.rs +++ b/src/name/ip_address.rs @@ -296,22 +296,18 @@ pub(super) fn presented_id_matches_constraint( pub(crate) fn is_valid_ipv4_address(ip_address: untrusted::Input) -> bool { let mut ip_address = untrusted::Reader::new(ip_address); let mut is_first_byte = true; - let mut current_textual_octet: [u8; 3] = [0, 0, 0]; - let mut current_textual_octet_size = 0; + let mut current: [u8; 3] = [0, 0, 0]; + let mut current_size = 0; let mut dot_count = 0; loop { // Returns a u32 so it's possible to identify (and error) when // provided textual octets > 255, not representable by u8. - fn textual_octets_to_octet(textual_octets: [u8; 3], textual_octet_size: usize) -> u32 { + fn radix10_to_octet(textual_octets: &[u8]) -> u32 { let mut result: u32 = 0; - for (i, textual_octet) in textual_octets.iter().rev().enumerate() { - if i >= textual_octet_size { - break; - } - if let Some(digit) = char::to_digit(*textual_octet as char, 10) { - result += digit * 10_u32.pow(i as u32); - } + for digit in textual_octets.iter() { + result *= 10; + result += u32::from(*digit); } result } @@ -331,34 +327,33 @@ pub(crate) fn is_valid_ipv4_address(ip_address: untrusted::Input) -> bool { return false; } dot_count += 1; - if current_textual_octet_size == 0 { + if current_size == 0 { // IPv4 address cannot contain two dots in a row. return false; } - if textual_octets_to_octet(current_textual_octet, current_textual_octet_size) > 255 - { + if radix10_to_octet(¤t[..current_size]) > 255 { // No octet can be greater than 255. return false; } // We move on to the next textual octet. - current_textual_octet = [0, 0, 0]; - current_textual_octet_size = 0; + current = [0, 0, 0]; + current_size = 0; } Ok(number @ b'0'..=b'9') => { if number == b'0' - && current_textual_octet_size == 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 false; } - if current_textual_octet_size >= current_textual_octet.len() { + if current_size >= current.len() { // More than 3 octets in a triple return false; } - current_textual_octet[current_textual_octet_size] = u8::from_be(number); - current_textual_octet_size += 1; + current[current_size] = number - b'0'; + current_size += 1; } _ => { return false; @@ -367,9 +362,7 @@ pub(crate) fn is_valid_ipv4_address(ip_address: untrusted::Input) -> bool { is_first_byte = false; if ip_address.at_end() { - if current_textual_octet_size > 0 - && textual_octets_to_octet(current_textual_octet, current_textual_octet_size) > 255 - { + if current_size > 0 && radix10_to_octet(¤t[..current_size]) > 255 { // No octet can be greater than 255. return false; } From 036fdfa659cd0b18f7940d3d7c423398d0a9783e Mon Sep 17 00:00:00 2001 From: Joseph Birr-Pixton Date: Sun, 18 Sep 2022 12:39:34 +0100 Subject: [PATCH 6/7] Add name.rs to package --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 4be84684..f8463034 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,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", From 61cd0b22dece2016b1e510ccf3a9a7b9f0bf2596 Mon Sep 17 00:00:00 2001 From: Joseph Birr-Pixton Date: Sun, 9 Oct 2022 15:26:18 +0100 Subject: [PATCH 7/7] Appease clippy explicit-auto-deref --- src/verify_cert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verify_cert.rs b/src/verify_cert.rs index 8a6bed91..0bf0a7cd 100644 --- a/src/verify_cert.rs +++ b/src/verify_cert.rs @@ -83,7 +83,7 @@ pub fn build_chain( loop_while_non_fatal_error(intermediate_certs, |cert_der| { let potential_issuer = - cert::parse_cert(untrusted::Input::from(*cert_der), EndEntityOrCa::Ca(cert))?; + cert::parse_cert(untrusted::Input::from(cert_der), EndEntityOrCa::Ca(cert))?; if potential_issuer.subject != cert.issuer { return Err(Error::UnknownIssuer);