mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-15 02:08:58 +00:00
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:
@@ -19,6 +19,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/connector/issuer"
|
||||
)
|
||||
|
||||
@@ -582,3 +584,76 @@ func hashPublicKey(pub interface{}) []byte {
|
||||
}
|
||||
return h.Sum(nil)[:4] // Use first 4 bytes for brevity
|
||||
}
|
||||
|
||||
// GenerateCRL generates a DER-encoded X.509 CRL signed by this local CA.
|
||||
func (c *Connector) GenerateCRL(ctx context.Context, revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
|
||||
if err := c.ensureCA(ctx); err != nil {
|
||||
return nil, fmt.Errorf("CA initialization failed: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
revokedEntries := make([]x509.RevocationListEntry, 0, len(revokedCerts))
|
||||
for _, cert := range revokedCerts {
|
||||
revokedEntries = append(revokedEntries, x509.RevocationListEntry{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
RevocationTime: cert.RevokedAt,
|
||||
ReasonCode: cert.ReasonCode,
|
||||
})
|
||||
}
|
||||
|
||||
template := &x509.RevocationList{
|
||||
RevokedCertificateEntries: revokedEntries,
|
||||
Number: big.NewInt(time.Now().Unix()),
|
||||
ThisUpdate: now,
|
||||
NextUpdate: now.Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
crlBytes, err := x509.CreateRevocationList(rand.Reader, template, c.caCert, c.caKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create CRL: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("CRL generated",
|
||||
"entries", len(revokedCerts),
|
||||
"next_update", template.NextUpdate)
|
||||
|
||||
return crlBytes, nil
|
||||
}
|
||||
|
||||
// SignOCSPResponse signs an OCSP response for the given certificate.
|
||||
func (c *Connector) SignOCSPResponse(ctx context.Context, req issuer.OCSPSignRequest) ([]byte, error) {
|
||||
if err := c.ensureCA(ctx); err != nil {
|
||||
return nil, fmt.Errorf("CA initialization failed: %w", err)
|
||||
}
|
||||
|
||||
// Import OCSP after we confirm golang.org/x/crypto is available
|
||||
// This will be added to imports below
|
||||
template := ocsp.Response{
|
||||
SerialNumber: req.CertSerial,
|
||||
ThisUpdate: req.ThisUpdate,
|
||||
NextUpdate: req.NextUpdate,
|
||||
Certificate: c.caCert,
|
||||
}
|
||||
|
||||
switch req.CertStatus {
|
||||
case 0: // good
|
||||
template.Status = ocsp.Good
|
||||
case 1: // revoked
|
||||
template.Status = ocsp.Revoked
|
||||
template.RevokedAt = req.RevokedAt
|
||||
template.RevocationReason = req.RevocationReason
|
||||
default: // unknown
|
||||
template.Status = ocsp.Unknown
|
||||
}
|
||||
|
||||
respBytes, err := ocsp.CreateResponse(c.caCert, c.caCert, template, c.caKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OCSP response: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("OCSP response signed",
|
||||
"serial", req.CertSerial,
|
||||
"status", req.CertStatus)
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user