Common MPC Pitfalls

tss-lib DLN bases without proofs or bounds

GG18/GG20 range proofs instantiate Pedersen commitments under auxiliary bases $h_1, h_2 \in \mathbb{Z}_{\tilde N}^*$ and assume those bases generate the same large subgroup. Two successive bugs hit the tss-lib library on this exact surface.

Original keygen broadcast ships bases with no DLN proof at all. Round 2 of ECDSA keygen stored the incoming triple $(\tilde N, h_1, h_2)$ directly, with no validation (source):

1// FILE: ecdsa/keygen/round_2.go
2// bnb-chain/tss-lib @ 6584db7f (pre-PR #89, vulnerable)
3for j, msg := range round.temp.kgRound1Messages {
4    r1msg := msg.Content().(*KGRound1Message)
5    round.save.PaillierPKs[j] = r1msg.UnmarshalPaillierPK() // used in round 4
6    round.save.NTildej[j] = r1msg.UnmarshalNTilde()
7    round.save.H1j[j], round.save.H2j[j] = r1msg.UnmarshalH1(), r1msg.UnmarshalH2()
8    // ...
9}

A malicious peer sets $h_2 = 1$ so each subsequent MtA range-proof commitment collapses to $z = h_1^s \bmod \tilde N$, revealing $h_1^s$; the attacker then reconstructs $s$ either by choosing $\tilde N$ as a product of small prime factors so $\phi(\tilde N)$ is smooth and applying Pohlig-Hellman on each factor combined with CRT, or by choosing $\tilde N$ large enough that recovery reduces to an integer logarithm problem.

PR #89 wraps the same loop with a $h_1 \ne h_2$ guard, a uniqueness check on $h_1, h_2$ across parties, and two concurrent DLN proof verifications before any party’s bases are accepted (source):

 1// FILE: ecdsa/keygen/round_2.go (lines 51-62)
 2// bnb-chain/tss-lib @ c66e035b (post-PR #89, v1.2.0+)
 3go func(j int, msg tss.ParsedMessage, r1msg *KGRound1Message, H1j, H2j, NTildej *big.Int) {
 4    if dlnProof1, err := r1msg.UnmarshalDLNProof1(); err != nil || !dlnProof1.Verify(H1j, H2j, NTildej) {
 5        dlnProof1FailCulprits[j] = msg.GetFrom()
 6    }
 7    wg.Done()
 8}(j, msg, r1msg, H1j, H2j, NTildej)
 9go func(j int, msg tss.ParsedMessage, r1msg *KGRound1Message, H1j, H2j, NTildej *big.Int) {
10    if dlnProof2, err := r1msg.UnmarshalDLNProof2(); err != nil || !dlnProof2.Verify(H2j, H1j, NTildej) {
11        dlnProof2FailCulprits[j] = msg.GetFrom()
12    }
13    wg.Done()
14}(j, msg, r1msg, H1j, H2j, NTildej)

Follow-on: Verify itself accepted degenerate $h_1, h_2$ (Trail of Bits TOB-BIN-8). Even with DLN proofs verified at keygen, the Verify routine inside crypto/dlnproof/proof.go ran the Sigma-protocol equation checks without any sanity on the inputs themselves. It accepted $h_1, h_2 \in \{0, 1, \tilde N - 1\}$ or $h_1 = h_2$, and likewise accepted arbitrary $T[i], \mathrm{Alpha}[i]$ (source):

 1// FILE: crypto/dlnproof/proof.go
 2// bnb-chain/tss-lib @ c65c3564 (pre-TOB-BIN-8, vulnerable)
 3func (p *Proof) Verify(h1, h2, N *big.Int) bool {
 4    if p == nil {
 5        return false
 6    }
 7    modN := common.ModInt(N)
 8    msg := append([]*big.Int{h1, h2, N}, p.Alpha[:]...)
 9    c := common.SHA512_256i(msg...)
10    // ... Iterations rounds of Sigma-protocol equation checks ...
11    return true
12}

The TOB-BIN-8 fix (commit c0a1d4e4) added bounds checks for $h_1, h_2$ in $(1, \tilde N)$, the $h_1 \ne h_2$ guard, and matching per-element bounds on every entry of $T[]$ and $\mathrm{Alpha}[]$ (source):

 1// FILE: crypto/dlnproof/proof.go
 2// bnb-chain/tss-lib @ c0a1d4e4 (fixed, TOB-BIN-8)
 3func (p *Proof) Verify(h1, h2, N *big.Int) bool {
 4    if p == nil {
 5        return false
 6    }
 7    if N.Sign() != 1 {
 8        return false
 9    }
10    modN := common.ModInt(N)
11    h1_ := new(big.Int).Mod(h1, N)
12    if h1_.Cmp(one) != 1 || h1_.Cmp(N) != -1 {
13        return false
14    }
15    h2_ := new(big.Int).Mod(h2, N)
16    if h2_.Cmp(one) != 1 || h2_.Cmp(N) != -1 {
17        return false
18    }
19    if h1_.Cmp(h2_) == 0 {
20        return false
21    }
22    for i := range p.T {
23        a := new(big.Int).Mod(p.T[i], N)
24        if a.Cmp(one) != 1 || a.Cmp(N) != -1 {
25            return false
26        }
27    }
28    for i := range p.Alpha {
29        a := new(big.Int).Mod(p.Alpha[i], N)
30        if a.Cmp(one) != 1 || a.Cmp(N) != -1 {
31            return false
32        }
33    }
34    // ... Sigma-protocol equation checks (Iterations rounds) unchanged ...
35    return true
36}