Skip to main content

x509_validator/
server_identity_policy.rs

1use core::net::{Ipv4Addr, Ipv6Addr};
2
3use crate::der_parser::Oid;
4use crate::extensions::GeneralName;
5use crate::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME;
6use crate::policy::{PolicyEvaluationResult, ValidationPolicy};
7use crate::unverified_chain::UnverifiedCertificateChain;
8use crate::{Certificate, PolicyFailureReason};
9
10const ASCII_PERIOD: u8 = b'.';
11const ASCII_ASTERISK: u8 = b'*';
12const ASCII_IDNA_IDENTIFIER: &[u8] = b"xn--";
13
14/// A [`ValidationPolicy`] that checks whether the leaf certificate is authoritative
15/// for a given hostname or IP address.
16///
17/// This policy is most commonly used to validate the leaf certificate presented by a server
18/// during a TLS handshake.
19///
20/// This policy implements the logic for service validation as specified by
21/// RFC 6125 (<https://tools.ietf.org/search/rfc6125>), which loosely speaking
22/// defines the common algorithm used for validating that an X.509 certificate
23/// is valid for a given service
24pub struct ServerIdentityPolicy {
25    server_hostname: Option<PreparedServerHostname>,
26    server_ip: Option<IpAddress>,
27}
28
29impl ServerIdentityPolicy {
30    /// Constructs a new [`ServerIdentityPolicy`].
31    ///
32    /// - Parameters:
33    ///     - server_hostname: The hostname used to connect to the server.
34    ///     - server_ip: The IP address of the server, if known.
35    pub fn new(server_hostname: Option<&str>, server_ip: Option<&str>) -> Self {
36        Self {
37            server_hostname: server_hostname.and_then(PreparedServerHostname::new),
38            server_ip: server_ip.and_then(IpAddress::parse),
39        }
40    }
41}
42
43/// id-ce-subjectAltName, RFC 5280 ยง4.2.1.6: 2.5.29.17.
44fn subject_alt_name_oid() -> Oid<'static> {
45    OID_X509_EXT_SUBJECT_ALT_NAME
46}
47
48impl ValidationPolicy for ServerIdentityPolicy {
49    fn verifying_critical_extensions(&self) -> Vec<Oid<'static>> {
50        vec![subject_alt_name_oid()]
51    }
52
53    fn chain_meets_policy_requirements(
54        &self,
55        chain: &UnverifiedCertificateChain<'_>,
56    ) -> PolicyEvaluationResult {
57        // We only validate the leaf node in this policy.
58        has_valid_identity_for_service(
59            chain.leaf(),
60            self.server_hostname.as_ref(),
61            self.server_ip.as_ref(),
62        )
63    }
64}
65
66/// Validates that a given leaf certificate is valid for a service.
67///
68/// This function implements the logic for service validation as specified by
69/// RFC 6125 (<https://tools.ietf.org/search/rfc6125>), which loosely speaking
70/// defines the common algorithm used for validating that an X.509 certificate
71/// is valid for a given service
72///
73/// The algorithm we're implementing is specified in RFC 6125 Section 6 if you want to
74/// follow along at home.
75fn has_valid_identity_for_service(
76    leaf: &Certificate<'_>,
77    server_hostname: Option<&PreparedServerHostname>,
78    server_ip: Option<&IpAddress>,
79) -> PolicyEvaluationResult {
80    // We want to begin by checking the subjectAlternativeName fields. If there are any fields
81    // in there that we could validate against (either IP or hostname) we will validate against
82    // them, and then refuse to check the commonName field. If there are no SAN fields to
83    // validate against, we'll check commonName.
84    //
85    // If the SAN field is invalid and we can't parse it, we fail.
86    let subject_alt_names = leaf
87        .tbs_certificate
88        .subject_alternative_name()
89        .map_err(|error| {
90            PolicyFailureReason::new(format!(
91                "error parsing SAN field, cert cannot be trusted: {}",
92                error
93            ))
94        })?
95        .map(|ext| ext.value.general_names.clone())
96        .unwrap_or_default();
97
98    let mut checked_match = false;
99
100    for name in &subject_alt_names {
101        checked_match = true;
102
103        match name {
104            GeneralName::DNSName(value) => {
105                if match_hostname(server_hostname, value.as_bytes()) {
106                    return Ok(());
107                }
108            }
109            GeneralName::IPAddress(value) => {
110                if let (Some(server_ip), Some(certificate_ip)) =
111                    (server_ip, IpAddress::from_san_bytes(value))
112                    && match_ip_address(server_ip, &certificate_ip)
113                {
114                    return Ok(());
115                }
116            }
117            _ => continue,
118        }
119    }
120
121    if checked_match {
122        // We had some subject alternative names, but none matched. We failed here.
123        return Err(PolicyFailureReason::new(
124            "none of the names in the SAN extension matched",
125        ));
126    }
127
128    // In the absence of any matchable subjectAlternativeNames, we can fall back to checking
129    // the common name. This is a deprecated practice, and in a future release we should
130    // stop doing this.
131    //
132    // As distinguished names move from least significant to most significant, we actually
133    // want the _last_ CN value.
134    let Some(common_name) = leaf
135        .subject()
136        .iter_common_name()
137        .last()
138        .and_then(|cn| cn.as_str().ok())
139    else {
140        // No CN, no match.
141        return Err(PolicyFailureReason::new(
142            "no SAN extension and no common name",
143        ));
144    };
145
146    // We have a common name. Let's check it against the provided hostname. We never check
147    // the common name against the IP address.
148    if match_hostname(server_hostname, common_name.as_bytes()) {
149        Ok(())
150    } else {
151        Err(PolicyFailureReason::new(
152            "common name does not match expected hostname",
153        ))
154    }
155}
156
157fn match_hostname(server_hostname: Option<&PreparedServerHostname>, dns_name: &[u8]) -> bool {
158    let Some(server_hostname) = server_hostname else {
159        // No server hostname was provided, so we cannot match.
160        return false;
161    };
162
163    // Now we validate the cert hostname.
164    let Some(analysed) = AnalysedCertificateHostname::new(dns_name) else {
165        // This is a hostname we can't match, return false.
166        return false;
167    };
168
169    analysed.valid_match_for_name(server_hostname)
170}
171
172fn match_ip_address(server_ip: &IpAddress, certificate_ip: &IpAddress) -> bool {
173    // These match if the two underlying IP address structures match. Different protocol
174    // families are never a match.
175    server_ip == certificate_ip
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179enum IpAddress {
180    V4(Ipv4Addr),
181    V6(Ipv6Addr),
182}
183
184impl IpAddress {
185    fn parse(s: &str) -> Option<Self> {
186        if let Ok(v4) = s.parse::<Ipv4Addr>() {
187            return Some(Self::V4(v4));
188        }
189        if let Ok(v6) = s.parse::<Ipv6Addr>() {
190            return Some(Self::V6(v6));
191        }
192        None
193    }
194
195    /// Creates an [`IpAddress`] from the raw bytes of a subjectAltName iPAddress field:
196    /// 4 bytes for IPv4, 16 bytes for IPv6, anything else is not a usable address.
197    fn from_san_bytes(bytes: &[u8]) -> Option<Self> {
198        match bytes.len() {
199            4 => {
200                let mut octets = [0u8; 4];
201                octets.copy_from_slice(bytes);
202                Some(Self::V4(Ipv4Addr::from(octets)))
203            }
204            16 => {
205                let mut octets = [0u8; 16];
206                octets.copy_from_slice(bytes);
207                Some(Self::V6(Ipv6Addr::from(octets)))
208            }
209            _ => None,
210        }
211    }
212}
213
214/// Creates a [`PreparedServerHostname`].
215///
216/// This consists of a non-NULL-terminated sequence of ASCII bytes and the index of the
217/// first period in that hostname.
218///
219/// If the string this is called with contains non-ASCII code points, this constructor fails.
220///
221/// This constructor exists to avoid doing repeated loops over the string buffer.
222/// In a naive implementation we'd loop at least four times: once to lowercase
223/// the string, once to get a buffer pointer to a contiguous buffer, once
224/// to confirm the string is ASCII, and once to find the first period for matching wildcards.
225/// Here we can do that all in one loop.
226#[derive(Debug, Clone)]
227struct PreparedServerHostname {
228    bytes: Vec<u8>,
229    first_period_index: Option<usize>,
230}
231
232impl PreparedServerHostname {
233    fn new(hostname: &str) -> Option<Self> {
234        let mut first_period_index = None;
235        let mut value = Vec::with_capacity(hostname.len());
236
237        for &byte in hostname.as_bytes() {
238            if !is_valid_dns_character(byte) {
239                return None;
240            }
241
242            if first_period_index.is_none() && byte == ASCII_PERIOD {
243                first_period_index = Some(value.len());
244            }
245
246            // We know we have only ASCII printables, we can safely unconditionally set the 6 bit to 1 to lowercase.
247            value.push(byte | 0x20);
248        }
249
250        // Strip trailing period.
251        if value.last() == Some(&ASCII_PERIOD) {
252            value.pop();
253        }
254
255        // The index was recorded before the trailing period was stripped, so it may now point at
256        // or past the end: a hostname of "." leaves an empty buffer still claiming a period at 0.
257        // Splitting around an index that is no longer inside the buffer would panic, and a period
258        // that is no longer present is not a label separator, so drop it.
259        if first_period_index.is_some_and(|index| index >= value.len()) {
260            first_period_index = None;
261        }
262
263        Some(Self {
264            bytes: value,
265            first_period_index,
266        })
267    }
268}
269
270/// Whether this character is a valid DNS character, which is the ASCII
271/// letters, digits, the hyphen, and the period.
272fn is_valid_dns_character(byte: u8) -> bool {
273    byte.is_ascii_alphanumeric() || byte == b'-' || byte == ASCII_PERIOD
274}
275
276/// Splits a byte slice in two around a given index. This index may be `None`, in which case the split
277/// will occur around the end.
278fn split_around_index(bytes: &[u8], index: Option<usize>) -> (&[u8], &[u8]) {
279    match index {
280        None => (bytes, &bytes[bytes.len()..]),
281        Some(index) => (&bytes[..index], &bytes[index + 1..]),
282    }
283}
284
285fn case_insensitive_ascii_match(a: &[u8], b: &[u8]) -> bool {
286    if a.len() != b.len() {
287        return false;
288    }
289    a.iter()
290        .zip(b.iter())
291        .all(|(&x, &y)| x.eq_ignore_ascii_case(&y))
292}
293
294/// This type contains a certificate hostname that has been analysed and prepared for matching.
295///
296/// A certificate hostname that is valid for matching meets the following criteria:
297///
298/// 1. Contains only valid DNS characters, plus the ASCII asterisk.
299/// 2. Contains zero or one ASCII asterisks.
300/// 3. Any ASCII asterisk present must be in the first DNS label (i.e. before the first period).
301/// 4. If the first label contains an ASCII asterisk, it must not also be an IDN A label.
302///
303/// Answering these questions potentially relies on multiple searches through the hostname. That's not
304/// ideal: it'd be better to do a single search that both validates the domain name meets the criteria
305/// and that also records information needed to validate that the name matches the one we're searching for.
306/// That's what this type does.
307enum AnalysedCertificateHostname<'a> {
308    SingleName(&'a [u8]),
309    Wildcard {
310        base_name: &'a [u8],
311        asterisk_index: usize,
312        first_period_index: Option<usize>,
313    },
314}
315
316impl<'a> AnalysedCertificateHostname<'a> {
317    fn new(base_name: &'a [u8]) -> Option<Self> {
318        let mut base_name = base_name;
319
320        // First, strip a trailing period from this name.
321        if base_name.last() == Some(&ASCII_PERIOD) {
322            base_name = &base_name[..base_name.len() - 1];
323        }
324
325        // Ok, start looping.
326        let mut first_period_index = None;
327        let mut asterisk_index = None;
328
329        for (index, &byte) in base_name.iter().enumerate() {
330            match byte {
331                ASCII_PERIOD if first_period_index.is_none() => {
332                    // This is the first period we've seen, great. Future
333                    // periods will be ignored.
334                    first_period_index = Some(index);
335                }
336                b if is_valid_dns_character(b) => {
337                    // Valid character, no notes.
338                }
339                ASCII_ASTERISK if asterisk_index.is_none() && first_period_index.is_none() => {
340                    // Found an asterisk, it's the first one, and it precedes any periods.
341                    asterisk_index = Some(index);
342                }
343                ASCII_ASTERISK => {
344                    // An extra asterisk, or an asterisk after a period, is unacceptable.
345                    return None;
346                }
347                _ => {
348                    // Unacceptable character in the name.
349                    return None;
350                }
351            }
352        }
353
354        // Now we can finally initialize ourself.
355        if let Some(asterisk_index) = asterisk_index {
356            // One final check: if we found a wildcard, we need to confirm that the first label isn't an IDNA A label.
357            let prefix_len = base_name.len().min(4);
358            if case_insensitive_ascii_match(
359                &base_name[..prefix_len],
360                &ASCII_IDNA_IDENTIFIER[..prefix_len],
361            ) {
362                return None;
363            }
364
365            Some(AnalysedCertificateHostname::Wildcard {
366                base_name,
367                asterisk_index,
368                first_period_index,
369            })
370        } else {
371            Some(AnalysedCertificateHostname::SingleName(base_name))
372        }
373    }
374
375    /// Whether this parsed name is a valid match for the one passed in.
376    fn valid_match_for_name(&self, target: &PreparedServerHostname) -> bool {
377        match self {
378            // For non-wildcard names, we just do a straightforward comparison.
379            AnalysedCertificateHostname::SingleName(base_name) => {
380                case_insensitive_ascii_match(base_name, &target.bytes)
381            }
382
383            AnalysedCertificateHostname::Wildcard {
384                base_name,
385                asterisk_index,
386                first_period_index,
387            } => {
388                // The wildcard can appear more-or-less anywhere in the first label. The wildcard
389                // character itself can match any number of characters, though it must match at least
390                // one.
391                // The algorithm for this is simple: first, we split the two names on their first period to get their
392                // first label and their subsequent components. Second, we check that the subcomponents match a straightforward
393                // bytewise comparison: if that fails, we can avoid the expensive wildcard checking operation.
394                // Third, we split the wildcard label on the wildcard character, and and confirm that
395                // the characters *before* the wildcard are the prefix of the target first label, and that the
396                // characters *after* the wildcard are the suffix of the target first label. This works well because
397                // the empty string is a prefix and suffix of all strings.
398                let (wildcard_label, remaining_components) =
399                    split_around_index(base_name, *first_period_index);
400                let (target_first_label, target_remaining_components) =
401                    split_around_index(&target.bytes, target.first_period_index);
402
403                if !case_insensitive_ascii_match(remaining_components, target_remaining_components)
404                {
405                    // Wildcard is irrelevant, the remaining components don't match.
406                    return false;
407                }
408
409                if target_first_label.len() < wildcard_label.len() {
410                    // The target label cannot possibly match the wildcard.
411                    return false;
412                }
413
414                let (wildcard_prefix, wildcard_suffix) =
415                    split_around_index(wildcard_label, Some(*asterisk_index));
416                let target_before_wildcard = &target_first_label[..wildcard_prefix.len()];
417                let target_after_wildcard =
418                    &target_first_label[target_first_label.len() - wildcard_suffix.len()..];
419
420                case_insensitive_ascii_match(target_before_wildcard, wildcard_prefix)
421                    && case_insensitive_ascii_match(target_after_wildcard, wildcard_suffix)
422            }
423        }
424    }
425}