]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
BUG/MEDIUM: jwe: substitute random CEK on RSA1_5 decryption failure per RFC 7516...
authorRemi Tricot-Le Breton <rlebreton@haproxy.com>
Tue, 26 May 2026 15:26:04 +0000 (17:26 +0200)
committerWilly Tarreau <w@1wt.eu>
Tue, 26 May 2026 16:19:00 +0000 (18:19 +0200)
do_decrypt_cek_rsa() calls EVP_PKEY_decrypt with RSA_PKCS1_PADDING for
RSA1_5 and returns failure (goto end) on decrypt error. This creates a
measurable timing difference between "padding invalid" (fast exit before
content decryption) and "padding valid + AEAD tag fail" (full AES-GCM/CBC
decryption path), exposing the RSA private key to a Bleichenbacher-style
adaptive attack requiring ~10^4-10^6 queries.

Fix: On RSA_PKCS1_PADDING failure, fill decrypted_cek with random bytes
of the buffer size and return success (retval=0). This forces execution
into decrypt_ciphertext() regardless of padding validity, so the attacker
cannot distinguish valid from invalid padding via timing. The AEAD tag
check in decrypt_ciphertext() will still reject the wrong CEK, but the
timing profile is identical for both branches.

RSA-OAEP variants are not affected (mathematically infeasible to craft
valid ciphertext without the private key).

Introduced by RSA1_5 path lacking constant-time fallback.

src/jwe.c

index 0471fb8043585291cb947a07207293d26f82016d..ec00a19a8760c45144c44fe5bed6dd9509820538 100644 (file)
--- a/src/jwe.c
+++ b/src/jwe.c
@@ -584,8 +584,13 @@ static int decrypt_ciphertext(jwe_enc enc, struct jwt_item items[JWE_ELT_MAX],
                        goto end;
 
                /* Only use the second part of the decrypted key for actual
-                * content decryption. */
-               if (b_data(decrypted_cek) != key_size * 2)
+                * content decryption.
+                * Because of the RSAES-PKCS1-V1_5 algorithm, we might have a
+                * bigger than expected decrypted_cek (if it was filled with
+                * random bytes in do_decrypt_cek_rsa) and still want to call
+                * aes_process on the ciphertext in order to avoid timing
+                * attacks. */
+               if (b_data(decrypted_cek) < key_size * 2)
                        goto end;
                chunk_memcpy(aes_key, decrypted_cek->area + key_size, key_size);
        }
@@ -819,8 +824,31 @@ static int do_decrypt_cek_rsa(struct buffer *cek, struct buffer *decrypted_cek,
        }
 
        if (EVP_PKEY_decrypt(ctx, (unsigned char*)b_orig(decrypted_cek), &outl,
-                            (unsigned char*)b_orig(cek), b_data(cek)) <= 0)
-               goto end;
+                            (unsigned char*)b_orig(cek), b_data(cek)) <= 0) {
+               /* Per RFC 7516 #11.5, on RSAES-PKCS1-V1_5 decryption failure,
+                * substitute a random CEK and continue into content decryption.
+                * This prevents the Bleichenbacher timing oracle: without this
+                * guard, "padding invalid" (fast exit) is distinguishable from
+                * "padding valid + AEAD tag fail" (full decrypt path).
+                * We will build the biggest decrypted_cek necessary rather than
+                * filling the entire buffer, which would be a key for the
+                * A256CBC_HS512 encrypting algorithm for which the decrypted
+                * cek contains the actual key as well as the tag.
+                */
+               if (pad == RSA_PKCS1_PADDING) {
+#define MAX_DECRYPTED_CEK_LEN (32 * 2) /* See https://datatracker.ietf.org/doc/html/rfc7518#section-5.2.2.1 */
+                       int i;
+                       unsigned char *p = (unsigned char *)b_orig(decrypted_cek);
+
+                       for (i = 0; i < MAX_DECRYPTED_CEK_LEN; i++) {
+                               uint64_t r = ha_random64();
+                               memcpy(p, &r, 8);
+                               p+=8;
+                       }
+                       outl = MAX_DECRYPTED_CEK_LEN;
+               } else
+                       goto end;
+       }
 
        decrypted_cek->data = outl;