golang/go · error

tls: client illegally modified second ClientHello

Error message

tls: client illegally modified second ClientHello

What it means

The second ClientHello (after HelloRetryRequest) must be identical to the first, except for a narrowly defined set of allowed changes: the key_share extension, cookie extension, early_data removal, and the obfuscated_ticket_age. The illegalClientHelloChange function performs this comparison per RFC 8446 Section 4.1.2. It checks that supportedVersions, cipherSuites, supportedCurves, supportedSignatureAlgorithms, supportedSignatureAlgorithmsCert, alpnProtocols all match in length and content, and that vers, random, sessionId, serverName, and other fields are unchanged.

Source

Thrown at src/crypto/tls/handshake_server_tls13.go:650

	if len(clientHello.keyShares) != 1 {
		c.sendAlert(alertIllegalParameter)
		return nil, errors.New("tls: client didn't send one key share in second ClientHello")
	}
	ks := &clientHello.keyShares[0]

	if ks.group != selectedGroup {
		c.sendAlert(alertIllegalParameter)
		return nil, errors.New("tls: client sent unexpected key share in second ClientHello")
	}

	if clientHello.earlyData {
		c.sendAlert(alertIllegalParameter)
		return nil, errors.New("tls: client indicated early data in second ClientHello")
	}

	if illegalClientHelloChange(clientHello, hs.clientHello) {
		c.sendAlert(alertIllegalParameter)
		return nil, errors.New("tls: client illegally modified second ClientHello")
	}

	c.didHRR = true
	hs.clientHello = clientHello
	return ks, nil
}

// illegalClientHelloChange reports whether the two ClientHello messages are
// different, with the exception of the changes allowed before and after a
// HelloRetryRequest. See RFC 8446, Section 4.1.2.
func illegalClientHelloChange(ch, ch1 *clientHelloMsg) bool {
	if len(ch.supportedVersions) != len(ch1.supportedVersions) ||
		len(ch.cipherSuites) != len(ch1.cipherSuites) ||
		len(ch.supportedCurves) != len(ch1.supportedCurves) ||
		len(ch.supportedSignatureAlgorithms) != len(ch1.supportedSignatureAlgorithms) ||
		len(ch.supportedSignatureAlgorithmsCert) != len(ch1.supportedSignatureAlgorithmsCert) ||
		len(ch.alpnProtocols) != len(ch1.alpnProtocols) {
		return true

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Ensure the client copies the first ClientHello and only modifies the key_share, cookie, and early_data fields per RFC 8446 §4.1.2.
  2. Verify no intermediary (proxy, load balancer, WAF) is modifying ClientHello fields between the two messages.
  3. Test against a reference TLS 1.3 implementation to confirm the retry ClientHello is well-formed.
  4. Use Wireshark to compare the first and second ClientHello field-by-field.

Example fix

// No server-side fix; client must preserve all fields except:
//   - key_share (update for HRR-selected group)
//   - cookie (echo server's cookie if present)
//   - early_data (must be removed)
// before (buggy): rebuild ClientHello from scratch
// after (correct): copy firstClientHello, modify only allowed fields
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: validate that only allowed fields changed
func validateRetryClientHello(ch1, ch2 *clientHelloMsg) error {
    // Only key_share, cookie, early_data, and obfuscated_ticket_age may differ
    if ch1.serverName != ch2.serverName {
        return errors.New("serverName must not change between ClientHello messages")
    }
    if len(ch1.cipherSuites) != len(ch2.cipherSuites) {
        return errors.New("cipher suites must not change")
    }
    // ... check all protected fields per RFC 8446 §4.1.2
    return nil
}

Try / catch

err := conn.Handshake()
if err != nil && strings.Contains(err.Error(), "illegally modified second ClientHello") {
    log.Printf("client modified disallowed fields after HRR: %v", err)
    conn.Close()
}

Prevention

When it happens

Trigger: The client modified disallowed fields between the first and second ClientHello — e.g. changed cipher suites, altered ALPN protocols, changed the SNI, modified supported signature algorithms, or changed supported curves. The illegalClientHelloChange function detects any such deviation.

Common situations: A buggy client that regenerates its ClientHello from scratch rather than copying and modifying the original; a MITM proxy that alters ClientHello fields; a client that attempts to negotiate different parameters on retry; non-conformant TLS libraries in IoT or embedded systems.

Understand the failure class

Related errors


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