mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:01:30 +00:00
a0b7f7da9d
Phase 2 of the CRL/OCSP responder bundle. Stops signing OCSP responses
with the CA private key directly; the local issuer now bootstraps a
dedicated responder cert + key per issuer, persists them, and rotates
within a grace window before expiry.
Why this matters:
- Every relying-party OCSP poll today triggers a CA-key signing op.
With this change those polls hit a cheap responder key; the CA key
only signs at responder bootstrap / rotation (rare).
- When the CA key lives on an HSM (PKCS#11 driver, V3-Pro item 3),
the dedicated responder removes the per-poll-HSM-op pressure.
- Carries id-pkix-ocsp-nocheck (RFC 6960 §4.2.2.2.1) so OCSP clients
do NOT recursively check the responder cert's revocation status.
What landed:
* migration 000020_ocsp_responder.up.sql (+down) — ocsp_responders table
keyed by issuer_id; rotated_from records the prior cert serial for
audit; not_after index drives the rotation scheduler query
* internal/domain/ocsp_responder.go — OCSPResponder type + NeedsRotation
helper (configurable grace window; default 7 days before expiry)
* internal/repository/postgres/ocsp_responder.go — Postgres impl with
upsert-on-Put + ListExpiring for the future rotation scheduler
* internal/repository/interfaces.go — OCSPResponderRepository interface
* internal/connector/issuer/local/ocsp_responder.go — bootstrap +
rotation logic; under c.mu so concurrent first-call OCSP requests
don't double-bootstrap; recovers gracefully from corrupt key ref
or corrupt cert PEM rather than failing the OCSP request
* internal/connector/issuer/local/local.go:
- Connector struct gains optional dependencies (ocspResponderRepo,
signerDriver, issuerID, rotation grace, validity, key dir)
- Set*() helpers for each dep matching the existing SCEPService
pattern (SetProfileRepo / SetProfileID)
- SignOCSPResponse refactored: ensureOCSPResponder dispatches on
whether deps are wired; fallback path (deps unset) preserves
pre-Phase-2 behavior of signing with CA key directly
* internal/connector/issuer/local/ocsp_responder_test.go — bootstrap
happy path; reuse-across-calls; fallback (no deps wired); rotation
on grace window; corrupt-key-ref recovery; corrupt-cert-PEM recovery;
SetOCSPResponderKeyDir setter
Coverage: local issuer 86.3% (above CI floor of 86; was 86.5% before
Phase 2 added ~140 LoC of new code). The recovered-from-drop tests are
real behavior tests of the new error paths I introduced, not
coverage-game artifacts.
Backward compat: unchanged for any caller that doesn't wire the
responder deps. The factory at internal/connector/issuerfactory/factory.go
still calls local.New(&cfg, logger) with no responder wiring; OCSP
responses continue to be signed by the CA key directly until the
operator wires the deps. cmd/server/main.go wiring lands in Phase 3
alongside the CRL cache service.
45 lines
2.3 KiB
SQL
45 lines
2.3 KiB
SQL
-- 000020_ocsp_responder.up.sql
|
|
--
|
|
-- Per-issuer OCSP responder cert + key tracking. Phase 2 of the
|
|
-- CRL/OCSP responder bundle.
|
|
--
|
|
-- WHY: RFC 6960 §2.6 + §4.2.2.2 strongly recommend that OCSP
|
|
-- responses be signed by a dedicated "OCSP responder cert" issued by
|
|
-- the CA, NOT by the CA's own private key. Signing OCSP with the CA
|
|
-- key directly means every relying-party OCSP fetch triggers a CA-key
|
|
-- signing operation — a problem when the CA key lives on an HSM
|
|
-- (every OCSP poll = HSM op = HSM-rate-limit risk + audit-volume
|
|
-- pressure) and a security smell otherwise (broader exposure surface
|
|
-- for the CA private key).
|
|
--
|
|
-- This table tracks one responder cert per issuer. The bootstrap
|
|
-- happens on first OCSP request (or at server startup if the row
|
|
-- doesn't exist) and rotates automatically when the responder cert
|
|
-- enters its 7-day-before-expiry window.
|
|
--
|
|
-- The responder cert MUST carry the id-pkix-ocsp-nocheck extension
|
|
-- (RFC 6960 §4.2.2.2.1) so OCSP clients don't recursively check the
|
|
-- responder cert's own revocation status.
|
|
--
|
|
-- Idempotent. Schema design: composite PK (issuer_id, cert_serial)
|
|
-- would let us track historical responder certs across rotations,
|
|
-- but operators don't need the history — only the current cert is
|
|
-- ever queried. PK on issuer_id alone, replace-on-rotate via UPSERT.
|
|
|
|
CREATE TABLE IF NOT EXISTS ocsp_responders (
|
|
issuer_id TEXT PRIMARY KEY REFERENCES issuers(id) ON DELETE CASCADE,
|
|
cert_pem TEXT NOT NULL, -- PEM-encoded responder cert
|
|
cert_serial TEXT NOT NULL, -- hex serial for ops grep / audit
|
|
key_path TEXT NOT NULL, -- filesystem path to the responder key (FileDriver) or driver-specific ref
|
|
key_alg TEXT NOT NULL, -- 'ECDSA-P256', 'RSA-2048', ... matches signer.Algorithm enum
|
|
not_before TIMESTAMPTZ NOT NULL,
|
|
not_after TIMESTAMPTZ NOT NULL,
|
|
rotated_from TEXT, -- previous cert_serial when rotation happens (NULL on first bootstrap)
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- Lets the rotation scheduler quickly find responders whose cert is
|
|
-- entering the 7-day-before-expiry window.
|
|
CREATE INDEX IF NOT EXISTS idx_ocsp_responders_not_after ON ocsp_responders(not_after);
|