← Week 2: Revocation: CRL and OCSP

Day 10: CRL Verification

Phase 2 · June 26, 2026

← Week 2: Revocation: CRL and OCSP

Agenda (2–3 hours)

  • Read (20 min): RFC 5280 §6.3 (CRL validation algorithm) — skim for the checks
  • Build (120 min): Implement check_crl_revocation() in validate.rs
  • Verify (30 min): Test with a revoked and a non-revoked cert
← Week 2: Revocation: CRL and OCSP

What CRL Checking Requires

Before trusting the CRL itself:

  1. CRL signature: verify with the issuing CA's public key
  2. CRL validity: thisUpdate ≤ now ≤ nextUpdate
  3. CRL issuer: CRL issuer must match the cert's issuer
  4. cRLNumber: monotonically increasing (detect rollback if caching)

Then check the revocation list:
5. Search revokedCertificates for the cert's serial number
6. If found: cert is revoked — return the reason code and revocation time

← Week 2: Revocation: CRL and OCSP

Implementing CRL Checking

pub enum RevocationStatus {
    Good,
    Revoked { reason: String, at: OffsetDateTime },
    Undetermined,  // CRL unavailable or not applicable
}

pub fn check_crl_revocation(
    cert: &X509Certificate,
    issuer: &X509Certificate,
    crl_der: &[u8],
    now: OffsetDateTime,
) -> anyhow::Result<RevocationStatus> {
    let (_, crl) = CertificateRevocationList::from_der(crl_der)?;

    // 1. Verify CRL signature against issuer public key
    crl.verify_signature(issuer.public_key())
        .map_err(|e| anyhow::anyhow!("CRL signature invalid: {e:?}"))?;

    // 2. Check CRL validity
    // 3. Check issuer DN matches
    // 4. Search for cert's serial
}
← Week 2: Revocation: CRL and OCSP

Fetching the CRL

In production, the CRL URL comes from the cert's CDP extension.
For toy-pki the CRL is local, but add a fetch path for completeness:

pub fn fetch_crl(cert: &X509Certificate) -> anyhow::Result<Vec<u8>> {
    // Extract CDP URL from cert
    let cdp_ext = cert.get_extension_unique(&OID_X509_EXT_CRL_DISTRIBUTION_POINTS)?
        .ok_or_else(|| anyhow::anyhow!("no CDP extension"))?;

    // For local toy-pki: check if URL is file:// or localhost
    // and serve from disk. For real URL: use reqwest.
    let url = extract_cdp_uri(cdp_ext)?;
    if url.starts_with("http://localhost") {
        // Read from local path mapped to URL
        read_local_crl(&url)
    } else {
        reqwest::blocking::get(&url)?.bytes().map(|b| b.to_vec())
    }
}
← Week 2: Revocation: CRL and OCSP

Soft-fail vs Hard-fail

Two schools of thought when CRL is unavailable:

Soft-fail (historical browser default):

  • If CRL can't be fetched → treat cert as valid
  • Rationale: don't break things when CA infrastructure is down
  • Risk: attacker takes down CRL server, revoked certs become "valid" again

Hard-fail (preferred for API/provisioning services):

  • If CRL can't be fetched → treat cert as untrusted
  • Rationale: availability is less important than security
  • For your team: use hard-fail; provisioning services aren't user-facing browsers

Implement as a configurable policy: RevocationPolicy::HardFail | SoftFail.

← Week 2: Revocation: CRL and OCSP

Challenge Assignment

Implement check_crl_revocation() with all checks from slide 2.

Write tests:

  1. test_crl_not_revoked: cert not in CRL → RevocationStatus::Good
  2. test_crl_revoked: cert in CRL → RevocationStatus::Revoked with correct reason
  3. test_crl_expired: now is past nextUpdate → error
  4. test_crl_wrong_issuer: CRL signed by a different CA → error
  5. test_crl_signature_tampered: flip a byte in the CRL DER → signature error

Then: add check_crl_revocation as an optional step in validate_chain(),
controlled by a RevocationPolicy parameter.

← Week 2: Revocation: CRL and OCSP

Resources

  • RFC 5280 §6.3: CRL validation algorithm
  • x509-parser CertificateRevocationList::verify_signature()
  • x509-parser get_extension_unique + OID_X509_EXT_CRL_DISTRIBUTION_POINTS