Pitfall

Curve Points Not Validated

What can go wrong. In MPC protocols that rely on elliptic-curve groups (e.g., threshold ECDSA in GG18/GG20 or BLS aggregate signatures) parties exchange curve points $(X, Y)$ that under any scalar multiplication may leak part or all of the secret, or break the soundness of an aggregation or pairing equation, if the point fails any of the validity conditions below. Three checks apply to every exchanged curve point:

  1. Canonical encoding. Require $X, Y \in [0, p)$ at the wire boundary.
  2. On-curve check. $(X, Y)$ must satisfy the curve equation $Y^2 \equiv X^3 + aX + b \pmod{p}$. A pair on an invalid curve $Y^2 = X^3 + aX + b'$ with smooth order $n'$ is silently processed by APIs (like Go’s elliptic.Curve.ScalarMult) that depend only on $p$ and $a$, not on the curve constant $b$. The point $(0, 0)$, which Go represents as the in-memory sentinel for the identity, is a second degenerate case and must be rejected independently.
  3. Subgroup membership. Even an on-curve point may lie outside the intended prime-order subgroup if the curve has cofactor $h > 1$. The standard check is $q \cdot P = \mathcal{O}$ for the prime-order subgroup of order $q$, or equivalently multiplication by the cofactor with rejection of the resulting identity.

Depending on the protocol, omitting any one of these checks lets a malicious party choose a curve point $P$ that leaks bits of a secret scalar on every scalar multiplication, breaks the soundness of an aggregation or pairing-based verification, or desynchronizes the transcript and commitment state across MPC rounds.

Security implication. Missing point validation entails different attacks depending on the protocol. In ECDH protocols, an adversary supplies a point whose order has a small factor $n'$, and every scalar multiplication $k \cdot P$ by an honest secret $k$ reveals $k \bmod n'$. This primitive underlies both invalid curve attacks (Biham, Neumann, SAC 2019) and small-subgroup attacks. In threshold EdDSA over Curve25519, ZenGo’s Baby Sharks analysis showed that an injected torsion component shifts the joint key off the prime-order subgroup, so a forged signature verifies with probability about $1/8$ under some implementations while others reject it; that cross-implementation discrepancy can split honest verifiers and halt consensus. In BLS aggregation over BLS12-381, a non-subgroup public key satisfies the pairing equation for crafted signatures without knowledge of the corresponding private key, giving full signature forgery. A separate failure mode appears when the wire encoding itself is non-canonical: the same point produces different Fiat-Shamir challenges between two honest parties, splitting them across sessions and invalidating commitment openings tied to the encoded representation.

How to avoid. Validate every externally-supplied point at the wire boundary, before any scalar multiplication by a secret touches it. For each $(X, Y)$ received from another party:

  • Canonical range: reject if $X \not\in [0, p)$ or $Y \not\in [0, p)$.
  • On-curve check: reject if $(X, Y)$ does not satisfy $Y^2 \equiv X^3 + aX + b \pmod{p}$, and reject the identity $(0, 0)$ independently.
  • Subgroup membership (when cofactor $h > 1$): check $q \cdot P = \mathcal{O}$, where $q$ is the order of the prime-order subgroup.

For fresh curve designs, prefer a prime-order curve ($h = 1$, e.g. secp256k1 or P-256), where the subgroup check is implied by the on-curve check, or the Ristretto255/Decaf abstraction, which exposes only the prime-order quotient group and makes torsion injection structurally impossible.

Example tss-lib threshold EdDSA missing cofactor clearing (Baby Sharks, PR #115)

Standard EdDSA defends against small-subgroup attacks via bit clamping on the single-party secret scalar. The threshold EdDSA path in tss-lib applied no equivalent defense to supplied points received from peers, so as the ZenGo’s Baby Sharks analysis showed, a malicious party could inject an order-8 torsion component into the joint public key so that $1/8$ of signing ceremonies verify, while the other will reject.

In the pre-fix tss-lib, the received commitment $R_j$ was constructed straight from peer-supplied coordinates and aggregated into the joint $R$ with no subgroup-membership step (source):

1// eddsa/signing/round_3.go — bnb-chain/tss-lib (pre-fix)
2Rj, err := crypto.NewECPoint(tss.EC(), coordinates[0], coordinates[1])
3if err != nil {
4    return round.WrapError(errors.Wrapf(err, "NewECPoint(Rj)"), Pj)
5}
6// ... proof.Verify(Rj) checks knowledge of the discrete log, not subgroup ...
7extendedRj := ecPointToExtendedElement(Rj.X(), Rj.Y())
8R = addExtendedElements(R, extendedRj)

The remediation landed in PR #115. It adds an EightInvEight() helper in crypto/ecpoint.go that multiplies by 8 then by $8^{-1} \bmod N$, projecting any input into the prime-order subgroup (source):

1// crypto/ecpoint.go — bnb-chain/tss-lib (post-fix)
2var (
3    eight    = big.NewInt(8)
4    eightInv = new(big.Int).ModInverse(eight, edwards.Edwards().Params().N)
5)
6
7func (p *ECPoint) EightInvEight() *ECPoint {
8    return p.ScalarMult(eight).ScalarMult(eightInv)
9}

The helper is then applied to every received point. The patch at the signing site, mirroring the pre-fix excerpt above (source):

1// eddsa/signing/round_3.go — bnb-chain/tss-lib (post-fix)
2Rj, err := crypto.NewECPoint(tss.EC(), coordinates[0], coordinates[1])
3Rj = Rj.EightInvEight()
4if err != nil {
5    return round.WrapError(errors.Wrapf(err, "NewECPoint(Rj)"), Pj)
6}