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