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);