Harden PKCS#7 EnvelopedData key unwrap

This commit is contained in:
Tobias Frauenschläger
2026-04-14 18:04:12 +02:00
parent 3fd4060458
commit 589feabc0c
4 changed files with 510 additions and 13 deletions
+1
View File
@@ -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
+245 -6
View File
@@ -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;
}
+21
View File
@@ -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), \
+243 -7
View File
@@ -44,6 +44,9 @@
#include <wolfssl/wolfcrypt/pkcs7.h>
#include <wolfssl/wolfcrypt/hash.h>
#ifndef NO_HMAC
#include <wolfssl/wolfcrypt/hmac.h>
#endif
#ifndef NO_RSA
#include <wolfssl/wolfcrypt/rsa.h>
#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;