Files
pinniped/internal/crypto/ptls/common.go

227 lines
8.7 KiB
Go

// 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
}