Skip to main content

uselesskey_core_x509_derive/
lib.rs

1//! Deterministic X.509 derivation helpers.
2//!
3//! This crate centralizes deterministic logic shared by X.509 fixture producers:
4//! - deterministic base-time derivation from stable identity inputs
5//! - deterministic positive serial number generation
6//! - length-prefixed hashing to avoid input-boundary collisions
7//!
8//! # Examples
9//!
10//! Derive a deterministic base time from identity parts:
11//!
12//! ```
13//! use uselesskey_core_x509_derive::{
14//!     deterministic_base_time_from_parts, BASE_TIME_EPOCH_UNIX, BASE_TIME_WINDOW_DAYS,
15//! };
16//! use time::OffsetDateTime;
17//!
18//! let t = deterministic_base_time_from_parts(&[b"my-label", b"leaf"]);
19//!
20//! let epoch = OffsetDateTime::from_unix_timestamp(BASE_TIME_EPOCH_UNIX).unwrap();
21//! let max = epoch + time::Duration::days(i64::from(BASE_TIME_WINDOW_DAYS));
22//! assert!(t >= epoch && t < max);
23//! ```
24//!
25//! Generate a deterministic serial number from a seed:
26//!
27//! ```
28//! use uselesskey_core_x509_derive::{deterministic_serial_number, SERIAL_NUMBER_BYTES};
29//! use uselesskey_core_seed::Seed;
30//!
31//! let serial = deterministic_serial_number(Seed::new([42u8; 32]));
32//! let bytes = serial.to_bytes();
33//! assert_eq!(bytes.len(), SERIAL_NUMBER_BYTES);
34//! assert_eq!(bytes[0] & 0x80, 0, "high bit must be cleared");
35//! ```
36
37#![forbid(unsafe_code)]
38#![warn(missing_docs)]
39
40use rand_chacha10::ChaCha20Rng;
41use rand_core10::{Rng, SeedableRng};
42use rcgen::SerialNumber;
43use time::OffsetDateTime;
44use uselesskey_core_hash::Hasher;
45pub use uselesskey_core_hash::write_len_prefixed;
46use uselesskey_core_seed::Seed;
47
48/// 2025-01-01T00:00:00Z used as the deterministic X.509 epoch.
49pub const BASE_TIME_EPOCH_UNIX: i64 = 1_735_689_600;
50
51/// Number of days in the deterministic base-time window.
52pub const BASE_TIME_WINDOW_DAYS: u32 = 365;
53
54/// Fixed serial-number byte length for deterministic certificate/CRL serials.
55pub const SERIAL_NUMBER_BYTES: usize = 16;
56
57/// Compute deterministic base time from length-prefixed identity parts.
58///
59/// Every part is hashed with a 32-bit length prefix to avoid boundary ambiguity.
60pub fn deterministic_base_time_from_parts(parts: &[&[u8]]) -> OffsetDateTime {
61    let mut hasher = Hasher::new();
62    for part in parts {
63        write_len_prefixed(&mut hasher, part);
64    }
65    deterministic_base_time(hasher)
66}
67
68/// Deterministic base time from a pre-configured BLAKE3 hasher.
69///
70/// Returns a time spread across one year from 2025-01-01 to 2026-01-01.
71pub fn deterministic_base_time(hasher: Hasher) -> OffsetDateTime {
72    let epoch = OffsetDateTime::from_unix_timestamp(BASE_TIME_EPOCH_UNIX)
73        .expect("failed to construct deterministic X.509 epoch");
74
75    let hash = hasher.finalize();
76    let bytes = hash.as_bytes();
77    let day_offset =
78        u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) % BASE_TIME_WINDOW_DAYS;
79    epoch + time::Duration::days(i64::from(day_offset))
80}
81
82/// Deterministic serial number derived from seed material.
83///
84/// Produces a 16-byte positive serial number (high bit cleared).
85pub fn deterministic_serial_number(seed: Seed) -> SerialNumber {
86    let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
87    deterministic_serial_number_with_rng(&mut rng)
88}
89
90fn deterministic_serial_number_with_rng(rng: &mut impl Rng) -> SerialNumber {
91    let mut bytes = [0u8; SERIAL_NUMBER_BYTES];
92    rng.fill_bytes(&mut bytes);
93    bytes[0] &= 0x7F;
94    SerialNumber::from_slice(&bytes)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use uselesskey_core_seed::Seed;
101
102    #[test]
103    fn deterministic_base_time_is_within_one_year() {
104        let epoch = OffsetDateTime::from_unix_timestamp(BASE_TIME_EPOCH_UNIX).unwrap();
105        let max = epoch + time::Duration::days(i64::from(BASE_TIME_WINDOW_DAYS - 1));
106
107        let base = deterministic_base_time(Hasher::new());
108        assert!(base >= epoch, "base time should be after epoch");
109        assert!(base <= max, "base time should be within one year");
110    }
111
112    #[test]
113    fn deterministic_base_time_is_deterministic() {
114        let a = deterministic_base_time_from_parts(&[b"label", b"leaf", b"root", b"2048"]);
115        let b = deterministic_base_time_from_parts(&[b"label", b"leaf", b"root", b"2048"]);
116        assert_eq!(a, b);
117    }
118
119    #[test]
120    fn deterministic_base_time_from_parts_is_boundary_safe() {
121        let a = deterministic_base_time_from_parts(&[b"ab", b"c"]);
122        let b = deterministic_base_time_from_parts(&[b"a", b"bc"]);
123        assert_ne!(a, b);
124    }
125
126    #[test]
127    fn deterministic_serial_number_is_positive_and_fixed_size() {
128        let serial = deterministic_serial_number(Seed::new([7u8; 32]));
129        let bytes = serial.to_bytes();
130
131        assert_eq!(bytes.len(), SERIAL_NUMBER_BYTES);
132        assert_eq!(bytes[0] & 0x80, 0, "high bit should be cleared");
133    }
134
135    #[test]
136    fn deterministic_serial_number_is_seed_stable() {
137        assert_eq!(
138            deterministic_serial_number(Seed::new([42u8; 32])).to_bytes(),
139            deterministic_serial_number(Seed::new([42u8; 32])).to_bytes()
140        );
141    }
142
143    #[test]
144    fn deterministic_serial_number_varies_by_seed() {
145        assert_ne!(
146            deterministic_serial_number(Seed::new([1u8; 32])).to_bytes(),
147            deterministic_serial_number(Seed::new([2u8; 32])).to_bytes()
148        );
149    }
150}