{"record":{"id":"7eee06eb0608bc25","repo":"golang/go","slug":"tls-client-illegally-modified-second-clienthello","errorCode":null,"errorMessage":"tls: client illegally modified second ClientHello","messagePattern":"tls: client illegally modified second ClientHello","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/tls/handshake_server_tls13.go","lineNumber":650,"sourceCode":"\tif len(clientHello.keyShares) != 1 {\n\t\tc.sendAlert(alertIllegalParameter)\n\t\treturn nil, errors.New(\"tls: client didn't send one key share in second ClientHello\")\n\t}\n\tks := &clientHello.keyShares[0]\n\n\tif ks.group != selectedGroup {\n\t\tc.sendAlert(alertIllegalParameter)\n\t\treturn nil, errors.New(\"tls: client sent unexpected key share in second ClientHello\")\n\t}\n\n\tif clientHello.earlyData {\n\t\tc.sendAlert(alertIllegalParameter)\n\t\treturn nil, errors.New(\"tls: client indicated early data in second ClientHello\")\n\t}\n\n\tif illegalClientHelloChange(clientHello, hs.clientHello) {\n\t\tc.sendAlert(alertIllegalParameter)\n\t\treturn nil, errors.New(\"tls: client illegally modified second ClientHello\")\n\t}\n\n\tc.didHRR = true\n\ths.clientHello = clientHello\n\treturn ks, nil\n}\n\n// illegalClientHelloChange reports whether the two ClientHello messages are\n// different, with the exception of the changes allowed before and after a\n// HelloRetryRequest. See RFC 8446, Section 4.1.2.\nfunc illegalClientHelloChange(ch, ch1 *clientHelloMsg) bool {\n\tif len(ch.supportedVersions) != len(ch1.supportedVersions) ||\n\t\tlen(ch.cipherSuites) != len(ch1.cipherSuites) ||\n\t\tlen(ch.supportedCurves) != len(ch1.supportedCurves) ||\n\t\tlen(ch.supportedSignatureAlgorithms) != len(ch1.supportedSignatureAlgorithms) ||\n\t\tlen(ch.supportedSignatureAlgorithmsCert) != len(ch1.supportedSignatureAlgorithmsCert) ||\n\t\tlen(ch.alpnProtocols) != len(ch1.alpnProtocols) {\n\t\treturn true","sourceCodeStart":632,"sourceCodeEnd":668,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/tls/handshake_server_tls13.go#L632-L668","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// No server-side fix; client must preserve all fields except:\n//   - key_share (update for HRR-selected group)\n//   - cookie (echo server's cookie if present)\n//   - early_data (must be removed)\n// before (buggy): rebuild ClientHello from scratch\n// after (correct): copy firstClientHello, modify only allowed fields","handlingStrategy":"validation","validationCode":"// Client-side: validate that only allowed fields changed\nfunc validateRetryClientHello(ch1, ch2 *clientHelloMsg) error {\n    // Only key_share, cookie, early_data, and obfuscated_ticket_age may differ\n    if ch1.serverName != ch2.serverName {\n        return errors.New(\"serverName must not change between ClientHello messages\")\n    }\n    if len(ch1.cipherSuites) != len(ch2.cipherSuites) {\n        return errors.New(\"cipher suites must not change\")\n    }\n    // ... check all protected fields per RFC 8446 §4.1.2\n    return nil\n}","typeGuard":null,"tryCatchPattern":"err := conn.Handshake()\nif err != nil && strings.Contains(err.Error(), \"illegally modified second ClientHello\") {\n    log.Printf(\"client modified disallowed fields after HRR: %v\", err)\n    conn.Close()\n}","preventionTips":["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."],"tags":["tls","tls13","handshake","helloretryrequest","protocol-conformance","server-side"],"backgroundTag":null,"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:31:55.035Z"}