use x509_cert::Certificate;
use crate::der_utils::{parse_tlv, parse_tlv_with_rest};
pub const NAME_CONSTRAINTS_OID: &str = "2.5.29.30";
const SAN_OID: &str = "2.5.29.17";
const GN_RFC822: u8 = 0x81; const GN_DNS: u8 = 0x82; const GN_DIRECTORY: u8 = 0xA4; const GN_IP: u8 = 0x87;
#[derive(Debug)]
pub enum NameConstraintError {
Violation(String),
Unsupported(String),
Parse(String),
}
impl std::fmt::Display for NameConstraintError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NameConstraintError::Violation(m) => write!(f, "name constraint violation: {m}"),
NameConstraintError::Unsupported(m) => {
write!(f, "unsupported name constraint (fail closed): {m}")
}
NameConstraintError::Parse(m) => write!(f, "malformed name constraints: {m}"),
}
}
}
#[derive(Debug, Clone)]
enum GeneralNameBase {
Dns(String),
Rfc822(String),
Ip(Vec<u8>, Vec<u8>),
Directory(Vec<u8>),
}
#[derive(Debug, Clone)]
enum AssertedName {
Dns(String),
Rfc822(String),
Ip(Vec<u8>),
Directory(Vec<u8>),
}
#[derive(Debug, Default, Clone)]
pub struct NameConstraintState {
permitted: Vec<GeneralNameBase>,
excluded: Vec<GeneralNameBase>,
permitted_dns: bool,
permitted_rfc822: bool,
permitted_ip: bool,
permitted_directory: bool,
}
impl NameConstraintState {
pub fn add_from_cert(&mut self, cert: &Certificate) -> Result<(), NameConstraintError> {
let nc_oid = const_oid::ObjectIdentifier::new_unwrap(NAME_CONSTRAINTS_OID);
let Some(extensions) = &cert.tbs_certificate.extensions else {
return Ok(());
};
let Some(ext) = extensions.iter().find(|e| e.extn_id == nc_oid) else {
return Ok(());
};
self.add_from_extension_value(ext.extn_value.as_bytes())
}
fn add_from_extension_value(&mut self, value: &[u8]) -> Result<(), NameConstraintError> {
let (tag, body) =
parse_tlv(value).map_err(|e| NameConstraintError::Parse(format!("outer: {e}")))?;
if tag != 0x30 {
return Err(NameConstraintError::Parse(format!(
"expected SEQUENCE (0x30), got 0x{tag:02x}"
)));
}
let mut pos = &body[..];
while !pos.is_empty() {
let (t, sub_body, rest) = parse_tlv_with_rest(pos)
.map_err(|e| NameConstraintError::Parse(format!("subtrees: {e}")))?;
match t {
0xA0 => {
let bases = parse_general_subtrees(sub_body)?;
for b in bases {
match &b {
GeneralNameBase::Dns(_) => self.permitted_dns = true,
GeneralNameBase::Rfc822(_) => self.permitted_rfc822 = true,
GeneralNameBase::Ip(..) => self.permitted_ip = true,
GeneralNameBase::Directory(_) => self.permitted_directory = true,
}
self.permitted.push(b);
}
}
0xA1 => {
let bases = parse_general_subtrees(sub_body)?;
self.excluded.extend(bases);
}
other => {
return Err(NameConstraintError::Parse(format!(
"unexpected NameConstraints member tag 0x{other:02x}"
)));
}
}
pos = rest;
}
Ok(())
}
pub fn is_empty(&self) -> bool {
self.permitted.is_empty() && self.excluded.is_empty()
}
pub fn check_cert(&self, cert: &Certificate) -> Result<(), NameConstraintError> {
if self.is_empty() {
return Ok(());
}
let names = collect_asserted_names(cert)?;
for name in &names {
self.check_name(name)?;
}
Ok(())
}
fn check_name(&self, name: &AssertedName) -> Result<(), NameConstraintError> {
for ex in &self.excluded {
if base_matches(ex, name)? {
return Err(NameConstraintError::Violation(format!(
"{name:?} falls within an excluded subtree"
)));
}
}
let (has_permitted_of_type, type_label) = match name {
AssertedName::Dns(_) => (self.permitted_dns, "dNSName"),
AssertedName::Rfc822(_) => (self.permitted_rfc822, "rfc822Name"),
AssertedName::Ip(_) => (self.permitted_ip, "iPAddress"),
AssertedName::Directory(_) => (self.permitted_directory, "directoryName"),
};
if has_permitted_of_type {
let mut matched = false;
for p in &self.permitted {
if base_matches(p, name)? {
matched = true;
break;
}
}
if !matched {
return Err(NameConstraintError::Violation(format!(
"{type_label} {name:?} is outside every permitted subtree"
)));
}
}
Ok(())
}
}
fn parse_general_subtrees(body: &[u8]) -> Result<Vec<GeneralNameBase>, NameConstraintError> {
let mut out = Vec::new();
let mut pos = body;
while !pos.is_empty() {
let (t, st_body, rest) = parse_tlv_with_rest(pos)
.map_err(|e| NameConstraintError::Parse(format!("GeneralSubtree: {e}")))?;
if t != 0x30 {
return Err(NameConstraintError::Parse(format!(
"expected GeneralSubtree SEQUENCE, got 0x{t:02x}"
)));
}
let (gn_tag, gn_body, gn_rest) = parse_tlv_with_rest(st_body)
.map_err(|e| NameConstraintError::Parse(format!("base GeneralName: {e}")))?;
validate_subtree_min_max(gn_rest)?;
out.push(decode_base_general_name(gn_tag, gn_body)?);
pos = rest;
}
Ok(out)
}
fn validate_subtree_min_max(mut rest: &[u8]) -> Result<(), NameConstraintError> {
while !rest.is_empty() {
let (tag, body, next) = parse_tlv_with_rest(rest)
.map_err(|e| NameConstraintError::Parse(format!("GeneralSubtree min/max: {e}")))?;
match tag {
0x80 => {
if body.len() != 1 || body[0] != 0 {
return Err(NameConstraintError::Violation(
"NameConstraints GeneralSubtree.minimum must be 0 (RFC 5280 §4.2.1.10)"
.into(),
));
}
}
0x81 => {
return Err(NameConstraintError::Violation(
"NameConstraints GeneralSubtree.maximum must be absent (RFC 5280 §4.2.1.10)"
.into(),
));
}
other => {
return Err(NameConstraintError::Parse(format!(
"unexpected field [tag 0x{other:02x}] in GeneralSubtree"
)));
}
}
rest = next;
}
Ok(())
}
fn decode_base_general_name(tag: u8, body: &[u8]) -> Result<GeneralNameBase, NameConstraintError> {
match tag {
GN_DNS => Ok(GeneralNameBase::Dns(ia5_string(body)?.to_ascii_lowercase())),
GN_RFC822 => Ok(GeneralNameBase::Rfc822(
ia5_string(body)?.to_ascii_lowercase(),
)),
GN_DIRECTORY => {
let (inner_tag, _inner) = parse_tlv(body)
.map_err(|e| NameConstraintError::Parse(format!("directoryName: {e}")))?;
if inner_tag != 0x30 {
return Err(NameConstraintError::Parse(format!(
"directoryName base inner tag 0x{inner_tag:02x}, expected SEQUENCE"
)));
}
Ok(GeneralNameBase::Directory(body.to_vec()))
}
GN_IP => {
match body.len() {
8 => Ok(GeneralNameBase::Ip(body[..4].to_vec(), body[4..].to_vec())),
32 => Ok(GeneralNameBase::Ip(
body[..16].to_vec(),
body[16..].to_vec(),
)),
n => Err(NameConstraintError::Parse(format!(
"iPAddress constraint must be 8 or 32 bytes (addr||mask), got {n}"
))),
}
}
other => Err(NameConstraintError::Unsupported(format!(
"GeneralName type [tag 0x{other:02x}] in NameConstraints is not supported"
))),
}
}
fn collect_asserted_names(cert: &Certificate) -> Result<Vec<AssertedName>, NameConstraintError> {
let mut names = Vec::new();
use der::Encode;
let subject_der = cert
.tbs_certificate
.subject
.to_der()
.map_err(|e| NameConstraintError::Parse(format!("subject DER: {e}")))?;
if subject_der.len() > 2 {
names.push(AssertedName::Directory(subject_der));
}
let san_oid = const_oid::ObjectIdentifier::new_unwrap(SAN_OID);
if let Some(extensions) = &cert.tbs_certificate.extensions {
if let Some(ext) = extensions.iter().find(|e| e.extn_id == san_oid) {
collect_san_names(ext.extn_value.as_bytes(), &mut names)?;
}
}
Ok(names)
}
fn collect_san_names(
value: &[u8],
names: &mut Vec<AssertedName>,
) -> Result<(), NameConstraintError> {
let (tag, body) =
parse_tlv(value).map_err(|e| NameConstraintError::Parse(format!("SAN: {e}")))?;
if tag != 0x30 {
return Err(NameConstraintError::Parse(format!(
"SAN expected SEQUENCE, got 0x{tag:02x}"
)));
}
let mut pos = &body[..];
while !pos.is_empty() {
let (gn_tag, gn_body, rest) = parse_tlv_with_rest(pos)
.map_err(|e| NameConstraintError::Parse(format!("SAN entry: {e}")))?;
match gn_tag {
GN_DNS => names.push(AssertedName::Dns(ia5_string(gn_body)?.to_ascii_lowercase())),
GN_RFC822 => names.push(AssertedName::Rfc822(
ia5_string(gn_body)?.to_ascii_lowercase(),
)),
GN_IP => {
match gn_body.len() {
4 | 16 => names.push(AssertedName::Ip(gn_body.to_vec())),
n => {
return Err(NameConstraintError::Parse(format!(
"SAN iPAddress must be 4 or 16 bytes, got {n}"
)));
}
}
}
GN_DIRECTORY => {
names.push(AssertedName::Directory(gn_body.to_vec()));
}
_ => {}
}
pos = rest;
}
Ok(())
}
fn ia5_string(body: &[u8]) -> Result<String, NameConstraintError> {
if body.is_ascii() {
Ok(String::from_utf8_lossy(body).into_owned())
} else {
Err(NameConstraintError::Parse(
"IA5String contains non-ASCII bytes".into(),
))
}
}
fn base_matches(base: &GeneralNameBase, name: &AssertedName) -> Result<bool, NameConstraintError> {
match (base, name) {
(GeneralNameBase::Dns(b), AssertedName::Dns(n)) => Ok(dns_matches(b, n)),
(GeneralNameBase::Rfc822(b), AssertedName::Rfc822(n)) => Ok(rfc822_matches(b, n)),
(GeneralNameBase::Ip(addr, mask), AssertedName::Ip(n)) => Ok(ip_matches(addr, mask, n)),
(GeneralNameBase::Directory(b), AssertedName::Directory(n)) => directory_matches(b, n),
_ => Ok(false),
}
}
fn dns_matches(base: &str, name: &str) -> bool {
if base.is_empty() {
return true;
}
if let Some(suffix) = base.strip_prefix('.') {
return name.len() > suffix.len()
&& name.ends_with(suffix)
&& name.as_bytes()[name.len() - suffix.len() - 1] == b'.';
}
if name == base {
return true;
}
name.len() > base.len()
&& name.ends_with(base)
&& name.as_bytes()[name.len() - base.len() - 1] == b'.'
}
fn rfc822_matches(base: &str, name: &str) -> bool {
if base.is_empty() {
return true;
}
let host = match name.rsplit_once('@') {
Some((_local, host)) => host,
None => name, };
if let Some(suffix) = base.strip_prefix('.') {
return host.len() > suffix.len()
&& host.ends_with(suffix)
&& host.as_bytes()[host.len() - suffix.len() - 1] == b'.';
}
if base.contains('@') {
return name == base;
}
host == base
|| (host.len() > base.len()
&& host.ends_with(base)
&& host.as_bytes()[host.len() - base.len() - 1] == b'.')
}
fn ip_matches(addr: &[u8], mask: &[u8], name: &[u8]) -> bool {
if name.len() != addr.len() || mask.len() != addr.len() {
return false;
}
addr.iter()
.zip(mask.iter())
.zip(name.iter())
.all(|((a, m), n)| (a & m) == (n & m))
}
fn directory_matches(
base_wrapped: &[u8],
name_wrapped: &[u8],
) -> Result<bool, NameConstraintError> {
let base_rdns = inner_name_rdns(base_wrapped)?;
let name_rdns = inner_name_rdns(name_wrapped)?;
if base_rdns.len() > name_rdns.len() {
return Ok(false);
}
for (b, n) in base_rdns.iter().zip(name_rdns.iter()) {
if !rdn_matches(b, n)? {
return Ok(false);
}
}
Ok(true)
}
const DIRECTORY_STRING_TAGS: [u8; 3] = [0x0c, 0x13, 0x16];
type Atv = (Vec<u8>, u8, Vec<u8>);
fn rdn_atvs(rdn_der: &[u8]) -> Result<Vec<Atv>, NameConstraintError> {
let (tag, body) = parse_tlv(rdn_der)
.map_err(|e| NameConstraintError::Parse(format!("directoryName RDN: {e}")))?;
if tag != 0x31 {
return Err(NameConstraintError::Parse(format!(
"directoryName RDN tag 0x{tag:02x}, expected SET"
)));
}
let mut atvs = Vec::new();
let mut pos = &body[..];
while !pos.is_empty() {
let (atv_tag, atv_body, rest) = parse_tlv_with_rest(pos)
.map_err(|e| NameConstraintError::Parse(format!("AttributeTypeAndValue: {e}")))?;
if atv_tag != 0x30 {
return Err(NameConstraintError::Parse(format!(
"AttributeTypeAndValue tag 0x{atv_tag:02x}, expected SEQUENCE"
)));
}
let (oid_tag, oid_body, val_rest) = parse_tlv_with_rest(atv_body)
.map_err(|e| NameConstraintError::Parse(format!("attribute type: {e}")))?;
if oid_tag != 0x06 {
return Err(NameConstraintError::Parse(format!(
"attribute type tag 0x{oid_tag:02x}, expected OID"
)));
}
let (val_tag, val_body, trailing) = parse_tlv_with_rest(val_rest)
.map_err(|e| NameConstraintError::Parse(format!("attribute value: {e}")))?;
if !trailing.is_empty() {
return Err(NameConstraintError::Parse(
"trailing data in AttributeTypeAndValue".into(),
));
}
if !directory_string_well_formed(val_tag, val_body) {
return Err(NameConstraintError::Parse(format!(
"malformed directoryName attribute value for string type (tag 0x{val_tag:02x})"
)));
}
atvs.push((oid_body.to_vec(), val_tag, val_body.to_vec()));
pos = rest;
}
Ok(atvs)
}
fn directory_string_well_formed(tag: u8, body: &[u8]) -> bool {
match tag {
0x0c => std::str::from_utf8(body).is_ok(),
0x13 | 0x16 => body.is_ascii(),
_ => true,
}
}
fn normalize_directory_string(bytes: &[u8]) -> Option<String> {
let s = std::str::from_utf8(bytes).ok()?;
let mut out = String::with_capacity(s.len());
let mut pending_space = false;
for c in s.chars() {
if c.is_whitespace() {
pending_space = !out.is_empty();
} else {
if pending_space {
out.push(' ');
pending_space = false;
}
out.extend(c.to_lowercase());
}
}
Some(out)
}
fn atv_value_matches(b_tag: u8, b_val: &[u8], n_tag: u8, n_val: &[u8]) -> bool {
if DIRECTORY_STRING_TAGS.contains(&b_tag) && DIRECTORY_STRING_TAGS.contains(&n_tag) {
return match (
normalize_directory_string(b_val),
normalize_directory_string(n_val),
) {
(Some(b), Some(n)) => b == n,
_ => false,
};
}
b_tag == n_tag && b_val == n_val
}
fn rdn_matches(base_rdn: &[u8], name_rdn: &[u8]) -> Result<bool, NameConstraintError> {
let base = rdn_atvs(base_rdn)?;
let name = rdn_atvs(name_rdn)?;
if base.len() != name.len() {
return Ok(false);
}
let mut used = vec![false; name.len()];
for (b_oid, b_tag, b_val) in &base {
let mut found = false;
for (i, (n_oid, n_tag, n_val)) in name.iter().enumerate() {
if used[i] || n_oid != b_oid {
continue;
}
if atv_value_matches(*b_tag, b_val, *n_tag, n_val) {
used[i] = true;
found = true;
break;
}
}
if !found {
return Ok(false);
}
}
Ok(true)
}
fn inner_name_rdns(der: &[u8]) -> Result<Vec<Vec<u8>>, NameConstraintError> {
let (tag, body) =
parse_tlv(der).map_err(|e| NameConstraintError::Parse(format!("directoryName: {e}")))?;
let name_body = if tag == GN_DIRECTORY {
let (inner_tag, inner_body) = parse_tlv(&body)
.map_err(|e| NameConstraintError::Parse(format!("directoryName inner Name: {e}")))?;
if inner_tag != 0x30 {
return Err(NameConstraintError::Parse(format!(
"directoryName inner tag 0x{inner_tag:02x}, expected SEQUENCE"
)));
}
inner_body
} else if tag == 0x30 {
body
} else {
return Err(NameConstraintError::Parse(format!(
"directoryName tag 0x{tag:02x}, expected [4] or SEQUENCE"
)));
};
let mut rdns = Vec::new();
let mut pos = &name_body[..];
while !pos.is_empty() {
let (rdn_tag, _rdn_body, rest) = parse_tlv_with_rest(pos)
.map_err(|e| NameConstraintError::Parse(format!("directoryName RDN: {e}")))?;
let consumed = pos.len() - rest.len();
if rdn_tag != 0x31 {
return Err(NameConstraintError::Parse(format!(
"directoryName RDN tag 0x{rdn_tag:02x}, expected SET"
)));
}
rdns.push(pos[..consumed].to_vec());
pos = rest;
}
Ok(rdns)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dns_label_boundary() {
assert!(dns_matches("example.com", "host.example.com"));
assert!(dns_matches("example.com", "example.com"));
assert!(dns_matches(".example.com", "host.example.com"));
assert!(!dns_matches(".example.com", "example.com"));
assert!(!dns_matches("example.com", "notexample.com"));
assert!(!dns_matches("example.com", "example.com.evil.com"));
assert!(dns_matches("", "anything.test")); }
#[test]
fn rfc822_rules() {
assert!(rfc822_matches("example.com", "alice@example.com"));
assert!(rfc822_matches("example.com", "bob@sub.example.com"));
assert!(!rfc822_matches("example.com", "bob@notexample.com"));
assert!(rfc822_matches("host.example.com", "carol@host.example.com"));
assert!(!rfc822_matches(
"host.example.com",
"carol@other.example.com"
));
assert!(rfc822_matches(".example.com", "dan@sub.example.com"));
assert!(!rfc822_matches(".example.com", "dan@example.com"));
assert!(rfc822_matches("alice@example.com", "alice@example.com"));
}
#[test]
fn ipv4_cidr() {
let addr = vec![10, 0, 0, 0];
let mask = vec![0xff, 0, 0, 0];
assert!(ip_matches(&addr, &mask, &[10, 1, 2, 3]));
assert!(!ip_matches(&addr, &mask, &[11, 0, 0, 1]));
assert!(!ip_matches(&addr, &mask, &[0u8; 16]));
}
#[test]
fn unsupported_constraint_fails_closed() {
let err = decode_base_general_name(0x86, b"http://x/").unwrap_err();
assert!(matches!(err, NameConstraintError::Unsupported(_)));
}
#[test]
fn malformed_ip_san_fails_closed() {
use crate::der_utils::{encode_sequence_raw, encode_tlv};
let san_value = encode_sequence_raw(&encode_tlv(GN_IP, &[192, 0, 2]));
let mut names = Vec::new();
let err = collect_san_names(&san_value, &mut names).unwrap_err();
assert!(matches!(err, NameConstraintError::Parse(ref m) if m.contains("iPAddress")));
assert!(names.is_empty());
}
#[test]
fn malformed_directory_name_fails_closed() {
let valid_name = [0x30, 0x00];
let invalid_name = [0x04, 0x00];
let err = base_matches(
&GeneralNameBase::Directory(valid_name.to_vec()),
&AssertedName::Directory(invalid_name.to_vec()),
)
.unwrap_err();
assert!(matches!(err, NameConstraintError::Parse(ref m) if m.contains("directoryName")));
}
fn dir_name(oid_body: &[u8], value_tag: u8, value: &[u8]) -> Vec<u8> {
use crate::der_utils::{encode_sequence_raw, encode_tlv};
let atv = encode_sequence_raw(
&[encode_tlv(0x06, oid_body), encode_tlv(value_tag, value)].concat(),
);
let rdn = encode_tlv(0x31, &atv);
encode_sequence_raw(&rdn)
}
const CN_OID: &[u8] = &[0x55, 0x04, 0x03]; const O_OID: &[u8] = &[0x55, 0x04, 0x0A];
#[test]
fn directory_name_case_and_string_type_insensitive() {
let printable = dir_name(CN_OID, 0x13, b"Example CA");
let utf8_lower = dir_name(CN_OID, 0x0c, b"example ca");
assert!(directory_matches(&printable, &utf8_lower).unwrap());
assert!(directory_matches(&utf8_lower, &printable).unwrap());
let padded = dir_name(CN_OID, 0x0c, b" Example CA ");
assert!(directory_matches(&printable, &padded).unwrap());
}
#[test]
fn directory_name_still_distinguishes_different_values() {
let a = dir_name(CN_OID, 0x13, b"Example CA");
let b = dir_name(CN_OID, 0x13, b"Other CA");
assert!(!directory_matches(&a, &b).unwrap());
let cn = dir_name(CN_OID, 0x13, b"Example CA");
let o = dir_name(O_OID, 0x13, b"Example CA");
assert!(!directory_matches(&cn, &o).unwrap());
}
#[test]
fn directory_name_non_string_values_exact_match_only() {
let a = dir_name(CN_OID, 0x04, b"AB");
let b = dir_name(CN_OID, 0x04, b"ab");
assert!(!directory_matches(&a, &b).unwrap());
assert!(directory_matches(&a, &a).unwrap());
}
#[test]
fn directory_name_malformed_string_value_fails_closed() {
let good = dir_name(CN_OID, 0x0c, b"example");
let bad_utf8 = dir_name(CN_OID, 0x0c, &[0xff, 0xfe, 0x00]);
let err = directory_matches(&bad_utf8, &good).unwrap_err();
assert!(matches!(err, NameConstraintError::Parse(_)));
let err = directory_matches(&good, &bad_utf8).unwrap_err();
assert!(matches!(err, NameConstraintError::Parse(_)));
let bad_printable = dir_name(CN_OID, 0x13, &[0x41, 0xc9, 0x42]); let err = directory_matches(&bad_printable, &good).unwrap_err();
assert!(matches!(err, NameConstraintError::Parse(_)));
}
#[test]
fn general_subtree_min_max_fail_closed() {
use crate::der_utils::{encode_sequence_raw, encode_tlv};
let base = encode_tlv(GN_DNS, b"example.com");
let ok = encode_sequence_raw(&base);
assert!(parse_general_subtrees(&ok).is_ok());
let mut st = base.clone();
st.extend(encode_tlv(0x80, &[0x00]));
assert!(parse_general_subtrees(&encode_sequence_raw(&st)).is_ok());
let mut st = base.clone();
st.extend(encode_tlv(0x80, &[0x05]));
assert!(matches!(
parse_general_subtrees(&encode_sequence_raw(&st)).unwrap_err(),
NameConstraintError::Violation(ref m) if m.contains("minimum")
));
let mut st = base.clone();
st.extend(encode_tlv(0x81, &[0x05]));
assert!(matches!(
parse_general_subtrees(&encode_sequence_raw(&st)).unwrap_err(),
NameConstraintError::Violation(ref m) if m.contains("maximum")
));
}
}