{"record":{"id":"0797b7b7d875ac94","repo":"golang/go","slug":"tls-server-selected-ml-dsa-with-tls-version-1-3","errorCode":null,"errorMessage":"tls: server selected ML-DSA with TLS version < 1.3","messagePattern":"tls: server selected ML-DSA with TLS version < 1\\.3","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/tls/key_agreement.go","lineNumber":299,"sourceCode":"\tif publicLen+4 > len(skx.key) {\n\t\treturn errServerKeyExchange\n\t}\n\tserverECDHEParams := skx.key[:4+publicLen]\n\tpublicKey := serverECDHEParams[4:]\n\n\tsig := skx.key[4+publicLen:]\n\tif len(sig) < 2 {\n\t\treturn errServerKeyExchange\n\t}\n\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 {","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/tls/key_agreement.go#L281-L317","documentation":"Raised by the ECDHE client (TLS 1.0-1.2 ServerKeyExchange parser) when the signature algorithm field in the server's ServerKeyExchange names an ML-DSA scheme (MLDSA44/65/87). ML-DSA is a post-quantum signature family defined only for TLS 1.3 key exchange; using it inside a pre-1.3 ECDHE handshake is forbidden by the drafts. The client refuses to honor it because the legacy ServerKeyExchange signature slot has no defined semantics for ML-DSA, and accepting it would let a server bypass the TLS 1.3 negotiation that ML-DSA was designed for.","triggerScenarios":"A TLS 1.2 (or earlier) handshake reaches processServerKeyExchange, ka.version >= VersionTLS12 is true, the parsed 2-byte SignatureScheme equals MLDSA44, MLDSA65, or MLDSA87. This typically means a misconfigured or non-conformant server (or an active attacker tampering with the wire) inserted an ML-DSA code point into a legacy handshake's signature algorithm field.","commonSituations":"Hitting an experimental/forked TLS stack that advertises ML-DSA in TLS 1.2; a man-in-the-middle or fuzzing tool injecting unexpected signature schemes; a server build that mixes post-quantum code points into pre-1.3 handshakes by mistake. Normal browsers and conformant servers never trigger it.","solutions":["Verify the server is speaking TLS 1.3 (check negotiated version / the server's supported_versions extension) — ML-DSA belongs only there.","On the server side, ensure ML-DSA signature schemes are not advertised or selected for TLS <= 1.2 ECDHE; gate them behind a VersionTLS13 check.","If you control the client config and must talk TLS 1.2, remove ML-DSA schemes from supportedSignatureAlgorithms so a server cannot select one.","Treat receipt of this error as a possible attack: log the peer identity and do not retry the connection with downgraded security."],"exampleFix":"// before (server-side selection logic, conceptual)\nif /* tls12-or-below */ {\n    sigAlg = pickAlg(clientSupported) // may return MLDSA*\n}\n// after: forbid ML-DSA unless TLS 1.3\nif /* tls12-or-below */ && isMLDSA(sigAlg) {\n    sigAlg = pickNonMLDSA(clientSupported)\n}","handlingStrategy":"try-catch","validationCode":"// Before dialing, ensure ML-DSA schemes are not offered for TLS <= 1.2.\n// (No public field exposes this directly; instead require TLS 1.3.)\nif cfg, ok := conn.ConnectionState(); ok {\n    _ = cfg\n}\n// Best pre-check: enforce TLS 1.3 so the legacy path can't surface it.\nconfig.MinVersion = tls.VersionTLS13","typeGuard":"// Guard: this error only arises on TLS < 1.3, so it is impossible once\n// MinVersion >= VersionTLS13.\nfunc isLegacyMLDSAPathBlocked(c *tls.Config) bool {\n    return c.MinVersion >= tls.VersionTLS13\n}","tryCatchPattern":"// errs := dial()\n// var e error\n// if errors.As(err, &e) && strings.Contains(e.Error(), \"ML-DSA with TLS version < 1.3\") {\n//     // peer is non-conformant or hostile; do not downgrade\n//     return fmt.Errorf(\"refusing insecure legacy ML-DSA handshake: %w\", err)\n// }","preventionTips":["Set MinVersion to VersionTLS13 to eliminate the pre-1.3 path entirely.","Do not place ML-DSA SignatureSchemes in a TLS 1.2 config.","Log peer identity if hit; treat as possible MITM."],"tags":["tls","post-quantum","ml-dsa","handshake","security","go"],"backgroundTag":null,"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}