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}