1use std::net::IpAddr;
10
11use rcgen::{
12 BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa,
13 Issuer, KeyPair, KeyUsagePurpose, PKCS_ECDSA_P256_SHA256, SanType,
14};
15use thiserror::Error;
16use time::{Duration, OffsetDateTime, error::Format, format_description::well_known::Rfc3339};
17
18#[derive(Debug, Error)]
19pub enum CaError {
20 #[error("rcgen: {0}")]
21 Rcgen(#[from] rcgen::Error),
22 #[error("time math overflow")]
23 TimeOverflow,
24 #[error("time format: {0}")]
25 TimeFormat(#[from] Format),
26}
27
28#[derive(Debug, Clone)]
30pub struct CaMaterial {
31 pub cert_pem: String,
32 pub key_pem: String,
33}
34
35pub struct UserCert {
37 pub cert_pem: String,
38 pub key_pem: String,
39 pub not_after: String,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
45pub enum SubjectAltName {
46 Dns(String),
47 Ip(IpAddr),
48}
49
50pub struct BrokerCert {
52 pub cert_pem: String,
53 pub key_pem: String,
54 pub not_after: String,
55}
56
57fn validity_window(validity_days: u32) -> Result<(OffsetDateTime, OffsetDateTime), CaError> {
58 let not_before = OffsetDateTime::now_utc();
59 let not_after = not_before
60 .checked_add(Duration::days(i64::from(validity_days)))
61 .ok_or(CaError::TimeOverflow)?;
62 Ok((not_before, not_after))
63}
64
65#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
72pub fn generate_clients_ca(cn: &str, validity_days: u32) -> Result<CaMaterial, CaError> {
75 let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
76
77 let mut params = CertificateParams::new(Vec::<String>::new())?;
78 let (not_before, not_after) = validity_window(validity_days)?;
79 params.not_before = not_before;
80 params.not_after = not_after;
81
82 let mut dn = DistinguishedName::new();
83 dn.push(DnType::CommonName, cn);
84 dn.push(DnType::OrganizationName, "krabka");
85 params.distinguished_name = dn;
86
87 params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
88 params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
89
90 let cert = params.self_signed(&key)?;
91
92 Ok(CaMaterial {
93 cert_pem: cert.pem(),
94 key_pem: key.serialize_pem(),
95 })
96}
97
98#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
108pub fn generate_cluster_ca(cn: &str, validity_days: u32) -> Result<CaMaterial, CaError> {
111 let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
112
113 let mut params = CertificateParams::new(Vec::<String>::new())?;
114 let (not_before, not_after) = validity_window(validity_days)?;
115 params.not_before = not_before;
116 params.not_after = not_after;
117
118 let mut dn = DistinguishedName::new();
119 dn.push(DnType::CommonName, cn);
120 dn.push(DnType::OrganizationName, "krabka");
121 dn.push(DnType::OrganizationalUnitName, "cluster");
122 params.distinguished_name = dn;
123
124 params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
125 params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
126
127 let cert = params.self_signed(&key)?;
128
129 Ok(CaMaterial {
130 cert_pem: cert.pem(),
131 key_pem: key.serialize_pem(),
132 })
133}
134
135#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
149pub fn renew_cluster_ca(key_pem: &str, cn: &str, validity_days: u32) -> Result<String, CaError> {
152 renew_ca(key_pem, cn, validity_days, true)
153}
154
155#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
163pub fn renew_clients_ca(key_pem: &str, cn: &str, validity_days: u32) -> Result<String, CaError> {
166 renew_ca(key_pem, cn, validity_days, false)
167}
168
169fn renew_ca(key_pem: &str, cn: &str, validity_days: u32, cluster: bool) -> Result<String, CaError> {
170 let key = KeyPair::from_pem(key_pem)?;
171
172 let mut params = CertificateParams::new(Vec::<String>::new())?;
173 let (not_before, not_after) = validity_window(validity_days)?;
174 params.not_before = not_before;
175 params.not_after = not_after;
176
177 let mut dn = DistinguishedName::new();
178 dn.push(DnType::CommonName, cn);
179 dn.push(DnType::OrganizationName, "krabka");
180 if cluster {
181 dn.push(DnType::OrganizationalUnitName, "cluster");
182 }
183 params.distinguished_name = dn;
184
185 params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
186 params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
187
188 let cert = params.self_signed(&key)?;
189 Ok(cert.pem())
190}
191
192#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
205pub fn issue_broker_cert(
210 ca_cert_pem: &str,
211 ca_key_pem: &str,
212 cn: &str,
213 base_sans: &[SubjectAltName],
214 extra_sans: &[SubjectAltName],
215 validity_days: u32,
216) -> Result<BrokerCert, CaError> {
217 let mut all_sans: Vec<SubjectAltName> = base_sans.to_vec();
218 for s in extra_sans {
219 if !all_sans.contains(s) {
220 all_sans.push(s.clone());
221 }
222 }
223
224 let ca_key = KeyPair::from_pem(ca_key_pem)?;
225 let ca_issuer = Issuer::from_ca_cert_pem(ca_cert_pem, ca_key)?;
226
227 let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
228
229 let mut params = CertificateParams::new(Vec::<String>::new())?;
230 let (not_before, not_after) = validity_window(validity_days)?;
231 params.not_before = not_before;
232 params.not_after = not_after;
233
234 let mut dn = DistinguishedName::new();
235 dn.push(DnType::CommonName, cn);
236 params.distinguished_name = dn;
237
238 params.is_ca = IsCa::NoCa;
239 params.key_usages = vec![
240 KeyUsagePurpose::DigitalSignature,
241 KeyUsagePurpose::KeyEncipherment,
242 ];
243 params.extended_key_usages = vec![
244 ExtendedKeyUsagePurpose::ServerAuth,
245 ExtendedKeyUsagePurpose::ClientAuth,
246 ];
247 params.subject_alt_names = all_sans
248 .iter()
249 .map(|s| match s {
250 SubjectAltName::Dns(d) => d.parse().map(SanType::DnsName),
251 SubjectAltName::Ip(ip) => Ok(SanType::IpAddress(*ip)),
252 })
253 .collect::<Result<_, rcgen::Error>>()?;
254
255 let leaf = params.signed_by(&leaf_key, &ca_issuer)?;
256 let not_after_str = not_after.format(&Rfc3339)?;
257
258 Ok(BrokerCert {
259 cert_pem: leaf.pem(),
260 key_pem: leaf_key.serialize_pem(),
261 not_after: not_after_str,
262 })
263}
264
265#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
275pub fn issue_user_cert(
278 ca_cert_pem: &str,
279 ca_key_pem: &str,
280 cn: &str,
281 validity_days: u32,
282) -> Result<UserCert, CaError> {
283 let ca_key = KeyPair::from_pem(ca_key_pem)?;
284 let ca_issuer = Issuer::from_ca_cert_pem(ca_cert_pem, ca_key)?;
285
286 let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
287
288 let mut params = CertificateParams::new(Vec::<String>::new())?;
289 let (not_before, not_after) = validity_window(validity_days)?;
290 params.not_before = not_before;
291 params.not_after = not_after;
292
293 let mut dn = DistinguishedName::new();
294 dn.push(DnType::CommonName, cn);
295 params.distinguished_name = dn;
296
297 params.is_ca = IsCa::NoCa;
298 params.key_usages = vec![
299 KeyUsagePurpose::DigitalSignature,
300 KeyUsagePurpose::KeyEncipherment,
301 ];
302 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
303
304 let leaf = params.signed_by(&leaf_key, &ca_issuer)?;
305 let not_after_str = not_after.format(&Rfc3339)?;
306
307 Ok(UserCert {
308 cert_pem: leaf.pem(),
309 key_pem: leaf_key.serialize_pem(),
310 not_after: not_after_str,
311 })
312}
313
314#[cfg(test)]
315mod tests {
316
317 use rustls::pki_types::{CertificateDer, pem::PemObject};
318 use x509_parser::{
319 extensions::GeneralName,
320 prelude::{FromDer, X509Certificate},
321 };
322
323 use super::*;
324
325 fn pem_to_der(pem: &str) -> CertificateDer<'static> {
326 CertificateDer::pem_slice_iter(pem.as_bytes())
327 .next()
328 .expect("at least one PEM cert")
329 .expect("valid PEM cert")
330 }
331
332 #[test]
333 fn generate_clients_ca_round_trips() {
334 let validity_days: u32 = 365;
335 let ca = generate_clients_ca("root", validity_days).expect("generate CA");
336
337 assert2::assert!(ca.cert_pem.contains("BEGIN CERTIFICATE"));
338 assert2::assert!(ca.key_pem.contains("BEGIN PRIVATE KEY"));
339
340 let der = pem_to_der(&ca.cert_pem);
341 let (_, cert) = X509Certificate::from_der(der.as_ref()).expect("parse CA DER");
342
343 let subject = cert.subject().to_string();
344 assert2::assert!(subject.contains("CN=root"));
345 assert2::assert!(subject.contains("O=krabka"));
346
347 let bc = cert
348 .basic_constraints()
349 .expect("basic constraints parse")
350 .expect("basic constraints present");
351 assert2::assert!(bc.value.ca);
352
353 let validity = cert.validity();
354 let span = validity.not_after.timestamp() - validity.not_before.timestamp();
355 let expected = i64::from(validity_days) * 86_400;
356 let tolerance: i64 = 60;
357 assert2::assert!((span - expected).abs() <= tolerance);
358 }
359
360 #[test]
361 fn issue_user_cert_signed_by_ca_and_bare_cn() {
362 let ca = generate_clients_ca("root", 365).expect("generate CA");
363 let user = issue_user_cert(&ca.cert_pem, &ca.key_pem, "alice", 365).expect("issue leaf");
364
365 let leaf_der = pem_to_der(&user.cert_pem);
366 let (_, leaf) = X509Certificate::from_der(leaf_der.as_ref()).expect("parse leaf DER");
367 assert2::assert!(leaf.subject().to_string() == "CN=alice");
368
369 let ca_der = pem_to_der(&ca.cert_pem);
370 let (_, ca_x509) = X509Certificate::from_der(ca_der.as_ref()).expect("parse CA DER");
371
372 leaf.verify_signature(Some(ca_x509.public_key()))
373 .expect("leaf signature must verify against CA pubkey");
374 }
375
376 #[test]
377 fn issue_user_cert_dn_matches_extract_principal() {
378 let ca = generate_clients_ca("root", 365).expect("generate CA");
379 let user = issue_user_cert(&ca.cert_pem, &ca.key_pem, "alice", 365).expect("issue leaf");
380
381 let der = pem_to_der(&user.cert_pem);
382 let dn = crate::extract_principal_from_cert(der.as_ref()).expect("extract principal");
383 assert2::assert!(dn == "CN=alice");
384 }
385
386 #[test]
387 fn extended_key_usage_is_client_auth_on_leaf() {
388 let ca = generate_clients_ca("root", 365).expect("generate CA");
389 let user = issue_user_cert(&ca.cert_pem, &ca.key_pem, "alice", 365).expect("issue leaf");
390
391 let der = pem_to_der(&user.cert_pem);
392 let (_, leaf) = X509Certificate::from_der(der.as_ref()).expect("parse leaf DER");
393 let eku = leaf
394 .extended_key_usage()
395 .expect("EKU parse")
396 .expect("EKU present");
397 assert2::assert!(eku.value.client_auth);
398 }
399
400 #[test]
401 fn each_generate_is_unique() {
402 let a = generate_clients_ca("x", 365).expect("generate CA a");
403 let b = generate_clients_ca("x", 365).expect("generate CA b");
404 assert2::assert!(a.cert_pem != b.cert_pem);
405 assert2::assert!(a.key_pem != b.key_pem);
406 }
407
408 #[test]
409 fn generate_cluster_ca_carries_ou_cluster() {
410 let ca = generate_cluster_ca("c1", 365).expect("generate cluster CA");
411 let der = pem_to_der(&ca.cert_pem);
412 let (_, cert) = X509Certificate::from_der(der.as_ref()).expect("parse cluster CA DER");
413 let subject = cert.subject().to_string();
414 for part in ["CN=c1", "O=krabka", "OU=cluster"] {
415 assert2::assert!(subject.contains(part));
416 }
417 let bc = cert
418 .basic_constraints()
419 .expect("BC parse")
420 .expect("BC present");
421 assert2::assert!(bc.value.ca);
422 }
423
424 #[test]
425 fn clients_ca_does_not_carry_ou_cluster() {
426 let ca = generate_clients_ca("root", 365).expect("generate clients CA");
427 let der = pem_to_der(&ca.cert_pem);
428 let (_, cert) = X509Certificate::from_der(der.as_ref()).expect("parse");
429 let subject = cert.subject().to_string();
430 assert2::assert!(!subject.contains("OU=cluster"));
431 }
432
433 #[test]
434 fn issue_broker_cert_has_server_and_client_auth_eku() {
435 use std::net::Ipv4Addr;
436 let ca = generate_cluster_ca("c1", 365).expect("CA");
437 let sans = vec![
438 SubjectAltName::Dns("c1-broker-0.c1-broker.default.svc.cluster.local".into()),
439 SubjectAltName::Dns("c1-broker-0".into()),
440 SubjectAltName::Ip(IpAddr::V4(Ipv4Addr::LOCALHOST)),
441 ];
442 let b = issue_broker_cert(&ca.cert_pem, &ca.key_pem, "c1-broker-0", &sans, &[], 365)
443 .expect("issue broker cert");
444
445 let der = pem_to_der(&b.cert_pem);
446 let (_, leaf) = X509Certificate::from_der(der.as_ref()).expect("parse leaf");
447
448 let eku = leaf
449 .extended_key_usage()
450 .expect("EKU parse")
451 .expect("EKU present");
452 assert2::assert!(eku.value.server_auth);
453 assert2::assert!(eku.value.client_auth);
454
455 let san_ext = leaf
456 .subject_alternative_name()
457 .expect("SAN parse")
458 .expect("SAN present");
459 let general_names: Vec<_> = san_ext.value.general_names.iter().collect();
460 assert2::assert!(general_names.iter().any(|gn| matches!(
461 gn,
462 GeneralName::DNSName(s) if *s == "c1-broker-0"
463 )));
464 assert2::assert!(
465 general_names
466 .iter()
467 .any(|gn| matches!(gn, GeneralName::IPAddress(_)))
468 );
469 }
470
471 #[test]
472 fn issue_broker_cert_deduplicates_extra_sans() {
473 let ca = generate_cluster_ca("c1", 365).expect("CA");
474 let base_sans = vec![SubjectAltName::Dns("c1-broker-0".into())];
475 let extra_sans = vec![
476 SubjectAltName::Dns("external.example".into()),
477 SubjectAltName::Dns("c1-broker-0".into()),
478 SubjectAltName::Dns("external.example".into()),
479 ];
480
481 let cert = issue_broker_cert(
482 &ca.cert_pem,
483 &ca.key_pem,
484 "c1-broker-0",
485 &base_sans,
486 &extra_sans,
487 365,
488 )
489 .expect("issue broker cert");
490
491 let der = pem_to_der(&cert.cert_pem);
492 let (_, leaf) = X509Certificate::from_der(der.as_ref()).expect("parse leaf");
493 let san_ext = leaf
494 .subject_alternative_name()
495 .expect("SAN parse")
496 .expect("SAN present");
497 let dns_names: Vec<_> = san_ext
498 .value
499 .general_names
500 .iter()
501 .filter_map(|gn| match gn {
502 GeneralName::DNSName(name) => Some(*name),
503 _ => None,
504 })
505 .collect();
506
507 assert2::assert!(dns_names == vec!["c1-broker-0", "external.example"]);
508 }
509
510 fn spki_der(cert_pem: &str) -> Vec<u8> {
511 let der = pem_to_der(cert_pem);
512 let (_, cert) = X509Certificate::from_der(der.as_ref()).expect("parse");
513 cert.public_key().raw.to_vec()
514 }
515
516 #[test]
517 fn renew_cluster_ca_reuses_key_and_preserves_subject() {
518 let orig = generate_cluster_ca("c1-cluster-ca", 30).expect("CA");
519 let renewed_pem = renew_cluster_ca(&orig.key_pem, "c1-cluster-ca", 365).expect("renew");
520
521 assert2::assert!(spki_der(&orig.cert_pem) == spki_der(&renewed_pem));
523
524 let der = pem_to_der(&renewed_pem);
526 let (_, cert) = X509Certificate::from_der(der.as_ref()).expect("parse renewed");
527 let subject = cert.subject().to_string();
528 for part in ["CN=c1-cluster-ca", "O=krabka", "OU=cluster"] {
529 assert2::assert!(subject.contains(part));
530 }
531 assert2::assert!(
532 cert.basic_constraints()
533 .expect("BC")
534 .expect("BC present")
535 .value
536 .ca
537 );
538 }
539
540 #[test]
541 fn renew_cluster_ca_extends_validity() {
542 let orig = generate_cluster_ca("c1-cluster-ca", 30).expect("CA");
543 let renewed_pem = renew_cluster_ca(&orig.key_pem, "c1-cluster-ca", 365).expect("renew");
544
545 let span = |pem: &str| {
546 let der = pem_to_der(pem);
547 let (_, c) = X509Certificate::from_der(der.as_ref()).expect("parse");
548 c.validity().not_after.timestamp() - c.validity().not_before.timestamp()
549 };
550 assert2::assert!(span(&renewed_pem) > span(&orig.cert_pem));
551 }
552
553 #[test]
554 fn leaf_under_old_cert_verifies_against_renewed_cert() {
555 let orig = generate_cluster_ca("c1-cluster-ca", 30).expect("CA");
559 let sans = vec![SubjectAltName::Dns("c1-broker-0".into())];
560 let leaf = issue_broker_cert(&orig.cert_pem, &orig.key_pem, "c1-broker-0", &sans, &[], 30)
561 .expect("leaf");
562 let renewed_pem = renew_cluster_ca(&orig.key_pem, "c1-cluster-ca", 365).expect("renew");
563
564 let leaf_der = pem_to_der(&leaf.cert_pem);
565 let (_, leaf_x509) = X509Certificate::from_der(leaf_der.as_ref()).expect("parse leaf");
566 let renewed_der = pem_to_der(&renewed_pem);
567 let (_, renewed_ca) =
568 X509Certificate::from_der(renewed_der.as_ref()).expect("parse renewed");
569
570 leaf_x509
571 .verify_signature(Some(renewed_ca.public_key()))
572 .expect("leaf must verify against the renewed CA public key");
573 }
574
575 #[test]
576 fn renew_clients_ca_has_no_ou_cluster() {
577 let orig = generate_clients_ca("c1-clients-ca", 30).expect("CA");
578 let renewed_pem = renew_clients_ca(&orig.key_pem, "c1-clients-ca", 365).expect("renew");
579 assert2::assert!(spki_der(&orig.cert_pem) == spki_der(&renewed_pem));
580 let der = pem_to_der(&renewed_pem);
581 let (_, cert) = X509Certificate::from_der(der.as_ref()).expect("parse");
582 assert2::assert!(!cert.subject().to_string().contains("OU=cluster"));
583 }
584
585 #[test]
586 fn issue_broker_cert_chains_to_cluster_ca() {
587 let ca = generate_cluster_ca("c1", 365).expect("CA");
588 let sans = vec![SubjectAltName::Dns("c1-broker-0".into())];
589 let b = issue_broker_cert(&ca.cert_pem, &ca.key_pem, "c1-broker-0", &sans, &[], 365)
590 .expect("leaf");
591
592 let leaf_der = pem_to_der(&b.cert_pem);
593 let (_, leaf) = X509Certificate::from_der(leaf_der.as_ref()).expect("parse leaf");
594 let ca_der = pem_to_der(&ca.cert_pem);
595 let (_, ca_x509) = X509Certificate::from_der(ca_der.as_ref()).expect("parse CA");
596
597 leaf.verify_signature(Some(ca_x509.public_key()))
598 .expect("leaf signature must verify against cluster CA pubkey");
599 }
600}