Skip to content

crypto/tls: allow reading unencrypted tls alert after server hello #76767

@fl0mb

Description

@fl0mb

Go version

go version go1.25.1 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/user/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/user/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build611339686=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/user/tool/go-valid-cert/go.mod'
GOMODCACHE='/home/user/bin/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/user/bin/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/user/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

I have a simple https server using an untrusted tls cert:

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"io"
	"log"
	"math/big"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

)

func genTLSmemory(cn string) *tls.Certificate {
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	if err != nil {
		panic(err)
	}

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			CommonName: cn,
		},
		NotBefore:             time.Now(),
		NotAfter:              time.Now().AddDate(5, 0, 0),
		BasicConstraintsValid: false,
	}

	privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		panic(err)
	}

	publicKey := &privateKey.PublicKey

	derEncodedCert, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey)
	if err != nil {
		panic(err)
	}

	cert := &tls.Certificate{
		Certificate: [][]byte{derEncodedCert},
		PrivateKey:  privateKey,
	}
	return cert
}


func main() {

	cert := genTLSmemory("xyz")
	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{*cert},
		MinVersion:   tls.VersionTLS12,
	}
	l, err := net.Listen("tcp", ":8443")
	if err != nil {
		log.Fatalf("Error listening: %s", err)
	}

	Server := &http.Server{
		TLSConfig: tlsConfig,
	}

	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "test")
		
	})
	Server.ServeTLS(l, "", "")

}

What did you see happen?

When using TLS 1.2 I see the correct error (unknown certificate). TLS 1.3 however attempts to decrypt the alert message which fails as it is send in plain text.

# TLS 1.2
curl https://127.0.0.1:8443 --tls-max 1.2   
# Server log output
# 2025/12/09 13:07:05 http: TLS handshake error from 127.0.0.1:54982: remote error: tls: unknown certificate authority

# TLS 1.3
curl https://127.0.0.1:8443 
# Server log output
# 2025/12/09 13:07:09 http: TLS handshake error from 127.0.0.1:54994: local error: tls: bad record MAC

While the spec mandates encrypted alert messages:

Like other messages, alert messages are encrypted as specified by the current connection state.

To my understanding It does allow plain text alerts if the handshake is not finished

Note that with the transitions as shown above, clients may send alerts that derive from post-ServerHello messages in the clear or with the early data keys. If clients need to send such alerts, they SHOULD first rekey to the handshake keys if possible.

What did you expect to see?

I would like to see plaintext alert messages in TLS 1.3, similar to TLS 1.2. This should allow better debugging of TLS issues. A quick google search for local error: tls: bad record MAC yields many results with users confused about the meaning.

Plaintext messages could be read but because of tls.(*halfConn).setTrafficSecret (/crypto/tls/conn.go:230)...

func (hc *halfConn) setTrafficSecret(suite *cipherSuiteTLS13, level QUICEncryptionLevel, secret []byte) {
	hc.trafficSecret = secret
	hc.level = level
	key, iv := suite.trafficKey(secret)
	hc.cipher = suite.aead(key, iv)  // <-- cipher is set
	for i := range hc.seq {
		hc.seq[i] = 0
	}
}

cipher is not nil in tls.(*halfConn).decrypt:

func (hc *halfConn) decrypt(record []byte) ([]byte, recordType, error) {
....
	if hc.cipher != nil {
		switch c := hc.cipher.(type) {
		case cipher.Stream:
			c.XORKeyStream(payload, payload)
		case aead:
			if len(payload) < explicitNonceLen {
				return nil, 0, alertBadRecordMAC
			}
			nonce := payload[:explicitNonceLen]
			if len(nonce) == 0 {
				nonce = hc.seq[:]
			}
			payload = payload[explicitNonceLen:]

			var additionalData []byte
			if hc.version == VersionTLS13 {
				additionalData = record[:recordHeaderLen]
			} else {
				additionalData = append(hc.scratchBuf[:0], hc.seq[:]...)
				additionalData = append(additionalData, record[:3]...)
				n := len(payload) - c.Overhead()
				additionalData = append(additionalData, byte(n>>8), byte(n))
			}

			var err error
			plaintext, err = c.Open(payload[:0], nonce, payload, additionalData)
			if err != nil {
				return nil, 0, alertBadRecordMAC  // <-- observed bad record MAC error 
			}
....
	} else {
		plaintext = payload
	}

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions