← Week 2: Revocation: CRL and OCSP

Day 13: OCSP HTTP Responder with Axum

Phase 2 · June 29, 2026

← Week 2: Revocation: CRL and OCSP

Agenda (2–3 hours)

  • Read (20 min): RFC 6960 §A.1 (HTTP transport); Axum routing and handler docs
  • Build (120 min): Implement serve_ocsp command in main.rs / revoke.rs
  • Verify (30 min): Test with openssl s_client -status pointing at localhost
← Week 2: Revocation: CRL and OCSP

HTTP OCSP Endpoint (RFC 6960 §A.1)

POST /ocsp
Content-Type: application/ocsp-request
→ binary DER body

HTTP 200 OK
Content-Type: application/ocsp-response
→ binary DER body

GET form (optional):

GET /ocsp/<base64url(DER)>

Your Axum server needs one route: POST /ocsp.

← Week 2: Revocation: CRL and OCSP

Axum Handler

use axum::{Router, routing::post, extract::State, body::Bytes, http::{StatusCode, header}};
use std::sync::Arc;

#[derive(Clone)]
pub struct OcspState {
    pub cert_store: Arc<Mutex<CertStore>>,
    pub crl_store:  Arc<Mutex<CrlStore>>,
    pub issuer_ca:  Arc<Ca>,
}

async fn ocsp_handler(
    State(state): State<OcspState>,
    body: Bytes,
) -> impl IntoResponse {
    match handle_ocsp_request(&state, &body) {
        Ok(resp_der) => (
            StatusCode::OK,
            [(header::CONTENT_TYPE, "application/ocsp-response")],
            resp_der,
        ).into_response(),
        Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
    }
}
← Week 2: Revocation: CRL and OCSP

Request Handling Logic

fn handle_ocsp_request(state: &OcspState, req_der: &[u8]) -> anyhow::Result<Vec<u8>> {
    // 1. Parse the OCSPRequest (Day 11)
    let cert_ids = parse_ocsp_request(req_der)?;

    // 2. For each CertID, look up status in the CrlStore
    let responses: Vec<(CertId, RevocationStatus)> = cert_ids
        .into_iter()
        .map(|id| {
            let status = state.crl_store.lock().unwrap().status_for(&id.serial_hex);
            (id, status)
        })
        .collect();

    // 3. Build and return a BasicOCSPResponse (Day 12)
    build_ocsp_response_multi(&responses, &state.issuer_ca)
}
← Week 2: Revocation: CRL and OCSP

Starting the Server

pub async fn serve_ocsp(port: u16, state: OcspState) -> anyhow::Result<()> {
    let app = Router::new()
        .route("/ocsp", post(ocsp_handler))
        .with_state(state);

    let listener = tokio::net::TcpListener::bind(
        format!("127.0.0.1:{port}")
    ).await?;

    println!("OCSP responder listening on http://127.0.0.1:{port}/ocsp");
    axum::serve(listener, app).await?;
    Ok(())
}
← Week 2: Revocation: CRL and OCSP

Testing with openssl

# Start your OCSP responder
cargo run -- serve-ocsp --port 8080 &

# Issue a cert and test OCSP for it
openssl ocsp \
  -issuer intermediate/ca.crt \
  -cert issued/0001.pem \
  -url http://localhost:8080/ocsp \
  -text

# Revoke it and test again
cargo run -- revoke 0001 --reason keyCompromise
openssl ocsp -issuer intermediate/ca.crt -cert issued/0001.pem \
  -url http://localhost:8080/ocsp -text
← Week 2: Revocation: CRL and OCSP

Challenge Assignment

Wire the OCSP handler into the Axum server and test the full flow:

  1. Start the OCSP responder
  2. Issue 3 certs
  3. Revoke cert 2
  4. Query all three via openssl ocsp — assert cert 1 and 3 are "good", cert 2 is "revoked"
  5. Write a Rust integration test using reqwest to send OCSP requests programmatically
    and parse the responses with x509-parser's OCSP support
← Week 2: Revocation: CRL and OCSP

Resources

  • RFC 6960 §A.1: HTTP transport for OCSP
  • Axum docs: docs.rs/axum — Router, State, Bytes, IntoResponse
  • tokio::sync::Mutex for shared mutable state across Axum handlers
  • openssl-ocsp(1): end-to-end testing tool