uselesskey_core_x509_spec/
spec.rs1use std::time::Duration;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
7pub struct KeyUsage {
8 pub key_cert_sign: bool,
10 pub crl_sign: bool,
12 pub digital_signature: bool,
14 pub key_encipherment: bool,
16}
17
18impl Default for KeyUsage {
19 fn default() -> Self {
20 Self::leaf()
21 }
22}
23
24impl KeyUsage {
25 pub fn leaf() -> Self {
27 Self {
28 key_cert_sign: false,
29 crl_sign: false,
30 digital_signature: true,
31 key_encipherment: true,
32 }
33 }
34
35 pub fn ca() -> Self {
37 Self {
38 key_cert_sign: true,
39 crl_sign: true,
40 digital_signature: true,
41 key_encipherment: false,
42 }
43 }
44
45 pub fn stable_bytes(&self) -> [u8; 4] {
47 let mut out = [0u8; 4];
48 out[0] = self.key_cert_sign as u8;
49 out[1] = self.crl_sign as u8;
50 out[2] = self.digital_signature as u8;
51 out[3] = self.key_encipherment as u8;
52 out
53 }
54}
55
56#[derive(Clone, Debug, Eq, PartialEq, Hash)]
58pub struct X509Spec {
59 pub subject_cn: String,
61 pub issuer_cn: String,
63 pub not_before_offset: NotBeforeOffset,
66 pub validity_days: u32,
69 pub key_usage: KeyUsage,
71 pub is_ca: bool,
73 pub rsa_bits: usize,
75 pub sans: Vec<String>,
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
81pub enum NotBeforeOffset {
82 DaysAgo(u32),
84 DaysFromNow(u32),
86}
87
88impl Default for NotBeforeOffset {
89 fn default() -> Self {
90 NotBeforeOffset::DaysAgo(1)
91 }
92}
93
94impl Default for X509Spec {
95 fn default() -> Self {
96 Self {
97 subject_cn: "Test Certificate".to_string(),
98 issuer_cn: "Test Certificate".to_string(),
99 not_before_offset: NotBeforeOffset::default(),
100 validity_days: 3650,
101 key_usage: KeyUsage::leaf(),
102 is_ca: false,
103 rsa_bits: 2048,
104 sans: Vec::new(),
105 }
106 }
107}
108
109impl X509Spec {
110 pub fn self_signed(cn: impl Into<String>) -> Self {
112 let cn = cn.into();
113 Self {
114 subject_cn: cn.clone(),
115 issuer_cn: cn,
116 ..Default::default()
117 }
118 }
119
120 pub fn self_signed_ca(cn: impl Into<String>) -> Self {
122 let cn = cn.into();
123 Self {
124 subject_cn: cn.clone(),
125 issuer_cn: cn,
126 key_usage: KeyUsage::ca(),
127 is_ca: true,
128 ..Default::default()
129 }
130 }
131
132 pub fn with_validity_days(mut self, days: u32) -> Self {
134 self.validity_days = days;
135 self
136 }
137
138 pub fn with_not_before(mut self, offset: NotBeforeOffset) -> Self {
140 self.not_before_offset = offset;
141 self
142 }
143
144 pub fn with_rsa_bits(mut self, bits: usize) -> Self {
146 self.rsa_bits = bits;
147 self
148 }
149
150 pub fn with_key_usage(mut self, key_usage: KeyUsage) -> Self {
152 self.key_usage = key_usage;
153 self
154 }
155
156 pub fn with_is_ca(mut self, is_ca: bool) -> Self {
158 self.is_ca = is_ca;
159 self
160 }
161
162 pub fn with_sans(mut self, sans: Vec<String>) -> Self {
164 self.sans = sans;
165 self
166 }
167
168 pub fn stable_bytes(&self) -> Vec<u8> {
172 let mut out = Vec::new();
173
174 out.push(4);
178
179 let subject_bytes = self.subject_cn.as_bytes();
181 out.extend_from_slice(&(subject_bytes.len() as u32).to_be_bytes());
182 out.extend_from_slice(subject_bytes);
183
184 let issuer_bytes = self.issuer_cn.as_bytes();
186 out.extend_from_slice(&(issuer_bytes.len() as u32).to_be_bytes());
187 out.extend_from_slice(issuer_bytes);
188
189 match self.not_before_offset {
191 NotBeforeOffset::DaysAgo(d) => {
192 out.push(0);
193 out.extend_from_slice(&d.to_be_bytes());
194 }
195 NotBeforeOffset::DaysFromNow(d) => {
196 out.push(1);
197 out.extend_from_slice(&d.to_be_bytes());
198 }
199 }
200
201 out.extend_from_slice(&self.validity_days.to_be_bytes());
203
204 out.extend_from_slice(&self.key_usage.stable_bytes());
206
207 out.push(self.is_ca as u8);
209
210 out.extend_from_slice(&(self.rsa_bits as u32).to_be_bytes());
212
213 let mut sorted_sans = self.sans.clone();
215 sorted_sans.sort();
216 sorted_sans.dedup();
217 out.extend_from_slice(&(sorted_sans.len() as u32).to_be_bytes());
218 for san in &sorted_sans {
219 let san_bytes = san.as_bytes();
220 out.extend_from_slice(&(san_bytes.len() as u32).to_be_bytes());
221 out.extend_from_slice(san_bytes);
222 }
223
224 out
225 }
226
227 pub fn not_before_duration(&self) -> Duration {
229 match self.not_before_offset {
230 NotBeforeOffset::DaysAgo(d) => Duration::from_secs(d as u64 * 24 * 60 * 60),
231 NotBeforeOffset::DaysFromNow(_) => Duration::ZERO,
232 }
233 }
234
235 pub fn not_after_duration(&self) -> Duration {
237 let base = match self.not_before_offset {
238 NotBeforeOffset::DaysAgo(_) => Duration::ZERO,
239 NotBeforeOffset::DaysFromNow(d) => Duration::from_secs(d as u64 * 24 * 60 * 60),
240 };
241 base + Duration::from_secs(self.validity_days as u64 * 24 * 60 * 60)
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_default_spec() {
251 let spec = X509Spec::default();
252 assert_eq!(spec.subject_cn, "Test Certificate");
253 assert_eq!(spec.issuer_cn, "Test Certificate");
254 assert_eq!(spec.not_before_offset, NotBeforeOffset::DaysAgo(1));
255 assert_eq!(spec.validity_days, 3650);
256 assert_eq!(spec.key_usage, KeyUsage::leaf());
257 assert!(!spec.is_ca);
258 assert_eq!(spec.rsa_bits, 2048);
259 assert!(spec.sans.is_empty());
260 }
261
262 #[test]
263 fn test_key_usage_default_is_leaf() {
264 assert_eq!(KeyUsage::default(), KeyUsage::leaf());
265 }
266
267 #[test]
268 fn test_self_signed_spec() {
269 let spec = X509Spec::self_signed("example.com");
270 assert_eq!(spec.subject_cn, "example.com");
271 assert_eq!(spec.issuer_cn, "example.com");
272 assert!(!spec.is_ca);
273 }
274
275 #[test]
276 fn test_ca_spec() {
277 let spec = X509Spec::self_signed_ca("My CA");
278 assert!(spec.is_ca);
279 assert!(spec.key_usage.key_cert_sign);
280 assert_eq!(spec.subject_cn, "My CA");
281 assert_eq!(spec.issuer_cn, "My CA");
282 }
283
284 #[test]
285 fn test_builder_methods_apply() {
286 let key_usage = KeyUsage::ca();
287 let sans: Vec<String> = vec!["a.example.com".into(), "b.example.com".into()];
288 let spec = X509Spec::self_signed("builder.example.com")
289 .with_validity_days(90)
290 .with_not_before(NotBeforeOffset::DaysFromNow(7))
291 .with_rsa_bits(4096)
292 .with_key_usage(key_usage)
293 .with_is_ca(true)
294 .with_sans(sans.clone());
295
296 assert_eq!(spec.validity_days, 90);
297 assert_eq!(spec.not_before_offset, NotBeforeOffset::DaysFromNow(7));
298 assert_eq!(spec.rsa_bits, 4096);
299 assert!(spec.is_ca);
300 assert_eq!(spec.key_usage, key_usage);
301 assert_eq!(spec.sans, sans);
302 }
303
304 #[test]
305 fn test_not_before_duration_variants() {
306 let days = 3u32;
307 let secs = days as u64 * 24 * 60 * 60;
308
309 let spec_ago = X509Spec::self_signed("ago").with_not_before(NotBeforeOffset::DaysAgo(days));
310 assert_eq!(spec_ago.not_before_duration(), Duration::from_secs(secs));
311
312 let spec_future =
313 X509Spec::self_signed("future").with_not_before(NotBeforeOffset::DaysFromNow(days));
314 assert_eq!(spec_future.not_before_duration(), Duration::ZERO);
315 }
316
317 #[test]
318 fn test_not_after_duration_variants() {
319 let days = 2u32;
320 let secs = days as u64 * 24 * 60 * 60;
321
322 let spec_ago = X509Spec::self_signed("ago").with_validity_days(days);
323 assert_eq!(spec_ago.not_after_duration(), Duration::from_secs(secs));
324
325 let spec_future = X509Spec::self_signed("future")
326 .with_not_before(NotBeforeOffset::DaysFromNow(days))
327 .with_validity_days(days);
328 assert_eq!(
329 spec_future.not_after_duration(),
330 Duration::from_secs(secs * 2)
331 );
332 }
333
334 #[test]
335 fn test_stable_bytes_determinism() {
336 let spec1 = X509Spec::self_signed("test");
337 let spec2 = X509Spec::self_signed("test");
338 assert_eq!(spec1.stable_bytes(), spec2.stable_bytes());
339
340 let spec3 = X509Spec::self_signed("different");
341 assert_ne!(spec1.stable_bytes(), spec3.stable_bytes());
342 }
343
344 #[test]
345 fn test_stable_bytes_deduplicates_sans() {
346 let with_dupes = X509Spec::self_signed("test").with_sans(vec![
347 "a.com".into(),
348 "a.com".into(),
349 "b.com".into(),
350 ]);
351 let without_dupes =
352 X509Spec::self_signed("test").with_sans(vec!["a.com".into(), "b.com".into()]);
353 assert_eq!(with_dupes.stable_bytes(), without_dupes.stable_bytes());
354 }
355
356 #[test]
357 fn test_stable_bytes_field_sensitivity() {
358 let base = X509Spec::self_signed("test");
359 let base_bytes = base.stable_bytes();
360
361 let changed = base.clone().with_validity_days(999);
363 assert_ne!(
364 changed.stable_bytes(),
365 base_bytes,
366 "validity_days must affect stable_bytes"
367 );
368
369 let changed = base.clone().with_is_ca(true);
371 assert_ne!(
372 changed.stable_bytes(),
373 base_bytes,
374 "is_ca must affect stable_bytes"
375 );
376
377 let changed = base.clone().with_rsa_bits(4096);
379 assert_ne!(
380 changed.stable_bytes(),
381 base_bytes,
382 "rsa_bits must affect stable_bytes"
383 );
384
385 let changed = base
387 .clone()
388 .with_not_before(NotBeforeOffset::DaysFromNow(7));
389 assert_ne!(
390 changed.stable_bytes(),
391 base_bytes,
392 "not_before_offset must affect stable_bytes"
393 );
394
395 let changed = base.clone().with_key_usage(KeyUsage::ca());
397 assert_ne!(
398 changed.stable_bytes(),
399 base_bytes,
400 "key_usage must affect stable_bytes"
401 );
402
403 let mut changed = base.clone();
405 changed.issuer_cn = "Other Issuer".to_string();
406 assert_ne!(
407 changed.stable_bytes(),
408 base_bytes,
409 "issuer_cn must affect stable_bytes"
410 );
411
412 let changed = base.clone().with_sans(vec!["san.example.com".into()]);
414 assert_ne!(
415 changed.stable_bytes(),
416 base_bytes,
417 "sans must affect stable_bytes"
418 );
419 }
420
421 #[test]
422 fn test_stable_bytes_not_before_offset_variants_differ() {
423 let days_ago = X509Spec::self_signed("test").with_not_before(NotBeforeOffset::DaysAgo(1));
424 let days_from_now =
425 X509Spec::self_signed("test").with_not_before(NotBeforeOffset::DaysFromNow(1));
426
427 assert_ne!(
428 days_ago.stable_bytes(),
429 days_from_now.stable_bytes(),
430 "DaysAgo(1) and DaysFromNow(1) must produce different stable_bytes (tag byte 0 vs 1)"
431 );
432 }
433}