use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CertificateValidation {
pub valid: bool,
pub expired: bool,
pub not_yet_valid: bool,
pub trust_path: Vec<String>,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl CertificateValidation {
#[must_use]
pub fn success(trust_path: Vec<String>) -> Self {
Self {
valid: true,
expired: false,
not_yet_valid: false,
trust_path,
errors: Vec::new(),
warnings: Vec::new(),
}
}
#[must_use]
pub fn failure(error: impl Into<String>) -> Self {
Self {
valid: false,
expired: false,
not_yet_valid: false,
trust_path: Vec::new(),
errors: vec![error.into()],
warnings: Vec::new(),
}
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.valid && self.errors.is_empty()
}
#[must_use]
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn add_error(&mut self, error: impl Into<String>) {
self.errors.push(error.into());
self.valid = false;
}
pub fn add_warning(&mut self, warning: impl Into<String>) {
self.warnings.push(warning.into());
}
}
impl Default for CertificateValidation {
fn default() -> Self {
Self::failure("Not validated")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CertificateInfo {
pub subject: String,
pub issuer: String,
pub serial_number: String,
pub not_before: String,
pub not_after: String,
pub is_ca: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub key_usage: Vec<KeyUsage>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extended_key_usage: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subject_alt_names: Vec<String>,
pub fingerprint_sha256: String,
}
impl CertificateInfo {
#[must_use]
pub fn new(
subject: impl Into<String>,
issuer: impl Into<String>,
serial_number: impl Into<String>,
) -> Self {
Self {
subject: subject.into(),
issuer: issuer.into(),
serial_number: serial_number.into(),
not_before: String::new(),
not_after: String::new(),
is_ca: false,
key_usage: Vec::new(),
extended_key_usage: Vec::new(),
subject_alt_names: Vec::new(),
fingerprint_sha256: String::new(),
}
}
#[must_use]
pub fn is_self_signed(&self) -> bool {
self.subject == self.issuer
}
#[must_use]
pub fn with_validity(
mut self,
not_before: impl Into<String>,
not_after: impl Into<String>,
) -> Self {
self.not_before = not_before.into();
self.not_after = not_after.into();
self
}
#[must_use]
pub fn with_ca(mut self, is_ca: bool) -> Self {
self.is_ca = is_ca;
self
}
#[must_use]
pub fn with_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
self.fingerprint_sha256 = fingerprint.into();
self
}
#[must_use]
pub fn with_key_usage(mut self, usage: KeyUsage) -> Self {
self.key_usage.push(usage);
self
}
#[must_use]
pub fn with_extended_key_usage(mut self, oid: impl Into<String>) -> Self {
self.extended_key_usage.push(oid.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum KeyUsage {
DigitalSignature,
NonRepudiation,
KeyEncipherment,
DataEncipherment,
KeyAgreement,
KeyCertSign,
#[strum(serialize = "cRLSign")]
CrlSign,
EncipherOnly,
DecipherOnly,
}
pub mod eku {
pub const SERVER_AUTH: &str = "1.3.6.1.5.5.7.3.1";
pub const CLIENT_AUTH: &str = "1.3.6.1.5.5.7.3.2";
pub const CODE_SIGNING: &str = "1.3.6.1.5.5.7.3.3";
pub const EMAIL_PROTECTION: &str = "1.3.6.1.5.5.7.3.4";
pub const TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
pub const DOCUMENT_SIGNING: &str = "1.3.6.1.5.5.7.3.36";
}
#[derive(Debug, Clone)]
pub struct CertificateChain {
pub certificates: Vec<CertificateInfo>,
}
impl CertificateChain {
#[must_use]
pub fn new(certificates: Vec<CertificateInfo>) -> Self {
Self { certificates }
}
#[must_use]
pub fn empty() -> Self {
Self {
certificates: Vec::new(),
}
}
#[must_use]
pub fn leaf(&self) -> Option<&CertificateInfo> {
self.certificates.first()
}
#[must_use]
pub fn root(&self) -> Option<&CertificateInfo> {
self.certificates.last()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.certificates.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.certificates.len()
}
pub fn push(&mut self, cert: CertificateInfo) {
self.certificates.push(cert);
}
#[must_use]
pub fn validate_structure(&self) -> CertificateValidation {
if self.certificates.is_empty() {
return CertificateValidation::failure("Certificate chain is empty");
}
let mut result = CertificateValidation::success(Vec::new());
for cert in &self.certificates {
result.trust_path.push(cert.subject.clone());
}
for i in 0..self.certificates.len() - 1 {
let cert = &self.certificates[i];
let issuer = &self.certificates[i + 1];
if cert.issuer != issuer.subject {
result.add_error(format!(
"Chain broken: '{}' issuer '{}' does not match next certificate subject '{}'",
cert.subject, cert.issuer, issuer.subject
));
}
if !issuer.is_ca {
result.add_warning(format!("Issuer '{}' is not marked as a CA", issuer.subject));
}
}
if let Some(root) = self.root() {
if !root.is_self_signed() {
result.add_warning(format!(
"Root certificate '{}' is not self-signed (issuer: '{}')",
root.subject, root.issuer
));
}
}
result
}
#[must_use]
pub fn validate_trust(&self, trusted_roots: &[CertificateInfo]) -> CertificateValidation {
let mut result = self.validate_structure();
if !result.valid {
return result;
}
if let Some(root) = self.root() {
let is_trusted = trusted_roots.iter().any(|trusted| {
trusted.fingerprint_sha256 == root.fingerprint_sha256
&& !trusted.fingerprint_sha256.is_empty()
});
if !is_trusted {
result.add_error(format!(
"Root certificate '{}' is not in the trusted roots",
root.subject
));
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_chain() -> CertificateChain {
let leaf = CertificateInfo::new("CN=leaf.example.com", "CN=Intermediate CA", "1234")
.with_fingerprint("aabbccdd")
.with_key_usage(KeyUsage::DigitalSignature);
let intermediate = CertificateInfo::new("CN=Intermediate CA", "CN=Root CA", "5678")
.with_ca(true)
.with_fingerprint("eeff0011")
.with_key_usage(KeyUsage::KeyCertSign);
let root = CertificateInfo::new("CN=Root CA", "CN=Root CA", "9999")
.with_ca(true)
.with_fingerprint("11223344")
.with_key_usage(KeyUsage::KeyCertSign);
CertificateChain::new(vec![leaf, intermediate, root])
}
#[test]
fn test_certificate_validation_success() {
let result = CertificateValidation::success(vec!["leaf".to_string(), "root".to_string()]);
assert!(result.is_valid());
assert!(!result.has_warnings());
assert_eq!(result.trust_path.len(), 2);
}
#[test]
fn test_certificate_validation_failure() {
let result = CertificateValidation::failure("Invalid certificate");
assert!(!result.is_valid());
assert_eq!(result.errors.len(), 1);
}
#[test]
fn test_certificate_info_self_signed() {
let self_signed = CertificateInfo::new("CN=Root CA", "CN=Root CA", "1234");
assert!(self_signed.is_self_signed());
let not_self_signed = CertificateInfo::new("CN=Leaf", "CN=Root CA", "5678");
assert!(!not_self_signed.is_self_signed());
}
#[test]
fn test_certificate_chain_structure() {
let chain = create_test_chain();
assert_eq!(chain.len(), 3);
assert!(!chain.is_empty());
assert_eq!(chain.leaf().unwrap().subject, "CN=leaf.example.com");
assert_eq!(chain.root().unwrap().subject, "CN=Root CA");
}
#[test]
fn test_validate_structure_valid() {
let chain = create_test_chain();
let result = chain.validate_structure();
assert!(result.is_valid());
assert_eq!(result.trust_path.len(), 3);
}
#[test]
fn test_validate_structure_empty_chain() {
let chain = CertificateChain::empty();
let result = chain.validate_structure();
assert!(!result.is_valid());
assert!(result.errors[0].contains("empty"));
}
#[test]
fn test_validate_structure_broken_chain() {
let leaf = CertificateInfo::new("CN=leaf.example.com", "CN=Wrong Issuer", "1234");
let root = CertificateInfo::new("CN=Root CA", "CN=Root CA", "9999").with_ca(true);
let chain = CertificateChain::new(vec![leaf, root]);
let result = chain.validate_structure();
assert!(!result.is_valid());
assert!(result.errors[0].contains("Chain broken"));
}
#[test]
fn test_validate_trust_trusted_root() {
let chain = create_test_chain();
let trusted_root =
CertificateInfo::new("CN=Root CA", "CN=Root CA", "9999").with_fingerprint("11223344");
let result = chain.validate_trust(&[trusted_root]);
assert!(result.is_valid());
}
#[test]
fn test_validate_trust_untrusted_root() {
let chain = create_test_chain();
let other_root = CertificateInfo::new("CN=Other Root", "CN=Other Root", "0000")
.with_fingerprint("99887766");
let result = chain.validate_trust(&[other_root]);
assert!(!result.is_valid());
assert!(result.errors[0].contains("not in the trusted roots"));
}
#[test]
fn test_key_usage_display() {
assert_eq!(KeyUsage::DigitalSignature.to_string(), "digitalSignature");
assert_eq!(KeyUsage::KeyCertSign.to_string(), "keyCertSign");
}
#[test]
fn test_certificate_info_serialization() {
let cert = CertificateInfo::new("CN=Test", "CN=Issuer", "1234")
.with_validity("2024-01-01T00:00:00Z", "2025-01-01T00:00:00Z")
.with_ca(true)
.with_fingerprint("abcd1234")
.with_key_usage(KeyUsage::DigitalSignature)
.with_extended_key_usage(eku::DOCUMENT_SIGNING);
let json = serde_json::to_string_pretty(&cert).unwrap();
assert!(json.contains("\"subject\": \"CN=Test\""));
assert!(json.contains("\"isCa\": true"));
let deserialized: CertificateInfo = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.subject, "CN=Test");
assert!(deserialized.is_ca);
}
#[test]
fn test_eku_constants() {
assert_eq!(eku::SERVER_AUTH, "1.3.6.1.5.5.7.3.1");
assert_eq!(eku::DOCUMENT_SIGNING, "1.3.6.1.5.5.7.3.36");
}
}