use super::{KeyUsage, NotBeforeOffset};
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ChainSpec {
pub leaf_cn: String,
pub leaf_sans: Vec<String>,
pub root_cn: String,
pub intermediate_cn: String,
pub rsa_bits: usize,
pub root_validity_days: u32,
pub intermediate_validity_days: u32,
pub leaf_validity_days: u32,
pub leaf_not_before: Option<NotBeforeOffset>,
pub intermediate_not_before: Option<NotBeforeOffset>,
pub intermediate_is_ca: Option<bool>,
pub intermediate_key_usage: Option<KeyUsage>,
}
impl ChainSpec {
pub fn new(leaf_cn: impl Into<String>) -> Self {
let leaf_cn = leaf_cn.into();
let root_cn = format!("{} Root CA", leaf_cn);
let intermediate_cn = format!("{} Intermediate CA", leaf_cn);
let leaf_sans = vec![leaf_cn.clone()];
Self {
leaf_cn,
leaf_sans,
root_cn,
intermediate_cn,
rsa_bits: 2048,
root_validity_days: 3650,
intermediate_validity_days: 1825,
leaf_validity_days: 3650,
leaf_not_before: None,
intermediate_not_before: None,
intermediate_is_ca: None,
intermediate_key_usage: None,
}
}
pub fn with_sans(mut self, sans: Vec<String>) -> Self {
self.leaf_sans = sans;
self
}
pub fn with_root_cn(mut self, cn: impl Into<String>) -> Self {
self.root_cn = cn.into();
self
}
pub fn with_intermediate_cn(mut self, cn: impl Into<String>) -> Self {
self.intermediate_cn = cn.into();
self
}
pub fn with_rsa_bits(mut self, bits: usize) -> Self {
self.rsa_bits = bits;
self
}
pub fn with_root_validity_days(mut self, days: u32) -> Self {
self.root_validity_days = days;
self
}
pub fn with_intermediate_validity_days(mut self, days: u32) -> Self {
self.intermediate_validity_days = days;
self
}
pub fn with_leaf_validity_days(mut self, days: u32) -> Self {
self.leaf_validity_days = days;
self
}
pub fn with_leaf_not_before(mut self, offset: NotBeforeOffset) -> Self {
self.leaf_not_before = Some(offset);
self
}
pub fn with_intermediate_not_before(mut self, offset: NotBeforeOffset) -> Self {
self.intermediate_not_before = Some(offset);
self
}
pub fn with_intermediate_is_ca(mut self, is_ca: bool) -> Self {
self.intermediate_is_ca = Some(is_ca);
self
}
pub fn with_intermediate_key_usage(mut self, key_usage: KeyUsage) -> Self {
self.intermediate_key_usage = Some(key_usage);
self
}
pub fn stable_bytes(&self) -> Vec<u8> {
if self.uses_v2_compat_encoding() {
return self.stable_bytes_v2_compat();
}
self.stable_bytes_v3()
}
fn uses_v2_compat_encoding(&self) -> bool {
self.intermediate_is_ca.is_none()
&& self.intermediate_key_usage.is_none()
&& supports_v2_not_before(self.leaf_not_before)
&& supports_v2_not_before(self.intermediate_not_before)
}
fn stable_bytes_v2_compat(&self) -> Vec<u8> {
let mut out = Vec::new();
out.push(2);
encode_common_fields(self, &mut out);
encode_optional_days_ago_i64(&mut out, self.leaf_not_before);
encode_optional_days_ago_i64(&mut out, self.intermediate_not_before);
out
}
fn stable_bytes_v3(&self) -> Vec<u8> {
let mut out = Vec::new();
out.push(3);
encode_common_fields(self, &mut out);
encode_optional_not_before(&mut out, self.leaf_not_before);
encode_optional_not_before(&mut out, self.intermediate_not_before);
match self.intermediate_is_ca {
None => out.push(0),
Some(false) => out.push(1),
Some(true) => out.push(2),
}
match self.intermediate_key_usage {
None => out.push(0),
Some(key_usage) => {
out.push(1);
out.extend_from_slice(&key_usage.stable_bytes());
}
}
out
}
}
fn encode_common_fields(spec: &ChainSpec, out: &mut Vec<u8>) {
let leaf_cn_bytes = spec.leaf_cn.as_bytes();
out.extend_from_slice(&(leaf_cn_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(leaf_cn_bytes);
let mut sorted_sans = spec.leaf_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);
}
let root_cn_bytes = spec.root_cn.as_bytes();
out.extend_from_slice(&(root_cn_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(root_cn_bytes);
let int_cn_bytes = spec.intermediate_cn.as_bytes();
out.extend_from_slice(&(int_cn_bytes.len() as u32).to_be_bytes());
out.extend_from_slice(int_cn_bytes);
out.extend_from_slice(&(spec.rsa_bits as u32).to_be_bytes());
out.extend_from_slice(&spec.root_validity_days.to_be_bytes());
out.extend_from_slice(&spec.intermediate_validity_days.to_be_bytes());
out.extend_from_slice(&spec.leaf_validity_days.to_be_bytes());
}
fn supports_v2_not_before(offset: Option<NotBeforeOffset>) -> bool {
matches!(offset, None | Some(NotBeforeOffset::DaysAgo(_)))
}
fn encode_optional_days_ago_i64(out: &mut Vec<u8>, offset: Option<NotBeforeOffset>) {
match offset {
None => out.push(0),
Some(NotBeforeOffset::DaysAgo(days)) => {
out.push(1);
out.extend_from_slice(&i64::from(days).to_be_bytes());
}
Some(NotBeforeOffset::DaysFromNow(_)) => {
unreachable!("DaysFromNow requires v3 encoding")
}
}
}
fn encode_optional_not_before(out: &mut Vec<u8>, offset: Option<NotBeforeOffset>) {
match offset {
None => out.push(0),
Some(NotBeforeOffset::DaysAgo(days)) => {
out.push(1);
out.extend_from_slice(&days.to_be_bytes());
}
Some(NotBeforeOffset::DaysFromNow(days)) => {
out.push(2);
out.extend_from_slice(&days.to_be_bytes());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_defaults() {
let spec = ChainSpec::new("test.example.com");
assert_eq!(spec.leaf_cn, "test.example.com");
assert_eq!(spec.leaf_sans, vec!["test.example.com"]);
assert_eq!(spec.root_cn, "test.example.com Root CA");
assert_eq!(spec.intermediate_cn, "test.example.com Intermediate CA");
assert_eq!(spec.rsa_bits, 2048);
assert_eq!(spec.root_validity_days, 3650);
assert_eq!(spec.intermediate_validity_days, 1825);
assert_eq!(spec.leaf_validity_days, 3650);
assert_eq!(spec.leaf_not_before, None);
assert_eq!(spec.intermediate_not_before, None);
assert_eq!(spec.intermediate_is_ca, None);
assert_eq!(spec.intermediate_key_usage, None);
}
#[test]
fn test_builders() {
let spec = ChainSpec::new("example.com")
.with_sans(vec![
"example.com".to_string(),
"www.example.com".to_string(),
])
.with_root_cn("My Root CA")
.with_intermediate_cn("My Int CA")
.with_rsa_bits(4096)
.with_root_validity_days(7300)
.with_intermediate_validity_days(3650)
.with_leaf_validity_days(90)
.with_leaf_not_before(NotBeforeOffset::DaysFromNow(7))
.with_intermediate_not_before(NotBeforeOffset::DaysAgo(30))
.with_intermediate_is_ca(false)
.with_intermediate_key_usage(KeyUsage::leaf());
assert_eq!(spec.leaf_sans.len(), 2);
assert_eq!(spec.root_cn, "My Root CA");
assert_eq!(spec.intermediate_cn, "My Int CA");
assert_eq!(spec.rsa_bits, 4096);
assert_eq!(spec.root_validity_days, 7300);
assert_eq!(spec.intermediate_validity_days, 3650);
assert_eq!(spec.leaf_validity_days, 90);
assert_eq!(spec.leaf_not_before, Some(NotBeforeOffset::DaysFromNow(7)));
assert_eq!(
spec.intermediate_not_before,
Some(NotBeforeOffset::DaysAgo(30))
);
assert_eq!(spec.intermediate_is_ca, Some(false));
assert_eq!(spec.intermediate_key_usage, Some(KeyUsage::leaf()));
}
#[test]
fn test_stable_bytes_determinism() {
let spec1 = ChainSpec::new("test.example.com");
let spec2 = ChainSpec::new("test.example.com");
assert_eq!(spec1.stable_bytes(), spec2.stable_bytes());
let spec3 = ChainSpec::new("other.example.com");
assert_ne!(spec1.stable_bytes(), spec3.stable_bytes());
}
#[test]
fn test_stable_bytes_san_order_independent() {
let spec1 = ChainSpec::new("test.example.com").with_sans(vec![
"a.example.com".to_string(),
"b.example.com".to_string(),
]);
let spec2 = ChainSpec::new("test.example.com").with_sans(vec![
"b.example.com".to_string(),
"a.example.com".to_string(),
]);
assert_eq!(spec1.stable_bytes(), spec2.stable_bytes());
}
#[test]
fn test_stable_bytes_field_sensitivity() {
let base = ChainSpec::new("test.example.com");
let base_bytes = base.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_root_validity_days(999);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"root_validity_days must affect stable_bytes"
);
let changed = base.clone().with_intermediate_validity_days(999);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"intermediate_validity_days must affect stable_bytes"
);
let changed = base.clone().with_leaf_validity_days(999);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"leaf_validity_days must affect stable_bytes"
);
let changed = base.clone().with_root_cn("Other Root CA");
assert_ne!(
changed.stable_bytes(),
base_bytes,
"root_cn must affect stable_bytes"
);
let changed = base.clone().with_intermediate_cn("Other Int CA");
assert_ne!(
changed.stable_bytes(),
base_bytes,
"intermediate_cn must affect stable_bytes"
);
let changed = base
.clone()
.with_sans(vec!["extra.example.com".to_string()]);
assert_ne!(
changed.stable_bytes(),
base_bytes,
"leaf_sans must affect stable_bytes"
);
}
#[test]
fn test_stable_bytes_optional_offset_sensitivity() {
let base = ChainSpec::new("test.example.com");
let base_bytes = base.stable_bytes();
let mut with_leaf_offset = base.clone();
with_leaf_offset.leaf_not_before = Some(NotBeforeOffset::DaysAgo(100));
assert_ne!(
with_leaf_offset.stable_bytes(),
base_bytes,
"leaf_not_before None vs Some must differ"
);
let mut with_leaf_offset2 = base.clone();
with_leaf_offset2.leaf_not_before = Some(NotBeforeOffset::DaysFromNow(100));
assert_ne!(
with_leaf_offset.stable_bytes(),
with_leaf_offset2.stable_bytes(),
"leaf_not_before days-ago vs days-from-now must differ"
);
let mut with_int_offset = base.clone();
with_int_offset.intermediate_not_before = Some(NotBeforeOffset::DaysAgo(100));
assert_ne!(
with_int_offset.stable_bytes(),
base_bytes,
"intermediate_not_before None vs Some must differ"
);
let mut with_int_offset2 = base.clone();
with_int_offset2.intermediate_not_before = Some(NotBeforeOffset::DaysAgo(200));
assert_ne!(
with_int_offset.stable_bytes(),
with_int_offset2.stable_bytes(),
"intermediate_not_before Some(100) vs Some(200) must differ"
);
let with_int_is_ca = base.clone().with_intermediate_is_ca(false);
assert_ne!(
with_int_is_ca.stable_bytes(),
base_bytes,
"intermediate_is_ca must affect stable_bytes"
);
let with_int_ku = base.clone().with_intermediate_key_usage(KeyUsage::leaf());
assert_ne!(
with_int_ku.stable_bytes(),
base_bytes,
"intermediate_key_usage must affect stable_bytes"
);
}
#[test]
fn test_stable_bytes_v3_encodes_not_before_offsets() {
let base = ChainSpec::new("test.example.com").with_intermediate_is_ca(false);
let base_bytes = base.stable_bytes();
let leaf_future = base
.clone()
.with_leaf_not_before(NotBeforeOffset::DaysFromNow(7));
assert_ne!(
leaf_future.stable_bytes(),
base_bytes,
"v3 leaf not_before offset must affect stable_bytes"
);
let leaf_past = base
.clone()
.with_leaf_not_before(NotBeforeOffset::DaysAgo(7));
assert_ne!(
leaf_future.stable_bytes(),
leaf_past.stable_bytes(),
"v3 leaf days-from-now and days-ago offsets must differ"
);
let intermediate_future =
base.with_intermediate_not_before(NotBeforeOffset::DaysFromNow(7));
assert_ne!(
intermediate_future.stable_bytes(),
base_bytes,
"v3 intermediate not_before offset must affect stable_bytes"
);
}
#[test]
fn test_stable_bytes_default_uses_v2_compat_prefix() {
let spec = ChainSpec::new("compat.example.com");
assert_eq!(spec.stable_bytes()[0], 2);
}
#[test]
fn test_stable_bytes_days_ago_only_stays_on_v2_compat() {
let spec = ChainSpec::new("compat.example.com")
.with_leaf_not_before(NotBeforeOffset::DaysAgo(7))
.with_intermediate_not_before(NotBeforeOffset::DaysAgo(30));
assert_eq!(spec.stable_bytes()[0], 2);
}
#[test]
fn test_stable_bytes_days_from_now_or_intermediate_overrides_use_v3() {
let future = ChainSpec::new("future.example.com")
.with_leaf_not_before(NotBeforeOffset::DaysFromNow(7));
assert_eq!(future.stable_bytes()[0], 3);
let not_ca = ChainSpec::new("path.example.com").with_intermediate_is_ca(false);
assert_eq!(not_ca.stable_bytes()[0], 3);
let wrong_ku =
ChainSpec::new("path.example.com").with_intermediate_key_usage(KeyUsage::leaf());
assert_eq!(wrong_ku.stable_bytes()[0], 3);
}
}