use der::{asn1::ObjectIdentifier, Decode as _};
use x509_cert::ext::pkix::name::GeneralName;
use x509_cert::Certificate;
use crate::{truncate_for_detail, Lint, LintResult, Scope, Severity, SubjectKind};
const OID_SUBJECT_ALT_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.17");
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rfc6125TlsServerSanLint;
impl Lint for Rfc6125TlsServerSanLint {
fn id(&self) -> &'static str {
"rfc6125.cert.san.tls_server_dns_or_ip_required"
}
fn citation(&self) -> &'static str {
"RFC 6125 §6.4.1"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Leaf
}
fn title(&self) -> &str {
"TLS server certificate must include SAN dNSName or iPAddress"
}
fn spec_section_id(&self) -> Option<&str> {
Some("rfc6125-6.4.1")
}
fn spec_url(&self) -> Option<&str> {
Some("https://www.rfc-editor.org/rfc/rfc6125#section-6.4.1")
}
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; RFC 6125 §6.4.1 \
requires a dNSName or iPAddress entry",
);
};
let san =
match x509_cert::ext::pkix::SubjectAltName::from_der(san_ext.extn_value.as_bytes()) {
Ok(san) => san,
Err(e) => {
let e_str = e.to_string();
let safe_e = truncate_for_detail(&e_str);
return LintResult::error(format!(
"SubjectAltName extension value is malformed DER: {safe_e}"
));
}
};
if san.0.is_empty() {
return LintResult::error("SubjectAltName extension is present but contains no names");
}
let has_dns_or_ip = san
.0
.iter()
.any(|gn| matches!(gn, GeneralName::DnsName(_) | GeneralName::IpAddress(_)));
if has_dns_or_ip {
LintResult::Pass
} else {
LintResult::error(
"SubjectAltName does not contain a dNSName or iPAddress entry; \
RFC 6125 §6.4.1 requires at least one for TLS server identity",
)
}
}
}
#[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 tls_server_san_lint_accepts_dns_san() {
let lint = Rfc6125TlsServerSanLint;
let cert = load_cert("leaf-p256-365d-san-eku.der");
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn tls_server_san_lint_rejects_missing_san() {
let lint = Rfc6125TlsServerSanLint;
let cert = load_cert("leaf-p256-365d-no-san.der");
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("SubjectAltName extension absent"),
"error detail must mention missing SAN; got: {detail}"
);
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn tls_server_san_lint_rejects_san_without_dns_or_ip() {
let lint = Rfc6125TlsServerSanLint;
let cert = load_cert("smime-self-signed-365d.der");
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("dNSName") || detail.contains("iPAddress"),
"error detail must name the required SAN types; got: {detail}"
);
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn tls_server_san_lint_metadata_matches_rfc_section() {
let lint = Rfc6125TlsServerSanLint;
assert_eq!(lint.id(), "rfc6125.cert.san.tls_server_dns_or_ip_required");
assert_eq!(lint.citation(), "RFC 6125 §6.4.1");
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("rfc6125-6.4.1"));
assert_eq!(
lint.spec_url(),
Some("https://www.rfc-editor.org/rfc/rfc6125#section-6.4.1")
);
}
}