Reasoning by Lego: The wrong way to think about cryptography.

Scott Arciszewski from Paragon Initiative pointed me to this example of PHP cryptography.

The code is bad and the crypto design is flawed, but as usual for this blog, we can learn something from it. Let’s ignore the fact that it’s using MCRYPT_RIJNDAEL_256 (the 256-bit block version of Rijndael, not AES) instead of MCRYPT_RIJNDAEL_128 (real AES), the fact that it’s not checking the return value of substr(), and the fact that it’s passing a hexadecimal-encoded key to a function that expects a binary string. I’ve covered all of these failings on this blog before, so I won’t touch on them again.

Instead, let’s focus on two facts. First, it is doing “MAC then Encrypt” (MtA), which means the Message Authentication Code (MAC) is being applied to the plaintext message before encryption – contrary to modern crypto wisdom. Second, that the MAC is checked with a non-timing-safe comparison, which means that if an attacker can get really precise timing measurements of a failed decryption, they can find out how much of the MAC matches. In the “Encrypt then MAC” (EtM) design, where the MAC is applied to the ciphertext after encryption, this kind of a timing leak usually lets you forge a message. But this time, the MAC is inside the ciphertext, encrypted, so at a first glance, exploiting it seems more difficult.

Indeed, the issue was brought up in the comment section of the page. The author of the code responded as follows:

I had written code for a constant-time text comparison of the MAC but then realized that this is not an issue when using the MAC-then-encrypt method because the MAC is only compared after decryption to verify that the data is intact and the same as when it was encrypted. Since the correct MAC is only exposed when the correct encryption key is used it doesn’t matter whether the text comparison is done in constant-time or not. Changing to an encrypt-then-MAC method (requiring a constant-time MAC comparison) would not be backwards compatible with the current version.

Let’s follow the logic of this statement. On a broad level, they are claiming that the timing attack does not matter because the MAC is inside the encryption. What is their reasoning for this claim? There are two ways to interpret what was written, depending on what you take “correct MAC” to mean.

In one interpretation, “correct MAC” means the MAC that’s stored within the message, and they are saying you need the key to access it, so even if you could use the timing channel to match the calculated MAC by twiddling the “correct MAC” you couldn’t actually learn what it is. This reasoning is flawed because to create a forgery you do not always need to know the MAC value, you just need to produce some new ciphertext that the decryption machinery will accept. Or perhaps they mean that even if you did know the calculated MAC value, you still couldn’t place it inside the plaintext message without knowing the encryption key. This assumes the encryption itself is providing some sort of integrity protection. That may well be so, but a more rigorous argument is necessary to justify it.

In the other interpretation, “correct MAC” means the MAC that’s computed from the plaintext message after decryption. This reading is hard to interpret in a positive light. It’s true that it’s only exposed after decryption with the correct key, but the side-channel in the code leaks information about it right after it is!

Neither of these readings may be what the author really meant, but I am trying to interpret their words in the best way possible.

I’m going to tell you how to attack against this code in just a moment, and the fact that an attack exists demonstrates that the author’s reasoning must be unsound. So why spend so many words analysing the argument? Because it is an example of an extremely common mistake, which I’ll call reasoning by Lego.

Reasoning by Lego happens when a designer or developer understands cryptography as Lego blocks stacked together. The blue block (the MAC) is sitting on top of the red block (the encryption), therefore the blue block won’t fall down. Textbooks put cryptography algorithms on display as circuit diagrams with primitives like ciphers and hashes wired up to data like keys and plaintexts. This leads to the view that crypto algorithms should be thought of as simple components wired together.

The Lego view is not wrong. It’s an intuitive way to think of crypto algorithms when you’re writing the code. But it’s the wrong way to think about cryptographic security. To talk about an algorithm’s security, you need to consider the entire system as a whole. In your house, you can safely replace an old incandescent lightbulb with a more energy-efficient compact fluorescent without worrying that it will cause your microwave to explode. Cryptography is different. Making a small change to one part of your algorithm can completely break the other seemingly-unrelated parts. Just because the parts are spatially separated in the circuit diagram doesn’t mean they don’t influence each other. You need to look at the bigger picture.

Professional cryptographers do just that. Instead of reasoning from the bottom up, as by saying, “The green Lego block is connected to the red Lego block in this way, therefore the blue Lego block on top won’t fall over.” they take a top-down approach, closer to, “Let’s suppose this Lego structure breaks in any way. The only way that’s possible [rigorous proof details omitted] is if the green Lego block splits in two. We are assuming individual Lego blocks do not break, so the whole structure will not break.”

In cryptographic terms, we start by assuming the system is vulnerable to a specific kind of adversary. Usually, we give the adversary as much power as possible. We let them encrypt chosen plaintexts and even decrypt chosen ciphertexts. Then we prove a claim along the lines of, “If that adversary can break the system as a whole (the entire Lego structure), then they can find a collision in the hash function (the indestructible Lego block).” This sort of reasoning lets us prove, formally, that if the crypto primitives we’re using (ciphers, hash functions, MACs) are secure, then the system as a whole is secure. That is the right way to reason about security in cryptography.

I’ve personally found that the top-down mindset is a good way to break bad cryptography, too. If you want to break some crypto, first find an attack that shows it isn’t secure according to the strict formal definitions of security (IND-CPA, IND-CCA2, etc.). The formal definitions of security are so strict that the attack you find might not have any practical relevance at all. Next, take that weakness and find a way to turn it into into a practically-relevant attack. For me, this works nearly every time.

The Attack (Proof of Concept)

Okay, I promised you earlier that I had an actual attack against this PHP encryption code, and now it’s time to deliver.

I like this attack because it demonstrates everything I’ve been saying up until now. On the Lego block view, the timing side-channel in the code only seems relevant to message integrity. We were so distracted by integrity that we forgot to consider its effect on confidentiality. It turns out to have a big effect. The attack I found lets an attacker decrypt the first byte of every block in a ciphertext. The only preconditions are: (1) The attacker needs some (less than 2000) known plaintext blocks, and (2) the attacker needs to be able to provide ciphertexts to the decryption function and get an accurate measure of the running time (i.e. they can exploit the side-channel).

I’m not going to explain the attack here. I feel like doing so would distract away from the message of this post. If you are interested, you can find a simulated proof of concept implementation of the attack here. The code is well-commented, so if you want to know how it works, you should be able to understand it by reading (and running) that code.

In the proof of concept, I’ve removed the serialize() and unserialize() calls, as well as the call to trim(). I’ve also modified the decryption function to return 0 if the MAC comparison fails on the first byte or 1 if the MAC comparison makes it past the first byte, simulating a side-channel with one-byte granularity.

I made these changes to make the attack easier and more reliable. Without the changes, the attack is still possible in theory, but much harder to demonstrate. In particular, string-comparison timing leaks do not have one-byte granularity in practice. Instead, strings are usually compared word-by-word (4 or 8 bytes at a time), which increases the number of decryption queries required to carry out the attack. If the reader is interested, they might try to get the attack working in a realistic setting.

As they say, “Attacks always get better; they never get worse.”

When we reason by Lego, we destroy the environment in favor of saving a single tree. In this case, we got distracted into thinking about MACs and forgeries and forgot to check the other security properties. Yes, that blue Lego block is held up by the red block, but until we took a step back, we couldn’t see that the red block was hanging off the edge, about to fall, bringing the blue block with it.

Thinking of cryptography as Lego blocks put together will lead you astray. If you want to build secure cryptography, insist on a security proof in a strict model with powerful adversaries. If you want to break cryptography, start by showing that it isn’t secure in a strict model, and then find a way to turn those weaknesses into practical attacks.

I Consult

If you would like to hire me to find the problems with your crypto design or some bugs in your code, you can find more information about my consulting services here.