mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:51:29 +00:00
21aeed4f4e
Phase 0 closure (Path B2, post-rewrite):
addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:
// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).
Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.
Generated via:
addlicense -c "certctl LLC" -y 2026 \
-f cowork/legal/copyright-header.tpl \
-ignore '**/testdata/**' -ignore '**/*_test.go' \
cmd/ internal/
Verification:
find cmd internal -name '*.go' -not -name '*_test.go' \
-not -path '*/testdata/*' \
-exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l
Returns: 0
gofmt clean. Header additions are comments only, no compile impact.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
565 lines
19 KiB
Go
565 lines
19 KiB
Go
// Copyright 2026 certctl LLC. All rights reserved.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
// Package stepca implements the issuer.Connector interface for Smallstep step-ca
|
|
// private certificate authority.
|
|
//
|
|
// step-ca is a popular open-source private CA that provides both ACME and native
|
|
// provisioner-based certificate issuance. This connector uses the native /sign API
|
|
// with JWK provisioner authentication, which is simpler than ACME for internal PKI:
|
|
// no challenge solving, no domain validation — just CSR + auth token → signed cert.
|
|
//
|
|
// For teams already using step-ca, this connector integrates certctl's lifecycle
|
|
// management (renewal policies, deployment, audit) with step-ca's certificate signing.
|
|
//
|
|
// Authentication: JWK provisioner with a shared provisioner password.
|
|
// The connector generates a short-lived token for each signing request using the
|
|
// provisioner key (loaded from disk or provided inline).
|
|
//
|
|
// step-ca API used:
|
|
//
|
|
// POST /sign — submit CSR with provisioner token, receive signed certificate
|
|
// POST /revoke — revoke a certificate by serial
|
|
// GET /health — check CA availability
|
|
package stepca
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/certctl-io/certctl/internal/connector/issuer"
|
|
)
|
|
|
|
// Config represents the step-ca issuer connector configuration.
|
|
type Config struct {
|
|
// CAURL is the base URL of the step-ca server (e.g., "https://ca.internal:9000").
|
|
CAURL string `json:"ca_url"`
|
|
|
|
// RootCertPath is the path to the step-ca root certificate PEM (for TLS verification).
|
|
// If empty, the system trust store is used.
|
|
RootCertPath string `json:"root_cert_path,omitempty"`
|
|
|
|
// ProvisionerName is the name of the JWK provisioner to use for signing.
|
|
ProvisionerName string `json:"provisioner_name"`
|
|
|
|
// ProvisionerKeyPath is the path to the provisioner's encrypted private key (JWK JSON).
|
|
// This is the key file generated by `step ca provisioner add`.
|
|
ProvisionerKeyPath string `json:"provisioner_key_path,omitempty"`
|
|
|
|
// ProvisionerPassword is the password to decrypt the provisioner key.
|
|
// Can also be set via CERTCTL_STEPCA_PROVISIONER_PASSWORD env var.
|
|
ProvisionerPassword string `json:"provisioner_password,omitempty"`
|
|
|
|
// ValidityDays is the requested certificate validity (step-ca may enforce a maximum).
|
|
// Defaults to 90.
|
|
ValidityDays int `json:"validity_days,omitempty"`
|
|
}
|
|
|
|
// Connector implements the issuer.Connector interface for step-ca.
|
|
type Connector struct {
|
|
config *Config
|
|
logger *slog.Logger
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// New creates a new step-ca connector with the given configuration and logger.
|
|
// If RootCertPath is set, the HTTP client will trust that CA certificate for TLS connections.
|
|
// Otherwise, the system trust store is used (which works if setup-trust.sh has run).
|
|
func New(config *Config, logger *slog.Logger) *Connector {
|
|
// Don't default ValidityDays — let step-ca use its own default duration.
|
|
// Operators can explicitly set ValidityDays if their step-ca is configured
|
|
// with longer max durations. A zero value means "omit from sign request."
|
|
|
|
httpClient := &http.Client{Timeout: 30 * time.Second}
|
|
|
|
// Load custom root CA cert if provided
|
|
if config != nil && config.RootCertPath != "" {
|
|
rootPEM, err := os.ReadFile(config.RootCertPath)
|
|
if err == nil {
|
|
pool := x509.NewCertPool()
|
|
if pool.AppendCertsFromPEM(rootPEM) {
|
|
httpClient.Transport = &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: pool,
|
|
},
|
|
}
|
|
logger.Info("step-ca custom root CA loaded", "path", config.RootCertPath)
|
|
}
|
|
} else {
|
|
logger.Warn("failed to read step-ca root cert, using system trust store", "path", config.RootCertPath, "error", err)
|
|
}
|
|
}
|
|
|
|
return &Connector{
|
|
config: config,
|
|
logger: logger,
|
|
httpClient: httpClient,
|
|
}
|
|
}
|
|
|
|
// ValidateConfig checks that the step-ca configuration is valid and the CA is reachable.
|
|
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 step-ca config: %w", err)
|
|
}
|
|
|
|
if cfg.CAURL == "" {
|
|
return fmt.Errorf("step-ca ca_url is required")
|
|
}
|
|
|
|
if cfg.ProvisionerName == "" {
|
|
return fmt.Errorf("step-ca provisioner_name is required")
|
|
}
|
|
|
|
// Don't default ValidityDays — 0 means "let step-ca use its own default duration"
|
|
|
|
// Check CA health
|
|
healthURL := cfg.CAURL + "/health"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create health check request: %w", err)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("step-ca not reachable at %s: %w", cfg.CAURL, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("step-ca health check returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
// Validate provisioner key path exists if provided
|
|
if cfg.ProvisionerKeyPath != "" {
|
|
if _, err := os.Stat(cfg.ProvisionerKeyPath); err != nil {
|
|
return fmt.Errorf("provisioner key not accessible: %w", err)
|
|
}
|
|
}
|
|
|
|
c.config = &cfg
|
|
c.logger.Info("step-ca configuration validated",
|
|
"ca_url", cfg.CAURL,
|
|
"provisioner", cfg.ProvisionerName)
|
|
|
|
return nil
|
|
}
|
|
|
|
// signRequest is the JSON body for the step-ca /sign endpoint.
|
|
type signRequest struct {
|
|
CsrPEM string `json:"csr"`
|
|
OTT string `json:"ott"` // One-Time Token (provisioner JWT)
|
|
NotBefore time.Time `json:"notBefore,omitempty"`
|
|
NotAfter time.Time `json:"notAfter,omitempty"`
|
|
}
|
|
|
|
// signResponse is the JSON response from the step-ca /sign endpoint.
|
|
type signResponse struct {
|
|
ServerPEM certificateChain `json:"serverPEM,omitempty"`
|
|
CaPEM certificateChain `json:"caPEM,omitempty"`
|
|
CertChainPEM []certBlock `json:"certChainPEM,omitempty"`
|
|
}
|
|
|
|
type certificateChain struct {
|
|
Certificate string `json:"certificate"`
|
|
}
|
|
|
|
type certBlock struct {
|
|
Certificate string `json:"certificate"`
|
|
}
|
|
|
|
// IssueCertificate submits a CSR to step-ca for signing.
|
|
func (c *Connector) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
|
|
c.logger.Info("processing step-ca issuance request",
|
|
"common_name", request.CommonName,
|
|
"san_count", len(request.SANs))
|
|
|
|
// Generate a provisioner token (OTT) for this request
|
|
ott, err := c.generateProvisionerToken(request.CommonName, request.SANs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate provisioner token: %w", err)
|
|
}
|
|
|
|
// Build the sign request.
|
|
// When ValidityDays is 0 (default), omit NotBefore/NotAfter so step-ca uses its
|
|
// own default duration (typically 24h). The signRequest struct has omitempty on
|
|
// both time fields, so zero-value time.Time{} gets stripped from the JSON.
|
|
signReq := signRequest{
|
|
CsrPEM: request.CSRPEM,
|
|
OTT: ott,
|
|
}
|
|
if c.config.ValidityDays > 0 || request.MaxTTLSeconds > 0 {
|
|
now := time.Now()
|
|
signReq.NotBefore = now
|
|
if c.config.ValidityDays > 0 {
|
|
signReq.NotAfter = now.AddDate(0, 0, c.config.ValidityDays)
|
|
}
|
|
// Cap validity to MaxTTLSeconds if profile specifies a maximum
|
|
if request.MaxTTLSeconds > 0 {
|
|
maxNotAfter := now.Add(time.Duration(request.MaxTTLSeconds) * time.Second)
|
|
if signReq.NotAfter.IsZero() || maxNotAfter.Before(signReq.NotAfter) {
|
|
signReq.NotAfter = maxNotAfter
|
|
}
|
|
}
|
|
}
|
|
|
|
body, err := json.Marshal(signReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal sign request: %w", err)
|
|
}
|
|
|
|
// POST /sign
|
|
signURL := c.config.CAURL + "/sign"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, signURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create sign request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("step-ca sign request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read sign response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("step-ca sign returned status %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
// Parse response — step-ca returns the cert chain
|
|
certPEM, chainPEM, serial, certNotBefore, certNotAfter, err := parseSignResponse(respBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse sign response: %w", err)
|
|
}
|
|
|
|
orderID := fmt.Sprintf("stepca-%s", serial)
|
|
|
|
c.logger.Info("step-ca certificate issued",
|
|
"common_name", request.CommonName,
|
|
"serial", serial,
|
|
"not_after", certNotAfter)
|
|
|
|
return &issuer.IssuanceResult{
|
|
CertPEM: certPEM,
|
|
ChainPEM: chainPEM,
|
|
Serial: serial,
|
|
NotBefore: certNotBefore,
|
|
NotAfter: certNotAfter,
|
|
OrderID: orderID,
|
|
}, nil
|
|
}
|
|
|
|
// RenewCertificate renews a certificate by creating a new signing request.
|
|
// For step-ca, renewal is functionally identical to issuance.
|
|
func (c *Connector) RenewCertificate(ctx context.Context, request issuer.RenewalRequest) (*issuer.IssuanceResult, error) {
|
|
c.logger.Info("processing step-ca 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,
|
|
MaxTTLSeconds: request.MaxTTLSeconds,
|
|
})
|
|
}
|
|
|
|
// revokeRequest is the JSON body for the step-ca /revoke endpoint.
|
|
type revokeRequest struct {
|
|
Serial string `json:"serial"`
|
|
ReasonCode int `json:"reasonCode,omitempty"`
|
|
Reason string `json:"reason,omitempty"`
|
|
OTT string `json:"ott"`
|
|
Passive bool `json:"passive"` // true = don't propagate to OCSP (just mark revoked)
|
|
}
|
|
|
|
// RevokeCertificate revokes a certificate at step-ca.
|
|
func (c *Connector) RevokeCertificate(ctx context.Context, request issuer.RevocationRequest) error {
|
|
c.logger.Info("processing step-ca revocation request", "serial", request.Serial)
|
|
|
|
ott, err := c.generateProvisionerToken(request.Serial, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate revocation token: %w", err)
|
|
}
|
|
|
|
reason := "unspecified"
|
|
if request.Reason != nil {
|
|
reason = *request.Reason
|
|
}
|
|
|
|
revokeReq := revokeRequest{
|
|
Serial: request.Serial,
|
|
Reason: reason,
|
|
OTT: ott,
|
|
Passive: true,
|
|
}
|
|
|
|
body, err := json.Marshal(revokeReq)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal revoke request: %w", err)
|
|
}
|
|
|
|
revokeURL := c.config.CAURL + "/revoke"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, revokeURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create revoke request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("step-ca revoke request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("step-ca revoke returned status %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
c.logger.Info("step-ca certificate revoked", "serial", request.Serial, "reason", reason)
|
|
return nil
|
|
}
|
|
|
|
// GetOrderStatus returns the status of a step-ca order.
|
|
// step-ca signs synchronously, so orders are always "completed" immediately.
|
|
func (c *Connector) GetOrderStatus(ctx context.Context, orderID string) (*issuer.OrderStatus, error) {
|
|
return &issuer.OrderStatus{
|
|
OrderID: orderID,
|
|
Status: "completed",
|
|
UpdatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// generateProvisionerToken creates a short-lived JWT (One-Time Token) for step-ca API calls.
|
|
// The JWT is signed with the provisioner's private key (loaded from the encrypted JWE file
|
|
// at ProvisionerKeyPath and decrypted with ProvisionerPassword).
|
|
func (c *Connector) generateProvisionerToken(subject string, sans []string) (string, error) {
|
|
var key *ecdsa.PrivateKey
|
|
var kid string
|
|
|
|
if c.config.ProvisionerKeyPath != "" {
|
|
// Production: load and decrypt the real provisioner key from disk
|
|
var err error
|
|
key, kid, err = c.loadProvisionerKey()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to load provisioner key: %w", err)
|
|
}
|
|
} else {
|
|
// Fallback: generate an ephemeral key (for testing or when key path not configured).
|
|
// This won't authenticate with a real step-ca server, but allows the connector
|
|
// to function against mock servers in tests.
|
|
c.logger.Warn("no provisioner key path configured, using ephemeral key (will not work with real step-ca)")
|
|
var err error
|
|
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate ephemeral key: %w", err)
|
|
}
|
|
kid = "ephemeral"
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
// step-ca expects: aud = <ca-url>/1.0/sign (the sign endpoint audience)
|
|
claims := map[string]interface{}{
|
|
"sub": subject,
|
|
"iss": c.config.ProvisionerName,
|
|
"aud": c.config.CAURL + "/1.0/sign",
|
|
"nbf": now.Unix(),
|
|
"iat": now.Unix(),
|
|
"exp": now.Add(5 * time.Minute).Unix(),
|
|
"jti": generateJTI(),
|
|
"sha": kid, // step-ca uses this to look up the provisioner by key fingerprint
|
|
}
|
|
|
|
if len(sans) > 0 {
|
|
claims["sans"] = sans
|
|
}
|
|
|
|
return signJWTWithKID(claims, key, kid)
|
|
}
|
|
|
|
// loadProvisionerKey loads and decrypts the step-ca provisioner key from disk.
|
|
// Returns the ECDSA private key and the key ID (JWK thumbprint).
|
|
func (c *Connector) loadProvisionerKey() (*ecdsa.PrivateKey, string, error) {
|
|
if c.config.ProvisionerKeyPath == "" {
|
|
return nil, "", fmt.Errorf("provisioner_key_path is required for step-ca JWK authentication")
|
|
}
|
|
|
|
jweData, err := os.ReadFile(c.config.ProvisionerKeyPath)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to read provisioner key file %s: %w", c.config.ProvisionerKeyPath, err)
|
|
}
|
|
|
|
password := c.config.ProvisionerPassword
|
|
if password == "" {
|
|
return nil, "", fmt.Errorf("provisioner_password is required to decrypt the provisioner key")
|
|
}
|
|
|
|
key, kid, err := decryptProvisionerKey(jweData, password)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to decrypt provisioner key: %w", err)
|
|
}
|
|
|
|
c.logger.Info("provisioner key loaded and decrypted",
|
|
"key_path", c.config.ProvisionerKeyPath,
|
|
"kid", kid)
|
|
|
|
return key, kid, nil
|
|
}
|
|
|
|
// generateJTI creates a unique JWT ID.
|
|
func generateJTI() string {
|
|
b := make([]byte, 16)
|
|
_, _ = rand.Read(b)
|
|
return base64.RawURLEncoding.EncodeToString(b)
|
|
}
|
|
|
|
// signJWTWithKID creates an ES256 JWT with a key ID in the header.
|
|
func signJWTWithKID(claims map[string]interface{}, key *ecdsa.PrivateKey, kid string) (string, error) {
|
|
// Header with kid so step-ca can look up the provisioner
|
|
header := map[string]string{
|
|
"alg": "ES256",
|
|
"typ": "JWT",
|
|
"kid": kid,
|
|
}
|
|
|
|
return signJWTRaw(claims, key, header)
|
|
}
|
|
|
|
// signJWTRaw creates an ES256 JWT from the given claims and header.
|
|
func signJWTRaw(claims map[string]interface{}, key *ecdsa.PrivateKey, header map[string]string) (string, error) {
|
|
|
|
headerJSON, err := json.Marshal(header)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
claimsJSON, err := json.Marshal(claims)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
|
|
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
|
signingInput := headerB64 + "." + claimsB64
|
|
|
|
// Sign with ES256
|
|
hash := sha256.Sum256([]byte(signingInput))
|
|
r, s, err := ecdsa.Sign(rand.Reader, key, hash[:])
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to sign JWT: %w", err)
|
|
}
|
|
|
|
// Encode signature as fixed-size concatenation (r || s, 32 bytes each for P-256)
|
|
sig := make([]byte, 64)
|
|
rBytes := r.Bytes()
|
|
sBytes := s.Bytes()
|
|
copy(sig[32-len(rBytes):32], rBytes)
|
|
copy(sig[64-len(sBytes):64], sBytes)
|
|
|
|
sigB64 := base64.RawURLEncoding.EncodeToString(sig)
|
|
return signingInput + "." + sigB64, nil
|
|
}
|
|
|
|
// parseSignResponse extracts the certificate and chain from step-ca's /sign response.
|
|
func parseSignResponse(respBody []byte) (certPEM string, chainPEM string, serial string, notBefore time.Time, notAfter time.Time, err error) {
|
|
// step-ca /sign response format:
|
|
// { "crt": "-----BEGIN CERTIFICATE-----\n...", "ca": "-----BEGIN CERTIFICATE-----\n..." }
|
|
// or
|
|
// { "serverPEM": { "certificate": "..." }, "caPEM": { "certificate": "..." } }
|
|
// or
|
|
// { "certChainPEM": [ { "certificate": "..." }, ... ] }
|
|
|
|
// Try the simple format first (crt/ca)
|
|
var simpleResp struct {
|
|
Crt string `json:"crt"`
|
|
Ca string `json:"ca"`
|
|
}
|
|
if err = json.Unmarshal(respBody, &simpleResp); err == nil && simpleResp.Crt != "" {
|
|
certPEM = simpleResp.Crt
|
|
chainPEM = simpleResp.Ca
|
|
} else {
|
|
// Try the structured format
|
|
var structResp signResponse
|
|
if err = json.Unmarshal(respBody, &structResp); err != nil {
|
|
return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("failed to parse sign response: %w", err)
|
|
}
|
|
|
|
if structResp.ServerPEM.Certificate != "" {
|
|
certPEM = structResp.ServerPEM.Certificate
|
|
chainPEM = structResp.CaPEM.Certificate
|
|
} else if len(structResp.CertChainPEM) > 0 {
|
|
certPEM = structResp.CertChainPEM[0].Certificate
|
|
for i := 1; i < len(structResp.CertChainPEM); i++ {
|
|
chainPEM += structResp.CertChainPEM[i].Certificate
|
|
}
|
|
}
|
|
}
|
|
|
|
if certPEM == "" {
|
|
return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("no certificate in sign response")
|
|
}
|
|
|
|
// Parse the leaf cert to extract metadata
|
|
block, _ := pem.Decode([]byte(certPEM))
|
|
if block == nil {
|
|
return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("failed to decode certificate PEM")
|
|
}
|
|
|
|
cert, parseErr := x509.ParseCertificate(block.Bytes)
|
|
if parseErr != nil {
|
|
return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("failed to parse certificate: %w", parseErr)
|
|
}
|
|
|
|
serial = cert.SerialNumber.String()
|
|
notBefore = cert.NotBefore
|
|
notAfter = cert.NotAfter
|
|
|
|
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")
|
|
}
|
|
|
|
// GetCACertPEM is not directly supported; step-ca serves its own /root endpoint.
|
|
func (c *Connector) GetCACertPEM(ctx context.Context) (string, error) {
|
|
return "", fmt.Errorf("step-ca serves its own CA certificate at /root; use step-ca's endpoint directly")
|
|
}
|
|
|
|
// GetRenewalInfo returns nil, nil as step-ca 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)
|