use crate::courierust_tls::TlsError;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone)]
pub struct Certificate {
pub der: Vec<u8>,
pub tbs: Vec<u8>,
pub sig_alg: SigAlg,
pub signature: Vec<u8>,
pub spki: Spki,
pub serial: Vec<u8>,
pub issuer_der: Vec<u8>,
pub subject_der: Vec<u8>,
pub not_before: i64,
pub not_after: i64,
pub dns_names: Vec<String>,
pub ip_names: Vec<Vec<u8>>,
pub is_ca: Option<bool>,
pub path_len: Option<u8>,
pub key_usage: KeyUsage,
pub eku: Vec<Vec<u8>>,
pub name_constraints: Option<NameConstraints>,
pub has_other_sans: bool,
pub unknown_critical: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NameConstraints {
pub permitted_dns: Vec<String>,
pub excluded_dns: Vec<String>,
pub permitted_ip: Vec<Vec<u8>>,
pub excluded_ip: Vec<Vec<u8>>,
pub has_other_forms: bool,
}
impl NameConstraints {
pub fn is_empty(&self) -> bool {
!self.has_other_forms
&& self.permitted_dns.is_empty()
&& self.excluded_dns.is_empty()
&& self.permitted_ip.is_empty()
&& self.excluded_ip.is_empty()
}
pub fn permits(&self, dns: &[String], ip: &[Vec<u8>], has_other_sans: bool) -> bool {
if self.has_other_forms && has_other_sans {
return false;
}
for d in dns {
let lower = d.trim_end_matches('.').to_ascii_lowercase();
if !self.permitted_dns.is_empty()
&& !self
.permitted_dns
.iter()
.any(|c| dns_name_matches_constraint(&lower, c, false))
{
return false;
}
if self
.excluded_dns
.iter()
.any(|c| dns_name_matches_constraint(&lower, c, true))
{
return false;
}
}
for a in ip {
if !self.permitted_ip.is_empty()
&& !self
.permitted_ip
.iter()
.any(|c| ip_matches_constraint(a, c) == Some(true))
{
return false;
}
if self
.excluded_ip
.iter()
.any(|c| ip_matches_constraint(a, c) == Some(true))
{
return false;
}
}
true
}
}
fn dns_name_matches_constraint(presented: &str, constraint: &str, is_excluded: bool) -> bool {
fn eq(a: &[u8], b: &[u8]) -> bool {
a.eq_ignore_ascii_case(b)
}
fn subdomain(name: &[u8], constraint: &[u8]) -> bool {
if name.len() <= constraint.len() {
return false;
}
let off = name.len() - constraint.len();
if constraint.first() == Some(&b'.') {
name[off..].eq_ignore_ascii_case(constraint)
} else {
name[off - 1] == b'.' && name[off..].eq_ignore_ascii_case(constraint)
}
}
let p = presented.as_bytes();
let c = constraint.as_bytes();
if c.is_empty() {
return true;
}
if is_excluded && p.starts_with(b"*.") {
let x = &p[2..];
if !c.starts_with(b".") && eq(x, c) {
return true;
}
if subdomain(x, c) {
return true;
}
let pre = c.len().saturating_sub(x.len());
if pre > 1 && c.ends_with(x) && c[pre - 1] == b'.' && !c[..pre - 1].contains(&b'.') {
return true;
}
if c.starts_with(b".") && eq(x, &c[1..]) {
return true;
}
return false;
}
if c[0] == b'.' {
return subdomain(p, c);
}
if eq(p, c) {
return true;
}
subdomain(p, c)
}
fn ip_matches_constraint(ip: &[u8], constraint: &[u8]) -> Option<bool> {
let half = constraint.len() / 2;
match (ip.len(), constraint.len()) {
(4, 8) | (16, 32) => {}
(4, 32) | (16, 8) => return Some(false),
_ => return None,
}
let (addr, mask) = constraint.split_at(half);
let mut seen_zero = false;
for (&a, (&b, &m)) in ip.iter().zip(addr.iter().zip(mask.iter())) {
let ones = m.leading_ones();
let zeros = m.trailing_zeros();
if ones + zeros != 8 || (seen_zero && m != 0) {
return None;
}
if m != 0xff {
seen_zero = true;
}
if (a ^ b) & m != 0 {
return Some(false);
}
}
Some(true)
}
pub(crate) const MAX_CHAIN_LEN: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigAlg {
RsaSha256,
RsaSha384,
RsaSha512,
EcdsaSha256,
EcdsaSha384,
EcdsaSha512,
Ed25519,
Unknown,
}
#[derive(Debug, Clone)]
pub struct Spki {
pub oid: Vec<u8>,
pub key: Vec<u8>,
pub ec_curve: Option<crate::courierust_tls::crypto::ecdsa::Curve>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct KeyUsage {
pub digital_signature: bool,
pub content_commitment: bool,
pub key_encipherment: bool,
pub data_encipherment: bool,
pub key_agreement: bool,
pub key_cert_sign: bool,
pub crl_sign: bool,
pub encipher_only: bool,
pub decipher_only: bool,
}
impl KeyUsage {
fn from_bits(bits: &[u8]) -> Self {
let mut u = Self::default();
if bits.is_empty() {
return u;
}
let b0 = bits[0];
let b1 = bits.get(1).copied().unwrap_or(0);
u.digital_signature = b0 & 0x80 != 0;
u.content_commitment = b0 & 0x40 != 0;
u.key_encipherment = b0 & 0x20 != 0;
u.data_encipherment = b0 & 0x10 != 0;
u.key_agreement = b0 & 0x08 != 0;
u.key_cert_sign = b0 & 0x04 != 0;
u.crl_sign = b0 & 0x02 != 0;
u.encipher_only = b0 & 0x01 != 0;
u.decipher_only = b1 & 0x80 != 0;
u
}
}
#[derive(Debug, Clone)]
pub struct RootCert {
pub der: Vec<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct RootStore {
roots: Vec<RootCert>,
}
impl RootStore {
pub fn new() -> Self {
Self { roots: Vec::new() }
}
pub fn add_der(&mut self, der: Vec<u8>) {
self.roots.push(RootCert { der });
}
pub fn add_pem(&mut self, pem: &str) -> crate::courierust_tls::TlsResult<usize> {
let certs = parse_pem_certificates(pem)?;
let n = certs.len();
for c in certs {
self.roots.push(RootCert { der: c });
}
Ok(n)
}
pub fn len(&self) -> usize {
self.roots.len()
}
pub fn is_empty(&self) -> bool {
self.roots.is_empty()
}
pub fn roots(&self) -> &[RootCert] {
&self.roots
}
}
pub fn parse_pem_certificates(pem: &str) -> crate::courierust_tls::TlsResult<Vec<Vec<u8>>> {
let mut out = Vec::new();
let mut current: Option<Vec<u8>> = None;
let mut in_cert = false;
for line in pem.lines() {
let line = line.trim();
if line.starts_with("-----BEGIN CERTIFICATE-----") {
in_cert = true;
current = Some(Vec::new());
} else if line.starts_with("-----END CERTIFICATE-----") {
if let Some(mut b64) = current.take() {
let raw: String = b64
.drain(..)
.filter(|c| !c.is_ascii_whitespace())
.map(|c| c as char)
.collect();
let der = base64_decode(&raw)
.ok_or_else(|| TlsError::Protocol("invalid PEM base64".into()))?;
out.push(der);
}
in_cert = false;
} else if in_cert {
if let Some(buf) = current.as_mut() {
buf.extend_from_slice(line.as_bytes());
}
}
}
Ok(out)
}
fn base64_decode(s: &str) -> Option<Vec<u8>> {
let mut out = Vec::new();
let mut acc: u32 = 0;
let mut nbits = 0;
for c in s.bytes() {
let v = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
b'=' => break,
_ => return None,
};
acc = (acc << 6) | v as u32;
nbits += 6;
if nbits >= 8 {
nbits -= 8;
out.push((acc >> nbits) as u8);
}
}
Some(out)
}
pub(crate) mod der;
pub fn parse_certificate(der: &[u8]) -> crate::courierust_tls::TlsResult<Certificate> {
der::parse_certificate(der)
}
pub fn hostname_matches(name: &str, dns_names: &[String], ip_names: &[Vec<u8>]) -> bool {
let name = name.trim_end_matches('.');
if let Ok(ip) = name.parse::<std::net::IpAddr>() {
let bytes = match ip {
std::net::IpAddr::V4(v4) => v4.octets().to_vec(),
std::net::IpAddr::V6(v6) => v6.octets().to_vec(),
};
return ip_names.contains(&bytes);
}
if dns_names.is_empty() {
return false;
}
let lower = name.to_ascii_lowercase();
for d in dns_names {
let d = d.trim_end_matches('.');
if dns_match(&lower, &d.to_ascii_lowercase()) {
return true;
}
}
false
}
fn dns_match(name: &str, pattern: &str) -> bool {
if name == pattern {
return true;
}
if !pattern.starts_with("*.") {
return false;
}
let suffix = &pattern[2..];
if suffix.contains('*') {
return false;
}
let Some(stripped) = name.strip_suffix(suffix) else {
return false;
};
if !stripped.ends_with('.') {
return false;
}
let label = &stripped[..stripped.len() - 1];
!label.is_empty() && !label.contains('.')
}
const EKU_ANY: &[u8] = &[0x55, 0x1d, 0x25, 0x00];
pub fn has_server_auth_eku(cert: &Certificate) -> bool {
if cert.eku.is_empty() {
return true;
}
cert.eku
.iter()
.any(|e| e.as_slice() == der::OID_KEY_USAGE_SERVER_AUTH || e.as_slice() == EKU_ANY)
}
pub fn validate_chain(
roots: &RootStore,
chain: &[Vec<u8>],
now: i64,
) -> crate::courierust_tls::TlsResult<()> {
if chain.is_empty() {
return Err(TlsError::Certificate("empty certificate chain".into()));
}
let certs: Vec<Certificate> = chain
.iter()
.map(|d| der::parse_certificate(d))
.collect::<crate::courierust_tls::TlsResult<Vec<_>>>()?;
if certs.len() > MAX_CHAIN_LEN {
return Err(TlsError::Certificate(format!(
"certificate chain exceeds the maximum depth of {}",
MAX_CHAIN_LEN
)));
}
for (i, c) in certs.iter().enumerate() {
if now < c.not_before || now > c.not_after {
return Err(TlsError::Certificate(format!(
"certificate {} outside validity window",
i
)));
}
}
for (i, c) in certs.iter().enumerate() {
if c.unknown_critical {
return Err(TlsError::Certificate(format!(
"certificate {i} carries an unsupported critical extension"
)));
}
}
for i in 0..certs.len() - 1 {
let child = &certs[i];
let parent = &certs[i + 1];
if child.issuer_der != parent.subject_der {
return Err(TlsError::Certificate("issuer/subject name mismatch".into()));
}
if !verify_cert_signature(child, &parent.spki) {
return Err(TlsError::Certificate(
"certificate signature invalid".into(),
));
}
}
for ca in certs.iter().skip(1) {
if ca.is_ca != Some(true) {
return Err(TlsError::Certificate("chain issuer is not a CA".into()));
}
if usage_present(&ca.key_usage) && !ca.key_usage.key_cert_sign {
return Err(TlsError::Certificate("CA lacks keyCertSign".into()));
}
}
for (i, ca) in certs
.iter()
.enumerate()
.skip(1)
.take(certs.len().saturating_sub(2))
{
if let Some(p) = ca.path_len {
let below = (1..i)
.filter(|&j| {
certs[j].is_ca == Some(true) && certs[j].issuer_der != certs[j].subject_der
})
.count();
if below > p as usize {
return Err(TlsError::Certificate(format!(
"certificate {i} exceeds its pathLenConstraint"
)));
}
}
}
{
let leaf = &certs[0];
if usage_present(&leaf.key_usage) {
let rsa = leaf.spki.oid == der::OID_RSA_ENCRYPTION;
if !(leaf.key_usage.digital_signature || (rsa && leaf.key_usage.key_encipherment)) {
return Err(TlsError::Certificate(
"leaf certificate key usage forbids server authentication".into(),
));
}
}
}
let last = certs.last().unwrap();
let anchored: bool = {
let mut found = false;
for root in roots.roots() {
if root.der == last.der {
found = true;
break;
}
}
found
};
if !anchored {
let mut verified = false;
for root in roots.roots() {
let Ok(root_cert) = der::parse_certificate(&root.der) else {
continue;
};
if last.issuer_der != root_cert.subject_der {
continue;
}
if root_cert.is_ca != Some(true) {
continue;
}
if usage_present(&root_cert.key_usage) && !root_cert.key_usage.key_cert_sign {
continue;
}
if verify_cert_signature(last, &root_cert.spki)
&& root_cert.issuer_der == root_cert.subject_der
{
verified = true;
break;
}
}
if !verified {
return Err(TlsError::Certificate("no trusted root found".into()));
}
} else if last.issuer_der != last.subject_der && !verify_cert_signature(last, &last.spki) {
return Err(TlsError::Certificate("root is not self-signed".into()));
}
if certs[0].is_ca == Some(true) && !(certs.len() == 1 && anchored) {
return Err(TlsError::Certificate("leaf certificate is a CA".into()));
}
for (i, ca) in certs.iter().enumerate().skip(1) {
let Some(nc) = &ca.name_constraints else {
continue;
};
if nc.is_empty() {
continue;
}
if i == certs.len() - 1 && anchored {
continue;
}
for subordinate in certs.iter().take(i) {
if !nc.permits(
&subordinate.dns_names,
&subordinate.ip_names,
subordinate.has_other_sans,
) {
return Err(TlsError::Certificate(format!(
"certificate {i} name constraints violated by a subordinate \
certificate's names"
)));
}
}
}
Ok(())
}
fn usage_present(u: &KeyUsage) -> bool {
u.digital_signature
|| u.content_commitment
|| u.key_encipherment
|| u.data_encipherment
|| u.key_agreement
|| u.key_cert_sign
|| u.crl_sign
|| u.encipher_only
|| u.decipher_only
}
fn verify_cert_signature(cert: &Certificate, issuer: &Spki) -> bool {
use crate::courierust_tls::crypto::hash::{Digest, Sha256, Sha384};
use crate::courierust_tls::crypto::rsa::{
RsaPublicKey, DIGEST_INFO_SHA256, DIGEST_INFO_SHA384, DIGEST_INFO_SHA512,
};
use crate::courierust_tls::crypto::{ecdsa, ed25519};
use crate::courierust_tls::x509::der::{
parse_rsa_public_key, OID_EC_PUBLIC_KEY, OID_ED25519, OID_RSA_ENCRYPTION,
};
match cert.sig_alg {
SigAlg::RsaSha256 | SigAlg::RsaSha384 | SigAlg::RsaSha512 => {
if issuer.oid != OID_RSA_ENCRYPTION {
return false;
}
let Some((n, e)) = parse_rsa_public_key(&issuer.key) else {
return false;
};
let key = RsaPublicKey { n, e };
let digest = match cert.sig_alg {
SigAlg::RsaSha256 => {
let mut h = Sha256::new();
h.update(&cert.tbs);
h.finalize()
}
SigAlg::RsaSha384 => {
let mut h = Sha384::new();
h.update(&cert.tbs);
h.finalize()
}
_ => {
let mut h = Sha512Digest::create();
h.update(&cert.tbs);
h.finalize()
}
};
let digest_info = match cert.sig_alg {
SigAlg::RsaSha256 => DIGEST_INFO_SHA256,
SigAlg::RsaSha384 => DIGEST_INFO_SHA384,
_ => DIGEST_INFO_SHA512,
};
key.verify_pkcs1v15(digest_info, &digest, &cert.signature)
}
SigAlg::EcdsaSha256 | SigAlg::EcdsaSha384 | SigAlg::EcdsaSha512 => {
use crate::courierust_tls::crypto::ecdsa::Curve;
if issuer.oid != OID_EC_PUBLIC_KEY {
return false;
}
let curve = match cert.sig_alg {
SigAlg::EcdsaSha256 => Curve::P256,
SigAlg::EcdsaSha384 => Curve::P384,
_ => Curve::P521,
};
if issuer.ec_curve != Some(curve) {
return false;
}
let clen = curve.coord_len();
if issuer.key.len() != 1 + 2 * clen || issuer.key[0] != 0x04 {
return false;
}
let qx = &issuer.key[1..1 + clen];
let qy = &issuer.key[1 + clen..1 + 2 * clen];
let digest = match cert.sig_alg {
SigAlg::EcdsaSha256 => {
let mut h = Sha256::new();
h.update(&cert.tbs);
h.finalize()
}
SigAlg::EcdsaSha384 => {
let mut h = Sha384::new();
h.update(&cert.tbs);
h.finalize()
}
_ => {
let mut h = Sha512Digest::create();
h.update(&cert.tbs);
h.finalize()
}
};
ecdsa::verify_der(curve, qx, qy, &digest, &cert.signature)
}
SigAlg::Ed25519 => {
if issuer.oid != OID_ED25519 || issuer.key.len() != 32 {
return false;
}
let mut pk = [0u8; 32];
pk.copy_from_slice(&issuer.key);
let mut sig = [0u8; 64];
if cert.signature.len() != 64 {
return false;
}
sig.copy_from_slice(&cert.signature);
ed25519::verify(&pk, &cert.tbs, &sig)
}
SigAlg::Unknown => false,
}
}
struct Sha512Digest;
impl Sha512Digest {
fn create() -> crate::courierust_tls::crypto::ed25519::Sha512 {
crate::courierust_tls::crypto::ed25519::Sha512::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
const NOW: i64 = 1_800_000_000;
fn p384_leaf() -> Vec<u8> {
crate::courierust_tls::testdata::P384_LEAF_CERT_DER.to_vec()
}
fn p384_intermediate() -> Vec<u8> {
crate::courierust_tls::testdata::P384_INTERMEDIATE_CERT_DER.to_vec()
}
fn p384_ca() -> Vec<u8> {
crate::courierust_tls::testdata::P384_CA_CERT_DER.to_vec()
}
fn p384_roots() -> RootStore {
let mut roots = RootStore::new();
roots.add_der(p384_ca());
roots
}
#[test]
fn validates_p384_intermediate_chain() {
let chain = vec![p384_leaf(), p384_intermediate()];
validate_chain(&p384_roots(), &chain, NOW).expect("P-384 intermediate chain must validate");
}
#[test]
fn validates_p384_full_chain_including_root() {
let chain = vec![p384_leaf(), p384_intermediate(), p384_ca()];
validate_chain(&p384_roots(), &chain, NOW)
.expect("full P-384 chain including the root must validate");
}
#[test]
fn p384_leaf_spki_shape() {
let cert = parse_certificate(&p384_leaf()).unwrap();
assert_eq!(
cert.sig_alg,
SigAlg::EcdsaSha384,
"leaf signed by P-384 intermediate must use ecdsa-with-SHA384"
);
let spki = &cert.spki;
assert_eq!(spki.oid, der::OID_EC_PUBLIC_KEY);
assert_eq!(
spki.ec_curve,
Some(crate::courierust_tls::crypto::ecdsa::Curve::P384)
);
assert_eq!(spki.key.len(), 1 + 2 * 48);
assert_eq!(spki.key[0], 0x04);
}
#[test]
fn p384_intermediate_spki_shape() {
let cert = parse_certificate(&p384_intermediate()).unwrap();
assert_eq!(cert.sig_alg, SigAlg::EcdsaSha384);
assert_eq!(cert.is_ca, Some(true));
let spki = &cert.spki;
assert_eq!(
spki.ec_curve,
Some(crate::courierust_tls::crypto::ecdsa::Curve::P384)
);
assert_eq!(spki.key.len(), 1 + 2 * 48);
}
#[test]
fn p384_leaf_hostname_matches() {
let cert = parse_certificate(&p384_leaf()).unwrap();
assert!(hostname_matches(
"localhost",
&cert.dns_names,
&cert.ip_names
));
assert!(hostname_matches(
"127.0.0.1",
&cert.dns_names,
&cert.ip_names
));
assert!(!hostname_matches(
"other.example",
&cert.dns_names,
&cert.ip_names
));
}
#[test]
fn p384_chain_rejected_without_trust() {
let chain = vec![p384_leaf(), p384_intermediate()];
let roots = RootStore::new();
assert!(validate_chain(&roots, &chain, NOW).is_err());
}
#[test]
fn p384_chain_rejects_tampered_intermediate() {
let chain = vec![p384_leaf(), p384_leaf()];
assert!(validate_chain(&p384_roots(), &chain, NOW).is_err());
}
#[test]
fn existing_identity_paths_unaffected() {
let ed = crate::courierust_tls::testdata::server_identity();
let mut roots = RootStore::new();
roots.add_der(ed.cert_chain[0].clone());
validate_chain(&roots, &ed.cert_chain, NOW).expect("Ed25519 identity must still validate");
let rsa = crate::courierust_tls::testdata::rsa_server_identity();
let mut rroots = RootStore::new();
rroots.add_der(rsa.cert_chain[0].clone());
validate_chain(&rroots, &rsa.cert_chain, NOW).expect("RSA identity must still validate");
}
#[test]
fn dns_constraint_plain_matching() {
let cases: &[(&str, &str, bool)] = &[
("example.com", "example.com", true),
("www.example.com", "example.com", true),
("a.b.example.com", "example.com", true),
("badexample.com", "example.com", false),
("host1.example.com", "host.example.com", false),
("WWW.EXAMPLE.COM", "example.com", true),
("www.example.com", "EXAMPLE.COM", true),
("www.example.com", ".example.com", true),
("example.com", ".example.com", false),
("badexample.com", ".example.com", false),
("a.b.example.com", ".example.com", true),
("www.example.com", "", true),
("www.example.com", "axample.com", false),
("www.example.com", "exampl.com", false),
];
for &(presented, constraint, expected) in cases {
assert_eq!(
dns_name_matches_constraint(presented, constraint, false),
expected,
"dns constraint {presented:?} vs {constraint:?}"
);
if !presented.contains('*') {
assert_eq!(
dns_name_matches_constraint(presented, constraint, true),
expected,
"excluded dns constraint {presented:?} vs {constraint:?}"
);
}
}
}
#[test]
fn dns_constraint_wildcard_permitted() {
let cases: &[(&str, &str, bool)] = &[
("*.example.com", "example.com", true),
("*.example.com", ".example.com", true),
("*.example.com", "www.example.com", false),
("*.example.com", "axample.com", false),
("*.example.com", "exampl.com", false),
("*.www.example.com", "www.example.com", true),
];
for &(presented, constraint, expected) in cases {
assert_eq!(
dns_name_matches_constraint(presented, constraint, false),
expected,
"permitted dns constraint {presented:?} vs {constraint:?}"
);
}
}
#[test]
fn dns_constraint_wildcard_excluded_cve() {
let cases: &[(&str, &str, bool)] = &[
("*.example.com", "evil.example.com", true),
("*.example.com", "example.com", true),
("*.example.com", ".example.com", true),
("*.example.com", "xample.com", false),
("*.example.com", "evil.example.org", false),
("*.a.example.com", "a.example.com", true),
("*.a.example.com", "evil.example.com", false),
];
for &(presented, constraint, expected) in cases {
assert_eq!(
dns_name_matches_constraint(presented, constraint, true),
expected,
"excluded dns constraint {presented:?} vs {constraint:?}"
);
}
}
#[test]
fn ip_constraint_matching() {
type IpCase = (&'static [u8; 4], [u8; 4], [u8; 4], Option<bool>);
let cases: &[IpCase] = &[
(
&[192, 0, 2, 0],
[192, 0, 2, 0],
[255, 255, 255, 0],
Some(true),
),
(
&[192, 0, 2, 255],
[192, 0, 2, 0],
[255, 255, 255, 0],
Some(true),
),
(
&[192, 0, 1, 255],
[192, 0, 2, 0],
[255, 255, 255, 0],
Some(false),
),
(
&[8, 8, 8, 8],
[8, 8, 8, 8],
[255, 255, 255, 255],
Some(true),
),
(
&[8, 8, 8, 9],
[8, 8, 8, 8],
[255, 255, 255, 255],
Some(false),
),
(&[8, 8, 8, 8], [8, 8, 8, 8], [255, 255, 255, 1], None),
(&[8, 8, 8, 8], [8, 8, 8, 8], [255, 255, 0, 255], None),
];
for &(ip, addr, mask, expected) in cases {
let mut constraint = Vec::with_capacity(8);
constraint.extend_from_slice(&addr);
constraint.extend_from_slice(&mask);
assert_eq!(
ip_matches_constraint(ip, &constraint),
expected,
"ip constraint {ip:?} vs {constraint:?}"
);
}
let v6_constraint = {
let mut c = vec![0u8; 32];
c[0] = 0x20;
c[15] = 1;
c[16..32].fill(0xff);
c
};
assert_eq!(
ip_matches_constraint(&[192, 0, 2, 1], &v6_constraint),
Some(false)
);
}
#[test]
fn name_constraints_permits_leaf() {
let nc = NameConstraints {
permitted_dns: vec!["example.com".into()],
..Default::default()
};
assert!(nc.permits(&["www.example.com".into()], &[], false));
assert!(nc.permits(&["example.com".into()], &[], false));
assert!(!nc.permits(&["evil.org".into()], &[], false));
assert!(!nc.permits(&["badexample.com".into()], &[], false));
let nc = NameConstraints {
excluded_dns: vec!["evil.example.com".into()],
..Default::default()
};
assert!(nc.permits(&["good.example.com".into()], &[], false));
assert!(!nc.permits(&["evil.example.com".into()], &[], false));
assert!(!nc.permits(&["*.example.com".into()], &[], false));
let nc = NameConstraints {
permitted_ip: vec![vec![192, 0, 2, 0, 255, 255, 255, 0]],
..Default::default()
};
assert!(nc.permits(&[], &[vec![192, 0, 2, 9]], false));
assert!(!nc.permits(&[], &[vec![192, 0, 3, 9]], false));
let nc = NameConstraints {
has_other_forms: true,
..Default::default()
};
assert!(!nc.permits(&[], &[], true));
assert!(nc.permits(&[], &[], false));
}
fn nc_leaf_ok() -> Vec<u8> {
crate::courierust_tls::testdata::NC_LEAF_OK_CERT_DER.to_vec()
}
fn nc_leaf_bad() -> Vec<u8> {
crate::courierust_tls::testdata::NC_LEAF_BAD_CERT_DER.to_vec()
}
fn nc_intermediate() -> Vec<u8> {
crate::courierust_tls::testdata::NC_INTERMEDIATE_CERT_DER.to_vec()
}
#[test]
fn name_constraint_permitted_leaf_validates() {
let chain = vec![nc_leaf_ok(), nc_intermediate()];
validate_chain(
&crate::courierust_tls::testdata::nc_root_store(),
&chain,
NOW,
)
.expect("localhost leaf must satisfy the permitted;DNS:localhost constraint");
}
#[test]
fn name_constraint_outside_subtree_rejected() {
let chain = vec![nc_leaf_bad(), nc_intermediate()];
let err = validate_chain(
&crate::courierust_tls::testdata::nc_root_store(),
&chain,
NOW,
)
.expect_err("evil.com leaf must violate the permitted;DNS:localhost constraint");
assert!(matches!(err, TlsError::Certificate(_)), "got {err:?}");
}
#[test]
fn name_constraint_extension_parsed() {
let inter = parse_certificate(&nc_intermediate()).unwrap();
let nc = inter
.name_constraints
.expect("intermediate must carry nameConstraints");
assert_eq!(nc.permitted_dns, vec!["localhost".to_string()]);
assert!(nc.excluded_dns.is_empty());
assert!(!nc.is_empty());
let leaf = parse_certificate(&nc_leaf_ok()).unwrap();
assert!(leaf.name_constraints.is_none());
}
#[test]
fn oversized_chain_rejected() {
let mut chain = Vec::new();
for _ in 0..(MAX_CHAIN_LEN + 2) {
chain.push(crate::courierust_tls::testdata::SERVER_CERT_DER.to_vec());
}
let roots = crate::courierust_tls::testdata::root_store();
let err =
validate_chain(&roots, &chain, NOW).expect_err("over-long chain must be rejected");
assert!(matches!(err, TlsError::Certificate(_)), "got {err:?}");
}
}