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

  1. Verify the server is speaking TLS 1.3 (check negotiated version / the server's supported_versions extension) — ML-DSA belongs only there.
  2. 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.
  3. If you control the client config and must talk TLS 1.2, remove ML-DSA schemes from supportedSignatureAlgorithms so a server cannot select one.
  4. 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

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

Related errors


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