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

Migrate more types #9254

Merged
merged 15 commits into from
Jul 26, 2023
Merged
220 changes: 219 additions & 1 deletion src/rust/cryptography-x509/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,120 @@ impl<'a> asn1::SimpleAsn1Writable for UnvalidatedVisibleString<'a> {
}
}

/// Like `UnvalidatedIA5String`, but preserves the invariant that the
/// underlying string is ASCII only.
#[derive(Debug, PartialEq)]
pub struct IA5String(String);

impl IA5String {
pub(crate) fn new(value: String) -> Option<Self> {
if value.is_ascii() {
Some(Self(value))
} else {
None
}
}

pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
woodruffw marked this conversation as resolved.
Show resolved Hide resolved

/// A `DNSName` is an `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].
///
/// Internally, a `DNSName` is normalized to lowercase ASCII. Non-ASCII
/// domain names (i.e., internationalized names) must be pre-encoded.
///
/// [RFC 5280 4.2.1.6]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
/// [RFC 1034 3.5]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.5
/// [RFC 1123 2.1]: https://datatracker.ietf.org/doc/html/rfc1123#section-2.1
#[derive(Debug, PartialEq)]
pub struct DNSName(IA5String);

impl DNSName {
pub fn new(value: &str) -> Option<Self> {
// Domains cannot be empty; cannot contain whitespace; must (practically)
// be less than 253 characters (255 in RFC 1034's octet encoding).
if value.is_empty() || value.chars().any(char::is_whitespace) || value.len() > 253 {
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
None
} else {
for label in value.split('.') {
// Individual labels cannot be empty; cannot exceed 63 characters;
// cannot start or end with `-`.
// NOTE: RFC 1034's grammar prohibits consecutive hyphens, but these
// are used as part of the IDN prefix (e.g. `xn--`)'; we allow them here.
if label.is_empty()
|| label.len() > 63
|| label.starts_with('-')
|| label.ends_with('-')
{
return None;
}

// Labels must only contain `a-zA-Z0-9-`.
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return None;
}
}
IA5String::new(value.to_lowercase()).map(Self)
}
}

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

/// Return this `DNSName`'s parent domain, if it has one.
pub fn parent(&self) -> Option<Self> {
match self.as_str().split_once('.') {
Some((_, parent)) => Self::new(parent),
None => None,
}
}
}

/// A `DNSPattern` represents a subset of the domain name wildcard matching
/// behavior defined in [RFC 6125 6.4.3]. In particular, all DNS patterns
/// must either be exact matches (post-normalization) *or* a single wildcard
/// matching a full label in the left-most label position. Partial label matching
/// (e.g. `f*o.example.com`) is not supported, nor is non-left-most matching
/// (e.g. `foo.*.example.com`).
///
/// [RFC 6125 6.4.3]: https://datatracker.ietf.org/doc/html/rfc6125#section-6.4.3
#[derive(Debug, PartialEq)]
pub enum DNSPattern {
Exact(DNSName),
Wildcard(DNSName),
}

impl DNSPattern {
pub fn new(pat: &str) -> Option<Self> {
if let Some(pat) = pat.strip_prefix("*.") {
DNSName::new(pat).map(Self::Wildcard)
} else {
DNSName::new(pat).map(Self::Exact)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

It should probably be an error to have a wildcard in any other position, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, and it is -- DNSName::new(...) will return None if there are any wildcards present (since they aren't valid domain characters).

e.g., in tests below:

        assert_eq!(DNSPattern::new("f*o.example.com"), None);
        assert_eq!(DNSPattern::new("*oo.example.com"), None);
        assert_eq!(DNSPattern::new("fo*.example.com"), None);
        assert_eq!(DNSPattern::new("foo.*.example.com"), None);
        assert_eq!(DNSPattern::new("*.foo.*.example.com"), None);


pub fn matches(&self, name: &DNSName) -> bool {
match self {
Self::Exact(pat) => pat == name,
Self::Wildcard(pat) => match name.parent() {
Some(ref parent) => pat == parent,
// No parent means we have a single label; wildcards cannot match single labels.
None => false,
},
}
}
}

#[cfg(test)]
mod tests {
use super::{Asn1ReadableOrWritable, RawTlv, UnvalidatedVisibleString};
use super::{
Asn1ReadableOrWritable, DNSName, DNSPattern, IA5String, RawTlv, UnvalidatedVisibleString,
};
use asn1::Asn1Readable;

#[test]
Expand All @@ -330,4 +441,111 @@ mod tests {
let t = asn1::Tag::from_bytes(&[0]).unwrap().0;
assert!(RawTlv::can_parse(t));
}

#[test]
fn test_ia5string_constructs() {
assert_eq!(IA5String::new("⚠️".into()), None);
assert_eq!(IA5String::new("foo".into()).unwrap().as_str(), "foo");
}

#[test]
fn test_dnsname_constructs() {
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(DNSName::new(""), None);
assert_eq!(DNSName::new("."), None);
assert_eq!(DNSName::new(".."), None);
assert_eq!(DNSName::new(".a."), None);
assert_eq!(DNSName::new("a.a."), None);
assert_eq!(DNSName::new(".a"), None);
assert_eq!(DNSName::new("a."), None);
assert_eq!(DNSName::new("a.."), None);
assert_eq!(DNSName::new(" "), None);
assert_eq!(DNSName::new("\t"), None);
assert_eq!(DNSName::new(" whitespace "), None);
assert_eq!(DNSName::new("!badlabel!"), None);
assert_eq!(DNSName::new("bad!label"), None);
assert_eq!(DNSName::new("goodlabel.!badlabel!"), None);
assert_eq!(DNSName::new("-foo.bar.example.com"), None);
assert_eq!(DNSName::new("foo-.bar.example.com"), None);
assert_eq!(DNSName::new("foo.-bar.example.com"), None);
assert_eq!(DNSName::new("foo.bar-.example.com"), None);
assert_eq!(DNSName::new(&"a".repeat(64)), None);
assert_eq!(DNSName::new("⚠️"), None);

let long_valid_label = "a".repeat(63);
let long_name = std::iter::repeat(long_valid_label)
.take(5)
.collect::<Vec<_>>()
.join(".");
assert_eq!(DNSName::new(&long_name), None);

assert_eq!(
DNSName::new(&"a".repeat(63)).unwrap().as_str(),
"a".repeat(63)
);
assert_eq!(DNSName::new("example.com").unwrap().as_str(), "example.com");
assert_eq!(
DNSName::new("123.example.com").unwrap().as_str(),
"123.example.com"
);
assert_eq!(DNSName::new("EXAMPLE.com").unwrap().as_str(), "example.com");
assert_eq!(DNSName::new("EXAMPLE.COM").unwrap().as_str(), "example.com");
assert_eq!(
DNSName::new("xn--bcher-kva.example").unwrap().as_str(),
"xn--bcher-kva.example"
);
}

#[test]
fn test_dnsname_parent() {
assert_eq!(DNSName::new("localhost").unwrap().parent(), None);
assert_eq!(
DNSName::new("example.com").unwrap().parent().unwrap(),
DNSName::new("com").unwrap()
);
assert_eq!(
DNSName::new("foo.example.com").unwrap().parent().unwrap(),
DNSName::new("example.com").unwrap()
);
}

#[test]
fn test_dnspattern_constructs() {
assert_eq!(DNSPattern::new("*"), None);
assert_eq!(DNSPattern::new("*."), None);
assert_eq!(DNSPattern::new("f*o.example.com"), None);
assert_eq!(DNSPattern::new("*oo.example.com"), None);
assert_eq!(DNSPattern::new("fo*.example.com"), None);
assert_eq!(DNSPattern::new("foo.*.example.com"), None);
assert_eq!(DNSPattern::new("*.foo.*.example.com"), None);

assert_eq!(
DNSPattern::new("example.com").unwrap(),
DNSPattern::Exact(DNSName::new("example.com").unwrap())
);
assert_eq!(
DNSPattern::new("*.example.com").unwrap(),
DNSPattern::Wildcard(DNSName::new("example.com").unwrap())
);
}

#[test]
fn test_dnspattern_matches() {
let exactly_localhost = DNSPattern::new("localhost").unwrap();
let any_localhost = DNSPattern::new("*.localhost").unwrap();
let exactly_example_com = DNSPattern::new("example.com").unwrap();
let any_example_com = DNSPattern::new("*.example.com").unwrap();

// Exact patterns match only the exact name.
assert!(exactly_localhost.matches(&DNSName::new("localhost").unwrap()));
assert!(exactly_example_com.matches(&DNSName::new("example.com").unwrap()));
assert!(!exactly_example_com.matches(&DNSName::new("foo.example.com").unwrap()));

// Wildcard patterns match any subdomain, but not the parent or nested subdomains.
assert!(any_example_com.matches(&DNSName::new("foo.example.com").unwrap()));
assert!(any_example_com.matches(&DNSName::new("bar.example.com").unwrap()));
assert!(!any_example_com.matches(&DNSName::new("example.com").unwrap()));
assert!(!any_example_com.matches(&DNSName::new("foo.bar.example.com").unwrap()));
assert!(!any_example_com.matches(&DNSName::new("foo.bar.baz.example.com").unwrap()));
assert!(!any_localhost.matches(&DNSName::new("localhost").unwrap()));
}
}
4 changes: 3 additions & 1 deletion src/rust/cryptography-x509/src/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

use crate::common;

pub type NameReadable<'a> = asn1::SequenceOf<'a, asn1::SetOf<'a, common::AttributeTypeValue<'a>>>;
woodruffw marked this conversation as resolved.
Show resolved Hide resolved

pub type Name<'a> = common::Asn1ReadableOrWritable<
'a,
asn1::SequenceOf<'a, asn1::SetOf<'a, common::AttributeTypeValue<'a>>>,
NameReadable<'a>,
asn1::SequenceOfWriter<
'a,
asn1::SetOfWriter<'a, common::AttributeTypeValue<'a>, Vec<common::AttributeTypeValue<'a>>>,
Expand Down