use core::net::{Ipv4Addr, Ipv6Addr};
use crate::der_parser::Oid;
use crate::extensions::GeneralName;
use crate::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME;
use crate::policy::{PolicyEvaluationResult, ValidationPolicy};
use crate::unverified_chain::UnverifiedCertificateChain;
use crate::{Certificate, PolicyFailureReason};
const ASCII_PERIOD: u8 = b'.';
const ASCII_ASTERISK: u8 = b'*';
const ASCII_IDNA_IDENTIFIER: &[u8] = b"xn--";
pub struct ServerIdentityPolicy {
server_hostname: Option<PreparedServerHostname>,
server_ip: Option<IpAddress>,
}
impl ServerIdentityPolicy {
pub fn new(server_hostname: Option<&str>, server_ip: Option<&str>) -> Self {
Self {
server_hostname: server_hostname.and_then(PreparedServerHostname::new),
server_ip: server_ip.and_then(IpAddress::parse),
}
}
}
fn subject_alt_name_oid() -> Oid<'static> {
OID_X509_EXT_SUBJECT_ALT_NAME
}
impl ValidationPolicy for ServerIdentityPolicy {
fn verifying_critical_extensions(&self) -> Vec<Oid<'static>> {
vec![subject_alt_name_oid()]
}
fn chain_meets_policy_requirements(
&self,
chain: &UnverifiedCertificateChain<'_>,
) -> PolicyEvaluationResult {
has_valid_identity_for_service(
chain.leaf(),
self.server_hostname.as_ref(),
self.server_ip.as_ref(),
)
}
}
fn has_valid_identity_for_service(
leaf: &Certificate<'_>,
server_hostname: Option<&PreparedServerHostname>,
server_ip: Option<&IpAddress>,
) -> PolicyEvaluationResult {
let subject_alt_names = leaf
.tbs_certificate
.subject_alternative_name()
.map_err(|error| {
PolicyFailureReason::new(format!(
"error parsing SAN field, cert cannot be trusted: {}",
error
))
})?
.map(|ext| ext.value.general_names.clone())
.unwrap_or_default();
let mut checked_match = false;
for name in &subject_alt_names {
checked_match = true;
match name {
GeneralName::DNSName(value) => {
if match_hostname(server_hostname, value.as_bytes()) {
return Ok(());
}
}
GeneralName::IPAddress(value) => {
if let (Some(server_ip), Some(certificate_ip)) =
(server_ip, IpAddress::from_san_bytes(value))
&& match_ip_address(server_ip, &certificate_ip)
{
return Ok(());
}
}
_ => continue,
}
}
if checked_match {
return Err(PolicyFailureReason::new(
"none of the names in the SAN extension matched",
));
}
let Some(common_name) = leaf
.subject()
.iter_common_name()
.last()
.and_then(|cn| cn.as_str().ok())
else {
return Err(PolicyFailureReason::new(
"no SAN extension and no common name",
));
};
if match_hostname(server_hostname, common_name.as_bytes()) {
Ok(())
} else {
Err(PolicyFailureReason::new(
"common name does not match expected hostname",
))
}
}
fn match_hostname(server_hostname: Option<&PreparedServerHostname>, dns_name: &[u8]) -> bool {
let Some(server_hostname) = server_hostname else {
return false;
};
let Some(analysed) = AnalysedCertificateHostname::new(dns_name) else {
return false;
};
analysed.valid_match_for_name(server_hostname)
}
fn match_ip_address(server_ip: &IpAddress, certificate_ip: &IpAddress) -> bool {
server_ip == certificate_ip
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IpAddress {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
impl IpAddress {
fn parse(s: &str) -> Option<Self> {
if let Ok(v4) = s.parse::<Ipv4Addr>() {
return Some(Self::V4(v4));
}
if let Ok(v6) = s.parse::<Ipv6Addr>() {
return Some(Self::V6(v6));
}
None
}
fn from_san_bytes(bytes: &[u8]) -> Option<Self> {
match bytes.len() {
4 => {
let mut octets = [0u8; 4];
octets.copy_from_slice(bytes);
Some(Self::V4(Ipv4Addr::from(octets)))
}
16 => {
let mut octets = [0u8; 16];
octets.copy_from_slice(bytes);
Some(Self::V6(Ipv6Addr::from(octets)))
}
_ => None,
}
}
}
#[derive(Debug, Clone)]
struct PreparedServerHostname {
bytes: Vec<u8>,
first_period_index: Option<usize>,
}
impl PreparedServerHostname {
fn new(hostname: &str) -> Option<Self> {
let mut first_period_index = None;
let mut value = Vec::with_capacity(hostname.len());
for &byte in hostname.as_bytes() {
if !is_valid_dns_character(byte) {
return None;
}
if first_period_index.is_none() && byte == ASCII_PERIOD {
first_period_index = Some(value.len());
}
value.push(byte | 0x20);
}
if value.last() == Some(&ASCII_PERIOD) {
value.pop();
}
if first_period_index.is_some_and(|index| index >= value.len()) {
first_period_index = None;
}
Some(Self {
bytes: value,
first_period_index,
})
}
}
fn is_valid_dns_character(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'-' || byte == ASCII_PERIOD
}
fn split_around_index(bytes: &[u8], index: Option<usize>) -> (&[u8], &[u8]) {
match index {
None => (bytes, &bytes[bytes.len()..]),
Some(index) => (&bytes[..index], &bytes[index + 1..]),
}
}
fn case_insensitive_ascii_match(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter()
.zip(b.iter())
.all(|(&x, &y)| x.eq_ignore_ascii_case(&y))
}
enum AnalysedCertificateHostname<'a> {
SingleName(&'a [u8]),
Wildcard {
base_name: &'a [u8],
asterisk_index: usize,
first_period_index: Option<usize>,
},
}
impl<'a> AnalysedCertificateHostname<'a> {
fn new(base_name: &'a [u8]) -> Option<Self> {
let mut base_name = base_name;
if base_name.last() == Some(&ASCII_PERIOD) {
base_name = &base_name[..base_name.len() - 1];
}
let mut first_period_index = None;
let mut asterisk_index = None;
for (index, &byte) in base_name.iter().enumerate() {
match byte {
ASCII_PERIOD if first_period_index.is_none() => {
first_period_index = Some(index);
}
b if is_valid_dns_character(b) => {
}
ASCII_ASTERISK if asterisk_index.is_none() && first_period_index.is_none() => {
asterisk_index = Some(index);
}
ASCII_ASTERISK => {
return None;
}
_ => {
return None;
}
}
}
if let Some(asterisk_index) = asterisk_index {
let prefix_len = base_name.len().min(4);
if case_insensitive_ascii_match(
&base_name[..prefix_len],
&ASCII_IDNA_IDENTIFIER[..prefix_len],
) {
return None;
}
Some(AnalysedCertificateHostname::Wildcard {
base_name,
asterisk_index,
first_period_index,
})
} else {
Some(AnalysedCertificateHostname::SingleName(base_name))
}
}
fn valid_match_for_name(&self, target: &PreparedServerHostname) -> bool {
match self {
AnalysedCertificateHostname::SingleName(base_name) => {
case_insensitive_ascii_match(base_name, &target.bytes)
}
AnalysedCertificateHostname::Wildcard {
base_name,
asterisk_index,
first_period_index,
} => {
let (wildcard_label, remaining_components) =
split_around_index(base_name, *first_period_index);
let (target_first_label, target_remaining_components) =
split_around_index(&target.bytes, target.first_period_index);
if !case_insensitive_ascii_match(remaining_components, target_remaining_components)
{
return false;
}
if target_first_label.len() < wildcard_label.len() {
return false;
}
let (wildcard_prefix, wildcard_suffix) =
split_around_index(wildcard_label, Some(*asterisk_index));
let target_before_wildcard = &target_first_label[..wildcard_prefix.len()];
let target_after_wildcard =
&target_first_label[target_first_label.len() - wildcard_suffix.len()..];
case_insensitive_ascii_match(target_before_wildcard, wildcard_prefix)
&& case_insensitive_ascii_match(target_after_wildcard, wildcard_suffix)
}
}
}
}