← Week 1: Certificate Issuance Pipeline

Day 2: Root CA Generation

Phase 2 · June 18, 2026

← Week 1: Certificate Issuance Pipeline

Agenda (2–3 hours)

  • Read (30 min): rcgen CertificateParams, KeyPair, IsCa docs; rcgen generate_simple_self_signed example
  • Build (120 min): Implement Ca::new_root() in ca.rs
  • Verify (30 min): Parse the output with openssl x509 and x509-parser
← Week 1: Certificate Issuance Pipeline

Root CA Requirements (recap from Phase 1)

A root CA cert must have:

  • BasicConstraints: cA=TRUE, critical — allows signing sub-certs
  • pathLenConstraint — set to 1 (one intermediate allowed below root)
  • KeyUsage: keyCertSign, cRLSign, critical
  • No ExtendedKeyUsage — that belongs on leaf certs
  • Long validity: 10–20 years
  • Strong key: P-256 or P-384
← Week 1: Certificate Issuance Pipeline

rcgen: Root CA Params

use rcgen::{
    BasicConstraints, CertificateParams, DistinguishedName,
    DnType, IsCa, KeyPair, KeyUsagePurpose,
};
use time::{Duration, OffsetDateTime};

pub fn new_root(common_name: &str) -> anyhow::Result<Ca> {
    let key = KeyPair::generate()?;

    let mut params = CertificateParams::default();
    params.distinguished_name.push(DnType::CommonName, common_name);
    params.is_ca = IsCa::Ca(BasicConstraints::Constrained(1)); // pathLen=1
    params.key_usages = vec![
        KeyUsagePurpose::KeyCertSign,
        KeyUsagePurpose::CrlSign,
    ];
    params.not_before = OffsetDateTime::now_utc();
    params.not_after  = OffsetDateTime::now_utc() + Duration::days(365 * 15);

    let cert = params.self_signed(&key)?;
    Ok(Ca { cert, key, kind: CaKind::Root })
}
← Week 1: Certificate Issuance Pipeline

Self-Signed vs Signed-By

params.self_signed(&key) — cert is signed by its own key.
Used for: root CAs, self-signed test certs.

params.signed_by(&leaf_key, &issuer_cert, &issuer_key) — cert signed by a CA.
Used for: intermediate CAs, leaf certs, client certs.

The rcgen::Certificate returned by self_signed contains both the cert bytes
and a reference to the signing key used — needed when you later call signed_by
on a child certificate.

← Week 1: Certificate Issuance Pipeline

Serialization

// Serialize to PEM
let cert_pem = ca.cert.pem();          // "-----BEGIN CERTIFICATE-----\n..."
let key_pem  = ca.key.serialize_pem(); // "-----BEGIN PRIVATE KEY-----\n..."

// Serialize to DER
let cert_der: &[u8] = ca.cert.der();

// Parse DER with x509-parser (for verification)
use x509_parser::prelude::*;
let (_, parsed) = X509Certificate::from_der(ca.cert.der())?;
println!("Subject: {}", parsed.subject());
println!("CA flag: {}", parsed.basic_constraints()?.map(|bc| bc.value.ca));
← Week 1: Certificate Issuance Pipeline

Challenge Assignment

Implement Ca::new_root() and Ca::save() in ca.rs:

impl Ca {
    pub fn new_root(common_name: &str) -> anyhow::Result<Self>;

    /// Save cert PEM to <dir>/ca.crt and key PEM to <dir>/ca.key
    pub fn save(&self, dir: &Path) -> anyhow::Result<()>;

    /// Load from saved PEM files
    pub fn load(dir: &Path) -> anyhow::Result<Self>;
}

Then write a #[test] that:

  1. Calls Ca::new_root("Test Root CA")
  2. Parses the output cert with x509-parser
  3. Asserts: cA=true, keyCertSign is set, validity is ~15 years
  4. Verifies openssl x509 -text output manually (print the PEM and inspect it)
← Week 1: Certificate Issuance Pipeline

Resources

  • rcgen docs: CertificateParams, IsCa, BasicConstraints, KeyUsagePurpose
  • rcgen source: github.com/rustls/rcgen — examples/gen_cert_for_server.rs
  • x509-parser: X509Certificate::basic_constraints(), key_usage()