← Week 1: Certificate Issuance Pipeline

Day 4: TLS Server Leaf Cert Issuance

Phase 2 · June 20, 2026

← Week 1: Certificate Issuance Pipeline

Agenda (2–3 hours)

  • Read (20 min): rcgen SanType, SubjectAltName; CA/B Forum leaf cert requirements (skim §7)
  • Build (120 min): Implement issue_server_cert() in issue.rs
  • Verify (30 min): Use the issued cert in a basic rustls server — does it validate?
← Week 1: Certificate Issuance Pipeline

TLS Server Cert Requirements

Field Value
BasicConstraints cA=FALSE (or absent)
KeyUsage digitalSignature
ExtendedKeyUsage id-kp-serverAuth
SubjectAltName one or more dNSName entries
AKI links to issuing intermediate
Validity ≤ 398 days (CA/B Forum requirement as of 2020)

CN is ignored for TLS validation — SANs are authoritative.
Omit CN or set it to the primary hostname as a human-readable label only.

← Week 1: Certificate Issuance Pipeline

rcgen: Issuing a Server Cert

use rcgen::{
    CertificateParams, ExtendedKeyUsagePurpose,
    KeyUsagePurpose, SanType, KeyPair,
};

pub fn issue_server_cert(
    hostnames: &[&str],
    issuer: &Ca,
) -> anyhow::Result<(rcgen::Certificate, rcgen::KeyPair)> {
    let key = KeyPair::generate()?;

    let mut params = CertificateParams::new(
        hostnames.iter().map(|s| s.to_string()).collect()
    )?;
    // ^ CertificateParams::new() sets SANs from the provided list

    params.is_ca = IsCa::NoCa;
    params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
    params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
    params.not_before = OffsetDateTime::now_utc();
    params.not_after  = OffsetDateTime::now_utc() + Duration::days(90);

    let cert = params.signed_by(&key, &issuer.cert, &issuer.key)?;
    Ok((cert, key))
}
← Week 1: Certificate Issuance Pipeline

Serial Numbers

rcgen generates a random serial by default.
You should store it in CertRecord so you can later revoke by serial.

// After generation, extract the serial from the parsed cert:
use x509_parser::prelude::*;
let (_, parsed) = X509Certificate::from_der(cert.der())?;
let serial_hex = hex::encode(parsed.serial.to_bytes_be());

The serial is the key you use for all future revocation and OCSP operations.

← Week 1: Certificate Issuance Pipeline

Verifying with rustls (quick check)

// Build a rustls RootCertStore from your root CA cert
let mut roots = RootCertStore::empty();
roots.add(CertificateDer::from(root_ca.cert.der().to_vec()))?;

// Build a ServerConfig with the leaf cert + intermediate
let server_cert_chain = vec![
    CertificateDer::from(leaf_cert.der().to_vec()),
    CertificateDer::from(intermediate.cert.der().to_vec()),
];
let private_key = PrivateKeyDer::from(/* leaf key DER */);
let server_cfg = ServerConfig::builder()
    .with_no_client_auth()
    .with_single_cert(server_cert_chain, private_key)?;

If this doesn't panic/error, rustls accepted your cert hierarchy.

← Week 1: Certificate Issuance Pipeline

Challenge Assignment

Implement issue_server_cert() in issue.rs. Add a CertRecord to the store.

Write a test test_server_cert_issuance that:

  1. Creates root → intermediate → server leaf for localhost
  2. Parses the leaf cert and asserts:
    • cA=false
    • KeyUsage = digitalSignature
    • EKU = serverAuth
    • SANs include dNSName: localhost
    • Validity ≤ 398 days
  3. Builds a rustls ServerConfig with the cert chain — assert it succeeds
← Week 1: Certificate Issuance Pipeline

Resources

  • rcgen CertificateParams::new() — takes a Vec of SAN strings
  • rcgen ExtendedKeyUsagePurpose — docs.rs/rcgen
  • CA/B Forum Baseline Requirements §7.1: leaf cert profile
  • rustls ServerConfig::builder() — docs.rs/rustls