feat: M15b — OCSP responder, DER CRL, short-lived exemption, revocation GUI

Backend:
- Embedded OCSP responder: GET /api/v1/ocsp/{issuer_id}/{serial} returns
  signed OCSP responses (good/revoked/unknown) using CA key
- DER-encoded X.509 CRL: GET /api/v1/crl/{issuer_id} returns proper DER CRL
  signed by issuing CA with 24h validity window
- Short-lived cert exemption: certs with profile TTL < 1 hour skip CRL/OCSP
  (expiry is sufficient revocation for ephemeral workloads)
- Extended issuer connector interface with GenerateCRL and SignOCSPResponse
- Local CA implements full CRL/OCSP signing; ACME and step-ca return
  appropriate "use native endpoint" errors
- IssuerConnectorAdapter bridges new methods between layers

Frontend:
- Revoke button on certificate detail page with RFC 5280 reason modal
- Revocation banner with reason display and timestamp
- Revocation status indicators in lifecycle section
- "Revoked" filter option in certificates list
- API client: revokeCertificate() function and Certificate type extensions

Tests: ~31 new tests across connector, service, handler, and adapter layers
Docs: milestones renumbered (M13-M14, M16-M18), M15b marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shankar
2026-03-22 14:39:10 -04:00
parent c2cd48c38b
commit 458a8c2740
22 changed files with 1470 additions and 15 deletions
+22
View File
@@ -11,6 +11,7 @@ import (
"encoding/pem"
"fmt"
"log/slog"
"math/big"
"time"
"github.com/shankar0123/certctl/internal/domain"
@@ -39,6 +40,10 @@ type IssuerConnector interface {
RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error)
// RevokeCertificate revokes a certificate by serial number with an optional reason.
RevokeCertificate(ctx context.Context, serial string, reason string) error
// GenerateCRL generates a DER-encoded X.509 CRL from the given revocation entries.
GenerateCRL(ctx context.Context, revokedCerts []CRLEntry) ([]byte, error)
// SignOCSPResponse signs an OCSP response for the given certificate serial.
SignOCSPResponse(ctx context.Context, req OCSPSignRequest) ([]byte, error)
}
// IssuanceResult holds the result of a certificate issuance or renewal operation.
@@ -50,6 +55,23 @@ type IssuanceResult struct {
NotAfter time.Time
}
// CRLEntry represents a revoked certificate for CRL generation.
type CRLEntry struct {
SerialNumber *big.Int
RevokedAt time.Time
ReasonCode int
}
// OCSPSignRequest contains the parameters for OCSP response signing.
type OCSPSignRequest struct {
CertSerial *big.Int
CertStatus int // 0=good, 1=revoked, 2=unknown
RevokedAt time.Time
RevocationReason int
ThisUpdate time.Time
NextUpdate time.Time
}
// NewRenewalService creates a new renewal service.
func NewRenewalService(
certRepo repository.CertificateRepository,