Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 2 additions & 19 deletions cmd/web_acme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
155 changes: 44 additions & 111 deletions cmd/web_https.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,121 +12,80 @@ 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
}
return defaultCiphersChaChaFirst
}
// 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)

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,
if curves := lookupAll(setting.SSLCurvePreferences, curveStringMap, "curve"); len(curves) > 0 {
tlsConfig.CurvePreferences = curves
}

defaultCiphersChaCha = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
if ciphers := lookupAll(setting.SSLCipherSuites, cipherStringMap, "cipher suite"); len(ciphers) > 0 {
tlsConfig.CipherSuites = ciphers
}

defaultCiphersAESfirst = append(defaultCiphersAES, defaultCiphersChaCha...)
defaultCiphersChaChaFirst = append(defaultCiphersChaCha, defaultCiphersAES...)
)
return tlsConfig
}

// runHTTPS listens on the provided network address and then calls
// Serve to handle requests on incoming TLS connections.
Expand All @@ -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)
Expand Down
110 changes: 110 additions & 0 deletions cmd/web_https_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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)
}

// TestHTTPSServesTLS13 pins the regression that an unconfigured server was capped at TLS 1.2
Comment thread
silverwind marked this conversation as resolved.
Outdated
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)
}
}
14 changes: 8 additions & 6 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.23
Expand Down Expand Up @@ -208,6 +207,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
Expand Down
4 changes: 0 additions & 4 deletions modules/graceful/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading