Skip to content

Commit 0071b3e

Browse files
woodruffwalex
andauthored
Migrate more types (#9254)
* x509: migrate more types Signed-off-by: William Woodruff <william@trailofbits.com> * common: clean up DNSName/Pattern types Cleans up the types a bit; patterns are now their own type, simplifying our matching logic. Signed-off-by: William Woodruff <william@trailofbits.com> * common: clippy Signed-off-by: William Woodruff <william@trailofbits.com> * common: round out coverage Signed-off-by: William Woodruff <william@trailofbits.com> * common: remove owned type, case cmp Signed-off-by: William Woodruff <william@trailofbits.com> * common: update docs Signed-off-by: William Woodruff <william@trailofbits.com> * name: remove type breakout Signed-off-by: William Woodruff <william@trailofbits.com> * common: doctests Signed-off-by: William Woodruff <william@trailofbits.com> * common: coverage Signed-off-by: William Woodruff <william@trailofbits.com> * Update src/rust/cryptography-x509/src/common.rs Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com> * Update src/rust/cryptography-x509/src/common.rs * crate skeleton, move to validation Signed-off-by: William Woodruff <william@trailofbits.com> * types: remove duped whitespace check Signed-off-by: William Woodruff <william@trailofbits.com> --------- Signed-off-by: William Woodruff <william@trailofbits.com> Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
1 parent a485161 commit 0071b3e

File tree

5 files changed

+285
-1
lines changed

5 files changed

+285
-1
lines changed

src/rust/Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ crate-type = ["cdylib"]
3535
overflow-checks = true
3636

3737
[workspace]
38-
members = ["cryptography-cffi", "cryptography-openssl", "cryptography-x509"]
38+
members = [
39+
"cryptography-cffi",
40+
"cryptography-openssl",
41+
"cryptography-x509",
42+
"cryptography-x509-validation",
43+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "cryptography-x509-validation"
3+
version = "0.1.0"
4+
authors = ["The cryptography developers <cryptography-dev@python.org>"]
5+
edition = "2021"
6+
publish = false
7+
# This specifies the MSRV
8+
rust-version = "1.56.0"
9+
10+
[dependencies]
11+
asn1 = { version = "0.15.0", default-features = false }
12+
cryptography-x509 = { path = "../cryptography-x509" }
13+
14+
[dev-dependencies]
15+
pem = "1.1"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is dual licensed under the terms of the Apache License, Version
2+
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
// for complete details.
4+
5+
#![forbid(unsafe_code)]
6+
7+
pub mod types;
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// This file is dual licensed under the terms of the Apache License, Version
2+
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
// for complete details.
4+
5+
/// A `DNSName` is an `asn1::IA5String` with additional invariant preservations
6+
/// per [RFC 5280 4.2.1.6], which in turn uses the preferred name syntax defined
7+
/// in [RFC 1034 3.5] and amended in [RFC 1123 2.1].
8+
///
9+
/// Non-ASCII domain names (i.e., internationalized names) must be pre-encoded;
10+
/// comparisons are case-insensitive.
11+
///
12+
/// [RFC 5280 4.2.1.6]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
13+
/// [RFC 1034 3.5]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.5
14+
/// [RFC 1123 2.1]: https://datatracker.ietf.org/doc/html/rfc1123#section-2.1
15+
///
16+
/// ```rust
17+
/// # use cryptography_x509_validation::types::DNSName;
18+
/// assert_eq!(DNSName::new("foo.com").unwrap(), DNSName::new("FOO.com").unwrap());
19+
/// ```
20+
#[derive(Debug)]
21+
pub struct DNSName<'a>(asn1::IA5String<'a>);
22+
23+
impl<'a> DNSName<'a> {
24+
pub fn new(value: &'a str) -> Option<Self> {
25+
// Domains cannot be empty and must (practically)
26+
// be less than 253 characters (255 in RFC 1034's octet encoding).
27+
if value.is_empty() || value.len() > 253 {
28+
None
29+
} else {
30+
for label in value.split('.') {
31+
// Individual labels cannot be empty; cannot exceed 63 characters;
32+
// cannot start or end with `-`.
33+
// NOTE: RFC 1034's grammar prohibits consecutive hyphens, but these
34+
// are used as part of the IDN prefix (e.g. `xn--`)'; we allow them here.
35+
if label.is_empty()
36+
|| label.len() > 63
37+
|| label.starts_with('-')
38+
|| label.ends_with('-')
39+
{
40+
return None;
41+
}
42+
43+
// Labels must only contain `a-zA-Z0-9-`.
44+
if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
45+
return None;
46+
}
47+
}
48+
asn1::IA5String::new(value).map(Self)
49+
}
50+
}
51+
52+
pub fn as_str(&self) -> &'a str {
53+
self.0.as_str()
54+
}
55+
56+
/// Return this `DNSName`'s parent domain, if it has one.
57+
///
58+
/// ```rust
59+
/// # use cryptography_x509_validation::types::DNSName;
60+
/// let domain = DNSName::new("foo.example.com").unwrap();
61+
/// assert_eq!(domain.parent().unwrap().as_str(), "example.com");
62+
/// ```
63+
pub fn parent(&self) -> Option<Self> {
64+
match self.as_str().split_once('.') {
65+
Some((_, parent)) => Self::new(parent),
66+
None => None,
67+
}
68+
}
69+
}
70+
71+
impl PartialEq for DNSName<'_> {
72+
fn eq(&self, other: &Self) -> bool {
73+
// DNS names are always case-insensitive.
74+
self.as_str().eq_ignore_ascii_case(other.as_str())
75+
}
76+
}
77+
78+
/// A `DNSPattern` represents a subset of the domain name wildcard matching
79+
/// behavior defined in [RFC 6125 6.4.3]. In particular, all DNS patterns
80+
/// must either be exact matches (post-normalization) *or* a single wildcard
81+
/// matching a full label in the left-most label position. Partial label matching
82+
/// (e.g. `f*o.example.com`) is not supported, nor is non-left-most matching
83+
/// (e.g. `foo.*.example.com`).
84+
///
85+
/// [RFC 6125 6.4.3]: https://datatracker.ietf.org/doc/html/rfc6125#section-6.4.3
86+
#[derive(Debug, PartialEq)]
87+
pub enum DNSPattern<'a> {
88+
Exact(DNSName<'a>),
89+
Wildcard(DNSName<'a>),
90+
}
91+
92+
impl<'a> DNSPattern<'a> {
93+
pub fn new(pat: &'a str) -> Option<Self> {
94+
if let Some(pat) = pat.strip_prefix("*.") {
95+
DNSName::new(pat).map(Self::Wildcard)
96+
} else {
97+
DNSName::new(pat).map(Self::Exact)
98+
}
99+
}
100+
101+
pub fn matches(&self, name: &DNSName) -> bool {
102+
match self {
103+
Self::Exact(pat) => pat == name,
104+
Self::Wildcard(pat) => match name.parent() {
105+
Some(ref parent) => pat == parent,
106+
// No parent means we have a single label; wildcards cannot match single labels.
107+
None => false,
108+
},
109+
}
110+
}
111+
}
112+
113+
#[cfg(test)]
114+
mod tests {
115+
use crate::types::{DNSName, DNSPattern};
116+
117+
#[test]
118+
fn test_dnsname_debug_trait() {
119+
// Just to get coverage on the `Debug` derive.
120+
assert_eq!(
121+
"DNSName(IA5String(\"example.com\"))",
122+
format!("{:?}", DNSName::new("example.com").unwrap())
123+
);
124+
}
125+
126+
#[test]
127+
fn test_dnsname_new() {
128+
assert_eq!(DNSName::new(""), None);
129+
assert_eq!(DNSName::new("."), None);
130+
assert_eq!(DNSName::new(".."), None);
131+
assert_eq!(DNSName::new(".a."), None);
132+
assert_eq!(DNSName::new("a.a."), None);
133+
assert_eq!(DNSName::new(".a"), None);
134+
assert_eq!(DNSName::new("a."), None);
135+
assert_eq!(DNSName::new("a.."), None);
136+
assert_eq!(DNSName::new(" "), None);
137+
assert_eq!(DNSName::new("\t"), None);
138+
assert_eq!(DNSName::new(" whitespace "), None);
139+
assert_eq!(DNSName::new("white. space"), None);
140+
assert_eq!(DNSName::new("!badlabel!"), None);
141+
assert_eq!(DNSName::new("bad!label"), None);
142+
assert_eq!(DNSName::new("goodlabel.!badlabel!"), None);
143+
assert_eq!(DNSName::new("-foo.bar.example.com"), None);
144+
assert_eq!(DNSName::new("foo-.bar.example.com"), None);
145+
assert_eq!(DNSName::new("foo.-bar.example.com"), None);
146+
assert_eq!(DNSName::new("foo.bar-.example.com"), None);
147+
assert_eq!(DNSName::new(&"a".repeat(64)), None);
148+
assert_eq!(DNSName::new("⚠️"), None);
149+
150+
let long_valid_label = "a".repeat(63);
151+
let long_name = std::iter::repeat(long_valid_label)
152+
.take(5)
153+
.collect::<Vec<_>>()
154+
.join(".");
155+
assert_eq!(DNSName::new(&long_name), None);
156+
157+
assert_eq!(
158+
DNSName::new(&"a".repeat(63)).unwrap().as_str(),
159+
"a".repeat(63)
160+
);
161+
assert_eq!(DNSName::new("example.com").unwrap().as_str(), "example.com");
162+
assert_eq!(
163+
DNSName::new("123.example.com").unwrap().as_str(),
164+
"123.example.com"
165+
);
166+
assert_eq!(DNSName::new("EXAMPLE.com").unwrap().as_str(), "EXAMPLE.com");
167+
assert_eq!(DNSName::new("EXAMPLE.COM").unwrap().as_str(), "EXAMPLE.COM");
168+
assert_eq!(
169+
DNSName::new("xn--bcher-kva.example").unwrap().as_str(),
170+
"xn--bcher-kva.example"
171+
);
172+
}
173+
174+
#[test]
175+
fn test_dnsname_equality() {
176+
assert_ne!(
177+
DNSName::new("foo.example.com").unwrap(),
178+
DNSName::new("example.com").unwrap()
179+
);
180+
181+
// DNS name comparisons are case insensitive.
182+
assert_eq!(
183+
DNSName::new("EXAMPLE.COM").unwrap(),
184+
DNSName::new("example.com").unwrap()
185+
);
186+
assert_eq!(
187+
DNSName::new("ExAmPLe.CoM").unwrap(),
188+
DNSName::new("eXaMplE.cOm").unwrap()
189+
);
190+
}
191+
192+
#[test]
193+
fn test_dnsname_parent() {
194+
assert_eq!(DNSName::new("localhost").unwrap().parent(), None);
195+
assert_eq!(
196+
DNSName::new("example.com").unwrap().parent().unwrap(),
197+
DNSName::new("com").unwrap()
198+
);
199+
assert_eq!(
200+
DNSName::new("foo.example.com").unwrap().parent().unwrap(),
201+
DNSName::new("example.com").unwrap()
202+
);
203+
}
204+
205+
#[test]
206+
fn test_dnspattern_new() {
207+
assert_eq!(DNSPattern::new("*"), None);
208+
assert_eq!(DNSPattern::new("*."), None);
209+
assert_eq!(DNSPattern::new("f*o.example.com"), None);
210+
assert_eq!(DNSPattern::new("*oo.example.com"), None);
211+
assert_eq!(DNSPattern::new("fo*.example.com"), None);
212+
assert_eq!(DNSPattern::new("foo.*.example.com"), None);
213+
assert_eq!(DNSPattern::new("*.foo.*.example.com"), None);
214+
215+
assert_eq!(
216+
DNSPattern::new("example.com").unwrap(),
217+
DNSPattern::Exact(DNSName::new("example.com").unwrap())
218+
);
219+
assert_eq!(
220+
DNSPattern::new("*.example.com").unwrap(),
221+
DNSPattern::Wildcard(DNSName::new("example.com").unwrap())
222+
);
223+
}
224+
225+
#[test]
226+
fn test_dnspattern_matches() {
227+
let exactly_localhost = DNSPattern::new("localhost").unwrap();
228+
let any_localhost = DNSPattern::new("*.localhost").unwrap();
229+
let exactly_example_com = DNSPattern::new("example.com").unwrap();
230+
let any_example_com = DNSPattern::new("*.example.com").unwrap();
231+
232+
// Exact patterns match only the exact name.
233+
assert!(exactly_localhost.matches(&DNSName::new("localhost").unwrap()));
234+
assert!(exactly_localhost.matches(&DNSName::new("LOCALHOST").unwrap()));
235+
assert!(exactly_example_com.matches(&DNSName::new("example.com").unwrap()));
236+
assert!(exactly_example_com.matches(&DNSName::new("EXAMPLE.com").unwrap()));
237+
assert!(!exactly_example_com.matches(&DNSName::new("foo.example.com").unwrap()));
238+
239+
// Wildcard patterns match any subdomain, but not the parent or nested subdomains.
240+
assert!(any_example_com.matches(&DNSName::new("foo.example.com").unwrap()));
241+
assert!(any_example_com.matches(&DNSName::new("bar.example.com").unwrap()));
242+
assert!(any_example_com.matches(&DNSName::new("BAZ.example.com").unwrap()));
243+
assert!(!any_example_com.matches(&DNSName::new("example.com").unwrap()));
244+
assert!(!any_example_com.matches(&DNSName::new("foo.bar.example.com").unwrap()));
245+
assert!(!any_example_com.matches(&DNSName::new("foo.bar.baz.example.com").unwrap()));
246+
assert!(!any_localhost.matches(&DNSName::new("localhost").unwrap()));
247+
}
248+
}

0 commit comments

Comments
 (0)