diff --git a/src/rust/cryptography-x509-validation/src/policy/mod.rs b/src/rust/cryptography-x509-validation/src/policy/mod.rs index 17e7e636e71d..b9bc437901b3 100644 --- a/src/rust/cryptography-x509-validation/src/policy/mod.rs +++ b/src/rust/cryptography-x509-validation/src/policy/mod.rs @@ -19,7 +19,7 @@ use cryptography_x509::oid::{ }; use crate::ops::CryptoOps; -use crate::types::{DNSName, DNSPattern, IPAddress, IPRange}; +use crate::types::{DNSName, DNSPattern, IPAddress, IPConstraint}; // RSASSA‐PKCS1‐v1_5 with SHA‐256 static RSASSA_PKCS1V15_SHA256: AlgorithmIdentifier<'_> = AlgorithmIdentifier { @@ -125,7 +125,7 @@ impl Subject<'_> { DNSPattern::new(pattern.0).map_or(false, |p| p.matches(name)) } (GeneralName::IPAddress(pattern), Self::IP(name)) => { - IPRange::from_bytes(pattern).map_or(false, |p| p.matches(name)) + IPConstraint::from_bytes(pattern).map_or(false, |p| p.matches(name)) } _ => false, } @@ -218,7 +218,6 @@ mod tests { use cryptography_x509::{ extensions::SubjectAlternativeName, name::{GeneralName, UnvalidatedIA5String}, - oid::EXTENDED_KEY_USAGE_OID, }; use crate::{ diff --git a/src/rust/cryptography-x509-validation/src/types.rs b/src/rust/cryptography-x509-validation/src/types.rs index 515962ad13aa..2868c59cc3ef 100644 --- a/src/rust/cryptography-x509-validation/src/types.rs +++ b/src/rust/cryptography-x509-validation/src/types.rs @@ -69,6 +69,12 @@ impl<'a> DNSName<'a> { None => None, } } + + /// Returns this DNS name's labels, in reversed order + /// (from top-level domain to most-specific subdomain). + fn rlabels(&self) -> impl Iterator { + self.as_str().rsplit('.') + } } impl PartialEq for DNSName<'_> { @@ -113,6 +119,48 @@ impl<'a> DNSPattern<'a> { } } +/// A `DNSConstraint` represents a DNS name constraint as defined in [RFC 5280 4.2.1.10]. +/// +/// [RFC 5280 4.2.1.10]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10 +pub struct DNSConstraint<'a>(DNSName<'a>); + +impl<'a> DNSConstraint<'a> { + pub fn new(pattern: &'a str) -> Option { + DNSName::new(pattern).map(Self) + } + + /// Returns true if this `DNSConstraint` matches the given name. + /// + /// Constraint matching is defined by RFC 5280: any DNS name that can + /// be constructed by simply adding zero or more labels to the left-hand + /// side of the name satisfies the name constraint. + /// + /// ```rust + /// # use cryptography_x509_validation::types::{DNSConstraint, DNSName}; + /// let example_com = DNSName::new("example.com").unwrap(); + /// let badexample_com = DNSName::new("badexample.com").unwrap(); + /// let foo_example_com = DNSName::new("foo.example.com").unwrap(); + /// assert!(DNSConstraint::new(example_com.as_str()).unwrap().matches(&example_com)); + /// assert!(DNSConstraint::new(example_com.as_str()).unwrap().matches(&foo_example_com)); + /// assert!(!DNSConstraint::new(example_com.as_str()).unwrap().matches(&badexample_com)); + /// ``` + pub fn matches(&self, name: &DNSName<'_>) -> bool { + // NOTE: This may seem like an obtuse way to perform label matching, + // but it saves us a few allocations: doing a substring check instead + // would require us to clone each string and do case normalization. + // Note also that we check the length in advance: Rust's zip + // implementation terminates with the shorter iterator, so we need + // to first check that the candidate name is at least as long as + // the constraint it's matching against. + name.as_str().len() >= self.0.as_str().len() + && self + .0 + .rlabels() + .zip(name.rlabels()) + .all(|(a, o)| a.eq_ignore_ascii_case(o)) + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct IPAddress(IpAddr); @@ -206,17 +254,17 @@ impl From for IPAddress { } #[derive(Debug, PartialEq, Eq)] -pub struct IPRange { +pub struct IPConstraint { address: IPAddress, prefix: u8, } -/// An `IPRange` represents a CIDR-style address range used in a name constraints +/// An `IPConstraint` represents a CIDR-style IP address range used in a name constraints /// extension, as defined by [RFC 5280 4.2.1.10]. /// /// [RFC 5280 4.2.1.10]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10 -impl IPRange { - /// Constructs an `IPRange` from a slice. The input slice must be 8 (IPv4) +impl IPConstraint { + /// Constructs an `IPConstraint` from a slice. The input slice must be 8 (IPv4) /// or 32 (IPv6) bytes long and contain two IP addresses, the first being /// a subnet and the second defining the subnet's mask. /// @@ -231,18 +279,18 @@ impl IPRange { }; let prefix = IPAddress::from_bytes(&b[slice_idx..])?.as_prefix()?; - Some(IPRange { + Some(IPConstraint { address: IPAddress::from_bytes(&b[..slice_idx])?.mask(prefix), prefix, }) } - /// Determines if the `addr` is within the `IPRange`. + /// Determines if the `addr` is within the `IPConstraint`. /// /// ```rust - /// # use cryptography_x509_validation::types::{IPAddress,IPRange}; + /// # use cryptography_x509_validation::types::{IPAddress, IPConstraint}; /// let range_bytes = b"\xc6\x33\x64\x00\xff\xff\xff\x00"; - /// let range = IPRange::from_bytes(range_bytes).unwrap(); + /// let range = IPConstraint::from_bytes(range_bytes).unwrap(); /// assert!(range.matches(&IPAddress::from_str("198.51.100.42").unwrap())); /// ``` pub fn matches(&self, addr: &IPAddress) -> bool { @@ -252,7 +300,7 @@ impl IPRange { #[cfg(test)] mod tests { - use crate::types::{DNSName, DNSPattern, IPAddress, IPRange}; + use crate::types::{DNSConstraint, DNSName, DNSPattern, IPAddress, IPConstraint}; #[test] fn test_dnsname_debug_trait() { @@ -286,6 +334,8 @@ mod tests { assert_eq!(DNSName::new("foo.bar-.example.com"), None); assert_eq!(DNSName::new(&"a".repeat(64)), None); assert_eq!(DNSName::new("⚠️"), None); + assert_eq!(DNSName::new(".foo.example"), None); + assert_eq!(DNSName::new(".example.com"), None); let long_valid_label = "a".repeat(63); let long_name = std::iter::repeat(long_valid_label) @@ -386,6 +436,36 @@ mod tests { assert!(!any_localhost.matches(&DNSName::new("localhost").unwrap())); } + #[test] + fn test_dnsconstraint_new() { + assert!(DNSConstraint::new("").is_none()); + assert!(DNSConstraint::new(".").is_none()); + assert!(DNSConstraint::new("*.").is_none()); + assert!(DNSConstraint::new("*").is_none()); + assert!(DNSConstraint::new(".example").is_none()); + assert!(DNSConstraint::new("*.example").is_none()); + assert!(DNSConstraint::new("*.example.com").is_none()); + + assert!(DNSConstraint::new("example").is_some()); + assert!(DNSConstraint::new("example.com").is_some()); + assert!(DNSConstraint::new("foo.example.com").is_some()); + } + + #[test] + fn test_dnsconstraint_matches() { + let example_com = DNSConstraint::new("example.com").unwrap(); + + // Exact domain and arbitrary subdomains match. + assert!(example_com.matches(&DNSName::new("example.com").unwrap())); + assert!(example_com.matches(&DNSName::new("foo.example.com").unwrap())); + assert!(example_com.matches(&DNSName::new("foo.bar.baz.quux.example.com").unwrap())); + + // Parent domains, distinct domains, and substring domains do not match. + assert!(!example_com.matches(&DNSName::new("com").unwrap())); + assert!(!example_com.matches(&DNSName::new("badexample.com").unwrap())); + assert!(!example_com.matches(&DNSName::new("wrong.com").unwrap())); + } + #[test] fn test_ipaddress_from_str() { assert_ne!(IPAddress::from_str("192.168.1.1"), None) @@ -442,7 +522,7 @@ mod tests { } #[test] - fn test_iprange_from_bytes() { + fn test_ipconstraint_from_bytes() { let ipv4_bad = b"\xc0\xa8\x01\x01\xff\xfe\xff\x00"; let ipv4_bad_many_bits = b"\xc0\xa8\x01\x01\xff\xfc\xff\x00"; let ipv4_bad_octet = b"\xc0\xa8\x01\x01\x00\xff\xff\xff"; @@ -458,38 +538,38 @@ mod tests { \x00\x00\x00\x00\x00\x00\x00\x00"; let bad = b"\xff\xff\xff"; - assert_eq!(IPRange::from_bytes(ipv4_bad), None); - assert_eq!(IPRange::from_bytes(ipv4_bad_many_bits), None); - assert_eq!(IPRange::from_bytes(ipv4_bad_octet), None); - assert_eq!(IPRange::from_bytes(ipv6_bad), None); - assert_ne!(IPRange::from_bytes(ipv6_good), None); - assert_eq!(IPRange::from_bytes(bad), None); + assert_eq!(IPConstraint::from_bytes(ipv4_bad), None); + assert_eq!(IPConstraint::from_bytes(ipv4_bad_many_bits), None); + assert_eq!(IPConstraint::from_bytes(ipv4_bad_octet), None); + assert_eq!(IPConstraint::from_bytes(ipv6_bad), None); + assert_ne!(IPConstraint::from_bytes(ipv6_good), None); + assert_eq!(IPConstraint::from_bytes(bad), None); // 192.168.1.1/16 let ipv4_with_extra = b"\xc0\xa8\x01\x01\xff\xff\x00\x00"; - assert_ne!(IPRange::from_bytes(ipv4_with_extra), None); + assert_ne!(IPConstraint::from_bytes(ipv4_with_extra), None); // 192.168.0.0/16 let ipv4_masked = b"\xc0\xa8\x00\x00\xff\xff\x00\x00"; assert_eq!( - IPRange::from_bytes(ipv4_with_extra), - IPRange::from_bytes(ipv4_masked) + IPConstraint::from_bytes(ipv4_with_extra), + IPConstraint::from_bytes(ipv4_masked) ); } #[test] - fn test_iprange_matches() { + fn test_ipconstraint_matches() { // 192.168.1.1/16 - let ipv4 = IPRange::from_bytes(b"\xc0\xa8\x01\x01\xff\xff\x00\x00").unwrap(); - let ipv4_32 = IPRange::from_bytes(b"\xc0\x00\x02\xde\xff\xff\xff\xff").unwrap(); - let ipv6 = IPRange::from_bytes( + let ipv4 = IPConstraint::from_bytes(b"\xc0\xa8\x01\x01\xff\xff\x00\x00").unwrap(); + let ipv4_32 = IPConstraint::from_bytes(b"\xc0\x00\x02\xde\xff\xff\xff\xff").unwrap(); + let ipv6 = IPConstraint::from_bytes( b"\x26\x00\x0d\xb8\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x01\ \xff\xff\xff\xff\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00", ) .unwrap(); - let ipv6_128 = IPRange::from_bytes( + let ipv6_128 = IPConstraint::from_bytes( b"\x26\x00\x0d\xb8\x00\x00\x00\x00\ \x00\x00\x00\x00\xff\x00\xde\xde\ \xff\xff\xff\xff\xff\xff\xff\xff\