#81199·Go

crypto/tls: add a GODEBUG to disable advertising ML-DSA signature algorithms in the ClientHello (Go 1.27 regression against TLS-inspecting middleboxes)

Author: pangaCreated Aug 28, 2026Updated Sep 17, 2026
LabelsNeedsInvestigation

Go version

go version go1.27.0 darwin/arm64

Output of go env in your module/workspace:

GOARCH='arm64'
GODEBUG=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOOS='darwin'
GOROOT='/opt/go/1.27.0'
GOTOOLCHAIN='local'
GOVERSION='go1.27.0'
CGO_ENABLED='1'

What did you do?

After upgrading to Go 1.27, all HTTPS/TLS 1.3 connections from Go programs to certain hosts started failing with read: connection reset by peer when the client is behind our corporate VPN / TLS-inspecting firewall. The same programs built with Go 1.26.3 work; curl, OpenSSL 3.6, and Node.js 24 work; and forcing MaxVersion = tls.VersionTLS12 works.

Minimal reproducer (fails only when routed through the affected middlebox):

package main

import (
	"fmt"
	"net/http"
)

func main() {
	// Any TLS 1.3 endpoint reached through the TLS-inspecting middlebox.
	resp, err := http.Get("https://<host-behind-inspecting-proxy>/")
	if err != nil {
		fmt.Println("ERR:", err)
		return
	}
	fmt.Println("OK:", resp.Status)
}

I bisected it to the ClientHello on the wire. The only difference between the Go 1.26.3 ClientHello (works) and the Go 1.27.0 ClientHello (reset) is that Go 1.27 adds the ML-DSA signature schemes MLDSA44 (0x0904), MLDSA65 (0x0905), MLDSA87 (0x0906) to both the signature_algorithms (0x000d) and signature_algorithms_cert (0x0032) extensions (added in defaultSupportedSignatureAlgorithms() in crypto/tls/defaults.go).

To isolate the exact trigger, I ran a transparent TCP relay that mutates the ClientHello in flight and forwards it over the same network path:

Client / mutation ML-DSA in 0x000d ML-DSA in 0x0032 Middlebox result
Go 1.26.3 (unmodified) no no ServerHello — ok
Go 1.27.0 (unmodified) yes yes RST
Go 1.27.0, strip ML-DSA from 0x0032 only yes no ServerHello — ok
Go 1.27.0, strip ML-DSA from 0x000d only no yes RST
Node 24 / OpenSSL 3.5 (unmodified) yes (no 0x0032 sent) ServerHello — ok
Node 24, inject 0x0032 with classic sigalgs yes classic only ServerHello — ok
Node 24, inject 0x0032 with ML-DSA yes yes RST

This pins the trigger precisely: the middlebox resets any ClientHello that carries ML-DSA codepoints (0x09040x0906) in the signature_algorithms_cert (0x0032) extension. It is:

  • not ClientHello size / TCP-segment spanning (a larger Node hello passes; a smaller Go hello fails),
  • not a JA3/JA4 fingerprint match (removing ML-DSA from 0x000d also changes the fingerprint but still resets; only the 0x0032 content matters),
  • not the ML-KEM key-share PQC (identical key_share in both; GODEBUG=tlsmlkem=0,tlssecpmlkem=0 does not help),
  • not client-specific (Node reproduces it the moment ML-DSA is present in 0x0032).

What did you see happen?

Get "https://.../": read tcp 172.24.x.x:NNNN->A.B.C.D:443: read: connection reset by peer

TLS 1.2 to the same host succeeds; TLS 1.3 is reset during the handshake (the peer sends a fatal alert + RST after the ClientHello). There is currently no way to keep TLS 1.3 while suppressing the ML-DSA advertisement: tlsmlkem / tlssecpmlkem only affect key exchange, and there is no GODEBUG for the signature-algorithms list.

What did you expect to see?

A successful TLS 1.3 handshake, as with Go 1.26 — or, failing that, a supported opt-out.

I understand the middlebox is non-compliant: RFC 8446 §4.2.3 requires peers to ignore unrecognized SignatureScheme values, and this appliance fails closed instead. We have opened a ticket with the appliance vendor. However:

  • This breaks real deployments silently on upgrade to 1.27 (any Go program — including prebuilt tools like kubectl — behind a common class of enterprise TLS-inspection proxy / NGFW), and there is no runtime workaround short of forcing TLS 1.2 or downgrading the toolchain.
  • Go has consistently shipped transition GODEBUGs for exactly this kind of new-default-on-the-wire change (e.g. tlsmlkem, tlskyber, tlssha1, x509sha256), letting operators unblock while the ecosystem/appliances catch up.

Request: add a GODEBUG (e.g. tlsmldsa=0, or a more specific tlsmldsacert=0 scoped to the signature_algorithms_cert extension) that suppresses advertising the ML-DSA signature schemes in the ClientHello, defaulting to on (tlsmldsa=1) per the compatibility policy, documented in godebug.md and the 1.27 release notes. This keeps TLS 1.3 usable behind these middleboxes without a downgrade to TLS 1.2.

Happy to provide the full standalone reproducer (client + ClientHello-mutating relay + packet-level extension decodes) if useful.