Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

verification: add RFC822Name #10487

Merged
merged 4 commits into from
Feb 26, 2024
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 114 additions & 1 deletion src/rust/cryptography-x509-verification/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
use std::net::IpAddr;
use std::str::FromStr;

use asn1::IA5String;

// RFC 2822 3.2.4
static ATEXT_CHARS: &str = "!#$%&'*+-/=?^_`{|}~";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might make more sense/be more efficient as &[u8] instead.


/// A `DNSName` is an `asn1::IA5String` with additional invariant preservations
/// per [RFC 5280 4.2.1.6], which in turn uses the preferred name syntax defined
/// in [RFC 1034 3.5] and amended in [RFC 1123 2.1].
Expand Down Expand Up @@ -298,9 +303,56 @@ impl IPConstraint {
}
}

/// An `RFC822Name` represents an email address, as defined in [RFC 822 6.1]
/// and as amended by [RFC 2821 4.1.2]. In particular, it represents the `Mailbox`
/// rule from RFC 2821's grammar.
///
/// This type does not currently support the quoted local-part form; email
/// addresses that use this form will be rejected.
///
/// [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
pub struct RFC822Name<'a>((IA5String<'a>, DNSName<'a>));
woodruffw marked this conversation as resolved.
Show resolved Hide resolved

impl<'a> RFC822Name<'a> {
pub fn new(value: &'a str) -> Option<Self> {
// Mailbox = Local-part "@" Domain
// Both must be present.
let (local_part, domain) = value.split_once('@')?;
let local_part = IA5String::new(local_part)?;

// Local-part = Dot-string / Quoted-string
// NOTE(ww): We do not support the Quoted-string form, for now.
//
// Dot-string: Atom *("." Atom)
// Atom = 1*atext
//
// NOTE(ww): `atext`'s production is in RFC 2822 3.2.4.
for component in local_part.as_str().split('.') {
if component.is_empty()
|| !component
.chars()
.all(|c| c.is_ascii_alphanumeric() || ATEXT_CHARS.contains(c))
{
return None;
}
}

DNSName::new(domain).map(|domain| Self((local_part, domain)))
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn mailbox(&self) -> &str {
(self.0).0.as_str()
}

pub fn domain(&self) -> &DNSName<'_> {
&(self.0).1
}
}

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

#[test]
fn test_dnsname_debug_trait() {
Expand Down Expand Up @@ -587,4 +639,65 @@ mod tests {
assert!(!ipv6_128.matches(&IPAddress::from_str("2600::ff00:dede").unwrap()));
assert!(!ipv6_128.matches(&IPAddress::from_str("2600:db8::ff00:0").unwrap()));
}

#[test]
fn test_rfc822name() {
let bad_cases = &[
"",
// Missing local-part.
"@example.com",
" @example.com",
" @example.com",
// Missing domain cases.
"foo",
"foo@",
"foo@ ",
"foo@ ",
// Invalid domains.
"foo@!!!",
"foo@white space",
"foo@🙈",
// Invalid local part (empty mailbox sections).
".@example.com",
"foo.@example.com",
".foo@example.com",
".foo.@example.com",
".f.o.o.@example.com",
// Invalid local part (@ in mailbox).
"lol@lol@example.com",
"lol\\@lol@example.com",
"example@example.com@example.com",
"@@example.com",
// Invalid local part (invalid characters).
"lol\"lol@example.com",
"lol;lol@example.com",
"🙈@example.com",
// Intentionally unsupported quoted local parts.
"\"validbutunsupported\"@example.com",
];

for case in bad_cases {
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
assert!(RFC822Name::new(case).is_none());
}

// Each good case is (address, (mailbox, domain)).
let good_cases = &[
// Normal mailboxes.
("foo@example.com", ("foo", "example.com")),
("foo.bar@example.com", ("foo.bar", "example.com")),
("foo.bar.baz@example.com", ("foo.bar.baz", "example.com")),
("1.2.3.4.5@example.com", ("1.2.3.4.5", "example.com")),
// Mailboxes with special but valid characters.
("{legal}@example.com", ("{legal}", "example.com")),
("{&*.legal}@example.com", ("{&*.legal}", "example.com")),
("``````````@example.com", ("``````````", "example.com")),
("hello?@sub.example.com", ("hello?", "sub.example.com")),
];

for (address, (mailbox, domain)) in good_cases {
let parsed = RFC822Name::new(&address).unwrap();
assert_eq!(&parsed.mailbox(), mailbox);
assert_eq!(&parsed.domain().as_str(), domain);
}
}
}