Everything You Know About Public-Key Encryption in PHP is Wrong

Last year, our security team identified CVE-2015-7503 a.k.a. ZF2015-10, a vulnerability in the RSA feature of Zend Framework's cryptography library.

The actual vulnerability (a padding oracle attack against RSA encryption that uses PKCS1v1.5 padding) was originally published in 1998 by Daniel Bleichenbacher. This padding oracle vulnerability allows an attacker to take an encrypted message, and repeatedly send altered ciphertexts to the server (each time looking for some indication of a padding error), and consequently decrypt the original message.

One might hope that any vulnerability (that has been known for over sixteen years) which allows attackers to decrypt arbitrary encrypted messages would be widely known by developers and/or mitigated out-of-the-box by all of the available tooling.

Sadly, when we look at PHP software (both open source and proprietary), we still uncover application-layer cryptography protocols written in 2016 that are vulnerable to this attack (and others we'll cover below).

We believe there are two primary forces at work here:

Most developers don't know enough about cryptography to safely implement public key encryption in any language. PHP's OpenSSL extension is insecure by default, and virtually nobody changes the default settings.

Quick Solution: Secure PHP Public-Key Encryption Libraries

If you're not interested in the why so much as the what, see instead: Choosing the Right Cryptography Library for your PHP Project.

Overview

How RSA Goes Bad

When it comes to application-layer cryptography, using RSA at all is a mistake. That doesn't always mean your application is totally doomed. However, there are a lot of RSA implementation flaws (some obvious, some non-obvious) that you have to avoid. Let's look at some of the ones that PHP developers are likely to encounter.

The Insecure Default That Bites Everyone

In PHP, most RSA implementations will make use of two functions to actually encrypt/decrypt data:

Look at both function prototypes. There's an insecure default value here.

bool openssl_public_encrypt ( string $data , string &$crypted , mixed $key [, int $padding = OPENSSL_PKCS1_PADDING ] )

bool openssl_private_decrypt ( string $data , string &$decrypted , mixed $key [, int $padding = OPENSSL_PKCS1_PADDING ] )

The constant OPENSSL_PKCS1_PADDING tells the OpenSSL extension, "We want to use PKCS1 v1.5 padding." But, as we said before, it has been public knowledge that RSA encryption that uses PKCS1 v1.5 padding is vulnerable to a padding oracle vulnerability since 1998. This attack is more generally known as the "million message attack" due to the attack cost requiring a million messages to recover a plaintext.

The solution is to specify OPENSSL_PKCS1_OAEP_PADDING whenever you use either function. This constant forces the use of OAEP padding instead of insecure PKCS1 v1.5 padding.

In our experience, virtually nobody does this (unless someone on our team points it out):

Even experienced cryptography developers often forget that they should use OAEP when encrypting in RSA.

Therefore, if you're forced to work with public key encryption (either as a developer or a penetration tester) and they mention using RSA, before the conversation even steers towards "Are 2048-bit keys sufficient or do we need 4096-bit?" check the padding mode being used. You may be able to recover the plaintext for any arbitrary ciphertexts in only a few thousand messages and totally defeat the security of the application.

The Perils of Direct RSA Encryption

If you read the previous section and thought, "Okay, if I just remember to use OAEP, I'm in the clear to directly encrypt arbitrary messages with RSA," not so fast. You actually cannot encrypt large messages with RSA directly (proof-of-concept code).

When confronted with this limitation, most developers try to be clever: They'll just break the message into 214-byte chunks (for 2048-bit keys) and encrypt each block independently. As a shorthand, we refer to this as RSA in ECB mode.

Aside from the obvious attack (you can duplicate, reorder, or delete 214-byte blocks at will without creating a decryption error) and the fun you can have with known-plaintext attacks, RSA is slow. Criminals are more likely to just abuse this feature for DDoS amplification (to great effect) than to pursue any cryptanalysis efforts.

Hybrid Cryptosystems Save the Day

The best way to implement public key encryption is to build a hybrid cryptosystem, which combines symmetric-key and and asymmetric-key cryptography algorithms. This has several advantages.

Performance: Symmetric-key authenticated encryption is much faster than asymmetric-key encryption.

Usability: There is no practical limit on message sizes.

Security: See below.

Hybrid RSA + AES

Combining RSA and AES usually entails:

Encrypt the message (using authenticated symmetric-key encryption), with a random symmetric key ($k_s$). Encrypt the symmetric key ($k_s$) with the desired public key ($k_{pub}$), so that only the corresponding private key ($k_{priv}$) can use it.

An RSA-AES hybrid cryptosystem is available in Zend\Crypt since version 3.1.0, and EasyRSA since its inception. The Zend Framework documentation on its hybrid cryptosystem is excellent for understanding how it works.

Due to the available AES key sizes, you're only ever going to encrypt 16, 24, or 32 bytes, which is less than the 214 bytes allowed by 2048-bit RSA. The actual message encryption is taken care of by AES in CBC or CTR mode (with a random IV or nonce), which is then authenticated by HMAC-SHA256. There is no practical upper limit on message size for encrypting this way for most applications. This is far easier to secure than a long chain of RSA ciphertexts.

Hybrid ECDH + Xsalsa20-Poly1305

Libsodium uses Elliptic Curve Diffie-Hellman (ECDH) instead of RSA to negotiate a shared secret key, which is then used by xsalsa20-poly1305 to encrypt the message and authenticate the ciphertext.

Typically, this is used between two known/trusted ECDH public keys to establish a shared secret. The relevant feature is called crypto_box() .

For situations where you want to encrypt only with the recipient's public key (i.e. the sender cannot decrypt), another libsodium feature generates a random keypair for each message and attaches the public key with the ciphertext. This is called crypto_box_seal() .

RSA Attacks are Advancing Faster than ECC Attacks

The security of RSA is predicated on the difficulty of factoring large integers into its prime components. However, there are two major threats to this security guarantee in the near future:

Improved attack algorithms that can recover a private key from only a public key faster than the general number field sieve, which do not affect elliptic curve cryptography. Quantum computers, which also break elliptic curve cryptography.

It's generally believed that a sophisticated attacker can break 1024-bit RSA in a few months time, but that 2048-bit RSA is still safe. However, a breakthrough attack that breaks 2048-bit RSA is likely to also break 4096-bit RSA too.

If you're planning to build cryptography into a new application in 2016, the looming threat of an RSA break (the real question is whether the break also affects ECDH and/or ECDSA too) should definitely be taken into consideration. In fact, you're almost certainly better off not using RSA, DSA, or classical Diffie-Hellman going forward.

In Summary

If you need to add public-key encryption to your PHP application: