mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 15:58:59 +00:00
dd79096b70
Add three new issuer connectors completing commercial and open-source CA coverage. Entrust uses mTLS client certificate auth with sync/async issuance. GlobalSign Atlas uses mTLS + API key/secret dual auth with serial-based tracking. EJBCA supports dual auth (mTLS or OAuth2) for self-hosted Keyfactor CAs. Each connector implements the full issuer.Connector interface (9 methods), includes httptest-based unit tests (~14 each), and follows established patterns (injectable HTTP clients, RFC 5280 revocation reason mapping, CRL/OCSP delegated to CA). Also includes: issuer factory cases, env var seeding, config structs, domain types, seed data (3 rows, all disabled), OpenAPI enum updates, frontend issuer catalog entries with config fields, and full docs (connectors.md, architecture.md, features.md, README). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
677 lines
19 KiB
Go
677 lines
19 KiB
Go
package globalsign_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shankar0123/certctl/internal/connector/issuer"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/globalsign"
|
|
)
|
|
|
|
func TestGlobalSignConnector(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 == "/v2/certificates" && r.Method == http.MethodGet {
|
|
if r.Header.Get("ApiKey") == "gs-test-key" && r.Header.Get("ApiSecret") == "gs-test-secret" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"certificates":[]}`))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"error":"invalid credentials"}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := globalsign.Config{
|
|
APIUrl: srv.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
ClientCertPath: "unused_for_httptest",
|
|
ClientKeyPath: "unused_for_httptest",
|
|
}
|
|
|
|
connector := globalsign.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
|
|
// This test will fail at mTLS validation since httptest.NewServer doesn't do TLS.
|
|
// We're mainly checking JSON parsing and header validation.
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil || !strings.Contains(err.Error(), "certificate") {
|
|
t.Logf("ValidateConfig correctly failed on cert loading: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingAPIUrl", func(t *testing.T) {
|
|
config := globalsign.Config{
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
ClientCertPath: "/tmp/cert.pem",
|
|
ClientKeyPath: "/tmp/key.pem",
|
|
}
|
|
|
|
connector := globalsign.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing api_url")
|
|
}
|
|
if !strings.Contains(err.Error(), "api_url") {
|
|
t.Errorf("Expected api_url error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingAPIKey", func(t *testing.T) {
|
|
config := globalsign.Config{
|
|
APIUrl: "https://api.example.com",
|
|
APISecret: "gs-test-secret",
|
|
ClientCertPath: "/tmp/cert.pem",
|
|
ClientKeyPath: "/tmp/key.pem",
|
|
}
|
|
|
|
connector := globalsign.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing api_key")
|
|
}
|
|
if !strings.Contains(err.Error(), "api_key") {
|
|
t.Errorf("Expected api_key error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingAPISecret", func(t *testing.T) {
|
|
config := globalsign.Config{
|
|
APIUrl: "https://api.example.com",
|
|
APIKey: "gs-test-key",
|
|
ClientCertPath: "/tmp/cert.pem",
|
|
ClientKeyPath: "/tmp/key.pem",
|
|
}
|
|
|
|
connector := globalsign.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing api_secret")
|
|
}
|
|
if !strings.Contains(err.Error(), "api_secret") {
|
|
t.Errorf("Expected api_secret error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingClientCertPath", func(t *testing.T) {
|
|
config := globalsign.Config{
|
|
APIUrl: "https://api.example.com",
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
ClientKeyPath: "/tmp/key.pem",
|
|
}
|
|
|
|
connector := globalsign.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing client_cert_path")
|
|
}
|
|
if !strings.Contains(err.Error(), "client_cert_path") {
|
|
t.Errorf("Expected client_cert_path error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingClientKeyPath", func(t *testing.T) {
|
|
config := globalsign.Config{
|
|
APIUrl: "https://api.example.com",
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
ClientCertPath: "/tmp/cert.pem",
|
|
}
|
|
|
|
connector := globalsign.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing client_key_path")
|
|
}
|
|
if !strings.Contains(err.Error(), "client_key_path") {
|
|
t.Errorf("Expected client_key_path error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("IssueCertificate_Immediate", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
testChainPEM, _ := generateTestCert(t)
|
|
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
|
// Verify auth headers are present
|
|
if r.Header.Get("ApiKey") != "gs-test-key" {
|
|
t.Error("ApiKey header missing or incorrect")
|
|
}
|
|
if r.Header.Get("ApiSecret") != "gs-test-secret" {
|
|
t.Error("ApiSecret header missing or incorrect")
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write([]byte(fmt.Sprintf(`{
|
|
"serial_number": "12345678901234567890",
|
|
"status": "issued",
|
|
"certificate": %s,
|
|
"chain": %s
|
|
}`, mustMarshalJSON(testCertPEM), mustMarshalJSON(testChainPEM))))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
_, csrPEM := generateTestCSR(t, "app.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "app.example.com",
|
|
SANs: []string{"app.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 != "12345678901234567890" {
|
|
t.Errorf("Expected OrderID '12345678901234567890', got '%s'", result.OrderID)
|
|
}
|
|
t.Logf("GlobalSign issued cert: serial=%s, orderID=%s", result.Serial, result.OrderID)
|
|
})
|
|
|
|
t.Run("IssueCertificate_Pending", func(t *testing.T) {
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write([]byte(`{
|
|
"serial_number": "98765432109876543210",
|
|
"status": "pending"
|
|
}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
_, 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.CertPEM != "" {
|
|
t.Error("CertPEM should be empty for pending issuance")
|
|
}
|
|
if result.OrderID != "98765432109876543210" {
|
|
t.Errorf("Expected OrderID '98765432109876543210', got '%s'", result.OrderID)
|
|
}
|
|
t.Logf("GlobalSign order pending: orderID=%s", result.OrderID)
|
|
})
|
|
|
|
t.Run("IssueCertificate_Error", func(t *testing.T) {
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error": "invalid CSR format"}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
_, csrPEM := generateTestCSR(t, "bad.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "bad.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for bad request")
|
|
}
|
|
t.Logf("Expected error received: %v", err)
|
|
})
|
|
|
|
t.Run("GetOrderStatus_Issued", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
testChainPEM, _ := generateTestCert(t)
|
|
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/v2/certificates/12345") && r.Method == http.MethodGet {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(fmt.Sprintf(`{
|
|
"serial_number": "12345",
|
|
"status": "issued",
|
|
"certificate": %s,
|
|
"chain": %s
|
|
}`, mustMarshalJSON(testCertPEM), mustMarshalJSON(testChainPEM))))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
status, err := connector.GetOrderStatus(ctx, "12345")
|
|
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")
|
|
}
|
|
t.Logf("Order status: %s", status.Status)
|
|
})
|
|
|
|
t.Run("GetOrderStatus_Pending", func(t *testing.T) {
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/v2/certificates/98765") && r.Method == http.MethodGet {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{
|
|
"serial_number": "98765",
|
|
"status": "pending"
|
|
}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
status, err := connector.GetOrderStatus(ctx, "98765")
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "pending" {
|
|
t.Errorf("Expected status 'pending', got '%s'", status.Status)
|
|
}
|
|
if status.Message == nil {
|
|
t.Error("Message should not be nil for pending status")
|
|
}
|
|
t.Logf("Order status: %s, message: %s", status.Status, *status.Message)
|
|
})
|
|
|
|
t.Run("RenewCertificate_Success", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
testChainPEM, _ := generateTestCert(t)
|
|
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write([]byte(fmt.Sprintf(`{
|
|
"serial_number": "renewal123",
|
|
"status": "issued",
|
|
"certificate": %s,
|
|
"chain": %s
|
|
}`, mustMarshalJSON(testCertPEM), mustMarshalJSON(testChainPEM))))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
_, csrPEM := generateTestCSR(t, "renew.example.com")
|
|
req := issuer.RenewalRequest{
|
|
CommonName: "renew.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.RenewCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("RenewCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Serial should not be empty")
|
|
}
|
|
t.Logf("Certificate renewed: serial=%s", result.Serial)
|
|
})
|
|
|
|
t.Run("RevokeCertificate_Success", func(t *testing.T) {
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/v2/certificates/") && strings.HasSuffix(r.URL.Path, "/revoke") && r.Method == http.MethodPut {
|
|
// Verify auth headers
|
|
if r.Header.Get("ApiKey") != "gs-test-key" {
|
|
t.Error("ApiKey header missing")
|
|
}
|
|
if r.Header.Get("ApiSecret") != "gs-test-secret" {
|
|
t.Error("ApiSecret header missing")
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
req := issuer.RevocationRequest{
|
|
Serial: "12345678901234567890",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
|
|
t.Logf("Certificate revoked: serial=%s", req.Serial)
|
|
})
|
|
|
|
t.Run("RevokeCertificate_Error", func(t *testing.T) {
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/v2/certificates/") && strings.HasSuffix(r.URL.Path, "/revoke") && r.Method == http.MethodPut {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte(`{"error": "certificate not found"}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
req := issuer.RevocationRequest{
|
|
Serial: "nonexistent",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for nonexistent certificate")
|
|
}
|
|
t.Logf("Expected error received: %v", err)
|
|
})
|
|
|
|
t.Run("AuthHeaders_OnAllRequests", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
testChainPEM, _ := generateTestCert(t)
|
|
authHeadersChecked := 0
|
|
|
|
httpClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
|
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Check for auth headers on every request
|
|
if r.Header.Get("ApiKey") == "gs-test-key" && r.Header.Get("ApiSecret") == "gs-test-secret" {
|
|
authHeadersChecked++
|
|
}
|
|
|
|
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
|
w.WriteHeader(http.StatusCreated)
|
|
w.Write([]byte(fmt.Sprintf(`{
|
|
"serial_number": "auth123",
|
|
"status": "issued",
|
|
"certificate": %s,
|
|
"chain": %s
|
|
}`, mustMarshalJSON(testCertPEM), mustMarshalJSON(testChainPEM))))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer mockServer.Close()
|
|
|
|
config := &globalsign.Config{
|
|
APIUrl: mockServer.URL,
|
|
APIKey: "gs-test-key",
|
|
APISecret: "gs-test-secret",
|
|
}
|
|
|
|
connector := globalsign.NewWithHTTPClient(config, logger, httpClient)
|
|
|
|
_, csrPEM := generateTestCSR(t, "auth.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "auth.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if authHeadersChecked < 1 {
|
|
t.Errorf("Auth headers not found on request")
|
|
}
|
|
t.Logf("Auth headers verified on %d request(s)", authHeadersChecked)
|
|
})
|
|
}
|
|
|
|
// generateTestCert generates a self-signed test certificate and returns PEM strings.
|
|
func generateTestCert(t *testing.T) (certPEM string, keyPEM string) {
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
|
Subject: pkix.Name{
|
|
CommonName: "test.example.com",
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
},
|
|
DNSNames: []string{"test.example.com"},
|
|
}
|
|
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create certificate: %v", err)
|
|
}
|
|
|
|
certBlock := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: certBytes,
|
|
})
|
|
|
|
privKeyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal private key: %v", err)
|
|
}
|
|
|
|
keyBlock := pem.EncodeToMemory(&pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: privKeyBytes,
|
|
})
|
|
|
|
return string(certBlock), string(keyBlock)
|
|
}
|
|
|
|
// generateTestCSR generates a test certificate signing request.
|
|
func generateTestCSR(t *testing.T, commonName string) (csrPEM string, keyPEM string) {
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate RSA key: %v", err)
|
|
}
|
|
|
|
template := &x509.CertificateRequest{
|
|
Subject: pkix.Name{
|
|
CommonName: commonName,
|
|
},
|
|
DNSNames: []string{commonName},
|
|
}
|
|
|
|
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, priv)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create CSR: %v", err)
|
|
}
|
|
|
|
csrBlock := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
Bytes: csrBytes,
|
|
})
|
|
|
|
privKeyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal private key: %v", err)
|
|
}
|
|
|
|
keyBlock := pem.EncodeToMemory(&pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: privKeyBytes,
|
|
})
|
|
|
|
return string(csrBlock), string(keyBlock)
|
|
}
|
|
|
|
// mustMarshalJSON marshals a value to JSON string, panicking on error.
|
|
// Used to safely embed PEM data in JSON responses.
|
|
func mustMarshalJSON(v interface{}) string {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to marshal JSON: %v", err))
|
|
}
|
|
return string(b)
|
|
}
|