RSA signature forgery is the trick of producing a value that a careless verifier accepts as a genuine signature, without ever holding the private key. It does not break RSA. It breaks the code that checks the padding around the signed hash. When a library parses PKCS#1 v1.5 signatures loosely and the key uses a small public exponent such as three, an attacker can craft a number whose cube happens to look like a correctly padded hash, and a lax verifier waves it through. This post shows the padding the check relies on, why loose parsing plus a low exponent is forgeable, and how to shut it.
How PKCS#1 v1.5 signature verification is meant to work
RSA signing is the private operation and verifying is the public one. To sign, the owner hashes the message, wraps the hash in a fixed padding structure, and raises the whole thing to the private exponent. To verify, anyone raises the signature to the public exponent e and checks that the result is that exact padded structure around the expected hash. The padded block for a key whose modulus is k bytes looks like this:
00 01 FF FF FF ... FF 00 [ DigestInfo ] [ hash ]
The block starts with 00 01, then a long run of FF bytes that fills the block out to full width, then a single 00 separator, then a small DER structure called DigestInfo that names the hash algorithm, then the hash itself. The FF padding exists precisely to push the hash to the far right and leave no free space. A correct verifier rebuilds this entire block from the message and compares it byte for byte against what the signature decrypted to. If even one byte differs, it rejects.
Where RSA signature forgery comes from
The flaw is a verifier that does not rebuild the whole block and compare it. Instead it parses. It scans from the left, sees 00 01, skips forward over the FF bytes, finds the 00 separator, reads the DigestInfo, reads as many hash bytes as the algorithm needs, and then stops. The bug is in that word stops. A loose parser does not check that the hash sits hard against the right edge of the block, and it does not check that nothing follows. It treats any bytes after the hash as none of its business.
That single omission is the opening. If trailing bytes are ignored, the attacker no longer has to match the full width of the modulus. They only have to produce a number whose top bytes spell out 00 01 FF ... 00 DigestInfo hash, while the bottom bytes can be anything at all. And with a low public exponent, producing such a number is a cube root away.
A correct verifier asks whether the signature equals the one right value. A loose verifier asks only whether the signature starts the right way, and that is a question an attacker can answer without the key.
The low exponent cube root
Many RSA keys use a public exponent of three because it makes verification fast. Verifying then means cubing the signature and reducing modulo N. Here is the forging idea in plain terms. The attacker writes down the target block they want the cube to resemble: the 00 01 prefix, a short run of FF, the separator, the DigestInfo, and the real hash of their chosen message, all packed into the high end of a number, with the low end left as free space. They then take the ordinary integer cube root of that target and round it to a whole number. Call that s.
Because the structured part lives in the high bytes, cubing s reproduces those high bytes exactly, and whatever falls out in the low bytes is garbage the attacker never had to control. The forged signature is simply s. When a loose verifier cubes it, reads the prefix, walks to the hash, confirms the hash matches the message, and ignores the trailing garbage, it declares the signature valid. No modular inverse, no private exponent, nothing secret. For an invented update service, call it Acme Updater, that ships with a loose verifier and an exponent of three, an attacker can forge the signature on a malicious package and the client accepts it as signed by the vendor. This is the same family as the classic attack disclosed by Bleichenbacher, and it rhymes with the decryption side covered in our bleichenbacher attack writeup: both turn sloppy PKCS#1 v1.5 handling into a break.
Why this keeps happening
You would think rebuilding and comparing the full block is the obvious way to verify, and it is, but parsing grew popular because it is flexible. A parser that walks the structure tolerates slightly different DigestInfo encodings across algorithms and libraries, so it felt convenient. That tolerance is the bug. Over the years several widely used libraries shipped parsers that skipped the right side checks, and the same forgery resurfaced under new names each time a low exponent key met a lenient verifier. The pattern is stable: permissive input handling on a security decision, and an attacker who lives in the gap between what the format requires and what the code enforces.
The fix: verify strictly, raise the exponent
Every defense closes the gap between parsing and rebuilding.
- Rebuild and compare, do not parse. Construct the full expected padded block from the message hash, then compare it against the decrypted signature byte for byte across the entire modulus width. This alone kills the forgery, because the trailing garbage no longer passes unnoticed.
- Check every field if you must parse. Confirm the
FFrun fills the block, the separator is present, the DigestInfo is exactly the expected DER with no alternative encodings, and the hash sits flush against the right edge with zero trailing bytes. - Use a larger public exponent. A common choice is 65537. A higher exponent makes the cube root style construction impractical, because the signature would have to match far more of the block at once.
- Prefer a modern scheme and verified code. RSA PSS uses randomized padding that resists this class, and a carefully reviewed library that rebuilds the block removes the footgun from your own hands.
For the wider group this sits in, see our hub on cryptographic oracle attacks, and for another way RSA leaks through its implementation rather than its math, read about the rsa timing attack. If you want the grounding first, start with vulnerability basics.
The assumption that breaks
One quiet assumption carries this bug: that accepting a signature which merely looks right in its opening bytes is the same as accepting one that is right. It is not. Verification is a decision about authenticity, and a decision that checks the easy part and trusts the rest is an open door. You find a bug like this by comparing what a format demands against what a verifier actually enforces, field by field, not by scanning for a bad string. That is what an autonomous researcher built to test assumptions does: reason about how an app uses a primitive, such as an RSA verifier, and whether a permissive check lets a crafted value through. Read more on our about page.
Frequently asked questions
What is RSA signature forgery?
It is producing a number that a verifier accepts as a genuine RSA signature without holding the private key. RSA itself is not broken. The attack works when the code that checks PKCS#1 v1.5 padding around the signed hash parses the structure loosely instead of rebuilding and comparing the whole block, so a crafted value that only looks right at the start slips through.
Why does a public exponent of three make forgery easier?
Verifying with exponent three means cubing the signature. An attacker builds the target block they want, with the padding and real hash packed into the high bytes and the low bytes left free, then takes the integer cube root and rounds it. Cubing that value reproduces the structured high bytes exactly while the low bytes come out as garbage a loose verifier ignores. A larger exponent such as 65537 makes this impractical.
What does a loose verifier do wrong?
It scans from the left for the 00 01 prefix, skips the FF padding, finds the separator, reads the DigestInfo and the hash, and then stops. It never checks that the hash sits flush against the right edge or that nothing follows it. Those trailing bytes are where the forgery hides. A strict verifier rebuilds the full padded block and compares every byte.
How do you prevent RSA signature forgery?
Rebuild the expected padded block from the message hash and compare it byte for byte across the whole modulus, rather than parsing field by field. Use a larger public exponent such as 65537, prefer the randomized RSA PSS scheme, and rely on a carefully reviewed library. The MITRE entry CWE-347, improper verification of cryptographic signature tracks the general class.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
