diff --git a/src/rust/cryptography-x509-verification/src/policy/extension.rs b/src/rust/cryptography-x509-verification/src/policy/extension.rs index ad6b628863e8..d6e94a018227 100644 --- a/src/rust/cryptography-x509-verification/src/policy/extension.rs +++ b/src/rust/cryptography-x509-verification/src/policy/extension.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use cryptography_x509::extensions::{Extension, Extensions}; use cryptography_x509::oid::{ AUTHORITY_INFORMATION_ACCESS_OID, AUTHORITY_KEY_IDENTIFIER_OID, BASIC_CONSTRAINTS_OID, - EXTENDED_KEY_USAGE_OID, KEY_USAGE_OID, NAME_CONSTRAINTS_OID, SUBJECT_ALTERNATIVE_NAME_OID, - SUBJECT_KEY_IDENTIFIER_OID, + CRL_DISTRIBUTION_POINTS_OID, EXTENDED_KEY_USAGE_OID, KEY_USAGE_OID, NAME_CONSTRAINTS_OID, + SUBJECT_ALTERNATIVE_NAME_OID, SUBJECT_KEY_IDENTIFIER_OID, }; use crate::ops::{CryptoOps, VerificationCertificate}; @@ -25,6 +25,7 @@ pub struct ExtensionPolicy<'cb, B: CryptoOps> { pub basic_constraints: ExtensionValidator<'cb, B>, pub name_constraints: ExtensionValidator<'cb, B>, pub extended_key_usage: ExtensionValidator<'cb, B>, + pub crl_distribution_points: ExtensionValidator<'cb, B>, } impl<'cb, B: CryptoOps + 'cb> ExtensionPolicy<'cb, B> { @@ -50,6 +51,7 @@ impl<'cb, B: CryptoOps + 'cb> ExtensionPolicy<'cb, B> { basic_constraints: make_permissive_validator(BASIC_CONSTRAINTS_OID), name_constraints: make_permissive_validator(NAME_CONSTRAINTS_OID), extended_key_usage: make_permissive_validator(EXTENDED_KEY_USAGE_OID), + crl_distribution_points: make_permissive_validator(CRL_DISTRIBUTION_POINTS_OID), } } @@ -112,6 +114,11 @@ impl<'cb, B: CryptoOps + 'cb> ExtensionPolicy<'cb, B> { Criticality::NonCritical, Some(Arc::new(ca::extended_key_usage)), ), + crl_distribution_points: ExtensionValidator::maybe_present( + CRL_DISTRIBUTION_POINTS_OID, + Criticality::NonCritical, + Some(Arc::new(common::crl_distribution_points)), + ), } } @@ -168,6 +175,126 @@ impl<'cb, B: CryptoOps + 'cb> ExtensionPolicy<'cb, B> { Criticality::NonCritical, Some(Arc::new(ee::extended_key_usage)), ), + // CA/B: 7.1.2.11.2: CRL Distribution Points + crl_distribution_points: ExtensionValidator::maybe_present( + CRL_DISTRIBUTION_POINTS_OID, + Criticality::NonCritical, + Some(Arc::new(common::crl_distribution_points)), + ), + } + } + + pub fn new_default_smime_ca() -> Self { + // CABF S/MIME BR 1.0.12 + ExtensionPolicy { + // 7.1.2.2.c: authorityInformationAccess (SHOULD be present) + authority_information_access: ExtensionValidator::maybe_present( + AUTHORITY_INFORMATION_ACCESS_OID, + Criticality::NonCritical, + Some(Arc::new(common::authority_information_access)), + ), + // 7.1.2.2.h: authorityKeyIdentifier (SHALL be present) + authority_key_identifier: ExtensionValidator::maybe_present( + AUTHORITY_KEY_IDENTIFIER_OID, + Criticality::NonCritical, + Some(Arc::new(ca::authority_key_identifier)), + ), + // 7.1.2.1.e: subjectKeyIdentifier (SHALL be present) + subject_key_identifier: ExtensionValidator::maybe_present( + SUBJECT_KEY_IDENTIFIER_OID, + Criticality::NonCritical, + None, + ), + // 7.1.2.1.b: keyUsage (SHALL be present) + key_usage: ExtensionValidator::present( + KEY_USAGE_OID, + Criticality::Critical, + Some(Arc::new(ca::key_usage)), + ), + // N/A + subject_alternative_name: ExtensionValidator::maybe_present( + SUBJECT_ALTERNATIVE_NAME_OID, + Criticality::Agnostic, + None, + ), + // 7.1.2.1.a: basicConstraints (SHALL be present) + basic_constraints: ExtensionValidator::present( + BASIC_CONSTRAINTS_OID, + Criticality::Critical, + None, + ), + // 7.1.2.2.f: nameConstraints (MAY be present) + name_constraints: ExtensionValidator::maybe_present( + NAME_CONSTRAINTS_OID, + Criticality::Agnostic, + Some(Arc::new(ca::name_constraints)), + ), + // 7.1.2.2.b: extKeyUsage (MAY be present for Cross Certificates; SHALL be present otherwise) + extended_key_usage: ExtensionValidator::maybe_present( + EXTENDED_KEY_USAGE_OID, + Criticality::NonCritical, + Some(Arc::new(ca::extended_key_usage)), + ), + // 7.1.2.2.b cRLDistributionPoints (SHALL be present) + crl_distribution_points: ExtensionValidator::maybe_present( + CRL_DISTRIBUTION_POINTS_OID, + Criticality::NonCritical, + Some(Arc::new(common::crl_distribution_points)), + ), + } + } + pub fn new_default_smime_ee() -> Self { + // CABF S/MIME BR 1.0.12 + ExtensionPolicy { + // 7.1.2.3.c: authorityInformationAccess (SHOULD be present) + authority_information_access: ExtensionValidator::maybe_present( + AUTHORITY_INFORMATION_ACCESS_OID, + Criticality::NonCritical, + Some(Arc::new(common::authority_information_access)), + ), + // 7.1.2.3.g: authorityKeyIdentifier (SHALL be present) + authority_key_identifier: ExtensionValidator::maybe_present( + AUTHORITY_KEY_IDENTIFIER_OID, + Criticality::NonCritical, + Some(Arc::new(ca::authority_key_identifier)), + ), + // 7.1.2.3.n: subjectKeyIdentifier (SHOULD be present) + subject_key_identifier: ExtensionValidator::maybe_present( + SUBJECT_KEY_IDENTIFIER_OID, + Criticality::NonCritical, + None, + ), + // 7.1.2.3.e: keyUsage (SHALL be present) + key_usage: ExtensionValidator::maybe_present( + KEY_USAGE_OID, + Criticality::Critical, + Some(Arc::new(ee::key_usage)), + ), + // 7.1.2.3.g: subjectAlternativeName (SHALL be present) + subject_alternative_name: ExtensionValidator::maybe_present( + SUBJECT_ALTERNATIVE_NAME_OID, + Criticality::Agnostic, + None, + ), + // 7.1.2.3.d: basicConstraints (optional) + basic_constraints: ExtensionValidator::maybe_present( + BASIC_CONSTRAINTS_OID, + Criticality::Critical, + None, + ), + name_constraints: ExtensionValidator::not_present(NAME_CONSTRAINTS_OID), + // 7.1.2.3.f: extKeyUsage (SHALL be present) + extended_key_usage: ExtensionValidator::present( + EXTENDED_KEY_USAGE_OID, + Criticality::NonCritical, + None, + ), + // 7.1.2.3.b: cRLDistributionPoints (SHALL be present) + crl_distribution_points: ExtensionValidator::maybe_present( + CRL_DISTRIBUTION_POINTS_OID, + Criticality::NonCritical, + Some(Arc::new(common::crl_distribution_points)), + ), } } @@ -652,11 +779,14 @@ mod ca { } mod common { - use cryptography_x509::common::Asn1Read; - use cryptography_x509::extensions::{Extension, SequenceOfAccessDescriptions}; - use crate::ops::{CryptoOps, VerificationCertificate}; use crate::policy::{Policy, ValidationResult}; + use crate::{ValidationError, ValidationErrorKind}; + use cryptography_x509::common::Asn1Read; + use cryptography_x509::extensions::{ + DistributionPoint, DistributionPointName, Extension, SequenceOfAccessDescriptions, + }; + use cryptography_x509::name::GeneralName; pub(crate) fn authority_information_access<'chain, B: CryptoOps>( _policy: &Policy<'_, B>, @@ -671,6 +801,72 @@ mod common { Ok(()) } + + pub(crate) fn crl_distribution_points<'chain, B: CryptoOps>( + _policy: &Policy<'_, B>, + _cert: &VerificationCertificate<'chain, B>, + extn: Option<&Extension<'_>>, + ) -> ValidationResult<'chain, (), B> { + if let Some(extn) = extn { + let crl_points: asn1::SequenceOf<'_, DistributionPoint<'_, Asn1Read>> = extn.value()?; + // CABF S/MIME (version 1.0.12): 7.1.2.2.b/7.1.2.3.c + // + // It SHALL contain at least one distributionPoint whose fullName value includes a GeneralName of type + // uniformResourceIdentifier that includes a URI where the Issuing CA’s CRL can be retrieved. + // + // Generation | Allowed URI scheme + // Strict and Multipurpose | Every uniformResourceIdentifier SHALL have the URI scheme HTTP. Other schemes + // SHALL NOT be present. + // Legacy | At least one uniformResourceIdentifier SHALL have the URI scheme HTTP. Other + // schemes (LDAP, FTP, …) MAY be present. + + // CABF Server TLS (version 2.2.2): 7.1.2.11.2 + // + // When present, the CRL Distribution Points extension MUST contain at least one DistributionPoint + // A fullName MUST contain at least one GeneralName; it MAY contain more than one. All + // GeneralNames MUST be of type uniformResourceIdentifier, and the scheme of each MUST + // be “http”. The first GeneralName must contain the HTTP URL of the Issuing CA’s CRL service for + // this certificate. + + // NOTE: This validator is satisfied if there is at least one DistributionPoint, with at least one + // FullName, with at least one URI, which is HTTP. + // This is the lowest common denominator and avoids being too strict. + let mut has_http_uri = false; + for cdp in crl_points { + if cdp.distribution_point.is_none() { + break; + } + match &cdp.distribution_point.unwrap() { + DistributionPointName::FullName(names) => { + for name in names.clone() { + match &name { + GeneralName::UniformResourceIdentifier(uri) => { + if uri.0.to_lowercase().starts_with("http://") { + has_http_uri = true; + break; + } + } + _ => { + // Anything besides UniformResourceIdentifier is technically not allowed + } + } + } + } + _ => { + // Distribution point might not be FullName, that's ok + } + } + } + + if !has_http_uri { + return Err(ValidationError::new(ValidationErrorKind::ExtensionError { + oid: extn.extn_id.clone(), + reason: "cRLDistributionPoints MUST contain at least one fullName that contains an HTTP URI", + })); + } + } + Ok(()) + } } #[cfg(test)] diff --git a/src/rust/cryptography-x509-verification/src/policy/mod.rs b/src/rust/cryptography-x509-verification/src/policy/mod.rs index 3693dfd37d9f..ecf401af9325 100644 --- a/src/rust/cryptography-x509-verification/src/policy/mod.rs +++ b/src/rust/cryptography-x509-verification/src/policy/mod.rs @@ -20,7 +20,7 @@ use cryptography_x509::extensions::{BasicConstraints, Extensions, SubjectAlterna use cryptography_x509::name::GeneralName; use cryptography_x509::oid::{ BASIC_CONSTRAINTS_OID, EC_SECP256R1, EC_SECP384R1, EC_SECP521R1, EKU_CLIENT_AUTH_OID, - EKU_SERVER_AUTH_OID, SUBJECT_ALTERNATIVE_NAME_OID, + EKU_EMAIL_PROTECTION_OID, EKU_SERVER_AUTH_OID, SUBJECT_ALTERNATIVE_NAME_OID, }; use crate::ops::CryptoOps; @@ -31,9 +31,12 @@ pub use crate::policy::extension::{ use crate::types::{DNSName, DNSPattern, IPAddress}; use crate::{ValidationError, ValidationErrorKind, ValidationResult, VerificationCertificate}; -// RSA key constraints, as defined in CA/B 6.1.5. +// RSA key constraints, as defined in CA/B Server TLS 6.1.5 const WEBPKI_MINIMUM_RSA_MODULUS: usize = 2048; +// RSA key constraints, as defined in CA/B S/MIME BR 6.1.5 +const SMIME_MINIMUM_RSA_MODULUS: usize = 2048; + // SubjectPublicKeyInfo AlgorithmIdentifier constants, as defined in CA/B 7.1.3.1. // RSA @@ -60,6 +63,18 @@ const SPKI_SECP521R1: AlgorithmIdentifier<'_> = AlgorithmIdentifier { params: AlgorithmParameters::Ec(EcParameters::NamedCurve(EC_SECP521R1)), }; +// Curve25519 +const SPKI_CURVE25519: AlgorithmIdentifier<'_> = AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: AlgorithmParameters::Ed25519, +}; + +// Curve448 +const SPKI_CURVE448: AlgorithmIdentifier<'_> = AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: AlgorithmParameters::Ed448, +}; + /// Permitted algorithms, from CA/B Forum's Baseline Requirements, section 7.1.3.1 (page 96) /// https://cabforum.org/wp-content/uploads/CA-Browser-Forum-BR-v2.0.0.pdf pub static WEBPKI_PERMITTED_SPKI_ALGORITHMS: LazyLock>>> = @@ -72,6 +87,27 @@ pub static WEBPKI_PERMITTED_SPKI_ALGORITHMS: LazyLock>>> = + LazyLock::new(|| { + Arc::new(HashSet::from([ + SPKI_RSA.clone(), + SPKI_SECP256R1.clone(), + SPKI_SECP384R1.clone(), + SPKI_SECP521R1.clone(), + SPKI_CURVE25519.clone(), + SPKI_CURVE448.clone(), + // Allowed, but not currently supported + // ML‐DSA‐44 + // ML‐DSA‐65 + // ML‐DSA‐87 + // ML‐KEM‐512 + // ML‐KEM‐768 + // ML‐KEM‐1024 + ])) + }); + // Signature AlgorithmIdentifier constants, as defined in CA/B 7.1.3.2. // RSASSA‐PKCS1‐v1_5 with SHA‐256 @@ -146,6 +182,18 @@ const ECDSA_SHA512: AlgorithmIdentifier<'_> = AlgorithmIdentifier { params: AlgorithmParameters::EcDsaWithSha512(None), }; +// Ed25519 +const ED25519: AlgorithmIdentifier<'_> = AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: AlgorithmParameters::Ed25519, +}; + +// Ed448 +const ED448: AlgorithmIdentifier<'_> = AlgorithmIdentifier { + oid: asn1::DefinedByMarker::marker(), + params: AlgorithmParameters::Ed448, +}; + /// Permitted algorithms, from CA/B Forum's Baseline Requirements, section 7.1.3.2 (pages 96-98) /// https://cabforum.org/wp-content/uploads/CA-Browser-Forum-BR-v2.0.0.pdf pub static WEBPKI_PERMITTED_SIGNATURE_ALGORITHMS: LazyLock>>> = @@ -163,6 +211,29 @@ pub static WEBPKI_PERMITTED_SIGNATURE_ALGORITHMS: LazyLock>>> = + LazyLock::new(|| { + Arc::new(HashSet::from([ + RSASSA_PKCS1V15_SHA256.clone(), + RSASSA_PKCS1V15_SHA384.clone(), + RSASSA_PKCS1V15_SHA512.clone(), + RSASSA_PSS_SHA256.clone(), + RSASSA_PSS_SHA384.clone(), + RSASSA_PSS_SHA512.clone(), + ECDSA_SHA256.clone(), + ECDSA_SHA384.clone(), + ECDSA_SHA512.clone(), + ED25519.clone(), + ED448.clone(), + // Allowed, but not currently supported + // ML-DSA-44 + // ML-DSA-65 + // ML-DSA-87 + ])) + }); + /// A default reasonable maximum chain depth. /// /// This depth was chosen to balance between common validation lengths @@ -291,6 +362,54 @@ impl<'a, B: CryptoOps + 'a> PolicyDefinition<'a, B> { Ok(retval) } + fn new_smime( + ops: B, + subject: Option>, + time: asn1::DateTime, + max_chain_depth: Option, + extended_key_usage: ObjectIdentifier, + ca_extension_policy: Option>, + ee_extension_policy: Option>, + ) -> Result { + let retval = Self { + ops, + max_chain_depth: max_chain_depth.unwrap_or(DEFAULT_MAX_CHAIN_DEPTH), + subject, + validation_time: time, + extended_key_usage, + minimum_rsa_modulus: SMIME_MINIMUM_RSA_MODULUS, + permitted_public_key_algorithms: Arc::clone(&*SMIME_PERMITTED_SPKI_ALGORITHMS), + permitted_signature_algorithms: Arc::clone(&*SMIME_PERMITTED_SIGNATURE_ALGORITHMS), + ca_extension_policy: ca_extension_policy + .unwrap_or_else(ExtensionPolicy::new_default_smime_ca), + ee_extension_policy: ee_extension_policy + .unwrap_or_else(ExtensionPolicy::new_default_smime_ee), + }; + + // Even without the following checks, verification would fail, + // but we want to fail early and provide a more specific error message. + if !matches!( + retval.ca_extension_policy.basic_constraints, + ExtensionValidator::Present { .. } + ) { + return Err( + "A CA extension policy must require the basicConstraints extension to be present.", + ); + } + + if retval.subject.is_some() + && !matches!( + retval.ee_extension_policy.subject_alternative_name, + ExtensionValidator::Present { .. } + ) + { + return Err( + "An EE extension policy used for S/MIME verification must require the subjectAltName extension to be present.", + ); + } + + Ok(retval) + } /// Create a new policy with suitable defaults for client certification /// validation. @@ -336,6 +455,30 @@ impl<'a, B: CryptoOps + 'a> PolicyDefinition<'a, B> { ee_extension_policy, ) } + + /// Create a new policy with suitable defaults for S/MIME subscriber + /// certificate validation. + /// + /// **IMPORTANT**: This is **not** the appropriate API for verifying + /// website or end-user (i.e. client or server) certificates. For that, + /// you **must** use [`Policy::server`] or [`Policy::client`]. + pub fn email( + ops: B, + time: asn1::DateTime, + max_chain_depth: Option, + ca_extension_policy: Option>, + ee_extension_policy: Option>, + ) -> Result { + Self::new_smime( + ops, + None, + time, + max_chain_depth, + EKU_EMAIL_PROTECTION_OID.clone(), + ca_extension_policy, + ee_extension_policy, + ) + } } pub struct Policy<'a, B: CryptoOps> {