diff --git a/Makefile b/Makefile index b95c7d0eb8..6157d83655 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,11 @@ verify: verify-podnetworkconnectivitychecks verify-podnetworkconnectivitychecks: $(MAKE) -C pkg/operator/connectivitycheckcontroller verify +.PHONY: update-mozilla-guidelines +update-mozilla-guidelines: + curl -sS -o pkg/crypto/testfiles/mozilla-guidelines.json https://ssl-config.mozilla.org/guidelines/latest.json + @echo "Mozilla TLS configuration guidelines updated to latest version" + test-e2e-encryption: GO_TEST_PACKAGES :=./test/e2e-encryption/... .PHONY: test-e2e-encryption diff --git a/pkg/crypto/crypto_test.go b/pkg/crypto/crypto_test.go index f0158efa5f..357d85c0ad 100644 --- a/pkg/crypto/crypto_test.go +++ b/pkg/crypto/crypto_test.go @@ -2,8 +2,11 @@ package crypto import ( "crypto" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" + _ "embed" + "encoding/json" "fmt" "go/importer" "os" @@ -549,3 +552,130 @@ func TestServerCertRegeneration(t *testing.T) { require.NotNil(t, serverCert) require.True(t, created) } + +//go:embed testfiles/mozilla-guidelines.json +var guidelinesJSON []byte + +// TestDefaultCiphersAgainstMozillaGuidelines compares our DefaultCiphers() with the Mozilla TLS +// configuration guidelines stored in testfiles/mozilla-guidelines.json. +func TestDefaultCiphersAgainstMozillaGuidelines(t *testing.T) { + + // Parse the JSON + var guidelines struct { + Version float64 `json:"version"` + Configurations map[string]struct { + TLSVersions []string `json:"tls_versions"` + Ciphers map[string][]string `json:"ciphers"` + Ciphersuites []string `json:"ciphersuites"` // TLS 1.3 ciphers + } `json:"configurations"` + } + if err := json.Unmarshal(guidelinesJSON, &guidelines); err != nil { + t.Fatalf("Failed to parse Mozilla guidelines JSON: %v", err) + } + + // Get Intermediate profile + intermediate, ok := guidelines.Configurations["intermediate"] + if !ok { + t.Fatalf("Mozilla guidelines missing 'intermediate' configuration") + } + + // Get Go cipher names from Mozilla (they use IANA names) + mozillaCipherNames := sets.New[string]() + mozillaCipherIDs := sets.New[uint16]() + unknownCiphers := []string{} + + // Process TLS 1.2 ciphers (from "go" field) + goCiphers := intermediate.Ciphers["go"] + for _, cipherName := range goCiphers { + mozillaCipherNames.Insert(cipherName) + + // Convert IANA name to cipher suite ID + cipherID, ok := ciphers[cipherName] + if ok { + mozillaCipherIDs.Insert(cipherID) + } else { + unknownCiphers = append(unknownCiphers, cipherName) + } + } + + // Process TLS 1.3 ciphersuites + for _, cipherName := range intermediate.Ciphersuites { + mozillaCipherNames.Insert(cipherName) + + // Convert IANA name to cipher suite ID + cipherID, ok := ciphers[cipherName] + if ok { + mozillaCipherIDs.Insert(cipherID) + } else { + unknownCiphers = append(unknownCiphers, cipherName) + } + } + + // Get our default ciphers + ourCiphers := DefaultCiphers() + ourCipherIDs := sets.New[uint16](ourCiphers...) + ourCipherNames := sets.New[string]() + for _, id := range ourCiphers { + ourCipherNames.Insert(CipherSuiteToNameOrDie(id)) + } + + // Find discrepancies + deprecated := ourCipherIDs.Difference(mozillaCipherIDs) + missing := mozillaCipherIDs.Difference(ourCipherIDs) + + // Report findings + t.Logf("Mozilla SSL Configuration Guidelines Version: %.1f", guidelines.Version) + t.Logf("Mozilla Intermediate Profile: TLS Versions %v", intermediate.TLSVersions) + t.Logf("Mozilla Intermediate Profile: %d cipher suites", len(intermediate.Ciphers["go"])+len(intermediate.Ciphersuites)) + t.Logf("DefaultCiphers(): %d cipher suites", len(ourCiphers)) + + if len(unknownCiphers) > 0 { + t.Errorf("Mozilla recommends %d cipher(s) that are not in our cipher mapping", len(unknownCiphers)) + for _, cipher := range unknownCiphers { + t.Errorf(" - %s (OpenSSL name)", cipher) + } + t.Errorf("To fix: Add these ciphers to DefaultCiphers() in crypto.go, or update the cached guidelines file with 'make update-mozilla-guidelines'") + } + + if len(deprecated) > 0 { + t.Errorf("DefaultCiphers() includes %d deprecated cipher(s) not in Mozilla Intermediate recommendations", len(deprecated)) + var deprecatedNames []string + for id := range deprecated { + name := CipherSuiteToNameOrDie(id) + deprecatedNames = append(deprecatedNames, name) + } + sort.Strings(deprecatedNames) + for _, name := range deprecatedNames { + t.Errorf(" - %s", name) + } + t.Errorf("To fix: Remove these ciphers from DefaultCiphers() in crypto.go, or update the cached guidelines file with 'make update-mozilla-guidelines'") + } + + if len(missing) > 0 { + t.Errorf("DefaultCiphers() is missing %d cipher(s) recommended by Mozilla Intermediate profile", len(missing)) + var missingNames []string + for id := range missing { + // Use tls.CipherSuiteName if available, otherwise lookup in our map + name := tls.CipherSuiteName(id) + if name == "" || !strings.HasPrefix(name, "TLS_") { + // Fallback to our mapping + for k, v := range ciphers { + if v == id { + name = k + break + } + } + } + missingNames = append(missingNames, name) + } + sort.Strings(missingNames) + for _, name := range missingNames { + t.Errorf(" - %s", name) + } + t.Errorf("To fix: Add these ciphers to the ciphers map in crypto.go, or update the cached guidelines file with 'make update-mozilla-guidelines'") + } + + if len(deprecated) == 0 && len(missing) == 0 { + t.Logf("DefaultCiphers() is perfectly aligned with Mozilla Intermediate profile") + } +} diff --git a/pkg/crypto/testfiles/mozilla-guidelines.json b/pkg/crypto/testfiles/mozilla-guidelines.json new file mode 100644 index 0000000000..764aadaebe --- /dev/null +++ b/pkg/crypto/testfiles/mozilla-guidelines.json @@ -0,0 +1,209 @@ +{ + "version": 5.7, + "href": "https://ssl-config.mozilla.org/guidelines/5.7.json", + "configurations": { + "modern": { + "certificate_curves": ["prime256v1", "secp384r1"], + "certificate_signatures": ["ecdsa-with-SHA256", "ecdsa-with-SHA384", "ecdsa-with-SHA512"], + "certificate_types": ["ecdsa"], + "ciphers": { + "caddy": [], + "go": [], + "iana": [], + "openssl": [] + }, + "ciphersuites": [ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ], + "dh_param_size": null, + "ecdh_param_size": 256, + "hsts_min_age": 63072000, + "maximum_certificate_lifespan": 90, + "ocsp_staple": true, + "oldest_clients": ["Firefox 63", "Android 10.0", "Chrome 70", "Edge 75", "Java 11", "OpenSSL 1.1.1", "Opera 57", "Safari 12.1"], + "recommended_certificate_lifespan": 90, + "rsa_key_size": null, + "server_preferred_order": false, + "tls_curves": ["X25519", "prime256v1", "secp384r1"], + "tls_versions": ["TLSv1.3"] + }, + "intermediate": { + "certificate_curves": ["prime256v1", "secp384r1"], + "certificate_signatures": ["sha256WithRSAEncryption", "ecdsa-with-SHA256", "ecdsa-with-SHA384", "ecdsa-with-SHA512"], + "certificate_types": ["ecdsa", "rsa"], + "ciphers": { + "caddy": [ + "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" + ], + "go": [ + "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", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" + ], + "iana": [ + "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_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + ], + "openssl": [ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305", + "DHE-RSA-AES128-GCM-SHA256", + "DHE-RSA-AES256-GCM-SHA384", + "DHE-RSA-CHACHA20-POLY1305" + ] + }, + "ciphersuites": [ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ], + "dh_param_size": 2048, + "ecdh_param_size": 256, + "hsts_min_age": 63072000, + "maximum_certificate_lifespan": 366, + "ocsp_staple": true, + "oldest_clients": ["Firefox 27", "Android 4.4.2", "Chrome 31", "Edge", "IE 11 on Windows 7", "Java 8u31", "OpenSSL 1.0.1", "Opera 20", "Safari 9"], + "recommended_certificate_lifespan": 90, + "rsa_key_size": 2048, + "server_preferred_order": false, + "tls_curves": ["X25519", "prime256v1", "secp384r1"], + "tls_versions": ["TLSv1.2", "TLSv1.3"] + }, + "old": { + "certificate_curves": ["prime256v1", "secp384r1"], + "certificate_signatures": ["sha256WithRSAEncryption"], + "certificate_types": ["rsa"], + "ciphers": { + "caddy": [ + "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_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", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA" + ], + "go": [ + "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", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_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", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA" + ], + "iana": [ + "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_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA" + ], + "openssl": [ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305", + "DHE-RSA-AES128-GCM-SHA256", + "DHE-RSA-AES256-GCM-SHA384", + "DHE-RSA-CHACHA20-POLY1305", + "ECDHE-ECDSA-AES128-SHA256", + "ECDHE-RSA-AES128-SHA256", + "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", + "ECDHE-ECDSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA384", + "ECDHE-ECDSA-AES256-SHA", + "ECDHE-RSA-AES256-SHA", + "DHE-RSA-AES128-SHA256", + "DHE-RSA-AES256-SHA256", + "AES128-GCM-SHA256", + "AES256-GCM-SHA384", + "AES128-SHA256", + "AES256-SHA256", + "AES128-SHA", + "AES256-SHA", + "DES-CBC3-SHA" + ] + }, + "ciphersuites": [ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256" + ], + "dh_param_size": 1024, + "ecdh_param_size": 256, + "hsts_min_age": 63072000, + "maximum_certificate_lifespan": 366, + "ocsp_staple": true, + "oldest_clients": ["Firefox 1", "Android 2.3", "Chrome 1", "Edge 12", "IE8 on Windows XP", "Java 6", "OpenSSL 0.9.8", "Opera 5", "Safari 1"], + "recommended_certificate_lifespan": 90, + "rsa_key_size": 2048, + "server_preferred_order": true, + "tls_curves": ["X25519", "prime256v1", "secp384r1"], + "tls_versions": ["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"] + } + } +}