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
- Identify and bypass or upgrade the middlebox that is replaying TLS 1.3 fields into a TLS 1.2 handshake.
- Confirm by capturing traffic on both sides of the suspected appliance.
- 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
- Treat this as a security signal, not a transient failure.
- Investigate the network path for middleboxes.
- Prefer TLS 1.3 endpoints.
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: invalid outer extensions
- tls: malformed encrypted_client_hello extension
- tls: downgrade attempt detected, possibly due to a MitM atta
- tls: received unexpected CertificateStatus message
- tls: server selected unsupported compression format
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/2afdd29b0ccf968a.
Report an issue: GitHub.