use std::borrow::Cow;
use der::{asn1::ObjectIdentifier, Decode as _};
use x509_cert::Certificate;
use crate::{
truncate_for_detail, Lint, LintParameter, LintResult, ParameterError, Scope, Severity,
SubjectKind,
};
const OID_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
const OID_EXTENDED_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37");
const OID_SUBJECT_ALT_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.17");
const ID_KP_SERVER_AUTH: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1");
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rfc5280MaxSerialLengthLint {
max_octets: usize,
parameters: Vec<LintParameter>,
}
impl Default for Rfc5280MaxSerialLengthLint {
fn default() -> Self {
Self::with_max_octets(20)
}
}
const MAX_OCTETS_RANGE: std::ops::RangeInclusive<usize> = 1..=21;
fn validate_max_octets(value: usize) -> Result<(), String> {
if MAX_OCTETS_RANGE.contains(&value) {
Ok(())
} else {
Err(format!(
"max-octets must be in the range {}..={} (got {value}); \
RFC 5280 §4.1.2.2 mandates serials of 1..=20 octets (encoded-content reading) \
or 1..=21 octets (unsigned-value reading). Wire a custom Lint impl for wider/narrower bounds.",
MAX_OCTETS_RANGE.start(),
MAX_OCTETS_RANGE.end(),
))
}
}
impl Rfc5280MaxSerialLengthLint {
#[must_use]
pub fn with_max_octets(max_octets: usize) -> Self {
if let Err(reason) = validate_max_octets(max_octets) {
panic!("Rfc5280MaxSerialLengthLint::with_max_octets: {reason}");
}
let parameters = vec![LintParameter {
id: Cow::Borrowed("max-octets"),
label: Cow::Borrowed("Maximum allowed serial number length in octets"),
default_value: Cow::Borrowed("20"),
}];
Self {
max_octets,
parameters,
}
}
#[must_use]
pub fn max_octets(&self) -> usize {
self.max_octets
}
}
impl Lint for Rfc5280MaxSerialLengthLint {
fn id(&self) -> &'static str {
"rfc5280.cert.serial_number.max_octets"
}
fn citation(&self) -> &'static str {
"RFC 5280 §4.1.2.2"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Any
}
fn title(&self) -> &str {
"Certificate serialNumber must not exceed 20 octets"
}
fn spec_section_id(&self) -> Option<&str> {
Some("rfc5280-4.1.2.2")
}
fn spec_url(&self) -> Option<&str> {
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.2")
}
fn parameters(&self) -> &[LintParameter] {
&self.parameters
}
fn set_parameter(&mut self, id: &str, value: &str) -> Result<(), ParameterError> {
match id {
"max-octets" => {
let parsed: usize = value.parse().map_err(|_| ParameterError::InvalidValue {
id: id.to_owned(),
reason: format!("expected non-negative integer, got '{value}'"),
})?;
validate_max_octets(parsed).map_err(|reason| ParameterError::InvalidValue {
id: id.to_owned(),
reason,
})?;
self.max_octets = parsed;
Ok(())
}
other => Err(ParameterError::UnknownParameter(other.to_owned())),
}
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let len = cert.tbs_certificate.serial_number.as_bytes().len();
let cap = self.max_octets;
if len > cap {
LintResult::error(format!(
"certificate serialNumber is {len} octets, exceeds cap of {cap} octets"
))
} else {
LintResult::Pass
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rfc5280BasicConstraintsCaLeafLint;
impl Lint for Rfc5280BasicConstraintsCaLeafLint {
fn id(&self) -> &'static str {
"rfc5280.cert.bc.ca_false_for_leaf"
}
fn citation(&self) -> &'static str {
"RFC 5280 §4.2.1.9"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Leaf
}
fn title(&self) -> &str {
"End-entity certificate must not assert BasicConstraints.cA=TRUE"
}
fn spec_section_id(&self) -> Option<&str> {
Some("rfc5280-4.2.1.9")
}
fn spec_url(&self) -> Option<&str> {
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9")
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let Some(extensions) = &cert.tbs_certificate.extensions else {
return LintResult::Pass;
};
let Some(bc_ext) = extensions
.iter()
.find(|e| e.extn_id == OID_BASIC_CONSTRAINTS)
else {
return LintResult::Pass;
};
match x509_cert::ext::pkix::BasicConstraints::from_der(bc_ext.extn_value.as_bytes()) {
Ok(bc) => {
if bc.ca {
LintResult::error(
"end-entity certificate asserts BasicConstraints.cA=TRUE; \
only CA certificates may assert cA",
)
} else {
LintResult::Pass
}
}
Err(e) => {
let e_str = e.to_string();
let safe_e = truncate_for_detail(&e_str);
LintResult::error(format!(
"BasicConstraints extension value is malformed DER: {safe_e}"
))
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rfc5280EkuServerAuthLint;
impl Lint for Rfc5280EkuServerAuthLint {
fn id(&self) -> &'static str {
"rfc5280.cert.eku.server_auth_required"
}
fn citation(&self) -> &'static str {
"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 {
"TLS server certificate must include id-kp-serverAuth EKU"
}
fn spec_section_id(&self) -> Option<&str> {
Some("rfc5280-4.2.1.12")
}
fn spec_url(&self) -> Option<&str> {
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12")
}
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(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}"
))
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rfc5280SanRequiredWhenSubjectEmptyLint;
impl Lint for Rfc5280SanRequiredWhenSubjectEmptyLint {
fn id(&self) -> &'static str {
"rfc5280.cert.san.required_when_subject_empty"
}
fn citation(&self) -> &'static str {
"RFC 5280 §4.2.1.6"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Any
}
fn title(&self) -> &str {
"Empty-subject certificate must include a critical subjectAltName"
}
fn spec_section_id(&self) -> Option<&str> {
Some("rfc5280-4.2.1.6")
}
fn spec_url(&self) -> Option<&str> {
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.6")
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
if !cert.tbs_certificate.subject.is_empty() {
return LintResult::Pass;
}
let Some(extensions) = &cert.tbs_certificate.extensions else {
return LintResult::error(
"empty-subject certificate has no extensions; \
RFC 5280 §4.2.1.6 requires a critical subjectAltName",
);
};
let Some(san_ext) = extensions
.iter()
.find(|e| e.extn_id == OID_SUBJECT_ALT_NAME)
else {
return LintResult::error(
"empty-subject certificate omits subjectAltName; \
RFC 5280 §4.2.1.6 requires it to be present and critical",
);
};
if !san_ext.critical {
return LintResult::error(
"empty-subject certificate carries subjectAltName but it is not marked critical; \
RFC 5280 §4.2.1.6 requires the extension to be critical when the Subject is empty",
);
}
LintResult::Pass
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rfc5280SignatureAlgorithmMatchLint;
impl Lint for Rfc5280SignatureAlgorithmMatchLint {
fn id(&self) -> &'static str {
"rfc5280.cert.signature_algorithm_match"
}
fn citation(&self) -> &'static str {
"RFC 5280 §4.1.1.2"
}
fn severity(&self) -> Severity {
Severity::Error
}
fn scope(&self) -> Scope {
Scope::Certificate
}
fn applies_to(&self) -> SubjectKind {
SubjectKind::Any
}
fn title(&self) -> &str {
"Outer signatureAlgorithm must equal tbsCertificate.signature"
}
fn spec_section_id(&self) -> Option<&str> {
Some("rfc5280-4.1.1.2")
}
fn spec_url(&self) -> Option<&str> {
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.1.1.2")
}
fn check_cert(&self, cert: &Certificate, _kind: SubjectKind, _now_unix: u64) -> LintResult {
let outer = &cert.signature_algorithm;
let inner = &cert.tbs_certificate.signature;
if outer == inner {
LintResult::Pass
} else {
LintResult::error(format!(
"outer signatureAlgorithm ({}) does not match \
tbsCertificate.signature ({}); \
RFC 5280 §4.1.1.2 requires both to be identical",
outer.oid, inner.oid
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use x509_cert::Certificate;
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 default_lint_accepts_20_octet_serial() {
let lint = Rfc5280MaxSerialLengthLint::default();
assert_eq!(lint.max_octets(), 20);
let cert = load_cert("leaf-p256-50d-post-sc081-100d.der");
assert_eq!(cert.tbs_certificate.serial_number.as_bytes().len(), 20);
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn default_lint_accepts_short_serial() {
let lint = Rfc5280MaxSerialLengthLint::default();
let cert = load_cert("leaf-rsa2047-365d-san-eku.der");
assert_eq!(cert.tbs_certificate.serial_number.as_bytes().len(), 3);
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn tightened_cap_rejects_20_octet_serial_with_attribution() {
let mut lint = Rfc5280MaxSerialLengthLint::default();
lint.set_parameter("max-octets", "10")
.expect("set_parameter ok");
let cert = load_cert("leaf-rsa2048-sha1.der");
assert_eq!(cert.tbs_certificate.serial_number.as_bytes().len(), 20);
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("20 octets"),
"error detail must name the actual length 20; got: {detail}"
);
assert!(
detail.contains("10 octets"),
"error detail must name the cap 10; got: {detail}"
);
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn set_parameter_to_3_keeps_3_octet_serial_passing() {
let mut lint = Rfc5280MaxSerialLengthLint::default();
lint.set_parameter("max-octets", "3").expect("set ok");
let cert = load_cert("leaf-rsa2047-365d-san-eku.der");
assert_eq!(cert.tbs_certificate.serial_number.as_bytes().len(), 3);
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn serial_length_lint_rejects_21_octet_high_bit_set_serial() {
use der::{Decode, Encode};
use x509_cert::serial_number::SerialNumber;
let mut serial_der: Vec<u8> = vec![0x02, 0x15, 0x00, 0x80];
serial_der.extend(std::iter::repeat(0x00).take(19));
assert_eq!(serial_der.len(), 2 + 21, "test oracle: 21 octets of INTEGER content");
let synthetic_serial: SerialNumber = SerialNumber::from_der(&serial_der)
.expect("Rfc5280 profile permits decode of up to 21 octets");
assert_eq!(
synthetic_serial.as_bytes().len(),
21,
"x509-cert decoded the 21-byte INTEGER content without stripping the sign byte"
);
let mut cert = load_cert("leaf-rsa2048-365d-san-eku.der");
cert.tbs_certificate.serial_number = synthetic_serial;
let cert_der = cert.to_der().expect("re-encode mutated cert");
let decoded = <Certificate as der::Decode>::from_der(&cert_der)
.expect("decode mutated cert through Rfc5280 profile");
assert_eq!(decoded.tbs_certificate.serial_number.as_bytes().len(), 21);
let lint = Rfc5280MaxSerialLengthLint::default();
assert_eq!(lint.max_octets(), 20);
match lint.check_cert(&decoded, SubjectKind::Any, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("21 octets"),
"error detail must report the actual encoded length 21; got: {detail}"
);
assert!(
detail.contains("20 octets"),
"error detail must report the cap 20; got: {detail}"
);
}
other => panic!(
"encoded-content reading: 21-octet INTEGER content must trigger Error; got: {other:?}"
),
}
}
#[test]
fn set_parameter_to_2_rejects_3_octet_serial() {
let mut lint = Rfc5280MaxSerialLengthLint::default();
lint.set_parameter("max-octets", "2").expect("set ok");
let cert = load_cert("leaf-rsa2047-365d-san-eku.der");
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(detail.contains("3 octets"));
assert!(detail.contains("2 octets"));
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn set_parameter_unknown_id_errors() {
let mut lint = Rfc5280MaxSerialLengthLint::default();
let err = lint
.set_parameter("not-a-real-parameter", "1")
.expect_err("unknown id must error");
match err {
ParameterError::UnknownParameter(id) => {
assert_eq!(id, "not-a-real-parameter");
}
other => panic!("expected UnknownParameter, got: {other:?}"),
}
}
#[test]
fn set_parameter_invalid_value_errors() {
let mut lint = Rfc5280MaxSerialLengthLint::default();
let err = lint
.set_parameter("max-octets", "not-a-number")
.expect_err("non-numeric value must error");
match err {
ParameterError::InvalidValue { id, .. } => assert_eq!(id, "max-octets"),
other => panic!("expected InvalidValue, got: {other:?}"),
}
let err_zero = lint
.set_parameter("max-octets", "0")
.expect_err("zero value must error");
match err_zero {
ParameterError::InvalidValue { id, reason } => {
assert_eq!(id, "max-octets");
assert!(
reason.contains("1..=21"),
"error reason must name the valid range; got: {reason}"
);
assert!(
reason.contains("got 0"),
"error reason must include the offending input; got: {reason}"
);
}
other => panic!("expected InvalidValue, got: {other:?}"),
}
}
#[test]
fn set_parameter_rejects_values_above_21() {
let mut lint = Rfc5280MaxSerialLengthLint::default();
for bogus in ["22", "64", "100", "18446744073709551615" ] {
let err = lint
.set_parameter("max-octets", bogus)
.expect_err("out-of-range value must error");
match err {
ParameterError::InvalidValue { id, reason } => {
assert_eq!(id, "max-octets");
assert!(
reason.contains("1..=21"),
"error reason must name the valid range; got: {reason}"
);
}
other => panic!("expected InvalidValue, got: {other:?}"),
}
}
}
#[test]
fn set_parameter_accepts_21_for_unsigned_value_reading() {
let mut lint = Rfc5280MaxSerialLengthLint::default();
lint.set_parameter("max-octets", "21")
.expect("21 is permitted (unsigned-value reading)");
assert_eq!(lint.max_octets(), 21);
}
#[test]
#[should_panic(expected = "max-octets must be in the range 1..=21")]
fn with_max_octets_zero_panics() {
let _ = Rfc5280MaxSerialLengthLint::with_max_octets(0);
}
#[test]
#[should_panic(expected = "max-octets must be in the range 1..=21")]
fn with_max_octets_above_21_panics() {
let _ = Rfc5280MaxSerialLengthLint::with_max_octets(22);
}
#[test]
fn parameters_advertises_max_octets() {
let lint = Rfc5280MaxSerialLengthLint::default();
let params = lint.parameters();
assert_eq!(params.len(), 1);
assert_eq!(params[0].id, "max-octets");
assert_eq!(params[0].default_value, "20");
assert!(!params[0].label.is_empty());
}
#[test]
fn metadata_matches_rfc_section() {
let lint = Rfc5280MaxSerialLengthLint::default();
assert_eq!(lint.id(), "rfc5280.cert.serial_number.max_octets");
assert_eq!(lint.citation(), "RFC 5280 §4.1.2.2");
assert_eq!(lint.spec_section_id(), Some("rfc5280-4.1.2.2"));
assert_eq!(
lint.spec_url(),
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.1.2.2")
);
}
#[test]
fn bc_ca_leaf_lint_accepts_normal_leaf() {
let lint = Rfc5280BasicConstraintsCaLeafLint;
let cert = load_cert("leaf-p256-365d-san-eku.der");
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn bc_ca_leaf_lint_rejects_cert_with_ca_true() {
let lint = Rfc5280BasicConstraintsCaLeafLint;
let cert = load_cert("webpki-self-signed-365d.der");
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("cA=TRUE"),
"error detail must mention cA=TRUE; got: {detail}"
);
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn bc_ca_leaf_lint_metadata_matches_rfc_section() {
let lint = Rfc5280BasicConstraintsCaLeafLint;
assert_eq!(lint.id(), "rfc5280.cert.bc.ca_false_for_leaf");
assert_eq!(lint.citation(), "RFC 5280 §4.2.1.9");
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("rfc5280-4.2.1.9"));
assert_eq!(
lint.spec_url(),
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9")
);
}
#[test]
fn eku_server_auth_lint_accepts_server_auth_eku() {
let lint = Rfc5280EkuServerAuthLint;
let cert = load_cert("leaf-p256-365d-san-eku.der");
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn eku_server_auth_lint_rejects_missing_eku() {
let lint = Rfc5280EkuServerAuthLint;
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 eku_server_auth_lint_rejects_wrong_eku() {
let lint = Rfc5280EkuServerAuthLint;
let cert = load_cert("leaf-p256-365d-wrong-eku.der");
match lint.check_cert(&cert, SubjectKind::Leaf, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("id-kp-serverAuth"),
"error detail must mention id-kp-serverAuth; got: {detail}"
);
}
other => panic!("expected Error, got: {other:?}"),
}
}
#[test]
fn eku_server_auth_lint_metadata_matches_rfc_section() {
let lint = Rfc5280EkuServerAuthLint;
assert_eq!(lint.id(), "rfc5280.cert.eku.server_auth_required");
assert_eq!(lint.citation(), "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("rfc5280-4.2.1.12"));
assert_eq!(
lint.spec_url(),
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12")
);
}
#[test]
fn san_required_when_subject_empty_passes_when_subject_present() {
let lint = Rfc5280SanRequiredWhenSubjectEmptyLint;
let cert = load_cert("leaf-p256-365d-san-eku.der");
assert!(
!cert.tbs_certificate.subject.is_empty(),
"fixture must have a non-empty Subject for this test"
);
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn san_required_when_subject_empty_passes_with_no_san_when_subject_present() {
let lint = Rfc5280SanRequiredWhenSubjectEmptyLint;
let cert = load_cert("leaf-p256-365d-no-san.der");
assert!(!cert.tbs_certificate.subject.is_empty());
assert_eq!(
lint.check_cert(&cert, SubjectKind::Leaf, 0),
LintResult::Pass
);
}
#[test]
fn san_required_when_subject_empty_metadata_matches_rfc_section() {
let lint = Rfc5280SanRequiredWhenSubjectEmptyLint;
assert_eq!(lint.id(), "rfc5280.cert.san.required_when_subject_empty");
assert_eq!(lint.citation(), "RFC 5280 §4.2.1.6");
assert_eq!(lint.severity(), Severity::Error);
assert_eq!(lint.scope(), Scope::Certificate);
assert_eq!(lint.applies_to(), SubjectKind::Any);
assert_eq!(lint.spec_section_id(), Some("rfc5280-4.2.1.6"));
assert_eq!(
lint.spec_url(),
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.6")
);
}
#[test]
fn signature_algorithm_match_passes_on_well_formed_rsa_fixture() {
let lint = Rfc5280SignatureAlgorithmMatchLint;
let cert = load_cert("leaf-rsa2048-365d-san-eku.der");
assert_eq!(
lint.check_cert(&cert, SubjectKind::Any, 0),
LintResult::Pass
);
}
#[test]
fn signature_algorithm_match_passes_on_well_formed_ecdsa_fixture() {
let lint = Rfc5280SignatureAlgorithmMatchLint;
let cert = load_cert("leaf-p256-365d-san-eku.der");
assert_eq!(
lint.check_cert(&cert, SubjectKind::Any, 0),
LintResult::Pass
);
}
#[test]
fn signature_algorithm_match_rejects_outer_null_inner_absent() {
use der::asn1::Any;
let lint = Rfc5280SignatureAlgorithmMatchLint;
let mut cert = load_cert("leaf-rsa2048-365d-san-eku.der");
assert!(cert.signature_algorithm.parameters.is_some());
assert_eq!(
cert.signature_algorithm, cert.tbs_certificate.signature,
"fixture must start with matching outer/inner identifiers"
);
cert.tbs_certificate.signature.parameters = None;
assert_eq!(
cert.signature_algorithm.parameters.as_ref().unwrap(),
&Any::null()
);
assert!(cert.tbs_certificate.signature.parameters.is_none());
match lint.check_cert(&cert, SubjectKind::Any, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("does not match"),
"error detail must report mismatch; got: {detail}"
);
}
other => panic!("expected Error on outer-NULL/inner-absent mismatch, got: {other:?}"),
}
}
#[test]
fn signature_algorithm_match_rejects_outer_absent_inner_null() {
let lint = Rfc5280SignatureAlgorithmMatchLint;
let mut cert = load_cert("leaf-rsa2048-365d-san-eku.der");
cert.signature_algorithm.parameters = None;
assert!(cert.tbs_certificate.signature.parameters.is_some());
match lint.check_cert(&cert, SubjectKind::Any, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("does not match"),
"error detail must report mismatch; got: {detail}"
);
}
other => panic!("expected Error on outer-absent/inner-NULL mismatch, got: {other:?}"),
}
}
#[test]
fn signature_algorithm_match_rejects_oid_mismatch() {
use der::oid::ObjectIdentifier;
let lint = Rfc5280SignatureAlgorithmMatchLint;
let mut cert = load_cert("leaf-rsa2048-365d-san-eku.der");
let ecdsa_with_sha256: ObjectIdentifier = "1.2.840.10045.4.3.2".parse().unwrap();
cert.tbs_certificate.signature.oid = ecdsa_with_sha256;
match lint.check_cert(&cert, SubjectKind::Any, 0) {
LintResult::Error(detail) => {
assert!(
detail.contains("1.2.840.10045.4.3.2"),
"error detail must name the mutated inner OID; got: {detail}"
);
}
other => panic!("expected Error on OID mismatch, got: {other:?}"),
}
}
#[test]
fn algorithm_identifier_decode_preserves_option_discriminant() {
use der::{Decode, Encode};
let mut cert = load_cert("leaf-rsa2048-365d-san-eku.der");
cert.signature_algorithm.parameters = None;
let der_bytes = cert.to_der().expect("encode mutated cert");
let decoded = x509_cert::Certificate::from_der(&der_bytes).expect("decode mutated cert");
assert!(
decoded.signature_algorithm.parameters.is_none(),
"x509-cert decode normalized None to Some(NULL); the lint's premise is invalidated"
);
let lint = Rfc5280SignatureAlgorithmMatchLint;
match lint.check_cert(&decoded, SubjectKind::Any, 0) {
LintResult::Error(_) => {}
other => panic!("expected Error after DER round-trip, got: {other:?}"),
}
}
#[test]
fn signature_algorithm_match_metadata_matches_rfc_section() {
let lint = Rfc5280SignatureAlgorithmMatchLint;
assert_eq!(lint.id(), "rfc5280.cert.signature_algorithm_match");
assert_eq!(lint.citation(), "RFC 5280 §4.1.1.2");
assert_eq!(lint.severity(), Severity::Error);
assert_eq!(lint.scope(), Scope::Certificate);
assert_eq!(lint.applies_to(), SubjectKind::Any);
assert_eq!(lint.spec_section_id(), Some("rfc5280-4.1.1.2"));
assert_eq!(
lint.spec_url(),
Some("https://www.rfc-editor.org/rfc/rfc5280#section-4.1.1.2")
);
}
}