golang/go · critical

tls: server echoed TLS 1.3 compatibility session ID in TLS 1

Error message

tls: server echoed TLS 1.3 compatibility session ID in TLS 1.2

What it means

In TLS 1.3 the client sends a random legacy session_id for middlebox compatibility (RFC 8446 4.1.2). If the handshake then negotiates TLS 1.2, a legitimate TLS 1.2 server must NOT echo that random ID because it cannot know it. hs.session == nil && hs.hello.sessionId != nil && bytes.Equal(hello.sessionId, serverHello.sessionId) means something in the path copied the TLS 1.3 compatibility ID into a TLS 1.2 ServerHello.

Source

Thrown at src/crypto/tls/handshake_client.go:547

	return nil
}

// Does the handshake, either a full one or resumes old session. Requires hs.c,
// hs.hello, hs.serverHello, and, optionally, hs.session to be set.
func (hs *clientHandshakeState) handshake() error {
	c := hs.c

	// If we did not load a session (hs.session == nil), but we did set a
	// session ID in the transmitted client hello (hs.hello.sessionId != nil),
	// it means we tried to negotiate TLS 1.3 and sent a random session ID as a
	// compatibility measure (see RFC 8446, Section 4.1.2).
	//
	// Since we're now handshaking for TLS 1.2, if the server echoed the
	// transmitted ID back to us, we know mischief is afoot: the session ID
	// was random and can't possibly be recognized by the server.
	if hs.session == nil && hs.hello.sessionId != nil && bytes.Equal(hs.hello.sessionId, hs.serverHello.sessionId) {
		c.sendAlert(alertIllegalParameter)
		return errors.New("tls: server echoed TLS 1.3 compatibility session ID in TLS 1.2")
	}

	isResume, err := hs.processServerHello()
	if err != nil {
		return err
	}

	hs.finishedHash = newFinishedHash(c.vers, hs.suite)

	// No signatures of the handshake are needed in a resumption.
	// Otherwise, in a full handshake, if we don't have any certificates
	// configured then we will never send a CertificateVerify message and
	// thus no signatures are needed in that case either.
	if isResume || (len(c.config.Certificates) == 0 && c.config.GetClientCertificate == nil) {
		hs.finishedHash.discardHandshakeBuffer()
	}

	if err := transcriptMsg(hs.hello, &hs.finishedHash); err != nil {

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Identify and bypass or upgrade the middlebox that is replaying TLS 1.3 fields into a TLS 1.2 handshake.
  2. Confirm by capturing traffic on both sides of the suspected appliance.
  3. Treat as a potential interception indicator in security-sensitive deployments.
Defensive patterns

Strategy: try-catch

Type guard

func isCompatSessionIDEcho(err error) bool {
    return err != nil && strings.Contains(err.Error(), "server echoed TLS 1.3 compatibility session ID")
}

Try / catch

if _, err := tls.Dial("tcp", addr, cfg); err != nil {
    if isCompatSessionIDEcho(err) {
        // Strong indicator of TLS interception; do not silently retry.
        security.ReportInterception(addr, err)
    }
}

Prevention

When it happens

Trigger: A middlebox that does TLS 1.3 with the client but re-originates TLS 1.2 upstream while copying the legacy session_id field verbatim; a proxy that mirrors session_id bytes without understanding them.

Common situations: TLS-terminating load balancer, captive portal, or DLP appliance that mixes protocol-version semantics; rare for genuine servers.

Understand the failure class

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/2afdd29b0ccf968a. Report an issue: GitHub.