use std::time::Duration;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct KeyUsage {
pub key_cert_sign: bool,
pub crl_sign: bool,
pub digital_signature: bool,
pub key_encipherment: bool,
}
impl Default for KeyUsage {
fn default() -> Self {
Self::leaf()
}
}
impl KeyUsage {
pub fn leaf() -> Self {
Self {
key_cert_sign: false,
crl_sign: false,
digital_signature: true,
key_encipherment: true,
}
}
pub fn ca() -> Self {
Self {
key_cert_sign: true,
crl_sign: true,
digital_signature: true,
key_encipherment: false,
}
}
pub fn stable_bytes(&self) -> [u8; 4] {
let mut out = [0u8; 4];
out[0] = self.key_cert_sign as u8;
out[1] = self.crl_sign as u8;
out[2] = self.digital_signature as u8;
out[3] = self.key_encipherment as u8;
out
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct X509Spec {
pub subject_cn: String,
pub issuer_cn: String,
pub not_before_offset: NotBeforeOffset,
pub validity_days: u32,
pub key_usage: KeyUsage,
pub is_ca: bool,
pub rsa_bits: usize,
pub sans: Vec<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum NotBeforeOffset {
DaysAgo(u32),
DaysFromNow(u32),
}
impl Default for NotBeforeOffset {
fn default() -> Self {
NotBeforeOffset::DaysAgo(1)
}
}
impl Default for X509Spec {
fn default() -> Self {
Self {
subject_cn: "Test Certificate".to_string(),
issuer_cn: "Test Certificate".to_string(),
not_before_offset: NotBeforeOffset::default(),
validity_days: 3650,
key_usage: KeyUsage::leaf(),
is_ca: false,
rsa_bits: 2048,
sans: Vec::new(),
}
}
}
impl X509Spec {
pub fn self_signed(cn: impl Into<String>) -> Self {
let cn = cn.into();
Self {
subject_cn: cn.clone(),
issuer_cn: cn,
..Default::default()
}
}
pub fn self_signed_ca(cn: impl Into<String>) -> Self {
let cn = cn.into();
Self {
subject_cn: cn.clone(),
issuer_cn: cn,
key_usage: KeyUsage::ca(),
is_ca: true,
..Default::default()
}
}
pub fn with_validity_days(mut self, days: u32) -> Self {
self.validity_days = days;
self
}
pub fn with_not_before(mut self, offset: NotBeforeOffset) -> Self {
self.not_before_offset = offset;
self
}
pub fn with_rsa_bits(mut self, bits: usize) -> Self {
self.rsa_bits = bits;
self
}
pub fn with_key_usage(mut self, key_usage: KeyUsage) -> Self {
self.key_usage = key_usage;
self
}
pub fn with_is_ca(mut self, is_ca: bool) -> Self {
self.is_ca = is_ca;
self
}
pub fn with_sans(mut self, sans: Vec<String>) -> Self {
self.sans = sans;
self
}
pub fn stable_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.push(4);
let subject_bytes = self.subject_cn.as_bytes();
out.extend_from_slice(&(subject_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(subject_bytes);
let issuer_bytes = self.issuer_cn.as_bytes();
out.extend_from_slice(&(issuer_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(issuer_bytes);
match self.not_before_offset {
NotBeforeOffset::DaysAgo(d) => {
out.push(0);
out.extend_from_slice(&d.to_be_bytes());
}
NotBeforeOffset::DaysFromNow(d) => {
out.push(1);
out.extend_from_slice(&d.to_be_bytes());
}
}
out.extend_from_slice(&self.validity_days.to_be_bytes());
out.extend_from_slice(&self.key_usage.stable_bytes());
out.push(self.is_ca as u8);
out.extend_from_slice(&(self.rsa_bits as u32).to_be_bytes());
let mut sorted_sans = self.sans.clone();
sorted_sans.sort();
sorted_sans.dedup();
out.extend_from_slice(&(sorted_sans.len() as u32).to_be_bytes());
for san in &sorted_sans {
let san_bytes = san.as_bytes();
out.extend_from_slice(&(san_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(san_bytes);
}
out
}
pub fn not_before_duration(&self) -> Duration {
match self.not_before_offset {
NotBeforeOffset::DaysAgo(d) => Duration::from_secs(d as u64 * 24 * 60 * 60),
NotBeforeOffset::DaysFromNow(_) => Duration::ZERO,
}
}
pub fn not_after_duration(&self) -> Duration {
let base = match self.not_before_offset {
NotBeforeOffset::DaysAgo(_) => Duration::ZERO,
NotBeforeOffset::DaysFromNow(d) => Duration::from_secs(d as u64 * 24 * 60 * 60),
};
base + Duration::from_secs(self.validity_days as u64 * 24 * 60 * 60)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_spec() {
let spec = X509Spec::default();
assert_eq!(spec.subject_cn, "Test Certificate");
assert_eq!(spec.issuer_cn, "Test Certificate");
assert_eq!(spec.not_before_offset, NotBeforeOffset::DaysAgo(1));
assert_eq!(spec.validity_days, 3650);
assert_eq!(spec.key_usage, KeyUsage::leaf());
assert!(!spec.is_ca);
assert_eq!(spec.rsa_bits, 2048);
assert!(spec.sans.is_empty());
}
#[test]
fn test_key_usage_default_is_leaf() {
assert_eq!(KeyUsage::default(), KeyUsage::leaf());
}
#[test]
fn test_self_signed_spec() {
let spec = X509Spec::self_signed("example.com");
assert_eq!(spec.subject_cn, "example.com");
assert_eq!(spec.issuer_cn, "example.com");
assert!(!spec.is_ca);
}
#[test]
fn test_ca_spec() {
let spec = X509Spec::self_signed_ca("My CA");
assert!(spec.is_ca);
assert!(spec.key_usage.key_cert_sign);
assert_eq!(spec.subject_cn, "My CA");
assert_eq!(spec.issuer_cn, "My CA");
}
#[test]
fn test_builder_methods_apply() {
let key_usage = KeyUsage::ca();
let sans: Vec<String> = vec!["a.example.com".into(), "b.example.com".into()];
let spec = X509Spec::self_signed("builder.example.com")
.with_validity_days(90)
.with_not_before(NotBeforeOffset::DaysFromNow(7))
.with_rsa_bits(4096)
.with_key_usage(key_usage)
.with_is_ca(true)
.with_sans(sans.clone());
assert_eq!(spec.validity_days, 90);
assert_eq!(spec.not_before_offset, NotBeforeOffset::DaysFromNow(7));
assert_eq!(spec.rsa_bits, 4096);
assert!(spec.is_ca);
assert_eq!(spec.key_usage, key_usage);
assert_eq!(spec.sans, sans);
}
#[test]
fn test_not_before_duration_variants() {
let days = 3u32;
let secs = days as u64 * 24 * 60 * 60;
let spec_ago = X509Spec::self_signed("ago").with_not_before(NotBeforeOffset::DaysAgo(days));
assert_eq!(spec_ago.not_before_duration(), Duration::from_secs(secs));
let spec_future =
X509Spec::self_signed("future").with_not_before(NotBeforeOffset::DaysFromNow(days));
assert_eq!(spec_future.not_before_duration(), Duration::ZERO);
}
#[test]
fn test_not_after_duration_variants() {
let days = 2u32;
let secs = days as u64 * 24 * 60 * 60;
let spec_ago = X509Spec::self_signed("ago").with_validity_days(days);
assert_eq!(spec_ago.not_after_duration(), Duration::from_secs(secs));
let spec_future = X509Spec::self_signed("future")
.with_not_before(NotBeforeOffset::DaysFromNow(days))
.with_validity_days(days);
assert_eq!(
spec_future.not_after_duration(),
Duration::from_secs(secs * 2)
);
}
#[test]
fn test_stable_bytes_determinism() {
let spec1 = X509Spec::self_signed("test");
let spec2 = X509Spec::self_signed("test");
assert_eq!(spec1.stable_bytes(), spec2.stable_bytes());
let spec3 = X509Spec::self_signed("different");
assert_ne!(spec1.stable_bytes(), spec3.stable_bytes());
}
#[test]
fn test_stable_bytes_deduplicates_sans() {
let with_dupes = X509Spec::self_signed("test").with_sans(vec![
"a.com".into(),
"a.com".into(),
"b.com".into(),
]);
let without_dupes =
X509Spec::self_signed("test").with_sans(vec!["a.com".into(), "b.com".into()]);
assert_eq!(with_dupes.stable_bytes(), without_dupes.stable_bytes());
}
#[test]
fn test_stable_bytes_field_sensitivity() {
let base = X509Spec::self_signed("test");
let base_bytes = base.stable_bytes();
let changed = base.clone().with_validity_days(999);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"validity_days must affect stable_bytes"
);
let changed = base.clone().with_is_ca(true);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"is_ca must affect stable_bytes"
);
let changed = base.clone().with_rsa_bits(4096);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"rsa_bits must affect stable_bytes"
);
let changed = base
.clone()
.with_not_before(NotBeforeOffset::DaysFromNow(7));
assert_ne!(
changed.stable_bytes(),
base_bytes,
"not_before_offset must affect stable_bytes"
);
let changed = base.clone().with_key_usage(KeyUsage::ca());
assert_ne!(
changed.stable_bytes(),
base_bytes,
"key_usage must affect stable_bytes"
);
let mut changed = base.clone();
changed.issuer_cn = "Other Issuer".to_string();
assert_ne!(
changed.stable_bytes(),
base_bytes,
"issuer_cn must affect stable_bytes"
);
let changed = base.clone().with_sans(vec!["san.example.com".into()]);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"sans must affect stable_bytes"
);
}
#[test]
fn test_stable_bytes_not_before_offset_variants_differ() {
let days_ago = X509Spec::self_signed("test").with_not_before(NotBeforeOffset::DaysAgo(1));
let days_from_now =
X509Spec::self_signed("test").with_not_before(NotBeforeOffset::DaysFromNow(1));
assert_ne!(
days_ago.stable_bytes(),
days_from_now.stable_bytes(),
"DaysAgo(1) and DaysFromNow(1) must produce different stable_bytes (tag byte 0 vs 1)"
);
}
}