feat(M43): Sectigo SCM issuer connector

Implement Sectigo Certificate Manager REST API connector with async
order model (enroll → poll → collect PEM), 3-header auth, DV/OV/EV
support, collect-not-ready (400/-183) graceful handling, and RFC 5280
revocation reason mapping. 20 tests with httptest mock API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-04-03 21:01:14 -04:00
parent bad02e6f23
commit 3a11e447cf
12 changed files with 1647 additions and 13 deletions
+47
View File
@@ -27,6 +27,7 @@ type Config struct {
ACME ACMEConfig
Vault VaultConfig
DigiCert DigiCertConfig
Sectigo SectigoConfig
Digest DigestConfig
}
@@ -194,6 +195,43 @@ type DigiCertConfig struct {
BaseURL string
}
// SectigoConfig contains Sectigo Certificate Manager issuer connector configuration.
type SectigoConfig struct {
// CustomerURI is the Sectigo customer URI (organization identifier).
// Required for Sectigo integration.
// Setting: CERTCTL_SECTIGO_CUSTOMER_URI environment variable.
CustomerURI string
// Login is the Sectigo API account login.
// Required for Sectigo integration.
// Setting: CERTCTL_SECTIGO_LOGIN environment variable.
Login string
// Password is the Sectigo API account password or API key.
// Required for Sectigo integration.
// Setting: CERTCTL_SECTIGO_PASSWORD environment variable.
Password string
// OrgID is the Sectigo organization ID for certificate enrollments.
// Required for Sectigo integration.
// Setting: CERTCTL_SECTIGO_ORG_ID environment variable.
OrgID int
// CertType is the Sectigo certificate type ID (from GET /ssl/v1/types).
// Required for enrollment. Set via CERTCTL_SECTIGO_CERT_TYPE environment variable.
CertType int
// Term is the certificate validity in days (e.g., 365, 730).
// Default: 365.
// Setting: CERTCTL_SECTIGO_TERM environment variable.
Term int
// BaseURL is the Sectigo SCM API base URL.
// Default: "https://cert-manager.com/api".
// Setting: CERTCTL_SECTIGO_BASE_URL environment variable.
BaseURL string
}
// DigestConfig controls the scheduled certificate digest email feature.
type DigestConfig struct {
// Enabled controls whether periodic digest emails are generated and sent.
@@ -500,6 +538,15 @@ func Load() (*Config, error) {
ProductType: getEnv("CERTCTL_DIGICERT_PRODUCT_TYPE", "ssl_basic"),
BaseURL: getEnv("CERTCTL_DIGICERT_BASE_URL", "https://www.digicert.com/services/v2"),
},
Sectigo: SectigoConfig{
CustomerURI: getEnv("CERTCTL_SECTIGO_CUSTOMER_URI", ""),
Login: getEnv("CERTCTL_SECTIGO_LOGIN", ""),
Password: getEnv("CERTCTL_SECTIGO_PASSWORD", ""),
OrgID: getEnvInt("CERTCTL_SECTIGO_ORG_ID", 0),
CertType: getEnvInt("CERTCTL_SECTIGO_CERT_TYPE", 0),
Term: getEnvInt("CERTCTL_SECTIGO_TERM", 365),
BaseURL: getEnv("CERTCTL_SECTIGO_BASE_URL", "https://cert-manager.com/api"),
},
ACME: ACMEConfig{
DirectoryURL: getEnv("CERTCTL_ACME_DIRECTORY_URL", ""),
Email: getEnv("CERTCTL_ACME_EMAIL", ""),
@@ -0,0 +1,618 @@
// Package sectigo implements the issuer.Connector interface for Sectigo Certificate Manager (SCM).
//
// Sectigo Certificate Manager is an enterprise certificate authority offering DV, OV, and EV
// certificates. Like DigiCert, Sectigo uses an asynchronous order model: submit an enrollment,
// receive an sslId, then poll for completion. OV/EV certificates require organization validation
// which may take hours or days; DV certificates may be issued immediately.
//
// This connector maps to certctl's existing job state machine:
// - IssueCertificate submits the enrollment; if status is "Issued", returns cert immediately.
// If status is "Applied" or "Pending", returns OrderID with empty CertPEM — the job system
// polls via GetOrderStatus.
// - GetOrderStatus polls the order; when status becomes "Issued", downloads and parses the
// PEM bundle via the collect endpoint.
//
// Authentication: Three custom headers on every request — customerUri, login, password.
//
// Sectigo SCM REST API used:
//
// POST /ssl/v1/enroll - Submit certificate enrollment
// GET /ssl/v1/{sslId} - Check enrollment status
// GET /ssl/v1/collect/{sslId}/pem - Download PEM bundle when issued
// POST /ssl/v1/revoke/{sslId} - Revoke certificate
// GET /ssl/v1/types - List available cert types (used for health check)
package sectigo
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"time"
"github.com/shankar0123/certctl/internal/connector/issuer"
)
// Config represents the Sectigo Certificate Manager issuer connector configuration.
type Config struct {
// CustomerURI is the Sectigo customer URI (organization identifier).
// Required. Set via CERTCTL_SECTIGO_CUSTOMER_URI environment variable.
CustomerURI string `json:"customer_uri"`
// Login is the Sectigo API account login.
// Required. Set via CERTCTL_SECTIGO_LOGIN environment variable.
Login string `json:"login"`
// Password is the Sectigo API account password or API key.
// Required. Set via CERTCTL_SECTIGO_PASSWORD environment variable.
Password string `json:"password"`
// OrgID is the Sectigo organization ID for certificate enrollments.
// Required. Set via CERTCTL_SECTIGO_ORG_ID environment variable.
OrgID int `json:"org_id"`
// CertType is the Sectigo certificate type ID (from GET /ssl/v1/types).
// Required for enrollment. Set via CERTCTL_SECTIGO_CERT_TYPE environment variable.
CertType int `json:"cert_type"`
// Term is the certificate validity in days (e.g., 365, 730).
// Default: 365. Set via CERTCTL_SECTIGO_TERM environment variable.
Term int `json:"term"`
// BaseURL is the Sectigo SCM API base URL.
// Default: "https://cert-manager.com/api".
// Set via CERTCTL_SECTIGO_BASE_URL environment variable.
BaseURL string `json:"base_url"`
}
// Connector implements the issuer.Connector interface for Sectigo Certificate Manager.
type Connector struct {
config *Config
logger *slog.Logger
httpClient *http.Client
}
// New creates a new Sectigo SCM connector with the given configuration and logger.
func New(config *Config, logger *slog.Logger) *Connector {
if config != nil {
if config.Term == 0 {
config.Term = 365
}
if config.BaseURL == "" {
config.BaseURL = "https://cert-manager.com/api"
}
}
return &Connector{
config: config,
logger: logger,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// enrollRequest is the JSON body for Sectigo certificate enrollment.
type enrollRequest struct {
OrgID int `json:"orgId"`
CSR string `json:"csr"`
CertType int `json:"certType"`
Term int `json:"term"`
SubjAltNames string `json:"subjAltNames,omitempty"`
Comments string `json:"comments,omitempty"`
ExternalRequester string `json:"externalRequester,omitempty"`
}
// enrollResponse is the JSON response from a certificate enrollment.
type enrollResponse struct {
SSLId int `json:"sslId"`
RenewId string `json:"renewId,omitempty"`
}
// statusResponse is the JSON response from an enrollment status check.
type statusResponse struct {
SSLId int `json:"sslId"`
Status string `json:"status"`
CommonName string `json:"commonName,omitempty"`
SerialNumber string `json:"serialNumber,omitempty"`
}
// setAuthHeaders sets the three Sectigo authentication headers on a request.
func (c *Connector) setAuthHeaders(req *http.Request) {
req.Header.Set("customerUri", c.config.CustomerURI)
req.Header.Set("login", c.config.Login)
req.Header.Set("password", c.config.Password)
req.Header.Set("Content-Type", "application/json")
}
// ValidateConfig checks that the Sectigo configuration is valid and API access works.
func (c *Connector) ValidateConfig(ctx context.Context, rawConfig json.RawMessage) error {
var cfg Config
if err := json.Unmarshal(rawConfig, &cfg); err != nil {
return fmt.Errorf("invalid Sectigo config: %w", err)
}
if cfg.CustomerURI == "" {
return fmt.Errorf("Sectigo customer_uri is required")
}
if cfg.Login == "" {
return fmt.Errorf("Sectigo login is required")
}
if cfg.Password == "" {
return fmt.Errorf("Sectigo password is required")
}
if cfg.OrgID == 0 {
return fmt.Errorf("Sectigo org_id is required")
}
if cfg.Term == 0 {
cfg.Term = 365
}
if cfg.BaseURL == "" {
cfg.BaseURL = "https://cert-manager.com/api"
}
// Test API access via GET /ssl/v1/types (health check)
typesURL := cfg.BaseURL + "/ssl/v1/types"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, typesURL, nil)
if err != nil {
return fmt.Errorf("failed to create API test request: %w", err)
}
req.Header.Set("customerUri", cfg.CustomerURI)
req.Header.Set("login", cfg.Login)
req.Header.Set("password", cfg.Password)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("Sectigo API not reachable at %s: %w", cfg.BaseURL, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("Sectigo API credentials are invalid (status %d)", resp.StatusCode)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Sectigo API returned status %d", resp.StatusCode)
}
c.config = &cfg
c.logger.Info("Sectigo Certificate Manager configuration validated",
"base_url", cfg.BaseURL,
"org_id", cfg.OrgID)
return nil
}
// IssueCertificate submits a certificate enrollment to Sectigo SCM.
// If the certificate is issued immediately (DV certs), returns the cert.
// If pending (OV/EV certs), returns OrderID with empty CertPEM for polling.
func (c *Connector) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
c.logger.Info("processing Sectigo enrollment request",
"common_name", request.CommonName,
"san_count", len(request.SANs),
"cert_type", c.config.CertType)
enrollReq := enrollRequest{
OrgID: c.config.OrgID,
CSR: request.CSRPEM,
CertType: c.config.CertType,
Term: c.config.Term,
Comments: "Issued by certctl",
}
if len(request.SANs) > 0 {
enrollReq.SubjAltNames = strings.Join(request.SANs, ",")
}
body, err := json.Marshal(enrollReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal enrollment request: %w", err)
}
enrollURL := c.config.BaseURL + "/ssl/v1/enroll"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, enrollURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to create enrollment request: %w", err)
}
c.setAuthHeaders(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("Sectigo enrollment request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read enrollment response: %w", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("Sectigo enrollment returned status %d: %s", resp.StatusCode, string(respBody))
}
var enrollResp enrollResponse
if err := json.Unmarshal(respBody, &enrollResp); err != nil {
return nil, fmt.Errorf("failed to parse enrollment response: %w", err)
}
orderID := fmt.Sprintf("%d", enrollResp.SSLId)
c.logger.Info("Sectigo enrollment submitted", "ssl_id", orderID)
// Check status immediately to see if cert was issued right away
status, err := c.checkStatus(ctx, enrollResp.SSLId)
if err != nil {
// Status check failed but enrollment succeeded — return as pending
c.logger.Warn("Sectigo status check after enrollment failed, treating as pending",
"ssl_id", orderID, "error", err)
return &issuer.IssuanceResult{
OrderID: orderID,
}, nil
}
if status.Status == "Issued" {
certPEM, chainPEM, serial, notBefore, notAfter, collectErr := c.collectCertificate(ctx, enrollResp.SSLId)
if collectErr != nil {
// Cert is issued but collect failed — might not be generated yet
c.logger.Warn("Sectigo certificate issued but collect failed, treating as pending",
"ssl_id", orderID, "error", collectErr)
return &issuer.IssuanceResult{
OrderID: orderID,
}, nil
}
c.logger.Info("Sectigo certificate issued immediately",
"ssl_id", orderID,
"serial", serial)
return &issuer.IssuanceResult{
CertPEM: certPEM,
ChainPEM: chainPEM,
Serial: serial,
NotBefore: notBefore,
NotAfter: notAfter,
OrderID: orderID,
}, nil
}
// Pending — return OrderID for polling via GetOrderStatus
c.logger.Info("Sectigo enrollment pending validation",
"ssl_id", orderID,
"status", status.Status)
return &issuer.IssuanceResult{
OrderID: orderID,
}, nil
}
// RenewCertificate renews a certificate by submitting a new enrollment.
// Sectigo supports POST /ssl/renewById/{sslId} but for simplicity we submit
// a new enrollment (same pattern as DigiCert).
func (c *Connector) RenewCertificate(ctx context.Context, request issuer.RenewalRequest) (*issuer.IssuanceResult, error) {
c.logger.Info("processing Sectigo renewal request",
"common_name", request.CommonName,
"san_count", len(request.SANs))
return c.IssueCertificate(ctx, issuer.IssuanceRequest{
CommonName: request.CommonName,
SANs: request.SANs,
CSRPEM: request.CSRPEM,
EKUs: request.EKUs,
})
}
// RevokeCertificate revokes a certificate at Sectigo SCM.
func (c *Connector) RevokeCertificate(ctx context.Context, request issuer.RevocationRequest) error {
c.logger.Info("processing Sectigo revocation request", "serial", request.Serial)
reason := "Unspecified"
if request.Reason != nil {
reason = mapRevocationReason(*request.Reason)
}
revokeBody := map[string]interface{}{
"reason": reason,
}
body, err := json.Marshal(revokeBody)
if err != nil {
return fmt.Errorf("failed to marshal revoke request: %w", err)
}
// Sectigo uses sslId in the URL path for revocation
revokeURL := fmt.Sprintf("%s/ssl/v1/revoke/%s", c.config.BaseURL, request.Serial)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, revokeURL, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create revoke request: %w", err)
}
c.setAuthHeaders(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("Sectigo revoke request failed: %w", err)
}
defer resp.Body.Close()
// Sectigo returns 204 No Content on successful revocation
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("Sectigo revoke returned status %d: %s", resp.StatusCode, string(respBody))
}
c.logger.Info("Sectigo certificate revoked", "serial", request.Serial, "reason", reason)
return nil
}
// GetOrderStatus checks the status of a Sectigo certificate enrollment.
// If the enrollment is "Issued", downloads the certificate and returns it.
// If still pending, returns pending status for continued polling.
func (c *Connector) GetOrderStatus(ctx context.Context, orderID string) (*issuer.OrderStatus, error) {
c.logger.Debug("checking Sectigo enrollment status", "ssl_id", orderID)
// Parse sslId from string
var sslId int
if _, err := fmt.Sscanf(orderID, "%d", &sslId); err != nil {
return nil, fmt.Errorf("invalid Sectigo ssl_id: %s", orderID)
}
status, err := c.checkStatus(ctx, sslId)
if err != nil {
return nil, err
}
now := time.Now()
switch status.Status {
case "Issued":
certPEM, chainPEM, serial, notBefore, notAfter, collectErr := c.collectCertificate(ctx, sslId)
if collectErr != nil {
// Cert approved but not yet generated — treat as pending
if isCollectNotReady(collectErr) {
msg := fmt.Sprintf("enrollment %s is issued but certificate not yet generated", orderID)
return &issuer.OrderStatus{
OrderID: orderID,
Status: "pending",
Message: &msg,
UpdatedAt: now,
}, nil
}
return nil, fmt.Errorf("failed to collect certificate: %w", collectErr)
}
c.logger.Info("Sectigo enrollment completed",
"ssl_id", orderID,
"serial", serial)
return &issuer.OrderStatus{
OrderID: orderID,
Status: "completed",
CertPEM: &certPEM,
ChainPEM: &chainPEM,
Serial: &serial,
NotBefore: &notBefore,
NotAfter: &notAfter,
UpdatedAt: now,
}, nil
case "Applied", "Pending":
msg := fmt.Sprintf("enrollment %s is %s", orderID, status.Status)
return &issuer.OrderStatus{
OrderID: orderID,
Status: "pending",
Message: &msg,
UpdatedAt: now,
}, nil
case "Rejected":
msg := fmt.Sprintf("enrollment %s was rejected", orderID)
return &issuer.OrderStatus{
OrderID: orderID,
Status: "failed",
Message: &msg,
UpdatedAt: now,
}, nil
case "Revoked", "Expired", "Not Enrolled":
msg := fmt.Sprintf("enrollment %s has status: %s", orderID, status.Status)
return &issuer.OrderStatus{
OrderID: orderID,
Status: "failed",
Message: &msg,
UpdatedAt: now,
}, nil
default:
msg := fmt.Sprintf("unknown enrollment status: %s", status.Status)
return &issuer.OrderStatus{
OrderID: orderID,
Status: "pending",
Message: &msg,
UpdatedAt: now,
}, nil
}
}
// checkStatus retrieves the enrollment status from Sectigo.
func (c *Connector) checkStatus(ctx context.Context, sslId int) (*statusResponse, error) {
statusURL := fmt.Sprintf("%s/ssl/v1/%d", c.config.BaseURL, sslId)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create status request: %w", err)
}
c.setAuthHeaders(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("Sectigo status request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read status response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Sectigo status returned %d: %s", resp.StatusCode, string(respBody))
}
var statusResp statusResponse
if err := json.Unmarshal(respBody, &statusResp); err != nil {
return nil, fmt.Errorf("failed to parse status response: %w", err)
}
return &statusResp, nil
}
// collectCertificate downloads the PEM bundle for a Sectigo certificate.
func (c *Connector) collectCertificate(ctx context.Context, sslId int) (certPEM string, chainPEM string, serial string, notBefore time.Time, notAfter time.Time, err error) {
collectURL := fmt.Sprintf("%s/ssl/v1/collect/%d/pem", c.config.BaseURL, sslId)
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, collectURL, nil)
if reqErr != nil {
err = fmt.Errorf("failed to create collect request: %w", reqErr)
return
}
c.setAuthHeaders(req)
resp, doErr := c.httpClient.Do(req)
if doErr != nil {
err = fmt.Errorf("Sectigo collect request failed: %w", doErr)
return
}
defer resp.Body.Close()
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
err = fmt.Errorf("failed to read collect response: %w", readErr)
return
}
// Sectigo returns 400 with code -183 when cert is approved but not yet generated
if resp.StatusCode == http.StatusBadRequest {
err = &collectNotReadyError{statusCode: resp.StatusCode, body: string(body)}
return
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("Sectigo collect returned status %d: %s", resp.StatusCode, string(body))
return
}
// Parse the PEM bundle: first cert is the leaf, rest are intermediates
certPEM, chainPEM, serial, notBefore, notAfter, err = parsePEMBundle(string(body))
return
}
// collectNotReadyError indicates the certificate is not yet generated.
type collectNotReadyError struct {
statusCode int
body string
}
func (e *collectNotReadyError) Error() string {
return fmt.Sprintf("certificate not yet available (status %d): %s", e.statusCode, e.body)
}
// isCollectNotReady checks if an error indicates the cert is not yet generated.
func isCollectNotReady(err error) bool {
_, ok := err.(*collectNotReadyError)
return ok
}
// parsePEMBundle splits a PEM bundle into leaf cert and chain, extracting metadata.
func parsePEMBundle(bundle string) (certPEM string, chainPEM string, serial string, notBefore time.Time, notAfter time.Time, err error) {
var certs []string
remaining := bundle
for {
var block *pem.Block
block, rest := pem.Decode([]byte(remaining))
if block == nil {
break
}
if block.Type == "CERTIFICATE" {
certs = append(certs, string(pem.EncodeToMemory(block)))
}
remaining = string(rest)
}
if len(certs) == 0 {
err = fmt.Errorf("no certificates found in PEM bundle")
return
}
certPEM = certs[0]
if len(certs) > 1 {
chainPEM = strings.Join(certs[1:], "")
}
// Parse leaf cert for metadata
block, _ := pem.Decode([]byte(certPEM))
if block == nil {
err = fmt.Errorf("failed to decode leaf certificate PEM")
return
}
cert, parseErr := x509.ParseCertificate(block.Bytes)
if parseErr != nil {
err = fmt.Errorf("failed to parse leaf certificate: %w", parseErr)
return
}
serial = cert.SerialNumber.String()
notBefore = cert.NotBefore
notAfter = cert.NotAfter
return
}
// mapRevocationReason maps RFC 5280 / certctl reason strings to Sectigo reason strings.
func mapRevocationReason(reason string) string {
switch strings.ToLower(reason) {
case "keycompromise", "key_compromise":
return "Compromised"
case "cessationofoperation", "cessation_of_operation":
return "Cessation of Operation"
case "affiliationchanged", "affiliation_changed":
return "Affiliation Changed"
case "superseded":
return "Superseded"
default:
return "Unspecified"
}
}
// GenerateCRL is not supported because Sectigo manages CRL distribution.
func (c *Connector) GenerateCRL(ctx context.Context, revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
return nil, fmt.Errorf("Sectigo manages CRL distribution; use Sectigo's CRL endpoints")
}
// SignOCSPResponse is not supported because Sectigo manages OCSP.
func (c *Connector) SignOCSPResponse(ctx context.Context, req issuer.OCSPSignRequest) ([]byte, error) {
return nil, fmt.Errorf("Sectigo manages OCSP; use Sectigo's OCSP responder")
}
// GetCACertPEM is not directly supported. Sectigo intermediate certificates
// come with each certificate issuance as part of the PEM bundle.
func (c *Connector) GetCACertPEM(ctx context.Context) (string, error) {
return "", fmt.Errorf("Sectigo intermediate certificates are included with each issued certificate")
}
// GetRenewalInfo returns nil, nil as Sectigo does not support ACME Renewal Information (ARI).
func (c *Connector) GetRenewalInfo(ctx context.Context, certPEM string) (*issuer.RenewalInfoResult, error) {
return nil, nil
}
// Ensure Connector implements the issuer.Connector interface.
var _ issuer.Connector = (*Connector)(nil)
@@ -0,0 +1,843 @@
package sectigo_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log/slog"
"math/big"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/shankar0123/certctl/internal/connector/issuer"
"github.com/shankar0123/certctl/internal/connector/issuer/sectigo"
)
func TestSectigoConnector(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
ctx := context.Background()
t.Run("ValidateConfig_Success", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/types" {
// Verify all 3 auth headers are present
if r.Header.Get("customerUri") != "test-org" {
t.Errorf("Expected customerUri 'test-org', got '%s'", r.Header.Get("customerUri"))
}
if r.Header.Get("login") != "api-user" {
t.Errorf("Expected login 'api-user', got '%s'", r.Header.Get("login"))
}
if r.Header.Get("password") != "api-pass" {
t.Errorf("Expected password 'api-pass', got '%s'", r.Header.Get("password"))
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`[{"id":423,"name":"Sectigo OV SSL","term":[365,730]}]`))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
config := sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
CertType: 423,
Term: 365,
BaseURL: srv.URL,
}
connector := sectigo.New(nil, logger)
rawConfig, _ := json.Marshal(config)
err := connector.ValidateConfig(ctx, rawConfig)
if err != nil {
t.Fatalf("ValidateConfig failed: %v", err)
}
})
t.Run("ValidateConfig_MissingCustomerURI", func(t *testing.T) {
config := sectigo.Config{
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
}
connector := sectigo.New(nil, logger)
rawConfig, _ := json.Marshal(config)
err := connector.ValidateConfig(ctx, rawConfig)
if err == nil {
t.Fatal("Expected error for missing customer_uri")
}
if !strings.Contains(err.Error(), "customer_uri is required") {
t.Errorf("Expected customer_uri required error, got: %v", err)
}
})
t.Run("ValidateConfig_MissingLogin", func(t *testing.T) {
config := sectigo.Config{
CustomerURI: "test-org",
Password: "api-pass",
OrgID: 12345,
}
connector := sectigo.New(nil, logger)
rawConfig, _ := json.Marshal(config)
err := connector.ValidateConfig(ctx, rawConfig)
if err == nil {
t.Fatal("Expected error for missing login")
}
if !strings.Contains(err.Error(), "login is required") {
t.Errorf("Expected login required error, got: %v", err)
}
})
t.Run("ValidateConfig_MissingPassword", func(t *testing.T) {
config := sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
OrgID: 12345,
}
connector := sectigo.New(nil, logger)
rawConfig, _ := json.Marshal(config)
err := connector.ValidateConfig(ctx, rawConfig)
if err == nil {
t.Fatal("Expected error for missing password")
}
if !strings.Contains(err.Error(), "password is required") {
t.Errorf("Expected password required error, got: %v", err)
}
})
t.Run("ValidateConfig_MissingOrgID", func(t *testing.T) {
config := sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
}
connector := sectigo.New(nil, logger)
rawConfig, _ := json.Marshal(config)
err := connector.ValidateConfig(ctx, rawConfig)
if err == nil {
t.Fatal("Expected error for missing org_id")
}
if !strings.Contains(err.Error(), "org_id is required") {
t.Errorf("Expected org_id required error, got: %v", err)
}
})
t.Run("ValidateConfig_InvalidCredentials", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/types" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"code":0,"description":"Invalid credentials"}`))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
config := sectigo.Config{
CustomerURI: "bad-org",
Login: "bad-user",
Password: "bad-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(nil, logger)
rawConfig, _ := json.Marshal(config)
err := connector.ValidateConfig(ctx, rawConfig)
if err == nil {
t.Fatal("Expected error for invalid credentials")
}
if !strings.Contains(err.Error(), "invalid") {
t.Logf("Got error: %v", err)
}
})
t.Run("IssueCertificate_ImmediateSuccess", func(t *testing.T) {
testCertPEM, _ := generateTestCert(t)
testChainPEM, _ := generateTestCert(t)
pemBundle := testCertPEM + testChainPEM
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify auth headers on every request
if r.Header.Get("customerUri") == "" || r.Header.Get("login") == "" || r.Header.Get("password") == "" {
t.Error("Missing auth headers on request")
}
switch {
case r.URL.Path == "/ssl/v1/enroll" && r.Method == http.MethodPost:
// Verify request body structure
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
if req["orgId"] == nil {
t.Error("Expected orgId in enrollment request")
}
if req["certType"] == nil {
t.Error("Expected certType in enrollment request")
}
// SANs should be comma-separated string, not array
if sans, ok := req["subjAltNames"].(string); ok {
if !strings.Contains(sans, ",") && len(sans) > 0 {
// Single SAN is fine
}
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55001,"renewId":"ren-abc"}`))
case r.URL.Path == "/ssl/v1/55001" && r.Method == http.MethodGet:
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55001,"status":"Issued","commonName":"app.example.com"}`))
case r.URL.Path == "/ssl/v1/collect/55001/pem" && r.Method == http.MethodGet:
w.WriteHeader(http.StatusOK)
w.Write([]byte(pemBundle))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
CertType: 423,
Term: 365,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
_, csrPEM := generateTestCSR(t, "app.example.com")
req := issuer.IssuanceRequest{
CommonName: "app.example.com",
SANs: []string{"app.example.com", "www.example.com"},
CSRPEM: csrPEM,
}
result, err := connector.IssueCertificate(ctx, req)
if err != nil {
t.Fatalf("IssueCertificate failed: %v", err)
}
if result.CertPEM == "" {
t.Error("CertPEM should not be empty for immediate issuance")
}
if result.Serial == "" {
t.Error("Serial should not be empty for immediate issuance")
}
if result.OrderID != "55001" {
t.Errorf("Expected OrderID '55001', got '%s'", result.OrderID)
}
t.Logf("Sectigo issued cert: serial=%s, orderID=%s", result.Serial, result.OrderID)
})
t.Run("IssueCertificate_Pending", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/ssl/v1/enroll":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55002}`))
case r.URL.Path == "/ssl/v1/55002":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55002,"status":"Applied","commonName":"secure.example.com"}`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
CertType: 423,
Term: 365,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
_, csrPEM := generateTestCSR(t, "secure.example.com")
req := issuer.IssuanceRequest{
CommonName: "secure.example.com",
CSRPEM: csrPEM,
}
result, err := connector.IssueCertificate(ctx, req)
if err != nil {
t.Fatalf("IssueCertificate failed: %v", err)
}
if result.OrderID != "55002" {
t.Errorf("Expected OrderID '55002', got '%s'", result.OrderID)
}
if result.CertPEM != "" {
t.Error("CertPEM should be empty for pending order")
}
if result.Serial != "" {
t.Error("Serial should be empty for pending order")
}
})
t.Run("IssueCertificate_ServerError", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"code":-14,"description":"Invalid CSR"}`))
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
CertType: 423,
Term: 365,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
req := issuer.IssuanceRequest{
CommonName: "test.example.com",
CSRPEM: "invalid-csr",
}
_, err := connector.IssueCertificate(ctx, req)
if err == nil {
t.Fatal("Expected error for server error response")
}
})
t.Run("GetOrderStatus_Issued", func(t *testing.T) {
testCertPEM, _ := generateTestCert(t)
testChainPEM, _ := generateTestCert(t)
pemBundle := testCertPEM + testChainPEM
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/ssl/v1/55001":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55001,"status":"Issued","commonName":"app.example.com"}`))
case "/ssl/v1/collect/55001/pem":
w.WriteHeader(http.StatusOK)
w.Write([]byte(pemBundle))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
status, err := connector.GetOrderStatus(ctx, "55001")
if err != nil {
t.Fatalf("GetOrderStatus failed: %v", err)
}
if status.Status != "completed" {
t.Errorf("Expected status 'completed', got '%s'", status.Status)
}
if status.CertPEM == nil || *status.CertPEM == "" {
t.Error("CertPEM should not be empty for issued order")
}
if status.Serial == nil || *status.Serial == "" {
t.Error("Serial should not be empty for issued order")
}
})
t.Run("GetOrderStatus_Pending", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/55002" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55002,"status":"Applied"}`))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
status, err := connector.GetOrderStatus(ctx, "55002")
if err != nil {
t.Fatalf("GetOrderStatus failed: %v", err)
}
if status.Status != "pending" {
t.Errorf("Expected status 'pending', got '%s'", status.Status)
}
if status.CertPEM != nil {
t.Error("CertPEM should be nil for pending order")
}
})
t.Run("GetOrderStatus_Rejected", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/55003" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55003,"status":"Rejected"}`))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
status, err := connector.GetOrderStatus(ctx, "55003")
if err != nil {
t.Fatalf("GetOrderStatus failed: %v", err)
}
if status.Status != "failed" {
t.Errorf("Expected status 'failed', got '%s'", status.Status)
}
})
t.Run("GetOrderStatus_CollectNotReady", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/ssl/v1/55004":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55004,"status":"Issued","commonName":"pending-collect.example.com"}`))
case "/ssl/v1/collect/55004/pem":
// Sectigo returns 400 with code -183 when cert not yet generated
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"code":-183,"description":"Certificate is not available"}`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
status, err := connector.GetOrderStatus(ctx, "55004")
if err != nil {
t.Fatalf("GetOrderStatus failed: %v", err)
}
// Should be treated as pending (cert approved but not yet generated)
if status.Status != "pending" {
t.Errorf("Expected status 'pending' for collect-not-ready, got '%s'", status.Status)
}
})
t.Run("RenewCertificate_NewOrder", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/ssl/v1/enroll":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55010}`))
case r.URL.Path == "/ssl/v1/55010":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55010,"status":"Applied"}`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
CertType: 423,
Term: 365,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
_, csrPEM := generateTestCSR(t, "renew.example.com")
renewReq := issuer.RenewalRequest{
CommonName: "renew.example.com",
CSRPEM: csrPEM,
}
result, err := connector.RenewCertificate(ctx, renewReq)
if err != nil {
t.Fatalf("RenewCertificate failed: %v", err)
}
if result.OrderID == "" {
t.Error("OrderID should not be empty")
}
})
t.Run("RevokeCertificate_Success", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/ssl/v1/revoke/") && r.Method == http.MethodPost {
// Verify auth headers
if r.Header.Get("customerUri") == "" {
t.Error("Missing customerUri header on revoke request")
}
if r.Header.Get("login") == "" {
t.Error("Missing login header on revoke request")
}
if r.Header.Get("password") == "" {
t.Error("Missing password header on revoke request")
}
// Verify reason in body
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
if req["reason"] == nil {
t.Error("Expected reason in revoke request body")
}
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
reason := "keyCompromise"
revokeReq := issuer.RevocationRequest{
Serial: "55001",
Reason: &reason,
}
err := connector.RevokeCertificate(ctx, revokeReq)
if err != nil {
t.Fatalf("RevokeCertificate failed: %v", err)
}
})
t.Run("RevokeCertificate_Error", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"code":-1,"description":"Certificate not found"}`))
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
revokeReq := issuer.RevocationRequest{
Serial: "00000",
}
err := connector.RevokeCertificate(ctx, revokeReq)
if err == nil {
t.Fatal("Expected error for revocation of nonexistent cert")
}
})
t.Run("GetRenewalInfo_ReturnsNil", func(t *testing.T) {
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: "https://cert-manager.com/api",
}
connector := sectigo.New(config, logger)
result, err := connector.GetRenewalInfo(ctx, "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----")
if err != nil {
t.Fatalf("GetRenewalInfo should not return error, got: %v", err)
}
if result != nil {
t.Fatal("GetRenewalInfo should return nil for Sectigo")
}
})
t.Run("DefaultTerm", func(t *testing.T) {
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
CertType: 423,
// Term intentionally left as 0
}
connector := sectigo.New(config, logger)
// Verify the connector was created (the default is set in New())
if connector == nil {
t.Fatal("Connector should not be nil")
}
// Verify via a request that uses the term
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/enroll" {
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
// Default term should be 365
if term, ok := req["term"].(float64); ok {
if int(term) != 365 {
t.Errorf("Expected default term 365, got %d", int(term))
}
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55099}`))
return
}
if r.URL.Path == "/ssl/v1/55099" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55099,"status":"Applied"}`))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
// Reconfigure with test server URL
config.BaseURL = srv.URL
connector = sectigo.New(config, logger)
_, csrPEM := generateTestCSR(t, "test.example.com")
req := issuer.IssuanceRequest{
CommonName: "test.example.com",
CSRPEM: csrPEM,
}
result, err := connector.IssueCertificate(ctx, req)
if err != nil {
t.Fatalf("IssueCertificate with default term failed: %v", err)
}
if result.OrderID == "" {
t.Error("OrderID should not be empty")
}
})
t.Run("AuthHeaders_PresentOnAllRequests", func(t *testing.T) {
requestCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++
// Every single request must have all 3 auth headers
if r.Header.Get("customerUri") != "verify-org" {
t.Errorf("Request %d: expected customerUri 'verify-org', got '%s'", requestCount, r.Header.Get("customerUri"))
}
if r.Header.Get("login") != "verify-user" {
t.Errorf("Request %d: expected login 'verify-user', got '%s'", requestCount, r.Header.Get("login"))
}
if r.Header.Get("password") != "verify-pass" {
t.Errorf("Request %d: expected password 'verify-pass', got '%s'", requestCount, r.Header.Get("password"))
}
switch {
case r.URL.Path == "/ssl/v1/enroll":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55050}`))
case r.URL.Path == "/ssl/v1/55050":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"sslId":55050,"status":"Applied"}`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "verify-org",
Login: "verify-user",
Password: "verify-pass",
OrgID: 12345,
CertType: 423,
Term: 365,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
_, csrPEM := generateTestCSR(t, "auth-check.example.com")
req := issuer.IssuanceRequest{
CommonName: "auth-check.example.com",
CSRPEM: csrPEM,
}
_, err := connector.IssueCertificate(ctx, req)
if err != nil {
t.Fatalf("IssueCertificate failed: %v", err)
}
if requestCount < 2 {
t.Errorf("Expected at least 2 requests (enroll + status), got %d", requestCount)
}
})
t.Run("RevocationReasonMapping", func(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"keyCompromise", "Compromised"},
{"cessationOfOperation", "Cessation of Operation"},
{"affiliationChanged", "Affiliation Changed"},
{"superseded", "Superseded"},
{"unspecified", "Unspecified"},
{"unknown_reason", "Unspecified"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
var receivedReason string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/ssl/v1/revoke/") {
body, _ := io.ReadAll(r.Body)
var req map[string]interface{}
json.Unmarshal(body, &req)
receivedReason = req["reason"].(string)
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer srv.Close()
config := &sectigo.Config{
CustomerURI: "test-org",
Login: "api-user",
Password: "api-pass",
OrgID: 12345,
BaseURL: srv.URL,
}
connector := sectigo.New(config, logger)
reason := tt.input
err := connector.RevokeCertificate(ctx, issuer.RevocationRequest{
Serial: "12345",
Reason: &reason,
})
if err != nil {
t.Fatalf("RevokeCertificate failed: %v", err)
}
if receivedReason != tt.expected {
t.Errorf("Expected reason '%s', got '%s'", tt.expected, receivedReason)
}
})
}
})
}
// generateTestCert creates a self-signed test certificate and returns the PEM strings.
func generateTestCert(t *testing.T) (certPEM string, keyPEM string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: fmt.Sprintf("Test Certificate %s", serial.String()[:8]),
},
DNSNames: []string{"test.example.com"},
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatalf("Failed to create certificate: %v", err)
}
certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}))
keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}))
return certPEM, keyPEM
}
// generateTestCSR creates a test CSR for the given common name.
func generateTestCSR(t *testing.T, commonName string) (*x509.CertificateRequest, string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
csrTemplate := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: commonName,
},
DNSNames: []string{commonName},
SignatureAlgorithm: x509.SHA256WithRSA,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, key)
if err != nil {
t.Fatalf("Failed to create CSR: %v", err)
}
csrPEM := string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrBytes,
}))
csr, err := x509.ParseCertificateRequest(csrBytes)
if err != nil {
t.Fatalf("Failed to parse CSR: %v", err)
}
return csr, csrPEM
}
+1
View File
@@ -71,6 +71,7 @@ const (
IssuerTypeOpenSSL IssuerType = "OpenSSL"
IssuerTypeVault IssuerType = "VaultPKI"
IssuerTypeDigiCert IssuerType = "DigiCert"
IssuerTypeSectigo IssuerType = "Sectigo"
)
// TargetType represents the type of deployment target.