use der::{asn1::ObjectIdentifier, Decode as _};
use x509_cert::Certificate;
use crate::{Lint, LintProfile, LintResult, LintRunner, Scope, Severity, SubjectKind};
const SHA1_WITH_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.5");
const ECDSA_WITH_SHA1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.1");
const RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const OID_SUBJECT_ALT_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.17");
const OID_EXTENDED_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37");
const OID_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
const ID_KP_SERVER_AUTH: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1");
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ValidityMaxLint;
impl Lint for ValidityMaxLint {
fn id(&self) -> &'static str {
"cabf.br.tls.validity.max"
}
fn citation(&self) -> &'static str {
"CA/B Forum TLS BR §6.3.2 (SC-081)"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Leaf
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let tbs = &cert.tbs_certificate;
let not_before = tbs.validity.not_before.to_unix_duration().as_secs();
let not_after = tbs.validity.not_after.to_unix_duration().as_secs();
if not_after < not_before {
return LintResult::Error(
"leaf certificate notAfter precedes notBefore (inverted validity period)",
);
}
let duration_secs = not_after - not_before;
let cap = pkix_profiles::sc081_validity_cap(not_before);
if duration_secs > cap {
LintResult::Error("leaf certificate validity period exceeds SC-081 cap")
} else {
LintResult::Pass
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Sha1ProhibitedLint;
impl Lint for Sha1ProhibitedLint {
fn id(&self) -> &'static str {
"cabf.br.tls.alg.sha1_prohibited"
}
fn citation(&self) -> &'static str {
"CA/B Forum TLS BR §7.1.3"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Any
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let sig_alg = cert.signature_algorithm.oid;
if matches!(sig_alg, SHA1_WITH_RSA | ECDSA_WITH_SHA1) {
LintResult::Error(
"certificate uses SHA-1 signature algorithm, prohibited by TLS BR §7.1.3",
)
} else {
LintResult::Pass
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct RsaMinKeySizeLint;
impl Lint for RsaMinKeySizeLint {
fn id(&self) -> &'static str {
"cabf.br.tls.rsa.min_key_size"
}
fn citation(&self) -> &'static str {
"CA/B Forum TLS BR §6.1.5"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Any
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let spki = &cert.tbs_certificate.subject_public_key_info;
if spki.algorithm.oid != RSA_ENCRYPTION {
return LintResult::NotApplicable;
}
let key_bytes = spki.subject_public_key.raw_bytes();
rsa_modulus_bit_len(key_bytes).map_or(
LintResult::Error("RSA public key structure is unparseable"),
|n_bits| {
if n_bits >= 2048 {
LintResult::Pass
} else {
LintResult::Error("RSA key modulus is less than 2048 bits")
}
},
)
}
}
fn rsa_modulus_bit_len(der: &[u8]) -> Option<usize> {
let (seq_content, _rest) = der_peel_tlv(der, 0x30)?;
let (mut int_value, _rest) = der_peel_tlv(seq_content, 0x02)?;
if int_value.len() >= 2 && int_value[0] == 0x00 && int_value[1] & 0x80 != 0 {
int_value = &int_value[1..];
}
while let Some((&first, rest)) = int_value.split_first() {
if first == 0 {
int_value = rest;
} else {
break;
}
}
let (&high, _) = int_value.split_first()?;
if high == 0 {
return None; }
let leading_zeros = high.leading_zeros() as usize;
Some((int_value.len() - 1) * 8 + (8 - leading_zeros))
}
fn der_peel_tlv(input: &[u8], expected_tag: u8) -> Option<(&[u8], &[u8])> {
let (tag, rest) = input.split_first()?;
if *tag != expected_tag {
return None;
}
let (len, rest) = parse_der_length(rest)?;
if rest.len() < len {
return None;
}
let (value, remaining) = rest.split_at(len);
Some((value, remaining))
}
fn parse_der_length(input: &[u8]) -> Option<(usize, &[u8])> {
let (first, rest) = input.split_first()?;
if *first < 0x80 {
return Some((*first as usize, rest));
}
let n_bytes = (*first & 0x7f) as usize;
if n_bytes == 0 || n_bytes > 4 || rest.len() < n_bytes {
return None; }
let (len_bytes, rest) = rest.split_at(n_bytes);
let mut length: usize = 0;
for &b in len_bytes {
length = length.checked_shl(8)?.checked_add(b as usize)?;
}
Some((length, rest))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct SanRequiredLint;
impl Lint for SanRequiredLint {
fn id(&self) -> &'static str {
"cabf.br.tls.san.required"
}
fn citation(&self) -> &'static str {
"CA/B Forum TLS BR §7.1.4.2"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Leaf
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let Some(extensions) = &cert.tbs_certificate.extensions else {
return LintResult::Error("leaf certificate has no extensions; SubjectAltName absent");
};
let Some(san_ext) = extensions
.iter()
.find(|e| e.extn_id == OID_SUBJECT_ALT_NAME)
else {
return LintResult::Error("SubjectAltName extension absent from leaf certificate");
};
match x509_cert::ext::pkix::SubjectAltName::from_der(san_ext.extn_value.as_bytes()) {
Ok(san) if san.0.is_empty() => {
LintResult::Error("SubjectAltName extension is present but contains no names")
}
Ok(_) => LintResult::Pass,
Err(_) => LintResult::Error("SubjectAltName extension value is malformed DER"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct EkuServerAuthLint;
impl Lint for EkuServerAuthLint {
fn id(&self) -> &'static str {
"cabf.br.tls.eku.server_auth"
}
fn citation(&self) -> &'static str {
"CA/B Forum TLS BR §7.1.2.7.3"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Leaf
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let Some(extensions) = &cert.tbs_certificate.extensions else {
return LintResult::Error(
"leaf certificate has no extensions; ExtendedKeyUsage absent",
);
};
let Some(eku_ext) = extensions
.iter()
.find(|e| e.extn_id == OID_EXTENDED_KEY_USAGE)
else {
return LintResult::Error("ExtendedKeyUsage extension absent from leaf certificate");
};
match x509_cert::ext::pkix::ExtendedKeyUsage::from_der(eku_ext.extn_value.as_bytes()) {
Ok(eku) => {
if eku.0.contains(&ID_KP_SERVER_AUTH) {
LintResult::Pass
} else {
LintResult::Error(
"ExtendedKeyUsage does not include id-kp-serverAuth (1.3.6.1.5.5.7.3.1)",
)
}
}
Err(_) => LintResult::Error("ExtendedKeyUsage extension value is malformed DER"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct BcCaFlagLint;
impl Lint for BcCaFlagLint {
fn id(&self) -> &'static str {
"cabf.br.tls.bc.ca_flag"
}
fn citation(&self) -> &'static str {
"CA/B Forum TLS BR §7.1.2.5"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::IntermediateCa
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let Some(extensions) = &cert.tbs_certificate.extensions else {
return LintResult::Error(
"intermediate CA certificate has no extensions; BasicConstraints absent",
);
};
let Some(bc_ext) = extensions
.iter()
.find(|e| e.extn_id == OID_BASIC_CONSTRAINTS)
else {
return LintResult::Error(
"BasicConstraints extension absent from intermediate CA certificate",
);
};
x509_cert::ext::pkix::BasicConstraints::from_der(bc_ext.extn_value.as_bytes()).map_or(
LintResult::Error("BasicConstraints extension value is malformed DER"),
|bc| {
if bc.ca {
LintResult::Pass
} else {
LintResult::Error("BasicConstraints present but cA flag is not TRUE")
}
},
)
}
}
pub struct CabfTlsBrProfile;
impl pkix_path::Profile for CabfTlsBrProfile {
fn id(&self) -> &'static str {
pkix_profiles::WebPkiProfile.id()
}
fn version(&self) -> &'static str {
pkix_profiles::WebPkiProfile.version()
}
fn policy(&self, now_unix: u64) -> pkix_path::ValidationPolicy {
pkix_profiles::WebPkiProfile.policy(now_unix)
}
fn policy_oids(&self) -> &[der::asn1::ObjectIdentifier] {
pkix_profiles::WebPkiProfile.policy_oids()
}
}
#[must_use]
pub fn all_lints() -> Vec<Box<dyn Lint>> {
vec![
Box::new(ValidityMaxLint),
Box::new(Sha1ProhibitedLint),
Box::new(RsaMinKeySizeLint),
Box::new(SanRequiredLint),
Box::new(EkuServerAuthLint),
Box::new(BcCaFlagLint),
]
}
impl LintProfile for CabfTlsBrProfile {
fn lints(&self) -> &[Box<dyn Lint>] {
static LINTS: std::sync::OnceLock<Vec<Box<dyn Lint>>> = std::sync::OnceLock::new();
LINTS.get_or_init(all_lints)
}
fn lint_runner(&self) -> LintRunner {
LintRunner::new(all_lints())
}
}