CRITICAL · Fiat-Shamir Hash Collision · CVE-2026-XXXX
Fireblocks MPC-LIB Vulnerability
C++ code compiled to WASM — runs real SHA256 in your browser · github.com/fireblocks/mpc-lib
Run the Exploit
Click "Init WASM" to load the C++ module, then "Exploit" to see the collision live.
WASM not loaded
Vulnerable Code Locations
Bug Detail
// mta.cpp:130 — generate_mta_range_zkp_seed (non-extended)
std::vector<uint8_t> n(BN_num_bytes(proof.A));
BN_bn2bin(proof.A, n.data());
SHA256_Update(&ctx, n.data(), BN_num_bytes(proof.S)); // ← 128 bytes hashed, not 512
// mta.cpp:104 — generate_mta_range_zkp_extended_seed (FIXED)
hasher.hash_bn(proof.A, verifier_paillier_pub_n_size * 2, "A"); // ← 512 bytes hashed
proof.A = Paillier ciphertext
512 bytes
2 × Paillier modulus (2048-bit key)
proof.S = Ring Pedersen commitment
128 bytes
Ring Pedersen modulus (1024-bit key)
Actually hashed by bug
128 bytes
Only 25% of A is bound to challenge e
Uncommitted by challenge
384 bytes
Can be modified without changing e
Attack
- Version rollback: Attacker claims version 8 (
MPC_RAND_R_VERSION) — below MPC_EXTENDED_MTA (11)
- Accept:
if (version > metadata.version) → 8 > 13 is false, accepted without error
- Activate bug:
metadata.version = version; → all parties use generate_mta_range_zkp_seed (buggy)
- Collide: Challenge e only depends on A[0:128]. Bytes 128-511 are arbitrary
- Forge: Attacker crafts A' (same A[0:128], different A[128:511]) and adjusts D' such that
C^z1 · enc(w,z2) = A' · D'^e passes with the unchanged e
C++ WASM Code
// /wasm/poc.cpp — compiled to 2.3KB WASM, zero dependencies
extern "C" {
void compute_all(const uint8_t* a1, const uint8_t* a2, int len, int blen, uint8_t* out) {
buggy_hash(a1, len, blen, out); // SHA256(first 128 bytes) → collision!
buggy_hash(a2, len, blen, out+32);
correct_hash(a1, len, out+64); // SHA256(all 512 bytes) → different
correct_hash(a2, len, out+96);
}
}
Fix
// mta.cpp:130 — REMOVE (wrong byte count):
SHA256_Update(&ctx, n.data(), BN_num_bytes(proof.S));
// REPLACE WITH (correct byte count):
SHA256_Update(&ctx, n.data(), BN_num_bytes(proof.A));
// AND in mpc_globals.h — enforce minimum version:
constexpr int MPC_MIN_SUPPORTED_PROTOCOL_VERSION = MPC_EXTENDED_MTA; // was 2