From d307f61f1b341b6eb7d5d07bd6902a939c709bc6 Mon Sep 17 00:00:00 2001 From: Daniel Kubec Date: Thu, 23 Jul 2026 11:09:55 +0200 Subject: Add test for CVE-2026-63072 Assisted-by: Claude:claude-fable-5 Reviewed-by: Milan Broz Reviewed-by: Andrew Dinh Merge-date: Mon Aug 24 14:44:54 2026 --- test/cmsapitest.c | 103 +++++++++++++++++++++++++++++++++++++++++- test/recipes/80-test_cmsapi.t | 3 +- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/test/cmsapitest.c b/test/cmsapitest.c index ba804ee1a60..df5f8d59dcd 100644 --- a/test/cmsapitest.c +++ b/test/cmsapitest.c @@ -21,6 +21,7 @@ static EVP_PKEY *privkey = NULL; static char *derin = NULL; static char *too_long_iv_cms_in = NULL; static char *pwri_kek_oob_der_in = NULL; +static char *ec_recip_in = NULL; /* * This is our bad cms data, it contains an AuthEnvelopedData field @@ -711,7 +712,100 @@ end: return ret; } -OPT_TEST_DECLARE_USAGE("certfile privkeyfile derfile tooLongIVpem pwriKekOobDer\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 ecrecip\n") int setup_tests(void) { @@ -727,7 +821,8 @@ int setup_tests(void) || !TEST_ptr(privkeyin = test_get_argument(1)) || !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_oob_der_in = test_get_argument(4)) + || !TEST_ptr(ec_recip_in = test_get_argument(5))) return 0; certbio = BIO_new_file(certin, "r"); @@ -767,6 +862,10 @@ int setup_tests(void) ADD_ALL_TESTS(test_d2i_CMS_decode, 2); ADD_TEST(test_cms_aesgcm_iv_too_long); ADD_TEST(test_pwri_kek_unwrap_short_encrypted_key); + +#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 3d1dae84646..1f5f7ae233e 100644 --- a/test/recipes/80-test_cmsapi.t +++ b/test/recipes/80-test_cmsapi.t @@ -20,5 +20,6 @@ ok(run(test(["cmsapitest", srctop_file("test", "certs", "servercert.pem"), srctop_file("test", "certs", "serverkey.pem"), srctop_file("test", "recipes", "80-test_cmsapi_data", "encryptedData.der"), 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_oob.der"), + srctop_file("test", "smime-certs", "smec1.pem")])), "running cmsapitest"); -- cgit