golang/go · error

tls: client sent unexpected key share in second ClientHello

Error message

tls: client sent unexpected key share in second ClientHello

What it means

After a HelloRetryRequest the server expects the single key share in the client's second ClientHello to use the exact group the server selected in the HRR's selected_group field. This error fires when ks.group != selectedGroup. RFC 8446 Section 4.1.2 requires the client to generate a fresh key share for the server-requested group.

Source

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

			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")
	}

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

// illegalClientHelloChange reports whether the two ClientHello messages are

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Confirm the client reads the HelloRetryRequest's key_share extension selected_group and generates a matching key share.
  2. Check that no intermediary (load balancer, TLS terminator) modifies the HRR selected_group value.
  3. Test with a reference TLS 1.3 implementation to confirm the server's HRR is well-formed.
  4. Inspect the packet capture: compare HRR selected_group vs. the key share group in the second ClientHello.

Example fix

// No server-side fix; this is a client protocol-conformance issue.
// Client must generate key share for the group the server requested in HRR:
// before (buggy): keyShare.group = clientOriginalGroup
// after (correct): keyShare.group = hrrSelectedGroup
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: validate key share group matches HRR before sending
func validateKeyShareGroup(group CurveID, hrrSelectedGroup CurveID) error {
    if group != hrrSelectedGroup {
        return fmt.Errorf("key share group %v != HRR group %v", group, hrrSelectedGroup)
    }
    return nil
}

Try / catch

// Server-side: handle during Handshake()
err := conn.Handshake()
if err != nil && strings.Contains(err.Error(), "unexpected key share") {
    log.Printf("client sent wrong key share group after HRR: %v", err)
    conn.Close()
}

Prevention

When it happens

Trigger: The server sent a HelloRetryRequest asking for group X (e.g. X25519), but the client's retry ClientHello contains a key share for a different group Y (e.g. P-256), or it resent the original group. The server's selectedGroup variable was set during the initial ClientHello processing.

Common situations: Client misreads or ignores the HRR selected_group field; a proxy alters the HRR or the retry ClientHello; interoperability mismatch between client and server curve preferences; a client bug where it generates a key share for the wrong curve after HRR.

Understand the failure class

Related errors


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