From ed7e02c4c68f7eb2a6535486a4664209ef9a7b9f Mon Sep 17 00:00:00 2001 From: Sameer Date: Tue, 11 Aug 2026 13:54:32 +0530 Subject: [PATCH] Fix panics under GODEBUG=fips140=only in SSHKey/Certificate generation Under Go's native FIPS 140-3 "only" enforcement mode, two vendored non-security primitives panic on every reconcile: - SSHKeyGenerator.fingerprintMD5 (github.com/cloudfoundry/config-server vendor/.../ssh_key_generator.go:72) uses crypto/md5 to compute the SSH public key fingerprint shown in SSHKey.PublicKeyFingerprint (the same convention as `ssh-keygen -l -E md5`). - CertificateGenerator.bigIntHash (vendor/.../certificate_generator.go:66) uses crypto/sha1 to derive the X.509 SubjectKeyId per RFC 5280 4.2.1.2 method (1). Neither is a cryptographic operation in the security sense - they're non-forgeability-sensitive identifiers, not signatures or authentication - but Go's fips140=only mode blocks the primitive itself regardless of call-site intent, so both panic unconditionally on every SSHKey and Certificate reconcile. Wrap both call sites (in our own pkg/generator code, not the vendored library) in crypto/fips140.WithoutEnforcement, added in Go 1.26. It scopes the FIPS 140-3 "only" exemption to just that call and is a documented no-op when strict enforcement isn't active, so this has no effect on non-FIPS builds. Found via a downstream FIPS build's real end-to-end run: the project's own test/e2e suite (TestSSHKey, TestSSHKeyTemplate, TestCertificate) against the actual shipped artifact, deployed to a real cluster. Added fips140_internal_test.go, gated on fips140.Enforced(), to exercise the same paths directly (skips as a no-op unless run with GOFIPS140=v1.0.0 GODEBUG=fips140=only). Co-Authored-By: Claude Sonnet 5 Signed-off-by: Sameer --- pkg/generator/certificate_reconciler.go | 11 ++++- pkg/generator/fips140_internal_test.go | 57 +++++++++++++++++++++++++ pkg/generator/ssh_key_reconciler.go | 11 ++++- 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 pkg/generator/fips140_internal_test.go diff --git a/pkg/generator/certificate_reconciler.go b/pkg/generator/certificate_reconciler.go index 1c99fc1a9..62eef8619 100644 --- a/pkg/generator/certificate_reconciler.go +++ b/pkg/generator/certificate_reconciler.go @@ -5,6 +5,7 @@ package generator import ( "context" + "crypto/fips140" "fmt" sgv1alpha1 "carvel.dev/secretgen-controller/pkg/apis/secretgen/v1alpha1" @@ -185,7 +186,15 @@ func (r *CertificateReconciler) generate(ctx context.Context, params certParams, gen := cfgtypes.NewCertificateGenerator(singleCertLoader{caCertSecret}) - certVal, err := gen.Generate(params) + // cfgtypes.CertificateGenerator.Generate uses crypto/sha1 to derive the certificate's + // SubjectKeyId (RFC 5280 4.2.1.2, method (1)): a non-cryptographic key identifier used + // for chain-building/lookup, not for signing or trust decisions. WithoutEnforcement + // scopes the FIPS 140-3 "only" exemption to just this call, so it has no effect when + // strict enforcement isn't active. + var certVal interface{} + fips140.WithoutEnforcement(func() { + certVal, err = gen.Generate(params) + }) if err != nil { return cfgtypes.CertResponse{}, err } diff --git a/pkg/generator/fips140_internal_test.go b/pkg/generator/fips140_internal_test.go new file mode 100644 index 000000000..d50a19c19 --- /dev/null +++ b/pkg/generator/fips140_internal_test.go @@ -0,0 +1,57 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "context" + "crypto/fips140" + "testing" + + sgv1alpha1 "carvel.dev/secretgen-controller/pkg/apis/secretgen/v1alpha1" +) + +// These tests only exercise anything meaningful when run with +// GOFIPS140=v1.0.0 GODEBUG=fips140=only, which enables strict FIPS 140-3 +// enforcement (crypto/fips140.Enforced() reports it at runtime). Without +// that, they degrade to a skip so they don't add noise to a normal `go test +// ./...` run. + +func TestSSHKeyReconciler_GenerateUnderFIPS140Only(t *testing.T) { + if !fips140.Enforced() { + t.Skip("run with GOFIPS140=v1.0.0 GODEBUG=fips140=only to exercise strict FIPS 140-3 enforcement") + } + + r := &SSHKeyReconciler{} + + sshKey, err := r.generate(&sgv1alpha1.SSHKey{}) + if err != nil { + t.Fatalf("generate: %v", err) + } + if sshKey.PrivateKey == "" || sshKey.PublicKey == "" || sshKey.PublicKeyFingerprint == "" { + t.Fatalf("generate returned incomplete SSHKey: %+v", sshKey) + } +} + +func TestCertificateReconciler_GenerateUnderFIPS140Only(t *testing.T) { + if !fips140.Enforced() { + t.Skip("run with GOFIPS140=v1.0.0 GODEBUG=fips140=only to exercise strict FIPS 140-3 enforcement") + } + + r := &CertificateReconciler{} + cert := &sgv1alpha1.Certificate{ + Spec: sgv1alpha1.CertificateSpec{ + CommonName: "test-ca", + IsCA: true, + }, + } + params := newCertParams(cert) + + certResp, err := r.generate(context.Background(), params, cert) + if err != nil { + t.Fatalf("generate: %v", err) + } + if certResp.Certificate == "" || certResp.PrivateKey == "" { + t.Fatalf("generate returned incomplete CertResponse: %+v", certResp) + } +} diff --git a/pkg/generator/ssh_key_reconciler.go b/pkg/generator/ssh_key_reconciler.go index 3fdeff8b8..959316a26 100644 --- a/pkg/generator/ssh_key_reconciler.go +++ b/pkg/generator/ssh_key_reconciler.go @@ -5,6 +5,7 @@ package generator import ( "context" + "crypto/fips140" "fmt" sgv1alpha1 "carvel.dev/secretgen-controller/pkg/apis/secretgen/v1alpha1" @@ -122,7 +123,15 @@ func (r *SSHKeyReconciler) generate(sshKey *sgv1alpha1.SSHKey) (cfgtypes.SSHKey, gen := cfgtypes.NewSSHKeyGenerator() // TODO allow type and number of bits? - sshKeyVal, err := gen.Generate(nil) + // cfgtypes.SSHKeyGenerator.Generate uses crypto/md5 to compute PublicKeyFingerprint, + // a display-only identifier (same convention as `ssh-keygen -l -E md5`), not a + // cryptographic operation. WithoutEnforcement scopes the FIPS 140-3 "only" exemption + // to just this call, so it has no effect when strict enforcement isn't active. + var sshKeyVal interface{} + var err error + fips140.WithoutEnforcement(func() { + sshKeyVal, err = gen.Generate(nil) + }) if err != nil { return cfgtypes.SSHKey{}, err }