mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-11 15:18:57 +00:00
648e2f7ab1
Convert `switch { case r.URL.Path == ... }` to `switch r.URL.Path { ... }`
in Vault and DigiCert connector tests to pass golangci-lint CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
528 lines
14 KiB
Go
528 lines
14 KiB
Go
package vault_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"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/vault"
|
|
)
|
|
|
|
func TestVaultConnector(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 == "/v1/sys/health" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"initialized":true,"sealed":false,"standby":false}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.test-token-12345",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
TTL: "8760h",
|
|
}
|
|
|
|
connector := vault.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingAddr", func(t *testing.T) {
|
|
config := vault.Config{
|
|
Token: "s.test-token",
|
|
Role: "web-certs",
|
|
}
|
|
|
|
connector := vault.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing addr")
|
|
}
|
|
if !strings.Contains(err.Error(), "addr is required") {
|
|
t.Errorf("Expected addr required error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingToken", func(t *testing.T) {
|
|
config := vault.Config{
|
|
Addr: "https://vault.example.com:8200",
|
|
Role: "web-certs",
|
|
}
|
|
|
|
connector := vault.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing token")
|
|
}
|
|
if !strings.Contains(err.Error(), "token is required") {
|
|
t.Errorf("Expected token required error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingRole", func(t *testing.T) {
|
|
config := vault.Config{
|
|
Addr: "https://vault.example.com:8200",
|
|
Token: "s.test-token",
|
|
}
|
|
|
|
connector := vault.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing role")
|
|
}
|
|
if !strings.Contains(err.Error(), "role is required") {
|
|
t.Errorf("Expected role required error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_UnreachableVault", func(t *testing.T) {
|
|
config := vault.Config{
|
|
Addr: "http://localhost:19999",
|
|
Token: "s.test-token",
|
|
Role: "web-certs",
|
|
}
|
|
|
|
connector := vault.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for unreachable Vault")
|
|
}
|
|
})
|
|
|
|
t.Run("IssueCertificate_Success", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/v1/sys/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"initialized":true,"sealed":false}`))
|
|
case strings.HasPrefix(r.URL.Path, "/v1/pki/sign/"):
|
|
// Verify auth header
|
|
if r.Header.Get("X-Vault-Token") != "s.test-token" {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"errors":["permission denied"]}`))
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
resp := fmt.Sprintf(`{
|
|
"data": {
|
|
"certificate": %q,
|
|
"issuing_ca": %q,
|
|
"ca_chain": [%q],
|
|
"serial_number": "aa:bb:cc:dd:ee:ff",
|
|
"expiration": 1893456000
|
|
}
|
|
}`, testCertPEM, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
TTL: "8760h",
|
|
}
|
|
connector := vault.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 is empty")
|
|
}
|
|
if result.Serial == "" {
|
|
t.Error("Serial is empty")
|
|
}
|
|
if result.OrderID == "" {
|
|
t.Error("OrderID is empty")
|
|
}
|
|
if !strings.HasPrefix(result.OrderID, "vault-") {
|
|
t.Errorf("Expected OrderID to start with 'vault-', got '%s'", result.OrderID)
|
|
}
|
|
// Verify serial normalization (colons replaced with dashes)
|
|
if strings.Contains(result.Serial, ":") {
|
|
t.Errorf("Serial should not contain colons, got '%s'", result.Serial)
|
|
}
|
|
t.Logf("Vault issued cert: serial=%s, orderID=%s", result.Serial, result.OrderID)
|
|
})
|
|
|
|
t.Run("IssueCertificate_ServerError", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/v1/sys/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case strings.HasPrefix(r.URL.Path, "/v1/pki/sign/"):
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"errors":["invalid CSR"]}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.New(config, logger)
|
|
|
|
_, csrPEM := generateTestCSR(t, "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")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid CSR") {
|
|
t.Logf("Got error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("IssueCertificate_Forbidden", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/v1/sys/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case strings.HasPrefix(r.URL.Path, "/v1/pki/sign/"):
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"errors":["permission denied"]}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.bad-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.New(config, logger)
|
|
|
|
_, csrPEM := generateTestCSR(t, "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 forbidden response")
|
|
}
|
|
if !strings.Contains(err.Error(), "permission denied") {
|
|
t.Logf("Got error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("RenewCertificate_Success", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.URL.Path == "/v1/sys/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case strings.HasPrefix(r.URL.Path, "/v1/pki/sign/"):
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
resp := fmt.Sprintf(`{
|
|
"data": {
|
|
"certificate": %q,
|
|
"issuing_ca": %q,
|
|
"serial_number": "11:22:33:44:55:66",
|
|
"expiration": 1893456000
|
|
}
|
|
}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.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.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 "/v1/sys/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/v1/pki/revoke":
|
|
// Verify token
|
|
if r.Header.Get("X-Vault-Token") == "" {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"data":{"revocation_time":1234567890}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.New(config, logger)
|
|
|
|
reason := "keyCompromise"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "aa-bb-cc-dd-ee-ff",
|
|
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 "/v1/sys/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/v1/pki/revoke":
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"errors":["serial not found"]}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.New(config, logger)
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "00-00-00-00",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err == nil {
|
|
t.Fatal("Expected error for server error response")
|
|
}
|
|
})
|
|
|
|
t.Run("GetCACertPEM_Success", func(t *testing.T) {
|
|
expectedPEM := "-----BEGIN CERTIFICATE-----\nTESTCA\n-----END CERTIFICATE-----\n"
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v1/pki/ca/pem":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(expectedPEM))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &vault.Config{
|
|
Addr: srv.URL,
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.New(config, logger)
|
|
|
|
caPEM, err := connector.GetCACertPEM(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCACertPEM failed: %v", err)
|
|
}
|
|
|
|
if caPEM != expectedPEM {
|
|
t.Errorf("Expected CA PEM %q, got %q", expectedPEM, caPEM)
|
|
}
|
|
})
|
|
|
|
t.Run("GetOrderStatus_Synchronous", func(t *testing.T) {
|
|
config := &vault.Config{
|
|
Addr: "https://vault.example.com:8200",
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.New(config, logger)
|
|
|
|
status, err := connector.GetOrderStatus(ctx, "vault-aa-bb-cc")
|
|
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 != "vault-aa-bb-cc" {
|
|
t.Errorf("Expected OrderID 'vault-aa-bb-cc', got '%s'", status.OrderID)
|
|
}
|
|
})
|
|
|
|
t.Run("GetRenewalInfo_ReturnsNil", func(t *testing.T) {
|
|
config := &vault.Config{
|
|
Addr: "https://vault.example.com:8200",
|
|
Token: "s.test-token",
|
|
Mount: "pki",
|
|
Role: "web-certs",
|
|
}
|
|
connector := vault.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 Vault")
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|