← Week 1: Certificate Issuance Pipeline

Day 3: Intermediate CA Issuance

Phase 2 · June 19, 2026

← Week 1: Certificate Issuance Pipeline

Agenda (2–3 hours)

  • Read (20 min): rcgen signed_by API; CertificateParams::from_ca_cert_der
  • Build (120 min): Implement Ca::new_intermediate() and Ca::issue_intermediate()
  • Verify (30 min): Validate chain with openssl verify
← Week 1: Certificate Issuance Pipeline

Intermediate CA Requirements

An intermediate CA cert must have:

  • BasicConstraints: cA=TRUE, pathLen=0, critical — can issue leaf certs, no further CAs
  • KeyUsage: keyCertSign, cRLSign, critical
  • AKI linking to root's SKI (rcgen sets this automatically)
  • Shorter validity than root: 5–8 years
  • A separate key from the root (never share keys between CA tiers)
← Week 1: Certificate Issuance Pipeline

rcgen: Signing an Intermediate with the Root

pub fn new_intermediate(
    common_name: &str,
    issuer: &Ca,
) -> 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(0)); // pathLen=0
    params.key_usages = vec![
        KeyUsagePurpose::KeyCertSign,
        KeyUsagePurpose::CrlSign,
    ];
    params.not_before = OffsetDateTime::now_utc();
    params.not_after  = OffsetDateTime::now_utc() + Duration::days(365 * 7);

    // Signed by the issuer — NOT self-signed
    let cert = params.signed_by(&key, &issuer.cert, &issuer.key)?;

    Ok(Ca { cert, key, kind: CaKind::Intermediate })
}
← Week 1: Certificate Issuance Pipeline

AKI/SKI: How rcgen Handles It

rcgen automatically computes and sets:

  • SubjectKeyIdentifier on every cert it generates
  • AuthorityKeyIdentifier on every non-self-signed cert (set to issuer's SKI)

This means chain building via AKI/SKI works out of the box.

You can verify this with x509-parser:

let aki = cert.authority_key_identifier()?;   // should match issuer's SKI
let ski = cert.subject_key_identifier()?;
← Week 1: Certificate Issuance Pipeline

Chain Validation with openssl

# Write certs to files from your Rust test
root_ca.save("./test-pki/root")?;
intermediate.save("./test-pki/intermediate")?;

# Validate the intermediate against the root
openssl verify -CAfile ./test-pki/root/ca.crt \
               ./test-pki/intermediate/ca.crt

# Expected: ./test-pki/intermediate/ca.crt: OK

Also verify with openssl x509 -text that pathLenConstraint=0 is present.

← Week 1: Certificate Issuance Pipeline

CRL Distribution Points

The intermediate CA cert should include a CDP extension pointing to where
its CRL will be served. We'll stub this as a localhost URL for now:

params.crl_distribution_points = vec![
    rcgen::CrlDistributionPoint {
        uris: vec!["http://localhost:8080/crl/intermediate.crl".to_string()],
    }
];

We'll implement the actual CRL server in Week 2. Setting the CDP now means
issued leaf certs will automatically inherit it from the issuer's policy.

← Week 1: Certificate Issuance Pipeline

Challenge Assignment

Implement Ca::new_intermediate() in ca.rs.

Then add a test test_three_tier_hierarchy that:

  1. Creates a root CA
  2. Issues an intermediate CA from the root
  3. Asserts intermediate's cA=true, pathLen=0
  4. Asserts intermediate's AKI matches root's SKI (use x509-parser to extract both)
  5. Writes both to ./test-pki/ and runs:
    std::process::Command::new("openssl")
        .args(["verify", "-CAfile", "root/ca.crt", "intermediate/ca.crt"])
    
    Assert the command exits 0.
← Week 1: Certificate Issuance Pipeline

Resources

  • rcgen signed_by: docs.rs/rcgen — CertificateParams::signed_by
  • rcgen CrlDistributionPoint: docs.rs/rcgen
  • x509-parser authority_key_identifier(), subject_key_identifier()
  • openssl-verify(1) man page