mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:11:31 +00:00
security(globalsign): remove InsecureSkipVerify and pin CA pool (H-5)
The GlobalSign Atlas HVCA connector previously used InsecureSkipVerify:true on its mTLS TLS config, disabling server certificate validation and defeating the purpose of the client-side mTLS handshake. This was a CWE-295 Improper Certificate Validation vulnerability silently degrading trust on every production call to GlobalSign's signing API. Remediation (per H-5 audit finding, Lens 4.4): - Remove InsecureSkipVerify from all three http.Client construction sites (ValidateConfig, getHTTPClient, and legacy initialisation path). - Introduce buildServerTLSConfig() helper that constructs tls.Config with MinVersion: tls.VersionTLS12 (addresses adjacent L-1 recommendation). - New optional config field `server_ca_path` (env: CERTCTL_GLOBALSIGN_SERVER_CA_PATH). When unset the connector trusts the system root CA bundle (correct default for GlobalSign's publicly-trusted HVCA endpoints). When set the bundle is loaded via x509.NewCertPool() + AppendCertsFromPEM, and only those roots are trusted (supports private HVCA deployments and defence-in-depth root pinning). - Error wrapping chain: "failed to read server CA bundle at %s" and "no valid PEM certificates found in server CA bundle at %s" surface config problems at ValidateConfig time instead of silently failing at request time. Docs, config, service env-seed, and GUI issuer type definition updated to expose the new field. Tests: 9 dead `InsecureSkipVerify: true` client TLSClientConfig blocks (no-ops against httptest.NewServer plain-HTTP) replaced with bare http.Client; new TestGlobalSign_ServerTLSConfig covers pinned-CA trust, untrusted-server rejection, missing-file and invalid-PEM error paths. Verification: - go build ./... clean - go vet ./... clean - go test -race ./internal/connector/issuer/globalsign/... ./internal/config/... ./internal/service/... ok - go test ./... (excluding testcontainers-gated repo layer) ok - golangci-lint run ./... 0 issues - govulncheck ./... 0 reachable vulns - Per-layer coverage: service 68.7% (≥55), handler 83.6% (≥60), domain 82.0% (≥40), middleware 63.8% (≥30) - globalsign package coverage: 75.9% - Invariant sweep: 0 InsecureSkipVerify references remain in globalsign package (only a test-file comment documenting the removal).
This commit is contained in:
@@ -465,9 +465,12 @@ GlobalSign Atlas High Volume CA REST API with dual authentication: mTLS for the
|
||||
| `CERTCTL_GLOBALSIGN_API_SECRET` | Yes | — | API secret for request authentication |
|
||||
| `CERTCTL_GLOBALSIGN_CLIENT_CERT_PATH` | Yes | — | Path to mTLS client certificate PEM |
|
||||
| `CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH` | Yes | — | Path to mTLS client private key PEM |
|
||||
| `CERTCTL_GLOBALSIGN_SERVER_CA_PATH` | No | system trust store | PEM bundle used to verify the Atlas API server certificate. Set this for private/lab Atlas deployments whose server TLS chain is not in the host's default trust bundle. |
|
||||
|
||||
**Authentication:** Dual — mTLS client certificate for TLS handshake plus `X-API-Key` and `X-API-Secret` headers on every request.
|
||||
|
||||
**TLS verification:** The connector always verifies the server certificate. When `server_ca_path` is set, the PEM bundle at that path is used as the trust anchor; otherwise the host's system trust store is used. TLS 1.2 is the minimum protocol version.
|
||||
|
||||
**Issuance model:** `POST /v2/certificates` returns a serial number. Certificate PEM is available after validation completes. Typically resolves within seconds for DV. `GetOrderStatus` polls the certificate endpoint.
|
||||
|
||||
**Note:** CRL and OCSP are managed by GlobalSign. certctl records revocations locally and notifies GlobalSign via `PUT /v2/certificates/{serial}/revoke`.
|
||||
|
||||
@@ -116,6 +116,14 @@ type GlobalSignConfig struct {
|
||||
// ClientKeyPath is the path to the mTLS client private key PEM file.
|
||||
// Setting: CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH environment variable.
|
||||
ClientKeyPath string
|
||||
|
||||
// ServerCAPath is the optional path to a PEM file containing the CA
|
||||
// certificate(s) used to verify the GlobalSign Atlas HVCA API server
|
||||
// certificate. If empty, the system trust store is used. Set this
|
||||
// for private/lab Atlas deployments whose server TLS chain is not
|
||||
// present in the host's default trust bundle.
|
||||
// Setting: CERTCTL_GLOBALSIGN_SERVER_CA_PATH environment variable.
|
||||
ServerCAPath string
|
||||
}
|
||||
|
||||
// EJBCAConfig contains EJBCA (Keyfactor) issuer connector configuration.
|
||||
@@ -887,6 +895,7 @@ func Load() (*Config, error) {
|
||||
APISecret: getEnv("CERTCTL_GLOBALSIGN_API_SECRET", ""),
|
||||
ClientCertPath: getEnv("CERTCTL_GLOBALSIGN_CLIENT_CERT_PATH", ""),
|
||||
ClientKeyPath: getEnv("CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH", ""),
|
||||
ServerCAPath: getEnv("CERTCTL_GLOBALSIGN_SERVER_CA_PATH", ""),
|
||||
},
|
||||
EJBCA: EJBCAConfig{
|
||||
APIUrl: getEnv("CERTCTL_EJBCA_API_URL", ""),
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -64,6 +65,14 @@ type Config struct {
|
||||
// Must match the certificate in ClientCertPath.
|
||||
// Required. Set via CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH environment variable.
|
||||
ClientKeyPath string `json:"client_key_path"`
|
||||
|
||||
// ServerCAPath is the filesystem path to a PEM file containing the CA
|
||||
// certificate(s) used to verify the GlobalSign Atlas HVCA API server certificate.
|
||||
// Optional. If empty, the system trust store is used. This option exists for
|
||||
// private/lab deployments of GlobalSign Atlas that terminate TLS with an
|
||||
// internal CA not present in the host's default trust bundle.
|
||||
// Set via CERTCTL_GLOBALSIGN_SERVER_CA_PATH environment variable.
|
||||
ServerCAPath string `json:"server_ca_path,omitempty"`
|
||||
}
|
||||
|
||||
// Connector implements the issuer.Connector interface for GlobalSign Atlas HVCA.
|
||||
@@ -153,14 +162,12 @@ func (c *Connector) ValidateConfig(ctx context.Context, rawConfig json.RawMessag
|
||||
return fmt.Errorf("failed to load GlobalSign client certificate: %w", err)
|
||||
}
|
||||
|
||||
// Create an mTLS client for validation
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
// InsecureSkipVerify=true allows testing against self-signed server certs.
|
||||
// In production, GlobalSign's API uses a proper certificate chain.
|
||||
// This matches the pattern used by other connectors (F5, network scanner, etc.)
|
||||
// that also need to bypass hostname verification for internal/lab environments.
|
||||
InsecureSkipVerify: true,
|
||||
// Build a verifying mTLS TLS config. If ServerCAPath is set, that PEM
|
||||
// bundle is used as the trust anchor for the server certificate;
|
||||
// otherwise the system trust store is used. TLS 1.2 is the minimum.
|
||||
tlsConfig, err := buildServerTLSConfig(&cfg, cert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build GlobalSign TLS config: %w", err)
|
||||
}
|
||||
|
||||
validationClient := &http.Client{
|
||||
@@ -225,9 +232,9 @@ func (c *Connector) getHTTPClient(ctx context.Context) (*http.Client, error) {
|
||||
return nil, fmt.Errorf("failed to load GlobalSign client certificate: %w", err)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
InsecureSkipVerify: true,
|
||||
tlsConfig, err := buildServerTLSConfig(c.config, cert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build GlobalSign TLS config: %w", err)
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
@@ -238,6 +245,38 @@ func (c *Connector) getHTTPClient(ctx context.Context) (*http.Client, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildServerTLSConfig returns a TLS configuration for the GlobalSign Atlas
|
||||
// HVCA API client. It always verifies the server certificate. When
|
||||
// cfg.ServerCAPath is set, the PEM bundle at that path is used as the
|
||||
// trust anchor (enables pinning a private/lab CA); otherwise the host's
|
||||
// system trust store is used. TLS 1.2 is the minimum protocol version.
|
||||
//
|
||||
// This helper is the single source of truth for both the ValidateConfig
|
||||
// probe client and the steady-state getHTTPClient production client, so
|
||||
// any future TLS policy change applies uniformly.
|
||||
func buildServerTLSConfig(cfg *Config, clientCert tls.Certificate) (*tls.Config, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
if cfg.ServerCAPath != "" {
|
||||
caPEM, err := os.ReadFile(cfg.ServerCAPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read server CA bundle at %s: %w", cfg.ServerCAPath, err)
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caPEM) {
|
||||
return nil, fmt.Errorf("no valid PEM certificates found in server CA bundle at %s", cfg.ServerCAPath)
|
||||
}
|
||||
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// IssueCertificate submits a certificate order to GlobalSign Atlas HVCA.
|
||||
// Returns the serial number immediately; typically the cert is available within seconds (DV) to minutes (OV).
|
||||
func (c *Connector) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
@@ -161,11 +160,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
testCertPEM, _ := generateTestCert(t)
|
||||
testChainPEM, _ := generateTestCert(t)
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
||||
@@ -223,11 +218,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("IssueCertificate_Pending", func(t *testing.T) {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
||||
@@ -271,11 +262,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("IssueCertificate_Error", func(t *testing.T) {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
||||
@@ -312,11 +299,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
testCertPEM, _ := generateTestCert(t)
|
||||
testChainPEM, _ := generateTestCert(t)
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
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 {
|
||||
@@ -356,11 +339,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("GetOrderStatus_Pending", func(t *testing.T) {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
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 {
|
||||
@@ -401,11 +380,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
testCertPEM, _ := generateTestCert(t)
|
||||
testChainPEM, _ := generateTestCert(t)
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost {
|
||||
@@ -448,11 +423,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("RevokeCertificate_Success", func(t *testing.T) {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
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 {
|
||||
@@ -492,11 +463,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("RevokeCertificate_Error", func(t *testing.T) {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
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 {
|
||||
@@ -532,11 +499,7 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
testChainPEM, _ := generateTestCert(t)
|
||||
authHeadersChecked := 0
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
httpClient := &http.Client{}
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check for auth headers on every request
|
||||
@@ -584,6 +547,177 @@ func TestGlobalSignConnector(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestGlobalSign_ServerTLSConfig exercises the server-side TLS verification
|
||||
// policy added by H-5. The connector must always verify the GlobalSign Atlas
|
||||
// HVCA API server certificate: by default against the host's system trust
|
||||
// store, and when ServerCAPath is set, against the pinned PEM bundle at that
|
||||
// path. InsecureSkipVerify is no longer reachable from any production code path.
|
||||
func TestGlobalSign_ServerTLSConfig(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
// writeClientMTLS generates a throwaway client cert+key pair and writes them
|
||||
// to disk. ValidateConfig requires valid ClientCertPath / ClientKeyPath files
|
||||
// before it reaches the server-CA validation path under test.
|
||||
writeClientMTLS := func(t *testing.T) (certPath, keyPath string) {
|
||||
t.Helper()
|
||||
certPEM, keyPEM := generateTestCert(t)
|
||||
dir := t.TempDir()
|
||||
certPath = dir + "/client-cert.pem"
|
||||
keyPath = dir + "/client-key.pem"
|
||||
if err := os.WriteFile(certPath, []byte(certPEM), 0600); err != nil {
|
||||
t.Fatalf("failed to write client cert: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, []byte(keyPEM), 0600); err != nil {
|
||||
t.Fatalf("failed to write client key: %v", err)
|
||||
}
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
// certToPEM re-encodes a parsed certificate as a PEM block for trust-store
|
||||
// pinning. httptest.NewTLSServer.Certificate() returns the server's self-
|
||||
// signed cert; pinning that cert trusts exactly that one server.
|
||||
certToPEM := func(t *testing.T, cert *x509.Certificate) string {
|
||||
t.Helper()
|
||||
return string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}))
|
||||
}
|
||||
|
||||
t.Run("PinnedCA_TrustsExpectedServer", func(t *testing.T) {
|
||||
// Mock Atlas API served over HTTPS with a self-signed cert. We pin
|
||||
// that cert's PEM as the client's trust anchor; the validation probe
|
||||
// should succeed because the pinned pool contains the server's issuer.
|
||||
srv := httptest.NewTLSServer(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)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
caPEM := certToPEM(t, srv.Certificate())
|
||||
caPath := t.TempDir() + "/atlas-ca.pem"
|
||||
if err := os.WriteFile(caPath, []byte(caPEM), 0600); err != nil {
|
||||
t.Fatalf("failed to write pinned CA: %v", err)
|
||||
}
|
||||
|
||||
clientCert, clientKey := writeClientMTLS(t)
|
||||
config := globalsign.Config{
|
||||
APIUrl: srv.URL,
|
||||
APIKey: "gs-test-key",
|
||||
APISecret: "gs-test-secret",
|
||||
ClientCertPath: clientCert,
|
||||
ClientKeyPath: clientKey,
|
||||
ServerCAPath: caPath,
|
||||
}
|
||||
|
||||
connector := globalsign.New(&config, logger)
|
||||
rawConfig, _ := json.Marshal(config)
|
||||
if err := connector.ValidateConfig(ctx, rawConfig); err != nil {
|
||||
t.Fatalf("ValidateConfig with pinned CA should succeed, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PinnedCA_RejectsUntrustedServer", func(t *testing.T) {
|
||||
// Mock server presents its own self-signed cert; we pin an UNRELATED
|
||||
// cert as the trust anchor. The TLS handshake must fail before any
|
||||
// request is sent — this is exactly what H-5 remediates.
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
unrelatedPEM, _ := generateTestCert(t)
|
||||
caPath := t.TempDir() + "/unrelated-ca.pem"
|
||||
if err := os.WriteFile(caPath, []byte(unrelatedPEM), 0600); err != nil {
|
||||
t.Fatalf("failed to write unrelated CA: %v", err)
|
||||
}
|
||||
|
||||
clientCert, clientKey := writeClientMTLS(t)
|
||||
config := globalsign.Config{
|
||||
APIUrl: srv.URL,
|
||||
APIKey: "gs-test-key",
|
||||
APISecret: "gs-test-secret",
|
||||
ClientCertPath: clientCert,
|
||||
ClientKeyPath: clientKey,
|
||||
ServerCAPath: caPath,
|
||||
}
|
||||
|
||||
connector := globalsign.New(&config, logger)
|
||||
rawConfig, _ := json.Marshal(config)
|
||||
err := connector.ValidateConfig(ctx, rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("ValidateConfig must fail when the server cert is not signed by the pinned CA")
|
||||
}
|
||||
// The failure must originate from TLS verification, not from any other path.
|
||||
if !strings.Contains(err.Error(), "x509") &&
|
||||
!strings.Contains(err.Error(), "certificate") &&
|
||||
!strings.Contains(err.Error(), "unknown authority") {
|
||||
t.Errorf("expected TLS verification error, got: %v", err)
|
||||
}
|
||||
t.Logf("Untrusted server cert correctly rejected: %v", err)
|
||||
})
|
||||
|
||||
t.Run("ServerCAPath_MissingFile", func(t *testing.T) {
|
||||
clientCert, clientKey := writeClientMTLS(t)
|
||||
config := globalsign.Config{
|
||||
APIUrl: "https://example.invalid",
|
||||
APIKey: "gs-test-key",
|
||||
APISecret: "gs-test-secret",
|
||||
ClientCertPath: clientCert,
|
||||
ClientKeyPath: clientKey,
|
||||
ServerCAPath: "/nonexistent/path/to/ca.pem",
|
||||
}
|
||||
|
||||
connector := globalsign.New(&config, logger)
|
||||
rawConfig, _ := json.Marshal(config)
|
||||
err := connector.ValidateConfig(ctx, rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("ValidateConfig must fail when ServerCAPath points to a missing file")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to read server CA bundle") {
|
||||
t.Errorf("expected 'failed to read server CA bundle' error, got: %v", err)
|
||||
}
|
||||
t.Logf("Missing server CA file correctly rejected: %v", err)
|
||||
})
|
||||
|
||||
t.Run("ServerCAPath_InvalidPEM", func(t *testing.T) {
|
||||
clientCert, clientKey := writeClientMTLS(t)
|
||||
badCAPath := t.TempDir() + "/garbage.pem"
|
||||
if err := os.WriteFile(badCAPath, []byte("this is not a PEM certificate at all"), 0600); err != nil {
|
||||
t.Fatalf("failed to write garbage file: %v", err)
|
||||
}
|
||||
|
||||
config := globalsign.Config{
|
||||
APIUrl: "https://example.invalid",
|
||||
APIKey: "gs-test-key",
|
||||
APISecret: "gs-test-secret",
|
||||
ClientCertPath: clientCert,
|
||||
ClientKeyPath: clientKey,
|
||||
ServerCAPath: badCAPath,
|
||||
}
|
||||
|
||||
connector := globalsign.New(&config, logger)
|
||||
rawConfig, _ := json.Marshal(config)
|
||||
err := connector.ValidateConfig(ctx, rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("ValidateConfig must fail when ServerCAPath contains no valid PEM certificates")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no valid PEM certificates") {
|
||||
t.Errorf("expected 'no valid PEM certificates' error, got: %v", err)
|
||||
}
|
||||
t.Logf("Invalid PEM correctly rejected: %v", err)
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
+14
-10
@@ -577,17 +577,21 @@ func (s *IssuerService) buildEnvVarSeeds(cfg *config.Config) []*domain.Issuer {
|
||||
|
||||
// Conditional: GlobalSign — only seed if API URL and API key are set
|
||||
if cfg.GlobalSign.APIUrl != "" && cfg.GlobalSign.APIKey != "" {
|
||||
globalSignConfig := map[string]interface{}{
|
||||
"api_url": cfg.GlobalSign.APIUrl,
|
||||
"api_key": cfg.GlobalSign.APIKey,
|
||||
"api_secret": cfg.GlobalSign.APISecret,
|
||||
"client_cert_path": cfg.GlobalSign.ClientCertPath,
|
||||
"client_key_path": cfg.GlobalSign.ClientKeyPath,
|
||||
}
|
||||
if cfg.GlobalSign.ServerCAPath != "" {
|
||||
globalSignConfig["server_ca_path"] = cfg.GlobalSign.ServerCAPath
|
||||
}
|
||||
seeds = append(seeds, &domain.Issuer{
|
||||
ID: "iss-globalsign",
|
||||
Name: "GlobalSign Atlas",
|
||||
Type: domain.IssuerTypeGlobalSign,
|
||||
Config: mustJSON(map[string]interface{}{
|
||||
"api_url": cfg.GlobalSign.APIUrl,
|
||||
"api_key": cfg.GlobalSign.APIKey,
|
||||
"api_secret": cfg.GlobalSign.APISecret,
|
||||
"client_cert_path": cfg.GlobalSign.ClientCertPath,
|
||||
"client_key_path": cfg.GlobalSign.ClientKeyPath,
|
||||
}),
|
||||
ID: "iss-globalsign",
|
||||
Name: "GlobalSign Atlas",
|
||||
Type: domain.IssuerTypeGlobalSign,
|
||||
Config: mustJSON(globalSignConfig),
|
||||
Enabled: true,
|
||||
Source: "env",
|
||||
CreatedAt: now,
|
||||
|
||||
@@ -195,6 +195,7 @@ export const issuerTypes: IssuerTypeConfig[] = [
|
||||
{ key: 'api_secret', label: 'API Secret', placeholder: 'GlobalSign API secret', required: true, type: 'password', sensitive: true },
|
||||
{ key: 'client_cert_path', label: 'Client Certificate Path', placeholder: '/path/to/client.crt', required: true },
|
||||
{ key: 'client_key_path', label: 'Client Key Path', placeholder: '/path/to/client.key', required: true, sensitive: true },
|
||||
{ key: 'server_ca_path', label: 'Server CA Path (optional)', placeholder: '/path/to/atlas-ca.pem', required: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user