golang/go · critical

crypto/ecdh: bad X25519 remote ECDH input: low order point

Error message

crypto/ecdh: bad X25519 remote ECDH input: low order point

What it means

Thrown by x25519Curve.ecdh after computing the shared secret via x25519ScalarMult. If the output is all zeros (isZero(out)), the remote public key was a low-order point — a well-known malicious or degenerate input that forces the shared secret to zero. This is a critical security check that prevents small-subgroup and all-zero output attacks per RFC 7748 Section 6.1.

Source

Thrown at src/crypto/ecdh/x25519.go:86

func (c *x25519Curve) NewPublicKey(key []byte) (*PublicKey, error) {
	if fips140only.Enforced() {
		return nil, errors.New("crypto/ecdh: use of X25519 is not allowed in FIPS 140-only mode")
	}
	if len(key) != x25519PublicKeySize {
		return nil, errors.New("crypto/ecdh: invalid public key")
	}
	return &PublicKey{
		curve:     c,
		publicKey: bytes.Clone(key),
	}, nil
}

func (c *x25519Curve) ecdh(local *PrivateKey, remote *PublicKey) ([]byte, error) {
	out := make([]byte, x25519SharedSecretSize)
	x25519ScalarMult(out, local.privateKey, remote.publicKey)
	if isZero(out) {
		return nil, errors.New("crypto/ecdh: bad X25519 remote ECDH input: low order point")
	}
	return out, nil
}

func x25519ScalarMult(dst, scalar, point []byte) {
	var e [32]byte

	copy(e[:], scalar[:])
	e[0] &= 248
	e[31] &= 127
	e[31] |= 64

	var x1, x2, z2, x3, z3, tmp0, tmp1 field.Element
	x1.SetBytes(point[:])
	x2.One()
	x3.Set(&x1)
	z3.One()

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Treat this as a security-critical failure: abort the key exchange and close the connection — never proceed with a zero shared secret.
  2. Validate the remote public key provenance (TLS certificate chain, signed key exchange messages) before calling ECDH to prevent adversarial inputs.
  3. If this occurs during testing, replace placeholder zero-byte keys with keys generated by ecdh.X25519().GenerateKey().

Example fix

// before
secret, err := priv.ECDH(remotePub)
if err != nil { log.Println(err); return err }

// after
secret, err := priv.ECDH(remotePub)
if err != nil {
    // low-order point — abort key exchange, do not retry
    return fmt.Errorf("key exchange failed (possible attack): %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

func isLikelyLowOrderPoint(pub *ecdh.PublicKey) bool {
    // X25519 all-zero key is the most common low-order point
    for _, b := range pub.Bytes() {
        if b != 0 { return false }
    }
    return true
}

Try / catch

secret, err := priv.ECDH(remotePub)
if err != nil {
    // Abort immediately — do not retry or fall back.
    // This may indicate an adversarial peer.
    return fmt.Errorf("ECDH failed, possible low-order attack: %w", err)
}

Prevention

When it happens

Trigger: Calling ECDH on a PrivateKey with a remote PublicKey that is a low-order point (e.g., all-zero bytes, or one of the few known small-subgroup points on the Montgomery curve). This typically arises when the peer sends a crafted or corrupted public key, or when an attacker injects an all-zero key to force a predictable shared secret.

Common situations: A TLS or Noise protocol peer sends a malformed or adversarial X25519 public key; a key exchange where the remote side has a bug producing zero-filled buffers; testing with placeholder/zero keys; a man-in-the-middle injecting a degenerate point to force a known shared secret.

Related errors


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