Files
certctl/internal/service/issuer_adapter.go
T
shankar0123 762c523d59 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>
2026-03-22 14:39:10 -04:00

98 lines
3.3 KiB
Go

package service
import (
"context"
"github.com/shankar0123/certctl/internal/connector/issuer"
)
// IssuerConnectorAdapter bridges the connector-layer issuer.Connector interface with the
// service-layer IssuerConnector interface. This maintains dependency inversion: the service
// layer defines the interface it needs, and this adapter wraps the concrete connector.
type IssuerConnectorAdapter struct {
connector issuer.Connector
}
// NewIssuerConnectorAdapter wraps an issuer.Connector to implement service.IssuerConnector.
func NewIssuerConnectorAdapter(c issuer.Connector) IssuerConnector {
return &IssuerConnectorAdapter{connector: c}
}
// IssueCertificate delegates to the underlying connector's IssueCertificate method,
// translating between service-layer and connector-layer types.
func (a *IssuerConnectorAdapter) IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error) {
result, err := a.connector.IssueCertificate(ctx, issuer.IssuanceRequest{
CommonName: commonName,
SANs: sans,
CSRPEM: csrPEM,
})
if err != nil {
return nil, err
}
return &IssuanceResult{
CertPEM: result.CertPEM,
ChainPEM: result.ChainPEM,
Serial: result.Serial,
NotBefore: result.NotBefore,
NotAfter: result.NotAfter,
}, nil
}
// RenewCertificate delegates to the underlying connector's RenewCertificate method,
// translating between service-layer and connector-layer types.
func (a *IssuerConnectorAdapter) RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error) {
result, err := a.connector.RenewCertificate(ctx, issuer.RenewalRequest{
CommonName: commonName,
SANs: sans,
CSRPEM: csrPEM,
})
if err != nil {
return nil, err
}
return &IssuanceResult{
CertPEM: result.CertPEM,
ChainPEM: result.ChainPEM,
Serial: result.Serial,
NotBefore: result.NotBefore,
NotAfter: result.NotAfter,
}, nil
}
// RevokeCertificate delegates to the underlying connector's RevokeCertificate method.
func (a *IssuerConnectorAdapter) RevokeCertificate(ctx context.Context, serial string, reason string) error {
var reasonPtr *string
if reason != "" {
reasonPtr = &reason
}
return a.connector.RevokeCertificate(ctx, issuer.RevocationRequest{
Serial: serial,
Reason: reasonPtr,
})
}
// GenerateCRL delegates to the underlying connector.
func (a *IssuerConnectorAdapter) GenerateCRL(ctx context.Context, entries []CRLEntry) ([]byte, error) {
// Convert service-layer CRLEntry to connector-layer RevokedCertEntry
connEntries := make([]issuer.RevokedCertEntry, len(entries))
for i, e := range entries {
connEntries[i] = issuer.RevokedCertEntry{
SerialNumber: e.SerialNumber,
RevokedAt: e.RevokedAt,
ReasonCode: e.ReasonCode,
}
}
return a.connector.GenerateCRL(ctx, connEntries)
}
// SignOCSPResponse delegates to the underlying connector.
func (a *IssuerConnectorAdapter) SignOCSPResponse(ctx context.Context, req OCSPSignRequest) ([]byte, error) {
return a.connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
CertSerial: req.CertSerial,
CertStatus: req.CertStatus,
RevokedAt: req.RevokedAt,
RevocationReason: req.RevocationReason,
ThisUpdate: req.ThisUpdate,
NextUpdate: req.NextUpdate,
})
}