mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 22:28:52 +00:00
3669556e57
Closes the #2 acquisition-readiness blocker from the 2026-05-01 issuer coverage audit. New() at ejbca.go:L79-L88 previously constructed an http.Client with only Timeout set — no Transport, no TLSClientConfig. When AuthMode=mtls (the default), the client never presented the configured ClientCert/ClientKey. The OAuth2 path worked; mTLS always failed authentication. Tests passed because they injected a pre-built *http.Client via NewWithHTTPClient, a path the production factory never took. This commit: - Rewrites New() to load ClientCertPath + ClientKeyPath via tls.LoadX509KeyPair when AuthMode=mtls, configure *http.Transport.TLSClientConfig with MinVersion: TLS 1.2 (compatibility floor for on-prem EJBCA installs that may predate TLS 1.3), and return (*Connector, error). Constructs a fresh *http.Transport — does NOT clone http.DefaultTransport, which would leak mutation across the package boundary. - OAuth2 mode unchanged: returns a client with no transport customization (the Bearer header path is wired in setAuthHeaders). - Invalid auth_mode values return (nil, error) immediately rather than falling through to the mtls default and erroring at cert load. - Updates the factory call site at issuerfactory/factory.go for the new signature; the factory's outer (issuer.Connector, error) shape was already in place. - Adds TestNew_MTLSWiresClientCert: calls production New() (NOT NewWithHTTPClient) with real cert/key files generated via stdlib crypto/x509, asserts httpClient.Transport.TLSClientConfig.Certificates is non-empty. Includes an httptest TLS server with ClientAuth: tls.RequireAndVerifyClientCert that proves the cert is actually presented on the wire — not just stashed in a struct field. - Adds TestNew_MTLSCertLoadFailure: missing-cert path returns an error wrapping fs.ErrNotExist (verified via errors.Is). - Adds TestNew_OAuth2NoTransportTuning: OAuth2 path leaves Transport nil, ensuring no accidental mTLS bleedthrough. - Adds TestNew_InvalidAuthMode: explicit guard that auth_mode values other than "mtls"/"oauth2" return (nil, error) at New() time. - Adds export_test.go with HTTPClientForTest helper so the external ejbca_test package can inspect the connector's internal *http.Client for the wiring assertions. Compile-only during `go test`; production builds don't expose it. - Adds mustNewForValidateConfig test helper (OAuth2 placeholder connector) for the existing ValidateConfig-only tests; pre-fix they used New(nil, ...) which is no longer valid because nil config falls into the mTLS default branch that requires non-nil cert paths. - Updates ejbca_stubs_test.go (internal package) for the new (*Connector, error) signature; switches the dummy connector to OAuth2 mode so Config{} doesn't error at New(). Out of scope (separate follow-ups, per the prompt's explicit fence): - OAuth2 token refresh missing - Config.Token plaintext at runtime (needs SecretRef abstraction) - RevokeCertificate composite OrderID parsing (the issuerDN := "" line at ejbca.go:L313) Verified locally: - gofmt clean - go vet ./... clean - staticcheck ./... clean - golangci-lint run --timeout 5m ./... → 0 issues - go test -short -count=1 ./internal/connector/issuer/ejbca/ green - go test -short -count=1 ./internal/connector/issuerfactory/ green - go test -short -count=1 ./internal/service/ green - go build ./... success Audit reference: cowork/issuer-coverage-audit-2026-05-01/RESULTS.md Top-10 fix #2.
135 lines
4.5 KiB
Go
135 lines
4.5 KiB
Go
package issuerfactory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/shankar0123/certctl/internal/connector/issuer"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/acme"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/awsacmpca"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/digicert"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/ejbca"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/entrust"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/globalsign"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/googlecas"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/local"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/openssl"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/sectigo"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/stepca"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/vault"
|
|
)
|
|
|
|
// NewFromConfig instantiates an issuer connector from its type string and config JSON.
|
|
// The config JSON keys use snake_case matching the connector Config struct json tags.
|
|
// This replaces the manual wiring in cmd/server/main.go.
|
|
//
|
|
// ctx is currently used only by the AWSACMPCA branch (passed to
|
|
// awsconfig.LoadDefaultConfig for SDK credential chain resolution). Other
|
|
// connectors take no context at construction; the parameter is kept on the
|
|
// signature so callers that have a ctx in scope thread it through cleanly
|
|
// (contextcheck linter).
|
|
func NewFromConfig(ctx context.Context, issuerType string, configJSON json.RawMessage, logger *slog.Logger) (issuer.Connector, error) {
|
|
if len(configJSON) == 0 {
|
|
configJSON = []byte("{}")
|
|
}
|
|
|
|
switch issuerType {
|
|
case "local", "local_ca", "GenericCA", "genericca":
|
|
var cfg local.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid Local CA config: %w", err)
|
|
}
|
|
return local.New(&cfg, logger), nil
|
|
|
|
case "ACME", "acme":
|
|
var cfg acme.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid ACME config: %w", err)
|
|
}
|
|
return acme.New(&cfg, logger), nil
|
|
|
|
case "StepCA", "stepca":
|
|
var cfg stepca.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid step-ca config: %w", err)
|
|
}
|
|
return stepca.New(&cfg, logger), nil
|
|
|
|
case "OpenSSL", "openssl":
|
|
var cfg openssl.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid OpenSSL config: %w", err)
|
|
}
|
|
return openssl.New(&cfg, logger), nil
|
|
|
|
case "VaultPKI", "vaultpki":
|
|
var cfg vault.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid Vault PKI config: %w", err)
|
|
}
|
|
return vault.New(&cfg, logger), nil
|
|
|
|
case "DigiCert", "digicert":
|
|
var cfg digicert.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid DigiCert config: %w", err)
|
|
}
|
|
return digicert.New(&cfg, logger), nil
|
|
|
|
case "Sectigo", "sectigo":
|
|
var cfg sectigo.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid Sectigo config: %w", err)
|
|
}
|
|
return sectigo.New(&cfg, logger), nil
|
|
|
|
case "GoogleCAS", "googlecas":
|
|
var cfg googlecas.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid Google CAS config: %w", err)
|
|
}
|
|
return googlecas.New(&cfg, logger), nil
|
|
|
|
case "AWSACMPCA", "awsacmpca":
|
|
var cfg awsacmpca.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid AWS ACM PCA config: %w", err)
|
|
}
|
|
conn, err := awsacmpca.New(ctx, &cfg, logger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("AWS ACM PCA init: %w", err)
|
|
}
|
|
return conn, nil
|
|
|
|
case "Entrust", "entrust":
|
|
var cfg entrust.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid Entrust config: %w", err)
|
|
}
|
|
return entrust.New(&cfg, logger), nil
|
|
|
|
case "GlobalSign", "globalsign":
|
|
var cfg globalsign.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid GlobalSign config: %w", err)
|
|
}
|
|
return globalsign.New(&cfg, logger), nil
|
|
|
|
case "EJBCA", "ejbca":
|
|
var cfg ejbca.Config
|
|
if err := json.Unmarshal(configJSON, &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid EJBCA config: %w", err)
|
|
}
|
|
conn, err := ejbca.New(&cfg, logger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("EJBCA init: %w", err)
|
|
}
|
|
return conn, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown issuer type: %q", issuerType)
|
|
}
|
|
}
|