diff --git a/.changelog/11645.txt b/.changelog/11645.txt new file mode 100644 index 0000000000..8457953274 --- /dev/null +++ b/.changelog/11645.txt @@ -0,0 +1,3 @@ +```release-note:improvement +types: add TLSVersion and TLSCipherSuite +``` diff --git a/types/tls.go b/types/tls.go new file mode 100644 index 0000000000..66c10b19b1 --- /dev/null +++ b/types/tls.go @@ -0,0 +1,185 @@ +package types + +import ( + "encoding/json" + "fmt" +) + +// TLSVersion is a strongly-typed int used for relative comparison +// (minimum, maximum, greater than, less than) of TLS versions +type TLSVersion int + +const ( + // Error value, excluded from lookup maps + TLSVersionInvalid TLSVersion = iota - 1 + + // Explicit unspecified zero-value to avoid overwriting parent defaults + TLSVersionUnspecified + + // Explictly allow implementation to select TLS version + // May be useful to supercede defaults specified at a higher layer + TLSVersionAuto + + _ // Placeholder for SSLv3, hopefully we won't have to add this + + // TLS versions + TLSv1_0 + TLSv1_1 + TLSv1_2 + TLSv1_3 +) + +var ( + TLSVersions = map[string]TLSVersion{ + "TLS_AUTO": TLSVersionAuto, + "TLSv1_0": TLSv1_0, + "TLSv1_1": TLSv1_1, + "TLSv1_2": TLSv1_2, + "TLSv1_3": TLSv1_3, + } + // NOTE: This interface is deprecated in favor of TLSVersions + // and should be eventually removed in a future release. + DeprecatedConsulAgentTLSVersions = map[string]TLSVersion{ + "": TLSVersionAuto, + "tls10": TLSv1_0, + "tls11": TLSv1_1, + "tls12": TLSv1_2, + "tls13": TLSv1_3, + } + HumanTLSVersionStrings = map[TLSVersion]string{ + TLSVersionAuto: "Allow implementation to select TLS version", + TLSv1_0: "TLS 1.0", + TLSv1_1: "TLS 1.1", + TLSv1_2: "TLS 1.2", + TLSv1_3: "TLS 1.3", + } + ConsulConfigTLSVersionStrings = func() map[TLSVersion]string { + inverted := make(map[TLSVersion]string, len(TLSVersions)) + for k, v := range TLSVersions { + inverted[v] = k + } + return inverted + }() + // NOTE: these currently map to the deprecated config strings to support the + // deployment pattern of upgrading servers first. This map should eventually + // be removed and any lookups updated to use ConsulConfigTLSVersionStrings + // with newer config strings instead in a future release. + ConsulAutoConfigTLSVersionStrings = map[TLSVersion]string{ + TLSVersionAuto: "", + TLSv1_0: "tls10", + TLSv1_1: "tls11", + TLSv1_2: "tls12", + TLSv1_3: "tls13", + } +) + +func (v TLSVersion) String() string { + return ConsulConfigTLSVersionStrings[v] +} + +func (v TLSVersion) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +func (v *TLSVersion) UnmarshalJSON(bytes []byte) error { + versionStr := string(bytes) + + if n := len(versionStr); n > 1 && versionStr[0] == '"' && versionStr[n-1] == '"' { + versionStr = versionStr[1 : n-1] // trim surrounding quotes + } + + if version, ok := TLSVersions[versionStr]; ok { + *v = version + return nil + } + + *v = TLSVersionInvalid + return fmt.Errorf("no matching TLS Version found for %s", versionStr) +} + +// IANA cipher suite constants and values as defined at +// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml +// This is the total list of TLS 1.2-style cipher suites +// which are currently supported by either Envoy 1.21 or the Consul agent +// via Go, and may change as some older suites are removed in future +// Envoy releases and Consul drops support for older Envoy versions, +// and as supported cipher suites in the Go runtime change. +// +// The naming convention for cipher suites changed in TLS 1.3 +// but constant values should still be globally unqiue +// Handling validation on a subset of TLSCipherSuite constants +// would be a future exercise if cipher suites for TLS 1.3 ever +// become configurable in BoringSSL, Envoy, or other implementation +type TLSCipherSuite uint16 + +const ( + // Envoy cipher suites also used by Consul agent + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLSCipherSuite = 0xc02b + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xcca9 // Not used by Consul agent yet + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xc02f + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xcca8 // Not used by Consul agent yet + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xc009 + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xc013 + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xc02c + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xc030 + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xc00a + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 0xc014 + + // Older cipher suites not supported for Consul agent TLS, will eventually be removed from Envoy defaults + TLS_RSA_WITH_AES_128_GCM_SHA256 = 0x009c + TLS_RSA_WITH_AES_128_CBC_SHA = 0x002f + TLS_RSA_WITH_AES_256_GCM_SHA384 = 0x009d + TLS_RSA_WITH_AES_256_CBC_SHA = 0x0035 + + // Additional cipher suites used by Consul agent but not Envoy + // TODO: these are both explicitly listed as insecure and disabled in the Go source, should they be removed? + // https://cs.opensource.google/go/go/+/refs/tags/go1.17.3:src/crypto/tls/cipher_suites.go;l=329-330 + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0x0023 + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xc027 +) + +var ( + TLSCipherSuites = map[string]TLSCipherSuite{ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + + "TLS_RSA_WITH_AES_128_GCM_SHA256": TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_128_CBC_SHA": TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_GCM_SHA384": TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_RSA_WITH_AES_256_CBC_SHA": TLS_RSA_WITH_AES_256_CBC_SHA, + + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + } + HumanTLSCipherSuiteStrings = func() map[TLSCipherSuite]string { + inverted := make(map[TLSCipherSuite]string, len(TLSCipherSuites)) + for k, v := range TLSCipherSuites { + inverted[v] = k + } + return inverted + }() + EnvoyTLSCipherSuiteStrings = map[TLSCipherSuite]string{ + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "ECDHE-ECDSA-AES128-GCM-SHA256", + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: "ECDHE-ECDSA-CHACHA20-POLY1305", + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "ECDHE-RSA-AES128-GCM-SHA256", + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: "ECDHE-RSA-CHACHA20-POLY1305", + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "ECDHE-ECDSA-AES128-SHA", + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "ECDHE-RSA-AES128-SHA", + TLS_RSA_WITH_AES_128_GCM_SHA256: "AES128-GCM-SHA256", + TLS_RSA_WITH_AES_128_CBC_SHA: "AES128-SHA", + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "ECDHE-ECDSA-AES256-GCM-SHA384", + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "ECDHE-RSA-AES256-GCM-SHA384", + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "ECDHE-ECDSA-AES256-SHA", + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "ECDHE-RSA-AES256-SHA", + TLS_RSA_WITH_AES_256_GCM_SHA384: "AES256-GCM-SHA384", + TLS_RSA_WITH_AES_256_CBC_SHA: "AES256-SHA", + } +) diff --git a/types/tls_test.go b/types/tls_test.go new file mode 100644 index 0000000000..0cf94e42f3 --- /dev/null +++ b/types/tls_test.go @@ -0,0 +1,49 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTLSVersion_PartialEq(t *testing.T) { + require.Greater(t, TLSv1_3, TLSv1_2) + require.Greater(t, TLSv1_2, TLSv1_1) + require.Greater(t, TLSv1_1, TLSv1_0) + + require.Less(t, TLSv1_2, TLSv1_3) + require.Less(t, TLSv1_1, TLSv1_2) + require.Less(t, TLSv1_0, TLSv1_1) +} + +func TestTLSVersion_Invalid(t *testing.T) { + var zeroValue TLSVersion + require.NotEqual(t, TLSVersionInvalid, zeroValue) + require.NotEqual(t, TLSVersionInvalid, TLSVersionUnspecified) + require.NotEqual(t, TLSVersionInvalid, TLSVersionAuto) +} + +func TestTLSVersion_Zero(t *testing.T) { + var zeroValue TLSVersion + require.Equal(t, TLSVersionUnspecified, zeroValue) + require.NotEqual(t, TLSVersionUnspecified, TLSVersionInvalid) + require.NotEqual(t, TLSVersionUnspecified, TLSVersionAuto) +} + +func TestTLSVersion_ToJSON(t *testing.T) { + var tlsVersion TLSVersion + err := tlsVersion.UnmarshalJSON([]byte(`"foo"`)) + require.Error(t, err) + require.Equal(t, tlsVersion, TLSVersionInvalid) + + for str, version := range TLSVersions { + versionJSON, err := json.Marshal(version) + require.NoError(t, err) + require.Equal(t, versionJSON, []byte(`"`+str+`"`)) + + err = tlsVersion.UnmarshalJSON([]byte(`"` + str + `"`)) + require.NoError(t, err) + require.Equal(t, tlsVersion, version) + } +}