#[cfg(feature = "alloc")]
use alloc::format;
use core::fmt::Write;
#[cfg(feature = "alloc")]
use pki_types::ServerName;
use pki_types::{DnsName, InvalidDnsNameError};
use super::{GeneralName, NameIterator};
use crate::cert::Cert;
use crate::error::{Error, InvalidNameContext};
use crate::subject_name::Subtrees;
pub(crate) fn verify_dns_names(reference: &DnsName<'_>, cert: &Cert<'_>) -> Result<(), Error> {
let dns_name = untrusted::Input::from(reference.as_ref().as_bytes());
let result = NameIterator::new(cert.subject_alt_name).find_map(|result| {
let name = match result {
Ok(name) => name,
Err(err) => return Some(Err(err)),
};
let presented_id = match name {
GeneralName::DnsName(presented) => presented,
_ => return None,
};
match presented_id_matches_reference_id(presented_id, IdRole::Reference, dns_name) {
Ok(true) => Some(Ok(())),
Ok(false) | Err(Error::MalformedDnsIdentifier) => None,
Err(e) => Some(Err(e)),
}
});
match result {
Some(result) => return result,
#[cfg(feature = "alloc")]
None => {}
#[cfg(not(feature = "alloc"))]
None => Err(Error::CertNotValidForName(InvalidNameContext {})),
}
#[cfg(feature = "alloc")]
{
Err(Error::CertNotValidForName(InvalidNameContext {
expected: ServerName::DnsName(reference.to_owned()),
presented: NameIterator::new(cert.subject_alt_name)
.filter_map(|result| Some(format!("{:?}", result.ok()?)))
.collect(),
}))
}
}
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub(crate) struct WildcardDnsNameRef<'a>(&'a [u8]);
impl<'a> WildcardDnsNameRef<'a> {
pub(crate) fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
if !is_valid_dns_id(
untrusted::Input::from(dns_name),
IdRole::Reference,
Wildcards::Allow,
) {
return Err(InvalidDnsNameError);
}
Ok(Self(dns_name))
}
pub(crate) fn as_str(&self) -> &'a str {
core::str::from_utf8(self.0).unwrap()
}
}
impl core::fmt::Debug for WildcardDnsNameRef<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
f.write_str("WildcardDnsNameRef(\"")?;
for &ch in self.0 {
f.write_char(char::from(ch).to_ascii_lowercase())?;
}
f.write_str("\")")
}
}
pub(super) fn presented_id_matches_reference_id(
presented_dns_id: untrusted::Input<'_>,
reference_dns_id_role: IdRole,
reference_dns_id: untrusted::Input<'_>,
) -> Result<bool, Error> {
if !is_valid_dns_id(presented_dns_id, IdRole::Presented, Wildcards::Allow) {
return Err(Error::MalformedDnsIdentifier);
}
if !is_valid_dns_id(reference_dns_id, reference_dns_id_role, Wildcards::Deny) {
return Err(match reference_dns_id_role {
IdRole::NameConstraint(_) => Error::MalformedNameConstraint,
_ => Error::MalformedDnsIdentifier,
});
}
let mut presented = untrusted::Reader::new(presented_dns_id);
let mut reference = untrusted::Reader::new(reference_dns_id);
match reference_dns_id_role {
IdRole::Reference => (),
IdRole::NameConstraint(_) if presented_dns_id.len() > reference_dns_id.len() => {
if reference_dns_id.is_empty() {
return Ok(true);
}
if reference.peek(b'.') {
if presented
.skip(presented_dns_id.len() - reference_dns_id.len())
.is_err()
{
unreachable!();
}
} else {
if presented
.skip(presented_dns_id.len() - reference_dns_id.len() - 1)
.is_err()
{
unreachable!();
}
if presented.read_byte() != Ok(b'.') {
return Ok(false);
}
}
}
IdRole::NameConstraint(_) => (),
IdRole::Presented => unreachable!(),
}
if presented.peek(b'*') && reference_dns_id_role != IdRole::NameConstraint(Subtrees::Permitted)
{
if presented.skip(1).is_err() {
unreachable!();
}
loop {
if reference.read_byte().is_err() {
return Ok(false);
}
if reference.peek(b'.') {
break;
}
}
}
loop {
let presented_byte = match (presented.read_byte(), reference.read_byte()) {
(Ok(p), Ok(r)) if ascii_lower(p) == ascii_lower(r) => p,
_ => {
return Ok(false);
}
};
if presented.at_end() {
if presented_byte == b'.' {
return Err(Error::MalformedDnsIdentifier);
}
break;
}
}
if !reference.at_end() {
if !matches!(reference_dns_id_role, IdRole::NameConstraint(_)) {
match reference.read_byte() {
Ok(b'.') => (),
_ => {
return Ok(false);
}
};
}
if !reference.at_end() {
return Ok(false);
}
}
assert!(presented.at_end());
assert!(reference.at_end());
Ok(true)
}
#[inline]
fn ascii_lower(b: u8) -> u8 {
match b {
b'A'..=b'Z' => b + b'a' - b'A',
_ => b,
}
}
#[derive(Clone, Copy, PartialEq)]
enum Wildcards {
Deny,
Allow,
}
#[derive(Clone, Copy, PartialEq)]
pub(super) enum IdRole {
Reference,
Presented,
NameConstraint(Subtrees),
}
fn is_valid_dns_id(
hostname: untrusted::Input<'_>,
id_role: IdRole,
allow_wildcards: Wildcards,
) -> bool {
if hostname.len() > 253 {
return false;
}
let mut input = untrusted::Reader::new(hostname);
if matches!(id_role, IdRole::NameConstraint(_)) && input.at_end() {
return true;
}
let mut dot_count = 0;
let mut label_length = 0;
let mut label_is_all_numeric = false;
let mut label_ends_with_hyphen = false;
let is_wildcard = allow_wildcards == Wildcards::Allow && input.peek(b'*');
let mut is_first_byte = !is_wildcard;
if is_wildcard {
if input.read_byte() != Ok(b'*') || input.read_byte() != Ok(b'.') {
return false;
}
dot_count += 1;
}
loop {
const MAX_LABEL_LENGTH: usize = 63;
match input.read_byte() {
Ok(b'-') => {
if label_length == 0 {
return false; }
label_is_all_numeric = false;
label_ends_with_hyphen = true;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
}
Ok(b'0'..=b'9') => {
if label_length == 0 {
label_is_all_numeric = true;
}
label_ends_with_hyphen = false;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
}
Ok(b'a'..=b'z') | Ok(b'A'..=b'Z') | Ok(b'_') => {
label_is_all_numeric = false;
label_ends_with_hyphen = false;
label_length += 1;
if label_length > MAX_LABEL_LENGTH {
return false;
}
}
Ok(b'.') => {
dot_count += 1;
let name_constrained = matches!(id_role, IdRole::NameConstraint(_));
if label_length == 0 && (!name_constrained || !is_first_byte) {
return false;
}
if label_ends_with_hyphen {
return false; }
label_length = 0;
}
_ => {
return false;
}
}
is_first_byte = false;
if input.at_end() {
break;
}
}
if label_length == 0 && id_role != IdRole::Reference {
return false;
}
if label_ends_with_hyphen {
return false; }
if label_is_all_numeric {
return false; }
if is_wildcard {
let label_count = if label_length == 0 {
dot_count
} else {
dot_count + 1
};
if label_count < 3 {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::type_complexity)]
const PRESENTED_MATCHES_REFERENCE: &[(&[u8], &[u8], Result<bool, Error>)] = &[
(b"", b"a", Err(Error::MalformedDnsIdentifier)),
(b"a", b"a", Ok(true)),
(b"b", b"a", Ok(false)),
(b"*.b.a", b"c.b.a", Ok(true)),
(b"*.b.a", b"b.a", Ok(false)),
(b"*.b.a", b"b.a.", Ok(false)),
(b"d.c.b.a", b"d.c.b.a", Ok(true)),
(b"d.*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"d.c*.b.a", b"d.c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"d.c*.b.a", b"d.cc.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"abcdefghijklmnopqrstuvwxyz",
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
Ok(true),
),
(
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
b"abcdefghijklmnopqrstuvwxyz",
Ok(true),
),
(b"aBc", b"Abc", Ok(true)),
(b"a1", b"a1", Ok(true)),
(b"example", b"example", Ok(true)),
(b"example.", b"example.", Err(Error::MalformedDnsIdentifier)),
(b"example", b"example.", Ok(true)),
(b"example.", b"example", Err(Error::MalformedDnsIdentifier)),
(b"example.com", b"example.com", Ok(true)),
(
b"example.com.",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"example.com", b"example.com.", Ok(true)),
(
b"example.com.",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com..",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com..",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"example.com...",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"x*.b.a", b"xa.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xna.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xn-a.b.a", Err(Error::MalformedDnsIdentifier)),
(b"x*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"xn-*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(b"xn*.b.a", b"xn--a.b.a", Err(Error::MalformedDnsIdentifier)),
(
b"xn-*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn---*.b.a",
b"xn--a.b.a",
Err(Error::MalformedDnsIdentifier),
),
(b"c*.b.a", b"c.b.a", Err(Error::MalformedDnsIdentifier)),
(b"foo.com", b"foo.com", Ok(true)),
(b"f", b"f", Ok(true)),
(b"i", b"h", Ok(false)),
(b"*.foo.com", b"bar.foo.com", Ok(true)),
(b"*.test.fr", b"www.test.fr", Ok(true)),
(b"*.test.FR", b"wwW.tESt.fr", Ok(true)),
(b".uk", b"f.uk", Err(Error::MalformedDnsIdentifier)),
(
b"?.bar.foo.com",
b"w.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"(www|ftp).foo.com",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
), (
b"www.foo.com\0",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"www.foo.com\0*.foo.com",
b"www.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"ww.house.example", b"www.house.example", Ok(false)),
(b"www.test.org", b"test.org", Ok(false)),
(b"*.test.org", b"test.org", Ok(false)),
(b"*.org", b"test.org", Err(Error::MalformedDnsIdentifier)),
(
b"w*.bar.foo.com",
b"w.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"ww*ww.bar.foo.com",
b"www.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"ww*ww.bar.foo.com",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"w*w.bar.foo.com",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"w*w.bar.foo.c0m",
b"wwww.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"wa*.bar.foo.com",
b"WALLY.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*Ly.bar.foo.com",
b"wally.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"*.test.de", b"www.test.co.jp", Ok(false)),
(
b"*.jp",
b"www.test.co.jp",
Err(Error::MalformedDnsIdentifier),
),
(b"www.test.co.uk", b"www.test.co.jp", Ok(false)),
(
b"www.*.co.jp",
b"www.test.co.jp",
Err(Error::MalformedDnsIdentifier),
),
(b"www.bar.foo.com", b"www.bar.foo.com", Ok(true)),
(b"*.foo.com", b"www.bar.foo.com", Ok(false)),
(
b"*.*.foo.com",
b"www.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"www.bath.org", b"www.bath.org", Ok(true)),
(
b"xn--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.xn--poema-9qae5a.com.br",
b"www.xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.xn--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Ok(false),
),
(
b"xn--poema-*.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(
b"xn--*-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(
b"*--poema-9qae5a.com.br",
b"xn--poema-9qae5a.com.br",
Err(Error::MalformedDnsIdentifier),
),
(b"*.example.com", b"foo.example.com", Ok(true)),
(b"*.example.com", b"bar.foo.example.com", Ok(false)),
(b"*.example.com", b"example.com", Ok(false)),
(
b"baz*.example.net",
b"baz1.example.net",
Err(Error::MalformedDnsIdentifier),
),
(
b"*baz.example.net",
b"foobaz.example.net",
Err(Error::MalformedDnsIdentifier),
),
(
b"b*z.example.net",
b"buzz.example.net",
Err(Error::MalformedDnsIdentifier),
),
(b"*.test.example", b"www.test.example", Ok(true)),
(b"*.example.co.uk", b"test.example.co.uk", Ok(true)),
(
b"*.example",
b"test.example",
Err(Error::MalformedDnsIdentifier),
),
(b"*.co.uk", b"example.co.uk", Ok(true)),
(b"*.com", b"foo.com", Err(Error::MalformedDnsIdentifier)),
(b"*.us", b"foo.us", Err(Error::MalformedDnsIdentifier)),
(b"*", b"foo", Err(Error::MalformedDnsIdentifier)),
(
b"*.xn--poema-9qae5a.com.br",
b"www.xn--poema-9qae5a.com.br",
Ok(true),
),
(
b"*.example.xn--mgbaam7a8h",
b"test.example.xn--mgbaam7a8h",
Ok(true),
),
(b"*.com.br", b"xn--poema-9qae5a.com.br", Ok(true)),
(
b"*.xn--mgbaam7a8h",
b"example.xn--mgbaam7a8h",
Err(Error::MalformedDnsIdentifier),
),
(b"*.appspot.com", b"www.appspot.com", Ok(true)),
(b"*.s3.amazonaws.com", b"foo.s3.amazonaws.com", Ok(true)),
(
b"*.*.com",
b"foo.example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.bar.*.com",
b"foo.bar.example.com",
Err(Error::MalformedDnsIdentifier),
),
(b"foo.com.", b"foo.com", Err(Error::MalformedDnsIdentifier)),
(b"foo.com", b"foo.com.", Ok(true)),
(b"foo.com.", b"foo.com.", Err(Error::MalformedDnsIdentifier)),
(b"f.", b"f", Err(Error::MalformedDnsIdentifier)),
(b"f", b"f.", Ok(true)),
(b"f.", b"f.", Err(Error::MalformedDnsIdentifier)),
(
b"*.bar.foo.com.",
b"www-3.bar.foo.com",
Err(Error::MalformedDnsIdentifier),
),
(b"*.bar.foo.com", b"www-3.bar.foo.com.", Ok(true)),
(
b"*.bar.foo.com.",
b"www-3.bar.foo.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com.",
b"example.com",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.com.",
b"example.com.",
Err(Error::MalformedDnsIdentifier),
),
(b"*.", b"foo.", Err(Error::MalformedDnsIdentifier)),
(b"*.", b"foo", Err(Error::MalformedDnsIdentifier)),
(
b"*.co.uk.",
b"foo.co.uk",
Err(Error::MalformedDnsIdentifier),
),
(
b"*.co.uk.",
b"foo.co.uk.",
Err(Error::MalformedDnsIdentifier),
),
];
#[test]
fn presented_matches_reference_test() {
for (presented, reference, expected_result) in PRESENTED_MATCHES_REFERENCE {
let actual_result = presented_id_matches_reference_id(
untrusted::Input::from(presented),
IdRole::Reference,
untrusted::Input::from(reference),
);
assert_eq!(
&actual_result, expected_result,
"presented_id_matches_reference_id(\"{presented:?}\", \"{reference:?}\")"
);
}
}
#[allow(clippy::type_complexity)]
const PRESENTED_MATCHES_CONSTRAINT: &[(&[u8], &[u8], Result<bool, Error>)] = &[
(b".", b"", Err(Error::MalformedDnsIdentifier)),
(b"www.example.com.", b"", Err(Error::MalformedDnsIdentifier)),
(
b"www.example.com.",
b"www.example.com.",
Err(Error::MalformedDnsIdentifier),
),
(
b"www.example.com",
b".",
Err(Error::MalformedNameConstraint),
),
(
b"www.example.com",
b"www.example.com.",
Err(Error::MalformedNameConstraint),
),
(
b"www.example.com",
b"*.example.com",
Err(Error::MalformedNameConstraint),
),
(b"", b"", Err(Error::MalformedDnsIdentifier)),
(b"example.com", b"", Ok(true)),
(b"*.example.com", b"", Ok(true)),
(b"www.example.com", b".example.com", Ok(true)),
(b"www.example.com", b".EXAMPLE.COM", Ok(true)),
(b"www.example.com", b".axample.com", Ok(false)),
(b"www.example.com", b".xample.com", Ok(false)),
(b"www.example.com", b".exampl.com", Ok(false)),
(b"badexample.com", b".example.com", Ok(false)),
(b"www.example.com", b"example.com", Ok(true)),
(b"www.example.com", b"EXAMPLE.COM", Ok(true)),
(b"www.example.com", b"axample.com", Ok(false)),
(b"www.example.com", b"xample.com", Ok(false)),
(b"www.example.com", b"exampl.com", Ok(false)),
(b"badexample.com", b"example.com", Ok(false)),
(b"*.example.com", b".example.com", Ok(true)),
(b"*.example.com", b"example.com", Ok(true)),
(b"*.example.com", b"www.example.com", Ok(false)),
(b"*.example.com", b"www.EXAMPLE.COM", Ok(false)),
(b"*.example.com", b"www.axample.com", Ok(false)),
(b"*.example.com", b".xample.com", Ok(false)),
(b"*.example.com", b"xample.com", Ok(false)),
(b"*.example.com", b".exampl.com", Ok(false)),
(b"*.example.com", b"exampl.com", Ok(false)),
(b"www.example.com", b"www.example.com", Ok(true)),
];
#[test]
fn presented_matches_constraint_test() {
for (presented, constraint, expected_result) in PRESENTED_MATCHES_CONSTRAINT {
let actual_result = presented_id_matches_reference_id(
untrusted::Input::from(presented),
IdRole::NameConstraint(Subtrees::Permitted),
untrusted::Input::from(constraint),
);
assert_eq!(
&actual_result, expected_result,
"presented_id_matches_constraint(\"{presented:?}\", \"{constraint:?}\")",
);
}
}
#[test]
fn wildcard_san_not_contained_in_constraint() {
for (presented, constraint, expected_result) in WILDCARD_CONSTRAINT_CONTAINMENT {
let actual_result = presented_id_matches_reference_id(
untrusted::Input::from(presented),
IdRole::NameConstraint(Subtrees::Permitted),
untrusted::Input::from(constraint),
);
assert_eq!(
&actual_result, expected_result,
"presented_id_matches_constraint(\"{presented:?}\", \"{constraint:?}\")",
);
}
}
#[expect(clippy::type_complexity)]
const WILDCARD_CONSTRAINT_CONTAINMENT: &[(&[u8], &[u8], Result<bool, Error>)] = &[
(b"*.example.com", b"www.example.com", Ok(false)),
(b"*.www.example.com", b"www.example.com", Ok(true)),
(b"*.example.com", b"a.b.example.com", Ok(false)),
(b"*.b.example.com", b"a.b.example.com", Ok(false)),
];
#[test]
fn wildcard_san_could_match_excluded_subtree() {
for (presented, constraint, expected_result) in WILDCARD_EXCLUDED_INTERSECTION {
let actual_result = presented_id_matches_reference_id(
untrusted::Input::from(presented),
IdRole::NameConstraint(Subtrees::Excluded),
untrusted::Input::from(constraint),
);
assert_eq!(
&actual_result, expected_result,
"presented_id_matches_constraint(\"{presented:?}\", \"{constraint:?}\")",
);
}
}
#[expect(clippy::type_complexity)]
const WILDCARD_EXCLUDED_INTERSECTION: &[(&[u8], &[u8], Result<bool, Error>)] = &[
(b"*.example.com", b"example.com", Ok(true)),
(b"*.example.com", b".example.com", Ok(true)),
(b"*.example.com", b"www.example.com", Ok(true)),
(b"*.example.com", b"www.EXAMPLE.COM", Ok(true)),
(b"*.example.com", b"a.b.example.com", Ok(false)),
(b"*.example.com", b"www.other.com", Ok(false)),
];
}