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 trueView on GitHub (pinned to b6b368adc5)
Solutions
- Ensure the client copies the first ClientHello and only modifies the key_share, cookie, and early_data fields per RFC 8446 §4.1.2.
- Verify no intermediary (proxy, load balancer, WAF) is modifying ClientHello fields between the two messages.
- Test against a reference TLS 1.3 implementation to confirm the retry ClientHello is well-formed.
- 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 implementing a client, copy the first ClientHello and only modify key_share, cookie, and remove early_data.
- Ensure no proxy modifies ClientHello fields between the two messages.
- Run RFC 8446 conformance tests for the HRR path.
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: client didn't send one key share in second ClientHello
- tls: client sent unexpected key share in second ClientHello
- tls: server sent an unnecessary HelloRetryRequest key_share
- tls: server sent two HelloRetryRequest messages
- tls: failed to sign handshake: %s
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/7eee06eb0608bc25.
Report an issue: GitHub.