uselesskey_x509/srp/spec/
chain_spec.rs1use super::{KeyUsage, NotBeforeOffset};
4
5#[derive(Clone, Debug, Eq, PartialEq, Hash)]
8pub struct ChainSpec {
9 pub leaf_cn: String,
11 pub leaf_sans: Vec<String>,
13 pub root_cn: String,
15 pub intermediate_cn: String,
17 pub rsa_bits: usize,
19 pub root_validity_days: u32,
21 pub intermediate_validity_days: u32,
23 pub leaf_validity_days: u32,
25 pub leaf_not_before: Option<NotBeforeOffset>,
29 pub intermediate_not_before: Option<NotBeforeOffset>,
33 pub intermediate_is_ca: Option<bool>,
37 pub intermediate_key_usage: Option<KeyUsage>,
41}
42
43impl ChainSpec {
44 pub fn new(leaf_cn: impl Into<String>) -> Self {
48 let leaf_cn = leaf_cn.into();
49 let root_cn = format!("{} Root CA", leaf_cn);
50 let intermediate_cn = format!("{} Intermediate CA", leaf_cn);
51 let leaf_sans = vec![leaf_cn.clone()];
52 Self {
53 leaf_cn,
54 leaf_sans,
55 root_cn,
56 intermediate_cn,
57 rsa_bits: 2048,
58 root_validity_days: 3650,
59 intermediate_validity_days: 1825,
60 leaf_validity_days: 3650,
61 leaf_not_before: None,
62 intermediate_not_before: None,
63 intermediate_is_ca: None,
64 intermediate_key_usage: None,
65 }
66 }
67
68 pub fn with_sans(mut self, sans: Vec<String>) -> Self {
72 self.leaf_sans = sans;
73 self
74 }
75
76 pub fn with_root_cn(mut self, cn: impl Into<String>) -> Self {
78 self.root_cn = cn.into();
79 self
80 }
81
82 pub fn with_intermediate_cn(mut self, cn: impl Into<String>) -> Self {
84 self.intermediate_cn = cn.into();
85 self
86 }
87
88 pub fn with_rsa_bits(mut self, bits: usize) -> Self {
90 self.rsa_bits = bits;
91 self
92 }
93
94 pub fn with_root_validity_days(mut self, days: u32) -> Self {
96 self.root_validity_days = days;
97 self
98 }
99
100 pub fn with_intermediate_validity_days(mut self, days: u32) -> Self {
102 self.intermediate_validity_days = days;
103 self
104 }
105
106 pub fn with_leaf_validity_days(mut self, days: u32) -> Self {
108 self.leaf_validity_days = days;
109 self
110 }
111
112 pub fn with_leaf_not_before(mut self, offset: NotBeforeOffset) -> Self {
114 self.leaf_not_before = Some(offset);
115 self
116 }
117
118 pub fn with_intermediate_not_before(mut self, offset: NotBeforeOffset) -> Self {
120 self.intermediate_not_before = Some(offset);
121 self
122 }
123
124 pub fn with_intermediate_is_ca(mut self, is_ca: bool) -> Self {
126 self.intermediate_is_ca = Some(is_ca);
127 self
128 }
129
130 pub fn with_intermediate_key_usage(mut self, key_usage: KeyUsage) -> Self {
132 self.intermediate_key_usage = Some(key_usage);
133 self
134 }
135
136 pub fn stable_bytes(&self) -> Vec<u8> {
144 if self.uses_v2_compat_encoding() {
145 return self.stable_bytes_v2_compat();
146 }
147
148 self.stable_bytes_v3()
149 }
150
151 fn uses_v2_compat_encoding(&self) -> bool {
152 self.intermediate_is_ca.is_none()
153 && self.intermediate_key_usage.is_none()
154 && supports_v2_not_before(self.leaf_not_before)
155 && supports_v2_not_before(self.intermediate_not_before)
156 }
157
158 fn stable_bytes_v2_compat(&self) -> Vec<u8> {
159 let mut out = Vec::new();
160
161 out.push(2);
163 encode_common_fields(self, &mut out);
164 encode_optional_days_ago_i64(&mut out, self.leaf_not_before);
165 encode_optional_days_ago_i64(&mut out, self.intermediate_not_before);
166 out
167 }
168
169 fn stable_bytes_v3(&self) -> Vec<u8> {
170 let mut out = Vec::new();
171
172 out.push(3);
174 encode_common_fields(self, &mut out);
175
176 encode_optional_not_before(&mut out, self.leaf_not_before);
178 encode_optional_not_before(&mut out, self.intermediate_not_before);
179
180 match self.intermediate_is_ca {
181 None => out.push(0),
182 Some(false) => out.push(1),
183 Some(true) => out.push(2),
184 }
185
186 match self.intermediate_key_usage {
187 None => out.push(0),
188 Some(key_usage) => {
189 out.push(1);
190 out.extend_from_slice(&key_usage.stable_bytes());
191 }
192 }
193
194 out
195 }
196}
197
198fn encode_common_fields(spec: &ChainSpec, out: &mut Vec<u8>) {
199 let leaf_cn_bytes = spec.leaf_cn.as_bytes();
201 out.extend_from_slice(&(leaf_cn_bytes.len() as u32).to_be_bytes());
202 out.extend_from_slice(leaf_cn_bytes);
203
204 let mut sorted_sans = spec.leaf_sans.clone();
206 sorted_sans.sort();
207 sorted_sans.dedup();
208 out.extend_from_slice(&(sorted_sans.len() as u32).to_be_bytes());
209 for san in &sorted_sans {
210 let san_bytes = san.as_bytes();
211 out.extend_from_slice(&(san_bytes.len() as u32).to_be_bytes());
212 out.extend_from_slice(san_bytes);
213 }
214
215 let root_cn_bytes = spec.root_cn.as_bytes();
217 out.extend_from_slice(&(root_cn_bytes.len() as u32).to_be_bytes());
218 out.extend_from_slice(root_cn_bytes);
219
220 let int_cn_bytes = spec.intermediate_cn.as_bytes();
222 out.extend_from_slice(&(int_cn_bytes.len() as u32).to_be_bytes());
223 out.extend_from_slice(int_cn_bytes);
224
225 out.extend_from_slice(&(spec.rsa_bits as u32).to_be_bytes());
227
228 out.extend_from_slice(&spec.root_validity_days.to_be_bytes());
230 out.extend_from_slice(&spec.intermediate_validity_days.to_be_bytes());
231 out.extend_from_slice(&spec.leaf_validity_days.to_be_bytes());
232}
233
234fn supports_v2_not_before(offset: Option<NotBeforeOffset>) -> bool {
235 matches!(offset, None | Some(NotBeforeOffset::DaysAgo(_)))
236}
237
238fn encode_optional_days_ago_i64(out: &mut Vec<u8>, offset: Option<NotBeforeOffset>) {
239 match offset {
240 None => out.push(0),
241 Some(NotBeforeOffset::DaysAgo(days)) => {
242 out.push(1);
243 out.extend_from_slice(&i64::from(days).to_be_bytes());
244 }
245 Some(NotBeforeOffset::DaysFromNow(_)) => {
246 unreachable!("DaysFromNow requires v3 encoding")
247 }
248 }
249}
250
251fn encode_optional_not_before(out: &mut Vec<u8>, offset: Option<NotBeforeOffset>) {
252 match offset {
253 None => out.push(0),
254 Some(NotBeforeOffset::DaysAgo(days)) => {
255 out.push(1);
256 out.extend_from_slice(&days.to_be_bytes());
257 }
258 Some(NotBeforeOffset::DaysFromNow(days)) => {
259 out.push(2);
260 out.extend_from_slice(&days.to_be_bytes());
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_defaults() {
271 let spec = ChainSpec::new("test.example.com");
272 assert_eq!(spec.leaf_cn, "test.example.com");
273 assert_eq!(spec.leaf_sans, vec!["test.example.com"]);
274 assert_eq!(spec.root_cn, "test.example.com Root CA");
275 assert_eq!(spec.intermediate_cn, "test.example.com Intermediate CA");
276 assert_eq!(spec.rsa_bits, 2048);
277 assert_eq!(spec.root_validity_days, 3650);
278 assert_eq!(spec.intermediate_validity_days, 1825);
279 assert_eq!(spec.leaf_validity_days, 3650);
280 assert_eq!(spec.leaf_not_before, None);
281 assert_eq!(spec.intermediate_not_before, None);
282 assert_eq!(spec.intermediate_is_ca, None);
283 assert_eq!(spec.intermediate_key_usage, None);
284 }
285
286 #[test]
287 fn test_builders() {
288 let spec = ChainSpec::new("example.com")
289 .with_sans(vec![
290 "example.com".to_string(),
291 "www.example.com".to_string(),
292 ])
293 .with_root_cn("My Root CA")
294 .with_intermediate_cn("My Int CA")
295 .with_rsa_bits(4096)
296 .with_root_validity_days(7300)
297 .with_intermediate_validity_days(3650)
298 .with_leaf_validity_days(90)
299 .with_leaf_not_before(NotBeforeOffset::DaysFromNow(7))
300 .with_intermediate_not_before(NotBeforeOffset::DaysAgo(30))
301 .with_intermediate_is_ca(false)
302 .with_intermediate_key_usage(KeyUsage::leaf());
303
304 assert_eq!(spec.leaf_sans.len(), 2);
305 assert_eq!(spec.root_cn, "My Root CA");
306 assert_eq!(spec.intermediate_cn, "My Int CA");
307 assert_eq!(spec.rsa_bits, 4096);
308 assert_eq!(spec.root_validity_days, 7300);
309 assert_eq!(spec.intermediate_validity_days, 3650);
310 assert_eq!(spec.leaf_validity_days, 90);
311 assert_eq!(spec.leaf_not_before, Some(NotBeforeOffset::DaysFromNow(7)));
312 assert_eq!(
313 spec.intermediate_not_before,
314 Some(NotBeforeOffset::DaysAgo(30))
315 );
316 assert_eq!(spec.intermediate_is_ca, Some(false));
317 assert_eq!(spec.intermediate_key_usage, Some(KeyUsage::leaf()));
318 }
319
320 #[test]
321 fn test_stable_bytes_determinism() {
322 let spec1 = ChainSpec::new("test.example.com");
323 let spec2 = ChainSpec::new("test.example.com");
324 assert_eq!(spec1.stable_bytes(), spec2.stable_bytes());
325
326 let spec3 = ChainSpec::new("other.example.com");
327 assert_ne!(spec1.stable_bytes(), spec3.stable_bytes());
328 }
329
330 #[test]
331 fn test_stable_bytes_san_order_independent() {
332 let spec1 = ChainSpec::new("test.example.com").with_sans(vec![
333 "a.example.com".to_string(),
334 "b.example.com".to_string(),
335 ]);
336 let spec2 = ChainSpec::new("test.example.com").with_sans(vec![
337 "b.example.com".to_string(),
338 "a.example.com".to_string(),
339 ]);
340 assert_eq!(spec1.stable_bytes(), spec2.stable_bytes());
341 }
342
343 #[test]
344 fn test_stable_bytes_field_sensitivity() {
345 let base = ChainSpec::new("test.example.com");
346 let base_bytes = base.stable_bytes();
347
348 let changed = base.clone().with_rsa_bits(4096);
350 assert_ne!(
351 changed.stable_bytes(),
352 base_bytes,
353 "rsa_bits must affect stable_bytes"
354 );
355
356 let changed = base.clone().with_root_validity_days(999);
358 assert_ne!(
359 changed.stable_bytes(),
360 base_bytes,
361 "root_validity_days must affect stable_bytes"
362 );
363
364 let changed = base.clone().with_intermediate_validity_days(999);
366 assert_ne!(
367 changed.stable_bytes(),
368 base_bytes,
369 "intermediate_validity_days must affect stable_bytes"
370 );
371
372 let changed = base.clone().with_leaf_validity_days(999);
374 assert_ne!(
375 changed.stable_bytes(),
376 base_bytes,
377 "leaf_validity_days must affect stable_bytes"
378 );
379
380 let changed = base.clone().with_root_cn("Other Root CA");
382 assert_ne!(
383 changed.stable_bytes(),
384 base_bytes,
385 "root_cn must affect stable_bytes"
386 );
387
388 let changed = base.clone().with_intermediate_cn("Other Int CA");
390 assert_ne!(
391 changed.stable_bytes(),
392 base_bytes,
393 "intermediate_cn must affect stable_bytes"
394 );
395
396 let changed = base
398 .clone()
399 .with_sans(vec!["extra.example.com".to_string()]);
400 assert_ne!(
401 changed.stable_bytes(),
402 base_bytes,
403 "leaf_sans must affect stable_bytes"
404 );
405 }
406
407 #[test]
408 fn test_stable_bytes_optional_offset_sensitivity() {
409 let base = ChainSpec::new("test.example.com");
410 let base_bytes = base.stable_bytes();
411
412 let mut with_leaf_offset = base.clone();
414 with_leaf_offset.leaf_not_before = Some(NotBeforeOffset::DaysAgo(100));
415 assert_ne!(
416 with_leaf_offset.stable_bytes(),
417 base_bytes,
418 "leaf_not_before None vs Some must differ"
419 );
420
421 let mut with_leaf_offset2 = base.clone();
423 with_leaf_offset2.leaf_not_before = Some(NotBeforeOffset::DaysFromNow(100));
424 assert_ne!(
425 with_leaf_offset.stable_bytes(),
426 with_leaf_offset2.stable_bytes(),
427 "leaf_not_before days-ago vs days-from-now must differ"
428 );
429
430 let mut with_int_offset = base.clone();
432 with_int_offset.intermediate_not_before = Some(NotBeforeOffset::DaysAgo(100));
433 assert_ne!(
434 with_int_offset.stable_bytes(),
435 base_bytes,
436 "intermediate_not_before None vs Some must differ"
437 );
438
439 let mut with_int_offset2 = base.clone();
441 with_int_offset2.intermediate_not_before = Some(NotBeforeOffset::DaysAgo(200));
442 assert_ne!(
443 with_int_offset.stable_bytes(),
444 with_int_offset2.stable_bytes(),
445 "intermediate_not_before Some(100) vs Some(200) must differ"
446 );
447
448 let with_int_is_ca = base.clone().with_intermediate_is_ca(false);
449 assert_ne!(
450 with_int_is_ca.stable_bytes(),
451 base_bytes,
452 "intermediate_is_ca must affect stable_bytes"
453 );
454
455 let with_int_ku = base.clone().with_intermediate_key_usage(KeyUsage::leaf());
456 assert_ne!(
457 with_int_ku.stable_bytes(),
458 base_bytes,
459 "intermediate_key_usage must affect stable_bytes"
460 );
461 }
462
463 #[test]
464 fn test_stable_bytes_v3_encodes_not_before_offsets() {
465 let base = ChainSpec::new("test.example.com").with_intermediate_is_ca(false);
466 let base_bytes = base.stable_bytes();
467
468 let leaf_future = base
469 .clone()
470 .with_leaf_not_before(NotBeforeOffset::DaysFromNow(7));
471 assert_ne!(
472 leaf_future.stable_bytes(),
473 base_bytes,
474 "v3 leaf not_before offset must affect stable_bytes"
475 );
476
477 let leaf_past = base
478 .clone()
479 .with_leaf_not_before(NotBeforeOffset::DaysAgo(7));
480 assert_ne!(
481 leaf_future.stable_bytes(),
482 leaf_past.stable_bytes(),
483 "v3 leaf days-from-now and days-ago offsets must differ"
484 );
485
486 let intermediate_future =
487 base.with_intermediate_not_before(NotBeforeOffset::DaysFromNow(7));
488 assert_ne!(
489 intermediate_future.stable_bytes(),
490 base_bytes,
491 "v3 intermediate not_before offset must affect stable_bytes"
492 );
493 }
494
495 #[test]
496 fn test_stable_bytes_default_uses_v2_compat_prefix() {
497 let spec = ChainSpec::new("compat.example.com");
498 assert_eq!(spec.stable_bytes()[0], 2);
499 }
500
501 #[test]
502 fn test_stable_bytes_days_ago_only_stays_on_v2_compat() {
503 let spec = ChainSpec::new("compat.example.com")
504 .with_leaf_not_before(NotBeforeOffset::DaysAgo(7))
505 .with_intermediate_not_before(NotBeforeOffset::DaysAgo(30));
506 assert_eq!(spec.stable_bytes()[0], 2);
507 }
508
509 #[test]
510 fn test_stable_bytes_days_from_now_or_intermediate_overrides_use_v3() {
511 let future = ChainSpec::new("future.example.com")
512 .with_leaf_not_before(NotBeforeOffset::DaysFromNow(7));
513 assert_eq!(future.stable_bytes()[0], 3);
514
515 let not_ca = ChainSpec::new("path.example.com").with_intermediate_is_ca(false);
516 assert_eq!(not_ca.stable_bytes()[0], 3);
517
518 let wrong_ku =
519 ChainSpec::new("path.example.com").with_intermediate_key_usage(KeyUsage::leaf());
520 assert_eq!(wrong_ku.stable_bytes()[0], 3);
521 }
522}