golang/go · error

tls: server selected unoffered curve

Error message

tls: server selected unoffered curve

What it means

The client's ECDHE key agreement validation found that the curve the server chose for key exchange (ka.curveID) is not present in the clientHello.supportedCurves list the client originally offered. TLS requires the server pick a mutually supported group; selecting one the client never offered violates RFC 8422/8446 negotiation and is treated as a protocol error or attack. This is a deliberate guard against downgrade and invalid-curve tricks.

Source

Thrown at src/crypto/tls/key_agreement.go:309

	if ka.version >= VersionTLS12 {
		ka.signatureAlgorithm = SignatureScheme(sig[0])<<8 | SignatureScheme(sig[1])
		sig = sig[2:]
		if len(sig) < 2 {
			return errServerKeyExchange
		}
		switch ka.signatureAlgorithm {
		case MLDSA44, MLDSA65, MLDSA87:
			return errors.New("tls: server selected ML-DSA with TLS version < 1.3")
		}
	}
	sigLen := int(sig[0])<<8 | int(sig[1])
	if sigLen+2 != len(sig) {
		return errServerKeyExchange
	}
	sig = sig[2:]

	if !slices.Contains(clientHello.supportedCurves, ka.curveID) {
		return errors.New("tls: server selected unoffered curve")
	}

	if _, ok := curveForCurveID(ka.curveID); !ok {
		return errors.New("tls: server selected unsupported curve")
	}

	key, err := generateECDHEKey(config.rand(), ka.curveID)
	if err != nil {
		return err
	}
	ka.key = key

	peerKey, err := key.Curve().NewPublicKey(publicKey)
	if err != nil {
		return errServerKeyExchange
	}
	ka.preMasterSecret, err = key.ECDH(peerKey)
	if err != nil {

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Inspect the negotiated curve and the client's CurvePreferences / supported groups to find the mismatch.
  2. If the client intentionally restricts curves, ensure the server offers at least one of them; otherwise widen CurvePreferences on the client to include the server's choice.
  3. Update or patch a non-conformant server to respect the client's supported_groups extension.
  4. Do not silently retry — investigate whether the connection was tampered with.

Example fix

// before
config.CurvePreferences = []tls.CurveID{tls.CurveP521} // server only offers P-256
// after: include a curve the server actually supports
config.CurvePreferences = []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP521}
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the server's likely curve is in your offered set before relying on it.
offered := map[tls.CurveID]bool{}
for _, c := range config.CurvePreferences {
    offered[c] = true
}
// After connection, verify the negotiated curve was offered:
// if !offered[conn.ConnectionState().Curve] { /* should never happen */ }

Type guard

// Confirm the intersection of local preferences and known-good curves is non-empty.
func hasOfferedCurve(prefs []tls.CurveID, want tls.CurveID) bool {
    for _, c := range prefs {
        if c == want { return true }
    }
    return false
}

Try / catch

// if err != nil && strings.Contains(err.Error(), "unoffered curve") {
//     log.Printf("server chose curve %d not in client prefs; investigate tampering", chosen)
// }

Prevention

When it happens

Trigger: After parsing the ServerKeyExchange, ka.curveID holds the server's chosen curve. slices.Contains(clientHello.supportedCurves, ka.curveID) returns false. Causes: a server bug mapping the wrong curve id, a MITM rewriting the ServerKeyExchange curve field, or a non-conformant server that ignores the client's supported_groups list.

Common situations: Interoperability testing against a server that hard-codes a curve (e.g. always P-521) the client did not advertise; a proxy/load-balancer rewriting TLS parameters; an attacker performing a downgrade; mismatches after a library upgrade that changed the default CurvePreferences.

Understand the failure class

Related errors


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