feat(m27): certificate export (PEM/PKCS#12) and S/MIME EKU support

Add certificate export in PEM (JSON or file download) and PKCS#12 formats.
Private keys are never included — they stay on agents. Add EKU-aware
issuance threading profile EKUs (serverAuth, clientAuth, codeSigning,
emailProtection, timeStamping) through the full issuance pipeline. Fix
agent CSR SAN splitting for email addresses, adaptive KeyUsage flags for
S/MIME vs TLS, and a pre-existing generateID collision bug in deployment
job creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-28 16:16:19 -04:00
parent 78c7bc16b0
commit a00bb349c4
26 changed files with 1354 additions and 53 deletions
+14 -1
View File
@@ -19,6 +19,7 @@ type AgentService struct {
certRepo repository.CertificateRepository
jobRepo repository.JobRepository
targetRepo repository.TargetRepository
profileRepo repository.CertificateProfileRepository
auditService *AuditService
issuerRegistry map[string]IssuerConnector
renewalService *RenewalService
@@ -45,6 +46,11 @@ func NewAgentService(
}
}
// SetProfileRepo sets the profile repository for EKU resolution during CSR signing.
func (s *AgentService) SetProfileRepo(repo repository.CertificateProfileRepository) {
s.profileRepo = repo
}
// Register creates a new agent and returns its API key (only once).
func (s *AgentService) Register(ctx context.Context, name string, hostname string) (*domain.Agent, string, error) {
if name == "" || hostname == "" {
@@ -159,7 +165,14 @@ func (s *AgentService) SubmitCSR(ctx context.Context, agentID string, certID str
// Fallback: direct issuer signing (no AwaitingCSR job — ad-hoc CSR submission)
connector, ok := s.issuerRegistry[cert.IssuerID]
if ok {
result, err := connector.IssueCertificate(ctx, cert.CommonName, cert.SANs, string(csrPEM))
// Resolve EKUs from the certificate profile if available
var ekus []string
if cert.CertificateProfileID != "" && s.profileRepo != nil {
if profile, profileErr := s.profileRepo.Get(ctx, cert.CertificateProfileID); profileErr == nil && profile != nil {
ekus = profile.AllowedEKUs
}
}
result, err := connector.IssueCertificate(ctx, cert.CommonName, cert.SANs, string(csrPEM), ekus)
if err != nil {
return fmt.Errorf("issuer signing failed: %w", err)
}
+2 -1
View File
@@ -116,7 +116,8 @@ func (s *ESTService) processEnrollment(ctx context.Context, csrPEM string, audit
"issuer", s.issuerID)
// Issue the certificate via the configured issuer connector
result, err := s.issuer.IssueCertificate(ctx, commonName, sans, csrPEM)
// EST enrollments use default EKUs (nil = serverAuth + clientAuth fallback in connector)
result, err := s.issuer.IssueCertificate(ctx, commonName, sans, csrPEM, nil)
if err != nil {
s.logger.Error("EST enrollment failed",
"action", auditAction,
+185
View File
@@ -0,0 +1,185 @@
package service
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"log/slog"
"github.com/shankar0123/certctl/internal/domain"
"github.com/shankar0123/certctl/internal/repository"
"software.sslmate.com/src/go-pkcs12"
)
// ExportService provides certificate export functionality (PEM and PKCS#12).
type ExportService struct {
certRepo repository.CertificateRepository
auditService *AuditService
}
// NewExportService creates a new export service.
func NewExportService(
certRepo repository.CertificateRepository,
auditService *AuditService,
) *ExportService {
return &ExportService{
certRepo: certRepo,
auditService: auditService,
}
}
// ExportPEMResult contains the PEM-encoded certificate chain.
type ExportPEMResult struct {
CertPEM string `json:"cert_pem"`
ChainPEM string `json:"chain_pem"`
FullPEM string `json:"full_pem"` // cert + chain concatenated
}
// ExportPEM returns the PEM-encoded certificate and chain for the latest version.
func (s *ExportService) ExportPEM(ctx context.Context, certID string) (*ExportPEMResult, error) {
// Verify certificate exists
cert, err := s.certRepo.Get(ctx, certID)
if err != nil {
return nil, fmt.Errorf("certificate not found: %w", err)
}
// Get latest version (contains the PEM chain)
version, err := s.certRepo.GetLatestVersion(ctx, certID)
if err != nil {
return nil, fmt.Errorf("no certificate version found: %w", err)
}
// Split PEM chain into leaf cert + chain
certPEM, chainPEM := splitPEMChain(version.PEMChain)
// Audit the export
if s.auditService != nil {
if auditErr := s.auditService.RecordEvent(ctx, "api", domain.ActorTypeUser,
"export_pem", "certificate", cert.ID,
map[string]interface{}{"serial": version.SerialNumber}); auditErr != nil {
slog.Error("failed to record audit event", "error", auditErr)
}
}
return &ExportPEMResult{
CertPEM: certPEM,
ChainPEM: chainPEM,
FullPEM: version.PEMChain,
}, nil
}
// ExportPKCS12 returns a PKCS#12 bundle containing the certificate chain.
// The private key is NOT included — it lives on the agent and never touches the control plane.
// The PKCS#12 bundle is encrypted with the provided password (can be empty for cert-only bundles).
func (s *ExportService) ExportPKCS12(ctx context.Context, certID string, password string) ([]byte, error) {
// Verify certificate exists
cert, err := s.certRepo.Get(ctx, certID)
if err != nil {
return nil, fmt.Errorf("certificate not found: %w", err)
}
// Get latest version
version, err := s.certRepo.GetLatestVersion(ctx, certID)
if err != nil {
return nil, fmt.Errorf("no certificate version found: %w", err)
}
// Parse PEM chain into x509.Certificate objects
certs, err := parsePEMCertificates(version.PEMChain)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate chain: %w", err)
}
if len(certs) == 0 {
return nil, fmt.Errorf("no certificates found in PEM chain")
}
// Build PKCS#12 bundle: leaf cert + CA chain (no private key)
leaf := certs[0]
var caCerts []*x509.Certificate
if len(certs) > 1 {
caCerts = certs[1:]
}
// Encode as PKCS#12 trust store (cert-only bundle, no private key)
pfxData, err := encodePKCS12CertOnly(leaf, caCerts, password)
if err != nil {
return nil, fmt.Errorf("failed to encode PKCS#12: %w", err)
}
// Audit the export
if s.auditService != nil {
if auditErr := s.auditService.RecordEvent(ctx, "api", domain.ActorTypeUser,
"export_pkcs12", "certificate", cert.ID,
map[string]interface{}{"serial": version.SerialNumber, "has_private_key": false}); auditErr != nil {
slog.Error("failed to record audit event", "error", auditErr)
}
}
return pfxData, nil
}
// encodePKCS12CertOnly creates a PKCS#12 bundle with certificate(s) but no private key.
// Uses the go-pkcs12 library's Modern encoder for strong encryption.
func encodePKCS12CertOnly(leaf *x509.Certificate, caCerts []*x509.Certificate, password string) ([]byte, error) {
// go-pkcs12's Modern.Encode expects a private key; for cert-only bundles we use
// EncodeTrustStore which stores certs as trusted entries.
// Include the leaf in the trust store alongside CA certs.
allCerts := make([]*x509.Certificate, 0, 1+len(caCerts))
allCerts = append(allCerts, leaf)
allCerts = append(allCerts, caCerts...)
return pkcs12.Modern.EncodeTrustStore(allCerts, password)
}
// splitPEMChain splits a PEM chain into the first certificate (leaf) and remaining chain.
func splitPEMChain(fullPEM string) (string, string) {
data := []byte(fullPEM)
var blocks []*pem.Block
for {
var block *pem.Block
block, data = pem.Decode(data)
if block == nil {
break
}
if block.Type == "CERTIFICATE" {
blocks = append(blocks, block)
}
}
if len(blocks) == 0 {
return fullPEM, ""
}
certPEM := string(pem.EncodeToMemory(blocks[0]))
var chainPEM string
for i := 1; i < len(blocks); i++ {
chainPEM += string(pem.EncodeToMemory(blocks[i]))
}
return certPEM, chainPEM
}
// parsePEMCertificates parses all certificates from a PEM-encoded string.
func parsePEMCertificates(pemData string) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
data := []byte(pemData)
for {
var block *pem.Block
block, data = pem.Decode(data)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
certs = append(certs, cert)
}
return certs, nil
}
+220
View File
@@ -0,0 +1,220 @@
package service
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"testing"
"time"
"github.com/shankar0123/certctl/internal/domain"
)
// generateTestCertPEM creates a self-signed test certificate PEM for export tests.
func generateTestCertPEM(t *testing.T) string {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Test Cert",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatalf("failed to create cert: %v", err)
}
return string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
}))
}
func newMockCertRepoWithVersion(certID string, cert *domain.ManagedCertificate, version *domain.CertificateVersion) *mockCertRepo {
repo := &mockCertRepo{
Certs: make(map[string]*domain.ManagedCertificate),
Versions: make(map[string][]*domain.CertificateVersion),
}
if cert != nil {
repo.Certs[certID] = cert
}
if version != nil {
repo.Versions[certID] = []*domain.CertificateVersion{version}
}
return repo
}
func TestExportPEM_Success(t *testing.T) {
certPEM := "-----BEGIN CERTIFICATE-----\nMIIBxz...\n-----END CERTIFICATE-----\n"
chainPEM := "-----BEGIN CERTIFICATE-----\nMIIByz...\n-----END CERTIFICATE-----\n"
fullPEM := certPEM + chainPEM
certRepo := newMockCertRepoWithVersion("mc-test-1",
&domain.ManagedCertificate{
ID: "mc-test-1",
CommonName: "test.example.com",
Status: domain.CertificateStatusActive,
},
&domain.CertificateVersion{
ID: "cv-1",
CertificateID: "mc-test-1",
SerialNumber: "abc123",
PEMChain: fullPEM,
},
)
auditSvc := &AuditService{auditRepo: &mockAuditRepo{}}
svc := NewExportService(certRepo, auditSvc)
result, err := svc.ExportPEM(context.Background(), "mc-test-1")
if err != nil {
t.Fatalf("ExportPEM failed: %v", err)
}
if result.FullPEM == "" {
t.Error("expected non-empty FullPEM")
}
if result.CertPEM == "" {
t.Error("expected non-empty CertPEM")
}
}
func TestExportPEM_CertNotFound(t *testing.T) {
certRepo := &mockCertRepo{
Certs: make(map[string]*domain.ManagedCertificate),
Versions: make(map[string][]*domain.CertificateVersion),
}
svc := NewExportService(certRepo, nil)
_, err := svc.ExportPEM(context.Background(), "nonexistent")
if err == nil {
t.Fatal("expected error for nonexistent certificate")
}
}
func TestExportPEM_NoVersion(t *testing.T) {
certRepo := newMockCertRepoWithVersion("mc-test-1",
&domain.ManagedCertificate{
ID: "mc-test-1",
CommonName: "test.example.com",
},
nil, // no version
)
svc := NewExportService(certRepo, nil)
_, err := svc.ExportPEM(context.Background(), "mc-test-1")
if err == nil {
t.Fatal("expected error when no version exists")
}
}
func TestExportPKCS12_Success(t *testing.T) {
testCertPEM := generateTestCertPEM(t)
certRepo := newMockCertRepoWithVersion("mc-test-1",
&domain.ManagedCertificate{
ID: "mc-test-1",
CommonName: "test.example.com",
Status: domain.CertificateStatusActive,
},
&domain.CertificateVersion{
ID: "cv-1",
CertificateID: "mc-test-1",
SerialNumber: "abc123",
PEMChain: testCertPEM,
},
)
auditSvc := &AuditService{auditRepo: &mockAuditRepo{}}
svc := NewExportService(certRepo, auditSvc)
pfxData, err := svc.ExportPKCS12(context.Background(), "mc-test-1", "testpass")
if err != nil {
t.Fatalf("ExportPKCS12 failed: %v", err)
}
if len(pfxData) == 0 {
t.Error("expected non-empty PKCS#12 data")
}
}
func TestExportPKCS12_EmptyPassword(t *testing.T) {
testCertPEM := generateTestCertPEM(t)
certRepo := newMockCertRepoWithVersion("mc-test-1",
&domain.ManagedCertificate{ID: "mc-test-1"},
&domain.CertificateVersion{
ID: "cv-1",
CertificateID: "mc-test-1",
PEMChain: testCertPEM,
},
)
svc := NewExportService(certRepo, nil)
pfxData, err := svc.ExportPKCS12(context.Background(), "mc-test-1", "")
if err != nil {
t.Fatalf("ExportPKCS12 with empty password failed: %v", err)
}
if len(pfxData) == 0 {
t.Error("expected non-empty PKCS#12 data")
}
}
func TestExportPKCS12_CertNotFound(t *testing.T) {
certRepo := &mockCertRepo{
Certs: make(map[string]*domain.ManagedCertificate),
Versions: make(map[string][]*domain.CertificateVersion),
}
svc := NewExportService(certRepo, nil)
_, err := svc.ExportPKCS12(context.Background(), "nonexistent", "pass")
if err == nil {
t.Fatal("expected error for nonexistent certificate")
}
}
func TestSplitPEMChain_TwoCerts(t *testing.T) {
cert1 := "-----BEGIN CERTIFICATE-----\nAAA=\n-----END CERTIFICATE-----\n"
cert2 := "-----BEGIN CERTIFICATE-----\nBBB=\n-----END CERTIFICATE-----\n"
certPEM, chainPEM := splitPEMChain(cert1 + cert2)
if certPEM == "" {
t.Error("expected non-empty certPEM")
}
if chainPEM == "" {
t.Error("expected non-empty chainPEM")
}
}
func TestSplitPEMChain_SingleCert(t *testing.T) {
cert1 := "-----BEGIN CERTIFICATE-----\nAAA=\n-----END CERTIFICATE-----\n"
certPEM, chainPEM := splitPEMChain(cert1)
if certPEM == "" {
t.Error("expected non-empty certPEM")
}
if chainPEM != "" {
t.Errorf("expected empty chainPEM, got %q", chainPEM)
}
}
func TestSplitPEMChain_EmptyInput(t *testing.T) {
certPEM, chainPEM := splitPEMChain("")
if certPEM != "" {
t.Errorf("expected empty certPEM for empty input, got %q", certPEM)
}
if chainPEM != "" {
t.Errorf("expected empty chainPEM for empty input, got %q", chainPEM)
}
}
+4 -2
View File
@@ -20,11 +20,12 @@ func NewIssuerConnectorAdapter(c issuer.Connector) IssuerConnector {
// 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) {
func (a *IssuerConnectorAdapter) IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []string) (*IssuanceResult, error) {
result, err := a.connector.IssueCertificate(ctx, issuer.IssuanceRequest{
CommonName: commonName,
SANs: sans,
CSRPEM: csrPEM,
EKUs: ekus,
})
if err != nil {
return nil, err
@@ -40,11 +41,12 @@ func (a *IssuerConnectorAdapter) IssueCertificate(ctx context.Context, commonNam
// 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) {
func (a *IssuerConnectorAdapter) RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []string) (*IssuanceResult, error) {
result, err := a.connector.RenewCertificate(ctx, issuer.RenewalRequest{
CommonName: commonName,
SANs: sans,
CSRPEM: csrPEM,
EKUs: ekus,
})
if err != nil {
return nil, err
+6 -6
View File
@@ -120,7 +120,7 @@ func TestIssuerConnectorAdapter_IssueCertificate_Success(t *testing.T) {
adapter := NewIssuerConnectorAdapter(mock)
result, err := adapter.IssueCertificate(ctx, "example.com", []string{"www.example.com"}, "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----")
result, err := adapter.IssueCertificate(ctx, "example.com", []string{"www.example.com"}, "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----", nil)
if err != nil {
t.Fatalf("IssueCertificate failed: %v", err)
@@ -157,7 +157,7 @@ func TestIssuerConnectorAdapter_IssueCertificate_Error(t *testing.T) {
adapter := NewIssuerConnectorAdapter(mock)
result, err := adapter.IssueCertificate(ctx, "example.com", []string{}, "csr")
result, err := adapter.IssueCertificate(ctx, "example.com", []string{}, "csr", nil)
if err == nil {
t.Fatal("expected error, got nil")
@@ -191,7 +191,7 @@ func TestIssuerConnectorAdapter_IssueCertificate_RequestTranslation(t *testing.T
sans := []string{"www.test.example.com", "api.test.example.com"}
csrPEM := "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----"
_, err := adapter.IssueCertificate(ctx, commonName, sans, csrPEM)
_, err := adapter.IssueCertificate(ctx, commonName, sans, csrPEM, nil)
if err != nil {
t.Fatalf("IssueCertificate failed: %v", err)
@@ -241,7 +241,7 @@ func TestIssuerConnectorAdapter_RenewCertificate_Success(t *testing.T) {
adapter := NewIssuerConnectorAdapter(mock)
result, err := adapter.RenewCertificate(ctx, "example.com", []string{"www.example.com"}, "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----")
result, err := adapter.RenewCertificate(ctx, "example.com", []string{"www.example.com"}, "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----", nil)
if err != nil {
t.Fatalf("RenewCertificate failed: %v", err)
@@ -278,7 +278,7 @@ func TestIssuerConnectorAdapter_RenewCertificate_Error(t *testing.T) {
adapter := NewIssuerConnectorAdapter(mock)
result, err := adapter.RenewCertificate(ctx, "example.com", []string{}, "csr")
result, err := adapter.RenewCertificate(ctx, "example.com", []string{}, "csr", nil)
if err == nil {
t.Fatal("expected error, got nil")
@@ -312,7 +312,7 @@ func TestIssuerConnectorAdapter_RenewCertificate_RequestTranslation(t *testing.T
sans := []string{"www.renew.example.com"}
csrPEM := "-----BEGIN CERTIFICATE REQUEST-----\nRENEW-CSR\n-----END CERTIFICATE REQUEST-----"
_, err := adapter.RenewCertificate(ctx, commonName, sans, csrPEM)
_, err := adapter.RenewCertificate(ctx, commonName, sans, csrPEM, nil)
if err != nil {
t.Fatalf("RenewCertificate failed: %v", err)
+37 -6
View File
@@ -12,6 +12,8 @@ import (
"fmt"
"log/slog"
"math/big"
"strings"
"sync/atomic"
"time"
"github.com/shankar0123/certctl/internal/domain"
@@ -35,9 +37,9 @@ type RenewalService struct {
// inversion. Use IssuerConnectorAdapter to bridge between the two.
type IssuerConnector interface {
// IssueCertificate issues a new certificate using the provided CSR PEM.
IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error)
IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []string) (*IssuanceResult, error)
// RenewCertificate renews a certificate using the provided CSR PEM.
RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error)
RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []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.
@@ -348,11 +350,23 @@ func (s *RenewalService) processRenewalServerKeygen(ctx context.Context, job *do
return fmt.Errorf("failed to generate private key: %w", err)
}
// Split SANs into DNS names and email addresses for proper CSR encoding
var csrDNSNames []string
var csrEmailAddresses []string
for _, san := range cert.SANs {
if strings.Contains(san, "@") {
csrEmailAddresses = append(csrEmailAddresses, san)
} else {
csrDNSNames = append(csrDNSNames, san)
}
}
csrTemplate := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: cert.CommonName,
},
DNSNames: cert.SANs,
DNSNames: csrDNSNames,
EmailAddresses: csrEmailAddresses,
}
csrDER, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privKey)
@@ -372,8 +386,16 @@ func (s *RenewalService) processRenewalServerKeygen(ctx context.Context, job *do
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
}))
// Resolve EKUs from the certificate profile
var ekus []string
if cert.CertificateProfileID != "" && s.profileRepo != nil {
if profile, profileErr := s.profileRepo.Get(ctx, cert.CertificateProfileID); profileErr == nil && profile != nil {
ekus = profile.AllowedEKUs
}
}
// Call issuer connector to renew
result, err := connector.RenewCertificate(ctx, cert.CommonName, cert.SANs, csrPEM)
result, err := connector.RenewCertificate(ctx, cert.CommonName, cert.SANs, csrPEM, ekus)
if err != nil {
s.failJob(ctx, job, fmt.Sprintf("issuer renewal failed: %v", err))
if notifErr := s.notificationSvc.SendRenewalNotification(ctx, cert, false, err); notifErr != nil {
@@ -480,8 +502,14 @@ func (s *RenewalService) CompleteAgentCSRRenewal(ctx context.Context, job *domai
return fmt.Errorf("failed to update job status: %w", err)
}
// Resolve EKUs from the certificate profile (for S/MIME, email certs, etc.)
var ekus []string
if profile != nil && len(profile.AllowedEKUs) > 0 {
ekus = profile.AllowedEKUs
}
// Sign the agent-submitted CSR via issuer
result, err := connector.RenewCertificate(ctx, cert.CommonName, cert.SANs, csrPEM)
result, err := connector.RenewCertificate(ctx, cert.CommonName, cert.SANs, csrPEM, ekus)
if err != nil {
s.failJob(ctx, job, fmt.Sprintf("issuer signing failed: %v", err))
if notifErr := s.notificationSvc.SendRenewalNotification(ctx, cert, false, err); notifErr != nil {
@@ -708,6 +736,9 @@ func (s *RenewalService) ExpireShortLivedCertificates(ctx context.Context) error
}
// generateID is a helper to generate unique IDs. In production, use a proper ID generator.
var idCounter atomic.Int64
func generateID(prefix string) string {
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
counter := idCounter.Add(1)
return fmt.Sprintf("%s-%d-%d", prefix, time.Now().UnixNano(), counter)
}
+3 -3
View File
@@ -589,7 +589,7 @@ type mockIssuerConnector struct {
Err error
}
func (m *mockIssuerConnector) IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error) {
func (m *mockIssuerConnector) IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []string) (*IssuanceResult, error) {
if m.Err != nil {
return nil, m.Err
}
@@ -606,11 +606,11 @@ func (m *mockIssuerConnector) IssueCertificate(ctx context.Context, commonName s
}, nil
}
func (m *mockIssuerConnector) RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error) {
func (m *mockIssuerConnector) RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string, ekus []string) (*IssuanceResult, error) {
if m.Err != nil {
return nil, m.Err
}
return m.IssueCertificate(ctx, commonName, sans, csrPEM)
return m.IssueCertificate(ctx, commonName, sans, csrPEM, ekus)
}
func (m *mockIssuerConnector) RevokeCertificate(ctx context.Context, serial string, reason string) error {