mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 21:58:52 +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:
@@ -609,3 +609,13 @@ func parseDERChain(derChain [][]byte) (certPEM string, chainPEM string, serial s
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateCRL is not supported by ACME issuers.
|
||||
func (c *Connector) GenerateCRL(ctx context.Context, revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
|
||||
return nil, fmt.Errorf("ACME issuers do not support CRL generation")
|
||||
}
|
||||
|
||||
// SignOCSPResponse is not supported by ACME issuers.
|
||||
func (c *Connector) SignOCSPResponse(ctx context.Context, req issuer.OCSPSignRequest) ([]byte, error) {
|
||||
return nil, fmt.Errorf("ACME issuers do not support OCSP response signing")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package issuer
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -22,6 +23,14 @@ type Connector interface {
|
||||
|
||||
// GetOrderStatus retrieves the status of an issuance or renewal order.
|
||||
GetOrderStatus(ctx context.Context, orderID string) (*OrderStatus, error)
|
||||
|
||||
// GenerateCRL generates a DER-encoded X.509 CRL signed by this issuer.
|
||||
// Returns nil if the issuer does not support CRL generation (e.g., ACME).
|
||||
GenerateCRL(ctx context.Context, revokedCerts []RevokedCertEntry) ([]byte, error)
|
||||
|
||||
// SignOCSPResponse signs an OCSP response for the given certificate serial.
|
||||
// Returns nil if the issuer does not support OCSP (e.g., ACME).
|
||||
SignOCSPResponse(ctx context.Context, req OCSPSignRequest) ([]byte, error)
|
||||
}
|
||||
|
||||
// IssuanceRequest contains the parameters for issuing a new certificate.
|
||||
@@ -67,3 +76,20 @@ type OrderStatus struct {
|
||||
NotAfter *time.Time `json:"not_after,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// RevokedCertEntry represents a revoked certificate for CRL generation.
|
||||
type RevokedCertEntry struct {
|
||||
SerialNumber *big.Int
|
||||
RevokedAt time.Time
|
||||
ReasonCode int
|
||||
}
|
||||
|
||||
// OCSPSignRequest contains the parameters for signing an OCSP response.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -542,3 +542,364 @@ func generateTestCSR(commonName string) (*x509.CertificateRequest, string, error
|
||||
|
||||
return csr, string(csrPEM), nil
|
||||
}
|
||||
|
||||
// M15b: CRL and OCSP Tests
|
||||
|
||||
func TestGenerateCRL_Empty(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
config := &local.Config{
|
||||
CACommonName: "Test CA",
|
||||
ValidityDays: 30,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
// Generate CRL with no revoked certs — should succeed with 0 entries
|
||||
crl, err := connector.GenerateCRL(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCRL failed: %v", err)
|
||||
}
|
||||
|
||||
if crl == nil {
|
||||
t.Fatal("CRL is nil")
|
||||
}
|
||||
|
||||
// Verify it's valid DER by parsing
|
||||
parsedCRL, err := x509.ParseRevocationList(crl)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse CRL: %v", err)
|
||||
}
|
||||
|
||||
if len(parsedCRL.RevokedCertificateEntries) != 0 {
|
||||
t.Errorf("expected 0 revoked entries, got %d", len(parsedCRL.RevokedCertificateEntries))
|
||||
}
|
||||
|
||||
t.Logf("Empty CRL generated successfully with %d entries", len(parsedCRL.RevokedCertificateEntries))
|
||||
}
|
||||
|
||||
func TestGenerateCRL_WithEntries(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
config := &local.Config{
|
||||
CACommonName: "Test CA",
|
||||
ValidityDays: 30,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
// Generate CRL with 2 revoked certs
|
||||
entries := []issuer.RevokedCertEntry{
|
||||
{SerialNumber: big.NewInt(12345), RevokedAt: time.Now().Add(-24 * time.Hour), ReasonCode: 1},
|
||||
{SerialNumber: big.NewInt(67890), RevokedAt: time.Now().Add(-1 * time.Hour), ReasonCode: 4},
|
||||
}
|
||||
|
||||
crl, err := connector.GenerateCRL(ctx, entries)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCRL failed: %v", err)
|
||||
}
|
||||
|
||||
if crl == nil {
|
||||
t.Fatal("CRL is nil")
|
||||
}
|
||||
|
||||
parsedCRL, err := x509.ParseRevocationList(crl)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse CRL: %v", err)
|
||||
}
|
||||
|
||||
if len(parsedCRL.RevokedCertificateEntries) != 2 {
|
||||
t.Errorf("expected 2 revoked entries, got %d", len(parsedCRL.RevokedCertificateEntries))
|
||||
}
|
||||
|
||||
// Verify entries contain expected serials
|
||||
serials := make(map[string]bool)
|
||||
for _, entry := range parsedCRL.RevokedCertificateEntries {
|
||||
serials[entry.SerialNumber.String()] = true
|
||||
}
|
||||
|
||||
if !serials["12345"] {
|
||||
t.Error("expected serial 12345 in CRL")
|
||||
}
|
||||
if !serials["67890"] {
|
||||
t.Error("expected serial 67890 in CRL")
|
||||
}
|
||||
|
||||
t.Logf("CRL with entries generated successfully: %d entries", len(parsedCRL.RevokedCertificateEntries))
|
||||
}
|
||||
|
||||
func TestGenerateCRL_BeforeCAInit(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
// CRL generation should init the CA automatically
|
||||
cfg := &local.Config{ValidityDays: 90}
|
||||
connector := local.New(cfg, logger)
|
||||
|
||||
crl, err := connector.GenerateCRL(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCRL failed: %v", err)
|
||||
}
|
||||
|
||||
if crl == nil {
|
||||
t.Fatal("CRL is nil")
|
||||
}
|
||||
|
||||
// Verify it's valid
|
||||
_, err = x509.ParseRevocationList(crl)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse CRL: %v", err)
|
||||
}
|
||||
|
||||
t.Log("CRL generated with auto-initialized CA")
|
||||
}
|
||||
|
||||
func TestGenerateCRL_WithReasonCodes(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
config := &local.Config{
|
||||
CACommonName: "Test CA",
|
||||
ValidityDays: 30,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
// Test all RFC 5280 reason codes
|
||||
entries := []issuer.RevokedCertEntry{
|
||||
{SerialNumber: big.NewInt(100), RevokedAt: time.Now(), ReasonCode: 0}, // unspecified
|
||||
{SerialNumber: big.NewInt(101), RevokedAt: time.Now(), ReasonCode: 1}, // keyCompromise
|
||||
{SerialNumber: big.NewInt(102), RevokedAt: time.Now(), ReasonCode: 2}, // caCompromise
|
||||
{SerialNumber: big.NewInt(103), RevokedAt: time.Now(), ReasonCode: 3}, // affiliationChanged
|
||||
{SerialNumber: big.NewInt(104), RevokedAt: time.Now(), ReasonCode: 4}, // superseded
|
||||
}
|
||||
|
||||
crl, err := connector.GenerateCRL(ctx, entries)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCRL failed: %v", err)
|
||||
}
|
||||
|
||||
parsedCRL, err := x509.ParseRevocationList(crl)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse CRL: %v", err)
|
||||
}
|
||||
|
||||
if len(parsedCRL.RevokedCertificateEntries) != 5 {
|
||||
t.Errorf("expected 5 revoked entries, got %d", len(parsedCRL.RevokedCertificateEntries))
|
||||
}
|
||||
|
||||
// Verify reason codes are preserved
|
||||
reasonCount := 0
|
||||
for _, entry := range parsedCRL.RevokedCertificateEntries {
|
||||
if entry.ReasonCode >= 0 {
|
||||
reasonCount++
|
||||
}
|
||||
}
|
||||
if reasonCount != 5 {
|
||||
t.Errorf("expected all 5 entries to have reason codes, got %d", reasonCount)
|
||||
}
|
||||
|
||||
t.Logf("CRL with %d reason codes generated successfully", reasonCount)
|
||||
}
|
||||
|
||||
func TestSignOCSPResponse_Good(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
config := &local.Config{
|
||||
CACommonName: "Test CA",
|
||||
ValidityDays: 30,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
now := time.Now()
|
||||
resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
|
||||
CertSerial: big.NewInt(12345),
|
||||
CertStatus: 0, // good
|
||||
ThisUpdate: now,
|
||||
NextUpdate: now.Add(1 * time.Hour),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SignOCSPResponse failed: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("OCSP response is nil")
|
||||
}
|
||||
|
||||
if len(resp) == 0 {
|
||||
t.Fatal("OCSP response is empty")
|
||||
}
|
||||
|
||||
t.Logf("OCSP response for good cert generated: %d bytes", len(resp))
|
||||
}
|
||||
|
||||
func TestSignOCSPResponse_Revoked(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
config := &local.Config{
|
||||
CACommonName: "Test CA",
|
||||
ValidityDays: 30,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
now := time.Now()
|
||||
revokedAt := now.Add(-24 * time.Hour)
|
||||
|
||||
resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
|
||||
CertSerial: big.NewInt(12345),
|
||||
CertStatus: 1, // revoked
|
||||
RevokedAt: revokedAt,
|
||||
RevocationReason: 1, // keyCompromise
|
||||
ThisUpdate: now,
|
||||
NextUpdate: now.Add(1 * time.Hour),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SignOCSPResponse failed: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("OCSP response is nil")
|
||||
}
|
||||
|
||||
if len(resp) == 0 {
|
||||
t.Fatal("OCSP response is empty")
|
||||
}
|
||||
|
||||
t.Logf("OCSP response for revoked cert generated: %d bytes", len(resp))
|
||||
}
|
||||
|
||||
func TestSignOCSPResponse_Unknown(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
config := &local.Config{
|
||||
CACommonName: "Test CA",
|
||||
ValidityDays: 30,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
now := time.Now()
|
||||
resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
|
||||
CertSerial: big.NewInt(12345),
|
||||
CertStatus: 2, // unknown
|
||||
ThisUpdate: now,
|
||||
NextUpdate: now.Add(1 * time.Hour),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SignOCSPResponse failed: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("OCSP response is nil")
|
||||
}
|
||||
|
||||
if len(resp) == 0 {
|
||||
t.Fatal("OCSP response is empty")
|
||||
}
|
||||
|
||||
t.Logf("OCSP response for unknown cert generated: %d bytes", len(resp))
|
||||
}
|
||||
|
||||
func TestSignOCSPResponse_BeforeCAInit(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
cfg := &local.Config{ValidityDays: 90}
|
||||
connector := local.New(cfg, logger)
|
||||
|
||||
now := time.Now()
|
||||
resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
|
||||
CertSerial: big.NewInt(999),
|
||||
CertStatus: 0,
|
||||
ThisUpdate: now,
|
||||
NextUpdate: now.Add(1 * time.Hour),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SignOCSPResponse failed: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil || len(resp) == 0 {
|
||||
t.Fatal("OCSP response is nil or empty")
|
||||
}
|
||||
|
||||
t.Log("OCSP response generated with auto-initialized CA")
|
||||
}
|
||||
|
||||
func TestGenerateCRL_SubCA(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
certPath, keyPath := generateTestSubCA(t, "rsa")
|
||||
defer os.Remove(certPath)
|
||||
defer os.Remove(keyPath)
|
||||
|
||||
config := &local.Config{
|
||||
ValidityDays: 30,
|
||||
CACertPath: certPath,
|
||||
CAKeyPath: keyPath,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
entries := []issuer.RevokedCertEntry{
|
||||
{SerialNumber: big.NewInt(555), RevokedAt: time.Now().Add(-12 * time.Hour), ReasonCode: 2},
|
||||
}
|
||||
|
||||
crl, err := connector.GenerateCRL(ctx, entries)
|
||||
if err != nil {
|
||||
t.Fatalf("SubCA GenerateCRL failed: %v", err)
|
||||
}
|
||||
|
||||
if crl == nil {
|
||||
t.Fatal("CRL is nil")
|
||||
}
|
||||
|
||||
parsedCRL, err := x509.ParseRevocationList(crl)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse SubCA CRL: %v", err)
|
||||
}
|
||||
|
||||
if len(parsedCRL.RevokedCertificateEntries) != 1 {
|
||||
t.Errorf("expected 1 entry in SubCA CRL, got %d", len(parsedCRL.RevokedCertificateEntries))
|
||||
}
|
||||
|
||||
t.Log("SubCA CRL generated successfully")
|
||||
}
|
||||
|
||||
func TestSignOCSPResponse_SubCA(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
certPath, keyPath := generateTestSubCA(t, "ecdsa")
|
||||
defer os.Remove(certPath)
|
||||
defer os.Remove(keyPath)
|
||||
|
||||
config := &local.Config{
|
||||
ValidityDays: 30,
|
||||
CACertPath: certPath,
|
||||
CAKeyPath: keyPath,
|
||||
}
|
||||
connector := local.New(config, logger)
|
||||
|
||||
now := time.Now()
|
||||
resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
|
||||
CertSerial: big.NewInt(777),
|
||||
CertStatus: 0,
|
||||
ThisUpdate: now,
|
||||
NextUpdate: now.Add(1 * time.Hour),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SubCA SignOCSPResponse failed: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil || len(resp) == 0 {
|
||||
t.Fatal("SubCA OCSP response is nil or empty")
|
||||
}
|
||||
|
||||
t.Log("SubCA OCSP response generated successfully")
|
||||
}
|
||||
|
||||
@@ -457,5 +457,15 @@ func parseSignResponse(respBody []byte) (certPEM string, chainPEM string, serial
|
||||
return certPEM, chainPEM, serial, notBefore, notAfter, nil
|
||||
}
|
||||
|
||||
// GenerateCRL is not supported by step-ca as step-ca provides its own CRL endpoint.
|
||||
func (c *Connector) GenerateCRL(ctx context.Context, revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
|
||||
return nil, fmt.Errorf("step-ca provides its own CRL endpoint; use step-ca's /crl directly")
|
||||
}
|
||||
|
||||
// SignOCSPResponse is not supported by step-ca as step-ca provides its own OCSP responder.
|
||||
func (c *Connector) SignOCSPResponse(ctx context.Context, req issuer.OCSPSignRequest) ([]byte, error) {
|
||||
return nil, fmt.Errorf("step-ca provides its own OCSP responder; use step-ca's /ocsp directly")
|
||||
}
|
||||
|
||||
// Ensure Connector implements the issuer.Connector interface.
|
||||
var _ issuer.Connector = (*Connector)(nil)
|
||||
|
||||
Reference in New Issue
Block a user