-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
verification: add RFC822Constraint (#10497)
* 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
Showing
1 changed file
with
166 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<'_> { | ||
|
@@ -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>, | ||
|
@@ -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. | ||
|
@@ -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); | ||
|
@@ -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())); | ||
} | ||
} | ||
} |