From 54bc1e2c5b94368abb0174a1a3d3fb797daa2b7b 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] Implement IP address validation 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. --- src/end_entity.rs | 13 +- src/lib.rs | 7 +- src/name.rs | 8 +- src/name/dns_name.rs | 9 +- src/name/ip_address.rs | 525 +++++++++++++++++++++++++++++++++++++++++ src/name/name.rs | 92 ++++++++ src/name/verify.rs | 78 +++++- src/verify_cert.rs | 2 + 8 files changed, 719 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..82a0263d 100644 --- a/src/name/ip_address.rs +++ b/src/name/ip_address.rs @@ -14,6 +14,217 @@ use crate::Error; +use core::convert::TryFrom; + +#[cfg(feature = "alloc")] +use alloc::string::String; + +#[cfg(feature = "alloc")] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum IpAddress { + IpV4Address(String, [u8; 4]), + 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() + } + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum IpAddressRef<'a> { + IpV4AddressRef(&'a [u8], [u8; 4]), + 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("IPv4 is correct by construction"), + ip_address_octets, + ), + IpAddressRef::IpV6AddressRef(ip_address, ip_address_octets) => IpAddress::IpV6Address( + String::from_utf8(ip_address.to_vec()).expect("IPv6 is correct 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) -> Result { + Ok(match self { + IpAddressRef::IpV4AddressRef(ip_address, ip_address_octets) => IpAddress::IpV4Address( + String::from_utf8(ip_address.to_vec()).expect("IPv4 is correct by construction"), + *ip_address_octets, + ), + IpAddressRef::IpV6AddressRef(ip_address, ip_address_octets) => IpAddress::IpV6Address( + String::from_utf8(ip_address.to_vec()).expect("IPv6 is correct by construction"), + *ip_address_octets, + ), + }) + } +} + +impl<'a> TryFrom> for &'a str { + type Error = InvalidIpAddressError; + fn try_from(ip_address: IpAddressRef<'a>) -> Result<&'a str, InvalidIpAddressError> { + match ip_address { + IpAddressRef::IpV4AddressRef(ip_address, _) + | IpAddressRef::IpV6AddressRef(ip_address, _) => { + core::str::from_utf8(ip_address).map_err(|_| InvalidIpAddressError) + } + } + } +} + +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 +273,317 @@ 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 { + // IPv6 address cannot start with a colon. + return false; + } + if ip_address.at_end() { + // 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 { + // We do not support compressed IPv6 addresses + 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::*; + + #[test] + fn is_valid_ipv4_address_test() { + // Valid IPv4 addresses + assert!(is_valid_ipv4_address(untrusted::Input::from( + "0.0.0.0".as_bytes() + ))); + assert!(is_valid_ipv4_address(untrusted::Input::from( + "127.0.0.1".as_bytes() + ))); + assert!(is_valid_ipv4_address(untrusted::Input::from( + "1.1.1.1".as_bytes() + ))); + assert!(is_valid_ipv4_address(untrusted::Input::from( + "255.255.255.255".as_bytes() + ))); + + // Invalid IPv4 addresses + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "...".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + ".0.0.0.0".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "0.0.0.0.".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "256.0.0.0".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "0.256.0.0".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "0.0.256.0".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "0.0.0.256".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "1..1.1.1".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "1.1..1.1".as_bytes() + ))); + assert!(!is_valid_ipv4_address(untrusted::Input::from( + "1.1.1..1".as_bytes() + ))); + } + + #[test] + fn is_valid_ipv6_address_test() { + // Valid IPv6 addresses + assert!(is_valid_ipv6_address(untrusted::Input::from( + "2a05:d018:076c:b685:e8ab:afd3:af51:3aed".as_bytes() + ))); + assert!(is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + + // Invalid IPv6 addresses + // Missing octets on uncompressed addresses. The unmatching letter has the violation + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "aaa:ffff:ffff:ffff:ffff:ffff:ffff:fffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:aaa:ffff:ffff:ffff:ffff:ffff:fffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:aaa:ffff:ffff:ffff:ffff:fffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:aaa:ffff:ffff:ffff:fffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:aaa:ffff:ffff:fffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:aaa:ffff:fffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff:aaa:fffff".as_bytes() + ))); + // Wrong hexadecimal characters on different positions + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffgf:ffff:ffff:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:gfff:ffff:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:fffg:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:fgff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:fffg:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:gfff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff:ffgf:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fgff".as_bytes() + ))); + // Wrong colons on uncompressed addresses + assert!(!is_valid_ipv6_address(untrusted::Input::from( + ":ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff::ffff:ffff:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff::ffff:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff::ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff::ffff:ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff::ffff:ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff::ffff:ffff".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff::ffff".as_bytes() + ))); + // More colons than allowed + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:".as_bytes() + ))); + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".as_bytes() + ))); + // This is a valid IPv6 address, but we don't support compressed addresses + assert!(!is_valid_ipv6_address(untrusted::Input::from( + "2a05:d018:76c:b685:e8ab:afd3:af51:3aed".as_bytes() + ))); + } + + #[test] + fn ipv4_octets_test() { + assert_eq!(ipv4_octets("0.0.0.0".as_bytes()), Ok([0, 0, 0, 0])); + assert_eq!( + ipv4_octets("54.155.246.232".as_bytes()), + Ok([54, 155, 246, 232]) + ); + } + + #[test] + fn ipv6_octets_test() { + assert_eq!( + ipv6_octets("2a05:d018:076c:b684:8e48:47c9:84aa:b34d".as_bytes()), + 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..39dd3a16 --- /dev/null +++ b/src/name/name.rs @@ -0,0 +1,92 @@ +// 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}; + +#[derive(Debug, Clone, Copy)] +pub enum DnsNameOrIpRef<'a> { + DnsName(DnsNameRef<'a>), + 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> { + 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..33449d0d 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,10 +29,11 @@ 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| { + #[allow(clippy::single_match)] match name { GeneralName::DnsName(presented_id) => { match dns_name::presented_id_matches_reference_id(presented_id, dns_name) { @@ -51,6 +53,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 +128,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 +302,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 +329,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 } } diff --git a/src/verify_cert.rs b/src/verify_cert.rs index 8a6bed91..72383f95 100644 --- a/src/verify_cert.rs +++ b/src/verify_cert.rs @@ -53,6 +53,7 @@ pub fn build_chain( // TODO: revocation. + #[allow(clippy::single_match)] match loop_while_non_fatal_error(trust_anchors, |trust_anchor: &TrustAnchor| { let trust_anchor_subject = untrusted::Input::from(trust_anchor.subject); if cert.issuer != trust_anchor_subject { @@ -339,6 +340,7 @@ where V: IntoIterator, { for v in values { + #[allow(clippy::single_match)] match f(v) { Ok(()) => { return Ok(());