mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:21:30 +00:00
fdd424bf5f
SCEP RFC 8894 + Intune master bundle — Phase 1.5 of 14.
Restructures SCEPConfig from a single flat struct (one IssuerID + one
RA pair + one challenge password) to a Profiles slice where each
profile binds its own URL path (/scep/<pathID>), issuer, optional
CertificateProfile, RA cert+key, and challenge password.
This phase is the FOUNDATION for Phases 2-12: every downstream handler
signature, service envelope, CertRep builder, GUI counter, and test
fixture takes a profile_id parameter from here on. Adding multi-profile
support post-bundle would cost 3x what greenfielding it now does.
Backward compat: legacy CERTCTL_SCEP_* flat env vars synthesise a
single-element Profiles[0] with PathID="" (legacy /scep root) when
CERTCTL_SCEP_PROFILES is unset. Existing operators see no behavior
change. New operators write multi-profile config directly via the
indexed env-var form.
Indexed env-var convention:
CERTCTL_SCEP_PROFILES=corp,iot,server
CERTCTL_SCEP_PROFILE_CORP_ISSUER_ID=iss-corp-laptop
CERTCTL_SCEP_PROFILE_CORP_PROFILE_ID=prof-corp-tls
CERTCTL_SCEP_PROFILE_CORP_CHALLENGE_PASSWORD=...
CERTCTL_SCEP_PROFILE_CORP_RA_CERT_PATH=/etc/certctl/scep/corp-ra.crt
CERTCTL_SCEP_PROFILE_CORP_RA_KEY_PATH=/etc/certctl/scep/corp-ra.key
... (etc per profile name)
internal/config/config.go
* SCEPConfig.Profiles []SCEPProfileConfig — primary multi-profile
dispatch source.
* Legacy flat fields (IssuerID, ProfileID, ChallengePassword,
RACertPath, RAKeyPath) preserved with updated docblocks marking
them as merge sources for the backward-compat shim.
* SCEPProfileConfig new struct (PathID, IssuerID, ProfileID,
ChallengePassword, RACertPath, RAKeyPath).
* loadSCEPProfilesFromEnv: reads CERTCTL_SCEP_PROFILES (comma-list
of names), expands each to per-profile env vars
CERTCTL_SCEP_PROFILE_<NAME>_*. Returns nil when unset so the
legacy-shim path takes over.
* mergeSCEPLegacyIntoProfiles: when SCEP enabled + Profiles empty +
any legacy flat field populated, synthesises Profiles[0] with
PathID="". No-op when Profiles already populated (structured form
wins) or SCEP disabled.
* validSCEPPathID: empty allowed (legacy /scep root); non-empty
must be [a-z0-9-] with no leading/trailing hyphen.
* Per-profile Validate gates: PathID format, uniqueness across the
slice, ChallengePassword presence (CWE-306 per profile), RA pair
presence (RFC 8894 §3.2.2), IssuerID presence.
* Legacy single-profile gates skip when Profiles is non-empty so
the per-profile loop owns the gating in the structured case
(avoids double-fire with overlapping error messages).
internal/api/router/router.go
* RegisterSCEPHandlers signature: map[string]handler.SCEPHandler
(was a single SCEPHandler).
* Empty PathID handler registered with literal r.Register('GET /scep'
+ 'POST /scep') so the openapi-parity AST scanner (Bundle D /
Audit M-027) continues to see the documented /scep route. Without
this preservation, the parity test fails because dynamic
string-built routes don't appear in *ast.BasicLit walks.
* Non-empty PathIDs registered dynamically as /scep/<pathID>.
* AuthExempt prefix /scep already covers all /scep[/...] paths via
prefix match — no change needed there.
cmd/server/main.go
* SCEP startup block iterates cfg.SCEP.Profiles, builds one service
+ one handler per profile, stuffs them into a {pathID -> handler}
map, hands the map to apiRouter.RegisterSCEPHandlers.
* Per-profile preflight: preflightSCEPChallengePassword,
preflightSCEPRACertKey, preflightEnrollmentIssuer fire ONCE PER
PROFILE with a profile-scoped slog.Logger so failures report
PathID + IssuerID. Each per-profile failure os.Exits(1) with a
targeted error message.
* Final 'SCEP server enabled' info log reports profile_count.
internal/config/config_scep_profiles_test.go (new, 9 tests / 22 sub-cases)
* TestSCEPConfig_LegacyFlatFields_SynthesizeSingleProfile — the
backward-compat smoke test.
* TestSCEPConfig_MultipleProfiles_LoadFromEnv — structured-form
happy path with two profiles.
* TestSCEPConfig_StructuredFormBeatsLegacy — when both forms set,
structured wins; legacy flat field MUST NOT leak into
Profiles[0].ChallengePassword.
* TestSCEPConfig_PathIDValidation — 13 sub-cases covering valid +
every reject mode (uppercase, slash, leading/trailing hyphen,
underscore, dot, space, non-ASCII).
* TestSCEPConfig_DuplicatePathID_Refuses.
* TestSCEPConfig_MissingPerProfileChallengePassword,
_MissingPerProfileRAPair (3 sub-cases),
_MissingPerProfileIssuerID — per-profile gate triplet.
* TestSCEPConfig_DisabledIgnoresProfiles — gates only fire when
SCEP is enabled.
internal/api/router/router_scep_profiles_test.go (new, 4 tests)
* TestRouter_RegisterSCEPHandlers_LegacyEmptyPathIDMapsToRoot —
empty PathID gets /scep root; both GET + POST routes registered.
* TestRouter_RegisterSCEPHandlers_NonEmptyPathIDMapsToSubpath —
non-empty PathID gets /scep/<pathID>; /scep root NOT registered
when no empty-PathID profile exists.
* TestRouter_RegisterSCEPHandlers_MultipleProfilesNoCrossBleed —
three profiles (default, corp, iot); each path reaches the right
handler instance, verified via per-profile-tagged GetCACaps mock
response.
* TestRouter_RegisterSCEPHandlers_EmptyMapRegistersNoRoutes — no
profiles → no /scep routes (deploy with SCEP disabled).
Verification:
* gofmt clean for the files I touched.
* go vet clean across config / router / cmd/server / domain.
* go test -short -count=1 green across config / router / cmd/server /
api/handler / service / domain / pkcs7.
* Coverage held: handler 79.0% / service 73.2% / pkcs7 100% /
config 96.0% / domain 88.6% / router 100% / cmd/server 19.2%.
* openapi-parity test green (literal /scep registrations preserved).
Phase 1.5 of 14 in SCEP RFC 8894 + Intune master bundle.
Living progress at cowork/scep-rfc8894-intune/progress.md.
360 lines
14 KiB
Go
360 lines
14 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// SCEP RFC 8894 + Intune master bundle Phase 1.5: per-issuer SCEP profiles.
|
|
// These tests pin:
|
|
// 1. Backward-compat shim: legacy CERTCTL_SCEP_* flat env vars synthesise
|
|
// a single-element Profiles[0] with PathID="" so existing /scep
|
|
// operators see no behavior change.
|
|
// 2. Structured form: CERTCTL_SCEP_PROFILES=corp,iot,server expands into
|
|
// per-profile env vars CERTCTL_SCEP_PROFILE_<NAME>_*.
|
|
// 3. PathID validation: only [a-z0-9-] with no leading/trailing hyphen,
|
|
// empty allowed (legacy /scep root). Validate() refuses anything else.
|
|
// 4. Per-profile gates: Validate() refuses each profile independently
|
|
// (empty challenge password, missing RA pair, missing IssuerID,
|
|
// duplicate PathID).
|
|
//
|
|
// Note these tests exercise the loader + Validate() in isolation; the
|
|
// per-profile preflight + router-registration paths are exercised by the
|
|
// cmd/server tests (existing) and the cmd/server/main.go startup path
|
|
// (manual via `make docker-up`).
|
|
|
|
// validBaseConfigForSCEPProfiles returns a Config that passes Validate
|
|
// EXCEPT for the SCEP fields the test under exercise sets. Mirrors the
|
|
// existing validBaseConfigForEncryption helper shape so the test file
|
|
// stays uniform with its siblings.
|
|
func validBaseConfigForSCEPProfiles(t *testing.T) *Config {
|
|
t.Helper()
|
|
return &Config{
|
|
Server: validServerConfig(t),
|
|
Database: DatabaseConfig{URL: "postgres://localhost/certctl", MaxConnections: 25},
|
|
Log: LogConfig{Level: "info", Format: "json"},
|
|
Auth: AuthConfig{Type: "api-key", Secret: "test-secret"},
|
|
Keygen: KeygenConfig{Mode: "agent"},
|
|
Scheduler: SchedulerConfig{
|
|
RenewalCheckInterval: 1 * time.Hour,
|
|
JobProcessorInterval: 30 * time.Second,
|
|
AgentHealthCheckInterval: 2 * time.Minute,
|
|
NotificationProcessInterval: 1 * time.Minute,
|
|
NotificationRetryInterval: 2 * time.Minute,
|
|
RetryInterval: 5 * time.Minute,
|
|
JobTimeoutInterval: 10 * time.Minute,
|
|
AwaitingCSRTimeout: 24 * time.Hour,
|
|
AwaitingApprovalTimeout: 168 * time.Hour,
|
|
},
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_LegacyFlatFields_SynthesizeSingleProfile is the
|
|
// load-time backward-compat test: an operator with the pre-Phase-1.5
|
|
// flat env vars (no CERTCTL_SCEP_PROFILES set) must end up with a
|
|
// single-element Profiles slice carrying PathID="" so /scep routes
|
|
// the same way it did before.
|
|
func TestSCEPConfig_LegacyFlatFields_SynthesizeSingleProfile(t *testing.T) {
|
|
clearCertctlEnv(t)
|
|
t.Setenv("CERTCTL_SCEP_ENABLED", "true")
|
|
t.Setenv("CERTCTL_SCEP_ISSUER_ID", "iss-legacy")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_ID", "prof-legacy")
|
|
t.Setenv("CERTCTL_SCEP_CHALLENGE_PASSWORD", "secret-from-flat-env")
|
|
t.Setenv("CERTCTL_SCEP_RA_CERT_PATH", "/etc/certctl/scep/ra.crt")
|
|
t.Setenv("CERTCTL_SCEP_RA_KEY_PATH", "/etc/certctl/scep/ra.key")
|
|
// Required infra envs so Load() doesn't fail on unrelated gates.
|
|
t.Setenv("CERTCTL_DB_URL", "postgres://localhost/certctl?sslmode=disable")
|
|
t.Setenv("CERTCTL_AUTH_TYPE", "api-key")
|
|
t.Setenv("CERTCTL_AUTH_SECRET", "test-secret")
|
|
srv := validServerConfig(t)
|
|
t.Setenv("CERTCTL_SERVER_TLS_CERT_PATH", srv.TLS.CertPath)
|
|
t.Setenv("CERTCTL_SERVER_TLS_KEY_PATH", srv.TLS.KeyPath)
|
|
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load() error = %v, want nil (legacy SCEP flat fields should pass)", err)
|
|
}
|
|
if len(cfg.SCEP.Profiles) != 1 {
|
|
t.Fatalf("len(Profiles) = %d, want 1 (legacy shim should synthesize single-element slice)", len(cfg.SCEP.Profiles))
|
|
}
|
|
got := cfg.SCEP.Profiles[0]
|
|
if got.PathID != "" {
|
|
t.Errorf("Profiles[0].PathID = %q, want \"\" (empty maps to legacy /scep root)", got.PathID)
|
|
}
|
|
if got.IssuerID != "iss-legacy" {
|
|
t.Errorf("Profiles[0].IssuerID = %q, want %q", got.IssuerID, "iss-legacy")
|
|
}
|
|
if got.ProfileID != "prof-legacy" {
|
|
t.Errorf("Profiles[0].ProfileID = %q, want %q", got.ProfileID, "prof-legacy")
|
|
}
|
|
if got.ChallengePassword != "secret-from-flat-env" {
|
|
t.Errorf("Profiles[0].ChallengePassword = %q, want flat env value", got.ChallengePassword)
|
|
}
|
|
if got.RACertPath != "/etc/certctl/scep/ra.crt" || got.RAKeyPath != "/etc/certctl/scep/ra.key" {
|
|
t.Errorf("Profiles[0] RA paths = (%q, %q), want flat env values", got.RACertPath, got.RAKeyPath)
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_MultipleProfiles_LoadFromEnv exercises the structured
|
|
// form: CERTCTL_SCEP_PROFILES=corp,iot expands into per-profile env vars.
|
|
func TestSCEPConfig_MultipleProfiles_LoadFromEnv(t *testing.T) {
|
|
clearCertctlEnv(t)
|
|
t.Setenv("CERTCTL_SCEP_ENABLED", "true")
|
|
t.Setenv("CERTCTL_SCEP_PROFILES", "corp,iot")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_CORP_ISSUER_ID", "iss-corp-laptop")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_CORP_PROFILE_ID", "prof-corp-tls")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_CORP_CHALLENGE_PASSWORD", "corp-secret")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_CORP_RA_CERT_PATH", "/etc/certctl/scep/corp-ra.crt")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_CORP_RA_KEY_PATH", "/etc/certctl/scep/corp-ra.key")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_IOT_ISSUER_ID", "iss-iot-device")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_IOT_CHALLENGE_PASSWORD", "iot-secret")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_IOT_RA_CERT_PATH", "/etc/certctl/scep/iot-ra.crt")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_IOT_RA_KEY_PATH", "/etc/certctl/scep/iot-ra.key")
|
|
// Required infra envs.
|
|
t.Setenv("CERTCTL_DB_URL", "postgres://localhost/certctl?sslmode=disable")
|
|
t.Setenv("CERTCTL_AUTH_TYPE", "api-key")
|
|
t.Setenv("CERTCTL_AUTH_SECRET", "test-secret")
|
|
srv := validServerConfig(t)
|
|
t.Setenv("CERTCTL_SERVER_TLS_CERT_PATH", srv.TLS.CertPath)
|
|
t.Setenv("CERTCTL_SERVER_TLS_KEY_PATH", srv.TLS.KeyPath)
|
|
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load() error = %v, want nil", err)
|
|
}
|
|
if len(cfg.SCEP.Profiles) != 2 {
|
|
t.Fatalf("len(Profiles) = %d, want 2", len(cfg.SCEP.Profiles))
|
|
}
|
|
// Order matters: env-list order is preserved by the loader.
|
|
if cfg.SCEP.Profiles[0].PathID != "corp" {
|
|
t.Errorf("Profiles[0].PathID = %q, want %q", cfg.SCEP.Profiles[0].PathID, "corp")
|
|
}
|
|
if cfg.SCEP.Profiles[1].PathID != "iot" {
|
|
t.Errorf("Profiles[1].PathID = %q, want %q", cfg.SCEP.Profiles[1].PathID, "iot")
|
|
}
|
|
if cfg.SCEP.Profiles[0].IssuerID != "iss-corp-laptop" {
|
|
t.Errorf("Profiles[0].IssuerID = %q, want %q", cfg.SCEP.Profiles[0].IssuerID, "iss-corp-laptop")
|
|
}
|
|
if cfg.SCEP.Profiles[1].IssuerID != "iss-iot-device" {
|
|
t.Errorf("Profiles[1].IssuerID = %q, want %q", cfg.SCEP.Profiles[1].IssuerID, "iss-iot-device")
|
|
}
|
|
if cfg.SCEP.Profiles[0].ChallengePassword != "corp-secret" {
|
|
t.Errorf("Profiles[0].ChallengePassword = %q, want %q", cfg.SCEP.Profiles[0].ChallengePassword, "corp-secret")
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_StructuredFormBeatsLegacy: when CERTCTL_SCEP_PROFILES is
|
|
// set, the legacy flat fields are NOT merged in (the structured form is
|
|
// the operator's explicit opt-in). Pins that the merge shim is no-op when
|
|
// Profiles is non-empty.
|
|
func TestSCEPConfig_StructuredFormBeatsLegacy(t *testing.T) {
|
|
clearCertctlEnv(t)
|
|
t.Setenv("CERTCTL_SCEP_ENABLED", "true")
|
|
// Both forms set — structured wins, flat is ignored.
|
|
t.Setenv("CERTCTL_SCEP_CHALLENGE_PASSWORD", "flat-secret-should-not-appear")
|
|
t.Setenv("CERTCTL_SCEP_PROFILES", "only")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_ONLY_ISSUER_ID", "iss-only")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_ONLY_CHALLENGE_PASSWORD", "structured-secret-wins")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_ONLY_RA_CERT_PATH", "/etc/certctl/scep/only-ra.crt")
|
|
t.Setenv("CERTCTL_SCEP_PROFILE_ONLY_RA_KEY_PATH", "/etc/certctl/scep/only-ra.key")
|
|
t.Setenv("CERTCTL_DB_URL", "postgres://localhost/certctl?sslmode=disable")
|
|
t.Setenv("CERTCTL_AUTH_TYPE", "api-key")
|
|
t.Setenv("CERTCTL_AUTH_SECRET", "test-secret")
|
|
srv := validServerConfig(t)
|
|
t.Setenv("CERTCTL_SERVER_TLS_CERT_PATH", srv.TLS.CertPath)
|
|
t.Setenv("CERTCTL_SERVER_TLS_KEY_PATH", srv.TLS.KeyPath)
|
|
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load() error = %v, want nil", err)
|
|
}
|
|
if len(cfg.SCEP.Profiles) != 1 {
|
|
t.Fatalf("len(Profiles) = %d, want 1 (structured form should NOT be augmented by legacy flat fields)", len(cfg.SCEP.Profiles))
|
|
}
|
|
if cfg.SCEP.Profiles[0].PathID != "only" {
|
|
t.Errorf("Profiles[0].PathID = %q, want %q", cfg.SCEP.Profiles[0].PathID, "only")
|
|
}
|
|
if cfg.SCEP.Profiles[0].ChallengePassword != "structured-secret-wins" {
|
|
t.Errorf("Profiles[0].ChallengePassword = %q, want structured value (legacy flat field MUST NOT leak in)", cfg.SCEP.Profiles[0].ChallengePassword)
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_PathIDValidation pins the path-safe slug constraint.
|
|
// Validate() refuses anything with uppercase, slashes, leading/trailing
|
|
// hyphens, or non-ASCII chars. The empty string is allowed (legacy root).
|
|
func TestSCEPConfig_PathIDValidation(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
pathID string
|
|
valid bool
|
|
}{
|
|
{"empty_legacy_root", "", true},
|
|
{"valid_lowercase", "corp", true},
|
|
{"valid_with_digits", "iot2", true},
|
|
{"valid_with_hyphen", "corp-laptop", true},
|
|
{"valid_long", "very-long-profile-name-with-many-segments", true},
|
|
{"reject_uppercase", "Corp", false},
|
|
{"reject_slash", "corp/laptop", false},
|
|
{"reject_leading_hyphen", "-corp", false},
|
|
{"reject_trailing_hyphen", "corp-", false},
|
|
{"reject_underscore", "corp_laptop", false},
|
|
{"reject_dot", "corp.laptop", false},
|
|
{"reject_space", "corp laptop", false},
|
|
{"reject_unicode", "corpé", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := validBaseConfigForSCEPProfiles(t)
|
|
cfg.SCEP = SCEPConfig{
|
|
Enabled: true,
|
|
Profiles: []SCEPProfileConfig{{
|
|
PathID: tc.pathID,
|
|
IssuerID: "iss-test",
|
|
ChallengePassword: "secret",
|
|
RACertPath: "/etc/certctl/scep/ra.crt",
|
|
RAKeyPath: "/etc/certctl/scep/ra.key",
|
|
}},
|
|
}
|
|
err := cfg.Validate()
|
|
if tc.valid && err != nil {
|
|
t.Errorf("Validate() = %v, want nil for valid PathID %q", err, tc.pathID)
|
|
}
|
|
if !tc.valid && err == nil {
|
|
t.Errorf("Validate() = nil, want error for invalid PathID %q", tc.pathID)
|
|
}
|
|
if !tc.valid && err != nil && !strings.Contains(err.Error(), "invalid PathID") {
|
|
t.Errorf("error should mention invalid PathID, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_DuplicatePathID_Refuses pins the uniqueness gate so
|
|
// the router never gets a {pathID -> handler} map with collisions.
|
|
func TestSCEPConfig_DuplicatePathID_Refuses(t *testing.T) {
|
|
cfg := validBaseConfigForSCEPProfiles(t)
|
|
cfg.SCEP = SCEPConfig{
|
|
Enabled: true,
|
|
Profiles: []SCEPProfileConfig{
|
|
{PathID: "corp", IssuerID: "iss-a", ChallengePassword: "x", RACertPath: "/a.crt", RAKeyPath: "/a.key"},
|
|
{PathID: "corp", IssuerID: "iss-b", ChallengePassword: "y", RACertPath: "/b.crt", RAKeyPath: "/b.key"},
|
|
},
|
|
}
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Fatal("Validate() = nil, want error for duplicate PathID")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicates PathID") {
|
|
t.Errorf("error should mention duplicates PathID, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_MissingPerProfileChallengePassword pins the per-profile
|
|
// CWE-306 gate. Each profile is independently required to carry a
|
|
// non-empty challenge password — defense in depth with the static-form
|
|
// gate that fired pre-Phase-1.5.
|
|
func TestSCEPConfig_MissingPerProfileChallengePassword(t *testing.T) {
|
|
cfg := validBaseConfigForSCEPProfiles(t)
|
|
cfg.SCEP = SCEPConfig{
|
|
Enabled: true,
|
|
Profiles: []SCEPProfileConfig{
|
|
{PathID: "good", IssuerID: "iss-a", ChallengePassword: "x", RACertPath: "/a.crt", RAKeyPath: "/a.key"},
|
|
{PathID: "bad", IssuerID: "iss-b", ChallengePassword: "", RACertPath: "/b.crt", RAKeyPath: "/b.key"},
|
|
},
|
|
}
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Fatal("Validate() = nil, want error for empty per-profile challenge password")
|
|
}
|
|
if !strings.Contains(err.Error(), "empty CHALLENGE_PASSWORD") {
|
|
t.Errorf("error should mention empty CHALLENGE_PASSWORD, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_MissingPerProfileRAPair pins the RA-pair gate per profile.
|
|
func TestSCEPConfig_MissingPerProfileRAPair(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raCertPath string
|
|
raKeyPath string
|
|
}{
|
|
{"both_missing", "", ""},
|
|
{"cert_missing", "", "/x.key"},
|
|
{"key_missing", "/x.crt", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := validBaseConfigForSCEPProfiles(t)
|
|
cfg.SCEP = SCEPConfig{
|
|
Enabled: true,
|
|
Profiles: []SCEPProfileConfig{{
|
|
PathID: "p",
|
|
IssuerID: "iss",
|
|
ChallengePassword: "secret",
|
|
RACertPath: tc.raCertPath,
|
|
RAKeyPath: tc.raKeyPath,
|
|
}},
|
|
}
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Fatalf("Validate() = nil, want error for %s", tc.name)
|
|
}
|
|
if !strings.Contains(err.Error(), "missing RA cert/key path") {
|
|
t.Errorf("error should mention missing RA cert/key path, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_MissingPerProfileIssuerID guards against a profile that
|
|
// references no issuer at all (a likely typo in CERTCTL_SCEP_PROFILE_X_ISSUER_ID).
|
|
func TestSCEPConfig_MissingPerProfileIssuerID(t *testing.T) {
|
|
cfg := validBaseConfigForSCEPProfiles(t)
|
|
cfg.SCEP = SCEPConfig{
|
|
Enabled: true,
|
|
Profiles: []SCEPProfileConfig{{
|
|
PathID: "p",
|
|
ChallengePassword: "secret",
|
|
RACertPath: "/x.crt",
|
|
RAKeyPath: "/x.key",
|
|
}},
|
|
}
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Fatal("Validate() = nil, want error for empty per-profile IssuerID")
|
|
}
|
|
if !strings.Contains(err.Error(), "empty IssuerID") {
|
|
t.Errorf("error should mention empty IssuerID, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestSCEPConfig_DisabledIgnoresProfiles pins that the per-profile gates
|
|
// only fire when SCEP is enabled. A disabled deploy can carry malformed
|
|
// Profiles entries (e.g. partially-populated by an automation tool) without
|
|
// blocking startup.
|
|
func TestSCEPConfig_DisabledIgnoresProfiles(t *testing.T) {
|
|
cfg := validBaseConfigForSCEPProfiles(t)
|
|
cfg.SCEP = SCEPConfig{
|
|
Enabled: false,
|
|
Profiles: []SCEPProfileConfig{
|
|
{PathID: "BAD UPPER", IssuerID: "", ChallengePassword: "", RACertPath: "", RAKeyPath: ""},
|
|
},
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Errorf("Validate() = %v, want nil for SCEP disabled with malformed profiles", err)
|
|
}
|
|
}
|
|
|
|
// clearCertctlEnv resets every CERTCTL_* env var so a Load()-based test
|
|
// runs in isolation. Mirrors the existing clearCertctlEnv in the sibling
|
|
// test file (config_test.go) but defined locally so the file stays
|
|
// self-contained for a future split.
|
|
func init() {
|
|
// Reuse the existing clearCertctlEnv from config_test.go via the package
|
|
// scope; declared in this init() block as a sanity check to ensure
|
|
// linking works. The actual helper lives in config_test.go.
|
|
_ = os.Getenv
|
|
}
|