Skip to main content

krabka_security/
ca.rs

1//! Pure X.509 CA + leaf-cert generation for the operator's
2//! clients-CA bootstrap. Inter-broker mTLS and cert hot-reload tests
3//! can reuse it.
4//!
5//! No async, no I/O. These helpers return PEM-encoded material
6//! that callers persist to Kubernetes Secrets, files, or anywhere
7//! else.
8
9use 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/// Self-signed clients-CA material.
29#[derive(Debug, Clone)]
30pub struct CaMaterial {
31    pub cert_pem: String,
32    pub key_pem: String,
33}
34
35/// A leaf cert issued by a clients-CA. `not_after` is RFC3339.
36pub struct UserCert {
37    pub cert_pem: String,
38    pub key_pem: String,
39    pub not_after: String,
40}
41
42/// SAN entry for a leaf cert. ECDSA leaf certs accept any mix of DNS
43/// names and IP addresses, and the broker-cert path uses a mix.
44#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
45pub enum SubjectAltName {
46    Dns(String),
47    Ip(IpAddr),
48}
49
50/// A broker leaf cert, which is a server cert and a client cert in one.
51pub 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/// Generate a self-signed clients CA with `Subject = CN=<cn>, O=krabka`,
66/// `BasicConstraints: CA:TRUE`, and `KeyUsage = keyCertSign|cRLSign`.
67/// ECDSA P-256.
68// Clients-CA issuance. skip_all keeps the generated private key (a local)
69// out of span fields; only the non-sensitive CN + validity are recorded.
70// `err` surfaces rcgen / time failures (Debug).
71#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
72/// # Errors
73/// Returns an error when credentials or key material are invalid, cryptographic verification fails, or the TLS, SASL, or Kerberos exchange is rejected.
74pub 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/// Generate a self-signed cluster CA.
99///
100/// The shape is the same as [`generate_clients_ca`]: ECDSA P-256, CA:TRUE, and
101/// KU keyCertSign + cRLSign. The subject DN carries `OU=cluster`, so a reader
102/// can tell the cluster CA and the clients CA apart in cert chains and audit
103/// logs.
104// Cluster-CA issuance. skip_all keeps the generated private key (a local)
105// out of span fields; only the non-sensitive CN + validity are recorded.
106// `err` surfaces rcgen / time failures (Debug).
107#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
108/// # Errors
109/// Returns an error when credentials or key material are invalid, cryptographic verification fails, or the TLS, SASL, or Kerberos exchange is rejected.
110pub 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/// Re-sign a cluster CA cert with an existing key, a same-key renewal.
136///
137/// This function generates a fresh self-signed cert with the SAME subject DN,
138/// `CN=<cn>, O=krabka, OU=cluster`, plus `CA:TRUE`, `KU keyCertSign|cRLSign`,
139/// and a new `validityDays` window. It keys the cert with `key_pem` and not
140/// with a freshly generated key. The public key (SPKI) and the subject DN are
141/// identical to the cert this one replaces, so leaf certs issued under the old
142/// cert still chain to the renewed one. The renewal is non-disruptive. This
143/// function returns the cert PEM only, because the caller already holds the
144/// key.
145// Cluster-CA same-key renewal. skip_all keeps the `key_pem` secret out of span
146// fields; only the non-sensitive CN + validity are recorded. `err` surfaces
147// rcgen / time failures (Debug).
148#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
149/// # Errors
150/// Returns an error when credentials or key material are invalid, cryptographic verification fails, or the TLS, SASL, or Kerberos exchange is rejected.
151pub 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/// Re-sign a clients CA cert with an existing key, a same-key renewal.
156///
157/// This works like [`renew_cluster_ca`] but uses the clients-CA subject DN,
158/// which has no `OU=cluster`.
159// Clients-CA same-key renewal. skip_all keeps the `key_pem` secret out of span
160// fields; only the non-sensitive CN + validity are recorded. `err` surfaces
161// rcgen / time failures (Debug).
162#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
163/// # Errors
164/// Returns an error when credentials or key material are invalid, cryptographic verification fails, or the TLS, SASL, or Kerberos exchange is rejected.
165pub 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/// Sign a broker leaf cert: a server cert and a client cert in one.
193///
194/// The cert carries `EKU = serverAuth + clientAuth` and
195/// `KU = digitalSignature + keyEncipherment`. SANs accept a mix of DNS names
196/// and IPs. The key is ECDSA P-256.
197///
198/// This function merges `base_sans` and `extra_sans` into a single SAN list and
199/// drops duplicates silently. `extra_sans` holds entries such as the external
200/// advertised addresses for `NodePort` or `LoadBalancer` listeners.
201// Broker leaf-cert issuance. skip_all keeps the signing `ca_key_pem` secret +
202// the generated leaf key (a local) out of span fields; only the non-sensitive
203// CN + validity are recorded. `err` surfaces rcgen / time failures (Debug).
204#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
205/// # Errors
206/// Returns an error when credentials or key material are invalid, cryptographic verification fails, or the TLS, SASL, or Kerberos exchange is rejected.
207/// # Panics
208/// Panics if validated key material has an impossible size or synchronized credential state is poisoned.
209pub 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/// Sign a leaf client cert with `Subject = CN=<cn>`.
266///
267/// The subject is a bare RDN. This matches Strimzi and avoids the RFC 2253
268/// against 4514 ordering ambiguity. The cert carries
269/// `ExtendedKeyUsage = clientAuth` and
270/// `KeyUsage = digitalSignature|keyEncipherment`. The key is ECDSA P-256.
271// User leaf-cert issuance. skip_all keeps the signing `ca_key_pem` secret + the
272// generated leaf key (a local) out of span fields; only the non-sensitive CN +
273// validity are recorded. `err` surfaces rcgen / time failures (Debug).
274#[tracing::instrument(level = "info", skip_all, fields(cn = %cn, validity_days), err)]
275/// # Errors
276/// Returns an error when credentials or key material are invalid, cryptographic verification fails, or the TLS, SASL, or Kerberos exchange is rejected.
277pub 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        // Same public key (same SPKI) — the renewal reuses the key.
522        assert2::assert!(spki_der(&orig.cert_pem) == spki_der(&renewed_pem));
523
524        // Same subject DN, including OU=cluster.
525        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        // A leaf issued by the ORIGINAL cluster CA must verify against the
556        // RENEWED cert's public key — this is what makes same-key renewal
557        // non-disruptive (existing broker leafs keep chaining).
558        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}