Commit 9e55749c83 for openssl.org

commit 9e55749c83f6b801ef17b069ac6ee74e6487005a
Author: Daniel Kubec <kubec@openssl.foundation>
Date:   Thu Jul 23 11:09:55 2026 +0200

    Add test for CVE-2026-63072

    Assisted-by: Claude:claude-fable-5
    Reviewed-by: Milan Broz <mbroz@openssl.org>
    Reviewed-by: Norbert Pocs <norbertp@openssl.org>
    Merge-date: Mon Aug 24 14:37:11 2026

diff --git a/test/cmsapitest.c b/test/cmsapitest.c
index e791e70098..5131b61b70 100644
--- a/test/cmsapitest.c
+++ b/test/cmsapitest.c
@@ -25,6 +25,7 @@ static char *derin = NULL;
 static char *too_long_iv_cms_in = NULL;
 static char *pwri_kek_oob_der_in = NULL;
 static char *pwri_kek_no_iv_in = NULL;
+static char *ec_recip_in = NULL;

 /*
  * This is our bad cms data, it contains an AuthEnvelopedData field
@@ -854,7 +855,99 @@ end:
     return ret;
 }

-OPT_TEST_DECLARE_USAGE("certfile privkeyfile derfile tooLongIVpem pwriKekOobDer pwriKekNoIv [ed448certfile ed448privkeyfile]\n")
+#ifndef OPENSSL_NO_EC
+/*
+ * Regression test for CVE-2026-63072: an 8-byte out-of-bounds heap write
+ * reachable through CMS_decrypt() when a KeyAgreeRecipientInfo names an
+ * id-aesNNN-wrap-pad key-wrap OID. CMS sizes the unwrap output buffer from
+ * the cipher's length query (inlen - 8), but AES-WRAP-PAD unwrap cleanses
+ * inlen bytes of it on every RFC 5649 integrity-failure path.
+ *
+ * We build a valid ECDH KARI message (which uses non-padded id-aes256-wrap),
+ * flip the single OID byte an attacker would flip on the wire to turn it into
+ * id-aes256-wrap-pad (key length unchanged), and decrypt with the matching
+ * private key. The unwrap must fail its integrity check without writing past
+ * the CMS-allocated buffer; CMS_decrypt() must fail cleanly.  Under a
+ * memory-checking build (e.g. valgrind) the overflow is flagged directly.
+ */
+static int test_kari_wrap_pad_unwrap_overflow(void)
+{
+    /* DER encoding of the id-aes256-wrap OID (2.16.840.1.101.3.4.1.45). */
+    static const unsigned char aes256_wrap_oid[] = {
+        0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2d
+    };
+    int ret = 0;
+    X509 *eccert = NULL;
+    EVP_PKEY *eckey = NULL;
+    BIO *certbio = NULL, *keybio = NULL, *msgbio = NULL, *outbio = NULL;
+    STACK_OF(X509) *recips = NULL;
+    CMS_ContentInfo *cms = NULL, *cms2 = NULL;
+    unsigned char *der = NULL;
+    const unsigned char *p;
+    int derlen, i, patched = 0;
+    const char *msg = "secret content for kari";
+
+    if ((certbio = BIO_new_file(ec_recip_in, "r")) == NULL
+        || PEM_read_bio_X509(certbio, &eccert, NULL, NULL) == NULL
+        || (keybio = BIO_new_file(ec_recip_in, "r")) == NULL
+        || PEM_read_bio_PrivateKey(keybio, &eckey, NULL, NULL) == NULL) {
+        goto end;
+    }
+
+    if (!TEST_ptr(recips = sk_X509_new_null())
+        || !TEST_int_gt(sk_X509_push(recips, eccert), 0))
+        goto end;
+
+    /* Build a normal ECDH KARI message; it uses non-padded id-aes256-wrap. */
+    if (!TEST_ptr(msgbio = BIO_new_mem_buf(msg, (int)strlen(msg)))
+        || !TEST_ptr(cms = CMS_encrypt(recips, msgbio, EVP_aes_256_cbc(),
+                         CMS_BINARY)))
+        goto end;
+
+    if (!TEST_int_gt(derlen = i2d_CMS_ContentInfo(cms, &der), 0))
+        goto end;
+
+    /* Swap id-aes256-wrap -> id-aes256-wrap-pad (0x2d -> 0x30). */
+    for (i = 0; i + (int)sizeof(aes256_wrap_oid) <= derlen; i++) {
+        if (memcmp(der + i, aes256_wrap_oid, sizeof(aes256_wrap_oid)) == 0) {
+            der[i + sizeof(aes256_wrap_oid) - 1] = 0x30;
+            patched = 1;
+            break;
+        }
+    }
+    if (!TEST_true(patched))
+        goto end;
+
+    p = der;
+    if (!TEST_ptr(cms2 = d2i_CMS_ContentInfo(NULL, &p, derlen)))
+        goto end;
+
+    /*
+     * The wrap-pad unwrap fails the AIV check; with the fix it does so without
+     * writing past the CMS-allocated buffer.  CMS_decrypt() must fail cleanly.
+     */
+    if (!TEST_ptr(outbio = BIO_new(BIO_s_mem()))
+        || !TEST_false(CMS_decrypt(cms2, eckey, eccert, NULL, outbio, 0)))
+        goto end;
+
+    ret = 1;
+end:
+    ERR_clear_error();
+    OPENSSL_free(der);
+    sk_X509_free(recips);
+    CMS_ContentInfo_free(cms);
+    CMS_ContentInfo_free(cms2);
+    BIO_free(certbio);
+    BIO_free(keybio);
+    BIO_free(msgbio);
+    BIO_free(outbio);
+    X509_free(eccert);
+    EVP_PKEY_free(eckey);
+    return ret;
+}
+#endif
+
+OPT_TEST_DECLARE_USAGE("certfile privkeyfile derfile tooLongIVpem pwriKekOobDer pwriKekNoIv ecrecip [ed448certfile ed448privkeyfile]\n")

 int setup_tests(void)
 {
@@ -871,7 +964,8 @@ int setup_tests(void)
         || !TEST_ptr(derin = test_get_argument(2))
         || !TEST_ptr(too_long_iv_cms_in = test_get_argument(3))
         || !TEST_ptr(pwri_kek_oob_der_in = test_get_argument(4))
-        || !TEST_ptr(pwri_kek_no_iv_in = test_get_argument(5)))
+        || !TEST_ptr(pwri_kek_no_iv_in = test_get_argument(5))
+        || !TEST_ptr(ec_recip_in = test_get_argument(6)))
         return 0;

     if (!TEST_ptr(cert = load_cert_pem(certin, NULL))
@@ -883,9 +977,9 @@ int setup_tests(void)
         return 0;
     }

-    if (test_get_argument_count() >= 8) {
-        ed448_certin = test_get_argument(6);
-        ed448_privkeyin = test_get_argument(7);
+    if (test_get_argument_count() >= 9) {
+        ed448_certin = test_get_argument(7);
+        ed448_privkeyin = test_get_argument(8);

         if (!TEST_ptr(ed448_cert = load_cert_pem(ed448_certin, NULL))
             || !TEST_ptr(ed448_privkey = load_pkey_pem(ed448_privkeyin, NULL))) {
@@ -919,6 +1013,9 @@ int setup_tests(void)
         ADD_TEST(test_CMS_add1_signer_ed448_signed_attrs_md);
         ADD_TEST(test_CMS_add1_signer_ed448_noattr);
     }
+#ifndef OPENSSL_NO_EC
+    ADD_TEST(test_kari_wrap_pad_unwrap_overflow);
+#endif
     return 1;
 }

diff --git a/test/recipes/80-test_cmsapi.t b/test/recipes/80-test_cmsapi.t
index 03bed6efa8..7edaca9423 100644
--- a/test/recipes/80-test_cmsapi.t
+++ b/test/recipes/80-test_cmsapi.t
@@ -26,5 +26,6 @@ ok(run(test(["cmsapitest", srctop_file("test", "certs", "servercert.pem"),
              srctop_file("test", "recipes", "80-test_cmsapi_data", "encDataWithTooLongIV.pem"),
              srctop_file("test", "recipes", "80-test_cmsapi_data", "cms_pwri_kek_oob.der"),
              srctop_file("test", "recipes", "80-test_cmsapi_data", "cms_pwri_kek_NoIV.der"),
+             srctop_file("test", "smime-certs", "smec1.pem"),
              @ed448_args])),
              "running cmsapitest");