Skip to content

Commit

Permalink
verification: add RFC822Constraint (#10497)
Browse files Browse the repository at this point in the history
* verification: add RFC822Constraint

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

* verification: derive, don't be so clever

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

* verification: reduce cleverness some more

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

---------

Signed-off-by: William Woodruff <[email protected]>
  • Loading branch information
woodruffw authored Feb 28, 2024
1 parent 9b4008b commit be31fd5
Showing 1 changed file with 166 additions and 0 deletions.
166 changes: 166 additions & 0 deletions src/rust/cryptography-x509-verification/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ impl<'a> DNSName<'a> {
fn rlabels(&self) -> impl Iterator<Item = &'_ str> {
self.as_str().rsplit('.')
}

/// Returns true if this domain is a subdomain of the other domain.
fn is_subdomain_of(&self, other: &DNSName<'_>) -> bool {
// NOTE: This is nearly identical to `DNSConstraint::matches`,
// except that the subdomain must be strictly longer than the parent domain.
self.as_str().len() > other.as_str().len()
&& self
.rlabels()
.zip(other.rlabels())
.all(|(a, o)| a.eq_ignore_ascii_case(o))
}
}

impl PartialEq for DNSName<'_> {
Expand Down Expand Up @@ -312,6 +323,7 @@ impl IPConstraint {
///
/// [RFC 822 6.1]: https://datatracker.ietf.org/doc/html/rfc822#section-6.1
/// [RFC 2821 4.1.2]: https://datatracker.ietf.org/doc/html/rfc2821#section-4.1.2
#[derive(PartialEq)]
pub struct RFC822Name<'a> {
pub mailbox: IA5String<'a>,
pub domain: DNSName<'a>,
Expand Down Expand Up @@ -348,10 +360,45 @@ impl<'a> RFC822Name<'a> {
}
}

/// An `RFC822Constraint` represents a Name Constraint on email addresses.
pub enum RFC822Constraint<'a> {
/// A constraint for an exact match on a specific email address.
Exact(RFC822Name<'a>),
/// A constraint for any mailbox on a particular domain.
OnDomain(DNSName<'a>),
/// A constraint for any mailbox *within* a particular domain.
/// For example, `InDomain("example.com")` will match `[email protected]`
/// but not `[email protected]`, since `bar.example.com` is in `example.com`
/// but `example.com` is not within itself.
InDomain(DNSName<'a>),
}

impl<'a> RFC822Constraint<'a> {
pub fn new(constraint: &'a str) -> Option<Self> {
if let Some(constraint) = constraint.strip_prefix('.') {
Some(Self::InDomain(DNSName::new(constraint)?))
} else if let Some(email) = RFC822Name::new(constraint) {
Some(Self::Exact(email))
} else {
Some(Self::OnDomain(DNSName::new(constraint)?))
}
}

pub fn matches(&self, email: &RFC822Name<'_>) -> bool {
match self {
Self::Exact(pat) => pat == email,
Self::OnDomain(pat) => &email.domain == pat,
Self::InDomain(pat) => email.domain.is_subdomain_of(pat),
}
}
}

#[cfg(test)]
mod tests {
use crate::types::{DNSConstraint, DNSName, DNSPattern, IPAddress, IPConstraint, RFC822Name};

use super::RFC822Constraint;

#[test]
fn test_dnsname_debug_trait() {
// Just to get coverage on the `Debug` derive.
Expand Down Expand Up @@ -442,6 +489,33 @@ mod tests {
);
}

#[test]
fn test_dnsname_is_subdomain_of() {
for (sup, sub, check) in &[
// good cases
("example.com", "sub.example.com", true),
("example.com", "a.b.example.com", true),
("sub.example.com", "sub.sub.example.com", true),
("sub.example.com", "sub.sub.sub.example.com", true),
("com", "example.com", true),
("example.com", "com.example.com", true),
("example.com", "com.example.example.com", true),
// bad cases
("example.com", "example.com", false),
("example.com", "com", false),
("sub.example.com", "example.com", false),
("sub.sub.example.com", "sub.sub.example.com", false),
("sub.sub.example.com", "example.com", false),
("com.example.com", "com.example.com", false),
("com.example.example.com", "com.example.example.com", false),
] {
let sup = DNSName::new(sup).unwrap();
let sub = DNSName::new(sub).unwrap();

assert_eq!(sub.is_subdomain_of(&sup), *check);
}
}

#[test]
fn test_dnspattern_new() {
assert_eq!(DNSPattern::new("*"), None);
Expand Down Expand Up @@ -694,4 +768,96 @@ mod tests {
assert_eq!(&parsed.domain.as_str(), domain);
}
}

#[test]
fn test_rfc822constraint_new() {
for (case, valid) in &[
// good cases
("[email protected]", true),
("[email protected]", true),
("[email protected]", true),
("example.com", true),
("sub.example.com", true),
("[email protected]", true),
("[email protected]", true),
("[email protected]", true),
(".example.com", true),
(".sub.example.com", true),
// bad cases
("@example.com", false),
("@@example.com", false),
("[email protected]", false),
("[email protected]", false),
("[email protected]", false),
("[email protected]", false),
("invaliddomain!", false),
("..example.com", false),
("foo..example.com", false),
(".foo..example.com", false),
("..foo..example.com", false),
] {
assert_eq!(RFC822Constraint::new(case).is_some(), *valid);
}
}

#[test]
fn test_rfc822constraint_matches() {
{
let exact = RFC822Constraint::new("[email protected]").unwrap();

// Ordinary exact match.
assert!(exact.matches(&RFC822Name::new("[email protected]").unwrap()));
// Case changes are okay in the domain.
assert!(exact.matches(&RFC822Name::new("[email protected]").unwrap()));

// Case changes are not okay in the mailbox.
assert!(!exact.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(!exact.matches(&RFC822Name::new("[email protected]").unwrap()));

// Different mailboxes and domains do not match.
assert!(!exact.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(!exact.matches(&RFC822Name::new("[email protected]").unwrap()));
}

{
let on_domain = RFC822Constraint::new("example.com").unwrap();

// Ordinary domain matches.
assert!(on_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(on_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(on_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(on_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
// Case changes are okay in the domain and in the mailbox,
// since any mailbox on the domain is okay.
assert!(on_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(on_domain.matches(&RFC822Name::new("[email protected]").unwrap()));

// Subdomains and other domains do not match.
assert!(!on_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(!on_domain.matches(&RFC822Name::new("foo@localhost").unwrap()));
}

{
let in_domain = RFC822Constraint::new(".example.com").unwrap();

// Any subdomain and mailbox matches.
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
// Case changes are okay in the subdomains and in the mailbox, since any mailbox
// in the domain is okay.
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));

// Superdomains and other domains do not match.
assert!(!in_domain.matches(&RFC822Name::new("[email protected]").unwrap()));
assert!(!in_domain.matches(&RFC822Name::new("foo@com").unwrap()));
}
}
}

0 comments on commit be31fd5

Please sign in to comment.