golang/go · error
tls: server selected ML-DSA with TLS version < 1.3
Error message
tls: server selected ML-DSA with TLS version < 1.3
What it means
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.
Source
Thrown at src/crypto/tls/key_agreement.go:299
if publicLen+4 > len(skx.key) {
return errServerKeyExchange
}
serverECDHEParams := skx.key[:4+publicLen]
publicKey := serverECDHEParams[4:]
sig := skx.key[4+publicLen:]
if len(sig) < 2 {
return errServerKeyExchange
}
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 {View on GitHub (pinned to b6b368adc5)
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.
Example fix
// before (server-side selection logic, conceptual)
if /* tls12-or-below */ {
sigAlg = pickAlg(clientSupported) // may return MLDSA*
}
// after: forbid ML-DSA unless TLS 1.3
if /* tls12-or-below */ && isMLDSA(sigAlg) {
sigAlg = pickNonMLDSA(clientSupported)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before dialing, ensure ML-DSA schemes are not offered for TLS <= 1.2.
// (No public field exposes this directly; instead require TLS 1.3.)
if cfg, ok := conn.ConnectionState(); ok {
_ = cfg
}
// Best pre-check: enforce TLS 1.3 so the legacy path can't surface it.
config.MinVersion = tls.VersionTLS13 Type guard
// Guard: this error only arises on TLS < 1.3, so it is impossible once
// MinVersion >= VersionTLS13.
func isLegacyMLDSAPathBlocked(c *tls.Config) bool {
return c.MinVersion >= tls.VersionTLS13
} Try / catch
// errs := dial()
// var e error
// if errors.As(err, &e) && strings.Contains(e.Error(), "ML-DSA with TLS version < 1.3") {
// // peer is non-conformant or hostile; do not downgrade
// return fmt.Errorf("refusing insecure legacy ML-DSA handshake: %w", err)
// } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- tls: server's certificate uses ML-DSA, which requires TLS 1.
- tls: invalid server key share
- tls: invalid signature by the server certificate: {err}
- tls: invalid server finished hash
- tls: client using inappropriate protocol fallback
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/0797b7b7d875ac94.
Report an issue: GitHub.