The last of the original crypto challenges… here we go!
Unpadded RSA is homomorphic, meaning that, if operations like multiplication and addition are carried out on ciphertext, it is as if the same operation were applied to the plaintext.
This can have many useful properties, but also produces some consequences. This challenge is analogous to a “security game” that tests for a property known as IND-CCA2 — indistinguishability under Chosen Ciphertext Attack, where the adversary has access to a decryption oracle. The challenger gives the adversary a ciphertext, and the adversary can ask for the encryption and decryption of anything he wants — except the challenge ciphertext, of course! If the adversary can learn any information about the ciphertext’s plaintext, then he wins the game.
A fully homomorphic scheme fails IND-CCA2 — the adversary can completely recover the message! The adversary can’t ask for the decryption of the challenge ciphertext, but he can ask for the decryption of 2ciphertext, or 3ciphertext, etc. and he knows that the result will be 2plaintext, 3plaintext. Just divide out the scaling factor, and you have the plaintext.
Another worry with low public exponent is that the message block, when encrypted, will not be large enough to wrap the modulus.
This blog illustrates an attack on the padding scheme PKCS#1 1.5. When signing a message using RSA, the key-holder generates
m ^ d mod N, and the recipient uses
(N, e) to check that
(m ^ d) ^ e = m mod N. But, instead of signing the message, you sign the PKCS1.5 encoding of the message’s
In PKCS#1 1.5, you take the hash of the message you want to sign, and then you encode it like this:
00 01 FF FF ... FF FF 00 ASN.1 HASH
A faulty PKCS 1.5 padding verifier might not check that all the
\xff bytes in the middle are present.
Here’s an example of two faulty verifier versions:
# Find the 00 separator between the padding and the payload
Looking at the weaker version, it’s very simple to construct a message block meeting those parameters.
msg_hash = hashlib.sha1(msg).digest()
and then, we simply take the cube root of this forged sig, which undoes the encryption (a cubing).
(cube_root, exact) = gmpy2.iroot(forge_sig_num, 3)
The cube root will verify to a valid signature in the faulty padding verifier.
The DSA signature scheme uses two cyclic groups.
One large cyclic group, , has an order of a 1024 bit prime. Another cyclic group, , has an order of a 160 bit prime.
The key generation is:
- Generate a 1024-bit prime .
- Find a 160-bit prime divisor of .
- Find an element which generates the subgroup of with elements; i.e., .
- Choose a random with .
The keys are now:
A DSA signature is generated and verified as follows:
- Choose a random ephemeral key
- Compute aux value
- Compute aux value
- Compute aux value
def dsa_sign(msg, d):
To recover the private key if we know , we just solve for in the equation for :
Let’s work out how we can recover the k given a pair of messages that use repeated k.
If we make the generator equal to , then raising it to any power mod p will be 1.
This allows us to forge a signature for any message.
The idea is to multiply the message by successive powers of 2, and using our parity oracle to check whether the result is even or odd. This allows us to update the upper or lower bound — if odd, the multiplication wrapped the modulus, and we update the lower bound. If even, we didn’t wrap the modulus, and we have a tighter higher bound.
The answer at this StackExchange post allows us to see why. Briefly, when we multiply by 2 and then 4, if we get (even, even) then we haven’t wrapped the modulus, and the message is < N/4. If we get (odd, odd), then we know that we’ve wrapped the modulus twice — only possible if 3/4N < P < N. Make sure you see why: the result of the first doubling will be between 1/2N and N, so the second doubling will again wrap the modulus.
Do this iteratively, until you tighten the bounds enough to get every byte of the message, which will be discovered byte-by-byte.
Bleichenbacher’s attack shows how we can break RSA+PKCS1 padding using an adaptive chosen-ciphertext attack and a PKCS oracle. The PKCS oracle will tell you whether a ciphertext decrypts to a plaintext with the following structure:
- The first two bytes are
- There is at least one null byte after the first null byte
Here’s the attack — the description in the original paper is good:
The attacker tries to find small values for which the ciphertext is PKCS conforming. For each successful value for , the attacker computes, using previous knowledge about , a set of intervals that must contain … The third phase starts when only one interval remains. Then, the attacker has sufficient information about to choose such that is much more likely to be PKCS conforming than is a randomly chosen message. The size of is increased gradually, narrowing the possible range of until only one possible value remains.
The paper splits this up into four steps, which we can each implement as separate functions directly. The first step, blinding, is not really that important — it asks you to choose random integers until you find one s.t. is PKCS-conforming. But, you can just make sure that is already PKCS-conforming (by PKCS-padding the corresponding plaintext ), so is trivially 1.
Most difficulties you might encounter when implementing this attack are with regards to the bounds. You have to be very careful about correctly handling all the and interval bounds.
Other things to note when reading the four steps to implement in the paper:
- As the prompt says, “a PKCS#1v1.5 conformant plaintext, one that starts with 00:02, must be a number between 02:00:00…00 and 02:FF:FF…FF — in other words, 2B and 3B-1, where B is the bit size of the modulus minus the first 16 bits”. You must see why this is true.
- In step 2c, you just need to find one next . Iterate over each , and then iterate over all ’s possible for that .
- In step 3, the most important step, you narrow the intervals in which the message can exist. Each interval is a pair , and you iterate over all for each pair.
Finally, once we have just one interval with one value left, we’re done! We can recover the original plaintext from the answer and .
if len(m_intervals) == 1 and m_intervals == m_intervals:
assert found_msg == b"kick it, CC"