From 859cf0788dcec098db34c3bd3bb1f94c19f0d278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 1 Jun 2026 09:52:33 +0200 Subject: [PATCH 01/20] psa-rsa: call pkcs1_v15 function directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Like the OAEP path calls the OAEP function directly Signed-off-by: Manuel Pégourié-Gonnard --- library/psa_crypto_rsa.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/library/psa_crypto_rsa.c b/library/psa_crypto_rsa.c index 38dc3b8edc..5413e9893c 100644 --- a/library/psa_crypto_rsa.c +++ b/library/psa_crypto_rsa.c @@ -652,14 +652,10 @@ psa_status_t mbedtls_psa_asymmetric_decrypt(const psa_key_attributes_t *attribut if (alg == PSA_ALG_RSA_PKCS1V15_CRYPT) { #if defined(MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_CRYPT) - status = mbedtls_to_psa_error( - mbedtls_rsa_pkcs1_decrypt(rsa, - mbedtls_psa_get_random, - MBEDTLS_PSA_RANDOM_STATE, - output_length, - input, - output, - output_size)); + int ret = mbedtls_rsa_rsaes_pkcs1_v15_decrypt( + rsa, mbedtls_psa_get_random, MBEDTLS_PSA_RANDOM_STATE, + output_length, input, output, output_size); + status = mbedtls_to_psa_error(ret); #else status = PSA_ERROR_NOT_SUPPORTED; #endif /* MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_CRYPT */ From 503c1375c81ee1eb4d45f04b6af439a7e45fbc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 1 Jun 2026 10:08:52 +0200 Subject: [PATCH 02/20] psa-rsa: fix side channel on invalid padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- library/psa_crypto_rsa.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/library/psa_crypto_rsa.c b/library/psa_crypto_rsa.c index 5413e9893c..170774ee1a 100644 --- a/library/psa_crypto_rsa.c +++ b/library/psa_crypto_rsa.c @@ -25,6 +25,7 @@ #include #include #include "rsa_internal.h" +#include "constant_time_internal.h" #if defined(MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_CRYPT) || \ defined(MBEDTLS_PSA_BUILTIN_ALG_RSA_OAEP) || \ @@ -655,7 +656,17 @@ psa_status_t mbedtls_psa_asymmetric_decrypt(const psa_key_attributes_t *attribut int ret = mbedtls_rsa_rsaes_pkcs1_v15_decrypt( rsa, mbedtls_psa_get_random, MBEDTLS_PSA_RANDOM_STATE, output_length, input, output, output_size); + + /* We want to convert ret into a psa status without allowing an + * attacker to distinguish between 0 and invalid padding. Since + * mbedtls_psa_error() is leaky, hide the difference from it. */ + mbedtls_ct_condition_t bad_padding = mbedtls_ct_uint_eq( + (mbedtls_ct_uint_t) ret, + (mbedtls_ct_uint_t) MBEDTLS_ERR_RSA_INVALID_PADDING); + ret = mbedtls_ct_error_if(bad_padding, 0, ret); status = mbedtls_to_psa_error(ret); + status = mbedtls_ct_error_if(bad_padding, + PSA_ERROR_INVALID_PADDING, status); #else status = PSA_ERROR_NOT_SUPPORTED; #endif /* MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_CRYPT */ From f1dc979f27af0a0e8ad261ea008c4a52229c5b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Fri, 29 May 2026 13:29:51 +0200 Subject: [PATCH 03/20] psa: avoid leaking padding errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Padding errors should be indistiguishable from success. Signed-off-by: Manuel Pégourié-Gonnard --- library/psa_crypto.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/psa_crypto.c b/library/psa_crypto.c index 0dfd71d540..c2785ea3cd 100644 --- a/library/psa_crypto.c +++ b/library/psa_crypto.c @@ -3568,7 +3568,12 @@ exit: LOCAL_INPUT_FREE(salt_external, salt); LOCAL_OUTPUT_FREE(output_external, output); - return (status == PSA_SUCCESS) ? unlock_status : status; + /* Don't branch on status as it is a sensitive value + * (it reveals whether padding was valid). + * Instead branch on unlock_status. That means when both status and + * unlock_status were errors, we'll return the unlock error while we would + * normally return the first error, but that's better than leaking info. */ + return (unlock_status != PSA_SUCCESS) ? unlock_status : status; } /****************************************************************/ From 682d8f3e894ea4b2ee88eec064984f230832dfa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 1 Jun 2026 12:49:33 +0200 Subject: [PATCH 04/20] psa-rsa: also protect OUTPUT_TOO_LARGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- library/psa_crypto_rsa.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/library/psa_crypto_rsa.c b/library/psa_crypto_rsa.c index 170774ee1a..322ede0701 100644 --- a/library/psa_crypto_rsa.c +++ b/library/psa_crypto_rsa.c @@ -658,15 +658,23 @@ psa_status_t mbedtls_psa_asymmetric_decrypt(const psa_key_attributes_t *attribut output_length, input, output, output_size); /* We want to convert ret into a psa status without allowing an - * attacker to distinguish between 0 and invalid padding. Since - * mbedtls_psa_error() is leaky, hide the difference from it. */ + * attacker to distinguish between 0, invalid padding, or buffer too + * small. (It's OK if the attacker distinguishes between one of the + * above three and other status such as out of memory.) Since + * mbedtls_to_psa_error() is leaky, hide the difference from it. */ mbedtls_ct_condition_t bad_padding = mbedtls_ct_uint_eq( (mbedtls_ct_uint_t) ret, (mbedtls_ct_uint_t) MBEDTLS_ERR_RSA_INVALID_PADDING); + mbedtls_ct_condition_t large_msg = mbedtls_ct_uint_eq( + (mbedtls_ct_uint_t) ret, + (mbedtls_ct_uint_t) MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); ret = mbedtls_ct_error_if(bad_padding, 0, ret); + ret = mbedtls_ct_error_if(large_msg, 0, ret); status = mbedtls_to_psa_error(ret); status = mbedtls_ct_error_if(bad_padding, PSA_ERROR_INVALID_PADDING, status); + status = mbedtls_ct_error_if(large_msg, + PSA_ERROR_BUFFER_TOO_SMALL, status); #else status = PSA_ERROR_NOT_SUPPORTED; #endif /* MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_CRYPT */ From 689a05df0b9170ae6bbbd6a012d5c4d46758986c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Tue, 2 Jun 2026 09:35:51 +0200 Subject: [PATCH 05/20] pk: test RSA decrypt with buffer too small MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- tests/suites/test_suite_pk.function | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/suites/test_suite_pk.function b/tests/suites/test_suite_pk.function index 9bfbb37bc9..6d397f096d 100644 --- a/tests/suites/test_suite_pk.function +++ b/tests/suites/test_suite_pk.function @@ -1546,6 +1546,12 @@ void pk_rsa_decrypt_test_vec(data_t *cipher, int mod, int padding, int md_alg, if (ret == 0) { TEST_ASSERT(olen == clear->len); TEST_ASSERT(memcmp(output, clear->x, olen) == 0); + + /* Try again with an output buffer that's too small */ + TEST_EQUAL(mbedtls_pk_decrypt(&pk, cipher->x, cipher->len, + output, &olen, clear->len - 1, + mbedtls_test_rnd_pseudo_rand, &rnd_info), + MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); } exit: From 8f4a4eac98788e152ae5ba46937645aa83d48354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Tue, 2 Jun 2026 09:22:12 +0200 Subject: [PATCH 06/20] pk: fix side channel in RSA decryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- library/pk_wrap.c | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/library/pk_wrap.c b/library/pk_wrap.c index 5265469349..876523e5c2 100644 --- a/library/pk_wrap.c +++ b/library/pk_wrap.c @@ -34,6 +34,7 @@ #if defined(MBEDTLS_RSA_C) #include "pkwrite.h" #include "rsa_internal.h" +#include "constant_time_internal.h" #endif #if defined(MBEDTLS_PK_CAN_ECDSA_SOME) @@ -329,21 +330,35 @@ static int rsa_decrypt_wrap(mbedtls_pk_context *pk, input, ilen, NULL, 0, output, osize, olen); - if (status != PSA_SUCCESS) { - ret = PSA_PK_RSA_TO_MBEDTLS_ERR(status); - goto cleanup; - } - ret = 0; + /* We want to convert status into an MBEDTLS code without allowing an + * attacker to distinguish between 0, invalid padding, or buffer too + * small. (It's OK if the attacker distinguishes between one of the + * above three and other status such as out of memory.) Since + * PSA_PK_RSA_TO_MBEDTLS_ERR() is leaky, hide the difference from it. */ + mbedtls_ct_condition_t bad_padding = mbedtls_ct_uint_eq( + (mbedtls_ct_uint_t) status, + (mbedtls_ct_uint_t) PSA_ERROR_INVALID_PADDING); + mbedtls_ct_condition_t large_msg = mbedtls_ct_uint_eq( + (mbedtls_ct_uint_t) status, + (mbedtls_ct_uint_t) PSA_ERROR_BUFFER_TOO_SMALL); + status = mbedtls_ct_error_if(bad_padding, 0, status); + status = mbedtls_ct_error_if(large_msg, 0, status); + ret = PSA_PK_RSA_TO_MBEDTLS_ERR(status); + ret = mbedtls_ct_error_if(bad_padding, + MBEDTLS_ERR_RSA_INVALID_PADDING, ret); + ret = mbedtls_ct_error_if(large_msg, + MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE, ret); cleanup: mbedtls_zeroize_and_free(buf, buf_size); status = psa_destroy_key(key_id); - if (ret == 0 && status != PSA_SUCCESS) { - ret = PSA_PK_TO_MBEDTLS_ERR(status); - } - - return ret; + /* Don't branch on ret as it is a sensitive value + * (it reveals whether padding was valid). + * Instead branch on status. That means when both ret and + * status were errors, we'll return the unlock status while we would + * normally return the first error, but that's better than leaking info. */ + return (status != PSA_SUCCESS) ? PSA_PK_TO_MBEDTLS_ERR(status) : ret; } #else /* MBEDTLS_USE_PSA_CRYPTO */ static int rsa_decrypt_wrap(mbedtls_pk_context *pk, From 54682ec425078e45299628c03d98be10bcef4f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Tue, 2 Jun 2026 10:30:59 +0200 Subject: [PATCH 07/20] rsa: add CF testing for PKCS#1 v1.5 decode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- library/rsa.c | 35 +++---------------- library/rsa_internal.h | 37 ++++++++++++++++++++ tests/suites/test_suite_pkcs1_v15.function | 39 ++++++++++++++++++++++ 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/library/rsa.c b/library/rsa.c index 2eb042ff5a..94db8282c4 100644 --- a/library/rsa.c +++ b/library/rsa.c @@ -414,37 +414,12 @@ end_of_export: #if defined(MBEDTLS_PKCS1_V15) && defined(MBEDTLS_RSA_C) && !defined(MBEDTLS_RSA_ALT) -/** This function performs the unpadding part of a PKCS#1 v1.5 decryption - * operation (EME-PKCS1-v1_5 decoding). - * - * \note The return value from this function is a sensitive value - * (this is unusual). #MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE shouldn't happen - * in a well-written application, but 0 vs #MBEDTLS_ERR_RSA_INVALID_PADDING - * is often a situation that an attacker can provoke and leaking which - * one is the result is precisely the information the attacker wants. - * - * \param input The input buffer which is the payload inside PKCS#1v1.5 - * encryption padding, called the "encoded message EM" - * by the terminology. - * \param ilen The length of the payload in the \p input buffer. - * \param output The buffer for the payload, called "message M" by the - * PKCS#1 terminology. This must be a writable buffer of - * length \p output_max_len bytes. - * \param olen The address at which to store the length of - * the payload. This must not be \c NULL. - * \param output_max_len The length in bytes of the output buffer \p output. - * - * \return \c 0 on success. - * \return #MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE - * The output buffer is too small for the unpadded payload. - * \return #MBEDTLS_ERR_RSA_INVALID_PADDING - * The input doesn't contain properly formatted padding. +/* + * EME-PKCS1-v1_5 decoding, see documentation in rsa_internal.h */ -static int mbedtls_ct_rsaes_pkcs1_v15_unpadding(unsigned char *input, - size_t ilen, - unsigned char *output, - size_t output_max_len, - size_t *olen) +MBEDTLS_STATIC_TESTABLE int mbedtls_ct_rsaes_pkcs1_v15_unpadding( + unsigned char *input, size_t ilen, + unsigned char *output, size_t output_max_len, size_t *olen) { int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED; size_t i, plaintext_max_size; diff --git a/library/rsa_internal.h b/library/rsa_internal.h index f79c3b7122..3b6186c7e8 100644 --- a/library/rsa_internal.h +++ b/library/rsa_internal.h @@ -118,4 +118,41 @@ int mbedtls_rsa_rsassa_pss_sign_no_mode_check(mbedtls_rsa_context *ctx, unsigned char *sig); #endif /* MBEDTLS_PKCS1_V21 */ +/* This would normally be in rsa_invasive.h but it didn't exist before 3.6 + * became an LTS, and I'd rather not add files in LTS if it can be avoided. */ +#if defined(MBEDTLS_TEST_HOOKS) +#if defined(MBEDTLS_PKCS1_V15) && defined(MBEDTLS_RSA_C) && !defined(MBEDTLS_RSA_ALT) + +/** This function performs the unpadding part of a PKCS#1 v1.5 decryption + * operation (EME-PKCS1-v1_5 decoding). + * + * \note The return value from this function is a sensitive value + * (this is unusual). #MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE shouldn't happen + * in a well-written application, but 0 vs #MBEDTLS_ERR_RSA_INVALID_PADDING + * is often a situation that an attacker can provoke and leaking which + * one is the result is precisely the information the attacker wants. + * + * \param input The input buffer which is the payload inside PKCS#1v1.5 + * encryption padding, called the "encoded message EM" + * by the terminology. + * \param ilen The length of the payload in the \p input buffer. + * \param output The buffer for the payload, called "message M" by the + * PKCS#1 terminology. This must be a writable buffer of + * length \p output_max_len bytes. + * \param olen The address at which to store the length of + * the payload. This must not be \c NULL. + * \param output_max_len The length in bytes of the output buffer \p output. + * + * \return \c 0 on success. + * \return #MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE + * The output buffer is too small for the unpadded payload. + * \return #MBEDTLS_ERR_RSA_INVALID_PADDING + * The input doesn't contain properly formatted padding. + */ +MBEDTLS_STATIC_TESTABLE int mbedtls_ct_rsaes_pkcs1_v15_unpadding( + unsigned char *input, size_t ilen, + unsigned char *output, size_t output_max_len, size_t *olen); +#endif /* MBEDTLS_PKCS1_V15 && MBEDTLS_RSA_C && ! MBEDTLS_RSA_ALT */ +#endif /* MBEDTLS_TEST_HOOKS */ + #endif /* rsa_internal.h */ diff --git a/tests/suites/test_suite_pkcs1_v15.function b/tests/suites/test_suite_pkcs1_v15.function index 7113274550..e7c1ce22fd 100644 --- a/tests/suites/test_suite_pkcs1_v15.function +++ b/tests/suites/test_suite_pkcs1_v15.function @@ -1,6 +1,39 @@ /* BEGIN_HEADER */ #include "mbedtls/rsa.h" #include "mbedtls/md.h" +#include "rsa_internal.h" +#include + +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_RSA_ALT) +static void pkcs1_v15_decode_raw(const unsigned char *input, + size_t input_size, + size_t output_size, + int expected_result, + size_t expected_plaintext_length) +{ + unsigned char *input_buf = NULL; + unsigned char *output_buf = NULL; + size_t olen = 0; + + TEST_CALLOC(output_buf, output_size); + + TEST_CALLOC(input_buf, input_size); + memcpy(input_buf, input, input_size); + + TEST_CF_SECRET(input_buf, input_size); + TEST_EQUAL(expected_result, + mbedtls_ct_rsaes_pkcs1_v15_unpadding( + input_buf, input_size, + output_buf, output_size, &olen)); + if (expected_result == 0) { + TEST_EQUAL(expected_plaintext_length, olen); + } + +exit: + mbedtls_free(input_buf); + mbedtls_free(output_buf); +} +#endif /* MBEDTLS_TEST_HOOKS && !MBEDTLS_RSA_ALT */ /* END_HEADER */ /* BEGIN_DEPENDENCIES @@ -257,6 +290,11 @@ void pkcs1_v15_decode(data_t *input, TEST_ASSERT(count < 16); } +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_RSA_ALT) + pkcs1_v15_decode_raw(original, sizeof(original), output_size, + expected_result, expected_plaintext_length); +#endif + exit: mbedtls_mpi_free(&Nmpi); mbedtls_mpi_free(&Empi); mbedtls_mpi_free(&Pmpi); mbedtls_mpi_free(&Qmpi); @@ -264,6 +302,7 @@ exit: } /* END_CASE */ + /* BEGIN_CASE */ void pkcs1_rsassa_v15_sign(int mod, char *input_P, char *input_Q, char *input_N, From ea0a2c6d03203f96a842a8e25f556b757c620246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Tue, 2 Jun 2026 10:45:34 +0200 Subject: [PATCH 08/20] rsa/psa/pk: warn more against PKCS#1 v1.5 decrypt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- include/mbedtls/pk.h | 14 +++++++++----- include/mbedtls/rsa.h | 7 ++++++- include/psa/crypto.h | 6 ++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/include/mbedtls/pk.h b/include/mbedtls/pk.h index 68b0f4befa..e065e4166d 100644 --- a/include/mbedtls/pk.h +++ b/include/mbedtls/pk.h @@ -930,6 +930,15 @@ int mbedtls_pk_sign_restartable(mbedtls_pk_context *ctx, /** * \brief Decrypt message (including padding if relevant). * + * \warning When using PKCS#1 v1.5 (see note below), this is an + * inherently dangerous function (CWE-242) and the return value + * is sensitive, see mbedtls_rsa_rsaes_pkcs1_v15_decrypt(). + * + * \note For keys of type #MBEDTLS_PK_RSA, the encryption algorithm is + * either PKCS#1 v1.5 or OAEP, depending on the padding mode in + * the underlying RSA context. For a pk object constructed by + * parsing, this is PKCS#1 v1.5 by default. + * * \param ctx The PK context to use. It must have been set up * with a private key. * \param input Input to decrypt @@ -940,11 +949,6 @@ int mbedtls_pk_sign_restartable(mbedtls_pk_context *ctx, * \param f_rng RNG function, must not be \c NULL. * \param p_rng RNG parameter * - * \note For keys of type #MBEDTLS_PK_RSA, the signature algorithm is - * either PKCS#1 v1.5 or OAEP, depending on the padding mode in - * the underlying RSA context. For a pk object constructed by - * parsing, this is PKCS#1 v1.5 by default. - * * \return 0 on success, or a specific error code. */ int mbedtls_pk_decrypt(mbedtls_pk_context *ctx, diff --git a/include/mbedtls/rsa.h b/include/mbedtls/rsa.h index 3f0881a434..d528b0a864 100644 --- a/include/mbedtls/rsa.h +++ b/include/mbedtls/rsa.h @@ -696,7 +696,9 @@ int mbedtls_rsa_rsaes_oaep_encrypt(mbedtls_rsa_context *ctx, * * \warning When \p ctx->padding is set to #MBEDTLS_RSA_PKCS_V15, * mbedtls_rsa_rsaes_pkcs1_v15_decrypt() is called, which is an - * inherently dangerous function (CWE-242). + * inherently dangerous function (CWE-242). In that case, the + * return value of this function is sensitive, see the + * documentation of mbedtls_rsa_rsaes_pkcs1_v15_decrypt(). * * \note The output buffer length \c output_max_len should be * as large as the size \p ctx->len of \p ctx->N (for example, @@ -738,6 +740,9 @@ int mbedtls_rsa_pkcs1_decrypt(mbedtls_rsa_context *ctx, * it is used in a side channel free and safe way (eg. * implementing the TLS protocol as per 7.4.7.1 of RFC 5246), * the calling code is vulnerable. + * Specifically, callers need to ensure an adversary cannot + * distinguish between success, MBEDTLS_ERR_RSA_INVALID_PADDING + * and MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE via side channels. * * \note The output buffer length \c output_max_len should be * as large as the size \p ctx->len of \p ctx->N, for example, diff --git a/include/psa/crypto.h b/include/psa/crypto.h index 2fe9f35ec3..1a86af7261 100644 --- a/include/psa/crypto.h +++ b/include/psa/crypto.h @@ -3125,6 +3125,12 @@ psa_status_t psa_asymmetric_encrypt(mbedtls_svc_key_id_t key, /** * \brief Decrypt a short message with a private key. * + * \warning When \p alg is #PSA_ALG_RSA_PKCS1V15_CRYPT, this is an + * inherently dangerous function (CWE-242): unless it is used + * in a side channel free and safe way (eg. implementing the + * TLS protocol as per 7.4.7.1 of RFC 5246), the calling code + * is vulnerable. + * * \param key Identifier of the key to use for the operation. * It must be an asymmetric key pair. It must * allow the usage #PSA_KEY_USAGE_DECRYPT. From fe47e9ed89f546010235736df5cdfb4991e5210a Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Thu, 4 Jun 2026 19:38:27 +0200 Subject: [PATCH 09/20] Define and use mbedtls_rsa_decrypt_decompose_ret() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a more structured way of isolating and translating sensitive error codes from RSA PKCS#1v1.5 decryption. Signed-off-by: Gilles Peskine Signed-off-by: Manuel Pégourié-Gonnard --- library/pk_wrap.c | 28 ++++++--------- library/psa_crypto_rsa.c | 32 +++++++---------- library/rsa.c | 26 ++++++++++++++ library/rsa_internal.h | 30 ++++++++++++++++ tests/suites/test_suite_pkcs1_v15.data | 12 +++++++ tests/suites/test_suite_pkcs1_v15.function | 41 ++++++++++++++++++++++ 6 files changed, 133 insertions(+), 36 deletions(-) diff --git a/library/pk_wrap.c b/library/pk_wrap.c index 876523e5c2..45c274aa94 100644 --- a/library/pk_wrap.c +++ b/library/pk_wrap.c @@ -331,24 +331,18 @@ static int rsa_decrypt_wrap(mbedtls_pk_context *pk, NULL, 0, output, osize, olen); - /* We want to convert status into an MBEDTLS code without allowing an - * attacker to distinguish between 0, invalid padding, or buffer too - * small. (It's OK if the attacker distinguishes between one of the - * above three and other status such as out of memory.) Since - * PSA_PK_RSA_TO_MBEDTLS_ERR() is leaky, hide the difference from it. */ - mbedtls_ct_condition_t bad_padding = mbedtls_ct_uint_eq( - (mbedtls_ct_uint_t) status, - (mbedtls_ct_uint_t) PSA_ERROR_INVALID_PADDING); - mbedtls_ct_condition_t large_msg = mbedtls_ct_uint_eq( - (mbedtls_ct_uint_t) status, - (mbedtls_ct_uint_t) PSA_ERROR_BUFFER_TOO_SMALL); - status = mbedtls_ct_error_if(bad_padding, 0, status); - status = mbedtls_ct_error_if(large_msg, 0, status); + /* Translate error codes from PSA to legacy + * Success vs INVALID_PADDING vs BUFFER_TOO_SMALL is sensitive + * (padding oracle attack), so we take care to translate that + * part in constant time. + */ + int problem; + status = mbedtls_rsa_decrypt_decompose_ret( + PSA_ERROR_INVALID_PADDING, MBEDTLS_ERR_RSA_INVALID_PADDING, + PSA_ERROR_BUFFER_TOO_SMALL, MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE, + status, &problem); ret = PSA_PK_RSA_TO_MBEDTLS_ERR(status); - ret = mbedtls_ct_error_if(bad_padding, - MBEDTLS_ERR_RSA_INVALID_PADDING, ret); - ret = mbedtls_ct_error_if(large_msg, - MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE, ret); + ret |= problem; cleanup: mbedtls_zeroize_and_free(buf, buf_size); diff --git a/library/psa_crypto_rsa.c b/library/psa_crypto_rsa.c index 322ede0701..85a9f88d22 100644 --- a/library/psa_crypto_rsa.c +++ b/library/psa_crypto_rsa.c @@ -653,28 +653,22 @@ psa_status_t mbedtls_psa_asymmetric_decrypt(const psa_key_attributes_t *attribut if (alg == PSA_ALG_RSA_PKCS1V15_CRYPT) { #if defined(MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_CRYPT) - int ret = mbedtls_rsa_rsaes_pkcs1_v15_decrypt( + int combined_ret = mbedtls_rsa_rsaes_pkcs1_v15_decrypt( rsa, mbedtls_psa_get_random, MBEDTLS_PSA_RANDOM_STATE, output_length, input, output, output_size); - /* We want to convert ret into a psa status without allowing an - * attacker to distinguish between 0, invalid padding, or buffer too - * small. (It's OK if the attacker distinguishes between one of the - * above three and other status such as out of memory.) Since - * mbedtls_to_psa_error() is leaky, hide the difference from it. */ - mbedtls_ct_condition_t bad_padding = mbedtls_ct_uint_eq( - (mbedtls_ct_uint_t) ret, - (mbedtls_ct_uint_t) MBEDTLS_ERR_RSA_INVALID_PADDING); - mbedtls_ct_condition_t large_msg = mbedtls_ct_uint_eq( - (mbedtls_ct_uint_t) ret, - (mbedtls_ct_uint_t) MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); - ret = mbedtls_ct_error_if(bad_padding, 0, ret); - ret = mbedtls_ct_error_if(large_msg, 0, ret); - status = mbedtls_to_psa_error(ret); - status = mbedtls_ct_error_if(bad_padding, - PSA_ERROR_INVALID_PADDING, status); - status = mbedtls_ct_error_if(large_msg, - PSA_ERROR_BUFFER_TOO_SMALL, status); + /* Translate error codes from legacy to PSA. + * Success vs INVALID_PADDING vs OUTPUT_TOO_LARGE is sensitive + * (padding oracle attack), so we take care to translate that + * part in constant time. + */ + int problem; + int public_ret = mbedtls_rsa_decrypt_decompose_ret( + MBEDTLS_ERR_RSA_INVALID_PADDING, PSA_ERROR_INVALID_PADDING, + MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE, PSA_ERROR_BUFFER_TOO_SMALL, + combined_ret, &problem); + status = mbedtls_to_psa_error(public_ret); + status |= problem; #else status = PSA_ERROR_NOT_SUPPORTED; #endif /* MBEDTLS_PSA_BUILTIN_ALG_RSA_PKCS1V15_CRYPT */ diff --git a/library/rsa.c b/library/rsa.c index 94db8282c4..e2b8fdd26b 100644 --- a/library/rsa.c +++ b/library/rsa.c @@ -542,6 +542,32 @@ MBEDTLS_STATIC_TESTABLE int mbedtls_ct_rsaes_pkcs1_v15_unpadding( #endif /* MBEDTLS_PKCS1_V15 && MBEDTLS_RSA_C && ! MBEDTLS_RSA_ALT */ +#if defined(MBEDTLS_PKCS1_V15) && defined(MBEDTLS_RSA_C) +int mbedtls_rsa_decrypt_decompose_ret( + int invalid_padding_in, int invalid_padding_out, + int output_too_large_in, int output_too_large_out, + int combined_ret, + int *problem) +{ + *problem = 0; + int ret = combined_ret; + + const mbedtls_ct_condition_t invalid_padding_cond = + mbedtls_ct_uint_eq((mbedtls_ct_uint_t) ret, + (mbedtls_ct_uint_t) invalid_padding_in); + *problem = mbedtls_ct_error_if(invalid_padding_cond, invalid_padding_out, *problem); + ret = mbedtls_ct_error_if(invalid_padding_cond, 0, ret); + + const mbedtls_ct_condition_t output_too_large_cond = + mbedtls_ct_uint_eq((mbedtls_ct_uint_t) ret, + (mbedtls_ct_uint_t) output_too_large_in); + *problem = mbedtls_ct_error_if(output_too_large_cond, output_too_large_out, *problem); + ret = mbedtls_ct_error_if(output_too_large_cond, 0, ret); + + return ret; +} +#endif /* MBEDTLS_PKCS1_V15 && MBEDTLS_RSA_C */ + #if !defined(MBEDTLS_RSA_ALT) int mbedtls_rsa_import(mbedtls_rsa_context *ctx, diff --git a/library/rsa_internal.h b/library/rsa_internal.h index 3b6186c7e8..9fe1eef811 100644 --- a/library/rsa_internal.h +++ b/library/rsa_internal.h @@ -155,4 +155,34 @@ MBEDTLS_STATIC_TESTABLE int mbedtls_ct_rsaes_pkcs1_v15_unpadding( #endif /* MBEDTLS_PKCS1_V15 && MBEDTLS_RSA_C && ! MBEDTLS_RSA_ALT */ #endif /* MBEDTLS_TEST_HOOKS */ +#if defined(MBEDTLS_PKCS1_V15) && defined(MBEDTLS_RSA_C) +/** Decompose sensitive return values out of a return code, in constant time. + * + * \param invalid_padding_in The value of \p combined_ret that indicates + * invalid padding. + * \param invalid_padding_out The value to set \p problem to in case of + * invalid padding. + * \param output_too_large_in The value of \p combined_ret that indicates + * an insufficient output buffer size. + * \param output_too_large_out The value to set \p problem to in case of + * an insufficient output buffer size. + * \param combined_ret The value to decompose. + * \param[out] problem On output: + * - \p invalid_padding_out, + * if \p combined_ret = \p invalid_padding_in; + * - \p output_too_large_out, + * if \p combined_ret = \p output_too_large_in; + * - otherwise \c 0. + * + * \return - \c 0 if \p combined_ret = \p invalid_padding_in + * or \p combined_ret = \p output_too_large_in; + * - otherwise \c combined_ret. + */ +int mbedtls_rsa_decrypt_decompose_ret( + int invalid_padding_in, int invalid_padding_out, + int output_too_large_in, int output_too_large_out, + int combined_ret, + int *problem); +#endif + #endif /* rsa_internal.h */ diff --git a/tests/suites/test_suite_pkcs1_v15.data b/tests/suites/test_suite_pkcs1_v15.data index 44e5a0124d..49f4598898 100644 --- a/tests/suites/test_suite_pkcs1_v15.data +++ b/tests/suites/test_suite_pkcs1_v15.data @@ -52,6 +52,18 @@ RSASSA-V15 Verification Test Vector Int depends_on:MBEDTLS_MD_CAN_SHA1 pkcs1_rsassa_v15_verify:1024:"a2ba40ee07e3b2bd2f02ce227f36a195024486e49c19cb41bbbdfbba98b22b0e577c2eeaffa20d883a76e65e394c69d4b3c05a1e8fadda27edb2a42bc000fe888b9b32c22d15add0cd76b3e7936e19955b220dd17d4ea904b1ec102b2e4de7751222aa99151024c7cb41cc5ea21d00eeb41f7c800834d2c6e06bce3bce7ea9a5":"010001":MBEDTLS_MD_SHA1:MBEDTLS_MD_SHA1:"37b66ae0445843353d47ecb0b4fd14c110e62d6a":"e3b5d5d002c1bce50c2b65ef88a188d83bce7e61":"2154f928615e5101fcdeb57bc08fc2f35c3d5996403861ae3efb1d0712f8bb05cc21f7f5f11f62e5b6ea9f0f2b62180e5cbe7ba535032d6ac8068fff7f362f73d2c3bf5eca6062a1723d7cfd5abb6dcf7e405f2dc560ffe6fc37d38bee4dc9e24fe2bece3e3b4a3f032701d3f0947b42930083dd4ad241b3309b514595482d42":0 +RSA sensitive ret: 0 +rsa_decrypt_decompose_ret:0:0:0 + +RSA sensitive ret: sensitive 1 +rsa_decrypt_decompose_ret:-1:0:-11 + +RSA sensitive ret: sensitive 2 +rsa_decrypt_decompose_ret:-2:0:-12 + +RSA sensitive ret: public +rsa_decrypt_decompose_ret:-3:-3:0 + RSAES-V15 decoding: good, payload=max, tight output buffer pkcs1_v15_decode:"0002505152535455565700":117:117:0 diff --git a/tests/suites/test_suite_pkcs1_v15.function b/tests/suites/test_suite_pkcs1_v15.function index e7c1ce22fd..0478b75820 100644 --- a/tests/suites/test_suite_pkcs1_v15.function +++ b/tests/suites/test_suite_pkcs1_v15.function @@ -157,6 +157,47 @@ exit: } /* END_CASE */ +/* BEGIN_CASE */ +void rsa_decrypt_decompose_ret(int input, + int expected_ret, int expected_problem) +{ + const int sensitive_in_1 = -1; + const int sensitive_in_2 = -2; + const int problem_1 = -11; + const int problem_2 = -12; + + int combined_ret = input; + TEST_CF_SECRET(&combined_ret, sizeof(combined_ret)); + int actual_problem = 42; + TEST_CF_SECRET(&actual_problem, sizeof(actual_problem)); + + int actual_ret = mbedtls_rsa_decrypt_decompose_ret(sensitive_in_1, problem_1, + sensitive_in_2, problem_2, + input, + &actual_problem); + + TEST_EQUAL(actual_problem, expected_problem); + TEST_EQUAL(actual_ret, expected_ret); + + /* Check invariant when there's no translation, only separation */ + actual_ret = mbedtls_rsa_decrypt_decompose_ret(sensitive_in_1, sensitive_in_1, + sensitive_in_2, sensitive_in_2, + input, + &actual_problem); + TEST_EQUAL(actual_ret, expected_ret); + if (expected_problem == problem_1) { + TEST_EQUAL(actual_problem, sensitive_in_1); + } else if (expected_problem == problem_2) { + TEST_EQUAL(actual_problem, sensitive_in_2); + } else { + TEST_EQUAL(actual_problem, 0); + } + TEST_CF_PUBLIC(&actual_problem, sizeof(actual_problem)); + TEST_CF_PUBLIC(&combined_ret, sizeof(combined_ret)); + TEST_EQUAL(actual_problem | actual_ret, combined_ret); +} +/* END_CASE */ + /* BEGIN_CASE */ void pkcs1_v15_decode(data_t *input, int expected_plaintext_length_arg, From 7e51c31aef2eab74d792a48ad3bb975764127122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 8 Jun 2026 11:14:53 +0200 Subject: [PATCH 10/20] rsa/psa: tune doc about RSA v1.5 decrypt usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- include/mbedtls/rsa.h | 7 ++++--- include/psa/crypto.h | 8 ++++++-- include/psa/crypto_values.h | 3 +-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/mbedtls/rsa.h b/include/mbedtls/rsa.h index d528b0a864..ac3a0f2c2c 100644 --- a/include/mbedtls/rsa.h +++ b/include/mbedtls/rsa.h @@ -737,12 +737,13 @@ int mbedtls_rsa_pkcs1_decrypt(mbedtls_rsa_context *ctx, * operation (RSAES-PKCS1-v1_5-DECRYPT). * * \warning This is an inherently dangerous function (CWE-242). Unless - * it is used in a side channel free and safe way (eg. - * implementing the TLS protocol as per 7.4.7.1 of RFC 5246), + * it is used in a side channel free and safe way, * the calling code is vulnerable. * Specifically, callers need to ensure an adversary cannot * distinguish between success, MBEDTLS_ERR_RSA_INVALID_PADDING - * and MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE via side channels. + * and MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE. Also, in the latter two + * cases, the values of the output bytes must be ignored, again + * without revealing whether that's the case. * * \note The output buffer length \c output_max_len should be * as large as the size \p ctx->len of \p ctx->N, for example, diff --git a/include/psa/crypto.h b/include/psa/crypto.h index 1a86af7261..7243ea1e5d 100644 --- a/include/psa/crypto.h +++ b/include/psa/crypto.h @@ -3127,9 +3127,13 @@ psa_status_t psa_asymmetric_encrypt(mbedtls_svc_key_id_t key, * * \warning When \p alg is #PSA_ALG_RSA_PKCS1V15_CRYPT, this is an * inherently dangerous function (CWE-242): unless it is used - * in a side channel free and safe way (eg. implementing the - * TLS protocol as per 7.4.7.1 of RFC 5246), the calling code + * in a side channel free and safe way, the calling code * is vulnerable. + * Specifically, callers need to ensure an adversary cannot + * distinguish between success, #PSA_ERROR_INVALID_PADDING and + * #PSA_ERROR_BUFFER_TOO_SMALL. Also, in the latter two cases, + * the values of the output bytes must be ignored, again + * without revealing whether that's the case. * * \param key Identifier of the key to use for the operation. * It must be an asymmetric key pair. It must diff --git a/include/psa/crypto_values.h b/include/psa/crypto_values.h index 1d678dbfc2..5e49e0d70e 100644 --- a/include/psa/crypto_values.h +++ b/include/psa/crypto_values.h @@ -1760,8 +1760,7 @@ * \warning Calling psa_asymmetric_decrypt() with this algorithm as a * parameter is considered an inherently dangerous function * (CWE-242). Unless it is used in a side channel free and safe - * way (eg. implementing the TLS protocol as per 7.4.7.1 of - * RFC 5246), the calling code is vulnerable. + * way, the calling code is vulnerable. * */ #define PSA_ALG_RSA_PKCS1V15_CRYPT ((psa_algorithm_t) 0x07000200) From 31ac657745112b1903bed1784a496c0c5b7b2e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 8 Jun 2026 11:18:40 +0200 Subject: [PATCH 11/20] rsa: move CT tests to .constant_time.data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- .../test_suite_pkcs1_v15.constant_time.data | 53 ++++++++++++++++++ tests/suites/test_suite_pkcs1_v15.data | 54 ------------------- 2 files changed, 53 insertions(+), 54 deletions(-) create mode 100644 tests/suites/test_suite_pkcs1_v15.constant_time.data diff --git a/tests/suites/test_suite_pkcs1_v15.constant_time.data b/tests/suites/test_suite_pkcs1_v15.constant_time.data new file mode 100644 index 0000000000..08dfa35862 --- /dev/null +++ b/tests/suites/test_suite_pkcs1_v15.constant_time.data @@ -0,0 +1,53 @@ +RSA sensitive ret: 0 +rsa_decrypt_decompose_ret:0:0:0 + +RSA sensitive ret: sensitive 1 +rsa_decrypt_decompose_ret:-1:0:-11 + +RSA sensitive ret: sensitive 2 +rsa_decrypt_decompose_ret:-2:0:-12 + +RSA sensitive ret: public +rsa_decrypt_decompose_ret:-3:-3:0 + +RSAES-V15 decoding: good, payload=max, tight output buffer +pkcs1_v15_decode:"0002505152535455565700":117:117:0 + +RSAES-V15 decoding: good, payload=max, larger output buffer +pkcs1_v15_decode:"0002505152535455565700":117:128:0 + +RSAES-V15 decoding: good, payload=max-1, tight output buffer +pkcs1_v15_decode:"000250515253545556575800":116:116:0 + +RSAES-V15 decoding: good, payload=max-1, larger output buffer +pkcs1_v15_decode:"000250515253545556575800":116:117:0 + +RSAES-V15 decoding: good, payload=1 +pkcs1_v15_decode:"00025050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505000":1:1:0 + +RSAES-V15 decoding: good, empty payload +pkcs1_v15_decode:"0002505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505000":0:0:0 + +RSAES-V15 decoding: payload=max, output too large +pkcs1_v15_decode:"0002505152535455565700":117:116:MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE + +RSAES-V15 decoding: payload=max-1, output too large +pkcs1_v15_decode:"000250515253545556575800":116:115:MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE + +RSAES-V15 decoding: bad first byte +pkcs1_v15_decode:"0102505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING + +RSAES-V15 decoding: bad second byte (0 instead of 2) +pkcs1_v15_decode:"0000505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING + +RSAES-V15 decoding: bad second byte (1 instead of 2) +pkcs1_v15_decode:"0001505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING + +RSAES-V15 decoding: padding too short (0) +pkcs1_v15_decode:"000200":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING + +RSAES-V15 decoding: padding too short (7) +pkcs1_v15_decode:"0002505050505050500000ffffffffffffffffff00":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING + +RSAES-V15 decoding: unfinished padding +pkcs1_v15_decode:"0002505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING diff --git a/tests/suites/test_suite_pkcs1_v15.data b/tests/suites/test_suite_pkcs1_v15.data index 49f4598898..128974b6d7 100644 --- a/tests/suites/test_suite_pkcs1_v15.data +++ b/tests/suites/test_suite_pkcs1_v15.data @@ -51,57 +51,3 @@ pkcs1_rsassa_v15_sign:1024:"d17f655bf27c8b16d35462c905cc04a26f37e2a67fa9c0ce0dce RSASSA-V15 Verification Test Vector Int depends_on:MBEDTLS_MD_CAN_SHA1 pkcs1_rsassa_v15_verify:1024:"a2ba40ee07e3b2bd2f02ce227f36a195024486e49c19cb41bbbdfbba98b22b0e577c2eeaffa20d883a76e65e394c69d4b3c05a1e8fadda27edb2a42bc000fe888b9b32c22d15add0cd76b3e7936e19955b220dd17d4ea904b1ec102b2e4de7751222aa99151024c7cb41cc5ea21d00eeb41f7c800834d2c6e06bce3bce7ea9a5":"010001":MBEDTLS_MD_SHA1:MBEDTLS_MD_SHA1:"37b66ae0445843353d47ecb0b4fd14c110e62d6a":"e3b5d5d002c1bce50c2b65ef88a188d83bce7e61":"2154f928615e5101fcdeb57bc08fc2f35c3d5996403861ae3efb1d0712f8bb05cc21f7f5f11f62e5b6ea9f0f2b62180e5cbe7ba535032d6ac8068fff7f362f73d2c3bf5eca6062a1723d7cfd5abb6dcf7e405f2dc560ffe6fc37d38bee4dc9e24fe2bece3e3b4a3f032701d3f0947b42930083dd4ad241b3309b514595482d42":0 - -RSA sensitive ret: 0 -rsa_decrypt_decompose_ret:0:0:0 - -RSA sensitive ret: sensitive 1 -rsa_decrypt_decompose_ret:-1:0:-11 - -RSA sensitive ret: sensitive 2 -rsa_decrypt_decompose_ret:-2:0:-12 - -RSA sensitive ret: public -rsa_decrypt_decompose_ret:-3:-3:0 - -RSAES-V15 decoding: good, payload=max, tight output buffer -pkcs1_v15_decode:"0002505152535455565700":117:117:0 - -RSAES-V15 decoding: good, payload=max, larger output buffer -pkcs1_v15_decode:"0002505152535455565700":117:128:0 - -RSAES-V15 decoding: good, payload=max-1, tight output buffer -pkcs1_v15_decode:"000250515253545556575800":116:116:0 - -RSAES-V15 decoding: good, payload=max-1, larger output buffer -pkcs1_v15_decode:"000250515253545556575800":116:117:0 - -RSAES-V15 decoding: good, payload=1 -pkcs1_v15_decode:"00025050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505000":1:1:0 - -RSAES-V15 decoding: good, empty payload -pkcs1_v15_decode:"0002505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505000":0:0:0 - -RSAES-V15 decoding: payload=max, output too large -pkcs1_v15_decode:"0002505152535455565700":117:116:MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE - -RSAES-V15 decoding: payload=max-1, output too large -pkcs1_v15_decode:"000250515253545556575800":116:115:MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE - -RSAES-V15 decoding: bad first byte -pkcs1_v15_decode:"0102505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING - -RSAES-V15 decoding: bad second byte (0 instead of 2) -pkcs1_v15_decode:"0000505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING - -RSAES-V15 decoding: bad second byte (1 instead of 2) -pkcs1_v15_decode:"0001505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING - -RSAES-V15 decoding: padding too short (0) -pkcs1_v15_decode:"000200":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING - -RSAES-V15 decoding: padding too short (7) -pkcs1_v15_decode:"0002505050505050500000ffffffffffffffffff00":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING - -RSAES-V15 decoding: unfinished padding -pkcs1_v15_decode:"0002505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050":0:42:MBEDTLS_ERR_RSA_INVALID_PADDING From 88d55b81408a681f2d9507a86bc274e91f963ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 8 Jun 2026 11:25:14 +0200 Subject: [PATCH 12/20] Add ChangeLog for the latest RSA padding issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- ChangeLog.d/rsa-padding.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 ChangeLog.d/rsa-padding.txt diff --git a/ChangeLog.d/rsa-padding.txt b/ChangeLog.d/rsa-padding.txt new file mode 100644 index 0000000000..3580a2bcf2 --- /dev/null +++ b/ChangeLog.d/rsa-padding.txt @@ -0,0 +1,8 @@ +Security + * Fix a side channel in RSA PKCS#1 v1.5 decryption: error handling in the + library would reveal through timing the difference between success, + invalid padding, or output too large for buffer, giving rise to a + Bleichenbacher attack. Note that while the library now handles sensitive + errors in constant-time, RSA PKCS#1 v1.5 decryption remains inherently + dangerous, should be avoided when possible, and otherwise requires great + care in application code. Found by zhengg. CVE-2026-50587 From c76cfd536e488b1397d5077e667e84339e471fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 8 Jun 2026 11:56:59 +0200 Subject: [PATCH 13/20] pk: also fix RSA-opaque v1.5 decrypt side channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- library/pk_wrap.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/library/pk_wrap.c b/library/pk_wrap.c index 45c274aa94..8ecda036c6 100644 --- a/library/pk_wrap.c +++ b/library/pk_wrap.c @@ -1512,11 +1512,17 @@ static int rsa_opaque_decrypt(mbedtls_pk_context *pk, } status = psa_asymmetric_decrypt(pk->priv_id, alg, input, ilen, NULL, 0, output, osize, olen); - if (status != PSA_SUCCESS) { - return PSA_PK_RSA_TO_MBEDTLS_ERR(status); - } - - return 0; + /* Translate error codes from PSA to legacy + * Success vs INVALID_PADDING vs BUFFER_TOO_SMALL is sensitive + * (padding oracle attack), so we take care to translate that + * part in constant time. + */ + int problem; + status = mbedtls_rsa_decrypt_decompose_ret( + PSA_ERROR_INVALID_PADDING, MBEDTLS_ERR_RSA_INVALID_PADDING, + PSA_ERROR_BUFFER_TOO_SMALL, MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE, + status, &problem); + return PSA_PK_RSA_TO_MBEDTLS_ERR(status) | problem; } #endif /* PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_BASIC */ From 4813b0c09349f183dc68aa74a84e83f8c3238788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 8 Jun 2026 12:00:02 +0200 Subject: [PATCH 14/20] pk: fix builds without PKCS#1 v1.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- library/pk_wrap.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/pk_wrap.c b/library/pk_wrap.c index 8ecda036c6..b8e5160e57 100644 --- a/library/pk_wrap.c +++ b/library/pk_wrap.c @@ -331,6 +331,7 @@ static int rsa_decrypt_wrap(mbedtls_pk_context *pk, NULL, 0, output, osize, olen); +#if defined(MBEDTLS_PKCS1_V15) /* Translate error codes from PSA to legacy * Success vs INVALID_PADDING vs BUFFER_TOO_SMALL is sensitive * (padding oracle attack), so we take care to translate that @@ -343,6 +344,9 @@ static int rsa_decrypt_wrap(mbedtls_pk_context *pk, status, &problem); ret = PSA_PK_RSA_TO_MBEDTLS_ERR(status); ret |= problem; +#else + ret = PSA_PK_RSA_TO_MBEDTLS_ERR(status); +#endif cleanup: mbedtls_zeroize_and_free(buf, buf_size); @@ -1512,6 +1516,7 @@ static int rsa_opaque_decrypt(mbedtls_pk_context *pk, } status = psa_asymmetric_decrypt(pk->priv_id, alg, input, ilen, NULL, 0, output, osize, olen); +#if defined(MBEDTLS_PKCS1_V15) /* Translate error codes from PSA to legacy * Success vs INVALID_PADDING vs BUFFER_TOO_SMALL is sensitive * (padding oracle attack), so we take care to translate that @@ -1523,6 +1528,9 @@ static int rsa_opaque_decrypt(mbedtls_pk_context *pk, PSA_ERROR_BUFFER_TOO_SMALL, MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE, status, &problem); return PSA_PK_RSA_TO_MBEDTLS_ERR(status) | problem; +#else + return PSA_PK_RSA_TO_MBEDTLS_ERR(status); +#endif } #endif /* PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_BASIC */ From d73eec656c71bdb809bb59747a446ae9f575d45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Wed, 10 Jun 2026 09:57:20 +0200 Subject: [PATCH 15/20] psa: test buffer sized just for actual plaintext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- tests/suites/test_suite_psa_crypto.function | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/suites/test_suite_psa_crypto.function b/tests/suites/test_suite_psa_crypto.function index 6d560928b9..c73aa9ad86 100644 --- a/tests/suites/test_suite_psa_crypto.function +++ b/tests/suites/test_suite_psa_crypto.function @@ -8744,6 +8744,12 @@ void asymmetric_encrypt_decrypt(int key_type_arg, TEST_LE_U(output_size, PSA_ASYMMETRIC_ENCRYPT_OUTPUT_MAX_SIZE); TEST_CALLOC(output, output_size); + /* We want the function to work with an output buffer that's just the size + * of the actual plaintext. The PSA spec actually requires callers to pass + * a buffer that's the maximum possible plaintext size, given by + * PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(). But historically we've accepted + * smaller sizes if the actual plaintext fits. People might depend on this + * (our TLS code does), so let's preserve this behaviour in LTS branches. */ output2_size = input_data->len; TEST_LE_U(output2_size, PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(key_type, key_bits, alg)); @@ -8813,8 +8819,15 @@ void asymmetric_decrypt(int key_type_arg, PSA_ASSERT(psa_get_key_attributes(key, &attributes)); key_bits = psa_get_key_bits(&attributes); - /* Determine the maximum ciphertext length */ - output_size = PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(key_type, key_bits, alg); + /* We want the function to work with an output buffer that's just the size + * of the actual plaintext. The PSA spec actually requires callers to pass + * a buffer that's the maximum possible plaintext size, given by + * PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(). But historically we've accepted + * smaller sizes if the actual plaintext fits. People might depend on this + * (our TLS code does), so let's preserve this behaviour in LTS branches. */ + output_size = expected_data->len; + TEST_LE_U(output_size, + PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(key_type, key_bits, alg)); TEST_LE_U(output_size, PSA_ASYMMETRIC_DECRYPT_OUTPUT_MAX_SIZE); TEST_CALLOC(output, output_size); From 9516cc4e70e41d6cb6e13791b8177598a8808aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Thu, 11 Jun 2026 10:40:11 +0200 Subject: [PATCH 16/20] psa: test buffer too small for RSA decrypt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- tests/suites/test_suite_psa_crypto.function | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/suites/test_suite_psa_crypto.function b/tests/suites/test_suite_psa_crypto.function index c73aa9ad86..090d18d9a3 100644 --- a/tests/suites/test_suite_psa_crypto.function +++ b/tests/suites/test_suite_psa_crypto.function @@ -8776,6 +8776,16 @@ void asymmetric_encrypt_decrypt(int key_type_arg, TEST_MEMORY_COMPARE(input_data->x, input_data->len, output2, output2_length); + /* Try again with an output buffer that's one byte too small */ + if (output2_size != 0) { + TEST_EQUAL(PSA_ERROR_BUFFER_TOO_SMALL, + psa_asymmetric_decrypt(key, alg, + output, output_length, + label->x, label->len, + output2, output2_size - 1, + &output2_length)); + } + exit: /* * Key attributes may have been returned by psa_get_key_attributes() @@ -8857,6 +8867,17 @@ void asymmetric_decrypt(int key_type_arg, output, output_length); } + /* Try again with an output buffer that's one byte too small */ + if (output_size != 0) { + TEST_EQUAL(PSA_ERROR_BUFFER_TOO_SMALL, + psa_asymmetric_decrypt(key, alg, + input_data->x, input_data->len, + label->x, label->len, + output, + output_size - 1, + &output_length)); + } + exit: psa_reset_key_attributes(&attributes); psa_destroy_key(key); From 29108ed8ce8c9f7ac6e6d0493f0ffb8c3c6cc838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Thu, 11 Jun 2026 11:52:18 +0200 Subject: [PATCH 17/20] pk: test buffer too small for RSA-opaque + RSA-alt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A previous commit added tests "normal" PK RSA contexts, but missed opaque RSA keys and RSA_ALT keys. Signed-off-by: Manuel Pégourié-Gonnard --- tests/suites/test_suite_pk.function | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/suites/test_suite_pk.function b/tests/suites/test_suite_pk.function index 6d397f096d..ea065095e5 100644 --- a/tests/suites/test_suite_pk.function +++ b/tests/suites/test_suite_pk.function @@ -1624,6 +1624,12 @@ void pk_wrap_rsa_decrypt_test_vec(data_t *cipher, int mod, if (ret == 0) { TEST_EQUAL(olen, clear->len); TEST_EQUAL(memcmp(output, clear->x, olen), 0); + + /* Try again with an output buffer that's too small */ + TEST_EQUAL(mbedtls_pk_decrypt(&pk, cipher->x, cipher->len, + output, &olen, clear->len - 1, + mbedtls_test_rnd_pseudo_rand, &rnd_info), + MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); } TEST_EQUAL(PSA_SUCCESS, psa_destroy_key(key_id)); @@ -1795,6 +1801,13 @@ void pk_rsa_alt() TEST_ASSERT(test_len == sizeof(msg)); TEST_ASSERT(memcmp(test, msg, test_len) == 0); + + /* Try again with an output buffer that's too small */ + TEST_EQUAL(mbedtls_pk_decrypt(&alt, ciph, ciph_len, + test, &test_len, sizeof(msg) - 1, + mbedtls_test_rnd_std_rand, NULL), + MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); + /* Test forbidden operations */ TEST_ASSERT(mbedtls_pk_encrypt(&alt, msg, sizeof(msg), ciph, &ciph_len, sizeof(ciph), From 9b3d8dc174b5d9d4d8b7c85330b37df495aa12e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Thu, 11 Jun 2026 12:26:16 +0200 Subject: [PATCH 18/20] rsa: fix constant-time test for decompose_ret() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were passing input, while only combined_ret is marked as secret... Signed-off-by: Manuel Pégourié-Gonnard --- tests/suites/test_suite_pkcs1_v15.function | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/suites/test_suite_pkcs1_v15.function b/tests/suites/test_suite_pkcs1_v15.function index 0478b75820..e43b77cfec 100644 --- a/tests/suites/test_suite_pkcs1_v15.function +++ b/tests/suites/test_suite_pkcs1_v15.function @@ -173,7 +173,7 @@ void rsa_decrypt_decompose_ret(int input, int actual_ret = mbedtls_rsa_decrypt_decompose_ret(sensitive_in_1, problem_1, sensitive_in_2, problem_2, - input, + combined_ret, &actual_problem); TEST_EQUAL(actual_problem, expected_problem); @@ -182,7 +182,7 @@ void rsa_decrypt_decompose_ret(int input, /* Check invariant when there's no translation, only separation */ actual_ret = mbedtls_rsa_decrypt_decompose_ret(sensitive_in_1, sensitive_in_1, sensitive_in_2, sensitive_in_2, - input, + combined_ret, &actual_problem); TEST_EQUAL(actual_ret, expected_ret); if (expected_problem == problem_1) { From 255fd41e1ef4619e4743acd1c687de8966a9a821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Thu, 11 Jun 2026 16:50:29 +0200 Subject: [PATCH 19/20] pk: tests: avoid corner case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- tests/suites/test_suite_pk.function | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/suites/test_suite_pk.function b/tests/suites/test_suite_pk.function index ea065095e5..aad93cbae7 100644 --- a/tests/suites/test_suite_pk.function +++ b/tests/suites/test_suite_pk.function @@ -1547,11 +1547,13 @@ void pk_rsa_decrypt_test_vec(data_t *cipher, int mod, int padding, int md_alg, TEST_ASSERT(olen == clear->len); TEST_ASSERT(memcmp(output, clear->x, olen) == 0); - /* Try again with an output buffer that's too small */ - TEST_EQUAL(mbedtls_pk_decrypt(&pk, cipher->x, cipher->len, - output, &olen, clear->len - 1, - mbedtls_test_rnd_pseudo_rand, &rnd_info), - MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); + if (clear->len != 0) { + /* Try again with an output buffer that's too small */ + TEST_EQUAL(mbedtls_pk_decrypt(&pk, cipher->x, cipher->len, + output, &olen, clear->len - 1, + mbedtls_test_rnd_pseudo_rand, &rnd_info), + MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); + } } exit: @@ -1625,11 +1627,13 @@ void pk_wrap_rsa_decrypt_test_vec(data_t *cipher, int mod, TEST_EQUAL(olen, clear->len); TEST_EQUAL(memcmp(output, clear->x, olen), 0); - /* Try again with an output buffer that's too small */ - TEST_EQUAL(mbedtls_pk_decrypt(&pk, cipher->x, cipher->len, - output, &olen, clear->len - 1, - mbedtls_test_rnd_pseudo_rand, &rnd_info), - MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); + if (clear->len != 0) { + /* Try again with an output buffer that's too small */ + TEST_EQUAL(mbedtls_pk_decrypt(&pk, cipher->x, cipher->len, + output, &olen, clear->len - 1, + mbedtls_test_rnd_pseudo_rand, &rnd_info), + MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE); + } } TEST_EQUAL(PSA_SUCCESS, psa_destroy_key(key_id)); From 591982e0477b6a70dba525b72073054cfdd5996b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=A9gouri=C3=A9-Gonnard?= Date: Mon, 15 Jun 2026 10:17:12 +0200 Subject: [PATCH 20/20] Clarify a commend in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Pégourié-Gonnard --- tests/suites/test_suite_psa_crypto.function | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/suites/test_suite_psa_crypto.function b/tests/suites/test_suite_psa_crypto.function index 090d18d9a3..86bb694e5f 100644 --- a/tests/suites/test_suite_psa_crypto.function +++ b/tests/suites/test_suite_psa_crypto.function @@ -8745,11 +8745,13 @@ void asymmetric_encrypt_decrypt(int key_type_arg, TEST_CALLOC(output, output_size); /* We want the function to work with an output buffer that's just the size - * of the actual plaintext. The PSA spec actually requires callers to pass + * of the actual plaintext. The PSA spec requires portable callers to pass * a buffer that's the maximum possible plaintext size, given by - * PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(). But historically we've accepted - * smaller sizes if the actual plaintext fits. People might depend on this - * (our TLS code does), so let's preserve this behaviour in LTS branches. */ + * PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(). When the buffer is smaller than + * that but large enough for the actual plaintext, implementations may either + * return PSA_ERROR_BUFFER_TOO_SMALL or success. Historically we've done the + * latter. Even though we never promised it, people might depend on this + * (our TLS code does), so let's test this behaviour in LTS branches. */ output2_size = input_data->len; TEST_LE_U(output2_size, PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(key_type, key_bits, alg)); @@ -8830,11 +8832,13 @@ void asymmetric_decrypt(int key_type_arg, key_bits = psa_get_key_bits(&attributes); /* We want the function to work with an output buffer that's just the size - * of the actual plaintext. The PSA spec actually requires callers to pass + * of the actual plaintext. The PSA spec requires portable callers to pass * a buffer that's the maximum possible plaintext size, given by - * PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(). But historically we've accepted - * smaller sizes if the actual plaintext fits. People might depend on this - * (our TLS code does), so let's preserve this behaviour in LTS branches. */ + * PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(). When the buffer is smaller than + * that but large enough for the actual plaintext, implementations may either + * return PSA_ERROR_BUFFER_TOO_SMALL or success. Historically we've done the + * latter. Even though we never promised it, people might depend on this + * (our TLS code does), so let's test this behaviour in LTS branches. */ output_size = expected_data->len; TEST_LE_U(output_size, PSA_ASYMMETRIC_DECRYPT_OUTPUT_SIZE(key_type, key_bits, alg));