diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 68fd5d00d8e..10a00cc4ec5 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -111,25 +111,8 @@ func runACME(listenAddr string, m http.Handler) error { } } - tlsConfig := magic.TLSConfig() - tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") - - if version := toTLSVersion(setting.SSLMinimumVersion); version != 0 { - tlsConfig.MinVersion = version - } - if version := toTLSVersion(setting.SSLMaximumVersion); version != 0 { - tlsConfig.MaxVersion = version - } - - // Set curve preferences - if curves := toCurvePreferences(setting.SSLCurvePreferences); len(curves) > 0 { - tlsConfig.CurvePreferences = curves - } - - // Set cipher suites - if ciphers := toTLSCiphers(setting.SSLCipherSuites); len(ciphers) > 0 { - tlsConfig.CipherSuites = ciphers - } + // certmagic only advertises its own ACME challenge protocol, applyTLSSettings appends ours + tlsConfig := applyTLSSettings(magic.TLSConfig()) if enableHTTPChallenge { go func() { diff --git a/cmd/web_https.go b/cmd/web_https.go index 2eabe32ca5b..fe285597aa1 100644 --- a/cmd/web_https.go +++ b/cmd/web_https.go @@ -12,122 +12,81 @@ import ( "gitea.dev/modules/graceful" "gitea.dev/modules/log" "gitea.dev/modules/setting" - - "github.com/klauspost/cpuid/v2" ) var tlsVersionStringMap = map[string]uint16{ - "": tls.VersionTLS12, // Default to tls.VersionTLS12 "tlsv1.0": tls.VersionTLS10, "tlsv1.1": tls.VersionTLS11, "tlsv1.2": tls.VersionTLS12, "tlsv1.3": tls.VersionTLS13, } +// toTLSVersion returns 0 when unset or unknown, which keeps the crypto/tls default func toTLSVersion(version string) uint16 { - tlsVersion, ok := tlsVersionStringMap[strings.TrimSpace(strings.ToLower(version))] - if !ok { + version = strings.TrimSpace(strings.ToLower(version)) + tlsVersion, ok := tlsVersionStringMap[version] + if !ok && version != "" { log.Warn("Unknown tls version: %s", version) - return 0 } return tlsVersion } var curveStringMap = map[string]tls.CurveID{ - "x25519": tls.X25519, - "p256": tls.CurveP256, - "p384": tls.CurveP384, - "p521": tls.CurveP521, + "x25519": tls.X25519, + "p256": tls.CurveP256, + "p384": tls.CurveP384, + "p521": tls.CurveP521, + "x25519mlkem768": tls.X25519MLKEM768, + "secp256r1mlkem768": tls.SecP256r1MLKEM768, + "secp384r1mlkem1024": tls.SecP384r1MLKEM1024, } -func toCurvePreferences(preferences []string) []tls.CurveID { - ids := make([]tls.CurveID, 0, len(preferences)) - for _, pref := range preferences { - id, ok := curveStringMap[strings.TrimSpace(strings.ToLower(pref))] +// lookupAll resolves configured names, warning about and skipping the ones crypto/tls does not know +func lookupAll[T comparable](names []string, table map[string]T, kind string) []T { + values := make([]T, 0, len(names)) + for _, name := range names { + value, ok := table[strings.TrimSpace(strings.ToLower(name))] if !ok { - log.Warn("Unknown curve: %s", pref) - } - if id != 0 { - ids = append(ids, id) + log.Warn("Unknown %s: %s", kind, name) + continue } + values = append(values, value) } - return ids + return values } -var cipherStringMap = map[string]uint16{ - "rsa_with_rc4_128_sha": tls.TLS_RSA_WITH_RC4_128_SHA, - "rsa_with_3des_ede_cbc_sha": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, - "rsa_with_aes_128_cbc_sha": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "rsa_with_aes_256_cbc_sha": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "rsa_with_aes_128_cbc_sha256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - "rsa_with_aes_128_gcm_sha256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - "rsa_with_aes_256_gcm_sha384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - "ecdhe_ecdsa_with_rc4_128_sha": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, - "ecdhe_ecdsa_with_aes_128_cbc_sha": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "ecdhe_ecdsa_with_aes_256_cbc_sha": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "ecdhe_rsa_with_rc4_128_sha": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, - "ecdhe_rsa_with_3des_ede_cbc_sha": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "ecdhe_rsa_with_aes_128_cbc_sha": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "ecdhe_rsa_with_aes_256_cbc_sha": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "ecdhe_ecdsa_with_aes_128_cbc_sha256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - "ecdhe_rsa_with_aes_128_cbc_sha256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - "ecdhe_rsa_with_aes_128_gcm_sha256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "ecdhe_ecdsa_with_aes_128_gcm_sha256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "ecdhe_rsa_with_aes_256_gcm_sha384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - "ecdhe_ecdsa_with_aes_256_gcm_sha384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - "ecdhe_rsa_with_chacha20_poly1305_sha256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - "ecdhe_ecdsa_with_chacha20_poly1305_sha256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - "ecdhe_rsa_with_chacha20_poly1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - "ecdhe_ecdsa_with_chacha20_poly1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - "aes_128_gcm_sha256": tls.TLS_AES_128_GCM_SHA256, - "aes_256_gcm_sha384": tls.TLS_AES_256_GCM_SHA384, - "chacha20_poly1305_sha256": tls.TLS_CHACHA20_POLY1305_SHA256, -} +// cipherStringMap is derived from crypto/tls so that suites Go adds or removes are picked up automatically +var cipherStringMap = buildCipherStringMap() -func toTLSCiphers(cipherStrings []string) []uint16 { - ciphers := make([]uint16, 0, len(cipherStrings)) - for _, cipherString := range cipherStrings { - cipher, ok := cipherStringMap[strings.TrimSpace(strings.ToLower(cipherString))] - if !ok { - log.Warn("Unknown cipher: %s", cipherString) - } - if cipher != 0 { - ciphers = append(ciphers, cipher) - } +func buildCipherStringMap() map[string]uint16 { + ciphers := map[string]uint16{ + // aliases for the two suites Go later renamed with a _SHA256 suffix + "ecdhe_rsa_with_chacha20_poly1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "ecdhe_ecdsa_with_chacha20_poly1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } + for _, cipher := range append(tls.CipherSuites(), tls.InsecureCipherSuites()...) { + ciphers[strings.ToLower(strings.TrimPrefix(cipher.Name, "TLS_"))] = cipher.ID } - return ciphers } -// defaultCiphers uses hardware support to check if AES is specifically -// supported by the CPU. -// -// If AES is supported AES ciphers will be preferred over ChaCha based ciphers -// (This code is directly inspired by the certmagic code.) -func defaultCiphers() []uint16 { - if cpuid.CPU.Supports(cpuid.AESNI) { - return defaultCiphersAESfirst +// applyTLSSettings applies the configured [server] TLS options. Every option +// left unset keeps the crypto/tls default, which Go keeps current. +func applyTLSSettings(tlsConfig *tls.Config) *tls.Config { + // the listener is already TLS wrapped when it reaches http.Server, so it never sets ALPN for us + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2", "http/1.1") + tlsConfig.MinVersion = toTLSVersion(setting.SSLMinimumVersion) + tlsConfig.MaxVersion = toTLSVersion(setting.SSLMaximumVersion) + + if curves := lookupAll(setting.SSLCurvePreferences, curveStringMap, "curve"); len(curves) > 0 { + tlsConfig.CurvePreferences = curves } - return defaultCiphersChaChaFirst + if ciphers := lookupAll(setting.SSLCipherSuites, cipherStringMap, "cipher suite"); len(ciphers) > 0 { + tlsConfig.CipherSuites = ciphers + } + return tlsConfig } -var ( - defaultCiphersAES = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - } - - defaultCiphersChaCha = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - } - - defaultCiphersAESfirst = append(defaultCiphersAES, defaultCiphersChaCha...) - defaultCiphersChaChaFirst = append(defaultCiphersChaCha, defaultCiphersAES...) -) - // runHTTPS listens on the provided network address and then calls // Serve to handle requests on incoming TLS connections. // @@ -136,33 +95,7 @@ var ( // certFile should be the concatenation of the server's certificate followed by the // CA's certificate. func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error { - tlsConfig := &tls.Config{} - if tlsConfig.NextProtos == nil { - tlsConfig.NextProtos = []string{"h2", "http/1.1"} - } - - if version := toTLSVersion(setting.SSLMinimumVersion); version != 0 { - tlsConfig.MinVersion = version - } - if version := toTLSVersion(setting.SSLMaximumVersion); version != 0 { - tlsConfig.MaxVersion = version - } - - // Set curve preferences - tlsConfig.CurvePreferences = []tls.CurveID{ - tls.X25519, - tls.CurveP256, - } - if curves := toCurvePreferences(setting.SSLCurvePreferences); len(curves) > 0 { - tlsConfig.CurvePreferences = curves - } - - // Set cipher suites - tlsConfig.CipherSuites = defaultCiphers() - if ciphers := toTLSCiphers(setting.SSLCipherSuites); len(ciphers) > 0 { - tlsConfig.CipherSuites = ciphers - } - + tlsConfig := applyTLSSettings(&tls.Config{}) tlsConfig.Certificates = make([]tls.Certificate, 1) certPEMBlock, err := os.ReadFile(certFile) diff --git a/cmd/web_https_test.go b/cmd/web_https_test.go new file mode 100644 index 00000000000..a6bc4dcd65d --- /dev/null +++ b/cmd/web_https_test.go @@ -0,0 +1,109 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "crypto/tls" + "math" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "gitea.dev/modules/setting" + "gitea.dev/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToTLSVersion(t *testing.T) { + assert.Zero(t, toTLSVersion(""), "unset must keep the crypto/tls default") + assert.Zero(t, toTLSVersion("unknown")) + assert.Equal(t, uint16(tls.VersionTLS12), toTLSVersion(" TLSV1.2 ")) +} + +func unsetTLSSettings(t *testing.T) { + t.Helper() + t.Cleanup(test.MockVariableValue(&setting.SSLMinimumVersion, "")) + t.Cleanup(test.MockVariableValue(&setting.SSLMaximumVersion, "")) + t.Cleanup(test.MockVariableValue(&setting.SSLCurvePreferences, nil)) + t.Cleanup(test.MockVariableValue(&setting.SSLCipherSuites, nil)) +} + +// TestApplyTLSSettingsUnsetKeepsGoDefaults guards against reintroducing hardcoded defaults, +// which is how the server ended up capped at TLS 1.2 and without post-quantum key exchange. +// It asserts the fields are left alone rather than which algorithms Go then picks, so a new +// Go release changing its preferences does not fail this test. +func TestApplyTLSSettingsUnsetKeepsGoDefaults(t *testing.T) { + unsetTLSSettings(t) + + tlsConfig := applyTLSSettings(&tls.Config{}) + assert.Zero(t, tlsConfig.MinVersion) + assert.Zero(t, tlsConfig.MaxVersion) + assert.Nil(t, tlsConfig.CurvePreferences) + assert.Nil(t, tlsConfig.CipherSuites) + assert.Equal(t, []string{"h2", "http/1.1"}, tlsConfig.NextProtos) +} + +func TestApplyTLSSettingsConfigured(t *testing.T) { + t.Cleanup(test.MockVariableValue(&setting.SSLMinimumVersion, "tlsv1.2")) + t.Cleanup(test.MockVariableValue(&setting.SSLMaximumVersion, "tlsv1.3")) + t.Cleanup(test.MockVariableValue(&setting.SSLCurvePreferences, []string{"p384"})) + t.Cleanup(test.MockVariableValue(&setting.SSLCipherSuites, []string{"ecdhe_rsa_with_aes_256_gcm_sha384"})) + + tlsConfig := applyTLSSettings(&tls.Config{}) + assert.Equal(t, uint16(tls.VersionTLS12), tlsConfig.MinVersion) + assert.Equal(t, uint16(tls.VersionTLS13), tlsConfig.MaxVersion) + assert.Equal(t, []tls.CurveID{tls.CurveP384}, tlsConfig.CurvePreferences) + assert.Equal(t, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384}, tlsConfig.CipherSuites) +} + +func TestHTTPSServesTLS13(t *testing.T) { + unsetTLSSettings(t) + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + server.TLS = applyTLSSettings(&tls.Config{}) + server.EnableHTTP2 = true + server.StartTLS() + defer server.Close() + + conn, err := tls.Dial("tcp", strings.TrimPrefix(server.URL, "https://"), &tls.Config{InsecureSkipVerify: true}) + require.NoError(t, err) + defer conn.Close() + assert.Equal(t, uint16(tls.VersionTLS13), conn.ConnectionState().Version) +} + +// TestCurveStringMapCoversGoCurves fails when Go gains a curve the map does not name, so the +// config vocabulary cannot silently go stale the way it did for the post-quantum key exchanges +func TestCurveStringMapCoversGoCurves(t *testing.T) { + for id := range math.MaxUint16 + 1 { + name := tls.CurveID(id).String() + if strings.HasPrefix(name, "CurveID(") { + continue // crypto/tls has no name for this ID, so it supports no such curve + } + assert.Contains(t, curveStringMap, strings.TrimPrefix(strings.ToLower(name), "curve"), "curve %s", name) + } +} + +// TestCipherStringMapCoversLegacyNames pins the names accepted before the map was derived +// from crypto/tls, so no existing SSL_CIPHER_SUITES config silently stops resolving +func TestCipherStringMapCoversLegacyNames(t *testing.T) { + for _, name := range []string{ + "rsa_with_rc4_128_sha", "rsa_with_3des_ede_cbc_sha", "rsa_with_aes_128_cbc_sha", + "rsa_with_aes_256_cbc_sha", "rsa_with_aes_128_cbc_sha256", "rsa_with_aes_128_gcm_sha256", + "rsa_with_aes_256_gcm_sha384", "ecdhe_ecdsa_with_rc4_128_sha", "ecdhe_ecdsa_with_aes_128_cbc_sha", + "ecdhe_ecdsa_with_aes_256_cbc_sha", "ecdhe_rsa_with_rc4_128_sha", "ecdhe_rsa_with_3des_ede_cbc_sha", + "ecdhe_rsa_with_aes_128_cbc_sha", "ecdhe_rsa_with_aes_256_cbc_sha", "ecdhe_ecdsa_with_aes_128_cbc_sha256", + "ecdhe_rsa_with_aes_128_cbc_sha256", "ecdhe_rsa_with_aes_128_gcm_sha256", "ecdhe_ecdsa_with_aes_128_gcm_sha256", + "ecdhe_rsa_with_aes_256_gcm_sha384", "ecdhe_ecdsa_with_aes_256_gcm_sha384", + "ecdhe_rsa_with_chacha20_poly1305_sha256", "ecdhe_ecdsa_with_chacha20_poly1305_sha256", + "ecdhe_rsa_with_chacha20_poly1305", "ecdhe_ecdsa_with_chacha20_poly1305", + "aes_128_gcm_sha256", "aes_256_gcm_sha384", "chacha20_poly1305_sha256", + } { + assert.NotZero(t, cipherStringMap[name], "cipher %q", name) + } +} diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c546c10d2e3..0eb33002153 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -112,15 +112,17 @@ ;; ;; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL ;REDIRECTOR_USE_PROXY_PROTOCOL = -;; Minimum and maximum supported TLS versions -;SSL_MIN_VERSION=TLSv1.2 +;; Minimum supported TLS version, one of TLSv1.0, TLSv1.1, TLSv1.2, TLSv1.3. Unset uses Go's default. +;SSL_MIN_VERSION= +;; +;; Maximum supported TLS version, one of TLSv1.0, TLSv1.1, TLSv1.2, TLSv1.3. Unset uses Go's default. ;SSL_MAX_VERSION= ;; -;; SSL Curve Preferences -;SSL_CURVE_PREFERENCES=X25519,P256 +;; Comma-separated list of SSL curve preferences. Unset uses Go's default. +;SSL_CURVE_PREFERENCES= ;; -;; SSL Cipher Suites -;SSL_CIPHER_SUITES=; Will default to "ecdhe_ecdsa_with_aes_256_gcm_sha384,ecdhe_rsa_with_aes_256_gcm_sha384,ecdhe_ecdsa_with_aes_128_gcm_sha256,ecdhe_rsa_with_aes_128_gcm_sha256,ecdhe_ecdsa_with_chacha20_poly1305,ecdhe_rsa_with_chacha20_poly1305" if aes is supported by hardware, otherwise chacha will be first. +;; Comma-separated list of SSL cipher suites. Unset uses Go's default. +;SSL_CIPHER_SUITES= ;; ;; Timeout for any write to the connection. (Set to -1 to disable all timeouts.) ;PER_WRITE_TIMEOUT = 30s diff --git a/go.mod b/go.mod index 24e7b12c97f..bafdf532cc8 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,6 @@ require ( github.com/jhillyerd/enmime/v2 v2.4.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.19.1 - github.com/klauspost/cpuid/v2 v2.4.0 github.com/lib/pq v1.12.3 github.com/markbates/goth v1.82.0 github.com/mattn/go-isatty v0.0.24 @@ -209,6 +208,7 @@ require ( github.com/jonboulle/clockwork v0.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/libdns/libdns v1.1.1 // indirect diff --git a/modules/graceful/server.go b/modules/graceful/server.go index 18e9b96b8fe..4cd138458f7 100644 --- a/modules/graceful/server.go +++ b/modules/graceful/server.go @@ -101,10 +101,6 @@ func (srv *Server) ListenAndServe(serve ServeFunction, useProxyProtocol bool) er func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFunction, useProxyProtocol, proxyProtocolTLSBridging bool) error { go srv.awaitShutdown() - if tlsConfig.MinVersion == 0 { - tlsConfig.MinVersion = tls.VersionTLS12 - } - listener, err := GetListener(srv.network, srv.address) if err != nil { log.Error("Unable to get Listener: %v", err)