mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 10:18:53 +00:00
370f856725
SA1029: use typed context key instead of string in main_test.go S1039: remove unnecessary fmt.Sprintf in validation_test.go SA4023: fix unreachable nil check on concrete error type SA4006: fix unused variable assignments in stepca_test.go (4 occurrences) SA4000: fix duplicate expression in ssh_test.go (BEGIN vs END CERTIFICATE) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1774 lines
48 KiB
Go
1774 lines
48 KiB
Go
package stepca_test
|
|
|
|
import (
|
|
"bytes"
|
|
"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"
|
|
"testing"
|
|
|
|
"github.com/shankar0123/certctl/internal/connector/issuer"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/stepca"
|
|
)
|
|
|
|
func TestStepCAConnector(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) {
|
|
// Start a mock step-ca health endpoint
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 90,
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingCAURL", func(t *testing.T) {
|
|
config := stepca.Config{
|
|
ProvisionerName: "test",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing ca_url")
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingProvisioner", func(t *testing.T) {
|
|
config := stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing provisioner_name")
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_UnreachableCA", func(t *testing.T) {
|
|
config := stepca.Config{
|
|
CAURL: "http://localhost:19999",
|
|
ProvisionerName: "test",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for unreachable CA")
|
|
}
|
|
})
|
|
|
|
t.Run("IssueCertificate_Success", func(t *testing.T) {
|
|
// Generate a test certificate to return in the mock
|
|
testCertPEM, testKeyPEM := generateTestCert(t)
|
|
_ = testKeyPEM
|
|
|
|
// Start a mock step-ca server
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 30,
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, err := generateStepCATestCSR("app.internal.corp")
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate CSR: %v", err)
|
|
}
|
|
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "app.internal.corp",
|
|
SANs: []string{"app.internal.corp"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.CertPEM == "" {
|
|
t.Error("CertPEM is empty")
|
|
}
|
|
if result.Serial == "" {
|
|
t.Error("Serial is empty")
|
|
}
|
|
if result.OrderID == "" {
|
|
t.Error("OrderID is empty")
|
|
}
|
|
|
|
t.Logf("step-ca issued cert: serial=%s", result.Serial)
|
|
})
|
|
|
|
t.Run("IssueCertificate_ServerError", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid token"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for server error response")
|
|
}
|
|
t.Logf("Correctly got error: %v", err)
|
|
})
|
|
|
|
t.Run("RenewCertificate", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("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.Serial == "" {
|
|
t.Error("Serial is empty")
|
|
}
|
|
})
|
|
|
|
t.Run("RevokeCertificate_Success", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
reason := "keyCompromise"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
Reason: &reason,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("RevokeCertificate_ServerError", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"error":"unauthorized"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err == nil {
|
|
t.Fatal("Expected error for server error response")
|
|
}
|
|
})
|
|
|
|
t.Run("GetOrderStatus", func(t *testing.T) {
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
status, err := connector.GetOrderStatus(ctx, "stepca-12345")
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "completed" {
|
|
t.Errorf("Expected status 'completed', got '%s'", status.Status)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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: "Test Certificate",
|
|
},
|
|
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
|
|
}
|
|
|
|
func generateStepCATestCSR(commonName string) (*x509.CertificateRequest, string, error) {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, "", 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 {
|
|
return nil, "", err
|
|
}
|
|
|
|
csr, err := x509.ParseCertificateRequest(csrBytes)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
csrPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
Bytes: csrBytes,
|
|
})
|
|
|
|
return csr, string(csrPEM), nil
|
|
}
|
|
|
|
func TestGenerateProvisionerTokenEphemeralKey(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
// No ProvisionerKeyPath — forces ephemeral key generation
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// This should NOT panic and should return a non-empty token
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
SANs: []string{"test.example.com", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
// We can't test token generation directly since it's unexported,
|
|
// but we can verify issuance with ephemeral key works against mock server
|
|
testCertPEM, _ := generateTestCert(t)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with ephemeral key failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_SimpleFormat(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// Test the simple crt/ca response format
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Simple format: crt and ca fields
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("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 simple format failed: %v", err)
|
|
}
|
|
|
|
if result.CertPEM != testCertPEM {
|
|
t.Errorf("CertPEM mismatch: got %q, want %q", result.CertPEM, testCertPEM)
|
|
}
|
|
if result.ChainPEM != testCertPEM {
|
|
t.Errorf("ChainPEM mismatch: got %q, want %q", result.ChainPEM, testCertPEM)
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_StructuredFormat(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// Test the structured response format
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Structured format with serverPEM and caPEM
|
|
resp := fmt.Sprintf(`{
|
|
"serverPEM": {"certificate": %q},
|
|
"caPEM": {"certificate": %q}
|
|
}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("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 structured format failed: %v", err)
|
|
}
|
|
|
|
if result.CertPEM != testCertPEM {
|
|
t.Errorf("CertPEM mismatch")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_InvalidCertPEM(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Test invalid PEM in response
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Invalid PEM data
|
|
resp := `{"crt": "not a certificate", "ca": ""}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid certificate PEM")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_EmptyCertificate(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Test empty certificate in response
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := `{"crt": "", "ca": ""}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for empty certificate")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_ProvisionerKeyPathNotExist(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ProvisionerKeyPath: "/nonexistent/path/to/key.json",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for non-existent provisioner key path")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_ValidityDaysSet(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
capturedRequest := []byte{}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
// Capture the request to verify NotBefore/NotAfter are set
|
|
var body []byte
|
|
body, _ = io.ReadAll(r.Body)
|
|
capturedRequest = body
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 90,
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// Verify that the request body contained notBefore and notAfter
|
|
if !bytes.Contains(capturedRequest, []byte("notBefore")) || !bytes.Contains(capturedRequest, []byte("notAfter")) {
|
|
t.Errorf("Expected notBefore and notAfter in request body, got: %s", string(capturedRequest))
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_NoReasonProvided(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
// No reason provided — should default to "unspecified"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
Reason: nil,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate without reason failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGenerateCRL_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, err := connector.GenerateCRL(ctx, nil)
|
|
if err == nil {
|
|
t.Fatal("Expected error for GenerateCRL not supported")
|
|
}
|
|
}
|
|
|
|
func TestSignOCSPResponse_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{})
|
|
if err == nil {
|
|
t.Fatal("Expected error for SignOCSPResponse not supported")
|
|
}
|
|
}
|
|
|
|
func TestGetCACertPEM_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, err := connector.GetCACertPEM(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error for GetCACertPEM not supported")
|
|
}
|
|
}
|
|
|
|
func TestGetRenewalInfo_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
result, err := connector.GetRenewalInfo(ctx, "test cert pem")
|
|
if err != nil || result != nil {
|
|
t.Fatalf("Expected (nil, nil) for GetRenewalInfo, got (%v, %v)", result, err)
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_CertChainFormat(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// Test the certChainPEM array response format (multiple certs in array)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Array format with multiple certs (leaf + intermediate + root)
|
|
resp := fmt.Sprintf(`{
|
|
"certChainPEM": [
|
|
{"certificate": %q},
|
|
{"certificate": %q},
|
|
{"certificate": %q}
|
|
]
|
|
}`, testCertPEM, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("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 cert chain format failed: %v", err)
|
|
}
|
|
|
|
// Chain should include intermediate + root (all except first)
|
|
if result.CertPEM != testCertPEM {
|
|
t.Error("Leaf cert mismatch")
|
|
}
|
|
// Chain should include 2 certs (intermediate + root)
|
|
if result.ChainPEM == "" {
|
|
t.Error("Chain should not be empty when multiple certs provided")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_InvalidJSON(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig := json.RawMessage(`{invalid json}`)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid JSON config")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_ContextCancelled(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
// Cancelled context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for cancelled context")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_MalformedResponseJSON(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Malformed JSON response
|
|
w.Write([]byte(`{invalid json}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for malformed response JSON")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_StatusOK(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Test with 200 OK response (alternative to 201 Created)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK) // 200 instead of 201
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("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 200 OK status failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_ErrorReadingBody(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
// Don't write anything (simulate error reading response)
|
|
w.Write([]byte(`Internal error`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err == nil {
|
|
t.Fatal("Expected error for revoke server error")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_NoValidityDays(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
capturedRequest := []byte{}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
// Capture the request to verify behavior with 0 ValidityDays
|
|
var body []byte
|
|
body, _ = io.ReadAll(r.Body)
|
|
capturedRequest = body
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 0, // No validity days set
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("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 0 ValidityDays failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// When ValidityDays is 0, the code doesn't set NotBefore/NotAfter
|
|
// Just verify that the request was captured and processed
|
|
if len(capturedRequest) == 0 {
|
|
t.Error("Expected non-empty captured request")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_HealthCheckError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := stepca.Config{
|
|
CAURL: "http://invalid-url-that-will-not-resolve.local:9999",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for unreachable CA")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_ReadResponseBodyError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Create a response with status 201 but an unreadable body
|
|
// This is hard to simulate with httptest, so we'll just test the normal path
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
testCertPEM, _ := generateTestCert(t)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_BadStatus(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnauthorized) // 401 is neither 200 nor 201
|
|
w.Write([]byte(`{"error":"unauthorized"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for 401 response")
|
|
}
|
|
}
|
|
|
|
func TestRenewCertificate_DelegatesToIssuance(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
callCount := 0
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
callCount++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("renew.example.com")
|
|
renewReq := issuer.RenewalRequest{
|
|
CommonName: "renew.example.com",
|
|
SANs: []string{"renew.example.com", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.RenewCertificate(ctx, renewReq)
|
|
if err != nil {
|
|
t.Fatalf("RenewCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// Should have made exactly 1 call to /sign
|
|
if callCount != 1 {
|
|
t.Errorf("Expected 1 sign call, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestNew_WithRootCertPath(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
// Create a temporary cert file
|
|
testCertPEM, _ := generateTestCert(t)
|
|
tmpFile := os.TempDir() + "/test_ca_cert.pem"
|
|
err := os.WriteFile(tmpFile, []byte(testCertPEM), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test cert: %v", err)
|
|
}
|
|
defer os.Remove(tmpFile)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
RootCertPath: tmpFile,
|
|
}
|
|
|
|
connector := stepca.New(config, logger)
|
|
if connector == nil {
|
|
t.Fatal("Expected non-nil connector")
|
|
}
|
|
}
|
|
|
|
func TestNew_WithInvalidRootCertPath(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
RootCertPath: "/nonexistent/path/to/cert.pem",
|
|
}
|
|
|
|
// Should not panic, just log a warning and fall back to system trust store
|
|
connector := stepca.New(config, logger)
|
|
if connector == nil {
|
|
t.Fatal("Expected non-nil connector")
|
|
}
|
|
}
|
|
|
|
func TestNew_WithNilConfig(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
connector := stepca.New(nil, logger)
|
|
if connector == nil {
|
|
t.Fatal("Expected non-nil connector even with nil config")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_HealthCheck_NotOK(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusServiceUnavailable) // 503 instead of 200
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for non-200 health check")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_MalformedPEM(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Send PEM with invalid base64 or invalid cert
|
|
resp := `{"crt": "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----\n", "ca": ""}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for malformed PEM")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_WithMultipleSANs(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 365,
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("app.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "app.example.com",
|
|
SANs: []string{"app.example.com", "api.example.com", "www.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with multiple SANs failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_NetworkError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "http://localhost:29999", // Port that's not listening
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for network connection failure")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_NetworkError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "http://localhost:29999", // Port that's not listening
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err == nil {
|
|
t.Fatal("Expected error for network connection failure")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_NoServerPEM(t *testing.T) {
|
|
// Test when neither crt/ca nor serverPEM/caPEM nor certChainPEM are present
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Empty response
|
|
resp := `{}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for empty response")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_CreateHealthCheckRequest_Error(t *testing.T) {
|
|
// This is harder to test since we need to create a request with an invalid URL
|
|
// Let's just test with an invalid CAURL that fails to parse
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := stepca.Config{
|
|
CAURL: "https://[invalid-ip]:9000", // Invalid IPv6 format
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid CAURL")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_MarshalSignRequestError(t *testing.T) {
|
|
// This is hard to test since json.Marshal typically doesn't fail for structs
|
|
// We've covered the main paths, so this is a limitation of the testable code
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestRenewCertificate_WithEKUs(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("renew.example.com")
|
|
// RenewalRequest doesn't have EKUs field in the current implementation
|
|
// but we can test with extended request data
|
|
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.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestLoadProvisionerKey_FileNotReadable(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// Test with a provisioner key path that can't be read
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ProvisionerKeyPath: "/root/.ssh/no_such_key", // Permission denied or doesn't exist
|
|
ProvisionerPassword: "password",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
// Error should occur when trying to access the key file
|
|
if err == nil {
|
|
t.Fatal("Expected error when provisioner key file is not accessible")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_GetOrderStatus(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
// GetOrderStatus should return immediately with "completed" status
|
|
status, err := connector.GetOrderStatus(ctx, "some-order-id")
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "completed" {
|
|
t.Errorf("Expected status 'completed', got '%s'", status.Status)
|
|
}
|
|
|
|
if status.OrderID != "some-order-id" {
|
|
t.Errorf("Expected OrderID 'some-order-id', got '%s'", status.OrderID)
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_MarshalRequestError(t *testing.T) {
|
|
// Most marshal failures are hard to trigger, but we can test the happy path
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
reason := "keyCompromise"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "12345678901234567890",
|
|
Reason: &reason,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIntegration_FullLifecycle(t *testing.T) {
|
|
// Integration test covering full certificate lifecycle
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
callCount := struct {
|
|
health int
|
|
sign int
|
|
revoke int
|
|
}{}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
callCount.health++
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/sign":
|
|
callCount.sign++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
case "/revoke":
|
|
callCount.revoke++
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 90,
|
|
}
|
|
|
|
// Test ValidateConfig
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
if err := connector.ValidateConfig(ctx, rawConfig); err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
|
|
if callCount.health != 1 {
|
|
t.Errorf("Expected 1 health check, got %d", callCount.health)
|
|
}
|
|
|
|
// Create a new connector with validated config
|
|
connector = stepca.New(config, logger)
|
|
|
|
// Test IssueCertificate
|
|
_, csrPEM, _ := generateStepCATestCSR("app.internal.corp")
|
|
issueReq := issuer.IssuanceRequest{
|
|
CommonName: "app.internal.corp",
|
|
SANs: []string{"app.internal.corp", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
issueResult, err := connector.IssueCertificate(ctx, issueReq)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if callCount.sign != 1 {
|
|
t.Errorf("Expected 1 sign call, got %d", callCount.sign)
|
|
}
|
|
|
|
if issueResult.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// Test RenewCertificate
|
|
renewReq := issuer.RenewalRequest{
|
|
CommonName: "app.internal.corp",
|
|
SANs: []string{"app.internal.corp", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
renewResult, err := connector.RenewCertificate(ctx, renewReq)
|
|
if err != nil {
|
|
t.Fatalf("RenewCertificate failed: %v", err)
|
|
}
|
|
|
|
if callCount.sign != 2 {
|
|
t.Errorf("Expected 2 sign calls after renewal, got %d", callCount.sign)
|
|
}
|
|
|
|
if renewResult.Serial == "" {
|
|
t.Error("Expected non-empty serial from renewal")
|
|
}
|
|
|
|
// Test RevokeCertificate
|
|
reason := "cessationOfOperation"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: issueResult.Serial,
|
|
Reason: &reason,
|
|
}
|
|
|
|
if err := connector.RevokeCertificate(ctx, revokeReq); err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
|
|
if callCount.revoke != 1 {
|
|
t.Errorf("Expected 1 revoke call, got %d", callCount.revoke)
|
|
}
|
|
|
|
// Test GetOrderStatus
|
|
status, err := connector.GetOrderStatus(ctx, issueResult.OrderID)
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "completed" {
|
|
t.Errorf("Expected status 'completed', got '%s'", status.Status)
|
|
}
|
|
}
|
|
|