Commit d4d338b3a0 for openssl.org
commit d4d338b3a0293985285e58b53136f8133220f3f8
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Sun Sep 20 11:52:05 2026 +0900
cms: check recipient key lengths during staged decryption
CMS_decrypt_set1_pkey_and_peer() processes recipients before the
subsequent CMS_decrypt() call sets havenocert. On a fresh object with
no recipient certificate, this bypasses the expected-key-length
filter and lets a later wrong-length candidate replace a usable key.
Set the mode in the key setter before processing recipients. Keep
the initialization in CMS_decrypt() for callers that initialize the
operation before using the direct recipient APIs.
Initialize the content cipher and interpret its parameters, then
check whether it accepts the recovered key length before replacing
the stored key. Comparing only against the cipher's default length
would reject valid RC2-40, RC2-64 and RC4-40 content keys. Clear any
rejected candidate and fetch the cipher by numeric OID consistently
with content initialization.
Add plaintext regression checks for the first and repeated decrypts
through both API sequences, and document the setter's mode selection.
Skip the legacy cipher cases when that provider is unavailable.
Fixes #32892
Assisted-by: Codex:gpt-6-astra
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Richard Levitte <levitte@openssl.foundation>
Merge-date: Wed Nov 4 17:51:20 2026
Merged-from: https://github.com/openssl/openssl/pull/32893
diff --git a/crypto/cms/cms_env.c b/crypto/cms/cms_env.c
index 659f94c04d..d71ac2a514 100644
--- a/crypto/cms/cms_env.c
+++ b/crypto/cms/cms_env.c
@@ -625,9 +625,10 @@ static int cms_RecipientInfo_ktri_decrypt(CMS_ContentInfo *cms,
CMS_KeyTransRecipientInfo *ktri = ri->d.ktri;
EVP_PKEY *pkey = ktri->pkey;
unsigned char *ek = NULL;
- size_t eklen;
+ size_t eklen = 0;
int ret = 0;
- size_t fixlen = 0;
+ EVP_CIPHER_CTX *cctx = NULL;
+ evp_cipher_aead_asn1_params aparams;
const EVP_CIPHER *cipher = NULL;
EVP_CIPHER *fetched_cipher = NULL;
CMS_EncryptedContentInfo *ec;
@@ -647,7 +648,7 @@ static int cms_RecipientInfo_ktri_decrypt(CMS_ContentInfo *cms,
X509_ALGOR *calg = ec->contentEncryptionAlgorithm;
char name[OSSL_MAX_NAME_SIZE];
- OBJ_obj2txt(name, sizeof(name), calg->algorithm, 0);
+ OBJ_obj2txt(name, sizeof(name), calg->algorithm, 1);
(void)ERR_set_mark();
fetched_cipher = EVP_CIPHER_fetch(libctx, name, propq);
@@ -663,8 +664,13 @@ static int cms_RecipientInfo_ktri_decrypt(CMS_ContentInfo *cms,
}
(void)ERR_pop_to_mark();
- fixlen = EVP_CIPHER_get_key_length(cipher);
- EVP_CIPHER_free(fetched_cipher);
+ cctx = EVP_CIPHER_CTX_new();
+ if (cctx == NULL
+ || EVP_DecryptInit_ex(cctx, cipher, NULL, NULL, NULL) <= 0
+ || evp_cipher_asn1_to_param_ex(cctx, calg->parameter, &aparams) <= 0) {
+ ERR_raise(ERR_LIB_CMS, CMS_R_CIPHER_PARAMETER_INITIALISATION_ERROR);
+ goto err;
+ }
}
ktri->pctx = EVP_PKEY_CTX_new_from_pkey(libctx, pkey, propq);
@@ -688,12 +694,18 @@ static int cms_RecipientInfo_ktri_decrypt(CMS_ContentInfo *cms,
if (!ec->havenocert && !ec->debug)
ec->harderr = cms_ktri_harderr_ok(ktri->pctx, pkey);
- if (evp_pkey_decrypt_alloc(ktri->pctx, &ek, &eklen, fixlen,
+ if (evp_pkey_decrypt_alloc(ktri->pctx, &ek, &eklen, 0,
ktri->encryptedKey->data,
ktri->encryptedKey->length)
<= 0)
goto err;
+ /* Use the same accepted key lengths as content initialization. */
+ if (cctx != NULL
+ && (eklen > INT_MAX
+ || EVP_CIPHER_CTX_set_key_length(cctx, (int)eklen) <= 0))
+ goto err;
+
ret = 1;
OPENSSL_clear_free(ec->key, ec->keylen);
@@ -701,10 +713,12 @@ static int cms_RecipientInfo_ktri_decrypt(CMS_ContentInfo *cms,
ec->keylen = eklen;
err:
+ EVP_CIPHER_CTX_free(cctx);
+ EVP_CIPHER_free(fetched_cipher);
EVP_PKEY_CTX_free(ktri->pctx);
ktri->pctx = NULL;
if (!ret)
- OPENSSL_free(ek);
+ OPENSSL_clear_free(ek, eklen);
return ret;
}
diff --git a/crypto/cms/cms_smime.c b/crypto/cms/cms_smime.c
index 6ed16afd82..a85f848265 100644
--- a/crypto/cms/cms_smime.c
+++ b/crypto/cms/cms_smime.c
@@ -834,6 +834,8 @@ int CMS_decrypt_set1_pkey_and_peer(CMS_ContentInfo *cms, EVP_PKEY *pk,
ec->key = NULL;
ec->keylen = 0;
ec->harderr = 0;
+ /* Select the mode before processing recipients in the staged API. */
+ ec->havenocert = cert == NULL;
}
if (ris != NULL && ec != NULL)
diff --git a/doc/man3/CMS_decrypt.pod b/doc/man3/CMS_decrypt.pod
index 4e40b37f21..02f33202dd 100644
--- a/doc/man3/CMS_decrypt.pod
+++ b/doc/man3/CMS_decrypt.pod
@@ -44,6 +44,9 @@ This call deallocates any decryption key stored in I<cms>.
CMS_decrypt_set1_pkey() is the same as
CMS_decrypt_set1_pkey_and_peer() with I<peer> being NULL.
+For either setter, recipient selection and content-key length checks use the
+current I<cert> argument, including when I<cms> is reused.
+
CMS_decrypt_set1_password() decrypts the CMS_ContentInfo structure I<cms>
using the secret I<pass> of length I<passlen>.
On success, it also records in I<cms> the decryption key used, and then
diff --git a/test/cmsapitest.c b/test/cmsapitest.c
index a38793c3c8..bd291167f9 100644
--- a/test/cmsapitest.c
+++ b/test/cmsapitest.c
@@ -15,6 +15,8 @@
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/x509.h>
+#include <openssl/rsa.h>
+#include <openssl/provider.h>
#include "../crypto/cms/cms_local.h" /* for d.signedData and d.envelopedData */
#include "testutil.h"
@@ -366,6 +368,113 @@ end:
return testresult;
}
+/* Exercise the key-length filter through both decryption API sequences. */
+static int test_decrypt_key_length(int idx)
+{
+ static const char msg[] = "CMS content key length check";
+ static const unsigned char wrongkey[17] = { 0 };
+ const EVP_CIPHER *cipher = EVP_aes_128_gcm();
+ OSSL_PROVIDER *legacy = NULL;
+ STACK_OF(X509) *recips = NULL;
+ CMS_ContentInfo *cms = NULL, *decoded = NULL;
+ CMS_RecipientInfo *ri;
+ EVP_PKEY_CTX *pctx = NULL;
+ X509 *second = NULL;
+ BIO *in = NULL, *out = NULL;
+ unsigned char *ek = NULL;
+ size_t eklen = 0;
+ char *plaintext;
+ long plaintextlen;
+ int ret = 0, run, staged = idx % 2;
+
+ if (idx / 2 != 0) {
+ if (idx / 2 == 3) {
+#ifndef OPENSSL_NO_RC4
+ cipher = EVP_rc4_40();
+#else
+ return TEST_skip("RC4 is disabled");
+#endif
+ } else {
+#ifndef OPENSSL_NO_RC2
+ cipher = idx / 2 == 1 ? EVP_rc2_40_cbc() : EVP_rc2_64_cbc();
+#else
+ return TEST_skip("RC2 is disabled");
+#endif
+ }
+ legacy = OSSL_PROVIDER_try_load(NULL, "legacy", 1);
+ if (legacy == NULL) {
+ ERR_clear_error();
+ return TEST_skip("The legacy provider is unavailable");
+ }
+ }
+
+ if (!TEST_ptr(recips = sk_X509_new_null())
+ || !TEST_int_gt(sk_X509_push(recips, cert), 0)
+ || !TEST_ptr(in = BIO_new_mem_buf(msg, sizeof(msg) - 1))
+ || !TEST_ptr(cms = CMS_encrypt(recips, in, cipher, CMS_BINARY)))
+ goto end;
+
+ if (idx / 2 == 0) {
+ /* Keep the valid AES key when a later recipient has the wrong length. */
+ if (!TEST_ptr(second = X509_dup(cert))
+ || !TEST_true(ASN1_INTEGER_set(X509_get_serialNumber(second),
+ ASN1_INTEGER_get(X509_get0_serialNumber(cert)) + 1))
+ || !TEST_ptr(ri = CMS_add1_recipient_cert(cms, second, 0))
+ || !TEST_ptr(pctx = EVP_PKEY_CTX_new_from_pkey(NULL,
+ X509_get0_pubkey(cert), NULL))
+ || !TEST_int_gt(EVP_PKEY_encrypt_init(pctx), 0)
+ || !TEST_int_gt(EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PADDING), 0)
+ || !TEST_int_gt(EVP_PKEY_encrypt(pctx, NULL, &eklen,
+ wrongkey, sizeof(wrongkey)),
+ 0)
+ || !TEST_ptr(ek = OPENSSL_malloc(eklen))
+ || !TEST_int_gt(EVP_PKEY_encrypt(pctx, ek, &eklen,
+ wrongkey, sizeof(wrongkey)),
+ 0)
+ || !TEST_true(ASN1_OCTET_STRING_set(ri->d.ktri->encryptedKey,
+ ek, (int)eklen)))
+ goto end;
+ }
+
+ /* Start from a decoded object, with the genuine recipient ordered first. */
+ if (!TEST_ptr(decoded = ASN1_item_dup(ASN1_ITEM_rptr(CMS_ContentInfo), cms)))
+ goto end;
+ ri = sk_CMS_RecipientInfo_value(CMS_get0_RecipientInfos(decoded), 0);
+ if (!TEST_int_eq(CMS_RecipientInfo_ktri_cert_cmp(ri, cert), 0))
+ goto end;
+
+ for (run = 0; run < 2; run++) {
+ if (!TEST_ptr(out = BIO_new(BIO_s_mem())))
+ goto end;
+ if (staged) {
+ if (!TEST_true(CMS_decrypt_set1_pkey(decoded, privkey, NULL))
+ || !TEST_true(CMS_decrypt(decoded, NULL, NULL, NULL,
+ out, CMS_BINARY)))
+ goto end;
+ } else if (!TEST_true(CMS_decrypt(decoded, privkey, NULL, NULL,
+ out, CMS_BINARY))) {
+ goto end;
+ }
+ plaintextlen = BIO_get_mem_data(out, &plaintext);
+ if (!TEST_mem_eq(plaintext, plaintextlen, msg, sizeof(msg) - 1))
+ goto end;
+ BIO_free(out);
+ out = NULL;
+ }
+ ret = 1;
+end:
+ BIO_free(in);
+ BIO_free(out);
+ CMS_ContentInfo_free(cms);
+ CMS_ContentInfo_free(decoded);
+ sk_X509_free(recips);
+ X509_free(second);
+ EVP_PKEY_CTX_free(pctx);
+ OPENSSL_free(ek);
+ OSSL_PROVIDER_unload(legacy);
+ return ret;
+}
+
static int test_CMS_add1_cert(void)
{
CMS_ContentInfo *cms = NULL;
@@ -1556,6 +1665,7 @@ int setup_tests(void)
ADD_TEST(test_short_mac_on_auth_envelope_data);
ADD_TEST(test_CMS_add_standard_smimecap_ex);
ADD_TEST(test_decrypt_with_wrong_key);
+ ADD_ALL_TESTS(test_decrypt_key_length, 8);
ADD_TEST(test_CMS_add1_cert);
ADD_TEST(test_CMS_SignerInfo_verify_sigalg_oid);
ADD_TEST(test_CMS_verify_result_no_signer_cert);