golang/go · error
tls: client didn't send one key share in second ClientHello
Error message
tls: client didn't send one key share in second ClientHello
What it means
During a TLS 1.3 HelloRetryRequest (HRR) round-trip, the server expects the client's second ClientHello to contain exactly one key share entry (the one for the group the server selected in HRR). This error fires when the server-side handshake processor sees len(clientHello.keyShares) != 1 after HRR. Per RFC 8446 Section 4.1.2, after HRR the client must drop all key shares except the one matching the server's selected group, yielding exactly one entry.
Solutions
- Verify the client TLS implementation correctly strips non-selected key shares after receiving HRR (RFC 8446 §4.1.2).
- Test with a known-good TLS 1.3 client (e.g. OpenSSL s_client, Firefox) to isolate whether the issue is client-side.
- If you control the client, ensure it sends exactly one keyShare extension entry whose group matches the server's HRR selected_group.
- Capture the handshake with Wireshark and inspect the second ClientHello's key_share extension.
Example fix
// No application-level fix; this is a protocol-conformance error.
// On the client side, after receiving HelloRetryRequest:
// - Remove all existing key shares
// - Generate exactly one new key share for the server-selected group
// - Send only that single key share in the retry ClientHello
// Example (conceptual):
// before (buggy): resend all original keyShares
// after (correct): keyShares = [{group: hrrSelectedGroup, data: newKeyShare}] Defensive patterns
Strategy: validation
Validate before calling
// This is a server-side protocol check; callers cannot prevent it.
// If implementing a TLS 1.3 client, validate before sending:
func validateRetryKeyShares(keyShares []keyShare, hrrGroup CurveID) error {
if len(keyShares) != 1 {
return errors.New("must send exactly one key share after HRR")
}
if keyShares[0].group != hrrGroup {
return errors.New("key share group must match HRR selected_group")
}
return nil
} Type guard
// Type guard for server-side: check keyShares slice before processing
func hasExactlyOneKeyShare(ch *clientHelloMsg) bool {
return len(ch.keyShares) == 1
} Try / catch
// Server-side: these errors surface via tls.Conn.Handshake() error
// Handle by logging and closing the connection:
err := conn.Handshake()
if err != nil {
if strings.Contains(err.Error(), "key share") {
log.Printf("client key share protocol violation: %v", err)
}
conn.Close()
} Prevention
- Test client TLS implementations against Go's TLS server for HRR compliance.
- Monitor for this error to detect non-conformant clients.
- Use Wireshark to verify client retry ClientHello structure during interop testing.
When it happens
Trigger: Server invokes processKeyShareClientHello after issuing a HelloRetryRequest; the returned second ClientHello contains zero key shares, or more than one. This typically arises from a buggy or non-conformant TLS client that either re-sends its original multi-share list, sends zero shares, or mis-implements the HRR response path.
Common situations: Interoperability testing against a custom or embedded TLS 1.3 client stack; a man-in-the-middle proxy that mangles key shares; a client library with an HRR bug (e.g. some older OpenSSL versions or custom IoT firmware); fuzzing the handshake with a test harness that doesn't faithfully replay HRR.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: client sent unexpected key share in second ClientHello
- tls: client illegally modified second ClientHello
- tls: failed to sign handshake
- tls: invalid client finished hash
- tls: invalid server key share
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/a53525b8baca3261.
Report an issue: GitHub.
Appendix: source
Thrown at src/crypto/tls/handshake_server_tls13.go:634
encodedInner, err := decryptECHPayload(hs.echContext.hpkeContext, clientHello.original, payload)
if err != nil {
c.sendAlert(alertDecryptError)
return nil, errors.New("tls: failed to decrypt second client hello encrypted client hello extension payload")
}
echInner, err := decodeInnerClientHello(clientHello, encodedInner)
if err != nil {
c.sendAlert(alertIllegalParameter)
return nil, errors.New("tls: client sent invalid encrypted client hello extension")
}
clientHello = echInner
}
}
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")
}
View on GitHub (pinned to b6b368adc5)