golang/go · error

tls: server sent an unnecessary HelloRetryRequest key_share

Error message

tls: server sent an unnecessary HelloRetryRequest key_share

What it means

Raised while processing a TLS 1.3 HelloRetryRequest. RFC 8446 §4.1.4 forbids the server from selecting (via the key_share extension) a group for which the client already sent a key share in its first ClientHello. Go enforces this and sends an `illegal_parameter` alert before aborting. It almost always indicates a non-conformant server, middlebox, or active attacker.

Source

Thrown at src/crypto/tls/handshake_client_tls13.go:321

	if hs.serverHello.serverShare.group != 0 {
		c.sendAlert(alertDecodeError)
		return errors.New("tls: received malformed key_share extension")
	}

	// If the server sent a key_share extension selecting a group, ensure it's
	// a group we advertised but did not send a key share for, and send a key
	// share for it this time.
	if curveID := hs.serverHello.selectedGroup; curveID != 0 {
		if !slices.Contains(hello.supportedCurves, curveID) {
			c.sendAlert(alertIllegalParameter)
			return errors.New("tls: server selected unsupported group")
		}
		if slices.ContainsFunc(hs.hello.keyShares, func(ks keyShare) bool {
			return ks.group == curveID
		}) {
			c.sendAlert(alertIllegalParameter)
			return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share")
		}
		ke, err := keyExchangeForCurveID(curveID)
		if err != nil {
			c.sendAlert(alertInternalError)
			return errors.New("tls: internal error: supportsCurve accepted unimplemented curve")
		}
		hs.keyShareKeys, hello.keyShares, err = ke.keyShares(c.config.rand())
		if err != nil {
			c.sendAlert(alertInternalError)
			return err
		}
		// Do not send the fallback ECDH key share in a HRR response.
		hello.keyShares = hello.keyShares[:1]
	}

	if len(hello.pskIdentities) > 0 {
		pskSuite := cipherSuiteTLS13ByID(hs.session.cipherSuite)
		if pskSuite == nil {

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Report the bug to the server/middlebox operator; the server must select a group the client did NOT offer a share for.
  2. Pin CurvePreferences to a single group (e.g. tls.CurveP256) so the server never has reason to send HRR for a different group.
  3. Reproduce against a known-good peer (Go tls.Server) to confirm the fault is on the remote side, not your config.
  4. If you control the server, fix its HelloRetryRequest logic to select an unadvertised-share group only.

Example fix

// before
cfg := &tls.Config{} // default curves; server may HRR-pick one you shared
conn, err := tls.Dial("tcp", addr, cfg)

// after: pin one curve so HRR is never needed
cfg := &tls.Config{CurvePreferences: []tls.CurveID{tls.X25519}}
conn, err := tls.Dial("tcp", addr, cfg)
Defensive patterns

Strategy: try-catch

Try / catch

// HelloRetryRequest violations are server-driven and cannot be prevented pre-flight.
// Treat as a fatal handshake error; do not retry blindly.
dialer := &net.Dialer{Timeout: 10 * time.Second}
conn, err := tls.DialWithDialer(dialer, "tcp", addr, cfg)
if err != nil {
    if strings.Contains(err.Error(), "unnecessary HelloRetryRequest key_share") {
        // server is non-conformant; log and route to an alternate peer or surface to user
        log.Printf("peer %s sent a non-conformant HRR: %v", addr, err)
    }
    return err
}

Prevention

When it happens

Trigger: Server returns HelloRetryRequest whose selectedGroup equals one of the groups in hs.hello.keyShares (i.e. the client already offered a share for it). Reachable via crypto/tls Dial/DialTLS/tls.Client.Handshake when the remote picks a curve the client pre-shared.

Common situations: Buggy in-house TLS 1.3 server, TLS-intercepting proxy/load-balancer, fuzz test against the Go client, or a malicious MITM. Rarely seen against mainstream servers (BoringSSL, OpenSSL, NSS, Go's own server).

Understand the failure class

Related errors


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