diff --git a/Dockerfile b/Dockerfile index df0245270..553fcaa19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,11 @@ ARG TARGETARCH # If provided, must be a comma-separated list of Go build tags. ARG ADDITIONAL_BUILD_TAGS +# Set this if you would like to enable Go's built-in FIPS compliance module. +# E.g. GOFIPS140=certified or GOFIPS140=v1.0.0 +# See https://go.dev/doc/security/fips140 for details. +ARG GOFIPS140=off + # Build the statically linked (CGO_ENABLED=0) binary. # Mount source, build cache, and module cache for performance reasons. # See https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/ @@ -31,7 +36,7 @@ RUN \ --mount=target=. \ --mount=type=cache,target=/cache/gocache \ --mount=type=cache,target=/cache/gomodcache \ - export GOCACHE=/cache/gocache GOMODCACHE=/cache/gomodcache CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH && \ + export GOCACHE=/cache/gocache GOMODCACHE=/cache/gomodcache CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GOFIPS140=$GOFIPS140 && \ go build -tags $ADDITIONAL_BUILD_TAGS -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-concierge-kube-cert-agent ./cmd/pinniped-concierge-kube-cert-agent/... && \ go build -tags $ADDITIONAL_BUILD_TAGS -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-server ./cmd/pinniped-server/... && \ ln -s /usr/local/bin/pinniped-server /usr/local/bin/pinniped-concierge && \ diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index ce7464f87..1423c3f8d 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -233,6 +233,7 @@ if [[ "$do_build" == "yes" ]]; then else log_note "Docker building the app with KUBE_GIT_VERSION='$testing_version'" # DOCKER_BUILDKIT=1 is optional on MacOS but required on linux. + # To build the server with GOFIPS140, add a build arg here, e.g.: --build-arg "GOFIPS140=certified" DOCKER_BUILDKIT=1 docker build . --tag "$registry_repo_tag" --build-arg "KUBE_GIT_VERSION=$testing_version" fi fi diff --git a/internal/crypto/ptls/common.go b/internal/crypto/ptls/common.go index 3fe78e798..8848f11f5 100644 --- a/internal/crypto/ptls/common.go +++ b/internal/crypto/ptls/common.go @@ -1,4 +1,4 @@ -// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2024-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package ptls @@ -209,7 +209,7 @@ func validateAllowedCiphers( // Note that it will return different values depending on if the code was compiled in FIPS or non-FIPS mode. func allHardcodedAllowedCipherSuites() []*tls.CipherSuite { // First append all secure and LDAP cipher suites. - result := translateIDIntoSecureCipherSuites(append(secureCipherSuiteIDs, additionalSecureCipherSuiteIDsOnlyForLDAPClients...)) + result := translateIDIntoSecureCipherSuites(append(secureCipherSuiteIDs(), additionalSecureCipherSuiteIDsOnlyForLDAPClients()...)) // Then append any insecure cipher suites that might be allowed. // insecureCipherSuiteIDs is empty except when compiled in FIPS mode. @@ -218,7 +218,7 @@ func allHardcodedAllowedCipherSuites() []*tls.CipherSuite { continue } - if slices.Contains(insecureCipherSuiteIDs, golangInsecureCipherSuite.ID) { + if slices.Contains(insecureCipherSuiteIDs(), golangInsecureCipherSuite.ID) { result = append(result, golangInsecureCipherSuite) } } diff --git a/internal/crypto/ptls/profiles.go b/internal/crypto/ptls/profiles.go index 921b01ce1..b18503187 100644 --- a/internal/crypto/ptls/profiles.go +++ b/internal/crypto/ptls/profiles.go @@ -1,12 +1,13 @@ -// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Note that everything in this file is overridden by profiles_fips_strict.go when Pinniped is built in FIPS-only mode. +// Note that everything in this file is overridden by profiles_fips_strict.go when Pinniped is built in legacy boring crypto FIPS-only mode. //go:build !fips_strict package ptls import ( + "crypto/fips140" "crypto/tls" "crypto/x509" "os" @@ -18,8 +19,25 @@ import ( "go.pinniped.dev/internal/plog" ) -var ( - // secureCipherSuiteIDs is the list of TLS ciphers to use for both clients and servers when using TLS 1.2. +// secureCipherSuiteIDs is the list of TLS ciphers to use for both clients and servers when using TLS 1.2. +func secureCipherSuiteIDs() []uint16 { + if fips140.Enabled() { + // FIPS allows the use of these ciphers which golang considers secure. + // For GOFIPS140, you can find Go's supported list in the Go source code at src/crypto/tls/defaults_fips140.go. + return []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + + // These are considered "insecure" by Go because they are returned by tls.InsecureCipherSuites(). + // So although they are included in Go's src/crypto/tls/defaults_fips140.go, they will not actually be used. + //tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + //tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + } + } + + // The list for non-FIPS builds. // // The order does not matter in go 1.17+ https://go.dev/blog/tls-cipher-suites. // We match crypto/tls.cipherSuitesPreferenceOrder because it makes unit tests easier to write. @@ -41,7 +59,7 @@ var ( // // These are all AEADs with ECDHE, some use ChaCha20Poly1305 while others use AES-GCM, // which provides forward secrecy, confidentiality and authenticity of data. - secureCipherSuiteIDs = []uint16{ //nolint:gochecknoglobals // please treat this as a const + return []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, @@ -49,16 +67,27 @@ var ( tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, } +} - // insecureCipherSuiteIDs is a list of additional ciphers that should be allowed for both clients - // and servers when using TLS 1.2. - // - // This list is empty when compiled in non-FIPS mode, so we will not use any insecure ciphers in non-FIPS mode. - insecureCipherSuiteIDs []uint16 //nolint:gochecknoglobals // please treat this as a const +// insecureCipherSuiteIDs is a list of additional ciphers that should be allowed for both clients +// and servers when using TLS 1.2. +// +// This list is empty when compiled in non-FIPS mode, so we will not use any insecure ciphers in non-FIPS mode. +// It should also be empty for GOFIPS140, because it does not use any ciphers considered insecure by Go. +func insecureCipherSuiteIDs() []uint16 { + return nil +} - // additionalSecureCipherSuiteIDsOnlyForLDAPClients are additional ciphers to use only for LDAP clients - // when using TLS 1.2. These can be used when the Pinniped Supervisor is making calls to an LDAP server - // configured by an LDAPIdentityProvider or ActiveDirectoryIdentityProvider. +// additionalSecureCipherSuiteIDsOnlyForLDAPClients are additional ciphers to use only for LDAP clients +// when using TLS 1.2. These can be used when the Pinniped Supervisor is making calls to an LDAP server +// configured by an LDAPIdentityProvider or ActiveDirectoryIdentityProvider. +func additionalSecureCipherSuiteIDsOnlyForLDAPClients() []uint16 { + if fips140.Enabled() { + // For GOFIPS140, we do not want to add any less secure ciphers to the list. + return nil + } + + // The list for non-FIPS builds. // // Adds less secure ciphers to support the default AWS Active Directory config. // @@ -66,13 +95,13 @@ var ( // these provide forward secrecy and confidentiality of data, but not authenticity. // MAC-then-Encrypt CBC ciphers are susceptible to padding oracle attacks. // See https://crypto.stackexchange.com/a/205 and https://crypto.stackexchange.com/a/224 - additionalSecureCipherSuiteIDsOnlyForLDAPClients = []uint16{ //nolint:gochecknoglobals // please treat this as a const + return []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, } -) +} // init prints a log message to tell the operator how Pinniped was compiled. This makes it obvious // that they are using Pinniped in FIPS-mode or not, which is otherwise hard to observe. @@ -85,35 +114,59 @@ func init() { //nolint:gochecknoinits // this init runs before we have parsed our config to determine our log level // thus we must use a log statement that will always print instead of conditionally print - plog.Always("this server was not compiled in FIPS-only mode", + plog.Always("this server was not compiled in legacy boring crypto FIPS-only mode", "go version", runtime.Version(), - "SecureProfileMinTLSVersionForNonFIPS", tls.VersionName(SecureProfileMinTLSVersionForNonFIPS)) + "SecureProfileMinTLSVersionForNonFIPS", tls.VersionName(SecureProfileMinTLSVersionForNonFIPS), + "Go native GOFIPS140 enabled", fips140.Enabled(), + "Go native GOFIPS140 version", fips140.Version(), + ) } // Default TLS profile should be used by: // A. servers whose clients are outside our control and who may reasonably wish to use TLS 1.2, and // B. clients who need to interact with servers that might not support TLS 1.3. -// Note that this will behave differently when compiled in FIPS mode (see profiles_fips_strict.go). +// Note that this will behave differently when compiled in legacy boring crypto FIPS mode (see profiles_fips_strict.go). // Default returns a tls.Config with a minimum of TLS1.2+ and a few ciphers that can be further constrained by configuration. func Default(rootCAs *x509.CertPool) *tls.Config { - ciphers := translateIDIntoSecureCipherSuites(secureCipherSuiteIDs) + if fips140.Enabled() { + config := buildTLSConfig(rootCAs, allHardcodedAllowedCipherSuites(), getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()) + // GOFIPS140 supports both TLS v1.2 and v1.3. + // You can find Go's supported list in the Go source code at src/crypto/tls/defaults_fips140.go. + config.MaxVersion = tls.VersionTLS13 + return config + } + + ciphers := translateIDIntoSecureCipherSuites(secureCipherSuiteIDs()) return buildTLSConfig(rootCAs, ciphers, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()) } // DefaultLDAP TLS profile should be used by clients who need to interact with potentially old LDAP servers // that might not support TLS 1.3 and that might use older ciphers. -// Note that this will behave differently when compiled in FIPS mode (see profiles_fips_strict.go). +// Note that this will behave differently when compiled in legacy boring crypto FIPS mode (see profiles_fips_strict.go). func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config { - ciphers := translateIDIntoSecureCipherSuites(secureCipherSuiteIDs) - ciphers = append(ciphers, translateIDIntoSecureCipherSuites(additionalSecureCipherSuiteIDsOnlyForLDAPClients)...) + if fips140.Enabled() { + // This chooses different cipher suites and/or TLS versions compared to non-FIPS mode. + // In FIPS mode, this is not any different from the Default profile. + return Default(rootCAs) + } + + ciphers := translateIDIntoSecureCipherSuites(secureCipherSuiteIDs()) + ciphers = append(ciphers, translateIDIntoSecureCipherSuites(additionalSecureCipherSuiteIDsOnlyForLDAPClients())...) return buildTLSConfig(rootCAs, ciphers, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()) } // Secure TLS profile should be used by: // A. servers whose clients are entirely known by us and who may reasonably be told that they must use TLS 1.3, and // B. clients who only need to interact with servers that are known by us to support TLS 1.3 (e.g. the Kubernetes API). -// Note that this will behave differently when compiled in FIPS mode (see profiles_fips_strict.go). +// Note that this will behave differently when compiled in legacy boring crypto FIPS mode (see profiles_fips_strict.go). func Secure(rootCAs *x509.CertPool) *tls.Config { + if fips140.Enabled() { + // This chooses different cipher suites and/or TLS versions compared to non-FIPS mode. + // Until it is safe to assume that a FIPS-compiled k8s server supports TLS 1.3, continue to + // make the Secure profile the same as the Default profile in FIPS mode, to allow both TLS 1.2 and 1.3. + return Default(rootCAs) + } + // as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, modern configuration, supports: // - Firefox 63 // - Android 10.0 @@ -141,6 +194,14 @@ func Secure(rootCAs *x509.CertPool) *tls.Config { // This function is only public so we can integration test it in ptls_fips_test.go. // Note that this will behave differently when compiled in FIPS mode (see profiles_fips_strict.go). func SecureServing(opts *options.SecureServingOptionsWithLoopback) { + if fips140.Enabled() { + // This chooses different cipher suites and/or TLS versions compared to non-FIPS mode. + // Until it is safe to assume that a FIPS-compiled k8s server supports TLS 1.3, continue to + // make SecureServing use the same as the defaultServing profile in FIPS mode, to allow both TLS 1.2 and 1.3. + defaultServing(opts) + return + } + // secureServingOptionsMinTLSVersion is the minimum tls version in the format // expected by SecureServingOptions.MinTLSVersion from // k8s.io/apiserver/pkg/server/options. diff --git a/internal/crypto/ptls/profiles_fips_strict.go b/internal/crypto/ptls/profiles_fips_strict.go index 97fbc126d..da7181401 100644 --- a/internal/crypto/ptls/profiles_fips_strict.go +++ b/internal/crypto/ptls/profiles_fips_strict.go @@ -1,7 +1,7 @@ -// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// This file overrides profiles.go when Pinniped is built in FIPS-only mode. +// This file overrides profiles.go when Pinniped is built in FIPS-only mode using the legacy boring crypto compiler. //go:build fips_strict package ptls @@ -20,34 +20,41 @@ import ( "go.pinniped.dev/internal/plog" ) -// The union of these three variables is all the FIPS-approved TLS 1.2 ciphers. +// secureCipherSuiteIDs(), insecureCipherSuiteIDs(), and additionalSecureCipherSuiteIDsOnlyForLDAPClients() +// together determine the ciphers to be used for TLS 1.2. +// The union of these three functions' results is all the FIPS-approved TLS 1.2 ciphers. // If this list does not match the boring crypto compiler's list then the TestFIPSCipherSuites integration // test should fail, which indicates that this list needs to be updated. -var ( - // secureCipherSuiteIDs is the list of TLS ciphers to use for both clients and servers when using TLS 1.2. - // - // FIPS allows the use of these ciphers which golang considers secure. - secureCipherSuiteIDs = []uint16{ + +// secureCipherSuiteIDs is the list of TLS ciphers to use for both clients and servers when using TLS 1.2. +// +// FIPS allows the use of these ciphers which golang considers secure. +func secureCipherSuiteIDs() []uint16 { + return []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, } +} - // insecureCipherSuiteIDs is a list of additional ciphers that should be allowed for both clients - // and servers when using TLS 1.2. - // - // Previous versions of FIPS allowed the use of some specific ciphers that golang considers insecure. - // Go 1.24 does not anymore, so now this list is empty. - insecureCipherSuiteIDs []uint16 +// insecureCipherSuiteIDs is a list of additional ciphers that should be allowed for both clients +// and servers when using TLS 1.2. +// +// Previous versions of FIPS allowed the use of some specific ciphers that golang considers insecure. +// Go 1.24 does not anymore, so now this list is empty. +func insecureCipherSuiteIDs() []uint16 { + return nil +} - // additionalSecureCipherSuiteIDsOnlyForLDAPClients are additional ciphers to use only for LDAP clients - // when using TLS 1.2. These can be used when the Pinniped Supervisor is making calls to an LDAP server - // configured by an LDAPIdentityProvider or ActiveDirectoryIdentityProvider. - // - // When compiled in FIPS mode, there are no extras for LDAP clients. - additionalSecureCipherSuiteIDsOnlyForLDAPClients []uint16 -) +// additionalSecureCipherSuiteIDsOnlyForLDAPClients are additional ciphers to use only for LDAP clients +// when using TLS 1.2. These can be used when the Pinniped Supervisor is making calls to an LDAP server +// configured by an LDAPIdentityProvider or ActiveDirectoryIdentityProvider. +// +// When compiled in FIPS mode, there are no extras for LDAP clients. +func additionalSecureCipherSuiteIDsOnlyForLDAPClients() []uint16 { + return nil +} // init: see comment in profiles.go. func init() { diff --git a/internal/testutil/tlsserver/tls13_ciphers.go b/internal/testutil/tlsserver/tls13_ciphers.go index 8f0b0647e..71925d196 100644 --- a/internal/testutil/tlsserver/tls13_ciphers.go +++ b/internal/testutil/tlsserver/tls13_ciphers.go @@ -1,15 +1,26 @@ -// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !fips_strict package tlsserver -import "crypto/tls" +import ( + "crypto/fips140" + "crypto/tls" +) -// GetExpectedTLS13Ciphers returns the expected TLS 1.3 cipher for a non-FIPS build. +// GetExpectedTLS13Ciphers returns the expected TLS 1.3 cipher. +// TLS 1.3 ciphers are not configurable, so we can hard-code them here. func GetExpectedTLS13Ciphers() []uint16 { - // TLS 1.3 ciphers are not configurable, so we can hard-code them here. + if fips140.Enabled() { + return []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + // tls.TLS_CHACHA20_POLY1305_SHA256 is not supported by GOFIPS140 + } + } + return []uint16{ tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, @@ -18,8 +29,18 @@ func GetExpectedTLS13Ciphers() []uint16 { } // GetExpectedTLS13CipherNMapKeyExchangeInfoValue returns the expected key exchange info value -// which is shown by nmap in parenthesis next to the cipher name for a non-FIPS build. +// which is shown by nmap in parentheses next to the cipher name. func GetExpectedTLS13CipherNMapKeyExchangeInfoValue(cipher uint16) string { + if fips140.Enabled() { + switch cipher { + case tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384: + return "secp256r1" + default: + return "unknown key exchange value" + } + } + switch cipher { case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, diff --git a/internal/testutil/tlsserver/tls13_ciphers_fips.go b/internal/testutil/tlsserver/tls13_ciphers_fips.go index ead0e3ffd..bd586b2c1 100644 --- a/internal/testutil/tlsserver/tls13_ciphers_fips.go +++ b/internal/testutil/tlsserver/tls13_ciphers_fips.go @@ -1,13 +1,14 @@ -// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// This file overrides tls13_ciphers.go when Pinniped is built in FIPS-only mode using the legacy boring crypto compiler. //go:build fips_strict package tlsserver import "crypto/tls" -// GetExpectedTLS13Ciphers returns the expected TLS 1.3 cipher for a FIPS build. +// GetExpectedTLS13Ciphers returns the expected TLS 1.3 cipher for a legacy boring crypto FIPS build. func GetExpectedTLS13Ciphers() []uint16 { // TLS 1.3 ciphers are not configurable, so we can hard-code them here. return []uint16{ @@ -18,7 +19,7 @@ func GetExpectedTLS13Ciphers() []uint16 { } // GetExpectedTLS13CipherNMapKeyExchangeInfoValue returns the expected key exchange info value -// which is shown by nmap in parenthesis next to the cipher name for a FIPS build. +// which is shown by nmap in parentheses next to the cipher name for a legacy boring crypto FIPS build. func GetExpectedTLS13CipherNMapKeyExchangeInfoValue(cipher uint16) string { switch cipher { case tls.TLS_AES_128_GCM_SHA256, diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index 338cd52bf..9ec4a4a29 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -1542,7 +1542,9 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10445) require.Empty(t, stderr) - require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Default(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) + expectedCiphers := testlib.GetExpectedCiphers(ptls.Default(nil), testlib.DefaultCipherSuitePreference()) + require.Contains(t, stdout, expectedCiphers, "wantedToContain:\n%s\nactual stdout:\n%s", expectedCiphers, stdout) + }) }) diff --git a/test/integration/limited_ciphers_test.go b/test/integration/limited_ciphers_test.go index ebfa44062..ccb2edcf9 100644 --- a/test/integration/limited_ciphers_test.go +++ b/test/integration/limited_ciphers_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2024-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !fips_strict @@ -8,12 +8,16 @@ package integration import ( "crypto/tls" "testing" + + "go.pinniped.dev/test/testlib" ) // TestLimitedCiphersNotFIPS_Disruptive will confirm that the Pinniped Supervisor and Concierge expose only those // ciphers listed in configuration, when compiled in non-FIPS mode. // This does not test the CLI, since it does not have a feature to limit cipher suites. func TestLimitedCiphersNotFIPS_Disruptive(t *testing.T) { + testlib.SkipTestWhenUsingGOFIPS140(t) + performLimitedCiphersTest(t, // The user-configured ciphers for both the Supervisor and Concierge. // This is a subset of the hardcoded ciphers from profiles.go. @@ -41,3 +45,40 @@ func TestLimitedCiphersNotFIPS_Disruptive(t *testing.T) { }, ) } + +// TestLimitedCiphersFIPS_Disruptive will confirm that the Pinniped Supervisor and Concierge expose only those +// ciphers listed in configuration, when compiled in GOFIPS140 mode. +// This does not test the CLI, since it does not have a feature to limit cipher suites. +func TestLimitedCiphersGOFIPS140_Disruptive(t *testing.T) { + testlib.SkipTestUnlessUsingGOFIPS140(t) + + performLimitedCiphersTest(t, + // The user-configured ciphers for both the Supervisor and Concierge. + // This is a subset of the hardcoded ciphers from profiles_fips_strict.go. + []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + }, + // Expected server configuration for the Supervisor's OIDC endpoints. + &tls.Config{ + MinVersion: tls.VersionTLS12, // Supervisor OIDC always allows TLS 1.2 clients to connect + MaxVersion: tls.VersionTLS13, + CipherSuites: []uint16{ + // Supervisor OIDC endpoints configured with EC certs use only EC ciphers. + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + }, + }, + // Expected server configuration for the Supervisor and Concierge aggregated API endpoints. + &tls.Config{ + MinVersion: tls.VersionTLS12, // always allow TLS 1.2 in fips mode + MaxVersion: tls.VersionTLS13, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + }, + }, + ) +} diff --git a/test/integration/ptls_fips_test.go b/test/integration/ptls_fips_test.go index 8609aa97d..1a7b6ca49 100644 --- a/test/integration/ptls_fips_test.go +++ b/test/integration/ptls_fips_test.go @@ -1,6 +1,7 @@ -// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// This file overrides ptls_gofips140_test.go when Pinniped is built in FIPS-only mode using the legacy boring crypto compiler. //go:build fips_strict package integration @@ -59,15 +60,17 @@ func TestFIPSCipherSuites_Parallel(t *testing.T) { } // Every profile should use the same cipher suites in FIPS mode, because FIPS requires these ciphers. -// Please treat this as a read-only const. -var expectedFIPSCipherSuites = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, +func expectedFIPSCipherSuites() []uint16 { + // These are the expected values for boring crypto. + return []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + } } -func TestDefault_Parallel(t *testing.T) { +func TestFIPSDefault_Parallel(t *testing.T) { _ = testlib.IntegrationEnv(t) // this function call is required for integration tests aCertPool := x509.NewCertPool() @@ -76,7 +79,7 @@ func TestDefault_Parallel(t *testing.T) { expected := &tls.Config{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, - CipherSuites: expectedFIPSCipherSuites, + CipherSuites: expectedFIPSCipherSuites(), NextProtos: []string{"h2", "http/1.1"}, RootCAs: aCertPool, } @@ -84,7 +87,7 @@ func TestDefault_Parallel(t *testing.T) { require.Equal(t, expected, actual) } -func TestDefaultLDAP_Parallel(t *testing.T) { +func TestFIPSDefaultLDAP_Parallel(t *testing.T) { _ = testlib.IntegrationEnv(t) // this function call is required for integration tests aCertPool := x509.NewCertPool() @@ -93,7 +96,7 @@ func TestDefaultLDAP_Parallel(t *testing.T) { expected := &tls.Config{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, - CipherSuites: expectedFIPSCipherSuites, + CipherSuites: expectedFIPSCipherSuites(), NextProtos: []string{"h2", "http/1.1"}, RootCAs: aCertPool, } @@ -101,7 +104,7 @@ func TestDefaultLDAP_Parallel(t *testing.T) { require.Equal(t, expected, actual) } -func TestSecure_Parallel(t *testing.T) { +func TestFIPSSecure_Parallel(t *testing.T) { _ = testlib.IntegrationEnv(t) // this function call is required for integration tests aCertPool := x509.NewCertPool() @@ -110,7 +113,7 @@ func TestSecure_Parallel(t *testing.T) { expected := &tls.Config{ MinVersion: tls.VersionTLS12, // allow TLS 1.2 in FIPS mode MaxVersion: tls.VersionTLS13, - CipherSuites: expectedFIPSCipherSuites, + CipherSuites: expectedFIPSCipherSuites(), NextProtos: []string{"h2", "http/1.1"}, RootCAs: aCertPool, } @@ -118,14 +121,14 @@ func TestSecure_Parallel(t *testing.T) { require.Equal(t, expected, actual) } -func TestSecureServing_Parallel(t *testing.T) { +func TestFIPSSecureServing_Parallel(t *testing.T) { _ = testlib.IntegrationEnv(t) // this function call is required for integration tests opts := &options.SecureServingOptionsWithLoopback{SecureServingOptions: &options.SecureServingOptions{}} ptls.SecureServing(opts) - expectedFIPSCipherSuiteNames := make([]string, len(expectedFIPSCipherSuites)) - for i, suite := range expectedFIPSCipherSuites { + expectedFIPSCipherSuiteNames := make([]string, len(expectedFIPSCipherSuites())) + for i, suite := range expectedFIPSCipherSuites() { expectedFIPSCipherSuiteNames[i] = tls.CipherSuiteName(suite) } diff --git a/test/integration/ptls_gofips140_test.go b/test/integration/ptls_gofips140_test.go new file mode 100644 index 000000000..7c5a4ec32 --- /dev/null +++ b/test/integration/ptls_gofips140_test.go @@ -0,0 +1,145 @@ +// Copyright 2026 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build !fips_strict + +package integration + +import ( + "crypto/tls" + "crypto/x509" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/server/options" + "k8s.io/client-go/util/cert" + + "go.pinniped.dev/internal/crypto/ptls" + "go.pinniped.dev/internal/testutil/tlsserver" + "go.pinniped.dev/test/testlib" +) + +// Note: Everything in this file is an integration test only because we do not support build tags on unit tests. +// These are effectively unit tests for the ptls package when compiled in FIPS mode. + +// TestFIPSCipherSuites_Parallel ensures that if the list of default FIPS cipher suites changes, then we will know. +// If this test ever fails during a golang upgrade, then we may need to change which ciphers we are using in +// the ptls package in FIPS mode. +func TestFIPSCipherSuites_Parallel(t *testing.T) { + _ = testlib.IntegrationEnv(t) // this function call is required for integration tests + testlib.SkipTestUnlessUsingGOFIPS140(t) + + server, ca := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // use the default fips config which contains a hard coded list of cipher suites + // that should be equal to the default list of fips cipher suites. + // assert that the client hello response has the same tls config as this test server. + tlsserver.AssertTLS(t, r, ptls.Default) + }), tlsserver.RecordTLSHello) + + pool, err := cert.NewPoolFromBytes(ca) + require.NoError(t, err) + // create a tls config that does not explicitly set cipher suites, + // and therefore uses goboring or native fips's default fips ciphers. + defaultConfig := &tls.Config{ + RootCAs: pool, + NextProtos: ptls.Default(nil).NextProtos, // we do not care about field for this test, so just make it match + } + transport := http.Transport{ + TLSClientConfig: defaultConfig, + ForceAttemptHTTP2: true, + } + // make a request against the test server, which will validate that the + // tls config of the client without explicitly set ciphers + // is the same as the tls config of the test server with explicitly + // set ciphers from ptls. + request, _ := http.NewRequest("GET", server.URL, nil) + response, err := transport.RoundTrip(request) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) +} + +// Every profile should use the same cipher suites in FIPS mode, because FIPS requires these ciphers. +func expectedFIPSCipherSuites() []uint16 { + // These are the expected values for Go's "native" GOFIPS140. + return []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + } +} + +func TestFIPSDefault_Parallel(t *testing.T) { + _ = testlib.IntegrationEnv(t) // this function call is required for integration tests + testlib.SkipTestUnlessUsingGOFIPS140(t) + + aCertPool := x509.NewCertPool() + + actual := ptls.Default(aCertPool) + expected := &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + CipherSuites: expectedFIPSCipherSuites(), + NextProtos: []string{"h2", "http/1.1"}, + RootCAs: aCertPool, + } + + require.Equal(t, expected, actual) +} + +func TestFIPSDefaultLDAP_Parallel(t *testing.T) { + _ = testlib.IntegrationEnv(t) // this function call is required for integration tests + testlib.SkipTestUnlessUsingGOFIPS140(t) + + aCertPool := x509.NewCertPool() + + actual := ptls.DefaultLDAP(aCertPool) + expected := &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + CipherSuites: expectedFIPSCipherSuites(), + NextProtos: []string{"h2", "http/1.1"}, + RootCAs: aCertPool, + } + + require.Equal(t, expected, actual) +} + +func TestFIPSSecure_Parallel(t *testing.T) { + _ = testlib.IntegrationEnv(t) // this function call is required for integration tests + testlib.SkipTestUnlessUsingGOFIPS140(t) + + aCertPool := x509.NewCertPool() + + actual := ptls.Secure(aCertPool) + expected := &tls.Config{ + MinVersion: tls.VersionTLS12, // allow TLS 1.2 in FIPS mode + MaxVersion: tls.VersionTLS13, + CipherSuites: expectedFIPSCipherSuites(), + NextProtos: []string{"h2", "http/1.1"}, + RootCAs: aCertPool, + } + + require.Equal(t, expected, actual) +} + +func TestFIPSSecureServing_Parallel(t *testing.T) { + _ = testlib.IntegrationEnv(t) // this function call is required for integration tests + testlib.SkipTestUnlessUsingGOFIPS140(t) + + opts := &options.SecureServingOptionsWithLoopback{SecureServingOptions: &options.SecureServingOptions{}} + ptls.SecureServing(opts) + + expectedFIPSCipherSuiteNames := make([]string, len(expectedFIPSCipherSuites())) + for i, suite := range expectedFIPSCipherSuites() { + expectedFIPSCipherSuiteNames[i] = tls.CipherSuiteName(suite) + } + + require.Equal(t, options.SecureServingOptionsWithLoopback{ + SecureServingOptions: &options.SecureServingOptions{ + CipherSuites: expectedFIPSCipherSuiteNames, + MinTLSVersion: "VersionTLS12", // allow TLS 1.2 in FIPS mode + }, + }, *opts) +} diff --git a/test/integration/securetls_test.go b/test/integration/securetls_test.go index 30d94f415..327f0b855 100644 --- a/test/integration/securetls_test.go +++ b/test/integration/securetls_test.go @@ -1,4 +1,4 @@ -// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -94,7 +94,9 @@ func TestSecureTLSConciergeAggregatedAPI_Parallel(t *testing.T) { stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10446) require.Empty(t, stderr) - require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) + expectedCiphers := testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference()) + require.Contains(t, stdout, expectedCiphers, + "wantedToContain:\n%s\nactual stdout:\n%s", expectedCiphers, stdout) } // TLS checks safe to run in parallel with serial tests, see main_test.go. @@ -109,7 +111,9 @@ func TestSecureTLSSupervisorAggregatedAPI_Parallel(t *testing.T) { stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", 10447) require.Empty(t, stderr) - require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) + expectedCiphers := testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference()) + require.Contains(t, stdout, expectedCiphers, + "wantedToContain:\n%s\nactual stdout:\n%s", expectedCiphers, stdout) } func TestSecureTLSSupervisor(t *testing.T) { @@ -151,7 +155,9 @@ func TestSecureTLSSupervisor(t *testing.T) { defaultECDSAOnly.CipherSuites = ciphers require.Empty(t, stderr) - require.Contains(t, stdout, testlib.GetExpectedCiphers(defaultECDSAOnly, testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) + expectedCiphers := testlib.GetExpectedCiphers(defaultECDSAOnly, testlib.DefaultCipherSuitePreference()) + require.Contains(t, stdout, expectedCiphers, + "wantedToContain:\n%s\nactual stdout:\n%s", expectedCiphers, stdout) } type fakeT struct { diff --git a/test/testlib/securetls_preference_fips.go b/test/testlib/securetls_preference_fips.go index 85c4de7e2..a650cc4a6 100644 --- a/test/testlib/securetls_preference_fips.go +++ b/test/testlib/securetls_preference_fips.go @@ -1,12 +1,16 @@ -// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// This file overrides securetls_preference_nonfips.go when Pinniped is built in FIPS-only mode using the legacy boring crypto compiler. //go:build fips_strict package testlib +// DefaultCipherSuitePreference returns an expected value for tests. // Because of a bug in nmap, the cipher suite preference is // incorrectly shown as 'client' in some cases. // in fips-only mode, it correctly shows the cipher preference // as 'server', while in non-fips mode it shows as 'client'. -const DefaultCipherSuitePreference = "server" +func DefaultCipherSuitePreference() string { + return "server" +} diff --git a/test/testlib/securetls_preference_nonfips.go b/test/testlib/securetls_preference_nonfips.go index e114aa1f0..e6ee1a475 100644 --- a/test/testlib/securetls_preference_nonfips.go +++ b/test/testlib/securetls_preference_nonfips.go @@ -1,12 +1,20 @@ -// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !fips_strict package testlib +import "crypto/fips140" + +// DefaultCipherSuitePreference returns an expected value for tests. // Because of a bug in nmap, the cipher suite preference is // incorrectly shown as 'client' in some cases. // in fips-only mode, it correctly shows the cipher preference // as 'server', while in non-fips mode it shows as 'client'. -const DefaultCipherSuitePreference = "client" +func DefaultCipherSuitePreference() string { + if fips140.Enabled() { + return "server" + } + return "client" +} diff --git a/test/testlib/skip.go b/test/testlib/skip.go index 9a052ecad..7b0ced753 100644 --- a/test/testlib/skip.go +++ b/test/testlib/skip.go @@ -1,9 +1,12 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testlib -import "testing" +import ( + "crypto/fips140" + "testing" +) // SkipUnlessIntegration skips the current test if `-short` has been passed to `go test`. func SkipUnlessIntegration(t *testing.T) { @@ -14,6 +17,22 @@ func SkipUnlessIntegration(t *testing.T) { } } +func SkipTestWhenUsingGOFIPS140(t *testing.T) { + t.Helper() + + if fips140.Enabled() { + t.Skip("this test is skipped when using GOFIPS140") + } +} + +func SkipTestUnlessUsingGOFIPS140(t *testing.T) { + t.Helper() + + if !fips140.Enabled() { + t.Skip("this test requires GOFIPS140") + } +} + func SkipTestWhenLDAPIsUnavailable(t *testing.T, env *TestEnv) { t.Helper()