Pitfall

Smooth or Non-Biprime Paillier Modulus

What can go wrong. The Paillier cryptosystem relies on a biprime modulus $N = pq$ where $p$ and $q$ are large primes, often required to be safe primes, $p = 2p' + 1$ with $p'$ prime. When parties in an MPC protocol publish their own modulus, skipping biprimality checks lets a malicious sender pick a structured $N$ that enables key-recovery attacks against the protocols that use it.

Security implication. The BitForge attack refers to a collection of zero-day vulnerabilities discovered by Fireblocks researchers that impact MPC wallets. Part of these vulnerability involves skipping the biprimality and no-small-factor checks on the Paillier modulus in the GG18 & GG20 protocols, which led to a vulnerability on the shared key (CVE-2023-33241, technical report). The attacker chooses $N_A = p_1 \cdots p_{16} \cdot q$ with each $p_i \approx 2^{16}$ (small enough to brute-force the range proof), then crafts an out-of-range plaintext $k = N_A / p_i$ in each MtA call and forges the range proof by brute-forcing a blinding factor in about $p_i \approx 2^{16}$ attempts. The victim’s encrypted share leaks $x \bmod p_i$ per signing session; after 16 sessions, CRT reconstructs the full $x$.

How to avoid. Require every party publishing a Paillier key to accompany it with two ZK proofs from CGGMP21: a Paillier-Blum Modulus proof, which proves $N = pq$ for primes $p \equiv q \equiv 3 \pmod 4$, and a No-Small-Factor proof, which rules out small prime factors (formally $p, q > \sqrt{N}/2^{\ell}$ with $\ell = 256$, i.e. no factor below about $2^{256}$). Some deployments additionally require $p$ and $q$ to be safe primes ($p = 2p' + 1$ with $p'$ prime). Reject the participant if either proof fails to verify, before the modulus is stored anywhere.

Example Safeheron multi-party-ecdsa-cpp missing Paillier modulus validation (POC, CVE-2023-33241, PR #7, PR #10)

Safeheron’s multi-party-ecdsa-cpp ran GG18/GG20 key generation without checking the structure of each co-signer’s Paillier modulus $N$, so a non-biprime or smooth $N$ flowed through keygen and into the GG20 signing rounds unchecked. One example of vulnerable code is the Round 3 keygen verifier (pre-fix source):

1// FILE: src/multi-party-ecdsa/gg18/key_gen/round3.cpp
2// Safeheron/multi-party-ecdsa-cpp @ b75d125f (pre-fix, vulnerable)
3ok = bc_message_arr_[pos].pail_proof_.Verify(
4    sign_key.remote_parties_[pos].pail_pub_,
5    sign_key.remote_parties_[pos].index_,
6    bc_message_arr_[pos].dlog_proof_x_.pk_.x(),
7    bc_message_arr_[pos].dlog_proof_x_.pk_.y());

A malicious party could then publish $N = p_1 \cdots p_{16} \cdot q$ with each $p_i \approx 2^{16}$. During GG20 signing, the 16-factor structure opens parallel CRT channels and the small factors keep the MtA range-proof brute force at ~$2^{16}$ per channel. The victim’s encrypted share $x$ leaks $x \bmod p_i$ per session; CRT reconstructs the full share over 16 to ~$10^9$ sessions (Fireblocks technical report, POC).

Safeheron’s fix introduces two CGGMP21 proofs: PR #7 added a no-small-factor proof, and PR #10 replaced the share-binding pail_proof_ with the Paillier-Blum Modulus proof ($N = pq$ with $p \equiv q \equiv 3 \pmod 4$) verified directly against $N$.