Skip to content

Commit

Permalink
validation/types: add DNSConstraint, rename IPConstraint (#9700)
Browse files Browse the repository at this point in the history
* validation/types: add DNSConstraint, rename IPConstraint

This further fleshes out the helper types for name constraint
checking, as a breakout from #8873.

Co-authored-by: Alex Cameron <[email protected]>

Signed-off-by: William Woodruff <[email protected]>

* types: drop unnecessary traits

Signed-off-by: William Woodruff <[email protected]>

* types: don't do coverage in doctests

Signed-off-by: William Woodruff <[email protected]>

* types: avoid unnecessary Vec + rev

Signed-off-by: William Woodruff <[email protected]>

* types: update comment

Signed-off-by: William Woodruff <[email protected]>

---------

Signed-off-by: William Woodruff <[email protected]>
  • Loading branch information
woodruffw authored Oct 6, 2023
1 parent 5b12d65 commit 4cd984e
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 27 deletions.
5 changes: 2 additions & 3 deletions src/rust/cryptography-x509-validation/src/policy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -218,7 +218,6 @@ mod tests {
use cryptography_x509::{
extensions::SubjectAlternativeName,
name::{GeneralName, UnvalidatedIA5String},
oid::EXTENDED_KEY_USAGE_OID,
};

use crate::{
Expand Down
128 changes: 104 additions & 24 deletions src/rust/cryptography-x509-validation/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &'_ str> {
self.as_str().rsplit('.')
}
}

impl PartialEq for DNSName<'_> {
Expand Down Expand Up @@ -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<Self> {
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);

Expand Down Expand Up @@ -206,17 +254,17 @@ impl From<IpAddr> 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.
///
Expand All @@ -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 {
Expand All @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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";
Expand All @@ -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\
Expand Down

0 comments on commit 4cd984e

Please sign in to comment.