Common MPC Pitfalls

tss-lib threshold EdDSA missing cofactor clearing

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}