Skip to main content

xml_sec/xmldsig/
x509.rs

1//! X.509 certificate path and revocation validation.
2
3use std::{
4    collections::HashSet,
5    time::{SystemTime, UNIX_EPOCH},
6};
7
8use x509_parser::{
9    certificate::X509Certificate,
10    extensions::{GeneralName, NameConstraints, ParsedExtension},
11    prelude::FromDer,
12    revocation_list::CertificateRevocationList,
13    time::ASN1Time,
14    x509::AlgorithmIdentifier,
15};
16
17use super::{
18    X509DataInfo,
19    parse::{distinguished_name_within_subtree, distinguished_names_equal, x509_name_to_rfc4514},
20};
21use crate::{
22    policy::{DsaKeyPolicy, ExtendedKeyPurpose, RsaKeyPolicy},
23    provider::X509SignatureAlgorithm,
24};
25
26/// Inputs controlling X.509 certificate-chain validation.
27#[derive(Debug, Clone)]
28pub struct X509ChainOptions<'a> {
29    /// DER-encoded certificates accepted as trust anchors.
30    pub trusted_certs: &'a [Vec<u8>],
31    /// Time used for certificate, CRL, and revocation checks.
32    pub verification_time: SystemTime,
33    /// Maximum number of certificates in the validated path, including the anchor.
34    pub max_chain_depth: usize,
35    /// Whether parsed `<X509CRL>` entries are enforced.
36    pub check_crls: bool,
37    /// Purposes accepted when any path certificate carries ExtendedKeyUsage.
38    pub allowed_extended_key_usages: Option<&'a HashSet<ExtendedKeyPurpose>>,
39    /// RSA strength requirements for every issuer key used by the path.
40    pub rsa_keys: RsaKeyPolicy,
41    /// DSA strength requirements for every issuer key used by the path.
42    pub dsa_keys: DsaKeyPolicy,
43}
44
45/// Certificate-chain validation failure.
46#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum X509ChainError {
49    /// The selected cryptographic provider rejected path authentication.
50    #[error("cryptographic provider rejected X.509 authentication: {0}")]
51    Provider(#[from] crate::provider::ProviderError),
52    /// The configured path limit cannot contain a certificate.
53    #[error("maximum certificate chain depth must be greater than zero")]
54    InvalidDepth,
55    /// A certificate or CRL is malformed DER.
56    #[error("invalid {kind} DER: {message}")]
57    InvalidDer {
58        /// Object type being parsed.
59        kind: &'static str,
60        /// Parser diagnostic.
61        message: String,
62    },
63    /// A certificate repeats an extension OID, which RFC 5280 forbids.
64    #[error("certificate at chain position {position} repeats extension {oid}")]
65    DuplicateExtension {
66        /// Position of the malformed certificate in the candidate path.
67        position: usize,
68        /// Repeated extension object identifier.
69        oid: String,
70    },
71    /// The ordered embedded path cannot be completed to a configured anchor.
72    #[error("certificate chain does not terminate at a trusted certificate")]
73    UntrustedRoot,
74    /// The path contains more certificates than allowed.
75    #[error("certificate chain exceeds maximum depth of {0}")]
76    DepthExceeded(usize),
77    /// A certificate is outside its validity period.
78    #[error("certificate at chain position {0} is expired or not yet valid")]
79    CertificateNotValid(usize),
80    /// An issuer certificate is not authorized to issue certificates.
81    #[error("certificate at chain position {0} is not a CA")]
82    IssuerNotCa(usize),
83    /// A CA path-length constraint is violated.
84    #[error("certificate at chain position {position} exceeds path length constraint {limit}")]
85    PathLengthExceeded {
86        /// Position of the constraining CA certificate.
87        position: usize,
88        /// Maximum permitted subordinate CA count.
89        limit: u32,
90    },
91    /// A subordinate certificate is outside a CA's permitted name space.
92    #[error(
93        "certificate at chain position {position} violates name constraints from position {constraining_position}"
94    )]
95    NameConstraintViolation {
96        /// Position of the subordinate certificate.
97        position: usize,
98        /// Position of the CA carrying NameConstraints.
99        constraining_position: usize,
100    },
101    /// A critical certificate extension is not implemented by path validation.
102    #[error("certificate at chain position {position} has unsupported critical extension {oid}")]
103    UnsupportedCriticalExtension {
104        /// Position of the certificate in the validated path.
105        position: usize,
106        /// Extension object identifier.
107        oid: String,
108    },
109    /// NameConstraints is not a critical CA extension as required by RFC 5280.
110    #[error("certificate at chain position {position} has invalid NameConstraints placement")]
111    InvalidNameConstraints {
112        /// Position of the certificate carrying the invalid extension.
113        position: usize,
114    },
115    /// A certificate key usage extension forbids the required operation.
116    #[error("certificate at chain position {position} does not permit {required}")]
117    InvalidKeyUsage {
118        /// Position of the certificate in the validated path.
119        position: usize,
120        /// RFC 5280 key usage required for the operation.
121        required: &'static str,
122    },
123    /// A certificate signature does not verify under its issuer key.
124    #[error("certificate signature at chain position {0} is invalid or unsupported")]
125    InvalidSignature(usize),
126    /// An issuer key violates the active key-strength policy.
127    #[error("certificate issuer key at chain position {position} is rejected by policy: {source}")]
128    KeyPolicy {
129        /// Position of the issuer certificate in the candidate path.
130        position: usize,
131        /// Typed key-policy rejection.
132        source: crate::policy::PolicyViolation,
133    },
134    /// The certificate or CRL declares an algorithm this build cannot verify.
135    #[error("unsupported X.509 signature algorithm: {oid}")]
136    UnsupportedSignatureAlgorithm {
137        /// AlgorithmIdentifier object identifier.
138        oid: String,
139    },
140    /// A CRL is not valid for the selected verification time or issuer.
141    #[error("CRL {0} is invalid or cannot be authenticated")]
142    InvalidCrl(usize),
143    /// A path certificate was revoked by an applicable CRL.
144    #[error("certificate at chain position {0} is revoked")]
145    Revoked(usize),
146}
147
148/// Verify the ordered certificate path parsed from one `<X509Data>` element.
149pub fn verify_x509_certificate_chain(
150    info: &X509DataInfo,
151    options: &X509ChainOptions<'_>,
152) -> Result<(), X509ChainError> {
153    verify_x509_certificate_chain_with_provider(info, options, crate::provider::default_provider())
154}
155
156pub(crate) fn verify_x509_certificate_chain_with_provider(
157    info: &X509DataInfo,
158    options: &X509ChainOptions<'_>,
159    provider: &dyn crate::provider::CryptoProvider,
160) -> Result<(), X509ChainError> {
161    if options.max_chain_depth == 0 {
162        return Err(X509ChainError::InvalidDepth);
163    }
164    if info.certificate_chain.is_empty() {
165        return Err(X509ChainError::UntrustedRoot);
166    }
167
168    let path_der = info
169        .certificate_chain
170        .iter()
171        .map(|&idx| {
172            info.certificates
173                .get(idx)
174                .map(Vec::as_slice)
175                .ok_or(X509ChainError::UntrustedRoot)
176        })
177        .collect::<Result<Vec<_>, _>>()?;
178
179    let last = parse_certificate(
180        path_der
181            .last()
182            .copied()
183            .ok_or(X509ChainError::UntrustedRoot)?,
184    )?;
185    let trusted_anchors = options
186        .trusted_certs
187        .iter()
188        .map(|der| parse_certificate(der).map(|cert| (der.as_slice(), cert)))
189        .collect::<Result<Vec<_>, _>>()?;
190    let verification_time = system_time_to_asn1(options.verification_time)?;
191    let embedded_anchor = trusted_anchors.iter().any(|(der, _)| *der == last.as_raw());
192    if embedded_anchor {
193        return validate_path(&path_der, info, options, verification_time, provider);
194    }
195
196    // Use the path-edge verifier here too: x509-parser does not verify legacy
197    // DSA-SHA1 roots, while our fallback must recognize them for rollover.
198    let replace_untrusted_root = if path_der.len() > 1
199        && certificate_names_equal(last.subject(), last.issuer())
200        && verify_certificate_signature_with_provider(&last, &last, provider)?
201    {
202        let child = parse_certificate(path_der[path_der.len() - 2])?;
203        certificate_names_equal(child.issuer(), last.subject())
204            && verify_certificate_signature_with_provider(&child, &last, provider)?
205    } else {
206        false
207    };
208    let candidate_base = if replace_untrusted_root {
209        &path_der[..path_der.len() - 1]
210    } else {
211        path_der.as_slice()
212    };
213    let candidate_child = parse_certificate(
214        candidate_base
215            .last()
216            .copied()
217            .ok_or(X509ChainError::UntrustedRoot)?,
218    )?;
219
220    let mut first_validation_error = None;
221    for (anchor_der, cert) in &trusted_anchors {
222        if !certificate_names_equal(cert.subject(), candidate_child.issuer())
223            || !verify_certificate_signature_with_provider(&candidate_child, cert, provider)?
224        {
225            continue;
226        }
227        let mut candidate_path = candidate_base.to_vec();
228        candidate_path.push(anchor_der);
229        match validate_path(&candidate_path, info, options, verification_time, provider) {
230            Ok(()) => return Ok(()),
231            Err(error) => first_validation_error.get_or_insert(error),
232        };
233    }
234
235    Err(first_validation_error.unwrap_or(X509ChainError::UntrustedRoot))
236}
237
238fn validate_path(
239    path_der: &[&[u8]],
240    info: &X509DataInfo,
241    options: &X509ChainOptions<'_>,
242    verification_time: ASN1Time,
243    provider: &dyn crate::provider::CryptoProvider,
244) -> Result<(), X509ChainError> {
245    if path_der.len() > options.max_chain_depth {
246        return Err(X509ChainError::DepthExceeded(options.max_chain_depth));
247    }
248
249    let path = path_der
250        .iter()
251        .map(|der| parse_certificate(der))
252        .collect::<Result<Vec<_>, _>>()?;
253    let mut effective_extended_key_usages = options.allowed_extended_key_usages.cloned();
254
255    for (position, cert) in path.iter().enumerate() {
256        validate_certificate_serial(cert)?;
257        validate_unique_extensions(cert, position)?;
258        if !cert.validity().is_valid_at(verification_time) {
259            return Err(X509ChainError::CertificateNotValid(position));
260        }
261        if position == 0 {
262            validate_leaf_key_usage(cert)?;
263        } else {
264            validate_ca_constraints(cert, position)?;
265        }
266        validate_extended_key_usage(cert, position, &mut effective_extended_key_usages)?;
267        validate_subject_identity(cert)?;
268        validate_critical_extensions(cert, position)?;
269    }
270    validate_path_length_constraints(&path)?;
271    validate_name_constraints(&path)?;
272
273    for (position, pair) in path.windows(2).enumerate() {
274        let [child, issuer] = pair else {
275            unreachable!()
276        };
277        validate_issuer_key_policy(issuer, position + 1, options.rsa_keys, options.dsa_keys)?;
278        if !certificate_names_equal(child.issuer(), issuer.subject())
279            || !verify_certificate_signature_with_provider(child, issuer, provider)?
280        {
281            return Err(X509ChainError::InvalidSignature(position));
282        }
283    }
284
285    if options.check_crls {
286        verify_crls(&path, &info.crls, verification_time, provider)?;
287    }
288    Ok(())
289}
290
291fn validate_issuer_key_policy(
292    issuer: &X509Certificate<'_>,
293    position: usize,
294    rsa_keys: RsaKeyPolicy,
295    dsa_keys: DsaKeyPolicy,
296) -> Result<(), X509ChainError> {
297    match issuer.public_key().parsed() {
298        Ok(x509_parser::public_key::PublicKey::RSA(key)) => rsa_keys
299            .validate_components("X.509 issuer verification", key.modulus, key.exponent)
300            .map(|_| ())
301            .map_err(|source| X509ChainError::KeyPolicy { position, source }),
302        Ok(x509_parser::public_key::PublicKey::DSA(_)) => {
303            match super::signature::validate_dsa_signature_spki_with_minimum(
304                issuer.public_key().raw,
305                dsa_keys.minimum_modulus_bits,
306            ) {
307                Ok(()) => Ok(()),
308                Err(super::SignatureVerificationError::KeyPolicy(source)) => {
309                    Err(X509ChainError::KeyPolicy { position, source })
310                }
311                Err(_) => Err(X509ChainError::InvalidDer {
312                    kind: "DSA issuer SubjectPublicKeyInfo",
313                    message: "invalid DSA key parameters".into(),
314                }),
315            }
316        }
317        Ok(_) => Ok(()),
318        Err(error) => Err(X509ChainError::InvalidDer {
319            kind: "issuer SubjectPublicKeyInfo",
320            message: error.to_string(),
321        }),
322    }
323}
324
325fn validate_certificate_serial(cert: &X509Certificate<'_>) -> Result<(), X509ChainError> {
326    validate_positive_serial_bytes(cert.raw_serial(), "certificate serial number")
327}
328
329fn validate_positive_serial_bytes(serial: &[u8], kind: &'static str) -> Result<(), X509ChainError> {
330    let magnitude = serial.strip_prefix(&[0]).unwrap_or(serial);
331    if serial.is_empty()
332        || serial[0] & 0x80 != 0
333        || magnitude.is_empty()
334        || magnitude.len() > 20
335        || magnitude.iter().all(|byte| *byte == 0)
336    {
337        return Err(X509ChainError::InvalidDer {
338            kind,
339            message: "RFC 5280 requires a positive, non-zero value of at most 20 octets".into(),
340        });
341    }
342    Ok(())
343}
344
345fn validate_unique_extensions(
346    cert: &X509Certificate<'_>,
347    position: usize,
348) -> Result<(), X509ChainError> {
349    let mut seen = std::collections::HashSet::with_capacity(cert.extensions().len());
350    for extension in cert.extensions() {
351        let oid = extension.oid.to_id_string();
352        if !seen.insert(oid.clone()) {
353            return Err(X509ChainError::DuplicateExtension { position, oid });
354        }
355    }
356    Ok(())
357}
358
359fn validate_subject_identity(cert: &X509Certificate<'_>) -> Result<(), X509ChainError> {
360    for attribute in cert.subject().iter_email() {
361        let email = attribute
362            .as_str()
363            .map_err(|error| X509ChainError::InvalidDer {
364                kind: "certificate subject emailAddress",
365                message: error.to_string(),
366            })?;
367        if !mailbox_has_valid_syntax(email) {
368            return Err(X509ChainError::InvalidDer {
369                kind: "certificate subject emailAddress",
370                message: format!("invalid RFC 5280 mailbox syntax: {email:?}"),
371            });
372        }
373    }
374
375    let subject_is_empty = cert.subject().iter().next().is_none();
376    let mut san_extensions = cert
377        .extensions()
378        .iter()
379        .filter(|extension| extension.oid.to_id_string() == "2.5.29.17");
380    let Some(extension) = san_extensions.next() else {
381        return if subject_is_empty {
382            Err(invalid_subject_identity(
383                "an empty subject requires a critical SubjectAlternativeName",
384            ))
385        } else {
386            Ok(())
387        };
388    };
389    if san_extensions.next().is_some() {
390        return Err(invalid_subject_identity(
391            "an empty subject must not contain duplicate SubjectAlternativeName extensions",
392        ));
393    }
394    let ParsedExtension::SubjectAlternativeName(names) = extension.parsed_extension() else {
395        return Err(invalid_subject_identity(
396            "SubjectAlternativeName could not be parsed",
397        ));
398    };
399    for name in &names.general_names {
400        validate_subject_alternative_name(name)?;
401    }
402    if subject_is_empty && (!extension.critical || names.general_names.is_empty()) {
403        return Err(invalid_subject_identity(
404            "an empty subject requires a critical, non-empty SubjectAlternativeName",
405        ));
406    }
407    Ok(())
408}
409
410fn validate_subject_alternative_name(name: &GeneralName<'_>) -> Result<(), X509ChainError> {
411    match name {
412        GeneralName::RFC822Name(value) => validate_rfc5280_mailbox(value),
413        GeneralName::DNSName(value) => {
414            // RFC 5280 section 4.2.1.6 requires RFC 1034/1123 preferred-name
415            // syntax here. RFC 9525 wildcard matching is an application-level
416            // TLS identity rule, not certificate-path profile validation.
417            validate_rfc5280_dns_name(value)
418        }
419        GeneralName::URI(value) => validate_rfc5280_uri(value),
420        GeneralName::IPAddress(value) if !matches!(value.len(), 4 | 16) => {
421            Err(invalid_subject_identity(
422                "SubjectAlternativeName iPAddress must contain 4 or 16 octets",
423            ))
424        }
425        GeneralName::Invalid(..) => Err(invalid_subject_identity(
426            "SubjectAlternativeName contains a malformed GeneralName",
427        )),
428        _ => Ok(()),
429    }
430}
431
432fn validate_rfc5280_mailbox(value: &str) -> Result<(), X509ChainError> {
433    if !mailbox_has_valid_syntax(value) {
434        return Err(invalid_subject_identity(
435            "SubjectAlternativeName rfc822Name has invalid RFC 5280 mailbox syntax",
436        ));
437    }
438    Ok(())
439}
440
441fn mailbox_has_valid_syntax(value: &str) -> bool {
442    value.rsplit_once('@').is_some_and(|(local, domain)| {
443        mailbox_local_part_has_valid_syntax(local) && mailbox_domain_has_valid_syntax(domain)
444    })
445}
446
447fn mailbox_domain_has_valid_syntax(domain: &str) -> bool {
448    let Some(literal) = domain
449        .strip_prefix('[')
450        .and_then(|value| value.strip_suffix(']'))
451    else {
452        return dns_name_has_valid_syntax(domain, false);
453    };
454    if literal
455        .get(..5)
456        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("IPv6:"))
457    {
458        literal[5..].parse::<std::net::Ipv6Addr>().is_ok()
459    } else {
460        literal.parse::<std::net::Ipv4Addr>().is_ok()
461    }
462}
463
464fn mailbox_local_part_has_valid_syntax(local: &str) -> bool {
465    if let Some(quoted) = local
466        .strip_prefix('"')
467        .and_then(|value| value.strip_suffix('"'))
468    {
469        if quoted.is_empty() {
470            return false;
471        }
472        let mut escaped = false;
473        for byte in quoted.bytes() {
474            if escaped {
475                if !(0x20..=0x7e).contains(&byte) {
476                    return false;
477                }
478                escaped = false;
479            } else if byte == b'\\' {
480                escaped = true;
481            } else if byte == b'"' || !(0x20..=0x7e).contains(&byte) {
482                return false;
483            }
484        }
485        return !escaped;
486    }
487
488    local.split('.').all(|atom| {
489        !atom.is_empty()
490            && atom.bytes().all(|byte| {
491                byte.is_ascii_alphanumeric()
492                    || matches!(
493                        byte,
494                        b'!' | b'#'
495                            | b'$'
496                            | b'%'
497                            | b'&'
498                            | b'\''
499                            | b'*'
500                            | b'+'
501                            | b'-'
502                            | b'/'
503                            | b'='
504                            | b'?'
505                            | b'^'
506                            | b'_'
507                            | b'`'
508                            | b'{'
509                            | b'|'
510                            | b'}'
511                            | b'~'
512                    )
513            })
514    })
515}
516
517fn validate_rfc5280_uri(value: &str) -> Result<(), X509ChainError> {
518    let Some((scheme, scheme_specific)) = value.split_once(':') else {
519        return Err(invalid_subject_identity(
520            "SubjectAlternativeName URI must be absolute",
521        ));
522    };
523    if scheme.is_empty()
524        || !scheme.as_bytes()[0].is_ascii_alphabetic()
525        || !scheme
526            .bytes()
527            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
528        || scheme_specific.is_empty()
529        || !uri_scheme_specific_part_has_valid_syntax(scheme_specific)
530    {
531        return Err(invalid_subject_identity(
532            "SubjectAlternativeName URI has invalid RFC 3986 syntax",
533        ));
534    }
535
536    if let Some(authority_and_path) = scheme_specific.strip_prefix("//") {
537        let authority = authority_and_path
538            .split(['/', '?', '#'])
539            .next()
540            .unwrap_or_default();
541        if !uri_authority_has_rfc5280_host(authority) {
542            return Err(invalid_subject_identity(
543                "SubjectAlternativeName URI authority requires a fully qualified host",
544            ));
545        }
546    }
547    Ok(())
548}
549
550fn uri_scheme_specific_part_has_valid_syntax(value: &str) -> bool {
551    if value.bytes().any(|byte| {
552        !byte.is_ascii()
553            || byte.is_ascii_control()
554            || byte == b' '
555            || !matches!(
556                byte,
557                b'A'..=b'Z'
558                    | b'a'..=b'z'
559                    | b'0'..=b'9'
560                    | b'-'
561                    | b'.'
562                    | b'_'
563                    | b'~'
564                    | b':'
565                    | b'/'
566                    | b'?'
567                    | b'#'
568                    | b'['
569                    | b']'
570                    | b'@'
571                    | b'!'
572                    | b'$'
573                    | b'&'
574                    | b'\''
575                    | b'('
576                    | b')'
577                    | b'*'
578                    | b'+'
579                    | b','
580                    | b';'
581                    | b'='
582                    | b'%'
583            )
584    }) || value.matches('#').count() > 1
585    {
586        return false;
587    }
588
589    let bytes = value.as_bytes();
590    let mut index = 0;
591    while index < bytes.len() {
592        if bytes[index] == b'%'
593            && (index + 2 >= bytes.len()
594                || !bytes[index + 1].is_ascii_hexdigit()
595                || !bytes[index + 2].is_ascii_hexdigit())
596        {
597            return false;
598        }
599        index += if bytes[index] == b'%' { 3 } else { 1 };
600    }
601    true
602}
603
604fn uri_authority_has_rfc5280_host(authority: &str) -> bool {
605    parse_uri_authority_host(authority).is_some()
606}
607
608#[derive(Clone, Copy)]
609enum UriAuthorityHost<'a> {
610    Dns(&'a str),
611    Ip,
612}
613
614fn parse_uri_authority_host(authority: &str) -> Option<UriAuthorityHost<'_>> {
615    let host_port = match authority.split_once('@') {
616        Some((userinfo, host_port))
617            if !host_port.contains('@') && uri_userinfo_has_valid_syntax(userinfo) =>
618        {
619            host_port
620        }
621        Some(_) => return None,
622        None => authority,
623    };
624    if let Some(bracketed) = host_port.strip_prefix('[') {
625        let (host, port) = bracketed.split_once(']')?;
626        return (host.parse::<std::net::Ipv6Addr>().is_ok() && uri_port_has_valid_syntax(port))
627            .then_some(UriAuthorityHost::Ip);
628    }
629    let (host, port) = host_port
630        .split_once(':')
631        .map_or((host_port, None), |(host, port)| (host, Some(port)));
632    if port.is_some_and(|port| port.is_empty() || !port.bytes().all(|byte| byte.is_ascii_digit())) {
633        return None;
634    }
635    if host.parse::<std::net::Ipv4Addr>().is_ok() {
636        Some(UriAuthorityHost::Ip)
637    } else if dns_name_has_valid_syntax(host, false) {
638        Some(UriAuthorityHost::Dns(host))
639    } else {
640        None
641    }
642}
643
644fn uri_port_has_valid_syntax(suffix: &str) -> bool {
645    suffix.is_empty()
646        || suffix
647            .strip_prefix(':')
648            .is_some_and(|port| !port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit()))
649}
650
651fn uri_userinfo_has_valid_syntax(userinfo: &str) -> bool {
652    let bytes = userinfo.as_bytes();
653    let mut index = 0;
654    while index < bytes.len() {
655        let byte = bytes[index];
656        if byte == b'%' {
657            if index + 2 >= bytes.len()
658                || !bytes[index + 1].is_ascii_hexdigit()
659                || !bytes[index + 2].is_ascii_hexdigit()
660            {
661                return false;
662            }
663            index += 3;
664            continue;
665        }
666        if !(byte.is_ascii_alphanumeric()
667            || matches!(
668                byte,
669                b'-' | b'.'
670                    | b'_'
671                    | b'~'
672                    | b'!'
673                    | b'$'
674                    | b'&'
675                    | b'\''
676                    | b'('
677                    | b')'
678                    | b'*'
679                    | b'+'
680                    | b','
681                    | b';'
682                    | b'='
683                    | b':'
684            ))
685        {
686            return false;
687        }
688        index += 1;
689    }
690    true
691}
692
693fn invalid_subject_identity(message: &str) -> X509ChainError {
694    X509ChainError::InvalidDer {
695        kind: "certificate subject identity",
696        message: message.into(),
697    }
698}
699
700#[cfg(test)]
701fn verify_certificate_signature(
702    certificate: &X509Certificate<'_>,
703    issuer: &X509Certificate<'_>,
704) -> bool {
705    verify_certificate_signature_with_provider(
706        certificate,
707        issuer,
708        crate::provider::default_provider(),
709    )
710    .unwrap_or(false)
711}
712
713fn verify_certificate_signature_with_provider(
714    certificate: &X509Certificate<'_>,
715    issuer: &X509Certificate<'_>,
716    provider: &dyn crate::provider::CryptoProvider,
717) -> Result<bool, X509ChainError> {
718    // RFC 5280 sections 4.1.1.2 and 4.1.2.3 require the outer and signed
719    // AlgorithmIdentifier values to be identical. Enforce this independently
720    // of the backend so the legacy DSA path cannot bypass the invariant.
721    verify_x509_signed_object_with_provider(
722        &certificate.signature_algorithm,
723        &certificate.tbs_certificate.signature,
724        &certificate.signature_value.data,
725        certificate.tbs_certificate.as_ref(),
726        issuer.public_key().raw,
727        provider,
728    )
729}
730
731/// Test a candidate certificate-path edge without assigning trust to either
732/// certificate. Path construction uses this only to distinguish certificates
733/// that share an issuer subject name; full policy validation still happens
734/// after the complete path has been assembled.
735#[cfg(test)]
736pub(crate) fn certificate_signature_matches(certificate_der: &[u8], issuer_der: &[u8]) -> bool {
737    certificate_signature_matches_with_provider(
738        certificate_der,
739        issuer_der,
740        crate::provider::default_provider(),
741    )
742    .unwrap_or(false)
743}
744
745pub(crate) fn certificate_signature_matches_with_provider(
746    certificate_der: &[u8],
747    issuer_der: &[u8],
748    provider: &dyn crate::provider::CryptoProvider,
749) -> Result<bool, X509ChainError> {
750    let (Ok(certificate), Ok(issuer)) = (
751        parse_certificate(certificate_der),
752        parse_certificate(issuer_der),
753    ) else {
754        return Ok(false);
755    };
756    verify_certificate_signature_with_provider(&certificate, &issuer, provider)
757}
758
759fn certificate_names_equal(
760    left: &x509_parser::x509::X509Name<'_>,
761    right: &x509_parser::x509::X509Name<'_>,
762) -> bool {
763    let (Ok(left), Ok(right)) = (x509_name_to_rfc4514(left), x509_name_to_rfc4514(right)) else {
764        return false;
765    };
766    distinguished_names_equal(&left, &right)
767}
768
769#[cfg(test)]
770fn verify_crl_signature(crl: &CertificateRevocationList<'_>, issuer: &X509Certificate<'_>) -> bool {
771    verify_crl_signature_with_provider(crl, issuer, crate::provider::default_provider())
772        .unwrap_or(false)
773}
774
775fn verify_crl_signature_with_provider(
776    crl: &CertificateRevocationList<'_>,
777    issuer: &X509Certificate<'_>,
778    provider: &dyn crate::provider::CryptoProvider,
779) -> Result<bool, X509ChainError> {
780    // RFC 5280 sections 5.1.1.2 and 5.1.2.2 impose the same equality rule on
781    // CRLs as certificates.
782    verify_x509_signed_object_with_provider(
783        &crl.signature_algorithm,
784        &crl.tbs_cert_list.signature,
785        &crl.signature_value.data,
786        crl.tbs_cert_list.as_ref(),
787        issuer.public_key().raw,
788        provider,
789    )
790}
791
792fn verify_x509_signed_object_with_provider(
793    outer_algorithm: &AlgorithmIdentifier<'_>,
794    signed_algorithm: &AlgorithmIdentifier<'_>,
795    signature_der: &[u8],
796    signed_data: &[u8],
797    issuer_spki_der: &[u8],
798    provider: &dyn crate::provider::CryptoProvider,
799) -> Result<bool, X509ChainError> {
800    if outer_algorithm != signed_algorithm {
801        return Ok(false);
802    }
803    verify_x509_signature_with_provider(
804        outer_algorithm,
805        signature_der,
806        signed_data,
807        issuer_spki_der,
808        provider,
809    )
810}
811
812fn verify_x509_signature_with_provider(
813    algorithm_identifier: &AlgorithmIdentifier<'_>,
814    signature_der: &[u8],
815    signed_data: &[u8],
816    issuer_spki_der: &[u8],
817    provider: &dyn crate::provider::CryptoProvider,
818) -> Result<bool, X509ChainError> {
819    let algorithm = x509_signature_algorithm(algorithm_identifier)?;
820    provider
821        .require_capability(crate::provider::ProviderCapability::VerifyCertificate(
822            algorithm,
823        ))
824        .map_err(X509ChainError::from)?;
825    provider
826        .verify_x509_signature(algorithm, signed_data, signature_der, issuer_spki_der)
827        .map_err(Into::into)
828}
829
830fn x509_signature_algorithm(
831    identifier: &AlgorithmIdentifier<'_>,
832) -> Result<X509SignatureAlgorithm, X509ChainError> {
833    let oid = identifier.algorithm.to_id_string();
834    let algorithm = match oid.as_str() {
835        "1.2.840.10040.4.3" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha1),
836        "2.16.840.1.101.3.4.3.1" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha224),
837        "2.16.840.1.101.3.4.3.2" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha256),
838        "2.16.840.1.101.3.4.3.3" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha384),
839        "2.16.840.1.101.3.4.3.4" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha512),
840        "1.2.840.113549.1.1.5" | "1.3.14.3.2.29" => {
841            X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha1)
842        }
843        "1.2.840.113549.1.1.14" => {
844            X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha224)
845        }
846        "1.2.840.113549.1.1.11" => {
847            X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha256)
848        }
849        "1.2.840.113549.1.1.12" => {
850            X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha384)
851        }
852        "1.2.840.113549.1.1.13" => {
853            X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha512)
854        }
855        "1.2.840.113549.1.1.10" => parse_rsa_pss_algorithm(identifier)?,
856        "1.2.840.10045.4.1" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha1),
857        "1.2.840.10045.4.3.1" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha224),
858        "1.2.840.10045.4.3.2" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha256),
859        "1.2.840.10045.4.3.3" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha384),
860        "1.2.840.10045.4.3.4" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha512),
861        "1.3.101.112" => X509SignatureAlgorithm::Ed25519,
862        _ => return Err(X509ChainError::UnsupportedSignatureAlgorithm { oid }),
863    };
864    match &algorithm {
865        X509SignatureAlgorithm::Dsa(_)
866        | X509SignatureAlgorithm::Ecdsa(_)
867        | X509SignatureAlgorithm::Ed25519 => require_absent_signature_parameters(identifier)?,
868        X509SignatureAlgorithm::RsaPkcs1v15(_) => {
869            require_null_or_absent_signature_parameters(identifier)?;
870        }
871        X509SignatureAlgorithm::RsaPss { .. } => {}
872    }
873    Ok(algorithm)
874}
875
876fn require_absent_signature_parameters(
877    identifier: &AlgorithmIdentifier<'_>,
878) -> Result<(), X509ChainError> {
879    if identifier.parameters.is_some() {
880        return Err(invalid_signature_parameters(
881            identifier,
882            "parameters must be absent",
883        ));
884    }
885    Ok(())
886}
887
888fn require_null_or_absent_signature_parameters(
889    identifier: &AlgorithmIdentifier<'_>,
890) -> Result<(), X509ChainError> {
891    if identifier
892        .parameters
893        .as_ref()
894        .is_some_and(|parameters| parameters.tag() != x509_parser::asn1_rs::Tag::Null)
895    {
896        return Err(invalid_signature_parameters(
897            identifier,
898            "parameters must be NULL or absent",
899        ));
900    }
901    Ok(())
902}
903
904fn invalid_signature_parameters(
905    identifier: &AlgorithmIdentifier<'_>,
906    requirement: &str,
907) -> X509ChainError {
908    X509ChainError::InvalidDer {
909        kind: "X.509 signature AlgorithmIdentifier parameters",
910        message: format!("{}: {requirement}", identifier.algorithm),
911    }
912}
913
914fn parse_rsa_pss_algorithm(
915    identifier: &AlgorithmIdentifier<'_>,
916) -> Result<X509SignatureAlgorithm, X509ChainError> {
917    let parameters = identifier
918        .parameters
919        .as_ref()
920        .ok_or_else(|| X509ChainError::InvalidDer {
921            kind: "RSASSA-PSS parameters",
922            message: "missing parameters".into(),
923        })?;
924    let parameters = x509_parser::signature_algorithm::RsaSsaPssParams::try_from(parameters)
925        .map_err(|error| X509ChainError::InvalidDer {
926            kind: "RSASSA-PSS parameters",
927            message: error.to_string(),
928        })?;
929    if parameters.trailer_field() != 1 {
930        return Err(X509ChainError::InvalidDer {
931            kind: "RSASSA-PSS parameters",
932            message: "trailerField must be 1".into(),
933        });
934    }
935    let digest = x509_digest_algorithm(&parameters.hash_algorithm_oid().to_id_string())?;
936    let mask = parameters
937        .mask_gen_algorithm()
938        .map_err(|error| X509ChainError::InvalidDer {
939            kind: "RSASSA-PSS parameters",
940            message: error.to_string(),
941        })?;
942    if mask.mgf.to_id_string() != "1.2.840.113549.1.1.8" {
943        return Err(X509ChainError::UnsupportedSignatureAlgorithm {
944            oid: mask.mgf.to_id_string(),
945        });
946    }
947    let mgf_digest = x509_digest_algorithm(&mask.hash.to_id_string())?;
948    let salt_len =
949        usize::try_from(parameters.salt_length()).map_err(|_| X509ChainError::InvalidDer {
950            kind: "RSASSA-PSS parameters",
951            message: "saltLength does not fit this platform".into(),
952        })?;
953    Ok(X509SignatureAlgorithm::RsaPss {
954        digest,
955        mgf_digest,
956        salt_len,
957    })
958}
959
960fn x509_digest_algorithm(oid: &str) -> Result<super::DigestAlgorithm, X509ChainError> {
961    match oid {
962        "1.3.14.3.2.26" => Ok(super::DigestAlgorithm::Sha1),
963        "2.16.840.1.101.3.4.2.4" => Ok(super::DigestAlgorithm::Sha224),
964        "2.16.840.1.101.3.4.2.1" => Ok(super::DigestAlgorithm::Sha256),
965        "2.16.840.1.101.3.4.2.2" => Ok(super::DigestAlgorithm::Sha384),
966        "2.16.840.1.101.3.4.2.3" => Ok(super::DigestAlgorithm::Sha512),
967        _ => Err(X509ChainError::UnsupportedSignatureAlgorithm {
968            oid: oid.to_owned(),
969        }),
970    }
971}
972
973fn validate_leaf_key_usage(cert: &X509Certificate<'_>) -> Result<(), X509ChainError> {
974    // RFC 5280 section 4.2.1.3 restricts key purpose only when KeyUsage is present.
975    if cert
976        .key_usage()
977        .map_err(|error| X509ChainError::InvalidDer {
978            kind: "certificate KeyUsage",
979            message: error.to_string(),
980        })?
981        .is_some_and(|usage| !usage.value.digital_signature() && !usage.value.non_repudiation())
982    {
983        return Err(X509ChainError::InvalidKeyUsage {
984            position: 0,
985            required: "digitalSignature or nonRepudiation",
986        });
987    }
988    Ok(())
989}
990
991fn validate_extended_key_usage(
992    cert: &X509Certificate<'_>,
993    position: usize,
994    effective_extended_key_usages: &mut Option<HashSet<ExtendedKeyPurpose>>,
995) -> Result<(), X509ChainError> {
996    let Some(usage) = cert
997        .extended_key_usage()
998        .map_err(|error| X509ChainError::InvalidDer {
999            kind: "certificate ExtendedKeyUsage",
1000            message: error.to_string(),
1001        })?
1002    else {
1003        return Ok(());
1004    };
1005    if usage.value.any {
1006        return Ok(());
1007    }
1008    if let Some(effective) = effective_extended_key_usages {
1009        effective.retain(|purpose| extended_key_usage_contains(usage.value, purpose));
1010        if !effective.is_empty() {
1011            return Ok(());
1012        }
1013    }
1014    Err(X509ChainError::InvalidKeyUsage {
1015        position,
1016        required: "an approved extended key usage",
1017    })
1018}
1019
1020fn extended_key_usage_contains(
1021    usage: &x509_parser::extensions::ExtendedKeyUsage<'_>,
1022    purpose: &ExtendedKeyPurpose,
1023) -> bool {
1024    match purpose {
1025        ExtendedKeyPurpose::ServerAuth => usage.server_auth,
1026        ExtendedKeyPurpose::ClientAuth => usage.client_auth,
1027        ExtendedKeyPurpose::CodeSigning => usage.code_signing,
1028        ExtendedKeyPurpose::EmailProtection => usage.email_protection,
1029        ExtendedKeyPurpose::TimeStamping => usage.time_stamping,
1030        ExtendedKeyPurpose::OcspSigning => usage.ocsp_signing,
1031        ExtendedKeyPurpose::Other(arcs) => usage.other.iter().any(|oid| {
1032            let oid = oid.to_id_string();
1033            arcs.iter()
1034                .map(u64::to_string)
1035                .collect::<Vec<_>>()
1036                .join(".")
1037                == oid
1038        }),
1039    }
1040}
1041
1042fn parse_certificate(der: &[u8]) -> Result<X509Certificate<'_>, X509ChainError> {
1043    let (rest, cert) =
1044        X509Certificate::from_der(der).map_err(|error| X509ChainError::InvalidDer {
1045            kind: "certificate",
1046            message: error.to_string(),
1047        })?;
1048    if !rest.is_empty() {
1049        return Err(X509ChainError::InvalidDer {
1050            kind: "certificate",
1051            message: "trailing data".into(),
1052        });
1053    }
1054    Ok(cert)
1055}
1056
1057fn system_time_to_asn1(time: SystemTime) -> Result<ASN1Time, X509ChainError> {
1058    let seconds = time
1059        .duration_since(UNIX_EPOCH)
1060        .map_err(|_| X509ChainError::CertificateNotValid(0))?
1061        .as_secs();
1062    let timestamp = i64::try_from(seconds).map_err(|_| X509ChainError::CertificateNotValid(0))?;
1063    ASN1Time::from_timestamp(timestamp).map_err(|error| X509ChainError::InvalidDer {
1064        kind: "verification time",
1065        message: error.to_string(),
1066    })
1067}
1068
1069fn validate_ca_constraints(
1070    cert: &X509Certificate<'_>,
1071    position: usize,
1072) -> Result<(), X509ChainError> {
1073    let extension = cert
1074        .extensions()
1075        .iter()
1076        .find(|extension| {
1077            matches!(
1078                extension.parsed_extension(),
1079                ParsedExtension::BasicConstraints(_)
1080            )
1081        })
1082        .ok_or(X509ChainError::IssuerNotCa(position))?;
1083    let ParsedExtension::BasicConstraints(constraints) = extension.parsed_extension() else {
1084        unreachable!("extension was selected by parsed type")
1085    };
1086    if !constraints.ca {
1087        return Err(X509ChainError::IssuerNotCa(position));
1088    }
1089    // RFC 5280 section 4.2.1.9 requires conforming issuers to mark CA
1090    // BasicConstraints critical, but the path-validation algorithm requires
1091    // the cA assertion and does not turn issuer non-conformance into a path
1092    // failure. OpenSSL/xmlsec1 accepts historical non-critical CA extensions.
1093
1094    if cert
1095        .key_usage()
1096        .map_err(|error| X509ChainError::InvalidDer {
1097            kind: "certificate KeyUsage",
1098            message: error.to_string(),
1099        })?
1100        .is_some_and(|usage| !usage.value.key_cert_sign())
1101    {
1102        return Err(X509ChainError::InvalidKeyUsage {
1103            position,
1104            required: "keyCertSign",
1105        });
1106    }
1107
1108    Ok(())
1109}
1110
1111fn basic_constraints(
1112    cert: &X509Certificate<'_>,
1113) -> Option<x509_parser::extensions::BasicConstraints> {
1114    cert.extensions()
1115        .iter()
1116        .find_map(|extension| match extension.parsed_extension() {
1117            ParsedExtension::BasicConstraints(value) => Some(value.clone()),
1118            _ => None,
1119        })
1120}
1121
1122fn validate_path_length_constraints(path: &[X509Certificate<'_>]) -> Result<(), X509ChainError> {
1123    for (position, cert) in path.iter().enumerate().skip(1) {
1124        let Some(limit) = basic_constraints(cert).and_then(|value| value.path_len_constraint)
1125        else {
1126            continue;
1127        };
1128        let subordinate_ca_count = path[1..position]
1129            .iter()
1130            .filter(|subordinate| {
1131                basic_constraints(subordinate).is_some_and(|value| value.ca)
1132                    && !certificate_names_equal(subordinate.subject(), subordinate.issuer())
1133            })
1134            .count();
1135        if subordinate_ca_count > limit as usize {
1136            return Err(X509ChainError::PathLengthExceeded { position, limit });
1137        }
1138    }
1139    Ok(())
1140}
1141
1142fn validate_critical_extensions(
1143    cert: &X509Certificate<'_>,
1144    position: usize,
1145) -> Result<(), X509ChainError> {
1146    for extension in cert
1147        .extensions()
1148        .iter()
1149        .filter(|extension| extension.critical)
1150    {
1151        let oid = extension.oid.to_id_string();
1152        if !matches!(
1153            oid.as_str(),
1154            "2.5.29.15" | "2.5.29.17" | "2.5.29.19" | "2.5.29.30" | "2.5.29.37"
1155        ) {
1156            return Err(X509ChainError::UnsupportedCriticalExtension { position, oid });
1157        }
1158        if matches!(
1159            extension.parsed_extension(),
1160            ParsedExtension::UnsupportedExtension { .. }
1161                | ParsedExtension::ParseError { .. }
1162                | ParsedExtension::Unparsed
1163        ) {
1164            return Err(X509ChainError::UnsupportedCriticalExtension { position, oid });
1165        }
1166    }
1167    Ok(())
1168}
1169
1170fn validate_name_constraints(path: &[X509Certificate<'_>]) -> Result<(), X509ChainError> {
1171    for (position, certificate) in path.iter().enumerate() {
1172        if let Some(extension) = certificate
1173            .extensions()
1174            .iter()
1175            .find(|extension| extension.oid.to_id_string() == "2.5.29.30")
1176            && (position == 0 || !extension.critical)
1177        {
1178            return Err(X509ChainError::InvalidNameConstraints { position });
1179        }
1180    }
1181    for (constraining_position, issuer) in path.iter().enumerate().skip(1) {
1182        let Some(extension) = issuer
1183            .extensions()
1184            .iter()
1185            .find(|extension| extension.oid.to_id_string() == "2.5.29.30")
1186        else {
1187            continue;
1188        };
1189        let ParsedExtension::NameConstraints(constraints) = extension.parsed_extension() else {
1190            continue;
1191        };
1192        validate_name_constraints_der(extension.value, constraining_position)?;
1193        ensure_supported_name_constraints(constraints, constraining_position)?;
1194        for (position, subordinate) in path[..constraining_position].iter().enumerate() {
1195            // The target certificate is always checked. Self-issued CA rollover
1196            // certificates between it and the constraint issuer are exempt.
1197            if position != 0 && certificate_names_equal(subordinate.subject(), subordinate.issuer())
1198            {
1199                continue;
1200            }
1201            validate_certificate_names(subordinate, constraints, position, constraining_position)?;
1202        }
1203    }
1204    Ok(())
1205}
1206
1207fn validate_name_constraints_der(
1208    extension_der: &[u8],
1209    position: usize,
1210) -> Result<(), X509ChainError> {
1211    use der::Decode as _;
1212
1213    // x509-parser intentionally omits GeneralSubtree distance fields from its
1214    // public model. Decode the raw extension as well so they cannot silently
1215    // acquire the zero-minimum, unbounded semantics implemented below.
1216    let constraints =
1217        x509_cert::ext::pkix::NameConstraints::from_der(extension_der).map_err(|error| {
1218            X509ChainError::InvalidDer {
1219                kind: "NameConstraints",
1220                message: error.to_string(),
1221            }
1222        })?;
1223    if constraints.permitted_subtrees.is_none() && constraints.excluded_subtrees.is_none()
1224        || constraints
1225            .permitted_subtrees
1226            .as_ref()
1227            .is_some_and(Vec::is_empty)
1228        || constraints
1229            .excluded_subtrees
1230            .as_ref()
1231            .is_some_and(Vec::is_empty)
1232    {
1233        return Err(X509ChainError::InvalidNameConstraints { position });
1234    }
1235    let unsupported = constraints
1236        .permitted_subtrees
1237        .iter()
1238        .flatten()
1239        .chain(constraints.excluded_subtrees.iter().flatten())
1240        .any(|subtree| subtree.minimum != 0 || subtree.maximum.is_some());
1241    if unsupported {
1242        return Err(X509ChainError::InvalidNameConstraints { position });
1243    }
1244    Ok(())
1245}
1246
1247fn ensure_supported_name_constraints(
1248    constraints: &NameConstraints<'_>,
1249    position: usize,
1250) -> Result<(), X509ChainError> {
1251    for subtree in constraints
1252        .permitted_subtrees
1253        .iter()
1254        .flatten()
1255        .chain(constraints.excluded_subtrees.iter().flatten())
1256    {
1257        match &subtree.base {
1258            GeneralName::DNSName(value) | GeneralName::URI(value) => {
1259                validate_dns_name_constraint(value)?;
1260            }
1261            GeneralName::RFC822Name(value) => validate_email_name_constraint(value)?,
1262            GeneralName::IPAddress(bytes) => {
1263                validate_ip_name_constraint(bytes)?;
1264            }
1265            _ => {}
1266        }
1267        if matches!(
1268            subtree.base,
1269            GeneralName::OtherName(..)
1270                | GeneralName::X400Address(..)
1271                | GeneralName::EDIPartyName(..)
1272                | GeneralName::RegisteredID(..)
1273                | GeneralName::Invalid(..)
1274        ) {
1275            return Err(X509ChainError::UnsupportedCriticalExtension {
1276                position,
1277                oid: "2.5.29.30".into(),
1278            });
1279        }
1280    }
1281    Ok(())
1282}
1283
1284fn validate_email_name_constraint(value: &str) -> Result<(), X509ChainError> {
1285    if value.contains('@') {
1286        if !mailbox_has_valid_syntax(value) {
1287            return Err(invalid_string_name_constraint(value));
1288        }
1289        Ok(())
1290    } else {
1291        validate_dns_name_constraint(value)
1292    }
1293}
1294
1295fn validate_dns_name_constraint(value: &str) -> Result<(), X509ChainError> {
1296    if !dns_name_has_valid_syntax(value, true) {
1297        return Err(invalid_string_name_constraint(value));
1298    }
1299    Ok(())
1300}
1301
1302fn validate_rfc5280_dns_name(value: &str) -> Result<(), X509ChainError> {
1303    if !dns_name_has_valid_syntax(value, false) {
1304        return Err(X509ChainError::InvalidDer {
1305            kind: "certificate DNS name",
1306            message: format!("invalid RFC 5280 dNSName: {value:?}"),
1307        });
1308    }
1309    Ok(())
1310}
1311
1312fn dns_name_has_valid_syntax(value: &str, allow_leading_dot: bool) -> bool {
1313    let domain = if allow_leading_dot {
1314        value.strip_prefix('.').unwrap_or(value)
1315    } else {
1316        value
1317    };
1318    if domain.is_empty()
1319        || domain.len() > 253
1320        || domain.split('.').any(|label| {
1321            label.is_empty()
1322                || label.len() > 63
1323                || !label
1324                    .bytes()
1325                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
1326                || !label
1327                    .as_bytes()
1328                    .first()
1329                    .is_some_and(u8::is_ascii_alphanumeric)
1330                || !label
1331                    .as_bytes()
1332                    .last()
1333                    .is_some_and(u8::is_ascii_alphanumeric)
1334        })
1335    {
1336        return false;
1337    }
1338    true
1339}
1340
1341fn invalid_string_name_constraint(value: &str) -> X509ChainError {
1342    X509ChainError::InvalidDer {
1343        kind: "string name constraint",
1344        message: format!("invalid RFC 5280 string name constraint: {value:?}"),
1345    }
1346}
1347
1348fn validate_certificate_names(
1349    certificate: &X509Certificate<'_>,
1350    constraints: &NameConstraints<'_>,
1351    position: usize,
1352    constraining_position: usize,
1353) -> Result<(), X509ChainError> {
1354    if certificate.subject().iter().next().is_some() {
1355        let subject = GeneralName::DirectoryName(certificate.subject().clone());
1356        validate_general_name(&subject, constraints, position, constraining_position)?;
1357    }
1358    for attribute in certificate.subject().iter_email() {
1359        let email = attribute
1360            .as_str()
1361            .map_err(|error| X509ChainError::InvalidDer {
1362                kind: "certificate subject emailAddress",
1363                message: error.to_string(),
1364            })?;
1365        validate_general_name(
1366            &GeneralName::RFC822Name(email),
1367            constraints,
1368            position,
1369            constraining_position,
1370        )?;
1371    }
1372    if let Some(names) =
1373        certificate
1374            .extensions()
1375            .iter()
1376            .find_map(|extension| match extension.parsed_extension() {
1377                ParsedExtension::SubjectAlternativeName(value) => Some(&value.general_names),
1378                _ => None,
1379            })
1380    {
1381        for name in names {
1382            validate_general_name(name, constraints, position, constraining_position)?;
1383        }
1384    }
1385    Ok(())
1386}
1387
1388fn validate_general_name(
1389    name: &GeneralName<'_>,
1390    constraints: &NameConstraints<'_>,
1391    position: usize,
1392    constraining_position: usize,
1393) -> Result<(), X509ChainError> {
1394    let permitted = constraints
1395        .permitted_subtrees
1396        .iter()
1397        .flatten()
1398        .filter(|subtree| general_names_have_same_form(name, &subtree.base));
1399    let mut has_permitted_form = false;
1400    let mut matches_permitted = false;
1401    for subtree in permitted {
1402        has_permitted_form = true;
1403        matches_permitted |=
1404            general_name_within_subtree(name, &subtree.base)? == NameConstraintMatch::Match;
1405    }
1406    let excluded = constraints
1407        .excluded_subtrees
1408        .iter()
1409        .flatten()
1410        .filter(|subtree| general_names_have_same_form(name, &subtree.base))
1411        .try_fold(false, |rejected, subtree| {
1412            general_name_within_subtree(name, &subtree.base)
1413                .map(|current| rejected || current != NameConstraintMatch::NoMatch)
1414        })?;
1415    if excluded || (has_permitted_form && !matches_permitted) {
1416        return Err(X509ChainError::NameConstraintViolation {
1417            position,
1418            constraining_position,
1419        });
1420    }
1421    Ok(())
1422}
1423
1424fn general_names_have_same_form(left: &GeneralName<'_>, right: &GeneralName<'_>) -> bool {
1425    matches!(
1426        (left, right),
1427        (GeneralName::RFC822Name(_), GeneralName::RFC822Name(_))
1428            | (GeneralName::DNSName(_), GeneralName::DNSName(_))
1429            | (GeneralName::DirectoryName(_), GeneralName::DirectoryName(_))
1430            | (GeneralName::URI(_), GeneralName::URI(_))
1431            | (GeneralName::IPAddress(_), GeneralName::IPAddress(_))
1432    )
1433}
1434
1435#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1436enum NameConstraintMatch {
1437    Match,
1438    NoMatch,
1439    Unevaluable,
1440}
1441
1442impl From<bool> for NameConstraintMatch {
1443    fn from(matched: bool) -> Self {
1444        if matched { Self::Match } else { Self::NoMatch }
1445    }
1446}
1447
1448fn general_name_within_subtree(
1449    name: &GeneralName<'_>,
1450    subtree: &GeneralName<'_>,
1451) -> Result<NameConstraintMatch, X509ChainError> {
1452    Ok(match (name, subtree) {
1453        (GeneralName::DNSName(name), GeneralName::DNSName(subtree)) => {
1454            dns_name_within_subtree(name, subtree, true).into()
1455        }
1456        (GeneralName::RFC822Name(name), GeneralName::RFC822Name(subtree)) => {
1457            email_within_subtree(name, subtree).into()
1458        }
1459        (GeneralName::DirectoryName(name), GeneralName::DirectoryName(subtree)) => {
1460            let name = x509_name_to_rfc4514(name).map_err(|error| X509ChainError::InvalidDer {
1461                kind: "certificate name constraint",
1462                message: error.to_string(),
1463            })?;
1464            let subtree =
1465                x509_name_to_rfc4514(subtree).map_err(|error| X509ChainError::InvalidDer {
1466                    kind: "certificate name constraint",
1467                    message: error.to_string(),
1468                })?;
1469            distinguished_name_within_subtree(&name, &subtree).into()
1470        }
1471        (GeneralName::URI(name), GeneralName::URI(subtree)) => uri_host(name)
1472            .map_or(NameConstraintMatch::Unevaluable, |host| {
1473                dns_name_within_subtree(host, subtree, false).into()
1474            }),
1475        (GeneralName::IPAddress(name), GeneralName::IPAddress(subtree)) => {
1476            ip_address_within_subtree(name, subtree)?.into()
1477        }
1478        _ => NameConstraintMatch::NoMatch,
1479    })
1480}
1481
1482fn dns_name_within_subtree(name: &str, subtree: &str, include_subdomains: bool) -> bool {
1483    let name = name.trim_end_matches('.');
1484    let subtree = subtree.trim_end_matches('.');
1485    if let Some(domain) = subtree.strip_prefix('.') {
1486        return name.len() > domain.len()
1487            && name.as_bytes()[name.len() - domain.len() - 1] == b'.'
1488            && name[name.len() - domain.len()..].eq_ignore_ascii_case(domain);
1489    }
1490    name.eq_ignore_ascii_case(subtree)
1491        || (include_subdomains
1492            && name.len() > subtree.len()
1493            && name.as_bytes()[name.len() - subtree.len() - 1] == b'.'
1494            && name[name.len() - subtree.len()..].eq_ignore_ascii_case(subtree))
1495}
1496
1497fn email_within_subtree(name: &str, subtree: &str) -> bool {
1498    let Some((local, domain)) = name.rsplit_once('@') else {
1499        return false;
1500    };
1501    if let Some((expected_local, expected_domain)) = subtree.rsplit_once('@') {
1502        return local == expected_local && domain.eq_ignore_ascii_case(expected_domain);
1503    }
1504    dns_name_within_subtree(domain, subtree, false)
1505}
1506
1507fn uri_host(uri: &str) -> Option<&str> {
1508    let authority = uri.split_once("://")?.1;
1509    let authority = authority.split(['/', '?', '#']).next()?;
1510    match parse_uri_authority_host(authority)? {
1511        UriAuthorityHost::Dns(host) => Some(host),
1512        UriAuthorityHost::Ip => None,
1513    }
1514}
1515
1516fn ip_address_within_subtree(address: &[u8], subtree: &[u8]) -> Result<bool, X509ChainError> {
1517    if !matches!(address.len(), 4 | 16) {
1518        return Err(X509ChainError::InvalidDer {
1519            kind: "IP subject alternative name",
1520            message: format!("expected 4 or 16 octets, got {}", address.len()),
1521        });
1522    }
1523    let (network, mask) = validate_ip_name_constraint(subtree)?;
1524    if network.len() != address.len() {
1525        return Ok(false);
1526    }
1527    Ok(address
1528        .iter()
1529        .zip(network)
1530        .zip(mask)
1531        .all(|((address, network), mask)| address & mask == network & mask))
1532}
1533
1534fn validate_ip_name_constraint(subtree: &[u8]) -> Result<(&[u8], &[u8]), X509ChainError> {
1535    if !matches!(subtree.len(), 8 | 32) {
1536        return Err(X509ChainError::InvalidDer {
1537            kind: "IP name constraint",
1538            message: format!("expected 8 or 32 octets, got {}", subtree.len()),
1539        });
1540    }
1541    let (network, mask) = subtree.split_at(subtree.len() / 2);
1542    if !ip_mask_is_contiguous(mask) {
1543        return Err(X509ChainError::InvalidDer {
1544            kind: "IP name constraint",
1545            message: "network mask is not contiguous".into(),
1546        });
1547    }
1548    Ok((network, mask))
1549}
1550
1551fn ip_mask_is_contiguous(mask: &[u8]) -> bool {
1552    let mut zero_seen = false;
1553    for byte in mask {
1554        for bit in (0..8).rev() {
1555            let set = byte & (1 << bit) != 0;
1556            if zero_seen && set {
1557                return false;
1558            }
1559            zero_seen |= !set;
1560        }
1561    }
1562    true
1563}
1564
1565fn certificate_subject_key_identifier<'a>(
1566    certificate: &'a X509Certificate<'a>,
1567) -> Option<&'a [u8]> {
1568    certificate
1569        .extensions()
1570        .iter()
1571        .find_map(|extension| match extension.parsed_extension() {
1572            ParsedExtension::SubjectKeyIdentifier(identifier) => Some(identifier.0),
1573            _ => None,
1574        })
1575}
1576
1577fn crl_authority_key_matches(
1578    crl: &CertificateRevocationList<'_>,
1579    issuer: &X509Certificate<'_>,
1580) -> Result<Option<bool>, X509ChainError> {
1581    let authority_key = crl
1582        .extensions()
1583        .iter()
1584        .find(|extension| extension.oid.to_id_string() == "2.5.29.35")
1585        .map(|extension| match extension.parsed_extension() {
1586            ParsedExtension::AuthorityKeyIdentifier(identifier) => {
1587                Ok(identifier.key_identifier.as_ref().map(|key| key.0))
1588            }
1589            _ => Err(X509ChainError::InvalidDer {
1590                kind: "CRL AuthorityKeyIdentifier",
1591                message: "extension could not be decoded".into(),
1592            }),
1593        })
1594        .transpose()?
1595        .flatten();
1596    Ok(authority_key
1597        .zip(certificate_subject_key_identifier(issuer))
1598        .map(|(authority, subject)| authority == subject))
1599}
1600
1601fn validate_crl_extensions(
1602    crl: &CertificateRevocationList<'_>,
1603    crl_index: usize,
1604) -> Result<(), X509ChainError> {
1605    validate_crl_extension_uniqueness(crl, crl_index)?;
1606    validate_crl_extension_semantics(crl, crl_index)
1607}
1608
1609fn validate_crl_extension_uniqueness(
1610    crl: &CertificateRevocationList<'_>,
1611    crl_index: usize,
1612) -> Result<(), X509ChainError> {
1613    crl.tbs_cert_list
1614        .extensions_map()
1615        .map_err(|_| X509ChainError::InvalidCrl(crl_index))?;
1616    for revoked in crl.iter_revoked_certificates() {
1617        validate_positive_serial_bytes(revoked.raw_serial(), "CRL revoked certificate serial")
1618            .map_err(|_| X509ChainError::InvalidCrl(crl_index))?;
1619        revoked
1620            .extensions_map()
1621            .map_err(|_| X509ChainError::InvalidCrl(crl_index))?;
1622    }
1623    Ok(())
1624}
1625
1626fn validate_crl_extension_semantics(
1627    crl: &CertificateRevocationList<'_>,
1628    crl_index: usize,
1629) -> Result<(), X509ChainError> {
1630    for extension in crl.extensions() {
1631        let oid = extension.oid.to_id_string();
1632        // IssuingDistributionPoint changes which certificates and issuers a CRL
1633        // covers. Delta CRLs also cannot be treated as complete CRLs: in particular,
1634        // removeFromCRL has the opposite meaning from a complete-list revocation.
1635        if matches!(oid.as_str(), "2.5.29.27" | "2.5.29.28")
1636            || (extension.critical && oid != "2.5.29.35")
1637        {
1638            return Err(X509ChainError::InvalidCrl(crl_index));
1639        }
1640        if oid == "2.5.29.35"
1641            && !matches!(
1642                extension.parsed_extension(),
1643                ParsedExtension::AuthorityKeyIdentifier(_)
1644            )
1645        {
1646            return Err(X509ChainError::InvalidCrl(crl_index));
1647        }
1648    }
1649    for revoked in crl.iter_revoked_certificates() {
1650        for extension in revoked.extensions() {
1651            let oid = extension.oid.to_id_string();
1652            // certificateIssuer carries the issuer identity for indirect CRLs.
1653            // removeFromCRL is meaningful only in a delta CRL, which this
1654            // complete-CRL validator rejects above.
1655            let invalid_reason = oid == "2.5.29.21"
1656                && !matches!(
1657                    extension.parsed_extension(),
1658                    ParsedExtension::ReasonCode(code)
1659                        if *code != x509_parser::x509::ReasonCode::RemoveFromCRL
1660                );
1661            if oid == "2.5.29.29" || extension.critical || invalid_reason {
1662                return Err(X509ChainError::InvalidCrl(crl_index));
1663            }
1664        }
1665    }
1666    Ok(())
1667}
1668
1669fn verify_crls(
1670    path: &[X509Certificate<'_>],
1671    crl_der: &[Vec<u8>],
1672    verification_time: ASN1Time,
1673    provider: &dyn crate::provider::CryptoProvider,
1674) -> Result<(), X509ChainError> {
1675    let crls = crl_der
1676        .iter()
1677        .enumerate()
1678        .map(|(idx, der)| {
1679            let (rest, crl) = CertificateRevocationList::from_der(der).map_err(|error| {
1680                X509ChainError::InvalidDer {
1681                    kind: "CRL",
1682                    message: error.to_string(),
1683                }
1684            })?;
1685            if !rest.is_empty() {
1686                return Err(X509ChainError::InvalidDer {
1687                    kind: "CRL",
1688                    message: "trailing data".into(),
1689                });
1690            }
1691            Ok((idx, crl))
1692        })
1693        .collect::<Result<Vec<_>, _>>()?;
1694
1695    for (position, cert) in path.iter().enumerate().take(path.len().saturating_sub(1)) {
1696        let issuer = &path[position + 1];
1697        for (crl_index, crl) in crls
1698            .iter()
1699            .filter(|(_, crl)| certificate_names_equal(crl.issuer(), cert.issuer()))
1700        {
1701            // Duplicate OIDs make first-match AKI filtering ambiguous, so this
1702            // structural invariant must hold before key applicability is tested.
1703            validate_crl_extension_uniqueness(crl, *crl_index)?;
1704            let authority_key_match = crl_authority_key_matches(crl, issuer)?;
1705            if authority_key_match == Some(false) {
1706                continue;
1707            }
1708            if !verify_crl_signature_with_provider(crl, issuer, provider)? {
1709                if authority_key_match == Some(true) {
1710                    return Err(X509ChainError::InvalidCrl(*crl_index));
1711                }
1712                continue;
1713            }
1714            // Extension semantics can reject an applicable CRL, but unrelated
1715            // untrusted CRL material must not influence the selected path.
1716            validate_crl_extensions(crl, *crl_index)?;
1717            if issuer
1718                .key_usage()
1719                .map_err(|error| X509ChainError::InvalidDer {
1720                    kind: "certificate KeyUsage",
1721                    message: error.to_string(),
1722                })?
1723                .is_some_and(|usage| !usage.value.crl_sign())
1724            {
1725                return Err(X509ChainError::InvalidKeyUsage {
1726                    position: position + 1,
1727                    required: "cRLSign",
1728                });
1729            }
1730            // RFC 5280 requires conforming CRL issuers to provide nextUpdate;
1731            // without it this verifier cannot establish a bounded freshness window.
1732            let time_valid = crl.next_update().is_some_and(|next| {
1733                crl.last_update() <= verification_time && verification_time <= next
1734            });
1735            if !time_valid {
1736                return Err(X509ChainError::InvalidCrl(*crl_index));
1737            }
1738            if crl.iter_revoked_certificates().any(|revoked| {
1739                revoked.raw_serial() == cert.raw_serial()
1740                    && revoked.revocation_date <= verification_time
1741            }) {
1742                return Err(X509ChainError::Revoked(position));
1743            }
1744        }
1745    }
1746    Ok(())
1747}
1748
1749#[cfg(test)]
1750mod tests {
1751    use std::str::FromStr as _;
1752
1753    use super::*;
1754    use crate::xml::dom::Document;
1755    use crate::xmldsig::{KeyInfoSource, parse::XMLDSIG_NS, parse_key_info};
1756    use p256::pkcs8::EncodePublicKey;
1757    use sha2::{Digest, Sha256, Sha384};
1758    use signature::hazmat::PrehashSigner;
1759    use std::time::Duration;
1760    use x509_parser::oid_registry::{OID_SIG_ECDSA_WITH_SHA256, OID_SIG_ECDSA_WITH_SHA384, Oid};
1761
1762    fn generated_certificate_params(common_name: &str, is_ca: bool) -> rcgen::CertificateParams {
1763        let mut params = rcgen::CertificateParams::new(Vec::new())
1764            .expect("empty SAN list should produce valid certificate parameters");
1765        params
1766            .distinguished_name
1767            .push(rcgen::DnType::CommonName, common_name);
1768        if is_ca {
1769            params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
1770            params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
1771        }
1772        params
1773    }
1774
1775    fn verify_generated_path(
1776        certificates: Vec<Vec<u8>>,
1777        trusted_anchor: Vec<u8>,
1778    ) -> Result<(), X509ChainError> {
1779        verify_generated_path_with_eku(certificates, trusted_anchor, None)
1780    }
1781
1782    fn verify_generated_path_with_eku(
1783        certificates: Vec<Vec<u8>>,
1784        trusted_anchor: Vec<u8>,
1785        allowed_extended_key_usages: Option<&HashSet<ExtendedKeyPurpose>>,
1786    ) -> Result<(), X509ChainError> {
1787        let info = X509DataInfo {
1788            certificate_chain: (0..certificates.len()).collect(),
1789            certificates,
1790            ..X509DataInfo::default()
1791        };
1792        let anchors = vec![trusted_anchor];
1793        verify_x509_certificate_chain(
1794            &info,
1795            &X509ChainOptions {
1796                trusted_certs: &anchors,
1797                verification_time: SystemTime::now(),
1798                max_chain_depth: info.certificate_chain.len(),
1799                check_crls: false,
1800                allowed_extended_key_usages,
1801                rsa_keys: RsaKeyPolicy::default(),
1802                dsa_keys: DsaKeyPolicy::default(),
1803            },
1804        )
1805    }
1806
1807    #[test]
1808    fn noncritical_ca_basic_constraints_remain_path_compatible() {
1809        // Criticality is an issuer conformance requirement, not an additional
1810        // relying-party path gate; historical xmlsec1 chains depend on this.
1811        let mut params = generated_certificate_params("non-critical authority", false);
1812        params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
1813        params
1814            .custom_extensions
1815            .push(rcgen::CustomExtension::from_oid_content(
1816                &[2, 5, 29, 19],
1817                vec![0x30, 0x03, 0x01, 0x01, 0xff],
1818            ));
1819        let certificate = params
1820            .self_signed(&rcgen::KeyPair::generate().expect("CA key generation should succeed"))
1821            .expect("test CA should be self-signable");
1822        let parsed = parse_certificate(certificate.der()).expect("test CA DER should parse");
1823
1824        assert_eq!(validate_ca_constraints(&parsed, 1), Ok(()));
1825    }
1826
1827    #[test]
1828    fn restricted_leaf_eku_requires_an_approved_purpose() {
1829        // A server-authentication certificate is not implicitly authorized for
1830        // XML signatures merely because its key permits digital signatures.
1831        let root = rcgen::CertifiedIssuer::self_signed(
1832            generated_certificate_params("EKU authority", true),
1833            rcgen::KeyPair::generate().expect("root key generation should succeed"),
1834        )
1835        .expect("root should be self-signable");
1836        let mut leaf_params = generated_certificate_params("TLS-only signer", false);
1837        leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
1838        leaf_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth];
1839        let leaf = leaf_params
1840            .signed_by(
1841                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
1842                &root,
1843            )
1844            .expect("root should sign leaf certificate");
1845        let leaf_der = leaf.der().to_vec();
1846        let root_der = root.der().to_vec();
1847
1848        assert!(matches!(
1849            verify_generated_path(vec![leaf_der.clone(), root_der.clone()], root_der.clone(),),
1850            Err(X509ChainError::InvalidKeyUsage {
1851                position: 0,
1852                required: "an approved extended key usage",
1853            })
1854        ));
1855
1856        let allowed = HashSet::from([ExtendedKeyPurpose::ServerAuth]);
1857        verify_generated_path_with_eku(vec![leaf_der, root_der.clone()], root_der, Some(&allowed))
1858            .expect("an explicitly approved leaf purpose must be accepted");
1859    }
1860
1861    #[test]
1862    fn critical_leaf_eku_uses_the_same_purpose_policy() {
1863        // Criticality changes whether an unknown extension may be ignored, not
1864        // the authorization semantics of an EKU that this validator implements.
1865        let root = rcgen::CertifiedIssuer::self_signed(
1866            generated_certificate_params("critical EKU authority", true),
1867            rcgen::KeyPair::generate().expect("root key generation should succeed"),
1868        )
1869        .expect("root should be self-signable");
1870        let mut leaf_params = generated_certificate_params("critical TLS signer", false);
1871        leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
1872        let mut extension = rcgen::CustomExtension::from_oid_content(
1873            &[2, 5, 29, 37],
1874            vec![
1875                0x30, 0x0a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
1876            ],
1877        );
1878        extension.set_criticality(true);
1879        leaf_params.custom_extensions.push(extension);
1880        let leaf = leaf_params
1881            .signed_by(
1882                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
1883                &root,
1884            )
1885            .expect("root should sign leaf certificate");
1886        let allowed = HashSet::from([ExtendedKeyPurpose::ServerAuth]);
1887
1888        verify_generated_path_with_eku(
1889            vec![leaf.der().to_vec(), root.der().to_vec()],
1890            root.der().to_vec(),
1891            Some(&allowed),
1892        )
1893        .expect("approved critical EKU must be processed rather than rejected as unknown");
1894    }
1895
1896    #[test]
1897    fn issuer_eku_restricts_the_entire_certificate_path() {
1898        // RFC 5280 applies an issuer EKU as a path-wide purpose constraint. A
1899        // leaf approval cannot override an incompatible critical CA authorization.
1900        for (issuer_purpose_der, accepted) in [
1901            (
1902                vec![
1903                    0x30, 0x0a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02,
1904                ],
1905                false,
1906            ),
1907            (
1908                vec![
1909                    0x30, 0x0a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
1910                ],
1911                true,
1912            ),
1913        ] {
1914            let mut root_params =
1915                generated_certificate_params("purpose-constrained authority", true);
1916            let mut extension =
1917                rcgen::CustomExtension::from_oid_content(&[2, 5, 29, 37], issuer_purpose_der);
1918            extension.set_criticality(true);
1919            root_params.custom_extensions.push(extension);
1920            let root = rcgen::CertifiedIssuer::self_signed(
1921                root_params,
1922                rcgen::KeyPair::generate().expect("root key generation should succeed"),
1923            )
1924            .expect("root should be self-signable");
1925            let mut leaf_params = generated_certificate_params("TLS server signer", false);
1926            leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
1927            leaf_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth];
1928            let leaf = leaf_params
1929                .signed_by(
1930                    &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
1931                    &root,
1932                )
1933                .expect("root should sign leaf certificate");
1934            let allowed = HashSet::from([
1935                ExtendedKeyPurpose::ServerAuth,
1936                ExtendedKeyPurpose::ClientAuth,
1937            ]);
1938            let result = verify_generated_path_with_eku(
1939                vec![leaf.der().to_vec(), root.der().to_vec()],
1940                root.der().to_vec(),
1941                Some(&allowed),
1942            );
1943
1944            if accepted {
1945                result.expect("a shared allowed purpose must satisfy the complete path");
1946            } else {
1947                assert!(matches!(
1948                    result,
1949                    Err(X509ChainError::InvalidKeyUsage {
1950                        position: 1,
1951                        required: "an approved extended key usage",
1952                    })
1953                ));
1954            }
1955        }
1956    }
1957
1958    #[test]
1959    fn any_extended_key_usage_does_not_restrict_xml_signing() {
1960        // RFC 5280 anyExtendedKeyUsage explicitly leaves the key unrestricted,
1961        // so it does not require a deployment-specific purpose allowlist entry.
1962        let root = rcgen::CertifiedIssuer::self_signed(
1963            generated_certificate_params("any EKU authority", true),
1964            rcgen::KeyPair::generate().expect("root key generation should succeed"),
1965        )
1966        .expect("root should be self-signable");
1967        let mut leaf_params = generated_certificate_params("unrestricted signer", false);
1968        leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
1969        leaf_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::Any];
1970        let leaf = leaf_params
1971            .signed_by(
1972                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
1973                &root,
1974            )
1975            .expect("root should sign leaf certificate");
1976
1977        verify_generated_path(
1978            vec![leaf.der().to_vec(), root.der().to_vec()],
1979            root.der().to_vec(),
1980        )
1981        .expect("anyExtendedKeyUsage must remain unrestricted");
1982    }
1983
1984    #[test]
1985    fn x509_ecdsa_hash_oid_does_not_select_the_issuer_curve() {
1986        // RFC 5758 signature OIDs select the digest while SubjectPublicKeyInfo
1987        // selects the curve. Both non-default pairings must therefore reach
1988        // the provider with the issuer's actual curve rather than a curve
1989        // inferred from the hash OID.
1990        let data = b"certificate tbs bytes";
1991
1992        let p384_key = p384::ecdsa::SigningKey::from_slice(&[0x42; 48])
1993            .expect("fixed P-384 test key must be valid");
1994        let p384_signature: p384::ecdsa::Signature = p384_key
1995            .sign_prehash(&Sha256::digest(data))
1996            .expect("P-384 must sign a SHA-256 prehash");
1997        let p384_spki = p384_key
1998            .verifying_key()
1999            .to_public_key_der()
2000            .expect("P-384 SPKI must encode");
2001        assert!(
2002            verify_x509_signature_with_provider(
2003                &AlgorithmIdentifier::new(OID_SIG_ECDSA_WITH_SHA256, None),
2004                p384_signature.to_der().as_bytes(),
2005                data,
2006                p384_spki.as_bytes(),
2007                crate::provider::default_provider(),
2008            )
2009            .expect("P-384 with SHA-256 must be a supported X.509 pairing")
2010        );
2011
2012        let p256_key = p256::ecdsa::SigningKey::from_slice(&[0x24; 32])
2013            .expect("fixed P-256 test key must be valid");
2014        let p256_signature: p256::ecdsa::Signature = p256_key
2015            .sign_prehash(&Sha384::digest(data))
2016            .expect("P-256 must sign a SHA-384 prehash");
2017        let p256_spki = p256_key
2018            .verifying_key()
2019            .to_public_key_der()
2020            .expect("P-256 SPKI must encode");
2021        assert!(
2022            verify_x509_signature_with_provider(
2023                &AlgorithmIdentifier::new(OID_SIG_ECDSA_WITH_SHA384, None),
2024                p256_signature.to_der().as_bytes(),
2025                data,
2026                p256_spki.as_bytes(),
2027                crate::provider::default_provider(),
2028            )
2029            .expect("P-256 with SHA-384 must be a supported X.509 pairing")
2030        );
2031    }
2032
2033    #[test]
2034    fn path_edge_signature_check_does_not_repeat_name_matching() {
2035        // Path construction performs RFC 5280 name matching before asking this
2036        // helper to disambiguate same-name candidates. Only proof of possession
2037        // of the issuer key belongs in this second gate.
2038        let issuer_key = rcgen::KeyPair::generate().expect("issuer key generation should succeed");
2039        let issuer_key_pem = issuer_key.serialize_pem();
2040        let mut signing_params = rcgen::CertificateParams::new(Vec::new())
2041            .expect("empty issuer SAN list should be valid");
2042        signing_params
2043            .distinguished_name
2044            .push(rcgen::DnType::CommonName, "signing name");
2045        signing_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
2046        signing_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
2047        let signing_issuer = rcgen::CertifiedIssuer::self_signed(signing_params, issuer_key)
2048            .expect("issuer certificate should be self-signable");
2049
2050        let mut alternate_params = rcgen::CertificateParams::new(Vec::new())
2051            .expect("empty alternate SAN list should be valid");
2052        alternate_params
2053            .distinguished_name
2054            .push(rcgen::DnType::CommonName, "name already matched by caller");
2055        alternate_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
2056        alternate_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
2057        let alternate_issuer = rcgen::CertifiedIssuer::self_signed(
2058            alternate_params,
2059            rcgen::KeyPair::from_pem(&issuer_key_pem)
2060                .expect("serialized issuer key should parse again"),
2061        )
2062        .expect("alternate issuer certificate should be self-signable");
2063
2064        let leaf = rcgen::CertificateParams::new(Vec::new())
2065            .expect("empty leaf SAN list should be valid")
2066            .signed_by(
2067                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2068                &signing_issuer,
2069            )
2070            .expect("issuer should sign leaf certificate");
2071
2072        assert!(certificate_signature_matches(
2073            leaf.der(),
2074            alternate_issuer.der()
2075        ));
2076    }
2077
2078    #[test]
2079    fn certificate_path_edge_preserves_ed25519_verification() {
2080        // Provider routing must preserve the certificate algorithms accepted by
2081        // the previous x509-parser verifier rather than narrowing them to the
2082        // XMLDSig SignatureMethod enum.
2083        let issuer_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
2084            .expect("Ed25519 issuer key generation should succeed");
2085        let mut issuer_params = rcgen::CertificateParams::new(Vec::new())
2086            .expect("empty issuer SAN list should be valid");
2087        issuer_params
2088            .distinguished_name
2089            .push(rcgen::DnType::CommonName, "Ed25519 issuer");
2090        issuer_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
2091        issuer_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
2092        let issuer = rcgen::CertifiedIssuer::self_signed(issuer_params, issuer_key)
2093            .expect("Ed25519 issuer certificate should be self-signable");
2094        let leaf_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
2095            .expect("Ed25519 leaf key generation should succeed");
2096        let leaf = rcgen::CertificateParams::new(Vec::new())
2097            .expect("empty leaf SAN list should be valid")
2098            .signed_by(&leaf_key, &issuer)
2099            .expect("Ed25519 issuer should sign leaf certificate");
2100
2101        assert!(certificate_signature_matches(leaf.der(), issuer.der()));
2102    }
2103
2104    #[test]
2105    fn every_modeled_non_parameterized_x509_algorithm_reaches_the_provider() {
2106        // Parsing and provider capability are separate contracts. Once an OID
2107        // has a typed representation, custom providers must get the chance to
2108        // implement it even when RustCrypto does not.
2109        for (oid, expected) in [
2110            (
2111                "2.16.840.1.101.3.4.3.1",
2112                X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha224),
2113            ),
2114            (
2115                "2.16.840.1.101.3.4.3.2",
2116                X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha256),
2117            ),
2118            (
2119                "2.16.840.1.101.3.4.3.3",
2120                X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha384),
2121            ),
2122            (
2123                "2.16.840.1.101.3.4.3.4",
2124                X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha512),
2125            ),
2126            (
2127                "1.2.840.10045.4.1",
2128                X509SignatureAlgorithm::Ecdsa(super::super::DigestAlgorithm::Sha1),
2129            ),
2130            (
2131                "1.2.840.113549.1.1.14",
2132                X509SignatureAlgorithm::RsaPkcs1v15(super::super::DigestAlgorithm::Sha224),
2133            ),
2134            (
2135                "1.2.840.10045.4.3.1",
2136                X509SignatureAlgorithm::Ecdsa(super::super::DigestAlgorithm::Sha224),
2137            ),
2138            (
2139                "1.2.840.10045.4.3.4",
2140                X509SignatureAlgorithm::Ecdsa(super::super::DigestAlgorithm::Sha512),
2141            ),
2142        ] {
2143            let identifier = AlgorithmIdentifier::new(
2144                Oid::from_str(oid).expect("static signature OID must parse"),
2145                None,
2146            );
2147            assert_eq!(x509_signature_algorithm(&identifier), Ok(expected), "{oid}");
2148        }
2149    }
2150
2151    #[test]
2152    fn x509_signature_parameters_follow_each_algorithm_profile() {
2153        use x509_parser::asn1_rs::{Any, Tag};
2154
2155        // DSA, ECDSA, and Ed25519 signature identifiers require absent
2156        // parameters. A NULL is not equivalent for these algorithm profiles.
2157        for oid in [
2158            "1.2.840.10040.4.3",
2159            "2.16.840.1.101.3.4.3.1",
2160            "2.16.840.1.101.3.4.3.2",
2161            "1.2.840.10045.4.1",
2162            "1.2.840.10045.4.3.1",
2163            "1.2.840.10045.4.3.2",
2164            "1.3.101.112",
2165        ] {
2166            let identifier = AlgorithmIdentifier::new(
2167                Oid::from_str(oid).expect("static signature OID must parse"),
2168                Some(Any::from_tag_and_data(Tag::Null, &[])),
2169            );
2170            assert!(matches!(
2171                x509_signature_algorithm(&identifier),
2172                Err(X509ChainError::InvalidDer {
2173                    kind: "X.509 signature AlgorithmIdentifier parameters",
2174                    ..
2175                })
2176            ));
2177        }
2178
2179        // RSA PKCS#1 signature identifiers accept absent and NULL parameters
2180        // for interoperability, but no other ASN.1 value.
2181        for (oid, digest) in [
2182            (
2183                "1.2.840.113549.1.1.14",
2184                super::super::DigestAlgorithm::Sha224,
2185            ),
2186            (
2187                "1.2.840.113549.1.1.11",
2188                super::super::DigestAlgorithm::Sha256,
2189            ),
2190        ] {
2191            let rsa_oid = Oid::from_str(oid).expect("static RSA signature OID must parse");
2192            for parameters in [None, Some(Any::from_tag_and_data(Tag::Null, &[]))] {
2193                assert_eq!(
2194                    x509_signature_algorithm(&AlgorithmIdentifier::new(
2195                        rsa_oid.clone(),
2196                        parameters,
2197                    )),
2198                    Ok(X509SignatureAlgorithm::RsaPkcs1v15(digest)),
2199                );
2200            }
2201            assert!(matches!(
2202                x509_signature_algorithm(&AlgorithmIdentifier::new(
2203                    rsa_oid,
2204                    Some(Any::from_tag_and_data(Tag::OctetString, &[])),
2205                )),
2206                Err(X509ChainError::InvalidDer {
2207                    kind: "X.509 signature AlgorithmIdentifier parameters",
2208                    ..
2209                })
2210            ));
2211        }
2212    }
2213
2214    #[test]
2215    fn unknown_x509_signature_algorithm_remains_diagnosable() {
2216        let oid = "1.2.3.4.5";
2217        let identifier = AlgorithmIdentifier::new(
2218            Oid::from_str(oid).expect("static unknown OID must parse"),
2219            None,
2220        );
2221
2222        assert_eq!(
2223            x509_signature_algorithm(&identifier),
2224            Err(X509ChainError::UnsupportedSignatureAlgorithm { oid: oid.into() })
2225        );
2226    }
2227
2228    #[test]
2229    fn parses_rsa_pss_certificate_parameters_without_xml_dsig_loss() {
2230        // RFC 4055 carries the digest, MGF digest, and salt length inside the
2231        // AlgorithmIdentifier. Preserve all three values at the provider edge.
2232        let der = [
2233            0x30, 0x41, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a, 0x30,
2234            0x34, 0xa0, 0x0f, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
2235            0x02, 0x01, 0x05, 0x00, 0xa1, 0x1c, 0x30, 0x1a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
2236            0xf7, 0x0d, 0x01, 0x01, 0x08, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65,
2237            0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0xa2, 0x03, 0x02, 0x01, 0x20,
2238        ];
2239        let (rest, identifier) = AlgorithmIdentifier::from_der(&der)
2240            .expect("standard SHA-256 RSA-PSS AlgorithmIdentifier must parse");
2241        assert!(rest.is_empty());
2242
2243        assert_eq!(
2244            x509_signature_algorithm(&identifier),
2245            Ok(X509SignatureAlgorithm::RsaPss {
2246                digest: super::super::DigestAlgorithm::Sha256,
2247                mgf_digest: super::super::DigestAlgorithm::Sha256,
2248                salt_len: 32,
2249            })
2250        );
2251    }
2252
2253    #[test]
2254    fn parses_sha224_rsa_pss_certificate_parameters() {
2255        // RFC 4055 permits SHA-224 independently for the message digest and
2256        // MGF1. The provider-neutral parser must preserve that capability.
2257        let der = [
2258            0x30, 0x41, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a, 0x30,
2259            0x34, 0xa0, 0x0f, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
2260            0x02, 0x04, 0x05, 0x00, 0xa1, 0x1c, 0x30, 0x1a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
2261            0xf7, 0x0d, 0x01, 0x01, 0x08, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65,
2262            0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0xa2, 0x03, 0x02, 0x01, 0x1c,
2263        ];
2264        let (rest, identifier) = AlgorithmIdentifier::from_der(&der)
2265            .expect("standard SHA-224 RSA-PSS AlgorithmIdentifier must parse");
2266        assert!(rest.is_empty());
2267
2268        assert_eq!(
2269            x509_signature_algorithm(&identifier),
2270            Ok(X509SignatureAlgorithm::RsaPss {
2271                digest: super::super::DigestAlgorithm::Sha224,
2272                mgf_digest: super::super::DigestAlgorithm::Sha224,
2273                salt_len: 28,
2274            })
2275        );
2276    }
2277
2278    #[test]
2279    fn dsa_rollover_replaces_embedded_root_before_depth_validation() {
2280        let leaf = include_bytes!(
2281            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
2282        )
2283        .to_vec();
2284        let embedded_root =
2285            include_bytes!("../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der")
2286                .to_vec();
2287
2288        // Trust-anchor self-signatures are not part of path validation. Changing
2289        // only that signature gives this test a distinct rollover certificate
2290        // with the same subject and DSA public key as the embedded stale root.
2291        let mut rollover_anchor = embedded_root.clone();
2292        *rollover_anchor
2293            .last_mut()
2294            .expect("certificate is non-empty") ^= 1;
2295        parse_certificate(&rollover_anchor).expect("modified trust anchor remains valid DER");
2296        let anchors = vec![rollover_anchor];
2297        let info = X509DataInfo {
2298            certificates: vec![leaf, embedded_root],
2299            certificate_chain: vec![0, 1],
2300            ..X509DataInfo::default()
2301        };
2302        let options = X509ChainOptions {
2303            trusted_certs: &anchors,
2304            verification_time: UNIX_EPOCH + Duration::from_secs(1_104_580_800),
2305            max_chain_depth: 2,
2306            check_crls: false,
2307            allowed_extended_key_usages: None,
2308            rsa_keys: RsaKeyPolicy::default(),
2309            dsa_keys: DsaKeyPolicy {
2310                minimum_modulus_bits: 1024,
2311            },
2312        };
2313
2314        verify_x509_certificate_chain(&info, &options)
2315            .expect("the stale DSA root must be replaced by the configured anchor");
2316    }
2317
2318    #[test]
2319    fn dsa_issuer_key_uses_the_configured_strength_policy() {
2320        let leaf = include_bytes!(
2321            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
2322        )
2323        .to_vec();
2324        let anchor =
2325            include_bytes!("../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der")
2326                .to_vec();
2327        let anchors = vec![anchor.clone()];
2328        let info = X509DataInfo {
2329            certificates: vec![leaf, anchor],
2330            certificate_chain: vec![0, 1],
2331            ..X509DataInfo::default()
2332        };
2333        let options = X509ChainOptions {
2334            trusted_certs: &anchors,
2335            verification_time: UNIX_EPOCH + Duration::from_secs(1_104_580_800),
2336            max_chain_depth: 2,
2337            check_crls: false,
2338            allowed_extended_key_usages: None,
2339            rsa_keys: RsaKeyPolicy::default(),
2340            dsa_keys: DsaKeyPolicy::default(),
2341        };
2342
2343        assert!(matches!(
2344            verify_x509_certificate_chain(&info, &options),
2345            Err(X509ChainError::KeyPolicy {
2346                position: 1,
2347                source: crate::policy::PolicyViolation::KeySize {
2348                    key_type: "DSA",
2349                    minimum_bits: 2048,
2350                    actual_bits: 1024,
2351                    ..
2352                }
2353            })
2354        ));
2355    }
2356
2357    #[test]
2358    fn path_length_excludes_self_issued_rollover_certificates() {
2359        // RFC 5280 excludes self-issued rollover CAs from pathLenConstraint;
2360        // only non-self-issued intermediate CA certificates consume the limit.
2361        let mut root_params = generated_certificate_params("rollover path authority", true);
2362        root_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
2363        let root = rcgen::CertifiedIssuer::self_signed(
2364            root_params,
2365            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2366        )
2367        .expect("root should be self-signable");
2368        let rollover_params = generated_certificate_params("rollover path authority", true);
2369        let rollover_key =
2370            rcgen::KeyPair::generate().expect("rollover key generation should succeed");
2371        let rollover_certificate = rollover_params
2372            .signed_by(&rollover_key, &root)
2373            .expect("root should sign same-name rollover certificate");
2374        let rollover_issuer = rcgen::Issuer::from_params(&rollover_params, &rollover_key);
2375        let leaf = generated_certificate_params("rollover path leaf", false)
2376            .signed_by(
2377                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2378                &rollover_issuer,
2379            )
2380            .expect("rollover key should sign leaf certificate");
2381
2382        verify_generated_path(
2383            vec![
2384                leaf.der().to_vec(),
2385                rollover_certificate.der().to_vec(),
2386                root.der().to_vec(),
2387            ],
2388            root.der().to_vec(),
2389        )
2390        .expect("self-issued rollover must not consume a zero path-length allowance");
2391    }
2392
2393    #[test]
2394    fn ca_name_constraints_reject_disallowed_dns_names() {
2395        let mut root_params = generated_certificate_params("constrained authority", true);
2396        root_params.name_constraints = Some(rcgen::NameConstraints {
2397            permitted_subtrees: vec![rcgen::GeneralSubtree::DnsName("example.com".into())],
2398            excluded_subtrees: vec![rcgen::GeneralSubtree::DnsName("blocked.example.com".into())],
2399        });
2400        let root = rcgen::CertifiedIssuer::self_signed(
2401            root_params,
2402            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2403        )
2404        .expect("constrained root should be self-signable");
2405
2406        for (dns_name, accepted) in [
2407            ("www.example.com", true),
2408            ("blocked.example.com", false),
2409            ("www.example.net", false),
2410        ] {
2411            let leaf = rcgen::CertificateParams::new(vec![dns_name.into()])
2412                .expect("DNS SAN should be valid")
2413                .signed_by(
2414                    &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2415                    &root,
2416                )
2417                .expect("root should sign leaf certificate");
2418            assert_eq!(
2419                verify_generated_path(
2420                    vec![leaf.der().to_vec(), root.der().to_vec()],
2421                    root.der().to_vec(),
2422                )
2423                .is_ok(),
2424                accepted,
2425                "unexpected name-constraint result for {dns_name}"
2426            );
2427        }
2428    }
2429
2430    #[test]
2431    fn rfc5280_dns_names_require_preferred_name_syntax() {
2432        let mut root_params = generated_certificate_params("DNS syntax authority", true);
2433        root_params.name_constraints = Some(rcgen::NameConstraints {
2434            permitted_subtrees: vec![rcgen::GeneralSubtree::DnsName("example.com".into())],
2435            excluded_subtrees: Vec::new(),
2436        });
2437        let root = rcgen::CertifiedIssuer::self_signed(
2438            root_params,
2439            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2440        )
2441        .expect("constrained root should be self-signable");
2442
2443        let mut leaf_params = generated_certificate_params("malformed DNS leaf", false);
2444        let dns_name = b"bad..example.com";
2445        let mut san_der = vec![
2446            0x30,
2447            u8::try_from(dns_name.len() + 2).expect("test SAN must fit short-form DER"),
2448            0x82,
2449        ];
2450        san_der.push(u8::try_from(dns_name.len()).expect("test DNS name must fit short-form DER"));
2451        san_der.extend_from_slice(dns_name);
2452        leaf_params
2453            .custom_extensions
2454            .push(rcgen::CustomExtension::from_oid_content(
2455                &[2, 5, 29, 17],
2456                san_der,
2457            ));
2458        let leaf = leaf_params
2459            .signed_by(
2460                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2461                &root,
2462            )
2463            .expect("root should sign malformed-DNS leaf");
2464
2465        assert!(matches!(
2466            verify_generated_path(
2467                vec![leaf.der().to_vec(), root.der().to_vec()],
2468                root.der().to_vec(),
2469            ),
2470            Err(X509ChainError::InvalidDer {
2471                kind: "certificate DNS name",
2472                ..
2473            })
2474        ));
2475
2476        for dns_name in ["*.example.com", "_signing.example.com"] {
2477            assert!(validate_rfc5280_dns_name(dns_name).is_err(), "{dns_name}");
2478        }
2479    }
2480
2481    #[test]
2482    fn name_constraint_matchers_cover_email_uri_and_ip_forms() {
2483        // RFC 5280 gives each GeneralName form distinct subtree semantics;
2484        // exercise those rules directly so a DNS-only implementation cannot pass.
2485        assert!(email_within_subtree("ops@example.com", "example.com"));
2486        assert!(email_within_subtree("ops@example.com", "ops@example.com"));
2487        assert!(!email_within_subtree(
2488            "other@example.com",
2489            "ops@example.com"
2490        ));
2491        assert_eq!(
2492            uri_host("https://user@api.example.com:8443/path"),
2493            Some("api.example.com")
2494        );
2495        assert_eq!(
2496            uri_host("https://user@other@example.com/path"),
2497            None,
2498            "a second userinfo delimiter must not expose a constraint-matchable host"
2499        );
2500        assert!(dns_name_within_subtree(
2501            uri_host("https://api.example.com/path").expect("URI must expose a DNS host"),
2502            ".example.com",
2503            false,
2504        ));
2505        assert!(
2506            ip_address_within_subtree(&[192, 0, 2, 42], &[192, 0, 2, 0, 255, 255, 255, 0],)
2507                .expect("valid IPv4 constraint must evaluate")
2508        );
2509        assert!(
2510            !ip_address_within_subtree(&[192, 0, 3, 42], &[192, 0, 2, 0, 255, 255, 255, 0],)
2511                .expect("valid non-matching IPv4 constraint must evaluate")
2512        );
2513        assert!(matches!(
2514            ip_address_within_subtree(&[192, 0, 2, 42], &[192, 0, 2, 0, 255, 0, 255, 0],),
2515            Err(X509ChainError::InvalidDer {
2516                kind: "IP name constraint",
2517                ..
2518            })
2519        ));
2520    }
2521
2522    #[test]
2523    fn malformed_ip_name_constraints_fail_before_matching() {
2524        use x509_parser::extensions::GeneralSubtree;
2525
2526        let name = GeneralName::IPAddress(&[192, 0, 2, 42]);
2527        for malformed in [
2528            &[192, 0, 2, 0, 255, 255, 255][..],
2529            &[192, 0, 2, 0, 255, 0, 255, 0][..],
2530        ] {
2531            for permitted in [true, false] {
2532                let subtree = GeneralSubtree {
2533                    base: GeneralName::IPAddress(malformed),
2534                };
2535                let constraints = NameConstraints {
2536                    permitted_subtrees: permitted.then(|| vec![subtree.clone()]),
2537                    excluded_subtrees: (!permitted).then(|| vec![subtree]),
2538                };
2539                assert!(matches!(
2540                    validate_general_name(&name, &constraints, 0, 1),
2541                    Err(X509ChainError::InvalidDer {
2542                        kind: "IP name constraint",
2543                        ..
2544                    })
2545                ));
2546            }
2547        }
2548    }
2549
2550    #[test]
2551    fn malformed_string_name_constraints_fail_before_matching() {
2552        use x509_parser::extensions::GeneralSubtree;
2553
2554        // Matchers assume admitted string constraints have RFC 5280 syntax.
2555        // Invalid values must not degrade into ordinary non-matches.
2556        for malformed in [
2557            GeneralName::DNSName(""),
2558            GeneralName::DNSName("example..com"),
2559            GeneralName::RFC822Name("@example.com"),
2560            GeneralName::RFC822Name("bad..local@example.com"),
2561            GeneralName::URI("https://example.com"),
2562        ] {
2563            let constraints = NameConstraints {
2564                permitted_subtrees: None,
2565                excluded_subtrees: Some(vec![GeneralSubtree { base: malformed }]),
2566            };
2567            assert!(matches!(
2568                ensure_supported_name_constraints(&constraints, 1),
2569                Err(X509ChainError::InvalidDer {
2570                    kind: "string name constraint",
2571                    ..
2572                })
2573            ));
2574        }
2575
2576        for valid in [
2577            GeneralName::DNSName("example.com"),
2578            GeneralName::DNSName(".example.com"),
2579            GeneralName::RFC822Name("ops@example.com"),
2580            GeneralName::RFC822Name("example.com"),
2581            GeneralName::URI(".example.com"),
2582        ] {
2583            let constraints = NameConstraints {
2584                permitted_subtrees: Some(vec![GeneralSubtree { base: valid }]),
2585                excluded_subtrees: None,
2586            };
2587            ensure_supported_name_constraints(&constraints, 1)
2588                .expect("valid string constraints must remain supported");
2589        }
2590    }
2591
2592    #[test]
2593    fn empty_name_constraint_collections_are_rejected() {
2594        use der::Encode as _;
2595        use x509_cert::ext::pkix::NameConstraints as EncodedNameConstraints;
2596
2597        // RFC 5280 requires at least one subtree overall and at least one entry
2598        // in every explicitly present GeneralSubtrees collection.
2599        for constraints in [
2600            EncodedNameConstraints {
2601                permitted_subtrees: None,
2602                excluded_subtrees: None,
2603            },
2604            EncodedNameConstraints {
2605                permitted_subtrees: Some(Vec::new()),
2606                excluded_subtrees: None,
2607            },
2608            EncodedNameConstraints {
2609                permitted_subtrees: None,
2610                excluded_subtrees: Some(Vec::new()),
2611            },
2612        ] {
2613            let der = constraints
2614                .to_der()
2615                .expect("malformed NameConstraints test input must encode");
2616            assert!(matches!(
2617                validate_name_constraints_der(&der, 1),
2618                Err(X509ChainError::InvalidNameConstraints { position: 1 })
2619            ));
2620        }
2621    }
2622
2623    #[test]
2624    fn unsupported_name_constraint_distances_fail_path_validation() {
2625        use der::{Encode as _, asn1::Ia5String};
2626        use x509_cert::ext::pkix::{
2627            NameConstraints as EncodedNameConstraints,
2628            constraints::name::GeneralSubtree as EncodedGeneralSubtree,
2629            name::GeneralName as EncodedGeneralName,
2630        };
2631
2632        // x509-parser exposes only GeneralSubtree::base. Exercise the complete
2633        // extension DER so unsupported distance fields cannot disappear before
2634        // RFC 5280 path validation sees them.
2635        for (permitted, minimum, maximum) in [
2636            (true, 1, None),
2637            (false, 1, None),
2638            (true, 0, Some(1)),
2639            (false, 0, Some(1)),
2640        ] {
2641            let dns_name = if permitted {
2642                "example.com"
2643            } else {
2644                "blocked.example.com"
2645            };
2646            let subtree = EncodedGeneralSubtree {
2647                base: EncodedGeneralName::DnsName(
2648                    Ia5String::new(dns_name.as_bytes()).expect("valid DNS IA5String"),
2649                ),
2650                minimum,
2651                maximum,
2652            };
2653            let constraints = EncodedNameConstraints {
2654                permitted_subtrees: permitted.then(|| vec![subtree.clone()]),
2655                excluded_subtrees: (!permitted).then(|| vec![subtree]),
2656            };
2657            let mut extension = rcgen::CustomExtension::from_oid_content(
2658                &[2, 5, 29, 30],
2659                constraints
2660                    .to_der()
2661                    .expect("NameConstraints must encode as DER"),
2662            );
2663            extension.set_criticality(true);
2664
2665            let mut root_params = generated_certificate_params("distance authority", true);
2666            root_params.custom_extensions.push(extension);
2667            let root = rcgen::CertifiedIssuer::self_signed(
2668                root_params,
2669                rcgen::KeyPair::generate().expect("root key generation should succeed"),
2670            )
2671            .expect("constrained root should be self-signable");
2672            let leaf = rcgen::CertificateParams::new(vec!["www.example.com".into()])
2673                .expect("leaf DNS SAN should be valid")
2674                .signed_by(
2675                    &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2676                    &root,
2677                )
2678                .expect("root should sign leaf certificate");
2679
2680            assert!(matches!(
2681                verify_generated_path(
2682                    vec![leaf.der().to_vec(), root.der().to_vec()],
2683                    root.der().to_vec(),
2684                ),
2685                Err(X509ChainError::InvalidNameConstraints { position: 1 })
2686            ));
2687        }
2688    }
2689
2690    #[test]
2691    fn name_constraints_cover_subject_email_and_directory_name() {
2692        // RFC 5280 requires subject emailAddress attributes to be checked even
2693        // without a SAN, and directoryName constraints compare RDN subtrees.
2694        let mut permitted_directory = rcgen::DistinguishedName::new();
2695        permitted_directory.push(rcgen::DnType::OrganizationName, "Example Corp");
2696        let mut root_params = generated_certificate_params("name authority", true);
2697        root_params.name_constraints = Some(rcgen::NameConstraints {
2698            permitted_subtrees: vec![
2699                rcgen::GeneralSubtree::Rfc822Name("example.com".into()),
2700                rcgen::GeneralSubtree::DirectoryName(permitted_directory),
2701            ],
2702            excluded_subtrees: Vec::new(),
2703        });
2704        let root = rcgen::CertifiedIssuer::self_signed(
2705            root_params,
2706            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2707        )
2708        .expect("constrained root should be self-signable");
2709
2710        for (organization, email, accepted) in [
2711            ("Example Corp", "ops@example.com", true),
2712            ("Other Corp", "ops@example.com", false),
2713            ("Example Corp", "ops@example.net", false),
2714            ("Example Corp", "bad..local@example.com", false),
2715        ] {
2716            let mut leaf_params = generated_certificate_params("name-constrained leaf", false);
2717            leaf_params.distinguished_name = rcgen::DistinguishedName::new();
2718            leaf_params
2719                .distinguished_name
2720                .push(rcgen::DnType::OrganizationName, organization);
2721            leaf_params
2722                .distinguished_name
2723                .push(rcgen::DnType::CommonName, "name-constrained leaf");
2724            leaf_params.distinguished_name.push(
2725                rcgen::DnType::CustomDnType(vec![1, 2, 840, 113549, 1, 9, 1]),
2726                rcgen::DnValue::Ia5String(
2727                    email
2728                        .try_into()
2729                        .expect("test email must be a valid IA5String"),
2730                ),
2731            );
2732            let leaf = leaf_params
2733                .signed_by(
2734                    &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2735                    &root,
2736                )
2737                .expect("root should sign leaf certificate");
2738            let result = verify_generated_path(
2739                vec![leaf.der().to_vec(), root.der().to_vec()],
2740                root.der().to_vec(),
2741            );
2742            assert_eq!(
2743                result.is_ok(),
2744                accepted,
2745                "unexpected subject constraint result for {organization} / {email}: {result:?}",
2746            );
2747        }
2748    }
2749
2750    #[test]
2751    fn empty_subject_requires_a_critical_nonempty_san() {
2752        let root = rcgen::CertifiedIssuer::self_signed(
2753            generated_certificate_params("subject identity authority", true),
2754            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2755        )
2756        .expect("root should be self-signable");
2757
2758        let mut missing_san = rcgen::CertificateParams::new(Vec::new())
2759            .expect("empty SAN list should produce certificate parameters");
2760        missing_san.distinguished_name = rcgen::DistinguishedName::new();
2761        let missing_san = missing_san
2762            .signed_by(
2763                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2764                &root,
2765            )
2766            .expect("test issuer should sign an empty-subject certificate");
2767
2768        let mut noncritical_san = rcgen::CertificateParams::new(Vec::new())
2769            .expect("empty SAN list should produce certificate parameters");
2770        noncritical_san.distinguished_name = rcgen::DistinguishedName::new();
2771        // GeneralNames ::= SEQUENCE { dNSName [2] "a" }. Using a custom
2772        // extension is intentional because rcgen correctly marks its normal
2773        // SAN extension critical whenever the subject is empty.
2774        noncritical_san
2775            .custom_extensions
2776            .push(rcgen::CustomExtension::from_oid_content(
2777                &[2, 5, 29, 17],
2778                vec![0x30, 0x03, 0x82, 0x01, b'a'],
2779            ));
2780        let noncritical_san = noncritical_san
2781            .signed_by(
2782                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2783                &root,
2784            )
2785            .expect("test issuer should sign a non-critical-SAN certificate");
2786
2787        for leaf in [missing_san, noncritical_san] {
2788            assert!(matches!(
2789                verify_generated_path(
2790                    vec![leaf.der().to_vec(), root.der().to_vec()],
2791                    root.der().to_vec(),
2792                ),
2793                Err(X509ChainError::InvalidDer {
2794                    kind: "certificate subject identity",
2795                    ..
2796                })
2797            ));
2798        }
2799    }
2800
2801    #[test]
2802    fn empty_subject_with_critical_san_skips_directory_name_constraints() {
2803        // RFC 5280 permits an empty subject when a critical SAN carries the
2804        // identity. An absent DirectoryName need not match a permitted subtree.
2805        let mut permitted_directory = rcgen::DistinguishedName::new();
2806        permitted_directory.push(rcgen::DnType::OrganizationName, "Example Corp");
2807        let mut root_params = generated_certificate_params("empty-subject authority", true);
2808        root_params.name_constraints = Some(rcgen::NameConstraints {
2809            permitted_subtrees: vec![rcgen::GeneralSubtree::DirectoryName(permitted_directory)],
2810            excluded_subtrees: Vec::new(),
2811        });
2812        let root = rcgen::CertifiedIssuer::self_signed(
2813            root_params,
2814            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2815        )
2816        .expect("constrained root should be self-signable");
2817        let mut leaf_params = rcgen::CertificateParams::new(vec!["allowed.example".into()])
2818            .expect("DNS SAN should be valid");
2819        leaf_params.distinguished_name = rcgen::DistinguishedName::new();
2820        let leaf = leaf_params
2821            .signed_by(
2822                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2823                &root,
2824            )
2825            .expect("root should sign empty-subject leaf");
2826
2827        verify_generated_path(
2828            vec![leaf.der().to_vec(), root.der().to_vec()],
2829            root.der().to_vec(),
2830        )
2831        .expect("only present name forms should be constrained");
2832    }
2833
2834    #[test]
2835    fn malformed_general_names_in_san_fail_path_validation() {
2836        let root = rcgen::CertifiedIssuer::self_signed(
2837            generated_certificate_params("malformed-SAN root", true),
2838            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2839        )
2840        .expect("root should be self-signable");
2841        for empty_subject in [true, false] {
2842            let mut leaf_params = generated_certificate_params("malformed-SAN leaf", false);
2843            if empty_subject {
2844                leaf_params.distinguished_name = rcgen::DistinguishedName::new();
2845            }
2846            // GeneralNames ::= SEQUENCE { dNSName [2] <invalid IA5 octet> }.
2847            let mut malformed_san = rcgen::CustomExtension::from_oid_content(
2848                &[2, 5, 29, 17],
2849                vec![0x30, 0x03, 0x82, 0x01, 0xff],
2850            );
2851            malformed_san.set_criticality(true);
2852            leaf_params.custom_extensions.push(malformed_san);
2853            let leaf = leaf_params
2854                .signed_by(
2855                    &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2856                    &root,
2857                )
2858                .expect("root should sign malformed-SAN leaf");
2859
2860            assert!(matches!(
2861                verify_generated_path(
2862                    vec![leaf.der().to_vec(), root.der().to_vec()],
2863                    root.der().to_vec(),
2864                ),
2865                Err(X509ChainError::InvalidDer {
2866                    kind: "certificate subject identity",
2867                    ..
2868                })
2869            ));
2870        }
2871    }
2872
2873    #[test]
2874    fn typed_subject_alternative_names_require_rfc5280_syntax() {
2875        let root = rcgen::CertifiedIssuer::self_signed(
2876            generated_certificate_params("typed-SAN root", true),
2877            rcgen::KeyPair::generate().expect("root key generation should succeed"),
2878        )
2879        .expect("root should be self-signable");
2880
2881        for (tag, value) in [
2882            (0x81, b"operator@".as_slice()),
2883            (0x81, b"first..last@example.com".as_slice()),
2884            (0x86, b"relative/path".as_slice()),
2885            (0x86, b"https://example.com/%zz".as_slice()),
2886            (0x86, b"https://user@other@example.com/path".as_slice()),
2887            (0x86, b"file:///path".as_slice()),
2888            (0x87, &[192, 0, 2][..]),
2889        ] {
2890            for empty_subject in [false, true] {
2891                let mut leaf_params = generated_certificate_params("typed-SAN leaf", false);
2892                if empty_subject {
2893                    leaf_params.distinguished_name = rcgen::DistinguishedName::new();
2894                }
2895                let mut san_der = vec![
2896                    0x30,
2897                    u8::try_from(value.len() + 2).expect("test SAN must fit short-form DER"),
2898                    tag,
2899                    u8::try_from(value.len()).expect("test GeneralName must fit short-form DER"),
2900                ];
2901                san_der.extend_from_slice(value);
2902                let mut san = rcgen::CustomExtension::from_oid_content(&[2, 5, 29, 17], san_der);
2903                san.set_criticality(true);
2904                leaf_params.custom_extensions.push(san);
2905                let leaf = leaf_params
2906                    .signed_by(
2907                        &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2908                        &root,
2909                    )
2910                    .expect("root should sign typed-SAN leaf");
2911
2912                assert!(matches!(
2913                    verify_generated_path(
2914                        vec![leaf.der().to_vec(), root.der().to_vec()],
2915                        root.der().to_vec(),
2916                    ),
2917                    Err(X509ChainError::InvalidDer {
2918                        kind: "certificate subject identity",
2919                        ..
2920                    })
2921                ));
2922            }
2923        }
2924
2925        for (tag, value) in [
2926            (0x81, b"operator@example.com".as_slice()),
2927            (0x81, b"operator@[192.0.2.1]".as_slice()),
2928            (0x81, b"operator@[IPv6:2001:db8::1]".as_slice()),
2929            (0x81, br#""operator desk"@example.com"#.as_slice()),
2930            (0x86, b"urn:example:operator".as_slice()),
2931            (
2932                0x86,
2933                b"https://operator@example.com:8443/path?q=1#id".as_slice(),
2934            ),
2935            (0x87, &[192, 0, 2, 1][..]),
2936            (
2937                0x87,
2938                &[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1][..],
2939            ),
2940        ] {
2941            let mut leaf_params = rcgen::CertificateParams::new(Vec::new())
2942                .expect("empty SAN list should produce certificate parameters");
2943            leaf_params.distinguished_name = rcgen::DistinguishedName::new();
2944            let mut san_der = vec![
2945                0x30,
2946                u8::try_from(value.len() + 2).expect("test SAN must fit short-form DER"),
2947                tag,
2948                u8::try_from(value.len()).expect("test GeneralName must fit short-form DER"),
2949            ];
2950            san_der.extend_from_slice(value);
2951            let mut san = rcgen::CustomExtension::from_oid_content(&[2, 5, 29, 17], san_der);
2952            san.set_criticality(true);
2953            leaf_params.custom_extensions.push(san);
2954            let leaf = leaf_params
2955                .signed_by(
2956                    &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
2957                    &root,
2958                )
2959                .expect("root should sign typed-SAN leaf");
2960
2961            verify_generated_path(
2962                vec![leaf.der().to_vec(), root.der().to_vec()],
2963                root.der().to_vec(),
2964            )
2965            .expect("valid typed SAN identity must satisfy an empty subject");
2966        }
2967    }
2968
2969    fn parsed_merlin_crl(der: &[u8]) -> CertificateRevocationList<'_> {
2970        CertificateRevocationList::from_der(der)
2971            .expect("modified Merlin CRL must remain parseable")
2972            .1
2973    }
2974
2975    fn merlin_crl_der() -> Vec<u8> {
2976        let xml = include_str!(
2977            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml"
2978        );
2979        let document = Document::parse(xml).expect("tracked Merlin document must parse");
2980        let key_info_node = document
2981            .descendants()
2982            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
2983            .expect("tracked Merlin document contains KeyInfo");
2984        let key_info = parse_key_info(key_info_node).expect("tracked Merlin KeyInfo must parse");
2985        let KeyInfoSource::X509Data(info) = &key_info.sources[0] else {
2986            panic!("expected X509Data")
2987        };
2988        info.crls[0].clone()
2989    }
2990
2991    #[test]
2992    fn duplicate_crl_and_entry_extension_oids_fail_closed() {
2993        use der::{Decode as _, Encode as _};
2994        use x509_cert::crl::CertificateList;
2995
2996        let original = merlin_crl_der();
2997        let mut duplicate_crl: CertificateList =
2998            CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
2999        let extensions = duplicate_crl
3000            .tbs_cert_list
3001            .crl_extensions
3002            .as_mut()
3003            .expect("tracked Merlin CRL must contain extensions");
3004        extensions.push(extensions[0].clone());
3005        let duplicate_crl = duplicate_crl
3006            .to_der()
3007            .expect("duplicate CRL extension test vector must encode");
3008        assert_eq!(
3009            validate_crl_extensions(&parsed_merlin_crl(&duplicate_crl), 0),
3010            Err(X509ChainError::InvalidCrl(0))
3011        );
3012
3013        let mut duplicate_entry: CertificateList =
3014            CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
3015        let duplicate = duplicate_entry
3016            .tbs_cert_list
3017            .crl_extensions
3018            .as_ref()
3019            .and_then(|extensions| extensions.first())
3020            .expect("tracked Merlin CRL must contain an extension")
3021            .clone();
3022        let revoked = duplicate_entry
3023            .tbs_cert_list
3024            .revoked_certificates
3025            .as_mut()
3026            .and_then(|entries| entries.first_mut())
3027            .expect("tracked Merlin CRL must contain a revoked entry");
3028        revoked.crl_entry_extensions = Some(vec![duplicate.clone(), duplicate]);
3029        let duplicate_entry = duplicate_entry
3030            .to_der()
3031            .expect("duplicate entry extension test vector must encode");
3032        assert_eq!(
3033            validate_crl_extensions(&parsed_merlin_crl(&duplicate_entry), 0),
3034            Err(X509ChainError::InvalidCrl(0))
3035        );
3036    }
3037
3038    #[test]
3039    fn malformed_revoked_certificate_serials_fail_closed() {
3040        // Mutate the signed Merlin CRL fixture without changing DER lengths so
3041        // zero and negative serials exercise the actual CRL parser path.
3042        let original = merlin_crl_der();
3043        let serial = parsed_merlin_crl(&original)
3044            .iter_revoked_certificates()
3045            .next()
3046            .expect("tracked Merlin CRL must contain a revoked entry")
3047            .raw_serial()
3048            .to_vec();
3049        let offsets = original
3050            .windows(serial.len())
3051            .enumerate()
3052            .filter_map(|(offset, bytes)| (bytes == serial).then_some(offset))
3053            .collect::<Vec<_>>();
3054        assert_eq!(
3055            offsets.len(),
3056            1,
3057            "revoked serial fixture must be unambiguous"
3058        );
3059
3060        for replacement in [vec![0; serial.len()], {
3061            let mut negative = serial.clone();
3062            negative[0] = 0x80;
3063            negative
3064        }] {
3065            let mut malformed = original.clone();
3066            malformed[offsets[0]..offsets[0] + serial.len()].copy_from_slice(&replacement);
3067            assert_eq!(
3068                validate_crl_extensions(&parsed_merlin_crl(&malformed), 0),
3069                Err(X509ChainError::InvalidCrl(0))
3070            );
3071        }
3072    }
3073
3074    #[test]
3075    fn delta_crl_indicator_is_rejected_regardless_of_criticality() {
3076        use der::{Decode as _, Encode as _, asn1::OctetString};
3077        use x509_cert::{crl::CertificateList, ext::Extension};
3078
3079        let original = merlin_crl_der();
3080        for critical in [false, true] {
3081            let mut encoded: CertificateList =
3082                CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
3083            encoded
3084                .tbs_cert_list
3085                .crl_extensions
3086                .get_or_insert_default()
3087                .push(Extension {
3088                    extn_id: der::asn1::ObjectIdentifier::new_unwrap("2.5.29.27"),
3089                    critical,
3090                    extn_value: OctetString::new([0x02, 0x01, 0x01])
3091                        .expect("DER INTEGER extension payload must be valid"),
3092                });
3093            let encoded = encoded
3094                .to_der()
3095                .expect("delta CRL indicator test vector must encode");
3096            assert_eq!(
3097                validate_crl_extensions(&parsed_merlin_crl(&encoded), 0),
3098                Err(X509ChainError::InvalidCrl(0)),
3099                "delta CRL indicator criticality must not change unsupported semantics"
3100            );
3101        }
3102    }
3103
3104    #[test]
3105    fn remove_from_crl_is_rejected_in_a_complete_crl() {
3106        use der::{Decode as _, Encode as _, asn1::OctetString};
3107        use x509_cert::{crl::CertificateList, ext::Extension};
3108
3109        let original = merlin_crl_der();
3110        for (reason, accepted) in [(1_u8, true), (8_u8, false)] {
3111            let mut encoded: CertificateList =
3112                CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
3113            let revoked = encoded
3114                .tbs_cert_list
3115                .revoked_certificates
3116                .as_mut()
3117                .and_then(|entries| entries.first_mut())
3118                .expect("tracked Merlin CRL must contain a revoked entry");
3119            revoked
3120                .crl_entry_extensions
3121                .get_or_insert_default()
3122                .push(Extension {
3123                    extn_id: der::asn1::ObjectIdentifier::new_unwrap("2.5.29.21"),
3124                    critical: false,
3125                    extn_value: OctetString::new([0x0a, 0x01, reason])
3126                        .expect("DER ENUMERATED extension payload must be valid"),
3127                });
3128            let encoded = encoded
3129                .to_der()
3130                .expect("reason-code CRL test vector must encode");
3131            let result = validate_crl_extensions(&parsed_merlin_crl(&encoded), 0);
3132            if accepted {
3133                assert_eq!(result, Ok(()), "ordinary revocation reasons remain valid");
3134            } else {
3135                assert_eq!(result, Err(X509ChainError::InvalidCrl(0)));
3136            }
3137        }
3138    }
3139
3140    #[test]
3141    fn unevaluable_uri_names_fail_closed_for_both_constraint_forms() {
3142        use x509_parser::extensions::GeneralSubtree;
3143
3144        // A URI without a DNS host is not a non-match: treating it that way
3145        // would bypass excluded URI subtrees while rejecting permitted ones.
3146        let uri = GeneralName::URI("urn:example:opaque");
3147        for constraints in [
3148            NameConstraints {
3149                permitted_subtrees: Some(vec![GeneralSubtree {
3150                    base: GeneralName::URI(".example.com"),
3151                }]),
3152                excluded_subtrees: None,
3153            },
3154            NameConstraints {
3155                permitted_subtrees: None,
3156                excluded_subtrees: Some(vec![GeneralSubtree {
3157                    base: GeneralName::URI(".example.com"),
3158                }]),
3159            },
3160        ] {
3161            assert_eq!(
3162                validate_general_name(&uri, &constraints, 0, 1),
3163                Err(X509ChainError::NameConstraintViolation {
3164                    position: 0,
3165                    constraining_position: 1,
3166                })
3167            );
3168        }
3169    }
3170
3171    #[test]
3172    fn unknown_critical_certificate_extension_fails_closed() {
3173        let root = rcgen::CertifiedIssuer::self_signed(
3174            generated_certificate_params("critical-extension root", true),
3175            rcgen::KeyPair::generate().expect("root key generation should succeed"),
3176        )
3177        .expect("root should be self-signable");
3178        let mut leaf_params = generated_certificate_params("critical-extension leaf", false);
3179        let mut extension =
3180            rcgen::CustomExtension::from_oid_content(&[1, 2, 3, 4], vec![0x05, 0x00]);
3181        extension.set_criticality(true);
3182        leaf_params.custom_extensions.push(extension);
3183        let leaf = leaf_params
3184            .signed_by(
3185                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
3186                &root,
3187            )
3188            .expect("root should sign leaf certificate");
3189
3190        assert_eq!(
3191            verify_generated_path(
3192                vec![leaf.der().to_vec(), root.der().to_vec()],
3193                root.der().to_vec(),
3194            ),
3195            Err(X509ChainError::UnsupportedCriticalExtension {
3196                position: 0,
3197                oid: "1.2.3.4".into(),
3198            })
3199        );
3200    }
3201
3202    #[test]
3203    fn duplicate_certificate_extension_oids_fail_closed() {
3204        // RFC 5280 forbids repeated extension OIDs. Enforce that certificate-wide
3205        // invariant before individual extension consumers select a first match.
3206        let root = rcgen::CertifiedIssuer::self_signed(
3207            generated_certificate_params("duplicate-extension root", true),
3208            rcgen::KeyPair::generate().expect("root key generation should succeed"),
3209        )
3210        .expect("root should be self-signable");
3211        let mut leaf_params = generated_certificate_params("duplicate-extension leaf", false);
3212        for _ in 0..2 {
3213            leaf_params
3214                .custom_extensions
3215                .push(rcgen::CustomExtension::from_oid_content(
3216                    &[1, 2, 3, 4],
3217                    vec![0x05, 0x00],
3218                ));
3219        }
3220        let leaf = leaf_params
3221            .signed_by(
3222                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
3223                &root,
3224            )
3225            .expect("root should sign leaf certificate");
3226
3227        assert_eq!(
3228            verify_generated_path(
3229                vec![leaf.der().to_vec(), root.der().to_vec()],
3230                root.der().to_vec(),
3231            ),
3232            Err(X509ChainError::DuplicateExtension {
3233                position: 0,
3234                oid: "1.2.3.4".into(),
3235            })
3236        );
3237    }
3238
3239    #[test]
3240    fn invalid_certificate_serial_numbers_fail_path_validation() {
3241        for serial in [vec![0], vec![1; 21]] {
3242            let root = rcgen::CertifiedIssuer::self_signed(
3243                generated_certificate_params("serial root", true),
3244                rcgen::KeyPair::generate().expect("root key generation should succeed"),
3245            )
3246            .expect("root should be self-signable");
3247            let mut leaf_params = generated_certificate_params("invalid serial leaf", false);
3248            leaf_params.serial_number = Some(rcgen::SerialNumber::from_slice(&serial));
3249            let leaf = leaf_params
3250                .signed_by(
3251                    &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
3252                    &root,
3253                )
3254                .expect("root should sign leaf certificate");
3255
3256            assert!(matches!(
3257                verify_generated_path(
3258                    vec![leaf.der().to_vec(), root.der().to_vec()],
3259                    root.der().to_vec(),
3260                ),
3261                Err(X509ChainError::InvalidDer {
3262                    kind: "certificate serial number",
3263                    ..
3264                })
3265            ));
3266        }
3267
3268        assert!(validate_positive_serial_bytes(&[0x80], "certificate serial number").is_err());
3269        assert!(validate_positive_serial_bytes(&[1; 20], "certificate serial number").is_ok());
3270
3271        let root = rcgen::CertifiedIssuer::self_signed(
3272            generated_certificate_params("serial-padding root", true),
3273            rcgen::KeyPair::generate().expect("root key generation should succeed"),
3274        )
3275        .expect("root should be self-signable");
3276        let mut leaf_params = generated_certificate_params("serial-padding leaf", false);
3277        leaf_params.serial_number = Some(rcgen::SerialNumber::from_slice(&[0x80; 20]));
3278        let leaf = leaf_params
3279            .signed_by(
3280                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
3281                &root,
3282            )
3283            .expect("root should sign a maximum-magnitude serial");
3284
3285        verify_generated_path(
3286            vec![leaf.der().to_vec(), root.der().to_vec()],
3287            root.der().to_vec(),
3288        )
3289        .expect("a 20-octet magnitude may require a DER sign-padding octet");
3290    }
3291
3292    #[test]
3293    fn name_constraints_are_rejected_on_end_entity_certificates() {
3294        // RFC 5280 limits NameConstraints to critical CA extensions; merely
3295        // parsing the extension on an end entity must not count as processing it.
3296        let root = rcgen::CertifiedIssuer::self_signed(
3297            generated_certificate_params("name-placement root", true),
3298            rcgen::KeyPair::generate().expect("root key generation should succeed"),
3299        )
3300        .expect("root should be self-signable");
3301        let mut leaf_params = generated_certificate_params("name-placement leaf", false);
3302        leaf_params.name_constraints = Some(rcgen::NameConstraints {
3303            permitted_subtrees: vec![rcgen::GeneralSubtree::DnsName("example.com".into())],
3304            excluded_subtrees: Vec::new(),
3305        });
3306        let leaf = leaf_params
3307            .signed_by(
3308                &rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
3309                &root,
3310            )
3311            .expect("root should sign leaf certificate");
3312
3313        assert!(matches!(
3314            verify_generated_path(
3315                vec![leaf.der().to_vec(), root.der().to_vec()],
3316                root.der().to_vec(),
3317            ),
3318            Err(X509ChainError::InvalidNameConstraints { position: 0 })
3319        ));
3320    }
3321
3322    #[test]
3323    fn dsa_certificate_rejects_mismatched_inner_signature_algorithm() {
3324        // The signed TBSCertificate algorithm is a separate RFC 5280 invariant;
3325        // a valid signature over the original bytes must not bypass a mismatch
3326        // in the parsed metadata through the legacy DSA fallback.
3327        let (_, mut certificate) = X509Certificate::from_der(include_bytes!(
3328            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
3329        ))
3330        .expect("the tracked Merlin certificate is valid DER");
3331        let (_, issuer) = X509Certificate::from_der(include_bytes!(
3332            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der"
3333        ))
3334        .expect("the tracked Merlin issuer is a DER certificate");
3335        assert!(verify_certificate_signature(&certificate, &issuer));
3336
3337        certificate.tbs_certificate.signature = issuer.public_key().algorithm.clone();
3338
3339        assert_ne!(
3340            certificate.tbs_certificate.signature,
3341            certificate.signature_algorithm
3342        );
3343        assert!(!verify_certificate_signature(&certificate, &issuer));
3344    }
3345
3346    #[test]
3347    fn dsa_sha1_crl_signature_uses_the_same_fallback_as_certificates() {
3348        let xml = include_str!(
3349            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml"
3350        );
3351        let document = Document::parse(xml).expect("the tracked Merlin document is valid XML");
3352        let key_info_node = document
3353            .descendants()
3354            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3355            .expect("the Merlin document contains KeyInfo");
3356        let key_info = parse_key_info(key_info_node).expect("the Merlin KeyInfo is valid");
3357        let KeyInfoSource::X509Data(info) = &key_info.sources[0] else {
3358            panic!("expected X509Data")
3359        };
3360        let (_, issuer) = X509Certificate::from_der(include_bytes!(
3361            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der"
3362        ))
3363        .expect("the tracked Merlin issuer is a DER certificate");
3364        let (_, crl) = CertificateRevocationList::from_der(&info.crls[0])
3365            .expect("the tracked Merlin CRL is valid DER");
3366
3367        assert!(verify_crl_signature(&crl, &issuer));
3368    }
3369
3370    #[test]
3371    fn dsa_crl_rejects_mismatched_inner_signature_algorithm() {
3372        let xml = include_str!(
3373            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml"
3374        );
3375        let document = Document::parse(xml).expect("the tracked Merlin document is valid XML");
3376        let key_info_node = document
3377            .descendants()
3378            .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
3379            .expect("the Merlin document contains KeyInfo");
3380        let key_info = parse_key_info(key_info_node).expect("the Merlin KeyInfo is valid");
3381        let KeyInfoSource::X509Data(info) = &key_info.sources[0] else {
3382            panic!("expected X509Data")
3383        };
3384        let (_, issuer) = X509Certificate::from_der(include_bytes!(
3385            "../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der"
3386        ))
3387        .expect("the tracked Merlin issuer is a DER certificate");
3388        let (_, mut crl) = CertificateRevocationList::from_der(&info.crls[0])
3389            .expect("the tracked Merlin CRL is valid DER");
3390        assert!(verify_crl_signature(&crl, &issuer));
3391
3392        crl.tbs_cert_list.signature = issuer.public_key().algorithm.clone();
3393
3394        assert_ne!(crl.tbs_cert_list.signature, crl.signature_algorithm);
3395        assert!(!verify_crl_signature(&crl, &issuer));
3396    }
3397}