Common MPC Pitfalls

tss-lib Shamir validation

Both failures appear in bnb-chain/tss-lib’s crypto/vss/feldman_vss.go and were disclosed together by Trail of Bits. They were fixed in a single PR #149.

Failure 1: zero index mod $q$. Before the fix, Create checked the party index against the integer literal 0 without reducing modulo $q$ first (source):

1// crypto/vss/feldman_vss.go, bnb-chain/tss-lib (vulnerable, pre-PR #149)
2for i := 0; i < num; i++ {
3    if indexes[i].Cmp(big.NewInt(0)) == 0 {
4        return nil, nil, fmt.Errorf("party index should not be 0")
5    }
6    // indexes[i] == q passes the check; evaluatePolynomial(q) ≡ f(0) = secret
7    share := evaluatePolynomial(ec, threshold, poly, indexes[i])
8    shares[i] = &Share{Threshold: threshold, ID: indexes[i], Share: share}
9}

A malicious party submits index $i = q$. The literal-zero check passes, but evaluatePolynomial(q) ≡ evaluatePolynomial(0) = f(0) = s, handing the attacker the shared secret as their share.

Failure 2: duplicate indices mod $q$. The same file’s ReConstruct performs Lagrange interpolation by inverting the index difference $x_j - x_k$ via ModInverse (source):

1// crypto/vss/feldman_vss.go, bnb-chain/tss-lib (Lagrange step in ReConstruct)
2sub := modN.Sub(xs[j], share.ID)
3subInv := modN.ModInverse(sub)         // nil if sub ≡ 0 mod q
4div := modN.Mul(xs[j], subInv)         // nil-pointer dereference
5times = modN.Mul(times, div)

A malicious party submits $x_j = x_k + q$ for some honest party $k$. The raw integers differ, so any non-modular != check passes; modular reduction makes $x_j \equiv x_k$, sub is zero, ModInverse returns nil, and the next operation panics, DoS-ing the signing ceremony.

Unified fix: CheckIndexes. PR #149 added a single validation helper called at the start of Create. It reduces each index modulo $q$, rejects zero, and rejects duplicates in one pass (source):

 1// crypto/vss/feldman_vss.go, bnb-chain/tss-lib (fixed, PR #149)
 2func CheckIndexes(ec elliptic.Curve, indexes []*big.Int) ([]*big.Int, error) {
 3    visited := make(map[string]struct{})
 4    for _, v := range indexes {
 5        vMod := new(big.Int).Mod(v, ec.Params().N)
 6        if vMod.Cmp(zero) == 0 {
 7            return nil, errors.New("party index should not be 0")
 8        }
 9        vModStr := vMod.String()
10        if _, ok := visited[vModStr]; ok {
11            return nil, fmt.Errorf("duplicate indexes %s", vModStr)
12        }
13        visited[vModStr] = struct{}{}
14    }
15    return indexes, nil
16}