diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 3943d47739..cd06d68a1f 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -829,6 +829,7 @@ WOLFSSL_NO_KCAPI_HMAC_SHA256 WOLFSSL_NO_KCAPI_HMAC_SHA384 WOLFSSL_NO_KCAPI_HMAC_SHA512 WOLFSSL_NO_KCAPI_SHA224 +WOLFSSL_NO_KTRI_ORACLE_WARNING WOLFSSL_NO_OCSP_DATE_CHECK WOLFSSL_NO_OCSP_ISSUER_CHAIN_CHECK WOLFSSL_NO_OCSP_OPTIONAL_CERTS diff --git a/tests/api/test_pkcs7.c b/tests/api/test_pkcs7.c index 646db1bdb8..a22fd9e10e 100644 --- a/tests/api/test_pkcs7.c +++ b/tests/api/test_pkcs7.c @@ -1118,6 +1118,229 @@ int test_wc_PKCS7_EnvelopedData_KTRI_RSA_PSS(void) #endif +/* + * Bleichenbacher padding-oracle regression: wc_PKCS7_DecryptKtri must not + * return a distinguishable error when RSA PKCS#1 v1.5 unwrap of the + * encrypted CEK fails vs. when it succeeds with a wrong key. The + * mitigation substitutes a deterministic pseudo-random CEK on RSA failure + * so content decryption fails indistinguishably. This test corrupts the + * encryptedKey in a valid EnvelopedData and asserts the error matches + * content corruption rather than surfacing an RSA/recipient-level code. + * Runs for AES-128 and AES-256 because the fake CEK is a fixed 32 bytes: + * AES-128 (key size 16) exercises the path where the fake size differs + * from the real CEK size. + */ +#if defined(HAVE_PKCS7) && !defined(NO_RSA) && !defined(NO_SHA256) && \ + !defined(NO_AES) && defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) && \ + defined(WOLFSSL_AES_256) && !defined(NO_HMAC) && \ + !defined(WOLFSSL_NO_MALLOC) && \ + (defined(USE_CERT_BUFFERS_2048) || defined(USE_CERT_BUFFERS_1024) || \ + !defined(NO_FILESYSTEM)) +static int pkcs7_ktri_bad_pad_case(int encryptOID, byte* rsaCert, + word32 rsaCertSz, byte* rsaPrivKey, + word32 rsaPrivKeySz, byte* encrypted, + word32 encryptedCap, byte* decoded, + word32 decodedCap) +{ + EXPECT_DECLS; + PKCS7* pkcs7 = NULL; + byte data[] = "PKCS7 KTRI bad-RSA-padding regression payload."; + int encryptedSz = 0; + int badKeyRet = 0; + int badContentRet = 0; + byte savedKeyByte = 0; + byte savedContentByte = 0; + word32 i; + word32 encryptedKeyOff = 0; + static const byte rsaEncOid[] = { + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, + 0x01, 0x01, 0x01 + }; + + ExpectNotNull(pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId)); + ExpectIntEQ(wc_PKCS7_InitWithCert(pkcs7, rsaCert, rsaCertSz), 0); + if (pkcs7 != NULL) { + pkcs7->content = data; + pkcs7->contentSz = (word32)sizeof(data); + pkcs7->contentOID = DATA; + pkcs7->encryptOID = encryptOID; + } + ExpectIntGT(encryptedSz = wc_PKCS7_EncodeEnvelopedData(pkcs7, + encrypted, encryptedCap), 0); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + + /* Locate the KTRI encryptedKey OCTET STRING. After the rsaEncryption + * OID there are NULL algorithm parameters (05 00), then a 256-byte + * OCTET STRING (tag 04, long-form length 82 01 00 for RSA-2048). */ + for (i = 0; (int)(i + sizeof(rsaEncOid)) < encryptedSz; i++) { + if (XMEMCMP(&encrypted[i], rsaEncOid, sizeof(rsaEncOid)) == 0) { + word32 p = i + (word32)sizeof(rsaEncOid); + if (p + 2 < (word32)encryptedSz && + encrypted[p] == 0x05 && encrypted[p + 1] == 0x00) { + p += 2; + } + if (p + 4 < (word32)encryptedSz && encrypted[p] == 0x04) { + if (encrypted[p + 1] == 0x82) { + encryptedKeyOff = p + 4; + } + else if (encrypted[p + 1] == 0x81) { + encryptedKeyOff = p + 3; + } + else { + encryptedKeyOff = p + 2; + } + } + break; + } + } + ExpectIntGT(encryptedKeyOff, 0); + ExpectIntLT(encryptedKeyOff + 32, (word32)encryptedSz); + + /* Case 1: corrupt a byte inside the RSA ciphertext, decode, restore. */ + savedKeyByte = encrypted[encryptedKeyOff + 16]; + encrypted[encryptedKeyOff + 16] ^= 0xA5; + + ExpectNotNull(pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId)); + ExpectIntEQ(wc_PKCS7_InitWithCert(pkcs7, rsaCert, rsaCertSz), 0); + if (pkcs7 != NULL) { + pkcs7->privateKey = rsaPrivKey; + pkcs7->privateKeySz = rsaPrivKeySz; + } + badKeyRet = wc_PKCS7_DecodeEnvelopedData(pkcs7, encrypted, + (word32)encryptedSz, decoded, decodedCap); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + encrypted[encryptedKeyOff + 16] = savedKeyByte; + + /* Case 2: corrupt a byte in the second-to-last AES ciphertext block. + * In CBC mode this deterministically XOR-flips the corresponding byte + * in the last plaintext block, invalidating the PKCS#7 padding + * (original pad byte 0x01 becomes 0x01^0xA5 = 0xA4 > blockSz). + * Corrupting the last ciphertext block directly would randomize the + * entire last plaintext block, giving ~1/256 chance of accidentally + * valid padding and intermittent test failures. */ + savedContentByte = encrypted[encryptedSz - 17]; + encrypted[encryptedSz - 17] ^= 0xA5; + + ExpectNotNull(pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId)); + ExpectIntEQ(wc_PKCS7_InitWithCert(pkcs7, rsaCert, rsaCertSz), 0); + if (pkcs7 != NULL) { + pkcs7->privateKey = rsaPrivKey; + pkcs7->privateKeySz = rsaPrivKeySz; + } + badContentRet = wc_PKCS7_DecodeEnvelopedData(pkcs7, encrypted, + (word32)encryptedSz, decoded, decodedCap); + wc_PKCS7_Free(pkcs7); + pkcs7 = NULL; + encrypted[encryptedSz - 17] = savedContentByte; + + /* Case 2 must always fail: the CBC-chain corruption deterministically + * invalidates the PKCS#7 padding. */ + ExpectIntLT(badContentRet, 0); + /* Bad-key must NOT leak as an RSA- or recipient-level error. */ + ExpectIntNE(badKeyRet, WC_NO_ERR_TRACE(PKCS7_RECIP_E)); + ExpectIntNE(badKeyRet, WC_NO_ERR_TRACE(RSA_PAD_E)); + ExpectIntNE(badKeyRet, WC_NO_ERR_TRACE(RSA_BUFFER_E)); + ExpectIntNE(badKeyRet, WC_NO_ERR_TRACE(BAD_PADDING_E)); + /* Case 1 (bad RSA key) decrypts content with a random fake CEK, + * producing fully random plaintext. With ~1/256 probability the + * PKCS#7 padding accidentally looks valid, causing a positive + * garbage-length return instead of an error. This does not leak + * RSA key information, so it is acceptable. When both cases do + * fail, verify they fail at the same content-decryption layer. */ + if (badKeyRet < 0) { + ExpectIntEQ(badKeyRet, badContentRet); + } + + return EXPECT_RESULT(); +} + +int test_wc_PKCS7_EnvelopedData_KTRI_BadRsaPad(void) +{ + EXPECT_DECLS; + byte encrypted[FOURK_BUF]; + byte decoded[FOURK_BUF]; + byte* rsaCert = NULL; + byte* rsaPrivKey = NULL; + word32 rsaCertSz = 0; + word32 rsaPrivKeySz = 0; +#if !defined(USE_CERT_BUFFERS_1024) && !defined(USE_CERT_BUFFERS_2048) && \ + !defined(NO_FILESYSTEM) + XFILE f = XBADFILE; +#endif + + /* Load RSA cert and key */ +#if defined(USE_CERT_BUFFERS_1024) + rsaCertSz = (word32)sizeof_client_cert_der_1024; + ExpectNotNull(rsaCert = (byte*)XMALLOC(rsaCertSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + if (rsaCert != NULL) + XMEMCPY(rsaCert, client_cert_der_1024, rsaCertSz); + rsaPrivKeySz = (word32)sizeof_client_key_der_1024; + ExpectNotNull(rsaPrivKey = (byte*)XMALLOC(rsaPrivKeySz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + if (rsaPrivKey != NULL) + XMEMCPY(rsaPrivKey, client_key_der_1024, rsaPrivKeySz); +#elif defined(USE_CERT_BUFFERS_2048) + rsaCertSz = (word32)sizeof_client_cert_der_2048; + ExpectNotNull(rsaCert = (byte*)XMALLOC(rsaCertSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + if (rsaCert != NULL) + XMEMCPY(rsaCert, client_cert_der_2048, rsaCertSz); + rsaPrivKeySz = (word32)sizeof_client_key_der_2048; + ExpectNotNull(rsaPrivKey = (byte*)XMALLOC(rsaPrivKeySz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + if (rsaPrivKey != NULL) + XMEMCPY(rsaPrivKey, client_key_der_2048, rsaPrivKeySz); +#elif !defined(NO_FILESYSTEM) + rsaCertSz = FOURK_BUF; + ExpectNotNull(rsaCert = (byte*)XMALLOC(rsaCertSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectTrue((f = XFOPEN("./certs/client-cert.der", "rb")) != XBADFILE); + ExpectTrue((rsaCertSz = (word32)XFREAD(rsaCert, 1, rsaCertSz, f)) > 0); + if (f != XBADFILE) { + XFCLOSE(f); + f = XBADFILE; + } + rsaPrivKeySz = FOURK_BUF; + ExpectNotNull(rsaPrivKey = (byte*)XMALLOC(rsaPrivKeySz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectTrue((f = XFOPEN("./certs/client-key.der", "rb")) != XBADFILE); + ExpectTrue((rsaPrivKeySz = (word32)XFREAD(rsaPrivKey, 1, + rsaPrivKeySz, f)) > 0); + if (f != XBADFILE) + XFCLOSE(f); +#endif + + if (rsaCert == NULL || rsaPrivKey == NULL) { + XFREE(rsaCert, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(rsaPrivKey, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return TEST_SKIPPED; + } + + /* AES-128: 32-byte fake CEK larger than real CEK size (16 bytes). */ + ExpectIntEQ(pkcs7_ktri_bad_pad_case(AES128CBCb, rsaCert, rsaCertSz, + rsaPrivKey, rsaPrivKeySz, encrypted, sizeof(encrypted), + decoded, sizeof(decoded)), TEST_SUCCESS); +#ifdef WOLFSSL_AES_192 + /* AES-192: fake CEK (32) vs real CEK (24) - another size mismatch. */ + ExpectIntEQ(pkcs7_ktri_bad_pad_case(AES192CBCb, rsaCert, rsaCertSz, + rsaPrivKey, rsaPrivKeySz, encrypted, sizeof(encrypted), + decoded, sizeof(decoded)), TEST_SUCCESS); +#endif + /* AES-256: fake CEK size matches real CEK size (32 bytes). */ + ExpectIntEQ(pkcs7_ktri_bad_pad_case(AES256CBCb, rsaCert, rsaCertSz, + rsaPrivKey, rsaPrivKeySz, encrypted, sizeof(encrypted), + decoded, sizeof(decoded)), TEST_SUCCESS); + + XFREE(rsaCert, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(rsaPrivKey, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + return EXPECT_RESULT(); +} /* END test_wc_PKCS7_EnvelopedData_KTRI_BadRsaPad */ +#endif + + /* * Testing wc_PKCS7_EncodeSignedData_ex() and wc_PKCS7_VerifySignedData_ex() */ @@ -2503,6 +2726,8 @@ int test_wc_PKCS7_DecodeEnvelopedData_multiple_recipients(void) bytes */ size_t testDerBufferSz = 0; byte decodedData[8192]; + byte serverDecodedData[8192]; + int serverRet = 0; ExpectTrue((f = XFOPEN(testFile, "rb")) != XBADFILE); if (f != XBADFILE) { @@ -2522,12 +2747,13 @@ int test_wc_PKCS7_DecodeEnvelopedData_multiple_recipients(void) ExpectIntEQ(wc_PKCS7_SetKey(pkcs7, (byte*)server_key_der_2048, sizeof_server_key_der_2048), 0); - ret = wc_PKCS7_DecodeEnvelopedData(pkcs7, testDerBuffer, - (word32)testDerBufferSz, decodedData, sizeof(decodedData)); + serverRet = wc_PKCS7_DecodeEnvelopedData(pkcs7, testDerBuffer, + (word32)testDerBufferSz, serverDecodedData, + sizeof(serverDecodedData)); #if defined(NO_AES) || defined(NO_AES_256) - ExpectIntEQ(ret, ALGO_ID_E); + ExpectIntEQ(serverRet, ALGO_ID_E); #else - ExpectIntGT(ret, 0); + ExpectIntGT(serverRet, 0); #endif wc_PKCS7_Free(pkcs7); pkcs7 = NULL; @@ -2553,7 +2779,14 @@ int test_wc_PKCS7_DecodeEnvelopedData_multiple_recipients(void) pkcs7 = NULL; } - /* test with ca cert recipient (which should fail) */ + /* Test with ca cert recipient. The ca cert is not a listed recipient, + * so RSA unwrap fails. The Bleichenbacher mitigation substitutes a + * pseudo-random fake CEK on unwrap failure, so the call normally + * returns a negative error when content decryption rejects the + * resulting garbage padding - but around 1/256 of the time the random + * CEK yields plaintext with accidentally-valid PKCS#7 padding and the + * call returns a non-negative "decrypted" size. That case must not + * produce the real plaintext. */ ExpectNotNull(pkcs7 = wc_PKCS7_New(HEAP_HINT, testDevId)); if (pkcs7) { ExpectIntEQ(wc_PKCS7_InitWithCert(pkcs7, (byte*)ca_cert_der_2048, @@ -2562,9 +2795,15 @@ int test_wc_PKCS7_DecodeEnvelopedData_multiple_recipients(void) ExpectIntEQ(wc_PKCS7_SetKey(pkcs7, (byte*)ca_key_der_2048, sizeof_ca_key_der_2048), 0); + XMEMSET(decodedData, 0, sizeof(decodedData)); ret = wc_PKCS7_DecodeEnvelopedData(pkcs7, testDerBuffer, (word32)testDerBufferSz, decodedData, sizeof(decodedData)); - ExpectIntLT(ret, 0); + #if defined(NO_AES) || defined(NO_AES_256) + ExpectIntEQ(ret, ALGO_ID_E); + #else + ExpectTrue(ret < 0 || ret != serverRet || + XMEMCMP(decodedData, serverDecodedData, (size_t)ret) != 0); + #endif wc_PKCS7_Free(pkcs7); pkcs7 = NULL; } diff --git a/tests/api/test_pkcs7.h b/tests/api/test_pkcs7.h index 084744d43c..e1c71c911d 100644 --- a/tests/api/test_pkcs7.h +++ b/tests/api/test_pkcs7.h @@ -38,6 +38,14 @@ int test_wc_PKCS7_EncodeSignedData_RSA_PSS(void); !defined(NO_AES) && defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_256) int test_wc_PKCS7_EnvelopedData_KTRI_RSA_PSS(void); #endif +#if defined(HAVE_PKCS7) && !defined(NO_RSA) && !defined(NO_SHA256) && \ + !defined(NO_AES) && defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) && \ + defined(WOLFSSL_AES_256) && !defined(NO_HMAC) && \ + !defined(WOLFSSL_NO_MALLOC) && \ + (defined(USE_CERT_BUFFERS_2048) || defined(USE_CERT_BUFFERS_1024) || \ + !defined(NO_FILESYSTEM)) +int test_wc_PKCS7_EnvelopedData_KTRI_BadRsaPad(void); +#endif int test_wc_PKCS7_EncodeSignedData_ex(void); int test_wc_PKCS7_VerifySignedData_RSA(void); int test_wc_PKCS7_VerifySignedData_ECC(void); @@ -82,6 +90,18 @@ int test_wc_PKCS7_VerifySignedData_IndefLenOOB(void); #define TEST_PKCS7_RSA_PSS_ED_DECL #endif +#if defined(HAVE_PKCS7) && !defined(NO_RSA) && !defined(NO_SHA256) && \ + !defined(NO_AES) && defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) && \ + defined(WOLFSSL_AES_256) && !defined(NO_HMAC) && \ + !defined(WOLFSSL_NO_MALLOC) && \ + (defined(USE_CERT_BUFFERS_2048) || defined(USE_CERT_BUFFERS_1024) || \ + !defined(NO_FILESYSTEM)) +#define TEST_PKCS7_KTRI_BADRSAPAD_DECL \ + TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_EnvelopedData_KTRI_BadRsaPad), +#else +#define TEST_PKCS7_KTRI_BADRSAPAD_DECL +#endif + #define TEST_PKCS7_SIGNED_DATA_DECLS \ TEST_DECL_GROUP("pkcs7_sd", test_wc_PKCS7_InitWithCert), \ TEST_DECL_GROUP("pkcs7_sd", test_wc_PKCS7_EncodeData), \ @@ -100,6 +120,7 @@ int test_wc_PKCS7_VerifySignedData_IndefLenOOB(void); TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_DecodeEnvelopedData_stream), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_EncodeDecodeEnvelopedData), \ TEST_PKCS7_RSA_PSS_ED_DECL \ + TEST_PKCS7_KTRI_BADRSAPAD_DECL \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_SetAESKeyWrapUnwrapCb), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_GetEnvelopedDataKariRid), \ TEST_DECL_GROUP("pkcs7_ed", test_wc_PKCS7_EncodeEncryptedData), \ diff --git a/wolfcrypt/src/pkcs7.c b/wolfcrypt/src/pkcs7.c index ec7fd317d2..5d1a25bb31 100644 --- a/wolfcrypt/src/pkcs7.c +++ b/wolfcrypt/src/pkcs7.c @@ -44,6 +44,9 @@ #include #include +#ifndef NO_HMAC + #include +#endif #ifndef NO_RSA #include #endif @@ -207,6 +210,8 @@ static void wc_PKCS7_ResetStream(wc_PKCS7* pkcs7) #endif /* free any buffers that may be allocated */ + if (pkcs7->stream->aad != NULL && pkcs7->stream->aadSz > 0) + ForceZero(pkcs7->stream->aad, pkcs7->stream->aadSz); XFREE(pkcs7->stream->aad, pkcs7->heap, DYNAMIC_TYPE_PKCS7); XFREE(pkcs7->stream->tag, pkcs7->heap, DYNAMIC_TYPE_PKCS7); XFREE(pkcs7->stream->nonce, pkcs7->heap, DYNAMIC_TYPE_PKCS7); @@ -7741,6 +7746,9 @@ static int wc_PKCS7_KariGenerateKEK(WC_PKCS7_KARI* kari, WC_RNG* rng, secret = (byte*)XMALLOC(secretSz, kari->heap, DYNAMIC_TYPE_PKCS7); if (secret == NULL) return MEMORY_E; +#ifdef WOLFSSL_CHECK_MEM_ZERO + wc_MemZero_Add("wc_PKCS7_KariGenerateKEK secret", secret, secretSz); +#endif #if defined(ECC_TIMING_RESISTANT) && (!defined(HAVE_FIPS) || \ (!defined(HAVE_FIPS_VERSION) || (HAVE_FIPS_VERSION != 2))) && \ @@ -10608,6 +10616,81 @@ int wc_PKCS7_EncodeEnvelopedData(wc_PKCS7* pkcs7, byte* output, word32 outputSz) } #ifndef NO_RSA +#if !defined(NO_HMAC) && !defined(NO_SHA256) +/* Bleichenbacher padding-oracle mitigation for PKCS#7/CMS KTRI: produce a + * WC_SHA256_DIGEST_SIZE-byte pseudo-random CEK derived from a fresh + * random seed and the encrypted-key ciphertext. The output is random per + * call (driven by the RNG seed); deriving via HMAC of the ciphertext + * simply gives the same value within one call regardless of where it is + * referenced. Called unconditionally so the work is in the timing path + * regardless of RSA padding validity. */ +static int wc_PKCS7_KtriFakeCEK(wc_PKCS7* pkcs7, const byte* encryptedKey, + word32 encryptedKeySz, byte* out) +{ + int ret; + byte seed[WC_SHA256_DIGEST_SIZE]; + WC_RNG* rng = NULL; + int ownRng = 0; + WC_DECLARE_VAR(localRng, WC_RNG, 1, pkcs7->heap); + WC_DECLARE_VAR(hmac, Hmac, 1, pkcs7->heap); + + if (pkcs7 == NULL || encryptedKey == NULL || out == NULL) { + return BAD_FUNC_ARG; + } + + WC_ALLOC_VAR_EX(hmac, Hmac, 1, pkcs7->heap, DYNAMIC_TYPE_HMAC, + return MEMORY_E); + + /* Prefer a caller-provided RNG to avoid paying a DRBG init/reseed cost + * on every decrypt (and to keep the timing envelope flatter on FIPS / + * HW-RNG builds). Fall back to a one-shot RNG when pkcs7->rng is not + * set. */ + if (pkcs7->rng != NULL) { + rng = pkcs7->rng; + } + else { + WC_ALLOC_VAR_EX(localRng, WC_RNG, 1, pkcs7->heap, DYNAMIC_TYPE_RNG, + WC_FREE_VAR_EX(hmac, pkcs7->heap, DYNAMIC_TYPE_HMAC); + return MEMORY_E); + ret = wc_InitRng_ex(localRng, pkcs7->heap, pkcs7->devId); + if (ret != 0) { + WC_FREE_VAR_EX(localRng, pkcs7->heap, DYNAMIC_TYPE_RNG); + WC_FREE_VAR_EX(hmac, pkcs7->heap, DYNAMIC_TYPE_HMAC); + return ret; + } + rng = localRng; + ownRng = 1; + } + + ret = wc_RNG_GenerateBlock(rng, seed, (word32)sizeof(seed)); + + if (ownRng) { + wc_FreeRng(localRng); + WC_FREE_VAR_EX(localRng, pkcs7->heap, DYNAMIC_TYPE_RNG); + } + + if (ret != 0) { + WC_FREE_VAR_EX(hmac, pkcs7->heap, DYNAMIC_TYPE_HMAC); + return ret; + } + + ret = wc_HmacInit(hmac, pkcs7->heap, pkcs7->devId); + if (ret == 0) { + ret = wc_HmacSetKey(hmac, WC_SHA256, seed, (word32)sizeof(seed)); + if (ret == 0) { + ret = wc_HmacUpdate(hmac, encryptedKey, encryptedKeySz); + } + if (ret == 0) { + ret = wc_HmacFinal(hmac, out); + } + wc_HmacFree(hmac); + } + ForceZero(seed, sizeof(seed)); + WC_FREE_VAR_EX(hmac, pkcs7->heap, DYNAMIC_TYPE_HMAC); + return ret; +} +#endif /* !NO_HMAC && !NO_SHA256 */ + /* decode KeyTransRecipientInfo (ktri), return 0 on success, <0 on error */ static int wc_PKCS7_DecryptKtri(wc_PKCS7* pkcs7, byte* in, word32 inSz, word32* idx, byte* decryptedKey, @@ -10967,25 +11050,146 @@ static int wc_PKCS7_DecryptKtri(wc_PKCS7* pkcs7, byte* in, word32 inSz, } wc_FreeRsaKey(privKey); + #if !defined(NO_HMAC) && !defined(NO_SHA256) + { + /* Bleichenbacher padding-oracle mitigation: always compute + * a pseudo-random fallback CEK so timing and error + * behaviour do not depend on RSA padding validity. On + * unwrap failure we substitute the fallback and let + * content decryption fail indistinguishably from "unwrap + * succeeded but CEK is wrong". */ + byte fakeKey[WC_SHA256_DIGEST_SIZE]; + int fakeRet = wc_PKCS7_KtriFakeCEK(pkcs7, encryptedKey, + (word32)encryptedKeySz, + fakeKey); + + if (fakeRet != 0) { + /* Fallback generation failed (e.g. RNG/HMAC error). + * Return the fallback-generation status, which does + * not depend on RSA padding validity, rather than the + * RSA status which would re-open the oracle. */ + ForceZero(fakeKey, sizeof(fakeKey)); + /* In the non-OAEP path RSA is decrypted in-place via + * wc_RsaPrivateDecryptInline, so encryptedKey holds + * the (possibly valid) plaintext CEK. Zero it before + * free. */ + ForceZero(encryptedKey, (word32)encryptedKeySz); + XFREE(encryptedKey, pkcs7->heap, DYNAMIC_TYPE_WOLF_BIGINT); + WC_FREE_VAR_EX(privKey, pkcs7->heap, + DYNAMIC_TYPE_TMP_BUFFER); + #ifndef WC_NO_RSA_OAEP + if (encOID == RSAESOAEPk) { + if (outKey != NULL) { + ForceZero(outKey, outKeySz); + XFREE(outKey, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); + } + } + #endif + return fakeRet; + } + + /* Constant-time select between fake and real CEK. On RSA + * failure outKey may be NULL or keySz may be <= 0; in + * both cases the mask selects fakeKey for every byte. + * + * To avoid data-dependent branches that leak realLen, + * copy the real key into a fixed-size zero-padded buffer + * first, then select byte-by-byte in constant time. */ + { + word32 i; + byte useFake; + int realLen = keySz; + byte realPad[WC_SHA256_DIGEST_SIZE]; + + XMEMSET(realPad, 0, sizeof(realPad)); + /* Constant-time copy: avoid data-dependent branches + * that could leak whether RSA padding was valid. + * When outKey is NULL (inline RSA failure), use + * encryptedKey as a safe readable source; the mask + * will zero out all bytes anyway. Both encryptedKey + * and outKey (when non-NULL) are at least + * sizeof(realPad) bytes for any RSA key size. + * + * Use constant-time pointer selection to avoid + * branching on outKey nullity, which would leak + * whether RSA PKCS#1 v1.5 padding was valid. */ + { + byte haveSrc = ctMaskGTE(realLen, 1); + const byte* srcTbl[2]; + const byte* src; + word32 j = 0; + word32 safeJ = 0; + + /* Select source without integer pointer synthesis. + * Some safety-oriented compilers (e.g. Fil-C) treat + * int-to-pointer reconstruction as a null-object + * pointer on dereference. */ + srcTbl[0] = encryptedKey; + srcTbl[1] = outKey; + src = srcTbl[haveSrc & 1]; + + /* safeJ is clamped to max(0, realLen-1): it + * only advances while the next index would + * still be inside realLen, so src[safeJ] is + * always in bounds. Bytes at j >= realLen are + * masked to zero by inBounds anyway. */ + for (j = 0; j < (word32)sizeof(realPad); j++) { + byte inBounds = ctMaskLT((int)j, realLen); + byte advance = ctMaskLT((int)(safeJ + 1), + realLen); + realPad[j] = src[safeJ] & haveSrc & inBounds; + safeJ += (word32)(advance & 1); + } + } + useFake = ctMaskLT(realLen, 1); /* 0xFF if realLen<=0 */ + + for (i = 0; i < (word32)sizeof(fakeKey); i++) { + decryptedKey[i] = ctMaskSel(useFake, fakeKey[i], + realPad[i]); + } + /* Report the real key size on success; on RSA + * failure (realLen <= 0) report sizeof(fakeKey). + * Constant-time select avoids branching on RSA + * padding validity. */ + *decryptedKeySz = (word32)ctMaskSelInt(useFake, + (int)sizeof(fakeKey), realLen); + ForceZero(realPad, sizeof(realPad)); + } + ForceZero(fakeKey, sizeof(fakeKey)); + /* In the non-OAEP path RSA is decrypted in-place via + * wc_RsaPrivateDecryptInline, so encryptedKey holds the + * plaintext CEK after the unwrap. Zero it before free. */ + ForceZero(encryptedKey, (word32)encryptedKeySz); + } + #else /* NO_HMAC || NO_SHA256: mitigation unavailable */ + #if !defined(WOLFSSL_NO_KTRI_ORACLE_WARNING) + #warning "PKCS7 KTRI Bleichenbacher mitigation requires HMAC " \ + "and SHA256; build without them leaves the RSA unwrap " \ + "error path observable to callers. " \ + "Define WOLFSSL_NO_KTRI_ORACLE_WARNING to silence." + #endif + if (keySz <= 0 || outKey == NULL) { ForceZero(encryptedKey, (word32)encryptedKeySz); XFREE(encryptedKey, pkcs7->heap, DYNAMIC_TYPE_WOLF_BIGINT); WC_FREE_VAR_EX(privKey, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); - #ifndef WC_NO_RSA_OAEP + #ifndef WC_NO_RSA_OAEP if (encOID == RSAESOAEPk) { if (outKey) { ForceZero(outKey, outKeySz); XFREE(outKey, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); } } - #endif + #endif return keySz; - } else { + } + else { *decryptedKeySz = (word32)keySz; XMEMCPY(decryptedKey, outKey, (word32)keySz); ForceZero(encryptedKey, (word32)encryptedKeySz); } + #endif /* !NO_HMAC && !NO_SHA256 */ XFREE(encryptedKey, pkcs7->heap, DYNAMIC_TYPE_WOLF_BIGINT); WC_FREE_VAR_EX(privKey, pkcs7->heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -12888,6 +13092,11 @@ int wc_PKCS7_DecodeEnvelopedData(wc_PKCS7* pkcs7, byte* in, DYNAMIC_TYPE_PKCS7); if (decryptedKey == NULL) return MEMORY_E; + XMEMSET(decryptedKey, 0, MAX_ENCRYPTED_KEY_SZ); + #ifdef WOLFSSL_CHECK_MEM_ZERO + wc_MemZero_Add("wc_PKCS7 decryptedKey", decryptedKey, + MAX_ENCRYPTED_KEY_SZ); + #endif wc_PKCS7_ChangeState(pkcs7, WC_PKCS7_ENV_2); tmpIdx = idx; recipientSetSz = (word32)ret; @@ -13130,10 +13339,18 @@ int wc_PKCS7_DecodeEnvelopedData(wc_PKCS7* pkcs7, byte* in, wc_PKCS7_StreamStoreVar(pkcs7, encOID, expBlockSz, explicitOctet); if (explicitOctet) { - /* initialize decryption state in preparation */ + /* initialize decryption state in preparation. Use + * contentSz (blockKeySz from the content algorithm) as + * the AES key size rather than aadSz (the unwrapped CEK + * length): the two are equal for well-formed messages, + * but using blockKeySz avoids BAD_FUNC_ARG on crafted + * messages where the CEK length does not match the + * content cipher, which would otherwise be a + * distinguishable error. */ if (pkcs7->decryptionCb == NULL) { ret = wc_PKCS7_DecryptContentInit(pkcs7, encOID, - pkcs7->stream->aad, pkcs7->stream->aadSz, + pkcs7->stream->aad, + (word32)pkcs7->stream->contentSz, pkcs7->stream->tmpIv, expBlockSz, pkcs7->devId, pkcs7->heap); if (ret != 0) @@ -13409,8 +13626,13 @@ int wc_PKCS7_DecodeEnvelopedData(wc_PKCS7* pkcs7, byte* in, ret = (int)pkcs7->totalEncryptedContentSz - padLen; #ifndef NO_PKCS7_STREAM - pkcs7->stream->aad = NULL; - pkcs7->stream->aadSz = 0; + /* decryptedKey (just freed) is the same buffer stream->aad + * aliases. Null the stream handle so ResetStream doesn't + * double-free it. */ + if (pkcs7->stream != NULL) { + pkcs7->stream->aad = NULL; + pkcs7->stream->aadSz = 0; + } wc_PKCS7_ResetStream(pkcs7); #endif wc_PKCS7_ChangeState(pkcs7, WC_PKCS7_START); @@ -13423,6 +13645,16 @@ int wc_PKCS7_DecodeEnvelopedData(wc_PKCS7* pkcs7, byte* in, #ifndef NO_PKCS7_STREAM if (ret < 0 && ret != WC_NO_ERR_TRACE(WC_PKCS7_WANT_READ_E)) { + /* stream->aad aliases the MAX_ENCRYPTED_KEY_SZ decryptedKey + * buffer in this flow. ResetStream only zeros aadSz bytes, so + * explicitly zero and release the full buffer here to satisfy + * WOLFSSL_CHECK_MEM_ZERO and avoid leaking key material. */ + if (pkcs7->stream != NULL && pkcs7->stream->aad != NULL) { + ForceZero(pkcs7->stream->aad, MAX_ENCRYPTED_KEY_SZ); + XFREE(pkcs7->stream->aad, pkcs7->heap, DYNAMIC_TYPE_PKCS7); + pkcs7->stream->aad = NULL; + pkcs7->stream->aadSz = 0; + } wc_PKCS7_ResetStream(pkcs7); wc_PKCS7_ChangeState(pkcs7, WC_PKCS7_START); if (pkcs7->cachedEncryptedContent != NULL) { @@ -14196,6 +14428,10 @@ int wc_PKCS7_DecodeAuthEnvelopedData(wc_PKCS7* pkcs7, byte* in, } else { XMEMSET(decryptedKey, 0, MAX_ENCRYPTED_KEY_SZ); + #ifdef WOLFSSL_CHECK_MEM_ZERO + wc_MemZero_Add("wc_PKCS7 decryptedKey", decryptedKey, + MAX_ENCRYPTED_KEY_SZ); + #endif } #ifndef NO_PKCS7_STREAM pkcs7->stream->key = decryptedKey;