pub use x509_parser::certificate::*;
use x509_parser::error::X509Error;
use x509_parser::nom;
use x509_parser::objects::{oid_registry, oid2sn};
use x509_parser::prelude::FromDer;
use crate::GeneralName;
use crate::asn1_rs::Oid;
use crate::extensions::ParsedExtension;
use crate::oid_registry::{
OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER, OID_X509_EXT_SUBJECT_KEY_IDENTIFIER,
};
pub type Certificate<'a> = X509Certificate<'a>;
pub trait CertificateExt<'a>: Sized {
fn parse(der: &'a [u8]) -> Result<Self, X509Error>;
fn subject_key_identifier(&self) -> Option<&'a [u8]>;
fn authority_key_identifier(&self) -> Option<&'a [u8]>;
fn subject_key(&self) -> Vec<u8>;
fn issuer_key(&self) -> Vec<u8>;
fn subject_alternative_names(&self) -> Vec<GeneralName<'a>>;
fn has_same_identity_as(&self, other: &Self) -> bool;
}
impl<'a> CertificateExt<'a> for Certificate<'a> {
fn parse(der: &'a [u8]) -> Result<Self, X509Error> {
Certificate::from_der(der)
.map(|(_, certificate)| certificate)
.map_err(|err| match err {
nom::Err::Error(e) | nom::Err::Failure(e) => e,
nom::Err::Incomplete(_) => X509Error::InvalidCertificate,
})
}
fn subject_key_identifier(&self) -> Option<&'a [u8]> {
let ext = self
.tbs_certificate
.get_extension_unique(&OID_X509_EXT_SUBJECT_KEY_IDENTIFIER)
.ok()??;
match ext.parsed_extension() {
ParsedExtension::SubjectKeyIdentifier(key_id) => Some(key_id.0),
_ => None,
}
}
fn authority_key_identifier(&self) -> Option<&'a [u8]> {
let ext = self
.tbs_certificate
.get_extension_unique(&OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER)
.ok()??;
match ext.parsed_extension() {
ParsedExtension::AuthorityKeyIdentifier(aki) => aki
.key_identifier
.as_ref()
.map(|id| id.0),
_ => None,
}
}
fn subject_key(&self) -> Vec<u8> {
self.subject().as_raw().to_vec()
}
fn issuer_key(&self) -> Vec<u8> {
self.issuer().as_raw().to_vec()
}
fn subject_alternative_names(&self) -> Vec<GeneralName<'a>> {
self.tbs_certificate
.subject_alternative_name()
.ok()
.flatten()
.map(|ext| ext.value.general_names.clone())
.unwrap_or_default()
}
fn has_same_identity_as(&self, other: &Self) -> bool {
self.subject() == other.subject()
&& self.public_key() == other.public_key()
&& self.subject_alternative_names() == other.subject_alternative_names()
}
}
pub fn format_certificate(cert: &Certificate<'_>) -> String {
let tbs = &cert.tbs_certificate;
let validity = tbs.validity();
format!(
"Certificate(version: {}, serialNumber: {}, issuer: {}, subject: {}, \
notValidBefore: {}, notValidAfter: {}, publicKey: {}, signature: {}, extensions: {})",
tbs.version.to_string().to_lowercase(),
cert.raw_serial_as_string(),
tbs.issuer,
tbs.subject,
validity.not_before,
validity.not_after,
algorithm_name(&tbs.subject_pki.algorithm.algorithm),
algorithm_name(&cert.signature_algorithm.algorithm),
format_extensions(cert),
)
}
fn algorithm_name(oid: &Oid<'_>) -> String {
match oid2sn(oid, oid_registry()) {
Ok(name) => name.to_string(),
Err(_) => oid.to_id_string(),
}
}
fn format_extensions(cert: &Certificate<'_>) -> String {
let rendered: Vec<String> = cert
.tbs_certificate
.iter_extensions()
.map(|ext| {
let name = algorithm_name(&ext.oid);
if ext.critical {
format!("{name} (critical)")
} else {
name
}
})
.collect();
format!("[{}]", rendered.join(", "))
}
#[cfg(test)]
mod tests {
use x509_validator_testkit::{cert, issue_ca, issue_leaf, self_signed_ca_with};
use crate::certificate::format_certificate;
fn assert_no_byte_dump(rendered: &str) {
assert!(!rendered.contains('\n'), "summary must be a single line");
assert!(
rendered.len() < 1024,
"summary is suspiciously long: {} bytes",
rendered.len()
);
let digit_runs = rendered
.split(|c: char| !c.is_ascii_digit() && c != ',' && c != ' ')
.filter(|run| run.matches(',').count() >= 3)
.count();
assert_eq!(
digit_runs, 0,
"summary appears to contain a byte-array dump: {rendered}"
);
}
#[test]
fn formats_self_signed_ca() {
let ca = self_signed_ca_with("Test Root CA", |_| {});
let cert = cert(ca.der);
let rendered = format_certificate(&cert);
assert!(
rendered.starts_with("Certificate(version: v3, serialNumber: "),
"{rendered}"
);
assert!(rendered.ends_with(')'), "{rendered}");
assert!(rendered.contains("issuer: CN=Test Root CA"), "{rendered}");
assert!(rendered.contains("subject: CN=Test Root CA"), "{rendered}");
assert!(rendered.contains("notValidBefore: "), "{rendered}");
assert!(rendered.contains("notValidAfter: "), "{rendered}");
assert!(rendered.contains("publicKey: "), "{rendered}");
assert!(rendered.contains("signature: "), "{rendered}");
assert!(
rendered.contains("basicConstraints (critical)"),
"{rendered}"
);
assert_no_byte_dump(&rendered);
}
#[test]
fn serial_number_is_hex_not_decimal_bytes() {
let ca = self_signed_ca_with("Serial Root", |_| {});
let cert = cert(ca.der);
let rendered = format_certificate(&cert);
let serial = rendered
.split("serialNumber: ")
.nth(1)
.and_then(|rest| rest.split(", issuer:").next())
.expect("serial field present");
assert!(!serial.is_empty());
assert!(
serial
.chars()
.all(|c| c.is_ascii_hexdigit() || c == ':'),
"serial should be hex: {serial}"
);
assert_eq!(serial, cert.raw_serial_as_string());
}
#[test]
fn formats_leaf_with_sans() {
let root = self_signed_ca_with("SAN Root", |_| {});
let leaf_der = issue_leaf("leaf", &["www.example.com", "example.com"], &root);
let cert = cert(leaf_der);
let rendered = format_certificate(&cert);
assert!(rendered.contains("subject: CN=leaf"), "{rendered}");
assert!(rendered.contains("issuer: CN=SAN Root"), "{rendered}");
assert!(rendered.contains("subjectAltName"), "{rendered}");
assert!(!rendered.contains("www.example.com"), "{rendered}");
assert_no_byte_dump(&rendered);
}
#[test]
fn formats_certificate_with_multiple_extensions() {
let root = self_signed_ca_with("Multi Root", |_| {});
let intermediate = issue_ca("Multi Intermediate", &root, Some(0), |params| {
params.key_usages = vec![
x509_validator_testkit::rcgen::KeyUsagePurpose::KeyCertSign,
x509_validator_testkit::rcgen::KeyUsagePurpose::CrlSign,
];
params.name_constraints = Some(x509_validator_testkit::name_constraints(
vec![x509_validator_testkit::dns_subtree("example.com")],
vec![],
));
});
let cert = cert(intermediate.der);
let rendered = format_certificate(&cert);
let extensions = rendered
.split("extensions: [")
.nth(1)
.and_then(|rest| rest.strip_suffix("])"))
.expect("extension list present");
assert!(extensions.contains("basicConstraints"), "{rendered}");
assert!(extensions.contains("keyUsage"), "{rendered}");
assert!(extensions.contains("nameConstraints"), "{rendered}");
assert!(extensions.split(", ").count() >= 3, "{rendered}");
assert!(extensions.contains("(critical)"), "{rendered}");
assert_no_byte_dump(&rendered);
}
}