{"record":{"id":"ba36a901fb5aee78","repo":"golang/go","slug":"tls-server-selected-unoffered-curve","errorCode":null,"errorMessage":"tls: server selected unoffered curve","messagePattern":"tls: server selected unoffered curve","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/tls/key_agreement.go","lineNumber":309,"sourceCode":"\tif ka.version >= VersionTLS12 {\n\t\tka.signatureAlgorithm = SignatureScheme(sig[0])<<8 | SignatureScheme(sig[1])\n\t\tsig = sig[2:]\n\t\tif len(sig) < 2 {\n\t\t\treturn errServerKeyExchange\n\t\t}\n\t\tswitch ka.signatureAlgorithm {\n\t\tcase MLDSA44, MLDSA65, MLDSA87:\n\t\t\treturn errors.New(\"tls: server selected ML-DSA with TLS version < 1.3\")\n\t\t}\n\t}\n\tsigLen := int(sig[0])<<8 | int(sig[1])\n\tif sigLen+2 != len(sig) {\n\t\treturn errServerKeyExchange\n\t}\n\tsig = sig[2:]\n\n\tif !slices.Contains(clientHello.supportedCurves, ka.curveID) {\n\t\treturn errors.New(\"tls: server selected unoffered curve\")\n\t}\n\n\tif _, ok := curveForCurveID(ka.curveID); !ok {\n\t\treturn errors.New(\"tls: server selected unsupported curve\")\n\t}\n\n\tkey, err := generateECDHEKey(config.rand(), ka.curveID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tka.key = key\n\n\tpeerKey, err := key.Curve().NewPublicKey(publicKey)\n\tif err != nil {\n\t\treturn errServerKeyExchange\n\t}\n\tka.preMasterSecret, err = key.ECDH(peerKey)\n\tif err != nil {","sourceCodeStart":291,"sourceCodeEnd":327,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/tls/key_agreement.go#L291-L327","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the negotiated curve and the client's CurvePreferences / supported groups to find the mismatch.","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.","Update or patch a non-conformant server to respect the client's supported_groups extension.","Do not silently retry — investigate whether the connection was tampered with."],"exampleFix":"// before\nconfig.CurvePreferences = []tls.CurveID{tls.CurveP521} // server only offers P-256\n// after: include a curve the server actually supports\nconfig.CurvePreferences = []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP521}","handlingStrategy":"validation","validationCode":"// Ensure the server's likely curve is in your offered set before relying on it.\noffered := map[tls.CurveID]bool{}\nfor _, c := range config.CurvePreferences {\n    offered[c] = true\n}\n// After connection, verify the negotiated curve was offered:\n// if !offered[conn.ConnectionState().Curve] { /* should never happen */ }","typeGuard":"// Confirm the intersection of local preferences and known-good curves is non-empty.\nfunc hasOfferedCurve(prefs []tls.CurveID, want tls.CurveID) bool {\n    for _, c := range prefs {\n        if c == want { return true }\n    }\n    return false\n}","tryCatchPattern":"// if err != nil && strings.Contains(err.Error(), \"unoffered curve\") {\n//     log.Printf(\"server chose curve %d not in client prefs; investigate tampering\", chosen)\n// }","preventionTips":["Keep CurvePreferences non-empty and intersected with server capabilities.","Avoid restricting to a single rare curve.","Audit after upgrades that change default CurvePreferences."],"tags":["tls","ecdhe","curve-negotiation","handshake","security","go"],"backgroundTag":null,"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:31:55.035Z"}