The Fireblocks cryptography research team found a complete forgery in a post-quantum signature candidate: a valid signature for any message can be constructed from the public key alone in a fraction of a second on a laptop.
The scheme is Shipovnik, a post quantum signature candidate in TC26, Russia’s technical committee for cryptography. If it were used to authorize transactions, anyone could sign any transaction for any wallet.
Both published implementations accept the forgery: the C implementation QApp published for TC26 with the designers, and the designers’ Python implementation.
Why We Audited It
Fireblocks has been researching post-quantum signature candidates as we prepare custody infrastructure for the schemes that will replace ECDSA and EdDSA, the default signature schemes used by most blockchains. Some of those candidates will one day be signed with institutional keys, so the best time to find a flaw in them is before anyone even issues a key.
The next sections are the technical writeup. In high level the finding can be summed up as: the signature contains an object the security proof relied on being well-formed, but the verifier never checks that it is. Without that check, anyone can forge a signature that verifies under any public key and any message they choose.
How Shipovnik Works
A Shipovnik public key is 1448 bits. The private key is 2896 bits, essentially two blocks of 1448 bits laid end to end: a left block, then a right block. A real private key has exactly 318 ones. That count is its weight, and the scheme’s security relies on it: a signature is a proof that the signer knows a valid private key with that count.
Turning the private key into the public key is done by a loop of XORs. For each index i:
public_key[i] = XOR(many bits of the left block) XOR right[i]
Which left bits get included is a fixed public table, the same for every key. One output bit mixes in many left bits. From the right block it mixes in one bit, the bit at the same index.
A signature is bound to a message, and it proves two things without revealing the private key. The XOR loop on the private key produces the public key, and the private key has weight 318.
Obviously the verifier cannot count the private key’s ones directly, so the signer builds a new string by reading the private key through a list of 2896 indexes, and the verifier counts the ones in that string:
reordered[i] = private_key[index_list[i]]
The list is supposed to be a permutation of 0..2895: each index once. Each bit of the private key is then written once, so the new string has the same number of ones, and a count of 318 means the private key has weight 318. The verifier never checks that each index appears once. Repeat an index and that bit is written more than once, so the count can be 318 for a private key of a different weight.
Each round the verifier asks one of three questions. Only one of them is this count. The other two compare hashes. Someone without the private key can prepare two of the three answers, and 219 rounds is what makes that bluff fail in practice. The message decides which question is asked, so the signature verifies only for the message it was built for. The rounds are Stern’s protocol, made non-interactive by Fiat-Shamir.
How The Forgery Works
This is the same shape of bug as ECDSA signatures accepted with both components zero, as elliptic curve points that were never checked to be on the curve, and as BitForge, where GG18 and GG20 implementations accepted a Paillier modulus that was never checked for small prime factors. The proof assumed a well-formed object, and the verifier never checked that it was.
To create a forged private key that can be used to forge a signature, copy the public key into the right block and fill the left block with zeros:
forged_private_key = 000...0 || public_key
Every left bit is 0, and x XOR 0 equals x, so the left bits in the loop change nothing. Output bit i equals right[i], which is the public key’s bit i. The loop returns the public key like the verifier expects.

The ones in this string are the ones in the public key. The left block adds none. A public key is 1448 random-looking bits, about half of them 1, so the forged private key has about 724 ones, which is more than the 318 the verifier expects. With an index list that is a valid permutation the weight check would see about 724 ones and reject it.
Instead, a list can be constructed such that it repeats a position that holds a 1, and fills every other entry with a position that holds a 0:
index_list = [index_of_a_one] * 318 + [index_of_a_zero] * (2896 - 318)
Read the forged private key through that list and the same 1 is written 318 times. On the round that counts ones, the verifier counts 318. The other two questions only check hashes, and they pass because the openings match the commitments and the XOR loop on this forged key returns the public key. The list is not a permutation of 0..2895, and the bug is that the verifier never tests that it is.
This Is What Public Review Is For
ECDSA and EdDSA are the default signature schemes on most production chains and wallets. They got there after years of public cryptanalysis and production use. NIST took the better part of a decade of review before it standardized ML-DSA and SLH-DSA.
That is why those processes are public and why they take years. Candidates are published for public cryptanalysis before a committee standardizes a scheme. That review is what makes a standard safe to deploy with production keys.
You can find more resources covering quantum and security within the Fireblocks content hub. If you’d like to run the proof of concept yourself, you can find the code here.