← Week 2: Revocation: CRL and OCSP

Day 8: CRL Design

Phase 2 · June 24, 2026

← Week 2: Revocation: CRL and OCSP

Agenda (2–3 hours)

  • Read (45 min): RFC 5280 §5 (review from Phase 1 Day 27); rcgen CertificateRevocationListParams docs
  • Design (45 min): CRL data model, storage, update strategy
  • Build (60 min): RevokedCert type, CrlStore, and file layout
← Week 2: Revocation: CRL and OCSP

What a CRL Contains (recap)

CertificateList {
    tbsCertList {
        issuer:           DN of the issuing CA
        thisUpdate:       timestamp of this CRL
        nextUpdate:       timestamp of next expected CRL
        crlNumber:        monotonically increasing integer
        revokedCerts: [
            { serialNumber, revocationDate, reasonCode }
        ]
    }
    signature: CA signs the entire tbsCertList
}

A CRL is issued and signed by a CA. It must be re-signed every time it changes.

← Week 2: Revocation: CRL and OCSP

CRL Data Model for toy-pki

// In revoke.rs
pub struct RevokedEntry {
    pub serial_hex:         String,
    pub revocation_time:    OffsetDateTime,
    pub reason:             RevocationReason,
}

pub struct CrlStore {
    pub issuer_path:  PathBuf,        // path to the CA that signs this CRL
    pub crl_number:   u64,            // monotonically increasing
    pub revoked:      Vec<RevokedEntry>,
}

pub enum RevocationReason {
    Unspecified,
    KeyCompromise,
    CaCompromise,
    AffiliationChanged,
    Superseded,
    CessationOfOperation,
}
← Week 2: Revocation: CRL and OCSP

CRL Lifecycle

1. CA issues a cert → nothing in CRL yet
2. Revocation requested → add entry to CrlStore (in memory / on disk)
3. Call generate_crl() → sign the tbsCertList with CA key → write .crl file
4. HTTP server serves the .crl file at the CDP URL

Time-based regeneration:
- CRL has a nextUpdate field
- Regenerate before nextUpdate expires (typically every 24 hours)
- Even if nothing was revoked — the CRL must be refreshed to stay valid
← Week 2: Revocation: CRL and OCSP

CRL Number

The cRLNumber extension is a monotonically increasing integer.
Clients use it to detect stale cached CRLs.

// Increment on every issuance
self.crl_number += 1;

For delta CRLs (not implementing in toy-pki), the delta CRL references
the base CRL by its number.

← Week 2: Revocation: CRL and OCSP

File Layout

toy-pki/
├── root/
│   ├── ca.crt
│   └── ca.key
├── intermediate/
│   ├── ca.crt
│   ├── ca.key
│   ├── crl.json          ← revocation list (mutable state)
│   └── intermediate.crl  ← signed CRL (regenerated from crl.json)
├── issued/
│   ├── 0001.pem          ← serial → cert mapping
│   └── index.json        ← CertRecord list
← Week 2: Revocation: CRL and OCSP

Challenge Assignment

Design and implement the data layer for revocation in revoke.rs:

  1. RevokedEntry and RevocationReason types
  2. CrlStore::load(path) / CrlStore::save(path) using serde_json
  3. CrlStore::revoke(serial_hex, reason) — adds an entry, increments crl_number

No CRL signing yet (that's tomorrow). Today is purely about the data model.

Write a test that: creates a CrlStore, revokes 3 serials with different reasons,
saves to disk, loads back, and asserts all 3 are present with correct reasons.

← Week 2: Revocation: CRL and OCSP

Resources

  • RFC 5280 §5.3: CRL entry extensions (reason codes)
  • rcgen RevocationReason enum: docs.rs/rcgen
  • serde_json for CrlStore persistence