use der::{asn1::ObjectIdentifier, Decode as _};
use x509_cert::Certificate;
use crate::{truncate_for_detail, Lint, LintResult, Scope, Severity, SubjectKind};
const OID_EXTENDED_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37");
const ID_KP_EMAIL_PROTECTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.4");
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rfc8551EkuEmailProtectionLint;
impl Lint for Rfc8551EkuEmailProtectionLint {
fn id(&self) -> &'static str {
"rfc8551.cert.eku.email_protection_required"
}
fn citation(&self) -> &'static str {
"RFC 8551 §3.3 (+ RFC 5280 §4.2.1.12)"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Leaf
}
fn title(&self) -> &str {
"S/MIME certificate must include id-kp-emailProtection EKU"
}
fn spec_section_id(&self) -> Option<&str> {
Some("rfc8551-3.3")
}
fn spec_url(&self) -> Option<&str> {
Some("https://www.rfc-editor.org/rfc/rfc8551#section-3.3")
}
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_EMAIL_PROTECTION) {
LintResult::Pass
} else {
LintResult::error(
"ExtendedKeyUsage does not include id-kp-emailProtection \
(1.3.6.1.5.5.7.3.4)",
)
}
}
Err(e) => {
let e_str = e.to_string();
let safe_e = truncate_for_detail(&e_str);
LintResult::error(format!(
"ExtendedKeyUsage extension value is malformed DER: {safe_e}"
))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn load_cert(name: &str) -> Certificate {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../pkix-path/tests/fixtures/policy-checks/")
.join(name);
let der =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
<Certificate as der::Decode>::from_der(&der)
.unwrap_or_else(|e| panic!("decode fixture {name}: {e}"))
}
#[test]
fn email_protection_lint_accepts_email_protection_eku() {
let lint = Rfc8551EkuEmailProtectionLint;
let cert = load_cert("smime-self-signed-365d.der");
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn email_protection_lint_accepts_wrong_eku_fixture() {
let lint = Rfc8551EkuEmailProtectionLint;
let cert = load_cert("leaf-p256-365d-wrong-eku.der");
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn email_protection_lint_rejects_missing_eku() {
let lint = Rfc8551EkuEmailProtectionLint;
let cert = load_cert("leaf-p256-365d-no-eku.der");
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("ExtendedKeyUsage extension absent"),
"error detail must mention missing EKU; got: {detail}"
);
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn email_protection_lint_rejects_server_auth_only() {
let lint = Rfc8551EkuEmailProtectionLint;
let cert = load_cert("leaf-p256-365d-san-eku.der");
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("id-kp-emailProtection"),
"error detail must mention id-kp-emailProtection; got: {detail}"
);
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn email_protection_lint_metadata_matches_rfc_section() {
let lint = Rfc8551EkuEmailProtectionLint;
assert_eq!(lint.id(), "rfc8551.cert.eku.email_protection_required");
assert_eq!(lint.citation(), "RFC 8551 §3.3 (+ RFC 5280 §4.2.1.12)");
assert_eq!(lint.severity(), Severity::Error);
assert_eq!(lint.scope(), Scope::Certificate);
assert_eq!(lint.applies_to(), SubjectKind::Leaf);
assert_eq!(lint.spec_section_id(), Some("rfc8551-3.3"));
assert_eq!(
lint.spec_url(),
Some("https://www.rfc-editor.org/rfc/rfc8551#section-3.3")
);
}
}