Threshold cryptography looks bulletproof on paper. You distribute trust across multiple parties, require a minimum threshold to reconstruct secrets or produce signatures, and suddenly you've got resilience against single points of failure. The math checks out, the security proofs are elegant, and the protocols feel rock-solid.
Then you ship to production and things start breaking. Sometimes it's a missing validation check that nobody thought mattered. Sometimes it's a subtle ambiguity in how data gets encoded. The gap between "provably secure threshold scheme" and "production-ready implementation that won't get your users rekt" is wider than most people realize.
This post walks through real attacks on real threshold systems, the kind that security auditors have actually found in the wild. The goal isn't to scare you away from threshold schemes, they're still one of the best tools we have for distributed trust. The goal is to give you a practical checklist of what actually goes wrong, so you can audit your own implementation with clear eyes before it hits mainnet.
Pedersen DKG: Wrong-Degree Polynomial Attack
During an audit of Frost, Trail of Bits discovered an interesting vulnerability. The participants do not check the length of the polynomial coefficients, meaning the polynomial can be of much higher degree which would result in an inability to recover the key.
Pedersen's distributed key generation builds on Feldman's verifiable secret sharing, which extends Shamir's scheme by letting participants verify their shares without a trusted dealer. The basic flow: each party Pi generates a random degree-t polynomial pi(x)=ai,0+ai,1x+⋯+ai,txt where ai,0 is their secret contribution. They publish commitments to each coefficient:
Ai,0=gai,0,Ai,1=gai,1,…,Ai,t=gai,t
Then Pi sends the evaluation si,j=pi(j) privately to each party Pj. Recipient Pj verifies their share by checking:
gsi,j=?k=0∏tAi,kjk
To form the final shared secret, each party Pj sums all shares they received: sj=∑i=1nsi,j. The collective polynomial is p(x)=∑i=1npi(x), which has degree t and secret s=p(0)=∑i=1nai,0. Any t+1 parties can recover s via Lagrange interpolation.
The Attack
During an audit of FROST implementations, they discovered that many don't validate the length of the commitment vector. A malicious Pi can submit a polynomial of degree T>t, say, degree 2t or even degree n. Since the final polynomial degree is the maximum of all input degrees, this silently raises the reconstruction threshold from t+1 to T+1.
If T≥n, the secret becomes unrecoverable: no subset of parties can reconstruct it, even with everyone's cooperation. All shares are now locked, and any funds controlled by that key are permanently lost. Even if T<n, signature attempts with only t+1 parties will mysteriously fail, and implementations may blame honest participants for the failure.
The fix is trivial: check that each commitment vector has exactly t+1 elements. Trail of Bits found ten vulnerable implementations, including multiple FROST variants and GG18/GG20 libraries.
Missing Discrete Log Check in MtA
The Multiplicative-to-Additive (MtA) protocol is common in threshold schemes, it converts shares of a product a×b into additive shares α+β≡a×b(modq). Most implementations include zero-knowledge proofs to ensure parties behave honestly, but these proofs require careful setup.
The Setup
Each party generates auxiliary parameters for the ZK proofs:
An RSA modulus N~=pq (factorization kept secret)
Two group elements h1,h2∈ZN~∗ that should be unrelated (no known discrete log between them)
The commitment in the proof typically looks like z=h1mh2ρmodN~, where m is the value being proven and ρ is blinding randomness. The security relies on the prover not knowing logh1(h2), if they did, they could break the proof's soundness.
The Attack
The protocol should include a check that h1 and h2 generate the same cyclic subgroup and that their discrete log relation is unknown. But many implementations skip this verification entirely, trusting the sender to have generated them correctly.
An attacker exploits this by setting h2=1. Now the commitment collapses:
z=h1mh2ρ=h1m⋅1ρ=h1mmodN~
The blinding term vanishes, and z directly reveals h1m. To extract m, the attacker has two options:
Option 1: Integer logarithm
Choose N~ extremely large so that computing discrete logs in ZN~∗ becomes as hard as computing integer logarithms, which is trivial with standard algorithms.
Option 2: Pohlig-Hellman
Choose N~ as a product of many small primes: N~=p1p2⋯pk where each pi is small. The order of ZN~∗ is then ϕ(N~)=(p1−1)(p2−1)⋯(pk−1), which is smooth. Using Pohlig-Hellman, the attacker computes mmod(pi−1) for each factor (fast, since the factors are small), then reconstructs m via the Chinese Remainder Theorem.
Either way, the attacker recovers m and breaks the proof. This leaks the victim's secret value that was supposed to be protected by the zero-knowledge property.
The Fix: The sender should prove in zero-knowledge that logh1(h2) is unknown and that h1 and h2 generate the same cyclic group. Additionally, the Paillier modulus N~ must be proven to be the product of two primes of sufficient size.
BitGo Wallet: Missing Proof of Knowledge
Proofs of knowledge aren't just formalities, they're critical checkpoints that prevent malicious parties from feeding garbage into the protocol. The BitGo wallet vulnerability shows what happens when you skip them.
The Protocol
BitGo's TSS implementation follows the GG18 key generation protocol, which has three phases:
Commit: Each player Pi selects a secret key share ui and broadcasts:
A commitment gui to their secret
A Paillier public key Ei with modulus Ni
Reveal & Share: Each player opens their commitment (reveals ui), then performs Feldman VSS to distribute shares of ui. The final shared secret is x=∑ui, with public key y=gx=∏gui. Each player ends up with a share xi such that any threshold subset can reconstruct x.
Prove: Each player should prove in zero-knowledge:
Knowledge of their share xi (proof of knowledge)
That their Paillier modulus Ni=piqi is the product of exactly two primes (biprimality proof)
BitGo's implementation skipped phase 3 entirely. No proofs. Just trust.
The Attack
The attacker exploits the missing biprimality proof by choosing a malicious Paillier modulus N=pq where q=2p+1. This is a safe prime construction, but with a twist: the multiplicative group ZN∗ has a special structure that leaks information. The attacker also picks any square V∈ZN∗ (say, V=4) and broadcasts (N,V) as their Paillier parameters.
During the protocol, an honest party Pj (the victim) will encrypt their share under the attacker's key. They send:
C=Vx⋅Enc(β)modN2
where x is the victim's secret share and β is some randomness. The Paillier encryption is Enc(β)=(1+N)βrN for random r.
Now the attacker reduces C modulo q:
C≡Vx⋅(1+N)βrN≡Vx⋅rN(modq)
The (1+N)β term vanishes mod q since N≡0(modq). Next, the attacker computes:
Cp+1≡(VxrN)p+1≡Vx(p+1)rN(p+1)(modq)
Here's the key observation: in Zq∗, the order is q−1=2p. Every square (like V) has order dividing p, and rpq≡1(modq) by Fermat's little theorem. So:
Cp+1≡Vx(p+1)≡Vx(modq)
because Vp≡1(modq).
Now the attacker has Vxmodq. Since q=2p+1 is small enough (the attacker chose it), they can brute-force the discrete log to recover xmodq.
Scaling the Attack
If q is too large for a single brute-force, the attacker can use a product of many such pairs: N=p1q1⋯p16q16 where each qi=2pi+1 and each pi is small (say, 16 bits). They run the attack in parallel to recover xmodqi for each i, then use the Chinese Remainder Theorem to reconstruct x.
With 16 such pairs and 16-bit primes, the attacker recovers a 256-bit secret share in a few hours on commodity hardware. Once they have one share, they can sign arbitrary transactions unilaterally, completely defeating the threshold property.
This vulnerability was discovered by Fireblocks during a security analysis of BitGo's implementation and described in their blog post.
The Fix: Include the missing proofs. The Paillier modulus Ni must be proven biprime (product of exactly two large primes), and each player must prove knowledge of their share xi.
Not So Secret Factorization
Sometimes the attack isn't a missing check, it's that the implementation gives the prover knowledge they were never supposed to have. This vulnerability in a two-party ECDSA reshare protocol shows how knowing "too much" can be weaponized.
The Reshare Protocol
Key resharing is standard practice in production TSS wallets, you periodically rotate shares to limit exposure. The protocol looks simple enough:
Alice generates a fresh Paillier keypair (n′,sk′)
Alice sends a new ciphertext c′=Enc(x1′) under her new key, where x1′=x1+r (her old share plus randomness)
Alice proves in zero-knowledge that c′ encrypts a value linearly related to her old ciphertext c=Enc(x1), i.e., c′=c⊕Enc(r) (using Paillier homomorphism)
Bob receives c′, verifies the proof, and updates his own share to x2′=x2−r. The combined secret remains unchanged: x1′+x2′=(x1+r)+(x2−r)=x1+x2=x.
The Problem
The zero-knowledge proof for step 3 requires two moduli n1 and n2. The security assumption from the literature: "Let n1 be a large composite number whose factorization is unknown by Alice and Bob, and n2 be another large number, prime or composite whose factorization is known or unknown by Alice."
But in this implementation, Alice knows both factorizations, she generated both Paillier keys herself. She can now turn the reshare protocol into an oracle that leaks Bob's share bit by bit.
The Attack
Alice manipulates her new share to be x1′′=x1′+kq where k is chosen so that:
∣ni∣−q≤x1+kq<∣ni∣
Bob doesn't detect anything wrong. He combines Alice's malicious share with his rotated share:
x12=x1′′+x2′=(x1′+kq)+(x2−r)=x1+x2+kq
When they run the signing protocol, Bob sends his partial signature to Alice. Alice decrypts the combined result and reduces it mod q. But during Paillier decryption, a reduction mod ni happens first. This is where the oracle kicks in:
Ifx2<ni−kq−x1, then the mod ni reduction doesn't wrap around. The final signature verifies correctly.
Ifx2≥ni−kq−x1, then the mod ni reduction changes the value. The signature fails verification.
Alice just learned whether x2 crosses a specific threshold. She records the result.
Extracting the Secret
By repeating this reshare-and-sign cycle with different values of k and ni, Alice narrows down Bob's share. Each successful or failed signature reveals one bit of information about x2. With enough iterations (proportional to the bit length of x2), Alice reconstructs Bob's entire share.
Once she has both x1 and x2, the threshold property collapses. Alice can sign any transaction unilaterally.
This issue was found during an analysis of a production two-party ECDSA implementation and is detailed in the paper "Attacking Threshold Wallets".
The Fix
The ZK proof must enforce that Alice doesn't know the factorization of n1. Better yet, the library maintainers did what made most sense: they replaced the vulnerable reshare with the full two-party key generation protocol. Alice and Bob run a coin flip to refresh their shares and repeat all ZK proofs from DKG. It's slower, but it's correct, and it doesn't give Alice an oracle.
Ambiguous Hash Encoding
Fiat-Shamir is supposed to be straightforward: hash the transcript, use the output as your challenge. But if you're sloppy about how you encode the transcript, you hand the prover a degree of freedom they're not supposed to have.
The Flaw
When converting an interactive proof to non-interactive via Fiat-Shamir, you need to hash multiple values together, group elements, integers, commitments, etc. A naive approach: concatenate everything with a delimiter.
H(α1,∣,D,∣,α2,∣,D,∣,…,∣,D,∣,αn)
where D is some delimiter byte sequence (say, || or a null byte). The problem: during the "opening" phase, there's ambiguity. Is the first element α1, or is it α1,∣,D,∣,α2? If the prover can shift boundaries around, they can manipulate which parts of the input get interpreted as which values, without changing the hash output.
Example: Discrete Log Proof
Consider a simple discrete log proof (like the one used in threshold signature ZK subprotocols):
Prover commits: Pick random ρ, send α=hρ (or α=hρg−1 depending on strategy)
Challenge: Compute challenge bits c=H(g,h,N,α1,α2,…,αλ) where λ is the number of repetitions
Response: For each iteration i, send τi=ρi+ci⋅secret
The proof is repeated λ times to amplify soundness. Each iteration contributes one challenge bit ci.
The Attack
The attacker (playing the prover) wants to forge a proof for loghgwithout knowing it. Here's how:
Guess the challenge bits: The attacker anticipates how many 1 bits will appear in the challenge c=(c1,…,cλ). Say they guess k ones.
Craft ambiguous commitments: For each iteration i, the attacker prepares two possible α values:
α1=hρi if they plan to answer as if ci=0
α2=hρig−1 if they plan to answer as if ci=1
Commit ambiguously: The attacker sends a byte stream that looks like α1,∣,D,∣,α2,∣,D,∣,… but can be parsed in multiple ways. Specifically, they construct it so that after hashing, they can "shuffle" which bytes belong to which αi based on the resulting challenge bits.
Reinterpret after seeing the challenge: Once the hash outputs challenge bits ci, the attacker retroactively decides how to parse their commitment. If ci=0, they claim they sent α1=hρi. If ci=1, they claim they sent α2=hρig−1. In both cases, they respond with τi=ρi.
Verification passes: The verifier checks hτi=?αi⋅gci. If the attacker correctly shuffled:
For ci=0: hρi=α1 ✓
For ci=1: hρi=α2⋅g=(hρig−1)⋅g ✓
The key insight: as long as the number of a tokens in the byte stream matches what the hash function expects (regardless of how they're interpreted), the hash remains unchanged. The attacker rearranges (α,α,…,α,β,β,…,β) to match the challenge bits, and it all verifies.
This vulnerability (the α-shuffle attack) was discovered by Verichains on Binance's tss-lib, Multichain and a few others. The affected implementations and full technical details are described in their blog post.
The Fix
Use unambiguous encoding. Don't just concatenate with delimiters. Modern protocols like FROST and CGGMP20 mandate specific serialization formats precisely to avoid this. Don't roll your own concatenation scheme.
Not Enough Soundness
Remember that discrete log proof we just talked about? It gets repeated λ times, and the soundness error is 2−λ. To forge a proof without knowing the discrete log, an attacker needs to guess all λ challenge bits correctly, probability 2−λ.
If you want 128-bit security, you need λ=128 repetitions. Straightforward.
The Attack
Some production implementations used λ=80, or even λ=32. At λ=80, an attacker has a 2−80 chance of forging a proof, feasible with enough compute. At λ=32, it's trivial.
The TSSHOCK paper found libraries that just... set the parameter too low. No clever cryptographic insight needed. Just brute-force the challenge space until you get lucky.
This attack (c-guess) was also discovered by Verichains in Multichain's implementation.
The Fix: Set λ≥128. Read the security proof. Use the parameters the paper recommends, not whatever seemed "good enough" during implementation.
Non-Existent Inverse
So you read the previous section and thought, "let's just use a larger challenge space instead of repeating the protocol 128 times, sample c from all 256-bit strings instead of just 0,1." Sounds reasonable. One round, bigger challenge, same security. Except it doesn't work in composite-order groups.
The Flaw
In the standard dlnproof, you're proving knowledge of loghg in a group of order ord(g). The protocol:
Prover commits α=gρ for random ρ
Challenge c∈0,1,…,2256−1 (large range, single round)
Prover responds τ=ρ+c⋅secret
The verifier checks gτ=?α⋅hc.
This works fine in prime-order groups. But in composite-order groups (like ZN~∗ for an RSA modulus N~), things break.
The Attack
Suppose the attacker wants to forge a proof for loghgwhen this discrete log doesn't exist. Specifically, let h=ge where e is a small divisor of ord(g) and 2∣ord(g): loghg=e1 doesn't exist in Zord(g) because e has no multiplicative inverse modulo ord(g) (since gcd(e,ord(g))=1).
The attacker proceeds as follows:
Brute-force random ρ values until the resulting challenge c=H(…) is divisible by e. Since c is 256 bits, the probability that e∣c is roughly e1. If e is small (say, 32 bits), this takes negligible time.
Once e∣c, the attacker constructs the proof: (α,τ)=(gρmodN~,ρ+ec).
Verification:
gτ=gρ+c/e=gρ⋅gc/e=α⋅(ge)c/e=α⋅hc
The proof passes, even though the attacker doesn't know loghg.
Why It Works
In a prime-order group, ec wouldn't be well-defined unless e has an inverse. But in composite-order groups with a small factor e dividing the order, the attacker can "split" the challenge by that factor and forge proofs for non-existent discrete logs.
This is the third attack (c-split) in Verichains’ TSSHOCK suite and affects Axelar, ZenGo and. ING. Full details can be found in their article.
The Fix
Either:
Stick with prime-order groups (like the elliptic curve subgroup, not ZN~∗)
Use the multi-round version with binary challenges (80–128 rounds)
If you must use composite-order groups with large challenges, ensure the group order is prime or prove that all relevant discrete logs exist before running the protocol
This is why modern threshold ECDSA protocols work over elliptic curves (prime-order subgroups) for signature generation, and only use RSA groups (composite order) carefully in auxiliary ZK proofs with proper parameter checks.
Small Paillier Attack
Even when range proofs are included, implementation bugs can still cause problems. This attack exploits a missing validation on the Paillier modulus size itself.
The Setup
In MtAwc with range proofs, Bob (the party with secret b) sends a zero-knowledge proof that includes:
A challenge e∈Zq (computed via Fiat-Shamir)
A random blinding value γ∈ZN∗ (where N is Alice's Paillier public key)
A proof element t1=eβ′+γ (computed over the integers), where β′ is Bob's secret from the MtA protocol
The proof is supposed to hide β′. The security relies on γ being large enough to mask β′ when divided by e.
The Attack
The attacker (Alice) generates a Paillier modulus N that's slightly smaller than 2256. If the protocol doesn't explicitly validate the key size, this gets through.
Now look at what happens when Bob computes t1=eβ′+γ:
et1=β′+eγ
Normally, γ∈ZN∗ with N≈22048 and e≈2256, so eγ is large enough to hide β′ when divided by e. But if N≈q≈2256, then γ is only about 2256 bits. Now:
With probability 21, we have eγ<1 (i.e., γ<e)
With probability 1−2−16, we have eγ<215 (i.e., less than 15 bits)
This means β′≈⌊et1⌋ with high probability. The blinding almost vanishes.
Extracting the Secret
Alice also knows that with N≈q, if she sets her input a=1 in the MtA protocol, the decrypted value α′=b+β′ (over the integers) falls into one of two ranges:
If b+β′<N: No modular reduction, so α=b+β′
If b+β′∈[N,2N): Reduction occurs, so α=b+β′−N
Alice knows α (she decrypted it) and can approximate β′≈⌊et1⌋ from the proof. She tries several candidates:
β∈⌊et1⌋modq,,⌊et1⌋−1modq,,⌊et1⌋−2modq,…
For each candidate β, she computes two possibilities for Bob's secret b :
b(1)=α−βmodq (assuming no wraparound)
b(2)=α−β+Nmodq (assuming wraparound)
She checks which one is correct by verifying gb(1)=?gb or gb(2)=?gb, where gb is Bob's public key (known to everyone).
One of these will match. Alice has recovered Bob's secret b in a single signature.
Why It Works
Two bugs compound:
Wrong range for γ: The ZK proof spec says γ should be sampled from Zq2N, not ZN∗. Using ZN∗ breaks honest-verifier zero-knowledge, dividing by e leaks bits of β′ regardless of Paillier modulus size.
Missing Paillier size check: Even with the wrong range, if N were properly validated to be ≥22048, the leakage would be negligible. But without validation, Alice can choose N≈q and completely remove the blinding.
Together, these bugs turn a single signature into a full key extraction.
Broadcast channels in MPC aren't just a nice-to-have, they're a security requirement. The assumption is when a party broadcasts a value, everyone receives the same value. But some implementations cut corners and implement "broadcast" as a loop that sends individual messages to each participant. That's not the same thing, and here's why it matters.
The Attack on Verifiable Secret Sharing
Recall that in Feldman VSS (the basis for Pedersen DKG), the dealer commits to a polynomial p(x)=a0+a1x+⋯+at−1xt−1 by publishing commitments yi=gai for each coefficient. Each party Pj receives a share sj=p(j) and verifies it using:
gsj=?i=0∏t−1yiji
The commitments y0,y1,…,yt−1 are supposed to be broadcast, everyone sees the same vector.
Breaking the Broadcast Assumption
Suppose the adversary wants to forge a sharing for a secret a0 they don't know. Specifically, they want everyone to accept y0=ga0 as the commitment to the secret, but the adversary doesn't actually know a0.
If broadcast is implemented as individual messages, the adversary can send different commitments to each party:
Create a truncated polynomial: Pick p(x)=a2x2+a3x3+⋯+at−1xt−1 (omit a0 and a1)
Send shares: Compute sj=p(j) for each party Pj and send them (these are valid evaluations of the truncated polynomial)
Compute most commitments honestly: For i≥2, compute yi=gai (these are the same for everyone)
Customize y1 for each party: For each party Pj, compute a personalized commitment:
y1,j=(y0−1)j−1
Send different vectors: Send the vector y0,y1,j,y2,…,yt−1 to party Pj
The y0 and y1 terms cancel out! And since sj=p(j)=a2j2+⋯+at−1jt−1, we have:
gsj=ga2j2+⋯+at−1jt−1=y2j2⋯yt−1jt−1
The check passes for every party, even though the adversary doesn't know a0 or a1. Each party thinks they have a valid sharing of the secret corresponding to y0=ga0, but the adversary never computed consistent shares for that secret.
Threshold schemes remain one of our best tools for distributed trust, but the gap between "provably secure protocol" and "production-ready implementation" is real. The attacks covered here aren't exhaustive, they're a snapshot of what auditors have found.
The pattern is clear: missing checks and skipped proofs. Before you ship threshold signatures to production, audit for these classes of bugs. Check parameter sizes. Validate inputs. And include all the proofs the paper specifies. The math works. Make sure your implementation does too.