diff --git a/deploy/concierge/deployment.yaml b/deploy/concierge/deployment.yaml index 26b892524..6aac1fd1e 100644 --- a/deploy/concierge/deployment.yaml +++ b/deploy/concierge/deployment.yaml @@ -100,6 +100,9 @@ data: log: level: (@= getAndValidateLogLevel() @) (@ end @) + tls: + onedottwo: + allowedCiphers: (@= str(data.values.allowed_ciphers_for_tls_onedottwo) @) --- #@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "": apiVersion: v1 diff --git a/deploy/concierge/values.yaml b/deploy/concierge/values.yaml index 0e5708956..24ccea66a 100644 --- a/deploy/concierge/values.yaml +++ b/deploy/concierge/values.yaml @@ -214,3 +214,20 @@ https_proxy: "" #@ localhost endpoints, and the known instance metadata IP address for public cloud providers." #@schema/desc no_proxy_desc no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,.cluster.local" + +#@schema/title "Allowed Ciphers for TLS 1.2" +#@ allowed_ciphers_for_tls_onedottwo_desc = "When specified, only the ciphers listed will be used for TLS 1.2. \ +#@ This includes both server-side and client-side TLS connections. \ +#@ This list must only include cipher suites that Pinniped is configured to accept \ +#@ (see internal/crypto/ptls/profiles.go and internal/crypto/ptls/profiles_fips_strict.go). \ +#@ Allowing too few ciphers may cause critical parts of Pinniped to be unable to function. For example, \ +#@ Kubernetes pod readiness checks, Pinniped pods acting as a client to the Kubernetes API server, \ +#@ Pinniped pods acting as a client to external identity providers, or Pinniped pods acting as an APIService server \ +#@ all need to be able to function with the allowed TLS cipher suites. \ +#@ An empty array means accept Pinniped's defaults." +#@schema/desc allowed_ciphers_for_tls_onedottwo_desc +#@schema/examples ("Example with a few secure ciphers", ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"]) +#! No type, default, or validation is required here. +#! An empty array is perfectly valid, as is any array of strings. +allowed_ciphers_for_tls_onedottwo: +- "" diff --git a/deploy/supervisor/helpers.lib.yaml b/deploy/supervisor/helpers.lib.yaml index 818b673a8..693dedaa1 100644 --- a/deploy/supervisor/helpers.lib.yaml +++ b/deploy/supervisor/helpers.lib.yaml @@ -53,6 +53,11 @@ _: #@ template.replace(data.values.custom_labels) #@ "apiService": defaultResourceNameWithSuffix("api"), #@ }, #@ "labels": labels(), +#@ "tls": { +#@ "onedottwo": { +#@ "allowedCiphers": data.values.allowed_ciphers_for_tls_onedottwo +#@ } +#@ } #@ } #@ if data.values.log_level: #@ config["log"] = {} diff --git a/deploy/supervisor/values.yaml b/deploy/supervisor/values.yaml index 750a5e075..f4aab5e62 100644 --- a/deploy/supervisor/values.yaml +++ b/deploy/supervisor/values.yaml @@ -203,3 +203,20 @@ no_proxy: "$(KUBERNETES_SERVICE_HOST),169.254.169.254,127.0.0.1,localhost,.svc,. #@schema/nullable #@schema/validation ("a map with keys 'http' and 'https', whose values are either the string 'disabled' or a map having keys 'network' and 'address', and the value of 'network' must be one of the allowed values", validate_endpoints) endpoints: { } + +#@schema/title "Allowed Ciphers for TLS 1.2" +#@ allowed_ciphers_for_tls_onedottwo_desc = "When specified, only the ciphers listed will be used for TLS 1.2. \ +#@ This includes both server-side and client-side TLS connections. \ +#@ This list must only include cipher suites that Pinniped is configured to accept \ +#@ (see internal/crypto/ptls/profiles.go and internal/crypto/ptls/profiles_fips_strict.go). \ +#@ Allowing too few ciphers may cause critical parts of Pinniped to be unable to function. For example, \ +#@ Kubernetes pod readiness checks, Pinniped pods acting as a client to the Kubernetes API server, \ +#@ Pinniped pods acting as a client to external identity providers, or Pinniped pods acting as an APIService server \ +#@ all need to be able to function with the allowed TLS cipher suites. \ +#@ An empty array means accept Pinniped's defaults." +#@schema/desc allowed_ciphers_for_tls_onedottwo_desc +#@schema/examples ("Example with a few secure ciphers", ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"]) +#! No type, default, or validation is required here. +#! An empty array is perfectly valid, as is any array of strings. +allowed_ciphers_for_tls_onedottwo: +- "" diff --git a/internal/concierge/server/server.go b/internal/concierge/server/server.go index 728acb353..1e19a8395 100644 --- a/internal/concierge/server/server.go +++ b/internal/concierge/server/server.go @@ -112,11 +112,14 @@ func (a *App) runServer(ctx context.Context) error { featuregates.DisableKubeFeatureGate(features.UnauthenticatedHTTP2DOSMitigation) // Read the server config file. - cfg, err := concierge.FromPath(ctx, a.configPath) + cfg, err := concierge.FromPath(ctx, a.configPath, ptls.SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo) if err != nil { return fmt.Errorf("could not load config: %w", err) } + // The above server config should have set the allowed ciphers global, so now log the ciphers for all profiles. + ptls.LogAllProfiles(plog.New()) + // Discover in which namespace we are installed. podInfo, err := downward.Load(a.downwardAPIPath) if err != nil { diff --git a/internal/config/concierge/config.go b/internal/config/concierge/config.go index a95619565..d2faeeda4 100644 --- a/internal/config/concierge/config.go +++ b/internal/config/concierge/config.go @@ -15,6 +15,7 @@ import ( "sigs.k8s.io/yaml" "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/plog" ) @@ -42,7 +43,7 @@ const ( // Note! The Config file should contain base64-encoded WebhookCABundle data. // This function will decode that base64-encoded data to PEM bytes to be stored // in the Config. -func FromPath(ctx context.Context, path string) (*Config, error) { +func FromPath(ctx context.Context, path string, setAllowedCiphers ptls.SetAllowedCiphersFunc) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) @@ -83,6 +84,10 @@ func FromPath(ctx context.Context, path string) (*Config, error) { return nil, fmt.Errorf("validate log level: %w", err) } + if err := setAllowedCiphers(config.TLS.OneDotTwo.AllowedCiphers); err != nil { + return nil, fmt.Errorf("validate tls: %w", err) + } + if config.Labels == nil { config.Labels = make(map[string]string) } diff --git a/internal/config/concierge/config_test.go b/internal/config/concierge/config_test.go index f1d52c3ed..37eacd150 100644 --- a/internal/config/concierge/config_test.go +++ b/internal/config/concierge/config_test.go @@ -5,6 +5,7 @@ package concierge import ( "context" + "fmt" "os" "testing" @@ -17,10 +18,11 @@ import ( func TestFromPath(t *testing.T) { tests := []struct { - name string - yaml string - wantConfig *Config - wantError string + name string + yaml string + allowedCiphersError error + wantConfig *Config + wantError string }{ { name: "Fully filled out", @@ -59,6 +61,12 @@ func TestFromPath(t *testing.T) { imagePullSecrets: [kube-cert-agent-image-pull-secret] log: level: debug + tls: + onedottwo: + allowedCiphers: + - foo + - bar + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 `), wantConfig: &Config{ DiscoveryInfo: DiscoveryInfoSpec{ @@ -98,6 +106,15 @@ func TestFromPath(t *testing.T) { Log: plog.LogSpec{ Level: plog.LevelDebug, }, + TLS: TLSSpec{ + OneDotTwo: TLSProtocolSpec{ + AllowedCiphers: []string{ + "foo", + "bar", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + }, + }, + }, }, }, { @@ -587,6 +604,31 @@ func TestFromPath(t *testing.T) { `), wantError: "validate apiGroupSuffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')", }, + { + name: "returns setAllowedCiphers errors", + yaml: here.Doc(` + --- + names: + servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate + credentialIssuer: pinniped-config + apiService: pinniped-api + impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value + impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value + impersonationCACertificateSecret: impersonationCACertificateSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + agentServiceAccount: agentServiceAccount-value + impersonationProxyServiceAccount: impersonationProxyServiceAccount-value + impersonationProxyLegacySecret: impersonationProxyLegacySecret-value + tls: + onedottwo: + allowedCiphers: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + `), + allowedCiphersError: fmt.Errorf("some error from setAllowedCiphers"), + wantError: "validate tls: some error from setAllowedCiphers", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -595,10 +637,10 @@ func TestFromPath(t *testing.T) { // Write yaml to temp file f, err := os.CreateTemp("", "pinniped-test-config-yaml-*") require.NoError(t, err) - defer func() { + t.Cleanup(func() { err := os.Remove(f.Name()) require.NoError(t, err) - }() + }) _, err = f.WriteString(test.yaml) require.NoError(t, err) err = f.Close() @@ -607,14 +649,23 @@ func TestFromPath(t *testing.T) { // Test FromPath() ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - config, err := FromPath(ctx, f.Name()) + + var actualCiphers []string + setAllowedCiphers := func(ciphers []string) error { + actualCiphers = ciphers + return test.allowedCiphersError + } + + config, err := FromPath(ctx, f.Name(), setAllowedCiphers) if test.wantError != "" { require.EqualError(t, err, test.wantError) - } else { - require.NoError(t, err) - require.Equal(t, test.wantConfig, config) + return } + + require.NoError(t, err) + require.Equal(t, test.wantConfig, config) + require.Equal(t, test.wantConfig.TLS.OneDotTwo.AllowedCiphers, actualCiphers) }) } } diff --git a/internal/config/concierge/types.go b/internal/config/concierge/types.go index ffcaac5b0..b4b814845 100644 --- a/internal/config/concierge/types.go +++ b/internal/config/concierge/types.go @@ -16,6 +16,18 @@ type Config struct { KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"` Labels map[string]string `json:"labels"` Log plog.LogSpec `json:"log"` + TLS TLSSpec `json:"tls"` +} + +type TLSSpec struct { + OneDotTwo TLSProtocolSpec `json:"onedottwo"` +} + +type TLSProtocolSpec struct { + // AllowedCiphers will permit Pinniped to use only the listed ciphers. + // This affects Pinniped both when it acts as a client and as a server. + // If empty, Pinniped will use a built-in list of ciphers. + AllowedCiphers []string `json:"allowedCiphers"` } // DiscoveryInfoSpec contains configuration knobs specific to diff --git a/internal/config/supervisor/config.go b/internal/config/supervisor/config.go index 1e7c60d1b..7c5119cbb 100644 --- a/internal/config/supervisor/config.go +++ b/internal/config/supervisor/config.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/yaml" "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/plog" ) @@ -35,7 +36,7 @@ const ( // FromPath loads an Config from a provided local file path, inserts any // defaults (from the Config documentation), and verifies that the config is // valid (Config documentation). -func FromPath(ctx context.Context, path string) (*Config, error) { +func FromPath(ctx context.Context, path string, setAllowedCiphers ptls.SetAllowedCiphersFunc) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) @@ -95,6 +96,9 @@ func FromPath(ctx context.Context, path string) (*Config, error) { if err := validateAtLeastOneEnabledEndpoint(*config.Endpoints.HTTPS, *config.Endpoints.HTTP); err != nil { return nil, fmt.Errorf("validate endpoints: %w", err) } + if err := setAllowedCiphers(config.TLS.OneDotTwo.AllowedCiphers); err != nil { + return nil, fmt.Errorf("validate tls: %w", err) + } return &config, nil } diff --git a/internal/config/supervisor/config_test.go b/internal/config/supervisor/config_test.go index 61f83aed8..d88344f3b 100644 --- a/internal/config/supervisor/config_test.go +++ b/internal/config/supervisor/config_test.go @@ -18,10 +18,11 @@ import ( func TestFromPath(t *testing.T) { tests := []struct { - name string - yaml string - wantConfig *Config - wantError string + name string + yaml string + allowedCiphersError error + wantConfig *Config + wantError string }{ { name: "Happy", @@ -45,6 +46,12 @@ func TestFromPath(t *testing.T) { level: info format: json aggregatedAPIServerPort: 12345 + tls: + onedottwo: + allowedCiphers: + - foo + - bar + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 `), wantConfig: &Config{ APIGroupSuffix: ptr.To("some.suffix.com"), @@ -70,6 +77,15 @@ func TestFromPath(t *testing.T) { Format: plog.FormatJSON, }, AggregatedAPIServerPort: ptr.To[int64](12345), + TLS: TLSSpec{ + OneDotTwo: TLSProtocolSpec{ + AllowedCiphers: []string{ + "foo", + "bar", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + }, + }, + }, }, }, { @@ -251,6 +267,20 @@ func TestFromPath(t *testing.T) { `), wantError: "validate aggregatedAPIServerPort: must be within range 1024 to 65535", }, + { + name: "returns setAllowedCiphers errors", + yaml: here.Doc(` + --- + names: + defaultTLSCertificateSecret: my-secret-name + tls: + onedottwo: + allowedCiphers: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + `), + allowedCiphersError: fmt.Errorf("some error from setAllowedCiphers"), + wantError: "validate tls: some error from setAllowedCiphers", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -259,10 +289,10 @@ func TestFromPath(t *testing.T) { // Write yaml to temp file f, err := os.CreateTemp("", "pinniped-test-config-yaml-*") require.NoError(t, err) - defer func() { + t.Cleanup(func() { err := os.Remove(f.Name()) require.NoError(t, err) - }() + }) _, err = f.WriteString(test.yaml) require.NoError(t, err) err = f.Close() @@ -271,14 +301,23 @@ func TestFromPath(t *testing.T) { // Test FromPath() ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - config, err := FromPath(ctx, f.Name()) + + var actualCiphers []string + setAllowedCiphers := func(ciphers []string) error { + actualCiphers = ciphers + return test.allowedCiphersError + } + + config, err := FromPath(ctx, f.Name(), setAllowedCiphers) if test.wantError != "" { require.EqualError(t, err, test.wantError) - } else { - require.NoError(t, err) - require.Equal(t, test.wantConfig, config) + return } + + require.NoError(t, err) + require.Equal(t, test.wantConfig, config) + require.Equal(t, test.wantConfig.TLS.OneDotTwo.AllowedCiphers, actualCiphers) }) } } diff --git a/internal/config/supervisor/types.go b/internal/config/supervisor/types.go index 918375f67..f1b2870ec 100644 --- a/internal/config/supervisor/types.go +++ b/internal/config/supervisor/types.go @@ -15,6 +15,18 @@ type Config struct { Log plog.LogSpec `json:"log"` Endpoints *Endpoints `json:"endpoints"` AggregatedAPIServerPort *int64 `json:"aggregatedAPIServerPort"` + TLS TLSSpec `json:"tls"` +} + +type TLSSpec struct { + OneDotTwo TLSProtocolSpec `json:"onedottwo"` +} + +type TLSProtocolSpec struct { + // AllowedCiphers will permit Pinniped to use only the listed ciphers. + // This affects Pinniped both when it acts as a client and as a server. + // If empty, Pinniped will use a built-in list of ciphers. + AllowedCiphers []string `json:"allowedCiphers"` } // NamesConfigSpec configures the names of some Kubernetes resources for the Supervisor. diff --git a/internal/crypto/ptls/common.go b/internal/crypto/ptls/common.go new file mode 100644 index 000000000..3fe78e798 --- /dev/null +++ b/internal/crypto/ptls/common.go @@ -0,0 +1,226 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ptls + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "slices" + "strings" + "sync/atomic" + + "k8s.io/apimachinery/pkg/util/sets" + + "go.pinniped.dev/internal/plog" +) + +// validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo is the validated configuration of allowed cipher suites +// provided by the user, as set by SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo(). +// This global variable is atomic so that it can not be set and read at the same time. +// +//nolint:gochecknoglobals // this needs to be global because it will be set at application startup from configuration values +var validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo atomic.Value + +type SetAllowedCiphersFunc func([]string) error + +// SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo allows configuration/setup components to constrain the +// allowed TLS ciphers for TLS1.2. It implements SetAllowedCiphersFunc. +func SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo(userConfiguredAllowedCipherSuitesForTLSOneDotTwo []string) error { + plog.Info("setting user-configured allowed ciphers for TLS 1.2", "userConfiguredAllowedCipherSuites", userConfiguredAllowedCipherSuitesForTLSOneDotTwo) + + validatedSuites, err := validateAllowedCiphers( + allHardcodedAllowedCipherSuites(), + userConfiguredAllowedCipherSuitesForTLSOneDotTwo, + ) + if err != nil { + return err + } + + validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo.Store(validatedSuites) + return nil +} + +// getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo returns the user-configured list of allowed ciphers for TLS1.2. +// It is not exported so that it is only available to this package. +func getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo() []*tls.CipherSuite { + userConfiguredAllowedCipherSuites, ok := (validatedUserConfiguredAllowedCipherSuitesForTLSOneDotTwo.Load()).([]*tls.CipherSuite) + if ok { + return userConfiguredAllowedCipherSuites + } + return nil +} + +// constrainCipherSuites returns the intersection of its parameters, as a list of cipher suite IDs. +// If userConfiguredAllowedCipherSuites is empty, it will return the list from cipherSuites as IDs. +func constrainCipherSuites( + cipherSuites []*tls.CipherSuite, + userConfiguredAllowedCipherSuites []*tls.CipherSuite, +) []uint16 { + // If the user did not configure any allowed ciphers suites, then return the IDs + // for the ciphers in cipherSuites in the same sort order as quickly as possible. + if len(userConfiguredAllowedCipherSuites) == 0 { + cipherSuiteIDs := make([]uint16, len(cipherSuites)) + for i := range cipherSuites { + cipherSuiteIDs[i] = cipherSuites[i].ID + } + return cipherSuiteIDs + } + + // Make two sets so we can intersect them below. + cipherSuiteIDsSet := sets.New[uint16]() + for _, s := range cipherSuites { + cipherSuiteIDsSet.Insert(s.ID) + } + userConfiguredAllowedCipherSuiteIDsSet := sets.New[uint16]() + for _, s := range userConfiguredAllowedCipherSuites { + userConfiguredAllowedCipherSuiteIDsSet.Insert(s.ID) + } + + // Calculate the intersection of sets. + intersection := cipherSuiteIDsSet.Intersection(userConfiguredAllowedCipherSuiteIDsSet) + + // If the user did not provide any valid allowed cipher suites, use cipherSuiteIDsSet. + // Note that the user-configured allowed cipher suites are validated elsewhere, so + // this should only happen when the user chose not to specify any allowed cipher suites. + if len(intersection) == 0 { + intersection = cipherSuiteIDsSet + } + + result := intersection.UnsortedList() + // Preserve the original order as shown in the cipherSuites parameter. + slices.SortFunc(result, func(a, b uint16) int { + return slices.IndexFunc(cipherSuites, func(cipher *tls.CipherSuite) bool { return cipher.ID == a }) - + slices.IndexFunc(cipherSuites, func(cipher *tls.CipherSuite) bool { return cipher.ID == b }) + }) + + return result +} + +func translateIDIntoSecureCipherSuites(ids []uint16) []*tls.CipherSuite { + golangSecureCipherSuites := tls.CipherSuites() + result := make([]*tls.CipherSuite, 0) + + for _, golangSecureCipherSuite := range golangSecureCipherSuites { + // As of golang 1.22, all cipher suites from tls.CipherSuites are secure, so this is just future-proofing. + if golangSecureCipherSuite.Insecure { // untested + continue + } + + if !slices.Contains(golangSecureCipherSuite.SupportedVersions, tls.VersionTLS12) { + continue + } + + if slices.Contains(ids, golangSecureCipherSuite.ID) { + result = append(result, golangSecureCipherSuite) + } + } + + // Preserve the order as shown in ids + slices.SortFunc(result, func(a, b *tls.CipherSuite) int { + return slices.Index(ids, a.ID) - slices.Index(ids, b.ID) + }) + + return result +} + +// buildTLSConfig will return a tls.Config with CipherSuites from the intersection of cipherSuites and userConfiguredAllowedCipherSuites. +func buildTLSConfig( + rootCAs *x509.CertPool, + cipherSuites []*tls.CipherSuite, + userConfiguredAllowedCipherSuites []*tls.CipherSuite, +) *tls.Config { + return &tls.Config{ + // Can't use SSLv3 because of POODLE and BEAST + // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher + // Can't use TLSv1.1 because of RC4 cipher usage + // + // The Kubernetes API Server must use TLS 1.2, at a minimum, + // to protect the confidentiality of sensitive data during electronic dissemination. + // https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242378 + MinVersion: tls.VersionTLS12, + + CipherSuites: constrainCipherSuites(cipherSuites, userConfiguredAllowedCipherSuites), + + // enable HTTP2 for go's 1.7 HTTP Server + // setting this explicitly is only required in very specific circumstances + // it is simpler to just set it here than to try and determine if we need to + NextProtos: []string{"h2", "http/1.1"}, + + // optional root CAs, nil means use the host's root CA set + RootCAs: rootCAs, + } +} + +// validateAllowedCiphers will take in the user-configured allowed cipher names and validate them against a list of +// ciphers. If any userConfiguredAllowedCipherSuites names are not in the cipherSuites, return a descriptive error. +// An empty list of userConfiguredAllowedCipherSuites means that the user wants the all default ciphers from cipherSuites. +// Returns the tls.CipherSuite representation when all userConfiguredAllowedCipherSuites are valid. +func validateAllowedCiphers( + cipherSuites []*tls.CipherSuite, + userConfiguredAllowedCipherSuites []string, +) ([]*tls.CipherSuite, error) { + if len(userConfiguredAllowedCipherSuites) < 1 { + return nil, nil + } + + cipherSuiteNames := make([]string, len(cipherSuites)) + for i, cipher := range cipherSuites { + cipherSuiteNames[i] = cipher.Name + } + + // Allow some loosening of the names for legacy reasons. + for i := range userConfiguredAllowedCipherSuites { + switch userConfiguredAllowedCipherSuites[i] { + case "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": + userConfiguredAllowedCipherSuites[i] = "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" + case "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": + userConfiguredAllowedCipherSuites[i] = "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + } + } + + // Make sure that all allowedCipherNames are actually configured within Pinniped. + var invalidCipherNames []string + for _, allowedCipherName := range userConfiguredAllowedCipherSuites { + if !slices.Contains(cipherSuiteNames, allowedCipherName) { + invalidCipherNames = append(invalidCipherNames, allowedCipherName) + } + } + + if len(invalidCipherNames) > 0 { + return nil, fmt.Errorf("unrecognized ciphers [%s], ciphers must be from list [%s]", + strings.Join(invalidCipherNames, ", "), + strings.Join(cipherSuiteNames, ", ")) + } + + // Now translate the allowedCipherNames into their *tls.CipherSuite representation. + var validCiphers []*tls.CipherSuite + for _, cipher := range cipherSuites { + if slices.Contains(userConfiguredAllowedCipherSuites, cipher.Name) { + validCiphers = append(validCiphers, cipher) + } + } + + return validCiphers, nil +} + +// allHardcodedAllowedCipherSuites returns the full list of all hardcoded ciphers that are allowed for any profile. +// 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...)) + + // Then append any insecure cipher suites that might be allowed. + // insecureCipherSuiteIDs is empty except when compiled in FIPS mode. + for _, golangInsecureCipherSuite := range tls.InsecureCipherSuites() { + if !slices.Contains(golangInsecureCipherSuite.SupportedVersions, tls.VersionTLS12) { + continue + } + + if slices.Contains(insecureCipherSuiteIDs, golangInsecureCipherSuite.ID) { + result = append(result, golangInsecureCipherSuite) + } + } + return result +} diff --git a/internal/crypto/ptls/common_test.go b/internal/crypto/ptls/common_test.go new file mode 100644 index 000000000..147cb840e --- /dev/null +++ b/internal/crypto/ptls/common_test.go @@ -0,0 +1,468 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ptls + +import ( + "crypto/tls" + "crypto/x509" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSetAllowedCiphersForTLSOneDotTwo(t *testing.T) { + t.Run("with valid ciphers, mutates the global state", func(t *testing.T) { + require.Empty(t, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()) + // With no user-configured allowed ciphers, expect all the hardcoded ciphers + require.Equal(t, []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, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + }, Default(nil).CipherSuites) + require.Equal(t, []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, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + 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, + }, DefaultLDAP(nil).CipherSuites) + require.Empty(t, Secure(nil).CipherSuites) + + t.Cleanup(func() { + err := SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo(nil) + require.NoError(t, err) + require.Nil(t, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()) + + require.Equal(t, []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, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + }, Default(nil).CipherSuites) + require.Equal(t, []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, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + 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, + }, DefaultLDAP(nil).CipherSuites) + require.Empty(t, Secure(nil).CipherSuites) + }) + + userConfiguredAllowedCipherSuites := []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // this is an LDAP-only cipher + } + err := SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo(userConfiguredAllowedCipherSuites) + require.NoError(t, err) + stored := getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo() + var storedNames []string + for _, suite := range stored { + storedNames = append(storedNames, suite.Name) + } + require.Equal(t, userConfiguredAllowedCipherSuites, storedNames) + + require.Equal(t, []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, Default(nil).CipherSuites) + require.Equal(t, []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + }, DefaultLDAP(nil).CipherSuites) + require.Empty(t, Secure(nil).CipherSuites) + }) + + t.Run("SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo calls validateAllowedCiphers and returns error", func(t *testing.T) { + err := SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo([]string{"foo"}) + require.Regexp(t, regexp.QuoteMeta("unrecognized ciphers [foo], ciphers must be from list [TLS")+".*"+regexp.QuoteMeta("]"), err.Error()) + }) +} + +func TestConstrainCipherSuites(t *testing.T) { + tests := []struct { + name string + cipherSuites []*tls.CipherSuite + userConfiguredAllowedCipherSuites []*tls.CipherSuite + wantCipherSuites []uint16 + }{ + { + name: "with empty inputs, returns empty output", + wantCipherSuites: make([]uint16, 0), + }, + { + name: "with empty userConfiguredAllowedCipherSuites, returns cipherSuites", + cipherSuites: []*tls.CipherSuite{ + {ID: 0}, + {ID: 1}, + {ID: 2}, + }, + wantCipherSuites: []uint16{0, 1, 2}, + }, + { + name: "with userConfiguredAllowedCipherSuites, returns only ciphers found in both inputs", + cipherSuites: []*tls.CipherSuite{ + {ID: 0}, + {ID: 1}, + {ID: 2}, + {ID: 3}, + {ID: 4}, + }, + userConfiguredAllowedCipherSuites: []*tls.CipherSuite{ + {ID: 1}, + {ID: 3}, + {ID: 999}, + }, + wantCipherSuites: []uint16{1, 3}, + }, + { + name: "with all invalid userConfiguredAllowedCipherSuites, returns cipherSuites", + cipherSuites: []*tls.CipherSuite{ + {ID: 0}, + {ID: 1}, + {ID: 2}, + {ID: 3}, + {ID: 4}, + }, + userConfiguredAllowedCipherSuites: []*tls.CipherSuite{ + {ID: 1000}, + {ID: 2000}, + {ID: 3000}, + }, + wantCipherSuites: []uint16{0, 1, 2, 3, 4}, + }, + { + name: "preserves order from cipherSuites", + cipherSuites: []*tls.CipherSuite{ + {ID: 0}, + {ID: 1}, + {ID: 2}, + {ID: 3}, + {ID: 4}, + }, + userConfiguredAllowedCipherSuites: []*tls.CipherSuite{ + {ID: 5}, + {ID: 4}, + {ID: 3}, + {ID: 2}, + }, + wantCipherSuites: []uint16{2, 3, 4}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actual := constrainCipherSuites(test.cipherSuites, test.userConfiguredAllowedCipherSuites) + require.Equal(t, test.wantCipherSuites, actual) + }) + } +} + +// TestTLSSecureCipherSuites checks whether golang has changed the list of secure ciphers. +// This was written against golang 1.22.2. +// Pinniped has chosen to use only secure ciphers returned by tls.CipherSuites, so in the future we want to be aware of +// changes to this list of ciphers (additions or removals). +// +// If golang adds ciphers, we should consider adding them to the list of possible ciphers for Pinniped's profiles. +// If golang removes ciphers, we should consider removing them from the list of possible ciphers for Pinniped's profiles. +// Any cipher modifications should be added to release notes so that Pinniped admins can choose to modify their +// allowedCiphers accordingly. +func TestTLSSecureCipherSuites(t *testing.T) { + expectedCipherSuites := []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + } + + tlsSecureCipherSuites := tls.CipherSuites() + require.Equal(t, len(expectedCipherSuites), len(tlsSecureCipherSuites)) + for _, suite := range tlsSecureCipherSuites { + require.False(t, suite.Insecure) + require.Contains(t, expectedCipherSuites, suite.ID) + } +} + +func TestBuildTLSConfig(t *testing.T) { + t.Parallel() + + aCertPool := x509.NewCertPool() + + tests := []struct { + name string + rootCAs *x509.CertPool + cipherSuites []*tls.CipherSuite + userConfiguredAllowedCipherSuiteIDs []uint16 + wantConfig *tls.Config + }{ + { + name: "happy path", + rootCAs: aCertPool, + cipherSuites: tls.CipherSuites(), + wantConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, //nolint:gosec // this is for testing purposes + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + }, + NextProtos: []string{"h2", "http/1.1"}, + RootCAs: aCertPool, + }, + }, + { + name: "with no userConfiguredAllowedCipherSuites, returns cipherSuites", + cipherSuites: func() []*tls.CipherSuite { + result := tls.CipherSuites() + return result[:2] + }(), + wantConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + }, + NextProtos: []string{"h2", "http/1.1"}, + }, + }, + { + name: "with allowed Ciphers, restricts CipherSuites to just those ciphers", + rootCAs: aCertPool, + cipherSuites: tls.CipherSuites(), + userConfiguredAllowedCipherSuiteIDs: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + wantConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, //nolint:gosec // this is a test + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + NextProtos: []string{"h2", "http/1.1"}, + RootCAs: aCertPool, + }, + }, + { + name: "with allowed ciphers in random order, returns ciphers in the order from cipherSuites", + cipherSuites: tls.CipherSuites(), + userConfiguredAllowedCipherSuiteIDs: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + wantConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, //nolint:gosec // this is for testing purposes + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + }, + NextProtos: []string{"h2", "http/1.1"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + userConfiguredAllowedCipherSuites := make([]*tls.CipherSuite, 0) + for _, allowedCipher := range test.userConfiguredAllowedCipherSuiteIDs { + for _, cipherSuite := range tls.CipherSuites() { + if allowedCipher == cipherSuite.ID { + userConfiguredAllowedCipherSuites = append(userConfiguredAllowedCipherSuites, cipherSuite) + } + } + } + + actualConfig := buildTLSConfig(test.rootCAs, test.cipherSuites, userConfiguredAllowedCipherSuites) + require.Equal(t, test.wantConfig, actualConfig) + }) + } +} + +func TestValidateAllowedCiphers(t *testing.T) { + cipherSuites := tls.CipherSuites() + + tests := []struct { + name string + cipherSuites []*tls.CipherSuite + userConfiguredAllowedCipherSuites []string + wantCipherSuites []*tls.CipherSuite + wantErr string + }{ + { + name: "empty inputs result in empty outputs", + }, + { + name: "with all valid inputs, returns the ciphers", + cipherSuites: cipherSuites, + userConfiguredAllowedCipherSuites: []string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + }, + wantCipherSuites: func() []*tls.CipherSuite { + var result []*tls.CipherSuite + for _, suite := range cipherSuites { + switch suite.Name { + case "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": + result = append(result, suite) + default: + } + } + return result + }(), + }, + { + name: "with all valid inputs, allows some legacy cipher names and returns the ciphers", + cipherSuites: cipherSuites, + userConfiguredAllowedCipherSuites: []string{ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + }, + wantCipherSuites: func() []*tls.CipherSuite { + var result []*tls.CipherSuite + for _, suite := range cipherSuites { + switch suite.Name { + case "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": + result = append(result, suite) + default: + } + } + return result + }(), + }, + { + name: "with invalid input, return an error with all known ciphers", + cipherSuites: cipherSuites[:2], + userConfiguredAllowedCipherSuites: []string{"foo"}, + wantErr: "unrecognized ciphers [foo], ciphers must be from list [TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384]", + }, + { + name: "with some valid and some invalid input, return an error with all known ciphers", + cipherSuites: cipherSuites[6:9], + userConfiguredAllowedCipherSuites: []string{ + "foo", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "bar", + }, + wantErr: "unrecognized ciphers [foo, bar], ciphers must be from list [TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384]", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actual, err := validateAllowedCiphers(test.cipherSuites, test.userConfiguredAllowedCipherSuites) + if len(test.wantErr) > 0 { + require.ErrorContains(t, err, test.wantErr) + } else { + require.NoError(t, err) + require.ElementsMatch(t, test.wantCipherSuites, actual) + } + }) + } +} + +func TestTranslateIDIntoSecureCipherSuites(t *testing.T) { + tests := []struct { + name string + inputs []uint16 + wantOutputs []uint16 + }{ + { + name: "returns ciphers found in tls.CipherSuites", + inputs: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + }, + wantOutputs: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + }, + }, + { + name: "returns ciphers in the input order", + inputs: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + }, + wantOutputs: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + }, + }, + { + name: "ignores cipher suites not returned by tls.CipherSuites", + inputs: []uint16{ + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + }, + }, + { + name: "ignores cipher suites that only support TLS1.3", + inputs: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actual := translateIDIntoSecureCipherSuites(test.inputs) + + require.Len(t, actual, len(test.wantOutputs)) + for i, suite := range actual { + require.Equal(t, test.wantOutputs[i], suite.ID) + } + }) + } +} diff --git a/internal/crypto/ptls/log_profiles.go b/internal/crypto/ptls/log_profiles.go new file mode 100644 index 000000000..1eec8f8ba --- /dev/null +++ b/internal/crypto/ptls/log_profiles.go @@ -0,0 +1,41 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ptls + +import ( + "crypto/tls" + + "go.pinniped.dev/internal/plog" +) + +func cipherSuiteNamesForCipherSuites(ciphers []uint16) []string { + names := make([]string, len(ciphers)) + for i, suite := range ciphers { + names[i] = tls.CipherSuiteName(suite) + } + return names +} + +func tlsVersionName(tlsVersion uint16) string { + if tlsVersion == 0 { + return "NONE" + } + return tls.VersionName(tlsVersion) +} + +func logProfile(name string, log plog.Logger, profile *tls.Config) { + log.Info("tls configuration", + "profile name", name, + "MinVersion", tlsVersionName(profile.MinVersion), + "MaxVersion", tlsVersionName(profile.MaxVersion), + "CipherSuites", cipherSuiteNamesForCipherSuites(profile.CipherSuites), + "NextProtos", profile.NextProtos, + ) +} + +func LogAllProfiles(log plog.Logger) { + logProfile("Default", log, Default(nil)) + logProfile("DefaultLDAP", log, DefaultLDAP(nil)) + logProfile("Secure", log, Secure(nil)) +} diff --git a/internal/crypto/ptls/log_profiles_test.go b/internal/crypto/ptls/log_profiles_test.go new file mode 100644 index 000000000..f5dac9b59 --- /dev/null +++ b/internal/crypto/ptls/log_profiles_test.go @@ -0,0 +1,30 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ptls + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/plog" +) + +func TestLogAllProfiles(t *testing.T) { + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + LogAllProfiles(logger) + + expectedLines := []string{ + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"ptls/log_profiles.go:$ptls.logProfile","message":"tls configuration","profile name":"Default","MinVersion":"TLS 1.2","MaxVersion":"NONE","CipherSuites":["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"],"NextProtos":["h2","http/1.1"]}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"ptls/log_profiles.go:$ptls.logProfile","message":"tls configuration","profile name":"DefaultLDAP","MinVersion":"TLS 1.2","MaxVersion":"NONE","CipherSuites":["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"],"NextProtos":["h2","http/1.1"]}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"ptls/log_profiles.go:$ptls.logProfile","message":"tls configuration","profile name":"Secure","MinVersion":"TLS 1.3","MaxVersion":"NONE","CipherSuites":[],"NextProtos":["h2","http/1.1"]}`, + } + expectedOutput := strings.Join(expectedLines, "\n") + "\n" + + require.Equal(t, expectedOutput, log.String()) +} diff --git a/internal/crypto/ptls/profiles.go b/internal/crypto/ptls/profiles.go index 1adb6a4b6..74e79f229 100644 --- a/internal/crypto/ptls/profiles.go +++ b/internal/crypto/ptls/profiles.go @@ -1,6 +1,7 @@ // Copyright 2021-2024 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. //go:build !fips_strict package ptls @@ -17,6 +18,62 @@ 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. + // + // 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. + // + // as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, intermediate configuration, supports: + // - Firefox 27 + // - Android 4.4.2 + // - Chrome 31 + // - Edge + // - IE 11 on Windows 7 + // - Java 8u31 + // - OpenSSL 1.0.1 + // - Opera 20 + // - Safari 9 + // https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=intermediate&guideline=5.6 + // + // The Kubernetes API server must use approved cipher suites. + // https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242418 + // + // 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 + 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, + 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 + + // 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. + // + // Adds less secure ciphers to support the default AWS Active Directory config. + // + // These are all CBC with ECDHE. Golang considers these to be secure. However, + // 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 + 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. func init() { //nolint:gochecknoinits @@ -39,73 +96,19 @@ const SecureTLSConfigMinTLSVersion = tls.VersionTLS13 // 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). +// 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 { - return &tls.Config{ - // Can't use SSLv3 because of POODLE and BEAST - // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher - // Can't use TLSv1.1 because of RC4 cipher usage - // - // The Kubernetes API Server must use TLS 1.2, at a minimum, - // to protect the confidentiality of sensitive data during electronic dissemination. - // https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242378 - MinVersion: tls.VersionTLS12, - - // 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 - // this list is ignored when TLS 1.3 is used - // - // as of 2021-10-19, Mozilla Guideline v5.6, Go 1.17.2, intermediate configuration, supports: - // - Firefox 27 - // - Android 4.4.2 - // - Chrome 31 - // - Edge - // - IE 11 on Windows 7 - // - Java 8u31 - // - OpenSSL 1.0.1 - // - Opera 20 - // - Safari 9 - // https://ssl-config.mozilla.org/#server=go&version=1.17.2&config=intermediate&guideline=5.6 - // - // The Kubernetes API server must use approved cipher suites. - // https://stigviewer.com/stig/kubernetes/2021-06-17/finding/V-242418 - CipherSuites: []uint16{ - // these are all AEADs with ECDHE, some use ChaCha20Poly1305 while others use AES-GCM - // this provides forward secrecy, confidentiality and authenticity of data - 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, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - }, - - // enable HTTP2 for go's 1.7 HTTP Server - // setting this explicitly is only required in very specific circumstances - // it is simpler to just set it here than to try and determine if we need to - NextProtos: []string{"h2", "http/1.1"}, - - // optional root CAs, nil means use the host's root CA set - RootCAs: rootCAs, - } + 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). func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config { - c := Default(rootCAs) - // add less secure ciphers to support the default AWS Active Directory config - c.CipherSuites = append(c.CipherSuites, - // CBC with ECDHE - // this provides 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 - 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, - ) - return c + ciphers := translateIDIntoSecureCipherSuites(secureCipherSuiteIDs) + ciphers = append(ciphers, translateIDIntoSecureCipherSuites(additionalSecureCipherSuiteIDsOnlyForLDAPClients)...) + return buildTLSConfig(rootCAs, ciphers, getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()) } // Secure TLS profile should be used by: diff --git a/internal/crypto/ptls/profiles_fips_strict.go b/internal/crypto/ptls/profiles_fips_strict.go index 03da3b5b7..6d5d41c53 100644 --- a/internal/crypto/ptls/profiles_fips_strict.go +++ b/internal/crypto/ptls/profiles_fips_strict.go @@ -1,7 +1,7 @@ // Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// The configurations here override the usual configs when Pinniped is built in fips-only mode. +// This file overrides profiles.go when Pinniped is built in FIPS-only mode. //go:build fips_strict package ptls @@ -20,6 +20,37 @@ import ( "go.pinniped.dev/internal/plog" ) +// The union of these three variables 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{ + 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. + // + // FIPS allows the use of these specific ciphers that golang considers insecure. + insecureCipherSuiteIDs = []uint16{ + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + } + + // 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 +) + // init: see comment in profiles.go. func init() { switch filepath.Base(os.Args[0]) { @@ -40,37 +71,18 @@ const SecureTLSConfigMinTLSVersion = tls.VersionTLS12 // Default: see comment in profiles.go. // This chooses different cipher suites and/or TLS versions compared to non-FIPS mode. +// In FIPS mode, this will use the union of the secureCipherSuiteIDs, additionalSecureCipherSuiteIDsOnlyForLDAPClients, +// and insecureCipherSuiteIDs values defined above. func Default(rootCAs *x509.CertPool) *tls.Config { - return &tls.Config{ - MinVersion: tls.VersionTLS12, - // Until goboring supports TLS 1.3, make the max version 1.2. - MaxVersion: tls.VersionTLS12, - - // This is all the fips-approved TLS 1.2 ciphers. - // The list is hard-coded for convenience of testing. - // 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. - 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, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - }, - - // enable HTTP2 for go's 1.7 HTTP Server - // setting this explicitly is only required in very specific circumstances - // it is simpler to just set it here than to try and determine if we need to - NextProtos: []string{"h2", "http/1.1"}, - - // optional root CAs, nil means use the host's root CA set - RootCAs: rootCAs, - } + config := buildTLSConfig(rootCAs, allHardcodedAllowedCipherSuites(), getUserConfiguredAllowedCipherSuitesForTLSOneDotTwo()) + // Until goboring supports TLS 1.3, make the max version 1.2. + config.MaxVersion = tls.VersionTLS12 + return config } // DefaultLDAP: see comment in profiles.go. // 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. func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config { return Default(rootCAs) } @@ -78,6 +90,7 @@ func DefaultLDAP(rootCAs *x509.CertPool) *tls.Config { // Secure: see comment in profiles.go. // This chooses different cipher suites and/or TLS versions compared to non-FIPS mode. // Until goboring supports TLS 1.3, make the Secure profile the same as the Default profile in FIPS mode. +// Until then, this is not any different from the Default profile in FIPS mode. func Secure(rootCAs *x509.CertPool) *tls.Config { return Default(rootCAs) } diff --git a/internal/crypto/ptls/profiles_test.go b/internal/crypto/ptls/profiles_test.go index ac175c06b..95cd6e2dd 100644 --- a/internal/crypto/ptls/profiles_test.go +++ b/internal/crypto/ptls/profiles_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/server/options" ) @@ -17,7 +18,6 @@ func TestDefault(t *testing.T) { aCertPool := x509.NewCertPool() - actual := Default(aCertPool) expected := &tls.Config{ MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ @@ -32,7 +32,7 @@ func TestDefault(t *testing.T) { RootCAs: aCertPool, } - require.Equal(t, expected, actual) + require.Equal(t, expected, Default(aCertPool)) } func TestDefaultLDAP(t *testing.T) { @@ -40,7 +40,6 @@ func TestDefaultLDAP(t *testing.T) { aCertPool := x509.NewCertPool() - actual := DefaultLDAP(aCertPool) expected := &tls.Config{ MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ @@ -59,7 +58,7 @@ func TestDefaultLDAP(t *testing.T) { RootCAs: aCertPool, } - require.Equal(t, expected, actual) + require.Equal(t, expected, DefaultLDAP(aCertPool)) } func TestSecure(t *testing.T) { @@ -67,7 +66,6 @@ func TestSecure(t *testing.T) { aCertPool := x509.NewCertPool() - actual := Secure(aCertPool) expected := &tls.Config{ MinVersion: tls.VersionTLS13, CipherSuites: nil, // TLS 1.3 ciphers are not configurable @@ -75,17 +73,76 @@ func TestSecure(t *testing.T) { RootCAs: aCertPool, } - require.Equal(t, expected, actual) + require.Equal(t, expected, Secure(aCertPool)) } func TestSecureServing(t *testing.T) { t.Parallel() opts := &options.SecureServingOptionsWithLoopback{SecureServingOptions: &options.SecureServingOptions{}} - SecureServing(opts) - require.Equal(t, options.SecureServingOptionsWithLoopback{ + + expected := options.SecureServingOptionsWithLoopback{ SecureServingOptions: &options.SecureServingOptions{ MinTLSVersion: "VersionTLS13", }, - }, *opts) + } + + SecureServing(opts) + require.Equal(t, expected, *opts) +} + +func TestCipherSuitesForDefault(t *testing.T) { + t.Run("contains exactly and only the expected values", func(t *testing.T) { + expected := []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, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + } + + require.Equal(t, expected, Default(nil).CipherSuites) + }) + + t.Run("is a subset of TestCipherSuitesForDefaultLDAP", func(t *testing.T) { + defaultSuiteIDs := Default(nil).CipherSuites + ldapSuiteIDs := DefaultLDAP(nil).CipherSuites + + require.Greater(t, len(defaultSuiteIDs), 0) + require.GreaterOrEqual(t, len(ldapSuiteIDs), len(defaultSuiteIDs)) + + require.Equal(t, 0, sets.New[uint16](defaultSuiteIDs...).Difference(sets.New[uint16](ldapSuiteIDs...)).Len()) + }) +} + +func TestCipherSuitesForDefaultLDAP(t *testing.T) { + t.Run("contains exactly and only the expected values", func(t *testing.T) { + expected := []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, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + + // Add these for LDAP + 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, + } + + require.Equal(t, expected, DefaultLDAP(nil).CipherSuites) + }) + + t.Run("is a superset of TestCipherSuitesForDefault", func(t *testing.T) { + defaultSuiteIDs := Default(nil).CipherSuites + ldapSuiteIDs := DefaultLDAP(nil).CipherSuites + + require.Greater(t, len(defaultSuiteIDs), 0) + require.GreaterOrEqual(t, len(ldapSuiteIDs), len(defaultSuiteIDs)) + + require.True(t, sets.New[uint16](ldapSuiteIDs...).IsSuperset(sets.New[uint16](defaultSuiteIDs...))) + }) } diff --git a/internal/crypto/ptls/ptls.go b/internal/crypto/ptls/ptls.go index 6ebd86e02..37f12785a 100644 --- a/internal/crypto/ptls/ptls.go +++ b/internal/crypto/ptls/ptls.go @@ -19,8 +19,6 @@ import ( "k8s.io/client-go/transport" ) -// TODO decide if we need to expose the four TLS levels (secure, default, default-ldap, legacy) as config. - // defaultServingOptionsMinTLSVersion is the minimum tls version in the format // expected by SecureServingOptions.MinTLSVersion from // k8s.io/apiserver/pkg/server/options. @@ -28,21 +26,6 @@ const defaultServingOptionsMinTLSVersion = "VersionTLS12" type ConfigFunc func(*x509.CertPool) *tls.Config -func Legacy(rootCAs *x509.CertPool) *tls.Config { - c := Default(rootCAs) - // add all the ciphers (even the crappy ones) except the ones that Go considers to be outright broken like 3DES - c.CipherSuites = suitesToIDs(tls.CipherSuites()) - return c -} - -func suitesToIDs(suites []*tls.CipherSuite) []uint16 { - out := make([]uint16, 0, len(suites)) - for _, suite := range suites { - out = append(out, suite.ID) - } - return out -} - func Merge(tlsConfigFunc ConfigFunc, tlsConfig *tls.Config) { secureTLSConfig := tlsConfigFunc(nil) diff --git a/internal/crypto/ptls/ptls_test.go b/internal/crypto/ptls/ptls_test.go index e3eff9935..f38ca3b99 100644 --- a/internal/crypto/ptls/ptls_test.go +++ b/internal/crypto/ptls/ptls_test.go @@ -5,11 +5,8 @@ package ptls import ( "crypto/tls" - "runtime" - "strings" "testing" - "github.com/coreos/go-semver/semver" "github.com/stretchr/testify/require" "k8s.io/apiserver/pkg/server/options" ) @@ -37,13 +34,6 @@ func TestDefaultServing(t *testing.T) { func TestMerge(t *testing.T) { t.Parallel() - runtimeVersion := runtime.Version() - if strings.HasPrefix(runtimeVersion, "go") { - runtimeVersion, _ = strings.CutPrefix(runtimeVersion, "go") - } - runtimeVersionSemver, err := semver.NewVersion(runtimeVersion) - require.NoError(t, err) - tests := []struct { name string tlsConfigFunc ConfigFunc @@ -167,33 +157,6 @@ func TestMerge(t *testing.T) { NextProtos: []string{"panda"}, }, }, - { - name: "legacy without NextProtos", - tlsConfigFunc: Legacy, - tlsConfig: &tls.Config{ - ServerName: "something-to-check-passthrough", - }, - want: &tls.Config{ - ServerName: "something-to-check-passthrough", - MinVersion: tls.VersionTLS12, - CipherSuites: wantLegacyCipherSuites(runtimeVersionSemver), - NextProtos: []string{"h2", "http/1.1"}, - }, - }, - { - name: "legacy with NextProtos", - tlsConfigFunc: Legacy, - tlsConfig: &tls.Config{ //nolint:gosec // not concerned with TLS MinVersion here - ServerName: "a different thing for passthrough", - NextProtos: []string{"panda"}, - }, - want: &tls.Config{ - ServerName: "a different thing for passthrough", - MinVersion: tls.VersionTLS12, - CipherSuites: wantLegacyCipherSuites(runtimeVersionSemver), - NextProtos: []string{"panda"}, - }, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -204,31 +167,3 @@ func TestMerge(t *testing.T) { }) } } - -func wantLegacyCipherSuites(runtime *semver.Version) []uint16 { - var ciphers []uint16 - if runtime.Major == 1 && runtime.Minor < 22 { - ciphers = append(ciphers, []uint16{ - tls.TLS_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - }...) - } - ciphers = append(ciphers, []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_AES_256_GCM_SHA384, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - }...) - return ciphers -} diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index b83922faa..e3d796127 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -742,11 +742,14 @@ func main() error { // return an error instead of plog.Fatal to allow defer stat ctx := signalCtx() // Read the server config file. - cfg, err := supervisor.FromPath(ctx, os.Args[2]) + cfg, err := supervisor.FromPath(ctx, os.Args[2], ptls.SetUserConfiguredAllowedCipherSuitesForTLSOneDotTwo) if err != nil { return fmt.Errorf("could not load config: %w", err) } + // The above server config should have set the allowed ciphers global, so now log the ciphers for all profiles. + ptls.LogAllProfiles(plog.New()) + return runSupervisor(ctx, podInfo, cfg) } diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index 0fc6b718d..968f322ba 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -1523,7 +1523,7 @@ 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)), "stdout:\n%s", stdout) + require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Default(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) }) }) diff --git a/test/integration/limited_ciphers_fips_test.go b/test/integration/limited_ciphers_fips_test.go new file mode 100644 index 000000000..7eeb6993b --- /dev/null +++ b/test/integration/limited_ciphers_fips_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build fips_strict + +package integration + +import ( + "crypto/tls" + "testing" +) + +// TestLimitedCiphersFIPS_Disruptive will confirm that the Pinniped Supervisor and Concierge expose only those +// ciphers listed in configuration, when compiled in FIPS mode. +// This does not test the CLI, since it does not have a feature to limit cipher suites. +func TestLimitedCiphersFIPS_Disruptive(t *testing.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", + "TLS_RSA_WITH_AES_256_GCM_SHA384", // this is an insecure cipher but allowed for FIPS + }, + // 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.VersionTLS12, // boringcrypto does not use TLS 1.3 yet + 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, // boringcrypto does not use TLS 1.3 yet + MaxVersion: tls.VersionTLS12, // boringcrypto does not use TLS 1.3 yet + 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, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + }, + }, + ) +} diff --git a/test/integration/limited_ciphers_test.go b/test/integration/limited_ciphers_test.go new file mode 100644 index 000000000..ebfa44062 --- /dev/null +++ b/test/integration/limited_ciphers_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//go:build !fips_strict + +package integration + +import ( + "crypto/tls" + "testing" +) + +// 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) { + performLimitedCiphersTest(t, + // The user-configured ciphers for both the Supervisor and Concierge. + // This is a subset of the hardcoded ciphers from profiles.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", + "TLS_ECDHE_RSA_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.VersionTLS13, // do not allow TLS 1.2 clients to connect + MaxVersion: tls.VersionTLS13, + CipherSuites: nil, // TLS 1.3 ciphers are not configurable + }, + ) +} diff --git a/test/integration/limited_ciphers_utils_test.go b/test/integration/limited_ciphers_utils_test.go new file mode 100644 index 000000000..51f52181d --- /dev/null +++ b/test/integration/limited_ciphers_utils_test.go @@ -0,0 +1,211 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "context" + "crypto/tls" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "go.pinniped.dev/internal/config/concierge" + "go.pinniped.dev/internal/config/supervisor" + "go.pinniped.dev/test/testlib" +) + +type stringEditorFunc func(t *testing.T, in string) string + +func performLimitedCiphersTest( + t *testing.T, + allowedCiphersConfig []string, + expectedConfigForSupervisorOIDCEndpoints *tls.Config, + expectedConfigForAggregatedAPIEndpoints *tls.Config, +) { + env := testEnvForPodShutdownTests(t) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + editSupervisorAllowedCiphersConfig := func(t *testing.T, configMapData string) string { + t.Helper() + + var config supervisor.Config + err := yaml.Unmarshal([]byte(configMapData), &config) + require.NoError(t, err) + + require.Empty(t, config.TLS.OneDotTwo.AllowedCiphers) // precondition + + config.TLS.OneDotTwo.AllowedCiphers = allowedCiphersConfig + + updatedConfig, err := yaml.Marshal(config) + require.NoError(t, err) + return string(updatedConfig) + } + + editConciergeAllowedCiphersConfig := func(t *testing.T, configMapData string) string { + t.Helper() + + var config concierge.Config + err := yaml.Unmarshal([]byte(configMapData), &config) + require.NoError(t, err) + + require.Empty(t, config.TLS.OneDotTwo.AllowedCiphers) // precondition + + config.TLS.OneDotTwo.AllowedCiphers = allowedCiphersConfig + + updatedConfig, err := yaml.Marshal(config) + require.NoError(t, err) + return string(updatedConfig) + } + + // Update Supervisor's allowed ciphers in its static configmap and restart pods. + updateStaticConfigMapAndRestartApp(t, + ctx, + env.SupervisorNamespace, + env.SupervisorAppName+"-static-config", + env.SupervisorAppName, + false, + editSupervisorAllowedCiphersConfig, + ) + + // Update Concierge's allowed ciphers in its static configmap and restart pods. + updateStaticConfigMapAndRestartApp(t, + ctx, + env.ConciergeNamespace, + env.ConciergeAppName+"-config", + env.ConciergeAppName, + true, + editConciergeAllowedCiphersConfig, + ) + + // Probe TLS config of Supervisor's OIDC endpoints. + expectTLSConfigForServicePort(t, ctx, + env.SupervisorAppName+"-nodeport", env.SupervisorNamespace, "10509", + expectedConfigForSupervisorOIDCEndpoints, + ) + + // Probe TLS config of Supervisor's aggregated endpoints. + expectTLSConfigForServicePort(t, ctx, + env.SupervisorAppName+"-api", env.SupervisorNamespace, "10510", + expectedConfigForAggregatedAPIEndpoints, + ) + + // Probe TLS config of Concierge's aggregated endpoints. + expectTLSConfigForServicePort(t, ctx, + env.ConciergeAppName+"-api", env.ConciergeNamespace, "10511", + expectedConfigForAggregatedAPIEndpoints, + ) +} + +func expectTLSConfigForServicePort( + t *testing.T, + ctx context.Context, + serviceName string, + serviceNamespace string, + localPortAsStr string, + expectedConfig *tls.Config, +) { + portAsInt, err := strconv.Atoi(localPortAsStr) + require.NoError(t, err) + portAsUint := uint16(portAsInt) // okay to cast because it will only be legal port numbers + + startKubectlPortForward(ctx, t, localPortAsStr, "443", serviceName, serviceNamespace) + + stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", portAsUint) + require.Empty(t, stderr) + + expectedNMapOutput := testlib.GetExpectedCiphers(expectedConfig, "server") + assert.Contains(t, + stdout, + expectedNMapOutput, + "actual nmap output:\n%s", stdout, + "but was expected to contain:\n%s", expectedNMapOutput, + ) +} + +func updateStaticConfigMapAndRestartApp( + t *testing.T, + ctx context.Context, + namespace string, + staticConfigMapName string, + appName string, + isConcierge bool, + editConfigMapFunc stringEditorFunc, +) { + configMapClient := testlib.NewKubernetesClientset(t).CoreV1().ConfigMaps(namespace) + + staticConfigMap, err := configMapClient.Get(ctx, staticConfigMapName, metav1.GetOptions{}) + require.NoError(t, err) + + originalConfig := staticConfigMap.Data["pinniped.yaml"] + require.NotEmpty(t, originalConfig) + + t.Cleanup(func() { + cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + staticConfigMapForCleanup, err := configMapClient.Get(cleanupCtx, staticConfigMapName, metav1.GetOptions{}) + require.NoError(t, err) + + staticConfigMapForCleanup.Data = make(map[string]string) + staticConfigMapForCleanup.Data["pinniped.yaml"] = originalConfig + + _, err = configMapClient.Update(cleanupCtx, staticConfigMapForCleanup, metav1.UpdateOptions{}) + require.NoError(t, err) + + restartAllPodsOfApp(t, namespace, appName, isConcierge) + }) + + staticConfigMap.Data = make(map[string]string) + staticConfigMap.Data["pinniped.yaml"] = editConfigMapFunc(t, originalConfig) + + _, err = configMapClient.Update(ctx, staticConfigMap, metav1.UpdateOptions{}) + require.NoError(t, err) + + restartAllPodsOfApp(t, namespace, appName, isConcierge) +} + +// restartAllPodsOfApp will immediately scale to 0 and then scale back. +// There are no uses of t.Cleanup since these actions need to happen immediately. +func restartAllPodsOfApp( + t *testing.T, + namespace string, + appName string, + isConcierge bool, +) { + t.Helper() + + ignorePodsWithNameSubstring := "" + if isConcierge { + ignorePodsWithNameSubstring = "-kube-cert-agent-" + } + + // Precondition: the app should have some pods running initially. + initialPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring) + require.Greater(t, len(initialPods), 0) + + // Scale down the deployment's number of replicas to 0, which will shut down all the pods. + originalScale := updateDeploymentScale(t, namespace, appName, 0) + require.Greater(t, originalScale, 0) + + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + newPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring) + requireEventually.Len(newPods, 0, "wanted zero pods") + }, 2*time.Minute, 200*time.Millisecond) + + // Reset the application to its original scale. + updateDeploymentScale(t, namespace, appName, originalScale) + + testlib.RequireEventually(t, func(requireEventually *require.Assertions) { + newPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring) + requireEventually.Len(newPods, originalScale, "wanted %d pods", originalScale) + requireEventually.True(allPodsReady(newPods), "wanted all new pods to be ready") + }, 2*time.Minute, 200*time.Millisecond) +} diff --git a/test/integration/pod_shutdown_test.go b/test/integration/pod_shutdown_test.go index 77ad4d1e9..a366d6dd1 100644 --- a/test/integration/pod_shutdown_test.go +++ b/test/integration/pod_shutdown_test.go @@ -24,18 +24,22 @@ import ( // before they die. // Never run this test in parallel since deleting the pods is disruptive, see main_test.go. func TestPodShutdown_Disruptive(t *testing.T) { - // Only run this test in CI on Kind clusters, because something about restarting the pods - // in this test breaks the "kubectl port-forward" commands that we are using in CI for - // AKS, EKS, and GKE clusters. The Go code that we wrote for graceful pod shutdown should - // not be sensitive to which distribution it runs on, so running this test only on Kind - // should give us sufficient coverage for what we are trying to test here. - env := testlib.IntegrationEnv(t, testlib.SkipPodRestartAssertions()). - WithKubeDistribution(testlib.KindDistro) + env := testEnvForPodShutdownTests(t) shutdownAllPodsOfApp(t, env, env.ConciergeNamespace, env.ConciergeAppName, true) shutdownAllPodsOfApp(t, env, env.SupervisorNamespace, env.SupervisorAppName, false) } +// testEnvForPodShutdownTests builds an env with the following description: +// Only run this test in CI on Kind clusters, because something about restarting the pods +// in this test breaks the "kubectl port-forward" commands that we are using in CI for +// AKS, EKS, and GKE clusters. The Go code that we wrote for graceful pod shutdown should +// not be sensitive to which distribution it runs on, so running this test only on Kind +// should give us sufficient coverage for what we are trying to test here. +func testEnvForPodShutdownTests(t *testing.T) *testlib.TestEnv { + return testlib.IntegrationEnv(t, testlib.SkipPodRestartAssertions()).WithKubeDistribution(testlib.KindDistro) +} + func shutdownAllPodsOfApp( t *testing.T, env *testlib.TestEnv, @@ -89,11 +93,12 @@ func shutdownAllPodsOfApp( t.Cleanup(func() { updateDeploymentScale(t, namespace, appName, originalScale) - // Wait for all the new pods to be running. + // Wait for all the new pods to be running and ready. var newPods []corev1.Pod testlib.RequireEventually(t, func(requireEventually *require.Assertions) { newPods = getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring) requireEventually.Len(newPods, originalScale, "wanted pods to return to original scale") + requireEventually.True(allPodsReady(newPods), "wanted all new pods to be ready") }, 2*time.Minute, 200*time.Millisecond) // After a short time, leader election should have finished and the lease should contain the name of @@ -181,6 +186,24 @@ func getRunningPodsByNamePrefix( return foundPods } +func allPodsReady(pods []corev1.Pod) bool { + for _, pod := range pods { + if !isPodReady(pod) { + return false + } + } + return true +} + +func isPodReady(pod corev1.Pod) bool { + for _, cond := range pod.Status.Conditions { + if cond.Type == corev1.PodReady { + return cond.Status == corev1.ConditionTrue + } + } + return false +} + func updateDeploymentScale(t *testing.T, namespace string, deploymentName string, newScale int) int { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) diff --git a/test/integration/securetls_test.go b/test/integration/securetls_test.go index a6bc9c499..9c86385ba 100644 --- a/test/integration/securetls_test.go +++ b/test/integration/securetls_test.go @@ -94,7 +94,7 @@ 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)), "stdout:\n%s", stdout) + require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) } // TLS checks safe to run in parallel with serial tests, see main_test.go. @@ -109,7 +109,7 @@ 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)), "stdout:\n%s", stdout) + require.Contains(t, stdout, testlib.GetExpectedCiphers(ptls.Secure(nil), testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) } func TestSecureTLSSupervisor(t *testing.T) { @@ -136,7 +136,7 @@ func TestSecureTLSSupervisor(t *testing.T) { defaultECDSAOnly.CipherSuites = ciphers require.Empty(t, stderr) - require.Contains(t, stdout, testlib.GetExpectedCiphers(defaultECDSAOnly), "stdout:\n%s", stdout) + require.Contains(t, stdout, testlib.GetExpectedCiphers(defaultECDSAOnly, testlib.DefaultCipherSuitePreference), "stdout:\n%s", stdout) } type fakeT struct { diff --git a/test/testlib/securetls.go b/test/testlib/securetls.go index 3111c39fc..153caaa8e 100644 --- a/test/testlib/securetls.go +++ b/test/testlib/securetls.go @@ -38,19 +38,22 @@ func RunNmapSSLEnum(t *testing.T, host string, port uint16) (string, string) { var stdout, stderr bytes.Buffer //nolint:gosec // we are not performing malicious argument injection against ourselves - cmd := exec.CommandContext(ctx, "nmap", "--script", "ssl-enum-ciphers", + cmd := exec.CommandContext(ctx, + "nmap", + "-Pn", + "--script", "+ssl-enum-ciphers", "-p", strconv.FormatUint(uint64(port), 10), host, ) cmd.Stdout = &stdout cmd.Stderr = &stderr - + t.Log("Running cmd: " + strings.Join(cmd.Args, " ")) require.NoErrorf(t, cmd.Run(), "stderr:\n%s\n\nstdout:\n%s\n\n", stderr.String(), stdout.String()) return stdout.String(), stderr.String() } -func GetExpectedCiphers(config *tls.Config) string { +func GetExpectedCiphers(config *tls.Config, preference string) string { skip12 := config.MinVersion == tls.VersionTLS13 skip13 := config.MaxVersion == tls.VersionTLS12 @@ -86,7 +89,7 @@ func GetExpectedCiphers(config *tls.Config) string { } s.WriteString("\n") } - tls12Bit = fmt.Sprintf(tls12Base, s.String(), cipherSuitePreference) + tls12Bit = fmt.Sprintf(tls12Base, s.String(), preference) } if !skip13 { diff --git a/test/testlib/securetls_preference_fips.go b/test/testlib/securetls_preference_fips.go index 3d76932e6..85c4de7e2 100644 --- a/test/testlib/securetls_preference_fips.go +++ b/test/testlib/securetls_preference_fips.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //go:build fips_strict @@ -9,4 +9,4 @@ package testlib // 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 cipherSuitePreference = "server" +const DefaultCipherSuitePreference = "server" diff --git a/test/testlib/securetls_preference_nonfips.go b/test/testlib/securetls_preference_nonfips.go index 3306a9c1b..e114aa1f0 100644 --- a/test/testlib/securetls_preference_nonfips.go +++ b/test/testlib/securetls_preference_nonfips.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //go:build !fips_strict @@ -9,4 +9,4 @@ package testlib // 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 cipherSuitePreference = "client" +const DefaultCipherSuitePreference = "client"