use std::{collections::HashSet, time::SystemTime};
use x509_parser::x509::AlgorithmIdentifier;
#[cfg(test)]
use x509_parser::{
certificate::X509Certificate, prelude::FromDer, revocation_list::CertificateRevocationList,
time::ASN1Time,
};
use super::X509DataInfo;
use super::pkix::X509DataSource;
use crate::{
policy::{DsaKeyPolicy, ExtendedKeyPurpose, RsaKeyPolicy},
provider::X509SignatureAlgorithm,
};
#[derive(Debug, Clone)]
pub struct X509ChainOptions<'a> {
pub utc_facts: Option<structured_pkix::temporal::LeapTable<'a>>,
pub trusted_certs: &'a [Vec<u8>],
pub verification_time: SystemTime,
pub max_chain_depth: usize,
pub check_crls: bool,
pub allowed_extended_key_usages: Option<&'a HashSet<ExtendedKeyPurpose>>,
pub rsa_keys: RsaKeyPolicy,
pub dsa_keys: DsaKeyPolicy,
pub certificate_signature_algorithms: Option<&'a crate::policy::CertificateSignatureAlgorithms>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum X509ChainError {
#[error("X.509 processing failed: {0}")]
Processing(#[source] structured_pkix::Error),
#[error("X.509 signature policy rejected authentication: {0}")]
Policy(#[from] crate::policy::PolicyViolation),
#[error("cryptographic provider rejected X.509 authentication: {0}")]
Provider(#[from] crate::provider::ProviderError),
#[error("certificate signature at chain position {0} is unsupported")]
UnsupportedSignature(usize),
#[error("maximum certificate chain depth must be greater than zero")]
InvalidDepth,
#[error("invalid {kind} DER: {message}")]
InvalidDer {
kind: &'static str,
message: String,
},
#[error("certificate at chain position {position} repeats extension {oid}")]
DuplicateExtension {
position: usize,
oid: String,
},
#[error("certificate chain does not terminate at a trusted certificate")]
UntrustedRoot,
#[error("certificate chain exceeds maximum depth of {0}")]
DepthExceeded(usize),
#[error("certificate path checking stopped at {0:?}")]
PathIncomplete(structured_pkix::path::Truncation),
#[error("certificate path policy constraints are not satisfied")]
CertificatePolicies,
#[error("certificate at chain position {0} is expired or not yet valid")]
CertificateNotValid(usize),
#[error("certificate at chain position {0} is not a CA")]
IssuerNotCa(usize),
#[error("certificate at chain position {position} exceeds path length constraint {limit}")]
PathLengthExceeded {
position: usize,
limit: u32,
},
#[error(
"certificate at chain position {position} violates name constraints from position {constraining_position}"
)]
NameConstraintViolation {
position: usize,
constraining_position: usize,
},
#[error("certificate at chain position {position} has unsupported critical extension {oid}")]
UnsupportedCriticalExtension {
position: usize,
oid: String,
},
#[error("certificate at chain position {position} has invalid NameConstraints placement")]
InvalidNameConstraints {
position: usize,
},
#[error("certificate at chain position {position} does not permit {required}")]
InvalidKeyUsage {
position: usize,
required: &'static str,
},
#[error("certificate signature at chain position {0} is invalid or unsupported")]
InvalidSignature(usize),
#[error("certificate issuer key at chain position {position} is rejected by policy: {source}")]
KeyPolicy {
position: usize,
source: crate::policy::PolicyViolation,
},
#[error("unsupported X.509 signature algorithm: {oid}")]
UnsupportedSignatureAlgorithm {
oid: String,
},
#[error("CRL {0} is invalid or cannot be authenticated")]
InvalidCrl(usize),
#[error("CRL evidence has incomplete reason coverage at chain position {0}")]
IncompleteCrlCoverage(usize),
#[error("CRL validation requires caller-supplied UTC facts")]
MissingUtcFacts,
#[error("UTC evidence cannot establish CRL {index} time: {reason:?}")]
CrlTimeUncertain {
index: usize,
reason: structured_pkix::temporal::Uncertainty,
},
#[error("certificate at chain position {0} is revoked")]
Revoked(usize),
}
impl X509ChainError {
pub(crate) fn is_execution_failure(&self) -> bool {
match self {
Self::Processing(error) => error.is_resource(),
Self::Policy(
crate::policy::PolicyViolation::ResourceLimit { .. }
| crate::policy::PolicyViolation::ResourceLimitExceeded { .. },
) => true,
Self::Provider(crate::provider::ProviderError::Unsupported { .. }) => false,
Self::Provider(_) => true,
Self::PathIncomplete(reason) => !matches!(
reason,
structured_pkix::path::Truncation::Depth
| structured_pkix::path::Truncation::Paths
| structured_pkix::path::Truncation::Policies
),
_ => false,
}
}
}
pub fn verify_x509_certificate_chain(
info: &X509DataInfo,
options: &X509ChainOptions<'_>,
) -> Result<(), X509ChainError> {
verify_x509_certificate_chain_with_resources(
info,
options,
&crate::policy::ResourcePolicy::default(),
crate::provider::default_provider(),
)
}
pub fn verify_x509_certificate_chain_with_resources(
info: &X509DataInfo,
options: &X509ChainOptions<'_>,
resources: &crate::policy::ResourcePolicy,
provider: &dyn crate::provider::CryptoProvider,
) -> Result<(), X509ChainError> {
resources.validate()?;
let session = super::pkix::CertificateSession::new(resources);
let count = info
.certificates
.len()
.checked_add(info.crls.len())
.and_then(|count| count.checked_add(options.trusted_certs.len()))
.ok_or(crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::X509_WORK,
maximum: resources.max_x509_work,
})?;
session
.memory()
.charge_work(count as u64)
.map_err(|reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.evidence",
0,
))
})?;
let mut remaining = resources.max_external_resource_total_bytes;
for bytes in info
.certificates
.iter()
.chain(&info.crls)
.chain(options.trusted_certs)
{
if bytes.len() > resources.max_external_resource_bytes {
return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_bytes,
}
.into());
}
if bytes.len() > remaining {
return Err(crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::AGGREGATE_EXTERNAL_RESOURCE_BYTES,
maximum: resources.max_external_resource_total_bytes,
}
.into());
}
remaining -= bytes.len();
}
verify_x509_certificate_chain_in_session(info, options, provider, &[], &session).map(|_| ())
}
pub(crate) fn verify_x509_certificate_chain_in_session(
info: &impl X509DataSource,
options: &X509ChainOptions<'_>,
provider: &dyn crate::provider::CryptoProvider,
additional_crls: &[Vec<u8>],
session: &super::pkix::CertificateSession,
) -> Result<super::pkix::ValidatedX509Key, X509ChainError> {
if options.max_chain_depth == 0 {
return Err(X509ChainError::InvalidDepth);
}
if info.chain().is_empty() {
return Err(X509ChainError::UntrustedRoot);
}
let storage_error = |reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.path",
0,
))
};
let mut path_der = session.memory().vector();
path_der
.try_reserve_exact(info.chain().len())
.map_err(storage_error)?;
for &index in info.chain() {
let bytes = info
.certificate(index)
.ok_or(X509ChainError::UntrustedRoot)?;
path_der.try_push(bytes).map_err(storage_error)?;
}
let last = session.read(
path_der
.last()
.copied()
.ok_or(X509ChainError::UntrustedRoot)?,
)?;
let trusted_anchors =
session
.read_pool(options.trusted_certs)
.map_err(|error| match error {
X509ChainError::Processing(error) if !error.is_resource() => {
X509ChainError::InvalidDer {
kind: "certificate",
message: error.to_string(),
}
}
error => error,
})?;
let embedded_anchor = trusted_anchors
.iter()
.any(|cert| cert.as_bytes() == last.as_bytes());
if embedded_anchor {
return validate_path(
&path_der,
info,
options,
(provider, session),
additional_crls,
);
}
let replace_untrusted_root = if path_der.len() > 1
&& session.source_names_equal(last.tbs().subject(), last.tbs().issuer())?
&& shared_certificate_signature_outcome(
&last,
&last,
provider,
options.certificate_signature_algorithms,
session,
)? == structured_pkix::path::Verified::Yes
{
let child = session.read(path_der[path_der.len() - 2])?;
session.source_names_equal(child.tbs().issuer(), last.tbs().subject())?
&& shared_certificate_signature_outcome(
&child,
&last,
provider,
options.certificate_signature_algorithms,
session,
)? == structured_pkix::path::Verified::Yes
} else {
false
};
let candidate_base = if replace_untrusted_root {
&path_der[..path_der.len() - 1]
} else {
&path_der[..]
};
let candidate_child = session.read(
candidate_base
.last()
.copied()
.ok_or(X509ChainError::UntrustedRoot)?,
)?;
let mut first_validation_error = None;
for cert in &trusted_anchors {
if !session.source_names_equal(cert.tbs().subject(), candidate_child.tbs().issuer())? {
continue;
}
match shared_certificate_signature_outcome(
&candidate_child,
cert,
provider,
options.certificate_signature_algorithms,
session,
)? {
structured_pkix::path::Verified::Yes | structured_pkix::path::Verified::Unsupported => {
}
_ => continue,
}
let mut candidate_path = session.memory().vector();
candidate_path
.try_extend_from_slice(candidate_base)
.map_err(storage_error)?;
candidate_path
.try_push(cert.as_bytes())
.map_err(storage_error)?;
match validate_path(
&candidate_path,
info,
options,
(provider, session),
additional_crls,
) {
Ok(key) => return Ok(key),
Err(error) if error.is_execution_failure() => return Err(error),
Err(error) => first_validation_error.get_or_insert(error),
};
}
Err(first_validation_error.unwrap_or(X509ChainError::UntrustedRoot))
}
fn validate_path(
path_der: &[&[u8]],
info: &impl X509DataSource,
options: &X509ChainOptions<'_>,
execution: (
&dyn crate::provider::CryptoProvider,
&super::pkix::CertificateSession,
),
additional_crls: &[Vec<u8>],
) -> Result<super::pkix::ValidatedX509Key, X509ChainError> {
let (provider, session) = execution;
if path_der.len() > options.max_chain_depth {
return Err(X509ChainError::DepthExceeded(options.max_chain_depth));
}
let mut decoded = session.memory().vector();
for der in path_der {
let certificate = session.read(der)?;
if decoded.is_empty() {
let at = super::pkix::verification_instant(options.verification_time)?;
let validity = certificate.tbs().validity();
if at < validity.not_before().instant() || at > validity.not_after().instant() {
return Err(X509ChainError::CertificateNotValid(0));
}
}
decoded.try_push(certificate).map_err(|reason| {
X509ChainError::Processing(structured_pkix::Error::resource(
reason,
"certificate.path",
0,
))
})?;
}
let mut effective_extended_key_usages = session.memory().vector();
if let Some(purposes) = options.allowed_extended_key_usages {
for purpose in purposes {
effective_extended_key_usages
.try_push(purpose)
.map_err(|reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.extended_key_usage",
0,
))
})?;
}
}
let mut anchor_limit = None;
let at = super::pkix::verification_instant(options.verification_time)?;
for (position, cert) in decoded.iter().enumerate() {
validate_positive_serial_bytes(
cert.tbs().serial().as_bytes(),
"certificate serial number",
)?;
if position == decoded.len() - 1
&& (at < cert.tbs().validity().not_before().instant()
|| at > cert.tbs().validity().not_after().instant())
{
return Err(X509ChainError::CertificateNotValid(position));
}
let extensions = cert.tbs().extensions();
if position == 0 {
if let Some(usage) = shared_key_usage(cert, position, session)?
&& !usage.digital_signature()
&& !usage.content_commitment()
{
return Err(X509ChainError::InvalidKeyUsage {
position,
required: "digitalSignature or nonRepudiation",
});
}
} else if position == decoded.len() - 1 {
let basic = extensions
.basic_constraints(session.memory())
.map_err(|error| session.error(error))?
.filter(|basic| basic.is_authority())
.ok_or(X509ChainError::IssuerNotCa(position))?;
anchor_limit = basic.path_length().and_then(|limit| limit.certificates());
if let Some(usage) = shared_key_usage(cert, position, session)?
&& !usage.key_cert_sign()
{
return Err(X509ChainError::InvalidKeyUsage {
position,
required: "keyCertSign",
});
}
}
validate_shared_extended_key_usage(
cert,
position,
&mut effective_extended_key_usages,
session,
)?;
validate_shared_subject_identity(cert, session)?;
if position == decoded.len() - 1 {
validate_shared_anchor_extensions(cert, position, session)?;
}
if let Some(extension) = extensions
.get(structured_pkix::oid::NAME_CONSTRAINTS, session.memory())
.map_err(|error| session.error(error))?
&& (position == 0 || !extension.is_critical())
{
return Err(X509ChainError::InvalidNameConstraints { position });
}
}
let mut effective_key = super::pkix::ValidatedX509Key {
effective_spki: None,
};
if path_der.len() > 1 {
let anchor = decoded.last().expect("nonempty path");
let mut chain = session.memory().vector();
for certificate in &decoded[..decoded.len() - 1] {
chain.try_push(certificate).map_err(|reason| {
X509ChainError::Processing(structured_pkix::Error::resource(
reason,
"certificate.path",
0,
))
})?;
}
let facts =
super::pkix::validate_supplied_path(&chain, anchor, options, provider, session)?;
if let Some(limit) = anchor_limit
&& facts.non_self_issued_intermediates > limit
{
return Err(X509ChainError::PathLengthExceeded {
position: decoded.len() - 1,
limit,
});
}
effective_key = super::pkix::effective_target_spki(facts.key, session.memory())?;
}
if options.check_crls {
super::pkix::verify_crls(
&decoded,
info.crls(),
additional_crls,
options,
provider,
session,
)?;
}
Ok(effective_key)
}
fn shared_key_usage<'a>(
certificate: &structured_pkix::Certificate<'a>,
position: usize,
session: &super::pkix::CertificateSession,
) -> Result<Option<structured_pkix::usage::KeyUsage<'a>>, X509ChainError> {
let Some(extension) = certificate
.tbs()
.extensions()
.get(structured_pkix::oid::KEY_USAGE, session.memory())
.map_err(|error| session.error(error))?
else {
return Ok(None);
};
structured_pkix::usage::KeyUsage::read(extension, session.memory())
.map(Some)
.map_err(|error| {
if error.is_resource() {
session.error(error)
} else if extension.is_critical() {
X509ChainError::UnsupportedCriticalExtension {
position,
oid: "2.5.29.15".into(),
}
} else {
session.error(error)
}
})
}
fn validate_shared_extended_key_usage(
certificate: &structured_pkix::Certificate<'_>,
position: usize,
effective: &mut structured_asn1::memory::OwnedVec<&ExtendedKeyPurpose>,
session: &super::pkix::CertificateSession,
) -> Result<(), X509ChainError> {
let Some(usage) = certificate
.tbs()
.extensions()
.extended_key_usage(session.memory())
.map_err(|error| session.error(error))?
else {
return Ok(());
};
if usage
.has(
structured_pkix::oid::ANY_EXTENDED_KEY_USAGE,
session.memory(),
)
.map_err(|error| session.error(error))?
{
return Ok(());
}
let mut kept = 0;
for index in 0..effective.len() {
if shared_usage_contains(&usage, effective[index], session)? {
effective[kept] = effective[index];
kept += 1;
}
}
while effective.len() > kept {
effective.pop();
}
if kept == 0 {
return Err(X509ChainError::InvalidKeyUsage {
position,
required: "an approved extended key usage",
});
}
Ok(())
}
fn shared_usage_contains(
usage: &structured_pkix::usage::ExtendedKeyUsage<'_>,
purpose: &ExtendedKeyPurpose,
session: &super::pkix::CertificateSession,
) -> Result<bool, X509ChainError> {
use structured_pkix::oid;
let kind = match purpose {
ExtendedKeyPurpose::ServerAuth => oid::SERVER_AUTH,
ExtendedKeyPurpose::ClientAuth => oid::CLIENT_AUTH,
ExtendedKeyPurpose::CodeSigning => oid::CODE_SIGNING,
ExtendedKeyPurpose::EmailProtection => oid::EMAIL_PROTECTION,
ExtendedKeyPurpose::TimeStamping => oid::TIME_STAMPING,
ExtendedKeyPurpose::OcspSigning => oid::OCSP_SIGNING,
ExtendedKeyPurpose::Other(expected) => {
for kind in usage.purposes() {
session
.memory()
.charge_work(kind.as_bytes().len() as u64 + expected.len() as u64 + 1)
.map_err(|reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.extended_key_usage",
0,
))
})?;
let mut arcs = kind.arcs();
let mut matches = true;
for &expected in expected {
if arcs.next().and_then(|arc| arc.as_u64()) != Some(expected) {
matches = false;
break;
}
}
if matches && arcs.next().is_none() {
return Ok(true);
}
}
return Ok(false);
}
};
usage
.has(kind, session.memory())
.map_err(|error| session.error(error))
}
fn validate_shared_subject_identity(
certificate: &structured_pkix::Certificate<'_>,
session: &super::pkix::CertificateSession,
) -> Result<(), X509ChainError> {
let subject = certificate.tbs().subject();
for attribute in subject.attributes_of(structured_pkix::oid::EMAIL_ADDRESS, session.memory()) {
let attribute = attribute.map_err(|error| session.error(error))?;
let text = attribute
.text(session.memory())
.map_err(|error| session.error(error))?
.ok_or_else(|| invalid_subject_identity("subject emailAddress is not text"))?;
let text = text.as_str(session.memory()).map_err(|reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.subject",
0,
))
})?;
session
.memory()
.charge_work(text.len() as u64)
.map_err(|reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.subject",
0,
))
})?;
if !mailbox_has_valid_syntax(text) {
return Err(invalid_subject_identity(
"invalid subject emailAddress syntax",
));
}
}
let extensions = certificate.tbs().extensions();
let extension = extensions
.get(structured_pkix::oid::SUBJECT_ALT_NAME, session.memory())
.map_err(|error| session.error(error))?;
let Some(extension) = extension else {
return if subject.is_empty() {
Err(invalid_subject_identity(
"an empty subject requires a critical SubjectAlternativeName",
))
} else {
Ok(())
};
};
if let Err(error) = structured_pkix::general_name::read_alternative(extension, session.memory())
{
return Err(shared_san_error(extension, error, session)?);
}
if subject.is_empty() && !extension.is_critical() {
return Err(invalid_subject_identity(
"an empty subject requires a critical SubjectAlternativeName",
));
}
Ok(())
}
fn shared_san_error(
extension: &structured_pkix::extension::Extension<'_>,
error: structured_pkix::Error,
session: &super::pkix::CertificateSession,
) -> Result<X509ChainError, X509ChainError> {
if error.is_resource() {
return Ok(session.error(error));
}
let decode_error = |error: structured_asn1::Error| {
let error = structured_pkix::Error::new(
structured_pkix::ErrorCode::Encoding(error.code()),
"certificate.subjectAltName",
error.offset(),
);
if error.is_resource() {
session.error(error)
} else {
invalid_subject_identity("invalid SubjectAlternativeName")
}
};
let mut reader = extension.reader(session.memory()).map_err(decode_error)?;
let sequence = reader
.expect(structured_asn1::Tag::constructed(
structured_asn1::universal::SEQUENCE,
))
.map_err(decode_error)?;
let mut inside = reader.enter(&sequence).map_err(decode_error)?;
while let Some(element) = inside.next_element().map_err(decode_error)? {
if element.span().start != error.offset() {
continue;
}
if element.tag() == structured_asn1::Tag::context(false, 2) {
inside
.charge(element.contents().len() as u64)
.map_err(|error| {
session.error(structured_pkix::Error::new(
structured_pkix::ErrorCode::Encoding(error.code()),
"certificate.subjectAltName",
error.offset(),
))
})?;
if element.contents().is_ascii() {
return Ok(X509ChainError::InvalidDer {
kind: "certificate DNS name",
message: "invalid SubjectAlternativeName dNSName".into(),
});
}
}
break;
}
Ok(invalid_subject_identity("invalid SubjectAlternativeName"))
}
fn validate_shared_anchor_extensions(
certificate: &structured_pkix::Certificate<'_>,
position: usize,
session: &super::pkix::CertificateSession,
) -> Result<(), X509ChainError> {
use structured_pkix::{error::CallbackError, oid};
let extensions = certificate.tbs().extensions();
for extension in extensions.criticals(session.memory()) {
let extension = extension.map_err(|error| session.error(error))?;
let kind = extension.kind();
let accepted = matches!(
kind,
oid::KEY_USAGE
| oid::SUBJECT_ALT_NAME
| oid::BASIC_CONSTRAINTS
| oid::NAME_CONSTRAINTS
| oid::EXT_KEY_USAGE
);
let understood = if accepted {
match extensions.understands(extension, &(), session.memory()) {
Ok(value) => value,
Err(CallbackError::Processing(error)) => return Err(session.error(error)),
Err(CallbackError::Callback(error)) => match error {},
}
} else {
false
};
if !understood {
if kind == oid::NAME_CONSTRAINTS {
return Err(X509ChainError::InvalidNameConstraints { position });
}
let oid = kind.text(session.memory()).map_err(|reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.anchor",
extension.span().start,
))
})?;
let oid = oid.as_str(session.memory()).map_err(|reason| {
session.error(structured_pkix::Error::resource(
reason,
"certificate.anchor",
extension.span().start,
))
})?;
return Err(X509ChainError::UnsupportedCriticalExtension {
position,
oid: oid.into(),
});
}
}
Ok(())
}
pub(crate) fn validate_issuer_spki_policy(
encoded: &[u8],
position: usize,
rsa_keys: RsaKeyPolicy,
dsa_keys: DsaKeyPolicy,
) -> Result<(), X509ChainError> {
use der::Decode as _;
let key = pkcs8::SubjectPublicKeyInfoRef::from_der(encoded).map_err(|error| {
X509ChainError::InvalidDer {
kind: "issuer SubjectPublicKeyInfo",
message: error.to_string(),
}
})?;
match key.algorithm.oid {
oid if oid == pkcs1::ALGORITHM_OID
|| oid.as_bytes() == structured_pkix::oid::RSASSA_PSS.as_bytes() =>
{
let octets =
key.subject_public_key
.as_bytes()
.ok_or_else(|| X509ChainError::InvalidDer {
kind: "issuer SubjectPublicKeyInfo",
message: "RSA public key is not octet-aligned".into(),
})?;
let key = pkcs1::RsaPublicKeyRef::from_der(octets).map_err(|error| {
X509ChainError::InvalidDer {
kind: "RSA issuer SubjectPublicKeyInfo",
message: error.to_string(),
}
})?;
rsa_keys
.validate_components(
"X.509 issuer verification",
key.modulus.as_bytes(),
key.public_exponent.as_bytes(),
)
.map(|_| ())
.map_err(|source| X509ChainError::KeyPolicy { position, source })
}
dsa::OID => {
match super::signature::validate_dsa_signature_spki_with_minimum(
encoded,
dsa_keys.minimum_modulus_bits,
) {
Ok(()) => Ok(()),
Err(super::SignatureVerificationError::KeyPolicy(source)) => {
Err(X509ChainError::KeyPolicy { position, source })
}
Err(_) => Err(X509ChainError::InvalidDer {
kind: "DSA issuer SubjectPublicKeyInfo",
message: "invalid DSA key parameters".into(),
}),
}
}
_ => Ok(()),
}
}
pub(crate) fn validate_positive_serial_bytes(
serial: &[u8],
kind: &'static str,
) -> Result<(), X509ChainError> {
let magnitude = serial.strip_prefix(&[0]).unwrap_or(serial);
if serial.is_empty()
|| serial[0] & 0x80 != 0
|| magnitude.is_empty()
|| magnitude.len() > 20
|| magnitude.iter().all(|byte| *byte == 0)
{
return Err(X509ChainError::InvalidDer {
kind,
message: "RFC 5280 requires a positive, non-zero value of at most 20 octets".into(),
});
}
Ok(())
}
fn mailbox_has_valid_syntax(value: &str) -> bool {
value.rsplit_once('@').is_some_and(|(local, domain)| {
mailbox_local_part_has_valid_syntax(local) && mailbox_domain_has_valid_syntax(domain)
})
}
fn mailbox_domain_has_valid_syntax(domain: &str) -> bool {
let Some(literal) = domain
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
else {
return dns_name_has_valid_syntax(domain, false);
};
if literal
.get(..5)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("IPv6:"))
{
literal[5..].parse::<std::net::Ipv6Addr>().is_ok()
} else {
literal.parse::<std::net::Ipv4Addr>().is_ok()
}
}
fn mailbox_local_part_has_valid_syntax(local: &str) -> bool {
if let Some(quoted) = local
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
{
if quoted.is_empty() {
return false;
}
let mut escaped = false;
for byte in quoted.bytes() {
if escaped {
if !(0x20..=0x7e).contains(&byte) {
return false;
}
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if byte == b'"' || !(0x20..=0x7e).contains(&byte) {
return false;
}
}
return !escaped;
}
local.split('.').all(|atom| {
!atom.is_empty()
&& atom.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'/'
| b'='
| b'?'
| b'^'
| b'_'
| b'`'
| b'{'
| b'|'
| b'}'
| b'~'
)
})
})
}
fn invalid_subject_identity(message: &str) -> X509ChainError {
X509ChainError::InvalidDer {
kind: "certificate subject identity",
message: message.into(),
}
}
#[cfg(test)]
fn verify_certificate_signature(
certificate: &X509Certificate<'_>,
issuer: &X509Certificate<'_>,
) -> bool {
verify_certificate_signature_with_provider(
certificate,
issuer,
crate::provider::default_provider(),
None,
)
.unwrap_or(false)
}
#[cfg(test)]
fn verify_certificate_signature_with_provider(
certificate: &X509Certificate<'_>,
issuer: &X509Certificate<'_>,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
) -> Result<bool, X509ChainError> {
verify_x509_signed_object_with_provider(
&certificate.signature_algorithm,
&certificate.tbs_certificate.signature,
&certificate.signature_value.data,
certificate.tbs_certificate.as_ref(),
issuer.public_key().raw,
provider,
allowed,
)
}
#[cfg(test)]
pub(crate) fn certificate_signature_matches(certificate_der: &[u8], issuer_der: &[u8]) -> bool {
certificate_signature_matches_with_provider(
certificate_der,
issuer_der,
crate::provider::default_provider(),
None,
&super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default()),
)
.unwrap_or(false)
}
pub(crate) fn certificate_signature_matches_with_provider(
certificate_der: &[u8],
issuer_der: &[u8],
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
session: &super::pkix::CertificateSession,
) -> Result<bool, X509ChainError> {
match certificate_signature_outcome_with_provider(
certificate_der,
issuer_der,
provider,
allowed,
session,
)? {
structured_pkix::path::Verified::Yes => Ok(true),
structured_pkix::path::Verified::No => Ok(false),
_ => Err(X509ChainError::Provider(
crate::provider::ProviderError::Unsupported {
operation: crate::provider::ProviderOperation::VerifyCertificate,
algorithm: None,
},
)),
}
}
pub(crate) fn certificate_signature_outcome_with_provider(
certificate_der: &[u8],
issuer_der: &[u8],
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
session: &super::pkix::CertificateSession,
) -> Result<structured_pkix::path::Verified, X509ChainError> {
let certificate = session.read(certificate_der)?;
let issuer = session.read(issuer_der)?;
shared_certificate_signature_outcome(&certificate, &issuer, provider, allowed, session)
}
pub(crate) fn shared_certificate_signature_outcome(
certificate: &structured_pkix::Certificate<'_>,
issuer: &structured_pkix::Certificate<'_>,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
session: &super::pkix::CertificateSession,
) -> Result<structured_pkix::path::Verified, X509ChainError> {
use structured_pkix::path::{Verify as _, WorkingKey};
let verifier = super::pkix::ProviderVerifier { provider, allowed };
verifier
.verify(
&certificate.tbs().signature(),
WorkingKey::standing(issuer.tbs().public_key()),
certificate.signed_bytes(),
certificate.signature(),
session.memory(),
)
.map_err(|error| match error {
X509ChainError::Processing(error) => session.error(error),
error => error,
})
}
#[cfg(test)]
fn verify_crl_signature(crl: &CertificateRevocationList<'_>, issuer: &X509Certificate<'_>) -> bool {
verify_crl_signature_with_provider(crl, issuer, crate::provider::default_provider(), None)
.unwrap_or(false)
}
#[cfg(test)]
fn verify_crl_signature_with_provider(
crl: &CertificateRevocationList<'_>,
issuer: &X509Certificate<'_>,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
) -> Result<bool, X509ChainError> {
verify_x509_signed_object_with_provider(
&crl.signature_algorithm,
&crl.tbs_cert_list.signature,
&crl.signature_value.data,
crl.tbs_cert_list.as_ref(),
issuer.public_key().raw,
provider,
allowed,
)
}
#[cfg(test)]
fn verify_x509_signed_object_with_provider(
outer_algorithm: &AlgorithmIdentifier<'_>,
signed_algorithm: &AlgorithmIdentifier<'_>,
signature_der: &[u8],
signed_data: &[u8],
issuer_spki_der: &[u8],
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
) -> Result<bool, X509ChainError> {
if outer_algorithm != signed_algorithm {
return Ok(false);
}
verify_x509_signature_with_provider(
outer_algorithm,
signature_der,
signed_data,
issuer_spki_der,
provider,
allowed,
)
}
#[cfg(test)]
pub(crate) fn verify_x509_signature_with_provider(
algorithm_identifier: &AlgorithmIdentifier<'_>,
signature_der: &[u8],
signed_data: &[u8],
issuer_spki_der: &[u8],
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
) -> Result<bool, X509ChainError> {
let algorithm = authorize_x509_signature_algorithm(algorithm_identifier, provider, allowed)?;
provider
.verify_x509_signature(algorithm, signed_data, signature_der, issuer_spki_der)
.map_err(Into::into)
}
pub(crate) fn authorize_x509_signature_algorithm(
algorithm_identifier: &AlgorithmIdentifier<'_>,
provider: &dyn crate::provider::CryptoProvider,
allowed: Option<&crate::policy::CertificateSignatureAlgorithms>,
) -> Result<X509SignatureAlgorithm, X509ChainError> {
let algorithm = x509_signature_algorithm(algorithm_identifier)?;
crate::policy::KeyTrustPolicy::check_certificate_signature_algorithm(algorithm, allowed)?;
provider
.require_capability(crate::provider::ProviderCapability::VerifyCertificate(
algorithm,
))
.map_err(X509ChainError::from)?;
Ok(algorithm)
}
fn x509_signature_algorithm(
identifier: &AlgorithmIdentifier<'_>,
) -> Result<X509SignatureAlgorithm, X509ChainError> {
let oid = identifier.algorithm.to_id_string();
let algorithm = match oid.as_str() {
"1.2.840.10040.4.3" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha1),
"2.16.840.1.101.3.4.3.1" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha224),
"2.16.840.1.101.3.4.3.2" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha256),
"2.16.840.1.101.3.4.3.3" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha384),
"2.16.840.1.101.3.4.3.4" => X509SignatureAlgorithm::Dsa(super::DigestAlgorithm::Sha512),
"1.2.840.113549.1.1.5" | "1.3.14.3.2.29" => {
X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha1)
}
"1.2.840.113549.1.1.14" => {
X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha224)
}
"1.2.840.113549.1.1.11" => {
X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha256)
}
"1.2.840.113549.1.1.12" => {
X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha384)
}
"1.2.840.113549.1.1.13" => {
X509SignatureAlgorithm::RsaPkcs1v15(super::DigestAlgorithm::Sha512)
}
"1.2.840.113549.1.1.10" => parse_rsa_pss_algorithm(identifier)?,
"1.2.840.10045.4.1" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha1),
"1.2.840.10045.4.3.1" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha224),
"1.2.840.10045.4.3.2" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha256),
"1.2.840.10045.4.3.3" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha384),
"1.2.840.10045.4.3.4" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha512),
"2.16.840.1.101.3.4.3.9" => X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha3_224),
"2.16.840.1.101.3.4.3.10" => {
X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha3_256)
}
"2.16.840.1.101.3.4.3.11" => {
X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha3_384)
}
"2.16.840.1.101.3.4.3.12" => {
X509SignatureAlgorithm::Ecdsa(super::DigestAlgorithm::Sha3_512)
}
"1.3.101.112" => X509SignatureAlgorithm::Ed25519,
"1.3.101.113" => X509SignatureAlgorithm::Ed448,
_ => {
let Some(algorithm) = super::PqAlgorithm::ALL
.into_iter()
.find(|algorithm| algorithm.oid() == oid)
else {
return Err(X509ChainError::UnsupportedSignatureAlgorithm { oid });
};
X509SignatureAlgorithm::PostQuantum(algorithm)
}
};
match &algorithm {
X509SignatureAlgorithm::Dsa(_)
| X509SignatureAlgorithm::Ecdsa(_)
| X509SignatureAlgorithm::Ed25519
| X509SignatureAlgorithm::Ed448 => require_absent_signature_parameters(identifier)?,
X509SignatureAlgorithm::PostQuantum(_) => {
require_absent_signature_parameters(identifier)?;
}
X509SignatureAlgorithm::RsaPkcs1v15(_) => {
require_null_or_absent_signature_parameters(identifier)?;
}
X509SignatureAlgorithm::RsaPss { .. } => {}
}
Ok(algorithm)
}
fn require_absent_signature_parameters(
identifier: &AlgorithmIdentifier<'_>,
) -> Result<(), X509ChainError> {
if identifier.parameters.is_some() {
return Err(invalid_signature_parameters(
identifier,
"parameters must be absent",
));
}
Ok(())
}
fn require_null_or_absent_signature_parameters(
identifier: &AlgorithmIdentifier<'_>,
) -> Result<(), X509ChainError> {
if identifier
.parameters
.as_ref()
.is_some_and(|parameters| parameters.tag() != x509_parser::asn1_rs::Tag::Null)
{
return Err(invalid_signature_parameters(
identifier,
"parameters must be NULL or absent",
));
}
Ok(())
}
fn invalid_signature_parameters(
identifier: &AlgorithmIdentifier<'_>,
requirement: &str,
) -> X509ChainError {
X509ChainError::InvalidDer {
kind: "X.509 signature AlgorithmIdentifier parameters",
message: format!("{}: {requirement}", identifier.algorithm),
}
}
fn parse_rsa_pss_algorithm(
identifier: &AlgorithmIdentifier<'_>,
) -> Result<X509SignatureAlgorithm, X509ChainError> {
let parameters = identifier
.parameters
.as_ref()
.ok_or_else(|| X509ChainError::InvalidDer {
kind: "RSASSA-PSS parameters",
message: "missing parameters".into(),
})?;
let parameters = x509_parser::signature_algorithm::RsaSsaPssParams::try_from(parameters)
.map_err(|error| X509ChainError::InvalidDer {
kind: "RSASSA-PSS parameters",
message: error.to_string(),
})?;
if parameters.trailer_field() != 1 {
return Err(X509ChainError::InvalidDer {
kind: "RSASSA-PSS parameters",
message: "trailerField must be 1".into(),
});
}
let digest = x509_digest_algorithm(¶meters.hash_algorithm_oid().to_id_string())?;
let mask = parameters
.mask_gen_algorithm()
.map_err(|error| X509ChainError::InvalidDer {
kind: "RSASSA-PSS parameters",
message: error.to_string(),
})?;
if mask.mgf.to_id_string() != "1.2.840.113549.1.1.8" {
return Err(X509ChainError::UnsupportedSignatureAlgorithm {
oid: mask.mgf.to_id_string(),
});
}
let mgf_digest = x509_digest_algorithm(&mask.hash.to_id_string())?;
let salt_len =
usize::try_from(parameters.salt_length()).map_err(|_| X509ChainError::InvalidDer {
kind: "RSASSA-PSS parameters",
message: "saltLength does not fit this platform".into(),
})?;
Ok(X509SignatureAlgorithm::RsaPss {
digest,
mgf_digest,
salt_len,
})
}
fn x509_digest_algorithm(oid: &str) -> Result<super::DigestAlgorithm, X509ChainError> {
match oid {
"1.3.14.3.2.26" => Ok(super::DigestAlgorithm::Sha1),
"2.16.840.1.101.3.4.2.4" => Ok(super::DigestAlgorithm::Sha224),
"2.16.840.1.101.3.4.2.1" => Ok(super::DigestAlgorithm::Sha256),
"2.16.840.1.101.3.4.2.2" => Ok(super::DigestAlgorithm::Sha384),
"2.16.840.1.101.3.4.2.3" => Ok(super::DigestAlgorithm::Sha512),
_ => Err(X509ChainError::UnsupportedSignatureAlgorithm {
oid: oid.to_owned(),
}),
}
}
#[cfg(test)]
fn parse_certificate(der: &[u8]) -> Result<X509Certificate<'_>, X509ChainError> {
let (rest, cert) =
X509Certificate::from_der(der).map_err(|error| X509ChainError::InvalidDer {
kind: "certificate",
message: error.to_string(),
})?;
if !rest.is_empty() {
return Err(X509ChainError::InvalidDer {
kind: "certificate",
message: "trailing data".into(),
});
}
Ok(cert)
}
#[cfg(test)]
fn system_time_to_asn1(time: SystemTime) -> Result<ASN1Time, X509ChainError> {
let instant = super::pkix::verification_instant(time)?;
let whole = ASN1Time::from_timestamp(instant.seconds()).map_err(|error| {
X509ChainError::InvalidDer {
kind: "verification time",
message: error.to_string(),
}
})?;
whole
.to_datetime()
.replace_nanosecond(instant.nanos())
.map(ASN1Time::new)
.map_err(|error| X509ChainError::InvalidDer {
kind: "verification time",
message: error.to_string(),
})
}
fn dns_name_has_valid_syntax(value: &str, allow_leading_dot: bool) -> bool {
let domain = if allow_leading_dot {
value.strip_prefix('.').unwrap_or(value)
} else {
value
};
if domain.is_empty()
|| domain.len() > 253
|| domain.split('.').any(|label| {
label.is_empty()
|| label.len() > 63
|| !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|| !label
.as_bytes()
.first()
.is_some_and(u8::is_ascii_alphanumeric)
|| !label
.as_bytes()
.last()
.is_some_and(u8::is_ascii_alphanumeric)
})
{
return false;
}
true
}
#[cfg(test)]
mod tests {
use std::str::FromStr as _;
use super::*;
use crate::xml::dom::Document;
use crate::xmldsig::{KeyInfoSource, parse::XMLDSIG_NS, parse_key_info};
use p256::pkcs8::EncodePublicKey;
use sha2::{Digest, Sha256, Sha384};
use signature::hazmat::PrehashSigner;
use std::time::{Duration, UNIX_EPOCH};
use x509_parser::oid_registry::{OID_SIG_ECDSA_WITH_SHA256, OID_SIG_ECDSA_WITH_SHA384, Oid};
#[test]
fn issuer_spki_preflight_checks_borrowed_components_and_exact_der()
-> Result<(), Box<dyn std::error::Error>> {
use der::Encode as _;
let modulus = [0xff; 32];
let exponent = [1, 0, 1];
let public = pkcs1::RsaPublicKeyRef {
modulus: der::asn1::UintRef::new(&modulus)?,
public_exponent: der::asn1::UintRef::new(&exponent)?,
}
.to_der()?;
let encoded = pkcs8::SubjectPublicKeyInfoRef {
algorithm: pkcs1::ALGORITHM_ID,
subject_public_key: der::asn1::BitStringRef::from_bytes(&public)?,
}
.to_der()?;
assert!(matches!(
validate_issuer_spki_policy(
&encoded,
7,
RsaKeyPolicy::default(),
DsaKeyPolicy::default()
),
Err(X509ChainError::KeyPolicy { position: 7, .. }),
));
let pss = pkcs8::SubjectPublicKeyInfoRef {
algorithm: pkcs8::AlgorithmIdentifierRef {
oid: der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.10"),
parameters: None,
},
subject_public_key: der::asn1::BitStringRef::from_bytes(&public)?,
}
.to_der()?;
assert!(matches!(
validate_issuer_spki_policy(&pss, 7, RsaKeyPolicy::default(), DsaKeyPolicy::default()),
Err(X509ChainError::KeyPolicy { position: 7, .. }),
));
let mut trailing = encoded.clone();
trailing.push(0);
assert!(matches!(
validate_issuer_spki_policy(
&trailing,
7,
RsaKeyPolicy::default(),
DsaKeyPolicy::default()
),
Err(X509ChainError::InvalidDer {
kind: "issuer SubjectPublicKeyInfo",
..
}),
));
let unaligned = pkcs8::SubjectPublicKeyInfoRef {
algorithm: pkcs1::ALGORITHM_ID,
subject_public_key: der::asn1::BitStringRef::new(1, &[0])?,
}
.to_der()?;
assert!(matches!(
validate_issuer_spki_policy(
&unaligned,
7,
RsaKeyPolicy::default(),
DsaKeyPolicy::default()
),
Err(X509ChainError::InvalidDer {
kind: "issuer SubjectPublicKeyInfo",
..
}),
));
let opaque = pkcs8::SubjectPublicKeyInfoRef {
algorithm: pkcs8::AlgorithmIdentifierRef {
oid: der::asn1::ObjectIdentifier::new_unwrap("1.2.3.4"),
parameters: None,
},
subject_public_key: der::asn1::BitStringRef::new(1, &[0])?,
}
.to_der()?;
validate_issuer_spki_policy(&opaque, 7, RsaKeyPolicy::default(), DsaKeyPolicy::default())?;
Ok(())
}
#[test]
fn verification_time_retains_subseconds_on_both_sides_of_epoch() {
for (input, seconds, nanos) in [
(UNIX_EPOCH + Duration::new(1, 123), 1, 123),
(UNIX_EPOCH - Duration::new(1, 123), -2, 999_999_877),
] {
let actual = system_time_to_asn1(input).expect("representable validation time");
assert_eq!(actual.timestamp(), seconds);
assert_eq!(actual.to_datetime().nanosecond(), nanos);
}
}
#[test]
fn exact_certificate_pin_does_not_bypass_execution_budget() {
let certificate = generated_certificate_params("pinned signer", false)
.self_signed(&rcgen::KeyPair::generate().expect("test key"))
.expect("test certificate");
let anchors = vec![certificate.der().to_vec()];
let info = X509DataInfo {
certificates: anchors.clone(),
certificate_chain: vec![0],
..X509DataInfo::default()
};
let options = X509ChainOptions {
trusted_certs: &anchors,
verification_time: SystemTime::now(),
max_chain_depth: 1,
utc_facts: None,
check_crls: false,
allowed_extended_key_usages: None,
rsa_keys: RsaKeyPolicy::default(),
dsa_keys: DsaKeyPolicy::default(),
certificate_signature_algorithms: None,
};
let session = super::super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy {
max_x509_work: 0,
..crate::policy::ResourcePolicy::default()
});
assert!(matches!(
verify_x509_certificate_chain_in_session(
&info,
&options,
crate::provider::default_provider(),
&[],
&session,
),
Err(X509ChainError::Policy(
crate::policy::PolicyViolation::ResourceLimitExceeded {
resource: crate::policy::resource_name::X509_WORK,
maximum: 0,
}
))
));
}
#[test]
fn crl_key_usage_absence_depends_on_certificate_version() {
use der::{Decode as _, Encode as _};
let mut params = rcgen::CertificateParams::new(Vec::new()).expect("empty SANs are valid");
params
.distinguished_name
.push(rcgen::DnType::CommonName, "CRL issuer");
let certificate = params
.self_signed(&rcgen::KeyPair::generate().expect("test key generation"))
.expect("test certificate signing");
let encoded =
x509_cert::Certificate::from_der(certificate.der()).expect("generated issuer DER");
let mut crl: x509_cert::crl::CertificateList =
x509_cert::crl::CertificateList::from_der(&merlin_crl_der()).expect("Merlin CRL DER");
crl.tbs_cert_list.issuer = encoded.tbs_certificate().subject().clone();
let crl_bytes = crl.to_der().expect("modified CRL encoding");
for version in [
x509_cert::Version::V1,
x509_cert::Version::V2,
x509_cert::Version::V3,
] {
use der::Tagged as _;
let mut fields = Vec::<der::asn1::Any>::from_der(
&encoded
.tbs_certificate()
.to_der()
.expect("issuer TBS encoding"),
)
.expect("issuer TBS fields");
fields.remove(0); fields.retain(|field| {
field.tag()
!= der::Tag::ContextSpecific {
constructed: true,
number: der::TagNumber(3),
}
});
if version != x509_cert::Version::V1 {
fields.insert(
0,
der::asn1::Any::from_der(&[0xa0, 3, 2, 1, version as u8])
.expect("version encoding"),
);
}
let sequence = vec![
der::asn1::Any::from_der(&fields.to_der().expect("modified TBS encoding"))
.expect("modified TBS sequence"),
der::asn1::Any::from_der(
&encoded
.signature_algorithm()
.to_der()
.expect("signature algorithm encoding"),
)
.expect("signature algorithm sequence"),
der::asn1::Any::from_der(
&encoded.signature().to_der().expect("signature encoding"),
)
.expect("signature BIT STRING"),
];
let bytes = sequence.to_der().expect("modified issuer encoding");
let session = super::super::pkix::CertificateSession::new(
&crate::policy::ResourcePolicy::default(),
);
let issuer = session.read(&bytes).expect("modified issuer parses");
let crl = session.read_crl(&crl_bytes).expect("modified CRL parses");
let outcome = crl
.authenticate(
&issuer,
structured_pkix::path::WorkingKey::standing(issuer.tbs().public_key()),
&FormatOnly,
session.memory(),
)
.expect("format-only authentication returns an outcome");
if version == x509_cert::Version::V3 {
assert!(matches!(
outcome,
structured_pkix::crl::Authentication::Rejected(
structured_pkix::crl::AuthenticationFailure::KeyUsage
)
));
} else {
assert!(matches!(
outcome,
structured_pkix::crl::Authentication::Authenticated(_)
));
}
}
}
fn generated_certificate_params(common_name: &str, is_ca: bool) -> rcgen::CertificateParams {
let mut params = rcgen::CertificateParams::new(Vec::new())
.expect("empty SAN list should produce valid certificate parameters");
params
.distinguished_name
.push(rcgen::DnType::CommonName, common_name);
if is_ca {
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
}
params
}
fn verify_generated_path(
certificates: Vec<Vec<u8>>,
trusted_anchor: Vec<u8>,
) -> Result<(), X509ChainError> {
verify_generated_path_with_eku(certificates, trusted_anchor, None)
}
fn verify_generated_path_with_eku(
certificates: Vec<Vec<u8>>,
trusted_anchor: Vec<u8>,
allowed_extended_key_usages: Option<&HashSet<ExtendedKeyPurpose>>,
) -> Result<(), X509ChainError> {
let info = X509DataInfo {
certificate_chain: (0..certificates.len()).collect(),
certificates,
..X509DataInfo::default()
};
let anchors = vec![trusted_anchor];
verify_x509_certificate_chain(
&info,
&X509ChainOptions {
trusted_certs: &anchors,
utc_facts: None,
verification_time: SystemTime::now(),
max_chain_depth: info.certificate_chain.len(),
check_crls: false,
allowed_extended_key_usages,
rsa_keys: RsaKeyPolicy::default(),
dsa_keys: DsaKeyPolicy::default(),
certificate_signature_algorithms: None,
},
)
}
fn verify_generated_constraints(
constraints: x509_cert::ext::pkix::NameConstraints,
name: x509_cert::ext::pkix::name::GeneralName,
) -> Result<(), X509ChainError> {
use der::Encode as _;
let mut root_params = generated_certificate_params("constraint authority", true);
let mut constraint_extension = rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 30],
constraints.to_der().expect("constraint fixture DER"),
);
constraint_extension.set_criticality(true);
root_params.custom_extensions.push(constraint_extension);
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("constraint root key"),
)
.expect("constraint root");
let mut leaf_params = generated_certificate_params("constraint signer", false);
let mut san = rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 17],
vec![name].to_der().expect("SAN fixture DER"),
);
san.set_criticality(true);
leaf_params.custom_extensions.push(san);
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("constraint leaf key"),
&root,
)
.expect("constraint leaf");
let session =
super::super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default());
let leaf = session.read(leaf.der())?;
let root = session.read(root.der())?;
let options = X509ChainOptions {
trusted_certs: &[],
utc_facts: None,
verification_time: SystemTime::now(),
max_chain_depth: 2,
check_crls: false,
allowed_extended_key_usages: None,
rsa_keys: RsaKeyPolicy::default(),
dsa_keys: DsaKeyPolicy::default(),
certificate_signature_algorithms: None,
};
super::super::pkix::validate_supplied_path(
&[&leaf],
&root,
&options,
crate::provider::default_provider(),
&session,
)
.map(|_| ())
}
#[test]
fn noncritical_ca_basic_constraints_remain_path_compatible() {
let mut params = generated_certificate_params("non-critical authority", false);
params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
params
.custom_extensions
.push(rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 19],
vec![0x30, 0x03, 0x01, 0x01, 0xff],
));
let key = rcgen::KeyPair::generate().expect("CA key generation should succeed");
let issuer = rcgen::Issuer::from_params(¶ms, &key);
let certificate = params
.clone()
.self_signed(&key)
.expect("test CA should be self-signable");
let leaf = generated_certificate_params("noncritical CA target", false)
.signed_by(&rcgen::KeyPair::generate().expect("target key"), &issuer)
.expect("target certificate");
verify_generated_path(
vec![leaf.der().to_vec(), certificate.der().to_vec()],
certificate.der().to_vec(),
)
.expect("the actual common path accepts noncritical cA");
}
#[test]
fn restricted_leaf_eku_requires_an_approved_purpose() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("EKU authority", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("TLS-only signer", false);
leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
leaf_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth];
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
let leaf_der = leaf.der().to_vec();
let root_der = root.der().to_vec();
assert!(matches!(
verify_generated_path(vec![leaf_der.clone(), root_der.clone()], root_der.clone(),),
Err(X509ChainError::InvalidKeyUsage {
position: 0,
required: "an approved extended key usage",
})
));
let allowed = HashSet::from([ExtendedKeyPurpose::ServerAuth]);
verify_generated_path_with_eku(vec![leaf_der, root_der.clone()], root_der, Some(&allowed))
.expect("an explicitly approved leaf purpose must be accepted");
}
#[test]
fn critical_leaf_eku_uses_the_same_purpose_policy() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("critical EKU authority", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("critical TLS signer", false);
leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
let mut extension = rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 37],
vec![
0x30, 0x0a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
],
);
extension.set_criticality(true);
leaf_params.custom_extensions.push(extension);
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
let allowed = HashSet::from([ExtendedKeyPurpose::ServerAuth]);
verify_generated_path_with_eku(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
Some(&allowed),
)
.expect("approved critical EKU must be processed rather than rejected as unknown");
}
#[test]
fn issuer_eku_restricts_the_entire_certificate_path() {
for (issuer_purpose_der, accepted) in [
(
vec![
0x30, 0x0a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02,
],
false,
),
(
vec![
0x30, 0x0a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
],
true,
),
] {
let mut root_params =
generated_certificate_params("purpose-constrained authority", true);
let mut extension =
rcgen::CustomExtension::from_oid_content(&[2, 5, 29, 37], issuer_purpose_der);
extension.set_criticality(true);
root_params.custom_extensions.push(extension);
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("TLS server signer", false);
leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
leaf_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::ServerAuth];
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
let allowed = HashSet::from([
ExtendedKeyPurpose::ServerAuth,
ExtendedKeyPurpose::ClientAuth,
]);
let result = verify_generated_path_with_eku(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
Some(&allowed),
);
if accepted {
result.expect("a shared allowed purpose must satisfy the complete path");
} else {
assert!(matches!(
result,
Err(X509ChainError::InvalidKeyUsage {
position: 1,
required: "an approved extended key usage",
})
));
}
}
}
#[test]
fn any_extended_key_usage_does_not_restrict_xml_signing() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("any EKU authority", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("unrestricted signer", false);
leaf_params.key_usages = vec![rcgen::KeyUsagePurpose::DigitalSignature];
leaf_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::Any];
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
)
.expect("anyExtendedKeyUsage must remain unrestricted");
}
#[test]
#[cfg(feature = "experimental-pq")]
fn pq_certificate_verification_requires_explicit_trust_permission() {
use super::super::{
PostQuantumSigningKey, PqAlgorithm, SignatureAlgorithm, SignatureContext, SigningKey,
};
let parameter = PqAlgorithm::MlDsa44;
let key = PostQuantumSigningKey::from_pkcs8_der(
parameter,
include_bytes!("../../tests/fixtures/xmldsig/keys/ml-dsa/ml-dsa-44-key.der"),
)
.expect("donor ML-DSA private key must import");
let data = b"PKIX signed data";
let provider = crate::provider::default_provider();
let signature = provider
.sign_with_context(
&key,
SignatureAlgorithm::PostQuantum(parameter),
&SignatureContext::default(),
data,
)
.expect("ML-DSA key must sign PKIX data");
let public = key.public_key_info().expect("public key must export");
let spki = public.spki_der().expect("PQ public key must contain SPKI");
let algorithm = X509SignatureAlgorithm::PostQuantum(parameter);
assert!(
provider
.verify_x509_signature(algorithm, data, &signature, spki)
.expect("provider must verify valid PQ signature")
);
let identifier = AlgorithmIdentifier::new(
parameter
.oid()
.parse()
.expect("registered PQ OID must parse"),
None,
);
assert!(
verify_x509_signature_with_provider(
&identifier,
&signature,
data,
spki,
provider,
None
)
.is_err(),
"default certificate policy must reject PQ before provider verification"
);
let permitted =
crate::policy::CertificateSignatureAlgorithms::Allowlist(HashSet::from([algorithm]));
assert!(
verify_x509_signed_object_with_provider(
&identifier,
&identifier,
&signature,
data,
spki,
provider,
Some(&permitted)
)
.expect("explicit permission must enable valid PQ authentication")
);
assert!(matches!(
verify_x509_signed_object_with_provider(
&identifier,
&identifier,
&signature,
data,
spki,
provider,
Some(&crate::policy::CertificateSignatureAlgorithms::Allowlist(
HashSet::new()
))
),
Err(X509ChainError::Policy(_))
));
}
#[test]
fn x509_ecdsa_hash_oid_does_not_select_the_issuer_curve() {
let data = b"certificate tbs bytes";
let p384_key = p384::ecdsa::SigningKey::from_slice(&[0x42; 48])
.expect("fixed P-384 test key must be valid");
let p384_signature: p384::ecdsa::Signature = p384_key
.sign_prehash(&Sha256::digest(data))
.expect("P-384 must sign a SHA-256 prehash");
let p384_spki = p384_key
.verifying_key()
.to_public_key_der()
.expect("P-384 SPKI must encode");
assert!(
verify_x509_signature_with_provider(
&AlgorithmIdentifier::new(OID_SIG_ECDSA_WITH_SHA256, None),
p384_signature.to_der().as_bytes(),
data,
p384_spki.as_bytes(),
crate::provider::default_provider(),
None,
)
.expect("P-384 with SHA-256 must be a supported X.509 pairing")
);
let p256_key = p256::ecdsa::SigningKey::from_slice(&[0x24; 32])
.expect("fixed P-256 test key must be valid");
let p256_signature: p256::ecdsa::Signature = p256_key
.sign_prehash(&Sha384::digest(data))
.expect("P-256 must sign a SHA-384 prehash");
let p256_spki = p256_key
.verifying_key()
.to_public_key_der()
.expect("P-256 SPKI must encode");
assert!(
verify_x509_signature_with_provider(
&AlgorithmIdentifier::new(OID_SIG_ECDSA_WITH_SHA384, None),
p256_signature.to_der().as_bytes(),
data,
p256_spki.as_bytes(),
crate::provider::default_provider(),
None,
)
.expect("P-256 with SHA-384 must be a supported X.509 pairing")
);
}
#[test]
fn path_edge_signature_check_does_not_repeat_name_matching() {
let issuer_key = rcgen::KeyPair::generate().expect("issuer key generation should succeed");
let issuer_key_pem = issuer_key.serialize_pem();
let mut signing_params = rcgen::CertificateParams::new(Vec::new())
.expect("empty issuer SAN list should be valid");
signing_params
.distinguished_name
.push(rcgen::DnType::CommonName, "signing name");
signing_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
signing_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
let signing_issuer = rcgen::CertifiedIssuer::self_signed(signing_params, issuer_key)
.expect("issuer certificate should be self-signable");
let mut alternate_params = rcgen::CertificateParams::new(Vec::new())
.expect("empty alternate SAN list should be valid");
alternate_params
.distinguished_name
.push(rcgen::DnType::CommonName, "name already matched by caller");
alternate_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
alternate_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
let alternate_issuer = rcgen::CertifiedIssuer::self_signed(
alternate_params,
rcgen::KeyPair::from_pem(&issuer_key_pem)
.expect("serialized issuer key should parse again"),
)
.expect("alternate issuer certificate should be self-signable");
let leaf = rcgen::CertificateParams::new(Vec::new())
.expect("empty leaf SAN list should be valid")
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&signing_issuer,
)
.expect("issuer should sign leaf certificate");
assert!(certificate_signature_matches(
leaf.der(),
alternate_issuer.der()
));
}
#[test]
fn certificate_path_edge_preserves_ed25519_verification() {
let issuer_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
.expect("Ed25519 issuer key generation should succeed");
let mut issuer_params = rcgen::CertificateParams::new(Vec::new())
.expect("empty issuer SAN list should be valid");
issuer_params
.distinguished_name
.push(rcgen::DnType::CommonName, "Ed25519 issuer");
issuer_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
issuer_params.key_usages = vec![rcgen::KeyUsagePurpose::KeyCertSign];
let issuer = rcgen::CertifiedIssuer::self_signed(issuer_params, issuer_key)
.expect("Ed25519 issuer certificate should be self-signable");
let leaf_key = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
.expect("Ed25519 leaf key generation should succeed");
let leaf = rcgen::CertificateParams::new(Vec::new())
.expect("empty leaf SAN list should be valid")
.signed_by(&leaf_key, &issuer)
.expect("Ed25519 issuer should sign leaf certificate");
assert!(certificate_signature_matches(leaf.der(), issuer.der()));
}
#[test]
fn ed448_certificate_signature_identifier_requires_absent_parameters() {
use x509_parser::asn1_rs::{Any, Tag};
let oid = Oid::from_str("1.3.101.113").expect("RFC 8410 Ed448 OID must parse");
assert!(x509_signature_algorithm(&AlgorithmIdentifier::new(oid.clone(), None)).is_ok());
assert!(matches!(
x509_signature_algorithm(&AlgorithmIdentifier::new(
oid,
Some(Any::from_tag_and_data(Tag::Null, &[]))
)),
Err(X509ChainError::InvalidDer {
kind: "X.509 signature AlgorithmIdentifier parameters",
..
})
));
}
#[test]
fn every_modeled_non_parameterized_x509_algorithm_reaches_the_provider() {
for (oid, expected) in [
(
"2.16.840.1.101.3.4.3.1",
X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha224),
),
(
"2.16.840.1.101.3.4.3.2",
X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha256),
),
(
"2.16.840.1.101.3.4.3.3",
X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha384),
),
(
"2.16.840.1.101.3.4.3.4",
X509SignatureAlgorithm::Dsa(super::super::DigestAlgorithm::Sha512),
),
(
"1.2.840.10045.4.1",
X509SignatureAlgorithm::Ecdsa(super::super::DigestAlgorithm::Sha1),
),
(
"1.2.840.113549.1.1.14",
X509SignatureAlgorithm::RsaPkcs1v15(super::super::DigestAlgorithm::Sha224),
),
(
"1.2.840.10045.4.3.1",
X509SignatureAlgorithm::Ecdsa(super::super::DigestAlgorithm::Sha224),
),
(
"1.2.840.10045.4.3.4",
X509SignatureAlgorithm::Ecdsa(super::super::DigestAlgorithm::Sha512),
),
] {
let identifier = AlgorithmIdentifier::new(
Oid::from_str(oid).expect("static signature OID must parse"),
None,
);
assert_eq!(x509_signature_algorithm(&identifier), Ok(expected), "{oid}");
}
}
#[test]
fn pq_certificate_identifiers_require_absent_parameters() {
use x509_parser::asn1_rs::{Any, Tag};
for algorithm in super::super::PqAlgorithm::ALL {
let oid = Oid::from_str(algorithm.oid()).expect("registered PQ signature OID");
assert!(x509_signature_algorithm(&AlgorithmIdentifier::new(oid.clone(), None)).is_ok());
assert!(matches!(
x509_signature_algorithm(&AlgorithmIdentifier::new(
oid,
Some(Any::from_tag_and_data(Tag::Null, &[]))
)),
Err(X509ChainError::InvalidDer {
kind: "X.509 signature AlgorithmIdentifier parameters",
..
})
));
}
}
#[test]
fn sha3_ecdsa_certificate_identifiers_require_absent_parameters() {
use x509_parser::asn1_rs::{Any, Tag};
for digest in [
super::super::DigestAlgorithm::Sha3_224,
super::super::DigestAlgorithm::Sha3_256,
super::super::DigestAlgorithm::Sha3_384,
super::super::DigestAlgorithm::Sha3_512,
] {
let expected = X509SignatureAlgorithm::Ecdsa(digest);
let oid = Oid::from_str(expected.oid().expect("registered signature OID"))
.expect("registered SHA3 ECDSA OID must parse");
assert_eq!(
x509_signature_algorithm(&AlgorithmIdentifier::new(oid.clone(), None)),
Ok(expected)
);
assert!(matches!(
x509_signature_algorithm(&AlgorithmIdentifier::new(
oid,
Some(Any::from_tag_and_data(Tag::Null, &[]))
)),
Err(X509ChainError::InvalidDer { .. })
));
}
}
#[test]
fn x509_signature_parameters_follow_each_algorithm_profile() {
use x509_parser::asn1_rs::{Any, Tag};
for oid in [
"1.2.840.10040.4.3",
"2.16.840.1.101.3.4.3.1",
"2.16.840.1.101.3.4.3.2",
"1.2.840.10045.4.1",
"1.2.840.10045.4.3.1",
"1.2.840.10045.4.3.2",
"1.3.101.112",
] {
let identifier = AlgorithmIdentifier::new(
Oid::from_str(oid).expect("static signature OID must parse"),
Some(Any::from_tag_and_data(Tag::Null, &[])),
);
assert!(matches!(
x509_signature_algorithm(&identifier),
Err(X509ChainError::InvalidDer {
kind: "X.509 signature AlgorithmIdentifier parameters",
..
})
));
}
for (oid, digest) in [
(
"1.2.840.113549.1.1.14",
super::super::DigestAlgorithm::Sha224,
),
(
"1.2.840.113549.1.1.11",
super::super::DigestAlgorithm::Sha256,
),
] {
let rsa_oid = Oid::from_str(oid).expect("static RSA signature OID must parse");
for parameters in [None, Some(Any::from_tag_and_data(Tag::Null, &[]))] {
assert_eq!(
x509_signature_algorithm(&AlgorithmIdentifier::new(
rsa_oid.clone(),
parameters,
)),
Ok(X509SignatureAlgorithm::RsaPkcs1v15(digest)),
);
}
assert!(matches!(
x509_signature_algorithm(&AlgorithmIdentifier::new(
rsa_oid,
Some(Any::from_tag_and_data(Tag::OctetString, &[])),
)),
Err(X509ChainError::InvalidDer {
kind: "X.509 signature AlgorithmIdentifier parameters",
..
})
));
}
}
#[test]
fn unknown_x509_signature_algorithm_remains_diagnosable() {
let oid = "1.2.3.4.5";
let identifier = AlgorithmIdentifier::new(
Oid::from_str(oid).expect("static unknown OID must parse"),
None,
);
assert_eq!(
x509_signature_algorithm(&identifier),
Err(X509ChainError::UnsupportedSignatureAlgorithm { oid: oid.into() })
);
}
#[test]
fn parses_rsa_pss_certificate_parameters_without_xml_dsig_loss() {
let der = [
0x30, 0x41, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a, 0x30,
0x34, 0xa0, 0x0f, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x02, 0x01, 0x05, 0x00, 0xa1, 0x1c, 0x30, 0x1a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x08, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65,
0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0xa2, 0x03, 0x02, 0x01, 0x20,
];
let (rest, identifier) = AlgorithmIdentifier::from_der(&der)
.expect("standard SHA-256 RSA-PSS AlgorithmIdentifier must parse");
assert!(rest.is_empty());
assert_eq!(
x509_signature_algorithm(&identifier),
Ok(X509SignatureAlgorithm::RsaPss {
digest: super::super::DigestAlgorithm::Sha256,
mgf_digest: super::super::DigestAlgorithm::Sha256,
salt_len: 32,
})
);
}
#[test]
fn parses_sha224_rsa_pss_certificate_parameters() {
let der = [
0x30, 0x41, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a, 0x30,
0x34, 0xa0, 0x0f, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x02, 0x04, 0x05, 0x00, 0xa1, 0x1c, 0x30, 0x1a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x08, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65,
0x03, 0x04, 0x02, 0x04, 0x05, 0x00, 0xa2, 0x03, 0x02, 0x01, 0x1c,
];
let (rest, identifier) = AlgorithmIdentifier::from_der(&der)
.expect("standard SHA-224 RSA-PSS AlgorithmIdentifier must parse");
assert!(rest.is_empty());
assert_eq!(
x509_signature_algorithm(&identifier),
Ok(X509SignatureAlgorithm::RsaPss {
digest: super::super::DigestAlgorithm::Sha224,
mgf_digest: super::super::DigestAlgorithm::Sha224,
salt_len: 28,
})
);
}
#[test]
fn dsa_rollover_replaces_embedded_root_before_depth_validation() {
let leaf = include_bytes!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
)
.to_vec();
let embedded_root =
include_bytes!("../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der")
.to_vec();
let mut rollover_anchor = embedded_root.clone();
*rollover_anchor
.last_mut()
.expect("certificate is non-empty") ^= 1;
parse_certificate(&rollover_anchor).expect("modified trust anchor remains valid DER");
let anchors = vec![rollover_anchor];
let info = X509DataInfo {
certificates: vec![leaf, embedded_root],
certificate_chain: vec![0, 1],
..X509DataInfo::default()
};
let options = X509ChainOptions {
trusted_certs: &anchors,
verification_time: UNIX_EPOCH + Duration::from_secs(1_104_580_800),
max_chain_depth: 2,
check_crls: false,
allowed_extended_key_usages: None,
rsa_keys: RsaKeyPolicy::default(),
dsa_keys: DsaKeyPolicy {
minimum_modulus_bits: 1024,
},
utc_facts: None,
certificate_signature_algorithms: None,
};
verify_x509_certificate_chain(&info, &options)
.expect("the stale DSA root must be replaced by the configured anchor");
}
#[test]
fn dsa_issuer_key_uses_the_configured_strength_policy() {
let leaf = include_bytes!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
)
.to_vec();
let anchor =
include_bytes!("../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der")
.to_vec();
let anchors = vec![anchor.clone()];
let info = X509DataInfo {
certificates: vec![leaf, anchor],
certificate_chain: vec![0, 1],
..X509DataInfo::default()
};
let options = X509ChainOptions {
trusted_certs: &anchors,
verification_time: UNIX_EPOCH + Duration::from_secs(1_104_580_800),
max_chain_depth: 2,
check_crls: false,
allowed_extended_key_usages: None,
rsa_keys: RsaKeyPolicy::default(),
dsa_keys: DsaKeyPolicy::default(),
utc_facts: None,
certificate_signature_algorithms: None,
};
assert!(matches!(
verify_x509_certificate_chain(&info, &options),
Err(X509ChainError::KeyPolicy {
position: 1,
source: crate::policy::PolicyViolation::KeySize {
key_type: "DSA",
minimum_bits: 2048,
actual_bits: 1024,
..
}
})
));
}
#[test]
fn path_length_excludes_self_issued_rollover_certificates() {
let mut root_params = generated_certificate_params("rollover path authority", true);
root_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let rollover_params = generated_certificate_params("rollover path authority", true);
let rollover_key =
rcgen::KeyPair::generate().expect("rollover key generation should succeed");
let rollover_certificate = rollover_params
.signed_by(&rollover_key, &root)
.expect("root should sign same-name rollover certificate");
let rollover_issuer = rcgen::Issuer::from_params(&rollover_params, &rollover_key);
let leaf = generated_certificate_params("rollover path leaf", false)
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&rollover_issuer,
)
.expect("rollover key should sign leaf certificate");
verify_generated_path(
vec![
leaf.der().to_vec(),
rollover_certificate.der().to_vec(),
root.der().to_vec(),
],
root.der().to_vec(),
)
.expect("self-issued rollover must not consume a zero path-length allowance");
}
#[test]
fn ca_name_constraints_reject_disallowed_dns_names() {
let mut root_params = generated_certificate_params("constrained authority", true);
root_params.name_constraints = Some(rcgen::NameConstraints {
permitted_subtrees: vec![rcgen::GeneralSubtree::DnsName("example.com".into())],
excluded_subtrees: vec![rcgen::GeneralSubtree::DnsName("blocked.example.com".into())],
});
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("constrained root should be self-signable");
for (dns_name, accepted) in [
("www.example.com", true),
("blocked.example.com", false),
("www.example.net", false),
] {
let leaf = rcgen::CertificateParams::new(vec![dns_name.into()])
.expect("DNS SAN should be valid")
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
assert_eq!(
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
)
.is_ok(),
accepted,
"unexpected name-constraint result for {dns_name}"
);
}
}
#[test]
fn rfc5280_dns_names_require_preferred_name_syntax() {
let mut root_params = generated_certificate_params("DNS syntax authority", true);
root_params.name_constraints = Some(rcgen::NameConstraints {
permitted_subtrees: vec![rcgen::GeneralSubtree::DnsName("example.com".into())],
excluded_subtrees: Vec::new(),
});
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("constrained root should be self-signable");
let mut leaf_params = generated_certificate_params("malformed DNS leaf", false);
let dns_name = b"bad..example.com";
let mut san_der = vec![
0x30,
u8::try_from(dns_name.len() + 2).expect("test SAN must fit short-form DER"),
0x82,
];
san_der.push(u8::try_from(dns_name.len()).expect("test DNS name must fit short-form DER"));
san_der.extend_from_slice(dns_name);
leaf_params
.custom_extensions
.push(rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 17],
san_der,
));
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign malformed-DNS leaf");
let outcome = verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
);
assert!(
matches!(
outcome,
Err(X509ChainError::InvalidDer {
kind: "certificate DNS name",
..
})
),
"{outcome:?}"
);
for dns_name in ["*.example.com", "_signing.example.com"] {
let mut params = generated_certificate_params("invalid DNS target", false);
let mut san = vec![
0x30,
u8::try_from(dns_name.len() + 2).expect("SAN length"),
0x82,
u8::try_from(dns_name.len()).expect("DNS length"),
];
san.extend_from_slice(dns_name.as_bytes());
params
.custom_extensions
.push(rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 17],
san,
));
let leaf = params
.signed_by(&rcgen::KeyPair::generate().expect("target key"), &root)
.expect("target certificate");
assert!(
matches!(
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec()
),
Err(X509ChainError::InvalidDer {
kind: "certificate DNS name",
..
})
),
"{dns_name}"
);
}
}
#[test]
fn name_constraint_matchers_cover_email_uri_and_ip_forms() {
use der::asn1::{Ia5String, OctetString};
use x509_cert::ext::pkix::{
NameConstraints as Constraints, constraints::name::GeneralSubtree,
name::GeneralName as Name,
};
let email = |value: &str| Name::Rfc822Name(Ia5String::new(value).expect("email fixture"));
let uri = |value: &str| {
Name::UniformResourceIdentifier(Ia5String::new(value).expect("URI fixture"))
};
let ip = |value: &[u8]| Name::IpAddress(OctetString::new(value).expect("IP fixture"));
for (name, base, accepted) in [
(email("ops@example.com"), email("example.com"), true),
(email("ops@example.com"), email("ops@example.com"), true),
(email("other@example.com"), email("ops@example.com"), false),
(
uri("https://user@api.example.com:8443/path"),
uri(".example.com"),
true,
),
(
uri("https://api.example.com/path"),
uri(".example.com"),
true,
),
(
ip(&[192, 0, 2, 42]),
ip(&[192, 0, 2, 0, 255, 255, 255, 0]),
true,
),
(
ip(&[192, 0, 3, 42]),
ip(&[192, 0, 2, 0, 255, 255, 255, 0]),
false,
),
] {
let result = verify_generated_constraints(
Constraints {
permitted_subtrees: Some(vec![GeneralSubtree {
base,
minimum: 0,
maximum: None,
}]),
excluded_subtrees: None,
},
name,
);
if accepted {
result.expect("matching name must be accepted");
} else {
assert_eq!(
result,
Err(X509ChainError::NameConstraintViolation {
position: 0,
constraining_position: 1,
})
);
}
}
let result = verify_generated_constraints(
Constraints {
permitted_subtrees: Some(vec![GeneralSubtree {
base: uri(".example.com"),
minimum: 0,
maximum: None,
}]),
excluded_subtrees: None,
},
uri("https://user@other@example.com/path"),
);
assert_eq!(
result,
Err(X509ChainError::UnsupportedCriticalExtension {
position: 0,
oid: "2.5.29.17".into(),
}),
"a second userinfo delimiter must not expose a constraint-matchable host"
);
let result = verify_generated_constraints(
Constraints {
permitted_subtrees: Some(vec![GeneralSubtree {
base: ip(&[192, 0, 2, 0, 255, 0, 255, 0]),
minimum: 0,
maximum: None,
}]),
excluded_subtrees: None,
},
ip(&[192, 0, 2, 42]),
);
assert_eq!(
result,
Err(X509ChainError::InvalidNameConstraints { position: 1 })
);
}
#[test]
fn malformed_ip_name_constraints_fail_before_matching() {
use der::asn1::OctetString;
use x509_cert::ext::pkix::{
NameConstraints as Constraints, constraints::name::GeneralSubtree,
name::GeneralName as Name,
};
for malformed in [
&[192, 0, 2, 0, 255, 255, 255][..],
&[192, 0, 2, 0, 255, 0, 255, 0][..],
] {
for permitted in [true, false] {
let subtree = GeneralSubtree {
base: Name::IpAddress(
OctetString::new(malformed).expect("IP constraint fixture"),
),
minimum: 0,
maximum: None,
};
let constraints = Constraints {
permitted_subtrees: permitted.then(|| vec![subtree.clone()]),
excluded_subtrees: (!permitted).then(|| vec![subtree]),
};
assert_eq!(
verify_generated_constraints(
constraints,
Name::IpAddress(OctetString::new([192, 0, 2, 42]).expect("IP SAN fixture")),
),
Err(X509ChainError::InvalidNameConstraints { position: 1 })
);
}
}
}
#[test]
fn malformed_string_name_constraints_fail_before_matching() {
use der::asn1::Ia5String;
use x509_cert::ext::pkix::{
NameConstraints as Constraints, constraints::name::GeneralSubtree,
name::GeneralName as Name,
};
let dns =
|value: &str| Name::DnsName(Ia5String::new(value).expect("DNS constraint fixture"));
let email = |value: &str| {
Name::Rfc822Name(Ia5String::new(value).expect("email constraint fixture"))
};
let uri = |value: &str| {
Name::UniformResourceIdentifier(Ia5String::new(value).expect("URI constraint fixture"))
};
for malformed in [
dns(""),
dns("example..com"),
email("@example.com"),
email("bad..local@example.com"),
uri("https://example.com"),
] {
let constraints = Constraints {
permitted_subtrees: None,
excluded_subtrees: Some(vec![GeneralSubtree {
base: malformed,
minimum: 0,
maximum: None,
}]),
};
assert_eq!(
verify_generated_constraints(constraints, dns("www.example.com")),
Err(X509ChainError::InvalidNameConstraints { position: 1 })
);
}
for valid in [
dns("example.com"),
dns(".example.com"),
email("ops@example.com"),
email("example.com"),
uri(".example.com"),
] {
let constraints = Constraints {
permitted_subtrees: Some(vec![GeneralSubtree {
base: valid,
minimum: 0,
maximum: None,
}]),
excluded_subtrees: None,
};
verify_generated_constraints(constraints, dns("www.example.com"))
.expect("valid string constraints must remain supported");
}
}
#[test]
fn empty_name_constraint_collections_are_rejected() {
use der::asn1::Ia5String;
use x509_cert::ext::pkix::NameConstraints as EncodedNameConstraints;
for constraints in [
EncodedNameConstraints {
permitted_subtrees: None,
excluded_subtrees: None,
},
EncodedNameConstraints {
permitted_subtrees: Some(Vec::new()),
excluded_subtrees: None,
},
EncodedNameConstraints {
permitted_subtrees: None,
excluded_subtrees: Some(Vec::new()),
},
] {
assert_eq!(
verify_generated_constraints(
constraints,
x509_cert::ext::pkix::name::GeneralName::DnsName(
Ia5String::new("www.example.com").expect("DNS SAN fixture")
),
),
Err(X509ChainError::InvalidNameConstraints { position: 1 })
);
}
}
#[test]
fn unsupported_name_constraint_distances_fail_path_validation() {
use der::{Encode as _, asn1::Ia5String};
use x509_cert::ext::pkix::{
NameConstraints as EncodedNameConstraints,
constraints::name::GeneralSubtree as EncodedGeneralSubtree,
name::GeneralName as EncodedGeneralName,
};
for (permitted, minimum, maximum) in [
(true, 1, None),
(false, 1, None),
(true, 0, Some(1)),
(false, 0, Some(1)),
] {
let dns_name = if permitted {
"example.com"
} else {
"blocked.example.com"
};
let subtree = EncodedGeneralSubtree {
base: EncodedGeneralName::DnsName(
Ia5String::new(dns_name.as_bytes()).expect("valid DNS IA5String"),
),
minimum,
maximum,
};
let constraints = EncodedNameConstraints {
permitted_subtrees: permitted.then(|| vec![subtree.clone()]),
excluded_subtrees: (!permitted).then(|| vec![subtree]),
};
let mut extension = rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 30],
constraints
.to_der()
.expect("NameConstraints must encode as DER"),
);
extension.set_criticality(true);
let mut root_params = generated_certificate_params("distance authority", true);
root_params.custom_extensions.push(extension);
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("constrained root should be self-signable");
let leaf = rcgen::CertificateParams::new(vec!["www.example.com".into()])
.expect("leaf DNS SAN should be valid")
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
let outcome = verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
);
assert!(
matches!(
outcome,
Err(X509ChainError::InvalidNameConstraints { position: 1 })
),
"{outcome:?}"
);
}
}
#[test]
fn name_constraints_cover_subject_email_and_directory_name() {
let mut permitted_directory = rcgen::DistinguishedName::new();
permitted_directory.push(rcgen::DnType::OrganizationName, "Example Corp");
let mut root_params = generated_certificate_params("name authority", true);
root_params.name_constraints = Some(rcgen::NameConstraints {
permitted_subtrees: vec![
rcgen::GeneralSubtree::Rfc822Name("example.com".into()),
rcgen::GeneralSubtree::DirectoryName(permitted_directory),
],
excluded_subtrees: Vec::new(),
});
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("constrained root should be self-signable");
for (organization, email, accepted) in [
("Example Corp", "ops@example.com", true),
("Other Corp", "ops@example.com", false),
("Example Corp", "ops@example.net", false),
("Example Corp", "bad..local@example.com", false),
] {
let mut leaf_params = generated_certificate_params("name-constrained leaf", false);
leaf_params.distinguished_name = rcgen::DistinguishedName::new();
leaf_params
.distinguished_name
.push(rcgen::DnType::OrganizationName, organization);
leaf_params
.distinguished_name
.push(rcgen::DnType::CommonName, "name-constrained leaf");
leaf_params.distinguished_name.push(
rcgen::DnType::CustomDnType(vec![1, 2, 840, 113549, 1, 9, 1]),
rcgen::DnValue::Ia5String(
email
.try_into()
.expect("test email must be a valid IA5String"),
),
);
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
let result = verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
);
assert_eq!(
result.is_ok(),
accepted,
"unexpected subject constraint result for {organization} / {email}: {result:?}",
);
}
}
#[test]
fn empty_subject_requires_a_critical_nonempty_san() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("subject identity authority", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut missing_san = rcgen::CertificateParams::new(Vec::new())
.expect("empty SAN list should produce certificate parameters");
missing_san.distinguished_name = rcgen::DistinguishedName::new();
let missing_san = missing_san
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("test issuer should sign an empty-subject certificate");
let mut noncritical_san = rcgen::CertificateParams::new(Vec::new())
.expect("empty SAN list should produce certificate parameters");
noncritical_san.distinguished_name = rcgen::DistinguishedName::new();
noncritical_san
.custom_extensions
.push(rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 17],
vec![0x30, 0x03, 0x82, 0x01, b'a'],
));
let noncritical_san = noncritical_san
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("test issuer should sign a non-critical-SAN certificate");
for leaf in [missing_san, noncritical_san] {
let error = verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
)
.expect_err("an empty subject without critical SAN must fail");
let X509ChainError::Processing(error) = error else {
panic!("expected located subject rejection: {error:?}");
};
assert_eq!(error.code(), structured_pkix::ErrorCode::MissingField);
assert_eq!(error.field(), "certificate.tbsCertificate.subject");
assert!(error.offset() > 0 && error.offset() < leaf.der().len() as u64);
}
}
#[test]
fn empty_subject_with_critical_san_skips_directory_name_constraints() {
let mut permitted_directory = rcgen::DistinguishedName::new();
permitted_directory.push(rcgen::DnType::OrganizationName, "Example Corp");
let mut root_params = generated_certificate_params("empty-subject authority", true);
root_params.name_constraints = Some(rcgen::NameConstraints {
permitted_subtrees: vec![rcgen::GeneralSubtree::DirectoryName(permitted_directory)],
excluded_subtrees: Vec::new(),
});
let root = rcgen::CertifiedIssuer::self_signed(
root_params,
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("constrained root should be self-signable");
let mut leaf_params = rcgen::CertificateParams::new(vec!["allowed.example".into()])
.expect("DNS SAN should be valid");
leaf_params.distinguished_name = rcgen::DistinguishedName::new();
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign empty-subject leaf");
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
)
.expect("only present name forms should be constrained");
}
#[test]
fn malformed_general_names_in_san_fail_path_validation() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("malformed-SAN root", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
for empty_subject in [true, false] {
let mut leaf_params = generated_certificate_params("malformed-SAN leaf", false);
if empty_subject {
leaf_params.distinguished_name = rcgen::DistinguishedName::new();
}
let mut malformed_san = rcgen::CustomExtension::from_oid_content(
&[2, 5, 29, 17],
vec![0x30, 0x03, 0x82, 0x01, 0xff],
);
malformed_san.set_criticality(true);
leaf_params.custom_extensions.push(malformed_san);
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign malformed-SAN leaf");
assert!(matches!(
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
),
Err(X509ChainError::InvalidDer {
kind: "certificate subject identity",
..
})
));
}
}
#[test]
fn typed_subject_alternative_names_require_rfc5280_syntax() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("typed-SAN root", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
for (tag, value) in [
(0x81, b"operator@".as_slice()),
(0x81, b"first..last@example.com".as_slice()),
(0x86, b"relative/path".as_slice()),
(0x86, b"https://example.com/%zz".as_slice()),
(0x86, b"https://user@other@example.com/path".as_slice()),
(0x86, b"file:///path".as_slice()),
(0x87, &[192, 0, 2][..]),
] {
for empty_subject in [false, true] {
let mut leaf_params = generated_certificate_params("typed-SAN leaf", false);
if empty_subject {
leaf_params.distinguished_name = rcgen::DistinguishedName::new();
}
let mut san_der = vec![
0x30,
u8::try_from(value.len() + 2).expect("test SAN must fit short-form DER"),
tag,
u8::try_from(value.len()).expect("test GeneralName must fit short-form DER"),
];
san_der.extend_from_slice(value);
let mut san = rcgen::CustomExtension::from_oid_content(&[2, 5, 29, 17], san_der);
san.set_criticality(true);
leaf_params.custom_extensions.push(san);
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign typed-SAN leaf");
assert!(matches!(
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
),
Err(X509ChainError::InvalidDer {
kind: "certificate subject identity",
..
})
));
}
}
for (tag, value) in [
(0x81, b"operator@example.com".as_slice()),
(0x81, b"operator@[192.0.2.1]".as_slice()),
(0x81, b"operator@[IPv6:2001:db8::1]".as_slice()),
(0x81, br#""operator desk"@example.com"#.as_slice()),
(0x86, b"urn:example:operator".as_slice()),
(
0x86,
b"https://operator@example.com:8443/path?q=1#id".as_slice(),
),
(0x87, &[192, 0, 2, 1][..]),
(
0x87,
&[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1][..],
),
] {
let mut leaf_params = rcgen::CertificateParams::new(Vec::new())
.expect("empty SAN list should produce certificate parameters");
leaf_params.distinguished_name = rcgen::DistinguishedName::new();
let mut san_der = vec![
0x30,
u8::try_from(value.len() + 2).expect("test SAN must fit short-form DER"),
tag,
u8::try_from(value.len()).expect("test GeneralName must fit short-form DER"),
];
san_der.extend_from_slice(value);
let mut san = rcgen::CustomExtension::from_oid_content(&[2, 5, 29, 17], san_der);
san.set_criticality(true);
leaf_params.custom_extensions.push(san);
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign typed-SAN leaf");
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
)
.unwrap_or_else(|error| {
panic!(
"valid typed SAN {tag:#x} {value:?} must satisfy an empty subject: {error:?}"
)
});
}
}
fn parsed_merlin_crl(der: &[u8]) -> CertificateRevocationList<'_> {
CertificateRevocationList::from_der(der)
.expect("modified Merlin CRL must remain parseable")
.1
}
fn merlin_crl_fixture() -> X509DataInfo {
let xml = include_str!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml"
);
let document = Document::parse(xml).expect("tracked Merlin document must parse");
let key_info_node = document
.descendants()
.find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
.expect("tracked Merlin document contains KeyInfo");
let key_info = parse_key_info(key_info_node).expect("tracked Merlin KeyInfo must parse");
let KeyInfoSource::X509Data(info) = &key_info.sources[0] else {
panic!("expected X509Data")
};
info.clone()
}
fn merlin_crl_der() -> Vec<u8> {
merlin_crl_fixture().crls[0].clone()
}
struct FormatOnly;
impl structured_pkix::path::Verify for FormatOnly {
type Error = std::convert::Infallible;
fn verify(
&self,
_: &structured_pkix::algorithm::AlgorithmIdentifier<'_>,
_: structured_pkix::path::WorkingKey<'_>,
_: &[u8],
_: structured_asn1::BitString<'_>,
_: &structured_asn1::memory::Memory,
) -> Result<structured_pkix::path::Verified, Self::Error> {
Ok(structured_pkix::path::Verified::Yes)
}
}
fn validate_crl_extensions(bytes: &[u8], index: usize) -> Result<(), X509ChainError> {
let session =
super::super::pkix::CertificateSession::new(&crate::policy::ResourcePolicy::default());
let crl = session
.read_crl(bytes)
.map_err(|_| X509ChainError::InvalidCrl(index))?;
let memory = session.memory();
for entry in crl.tbs().revoked_certificates() {
validate_positive_serial_bytes(
entry.serial().as_bytes(),
"CRL revoked certificate serial",
)
.map_err(|_| X509ChainError::InvalidCrl(index))?;
}
if crl
.tbs()
.extensions()
.get(structured_pkix::oid::DELTA_CRL_INDICATOR, memory)
.map_err(|_| X509ChainError::InvalidCrl(index))?
.is_some()
{
return Err(X509ChainError::InvalidCrl(index));
}
let mut fixture = merlin_crl_fixture();
fixture.certificates.push(
pem::parse(include_bytes!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.pem"
))
.expect("public Merlin issuer PEM")
.into_contents(),
);
for encoded in &fixture.certificates {
use der::{Decode as _, Encode as _, Tagged as _};
let mut envelope =
Vec::<der::asn1::Any>::from_der(encoded).expect("fixture certificate fields");
let mut fields = Vec::<der::asn1::Any>::from_der(
&envelope[0].to_der().expect("fixture TBS encoding"),
)
.expect("fixture TBS fields");
fields.retain(|field| {
!matches!(
field.tag(),
der::Tag::ContextSpecific {
number: der::TagNumber(0 | 3),
..
}
)
});
envelope[0] =
der::asn1::Any::from_der(&fields.to_der().expect("versionless TBS encoding"))
.expect("versionless TBS sequence");
let bytes = envelope.to_der().expect("versionless issuer encoding");
let issuer = session.read(&bytes)?;
if !crl
.tbs()
.issuer()
.matches(issuer.tbs().subject(), memory)
.map_err(|_| X509ChainError::InvalidCrl(index))?
{
continue;
}
let proof = crl
.authenticate(
&issuer,
structured_pkix::path::WorkingKey::standing(issuer.tbs().public_key()),
&FormatOnly,
memory,
)
.map_err(|_| X509ChainError::InvalidCrl(index))?;
let structured_pkix::crl::Authentication::Authenticated(proof) = proof else {
return Err(X509ChainError::InvalidCrl(index));
};
return match proof
.entry(&issuer, memory)
.map_err(|_| X509ChainError::InvalidCrl(index))?
{
structured_pkix::crl::EntryLookup::Absent
| structured_pkix::crl::EntryLookup::Found(_) => Ok(()),
_ => Err(X509ChainError::InvalidCrl(index)),
};
}
Err(X509ChainError::InvalidCrl(index))
}
#[test]
fn duplicate_crl_and_entry_extension_oids_fail_closed() {
use der::{Decode as _, Encode as _};
use x509_cert::crl::CertificateList;
let original = merlin_crl_der();
let mut duplicate_crl: CertificateList =
CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
let extensions = duplicate_crl
.tbs_cert_list
.crl_extensions
.as_mut()
.expect("tracked Merlin CRL must contain extensions");
extensions.push(extensions[0].clone());
let duplicate_crl = duplicate_crl
.to_der()
.expect("duplicate CRL extension test vector must encode");
assert_eq!(
validate_crl_extensions(&duplicate_crl, 0),
Err(X509ChainError::InvalidCrl(0))
);
let mut duplicate_entry: CertificateList =
CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
let duplicate = duplicate_entry
.tbs_cert_list
.crl_extensions
.as_ref()
.and_then(|extensions| extensions.first())
.expect("tracked Merlin CRL must contain an extension")
.clone();
let revoked = duplicate_entry
.tbs_cert_list
.revoked_certificates
.as_mut()
.and_then(|entries| entries.first_mut())
.expect("tracked Merlin CRL must contain a revoked entry");
revoked.crl_entry_extensions = Some(vec![duplicate.clone(), duplicate]);
let duplicate_entry = duplicate_entry
.to_der()
.expect("duplicate entry extension test vector must encode");
assert_eq!(
validate_crl_extensions(&duplicate_entry, 0),
Err(X509ChainError::InvalidCrl(0))
);
}
#[test]
fn malformed_revoked_certificate_serials_fail_closed() {
let original = merlin_crl_der();
let serial = parsed_merlin_crl(&original)
.iter_revoked_certificates()
.next()
.expect("tracked Merlin CRL must contain a revoked entry")
.raw_serial()
.to_vec();
let offsets = original
.windows(serial.len())
.enumerate()
.filter_map(|(offset, bytes)| (bytes == serial).then_some(offset))
.collect::<Vec<_>>();
assert_eq!(
offsets.len(),
1,
"revoked serial fixture must be unambiguous"
);
for replacement in [vec![0; serial.len()], {
let mut negative = serial.clone();
negative[0] = 0x80;
negative
}] {
let mut malformed = original.clone();
malformed[offsets[0]..offsets[0] + serial.len()].copy_from_slice(&replacement);
assert_eq!(
validate_crl_extensions(&malformed, 0),
Err(X509ChainError::InvalidCrl(0))
);
}
}
#[test]
fn delta_crl_indicator_is_rejected_regardless_of_criticality() {
use der::{Decode as _, Encode as _, asn1::OctetString};
use x509_cert::{crl::CertificateList, ext::Extension};
let original = merlin_crl_der();
for critical in [false, true] {
let mut encoded: CertificateList =
CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
encoded
.tbs_cert_list
.crl_extensions
.get_or_insert_default()
.push(Extension {
extn_id: der::asn1::ObjectIdentifier::new_unwrap("2.5.29.27"),
critical,
extn_value: OctetString::new([0x02, 0x01, 0x01])
.expect("DER INTEGER extension payload must be valid"),
});
let encoded = encoded
.to_der()
.expect("delta CRL indicator test vector must encode");
assert_eq!(
validate_crl_extensions(&encoded, 0),
Err(X509ChainError::InvalidCrl(0)),
"delta CRL indicator criticality must not change unsupported semantics"
);
}
}
#[test]
fn remove_from_crl_is_rejected_in_a_complete_crl() {
use der::{Decode as _, Encode as _, asn1::OctetString};
use x509_cert::{crl::CertificateList, ext::Extension};
let original = merlin_crl_der();
for (reason, accepted) in [(1_u8, true), (8_u8, false)] {
let mut encoded: CertificateList =
CertificateList::from_der(&original).expect("tracked Merlin CRL must decode");
encoded.tbs_cert_list.crl_extensions = None;
let revoked = encoded
.tbs_cert_list
.revoked_certificates
.as_mut()
.and_then(|entries| entries.first_mut())
.expect("tracked Merlin CRL must contain a revoked entry");
revoked
.crl_entry_extensions
.get_or_insert_default()
.push(Extension {
extn_id: der::asn1::ObjectIdentifier::new_unwrap("2.5.29.21"),
critical: false,
extn_value: OctetString::new([0x0a, 0x01, reason])
.expect("DER ENUMERATED extension payload must be valid"),
});
let encoded = encoded
.to_der()
.expect("reason-code CRL test vector must encode");
let result = validate_crl_extensions(&encoded, 0);
if accepted {
assert_eq!(result, Ok(()), "ordinary revocation reasons remain valid");
} else {
assert_eq!(result, Err(X509ChainError::InvalidCrl(0)));
}
}
}
#[test]
fn unevaluable_uri_names_fail_closed_for_both_constraint_forms() {
use der::asn1::Ia5String;
use x509_cert::ext::pkix::{
NameConstraints as Constraints, constraints::name::GeneralSubtree,
name::GeneralName as Name,
};
let uri = || {
Name::UniformResourceIdentifier(
Ia5String::new("urn:example:opaque").expect("URI SAN fixture"),
)
};
let base = || {
Name::UniformResourceIdentifier(
Ia5String::new(".example.com").expect("URI constraint fixture"),
)
};
for constraints in [
Constraints {
permitted_subtrees: Some(vec![GeneralSubtree {
base: base(),
minimum: 0,
maximum: None,
}]),
excluded_subtrees: None,
},
Constraints {
permitted_subtrees: None,
excluded_subtrees: Some(vec![GeneralSubtree {
base: base(),
minimum: 0,
maximum: None,
}]),
},
] {
assert_eq!(
verify_generated_constraints(constraints, uri()),
Err(X509ChainError::NameConstraintViolation {
position: 0,
constraining_position: 1,
})
);
}
}
#[test]
fn unknown_critical_certificate_extension_fails_closed() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("critical-extension root", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("critical-extension leaf", false);
let mut extension =
rcgen::CustomExtension::from_oid_content(&[1, 2, 3, 4], vec![0x05, 0x00]);
extension.set_criticality(true);
leaf_params.custom_extensions.push(extension);
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
assert_eq!(
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
),
Err(X509ChainError::UnsupportedCriticalExtension {
position: 0,
oid: "1.2.3.4".into(),
})
);
}
#[test]
fn duplicate_certificate_extension_oids_fail_closed() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("duplicate-extension root", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("duplicate-extension leaf", false);
for _ in 0..2 {
leaf_params
.custom_extensions
.push(rcgen::CustomExtension::from_oid_content(
&[1, 2, 3, 4],
vec![0x05, 0x00],
));
}
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
let error = verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
)
.expect_err("duplicate extension must fail");
let X509ChainError::Processing(error) = error else {
panic!("expected located duplicate-extension rejection: {error:?}");
};
assert_eq!(error.code(), structured_pkix::ErrorCode::DuplicateExtension);
assert_eq!(error.field(), "certificate.tbsCertificate.extensions");
assert!(error.offset() > 0 && error.offset() < leaf.der().len() as u64);
}
#[test]
fn invalid_certificate_serial_numbers_fail_path_validation() {
for serial in [vec![0], vec![1; 21]] {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("serial root", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("invalid serial leaf", false);
leaf_params.serial_number = Some(rcgen::SerialNumber::from_slice(&serial));
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
assert!(matches!(
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
),
Err(X509ChainError::InvalidDer {
kind: "certificate serial number",
..
})
));
}
assert!(validate_positive_serial_bytes(&[0x80], "certificate serial number").is_err());
assert!(validate_positive_serial_bytes(&[1; 20], "certificate serial number").is_ok());
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("serial-padding root", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("serial-padding leaf", false);
leaf_params.serial_number = Some(rcgen::SerialNumber::from_slice(&[0x80; 20]));
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign a maximum-magnitude serial");
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
)
.expect("a 20-octet magnitude may require a DER sign-padding octet");
}
#[test]
fn name_constraints_are_rejected_on_end_entity_certificates() {
let root = rcgen::CertifiedIssuer::self_signed(
generated_certificate_params("name-placement root", true),
rcgen::KeyPair::generate().expect("root key generation should succeed"),
)
.expect("root should be self-signable");
let mut leaf_params = generated_certificate_params("name-placement leaf", false);
leaf_params.name_constraints = Some(rcgen::NameConstraints {
permitted_subtrees: vec![rcgen::GeneralSubtree::DnsName("example.com".into())],
excluded_subtrees: Vec::new(),
});
let leaf = leaf_params
.signed_by(
&rcgen::KeyPair::generate().expect("leaf key generation should succeed"),
&root,
)
.expect("root should sign leaf certificate");
assert!(matches!(
verify_generated_path(
vec![leaf.der().to_vec(), root.der().to_vec()],
root.der().to_vec(),
),
Err(X509ChainError::InvalidNameConstraints { position: 0 })
));
}
#[test]
fn dsa_certificate_rejects_mismatched_inner_signature_algorithm() {
let (_, mut certificate) = X509Certificate::from_der(include_bytes!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/balor.der"
))
.expect("the tracked Merlin certificate is valid DER");
let (_, issuer) = X509Certificate::from_der(include_bytes!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der"
))
.expect("the tracked Merlin issuer is a DER certificate");
assert!(verify_certificate_signature(&certificate, &issuer));
certificate.tbs_certificate.signature = issuer.public_key().algorithm.clone();
assert_ne!(
certificate.tbs_certificate.signature,
certificate.signature_algorithm
);
assert!(!verify_certificate_signature(&certificate, &issuer));
}
#[test]
fn dsa_sha1_crl_signature_uses_the_same_fallback_as_certificates() {
let xml = include_str!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml"
);
let document = Document::parse(xml).expect("the tracked Merlin document is valid XML");
let key_info_node = document
.descendants()
.find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
.expect("the Merlin document contains KeyInfo");
let key_info = parse_key_info(key_info_node).expect("the Merlin KeyInfo is valid");
let KeyInfoSource::X509Data(info) = &key_info.sources[0] else {
panic!("expected X509Data")
};
let (_, issuer) = X509Certificate::from_der(include_bytes!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der"
))
.expect("the tracked Merlin issuer is a DER certificate");
let (_, crl) = CertificateRevocationList::from_der(&info.crls[0])
.expect("the tracked Merlin CRL is valid DER");
assert!(verify_crl_signature(&crl, &issuer));
}
#[test]
fn dsa_crl_rejects_mismatched_inner_signature_algorithm() {
let xml = include_str!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/signature-x509-crt-crl.xml"
);
let document = Document::parse(xml).expect("the tracked Merlin document is valid XML");
let key_info_node = document
.descendants()
.find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo")))
.expect("the Merlin document contains KeyInfo");
let key_info = parse_key_info(key_info_node).expect("the Merlin KeyInfo is valid");
let KeyInfoSource::X509Data(info) = &key_info.sources[0] else {
panic!("expected X509Data")
};
let (_, issuer) = X509Certificate::from_der(include_bytes!(
"../../tests/fixtures/xmldsig/merlin-xmldsig-twenty-three/certs/ca.der"
))
.expect("the tracked Merlin issuer is a DER certificate");
let (_, mut crl) = CertificateRevocationList::from_der(&info.crls[0])
.expect("the tracked Merlin CRL is valid DER");
assert!(verify_crl_signature(&crl, &issuer));
crl.tbs_cert_list.signature = issuer.public_key().algorithm.clone();
assert_ne!(crl.tbs_cert_list.signature, crl.signature_algorithm);
assert!(!verify_crl_signature(&crl, &issuer));
}
}