diff --git a/src/wp_internal.c b/src/wp_internal.c index b5bfec7c..f8f5609e 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -1152,7 +1152,7 @@ BIO* wp_corebio_get_bio(WOLFPROV_CTX* provCtx, OSSL_CORE_BIO *coreBio) /** * Constant time, set mask when first value is equal to second. * - * @param [in] a First valuue. + * @param [in] a First value. * @param [in] b Second value. * @return All bits set when true. * @return 0 when false. @@ -1165,20 +1165,20 @@ byte wp_ct_byte_mask_eq(byte a, byte b) /** * Constant time, set mask when first value is not equal to second. * - * @param [in] a First valuue. + * @param [in] a First value. * @param [in] b Second value. * @return All bits set when true. * @return 0 when false. */ byte wp_ct_byte_mask_ne(byte a, byte b) { - return (((int32_t)b - a) >> 31) & (((int32_t)a - b) >> 31); + return ~wp_ct_byte_mask_eq(a, b); } /** * Constant time, set mask when first value is greater than or equal second. * - * @param [in] a First valuue. + * @param [in] a First value. * @param [in] b Second value. * @return All bits set when true. * @return 0 when false. diff --git a/test/include.am b/test/include.am index 9651d250..f0043c3f 100644 --- a/test/include.am +++ b/test/include.am @@ -15,6 +15,7 @@ test_unit_test_SOURCES = \ test/test_aestag.c \ test/test_cipher.c \ test/test_cmac.c \ + test/test_ct.c \ test/test_dh.c \ test/test_digest.c \ test/test_rand_seed.c \ diff --git a/test/test_cipher.c b/test/test_cipher.c index 763eddf3..2a95155a 100644 --- a/test/test_cipher.c +++ b/test/test_cipher.c @@ -457,6 +457,86 @@ int test_des3_cbc_stream(void *data) return err; } +/* + * Negative PKCS#7 padding test for DES3-CBC. + * Encrypt block-aligned plaintext (produces a full padding block), corrupt a + * ciphertext byte so the padding block is garbled, verify DecryptFinal rejects. + */ +int test_des3_cbc_bad_pad(void *data) +{ + int err = 0; + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *ctx = NULL; + unsigned char key[24]; + unsigned char iv[8]; + unsigned char pt[8]; /* block-aligned: PKCS#7 adds full 8-byte pad block */ + unsigned char ct[24]; /* 8 pt + 8 pad = 16, but EncryptFinal may need room */ + unsigned char dec[24]; + int outLen = 0, fLen = 0; + + (void)data; + + PRINT_MSG("DES3-CBC negative PKCS#7 padding"); + + memset(key, 0xAA, sizeof(key)); + memset(iv, 0xBB, sizeof(iv)); + memset(pt, 0x42, sizeof(pt)); + + cipher = EVP_CIPHER_fetch(wpLibCtx, "DES-EDE3-CBC", ""); + if (cipher == NULL) { + err = 1; + } + + /* Encrypt with padding. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + err = 1; + } + if (err == 0) { + err = EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv) != 1; + } + if (err == 0) { + err = EVP_EncryptUpdate(ctx, ct, &outLen, pt, (int)sizeof(pt)) != 1; + } + if (err == 0) { + err = EVP_EncryptFinal_ex(ctx, ct + outLen, &fLen) != 1; + outLen += fLen; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + + /* Corrupt first ciphertext block -- CBC garbles the padding block. */ + if (err == 0) { + ct[0] ^= 0x01; + } + + /* Decrypt -- DecryptFinal should fail due to garbled padding. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + err = 1; + } + if (err == 0) { + err = EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv) != 1; + } + if (err == 0) { + fLen = 0; + err = EVP_DecryptUpdate(ctx, dec, &fLen, ct, outLen) != 1; + } + if (err == 0) { + int ret = EVP_DecryptFinal_ex(ctx, dec + fLen, &fLen); + if (ret == 1) { + PRINT_ERR_MSG("DES3-CBC bad-pad: DecryptFinal should have failed"); + err = 1; + } + } + + EVP_CIPHER_CTX_free(ctx); + EVP_CIPHER_free(cipher); + return err; +} + #endif /* WP_HAVE_DES3CBC */ /******************************************************************************/ @@ -1326,4 +1406,86 @@ int test_aes256_cbc_multiple(void *data) return err; } + +/* + * Negative PKCS#7 padding test for AES-256-CBC. + * Encrypt block-aligned plaintext (produces a full padding block), corrupt a + * ciphertext byte so the padding block is garbled, verify DecryptFinal rejects. + */ +int test_aes256_cbc_bad_pad(void *data) +{ + int err = 0; + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *ctx = NULL; + unsigned char key[32]; + unsigned char iv[16]; + unsigned char pt[16]; /* block-aligned: PKCS#7 adds full 16-byte pad block */ + unsigned char ct[48]; + unsigned char dec[48]; + int outLen = 0, fLen = 0; + + (void)data; + + PRINT_MSG("AES-256-CBC negative PKCS#7 padding"); + + memset(key, 0xAA, sizeof(key)); + memset(iv, 0xBB, sizeof(iv)); + memset(pt, 0x42, sizeof(pt)); + + cipher = EVP_CIPHER_fetch(wpLibCtx, "AES-256-CBC", ""); + if (cipher == NULL) { + err = 1; + } + + /* Encrypt with padding. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + err = 1; + } + if (err == 0) { + err = EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv) != 1; + } + if (err == 0) { + err = EVP_EncryptUpdate(ctx, ct, &outLen, pt, (int)sizeof(pt)) != 1; + } + if (err == 0) { + err = EVP_EncryptFinal_ex(ctx, ct + outLen, &fLen) != 1; + outLen += fLen; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + + /* Corrupt first ciphertext block -- CBC garbles the padding block. */ + if (err == 0) { + ct[0] ^= 0x01; + } + + /* Decrypt -- DecryptFinal should fail due to garbled padding. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + err = 1; + } + if (err == 0) { + err = EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv) != 1; + } + if (err == 0) { + fLen = 0; + err = EVP_DecryptUpdate(ctx, dec, &fLen, ct, outLen) != 1; + } + if (err == 0) { + int ret = EVP_DecryptFinal_ex(ctx, dec + fLen, &fLen); + if (ret == 1) { + PRINT_ERR_MSG("AES-256-CBC bad-pad: DecryptFinal should have " + "failed"); + err = 1; + } + } + + EVP_CIPHER_CTX_free(ctx); + EVP_CIPHER_free(cipher); + return err; +} + #endif /* WP_HAVE_AESCBC */ diff --git a/test/test_ct.c b/test/test_ct.c new file mode 100644 index 00000000..5b2c09cb --- /dev/null +++ b/test/test_ct.c @@ -0,0 +1,132 @@ +/* test_ct.c + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include +#include "unit.h" + +int test_ct_masks(void *data) +{ + int err = 0; + int a, b; + byte res; + + (void)data; + + PRINT_MSG("Testing CT byte mask functions (exhaustive)"); + + /* Exhaustive test of all 65536 byte pairs for eq, ne, and their + * relationship: ne(a,b) == (byte)~eq(a,b). */ + for (a = 0; a <= 255 && err == 0; a++) { + for (b = 0; b <= 255 && err == 0; b++) { + byte eqRes = wp_ct_byte_mask_eq((byte)a, (byte)b); + byte neRes = wp_ct_byte_mask_ne((byte)a, (byte)b); + byte expEq = (a == b) ? 0xFF : 0x00; + byte expNe = (a != b) ? 0xFF : 0x00; + byte eqNeg; + + if (eqRes != expEq) { + PRINT_ERR_MSG("ct_byte_mask_eq(%d, %d) = 0x%02x, expected " + "0x%02x", a, b, eqRes, expEq); + err = 1; + } + if (neRes != expNe) { + PRINT_ERR_MSG("ct_byte_mask_ne(%d, %d) = 0x%02x, expected " + "0x%02x", a, b, neRes, expNe); + err = 1; + } + eqNeg = (byte)(eqRes ^ (byte)0xFF); + if (eqNeg != neRes) { + PRINT_ERR_MSG("ct_byte_mask ne/eq mismatch at (%d, %d): " + "~eq=0x%02x ne=0x%02x", a, b, + eqNeg, neRes); + err = 1; + } + } + } + + PRINT_MSG("Testing CT int mask functions (boundary values)"); + + /* Test int comparison functions over a set of boundary values that cover + * the actual usage domain (padding indices, record lengths, versions). */ + { + static const int vals[] = {0, 1, 2, 127, 128, 254, 255, 256, 1000}; + int nvals = (int)(sizeof(vals) / sizeof(vals[0])); + int i, j; + + for (i = 0; i < nvals && err == 0; i++) { + for (j = 0; j < nvals && err == 0; j++) { + a = vals[i]; + b = vals[j]; + + res = wp_ct_int_mask_gte(a, b); + if (res != ((a >= b) ? 0xFF : 0x00)) { + PRINT_ERR_MSG("ct_int_mask_gte(%d, %d) = 0x%02x, expected " + "0x%02x", a, b, res, + (a >= b) ? 0xFF : 0x00); + err = 1; + } + + res = wp_ct_int_mask_eq(a, b); + if (res != ((a == b) ? 0xFF : 0x00)) { + PRINT_ERR_MSG("ct_int_mask_eq(%d, %d) = 0x%02x, expected " + "0x%02x", a, b, res, + (a == b) ? 0xFF : 0x00); + err = 1; + } + + res = wp_ct_int_mask_lt(a, b); + if (res != ((a < b) ? 0xFF : 0x00)) { + PRINT_ERR_MSG("ct_int_mask_lt(%d, %d) = 0x%02x, expected " + "0x%02x", a, b, res, + (a < b) ? 0xFF : 0x00); + err = 1; + } + } + } + } + + PRINT_MSG("Testing CT byte mask sel"); + + /* Selection: mask=0xFF picks a, mask=0x00 picks b. */ + res = wp_ct_byte_mask_sel(0xFF, 0xAB, 0xCD); + if (res != 0xAB) { + PRINT_ERR_MSG("ct_byte_mask_sel(0xFF, 0xAB, 0xCD) = 0x%02x", res); + err = 1; + } + res = wp_ct_byte_mask_sel(0x00, 0xAB, 0xCD); + if (res != 0xCD) { + PRINT_ERR_MSG("ct_byte_mask_sel(0x00, 0xAB, 0xCD) = 0x%02x", res); + err = 1; + } + + /* Selection driven by eq/ne masks. */ + res = wp_ct_byte_mask_sel(wp_ct_byte_mask_eq(5, 5), 0x11, 0x22); + if (res != 0x11) { + PRINT_ERR_MSG("ct_byte_mask_sel(eq(5,5), 0x11, 0x22) = 0x%02x", res); + err = 1; + } + res = wp_ct_byte_mask_sel(wp_ct_byte_mask_eq(5, 6), 0x11, 0x22); + if (res != 0x22) { + PRINT_ERR_MSG("ct_byte_mask_sel(eq(5,6), 0x11, 0x22) = 0x%02x", res); + err = 1; + } + + return err; +} diff --git a/test/test_tls_cbc.c b/test/test_tls_cbc.c index 4e514951..10da08d1 100644 --- a/test/test_tls_cbc.c +++ b/test/test_tls_cbc.c @@ -237,4 +237,217 @@ int test_tls12_cbc_ossl(void *data) return err; } +/* + * AES TLS CBC negative padding test. + * + * MtE TLS with macSize > 0 always returns success from decryption -- bad + * padding causes a random MAC substitution (preventing padding oracle). + * Verify the extracted MAC does NOT match the original. + */ +static int test_aes_tls_cbc_bad_pad_helper(OSSL_LIB_CTX *libCtx, + const char *cipherName, int keyLen, int macSize) +{ + int err = 0; + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *ctx = NULL; + OSSL_PARAM params[3]; + OSSL_PARAM getParams[2]; + unsigned int tlsVer = TLS1_2_VERSION; + size_t macSz = (size_t)macSize; + unsigned char key[32]; + unsigned char iv[BS]; + unsigned char mac[48]; + unsigned char buf[BS + sizeof(testPlain) + 48 + BS]; + int encLen = 0; + int decLen = 0; + int ptLen = (int)sizeof(testPlain); + unsigned char *tlsMac = NULL; + + memset(key, 0xAA, keyLen); + memset(iv, 0xBB, BS); + memset(mac, 0xCC, macSize); + + cipher = EVP_CIPHER_fetch(libCtx, cipherName, ""); + if (cipher == NULL) { + err = 1; + } + + /* Encrypt a valid TLS record. */ + if (err == 0) { + err = test_tls_cbc_enc(cipher, key, iv, testPlain, ptLen, + mac, macSize, buf, &encLen); + } + + /* CBC bit-flip: corrupt second-to-last byte in second-to-last ciphertext + * block, flipping a padding byte in the last plaintext block. */ + if (err == 0) { + int corruptOffset = encLen - BS - 2; + buf[corruptOffset] ^= 0x01; + } + + /* Decrypt -- MtE TLS returns success but substitutes a random MAC. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + err = 1; + } + } + if (err == 0) { + err = EVP_CipherInit_ex(ctx, cipher, NULL, key, iv, 0) != 1; + } + if (err == 0) { + params[0] = OSSL_PARAM_construct_uint(OSSL_CIPHER_PARAM_TLS_VERSION, + &tlsVer); + params[1] = OSSL_PARAM_construct_size_t(OSSL_CIPHER_PARAM_TLS_MAC_SIZE, + &macSz); + params[2] = OSSL_PARAM_construct_end(); + err = EVP_CIPHER_CTX_set_params(ctx, params) != 1; + } + if (err == 0) { + err = EVP_CipherUpdate(ctx, buf, &decLen, buf, encLen) != 1; + } + + /* Bad padding should have triggered random MAC substitution. */ + if (err == 0) { + getParams[0] = OSSL_PARAM_construct_octet_ptr( + OSSL_CIPHER_PARAM_TLS_MAC, (void **)&tlsMac, macSize); + getParams[1] = OSSL_PARAM_construct_end(); + err = EVP_CIPHER_CTX_get_params(ctx, getParams) != 1; + } + if (err == 0) { + if (tlsMac != NULL && memcmp(tlsMac, mac, macSize) == 0) { + PRINT_ERR_MSG("TLS CBC bad-pad: MAC should have been randomized " + "but matches original (%s)", cipherName); + err = 1; + } + } + + EVP_CIPHER_CTX_free(ctx); + EVP_CIPHER_free(cipher); + return err; +} + +int test_aes_tls_cbc_bad_pad(void *data) +{ + int err = 0; + + (void)data; + + PRINT_MSG("TLS 1.2 AES-256-CBC negative padding (wolfProvider)"); + err = test_aes_tls_cbc_bad_pad_helper(wpLibCtx, "AES-256-CBC", 32, 48); + if (err == 0) { + PRINT_MSG("TLS 1.2 AES-128-CBC negative padding (wolfProvider)"); + err = test_aes_tls_cbc_bad_pad_helper(wpLibCtx, "AES-128-CBC", 16, 32); + } + + return err; +} + #endif /* WP_HAVE_AESCBC && WP_HAVE_RSA && WP_HAVE_ECDH && WP_HAVE_SHA384 */ + +#ifdef WP_HAVE_DES3CBC +#if !defined(HAVE_FIPS) || defined(WP_ALLOW_NON_FIPS) + +#define DES3_BS 8 + +/* + * DES3 TLS CBC negative padding test. + * Exercises wp_ct_byte_mask_ne in the DES3 TLS constant-time padding path. + * DES3 TLS does not use TLS_MAC_SIZE/TLS_MAC -- it only validates padding. + */ +static int test_des3_tls_cbc_bad_pad_helper(OSSL_LIB_CTX *libCtx) +{ + int err = 0; + EVP_CIPHER *cipher = NULL; + EVP_CIPHER_CTX *ctx = NULL; + OSSL_PARAM params[2]; + unsigned int tlsVer = TLS1_2_VERSION; + unsigned char key[24]; + unsigned char iv[DES3_BS]; + /* 10 bytes of plaintext. Padding: off=10%8=2, pad=8-2-1=5, padded=16. */ + unsigned char pt[10]; + unsigned char buf[64]; + int encLen = 0; + int decLen = 0; + + memset(key, 0xAA, sizeof(key)); + memset(iv, 0xBB, sizeof(iv)); + memset(pt, 0x42, sizeof(pt)); + + cipher = EVP_CIPHER_fetch(libCtx, "DES-EDE3-CBC", ""); + if (cipher == NULL) { + err = 1; + } + + /* Encrypt in TLS mode. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + err = 1; + } + } + if (err == 0) { + err = EVP_CipherInit_ex(ctx, cipher, NULL, key, iv, 1) != 1; + } + if (err == 0) { + params[0] = OSSL_PARAM_construct_uint(OSSL_CIPHER_PARAM_TLS_VERSION, + &tlsVer); + params[1] = OSSL_PARAM_construct_end(); + err = EVP_CIPHER_CTX_set_params(ctx, params) != 1; + } + if (err == 0) { + /* Copy plaintext into buf; the provider pads in-place in the output. */ + memcpy(buf, pt, sizeof(pt)); + err = EVP_CipherUpdate(ctx, buf, &encLen, buf, (int)sizeof(pt)) != 1; + } + EVP_CIPHER_CTX_free(ctx); + ctx = NULL; + + /* CBC bit-flip: corrupt a padding byte in the last plaintext block + * without touching the pad-length byte at the final position. */ + if (err == 0) { + buf[encLen - DES3_BS - 2] ^= 0x01; + } + + /* Decrypt -- should fail due to bad padding. */ + if (err == 0) { + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + err = 1; + } + } + if (err == 0) { + err = EVP_CipherInit_ex(ctx, cipher, NULL, key, iv, 0) != 1; + } + if (err == 0) { + params[0] = OSSL_PARAM_construct_uint(OSSL_CIPHER_PARAM_TLS_VERSION, + &tlsVer); + params[1] = OSSL_PARAM_construct_end(); + err = EVP_CIPHER_CTX_set_params(ctx, params) != 1; + } + if (err == 0) { + int ret = EVP_CipherUpdate(ctx, buf, &decLen, buf, encLen); + if (ret == 1) { + PRINT_ERR_MSG("DES3 TLS CBC bad-pad: decryption should have failed " + "but succeeded"); + err = 1; + } + } + + EVP_CIPHER_CTX_free(ctx); + EVP_CIPHER_free(cipher); + return err; +} + +int test_des3_tls_cbc_bad_pad(void *data) +{ + (void)data; + + PRINT_MSG("DES3 TLS 1.2 CBC negative padding (wolfProvider)"); + return test_des3_tls_cbc_bad_pad_helper(wpLibCtx); +} + +#undef DES3_BS + +#endif /* !HAVE_FIPS || WP_ALLOW_NON_FIPS */ +#endif /* WP_HAVE_DES3CBC */ diff --git a/test/unit.c b/test/unit.c index 68fb7cab..564b05ae 100644 --- a/test/unit.c +++ b/test/unit.c @@ -171,6 +171,7 @@ static unsigned long flags = 0; TEST_CASE test_case[] = { TEST_DECL(test_logging, &debug), + TEST_DECL(test_ct_masks, NULL), #ifdef WP_HAVE_SHA1 TEST_DECL(test_sha, NULL), #endif @@ -227,6 +228,7 @@ TEST_CASE test_case[] = { #if !defined(HAVE_FIPS) || defined(WP_ALLOW_NON_FIPS) TEST_DECL(test_des3_cbc, NULL), TEST_DECL(test_des3_cbc_stream, NULL), + TEST_DECL(test_des3_cbc_bad_pad, NULL), #endif #endif #ifdef WP_HAVE_AESECB @@ -245,6 +247,7 @@ TEST_CASE test_case[] = { TEST_DECL(test_aes192_cbc_stream, NULL), TEST_DECL(test_aes256_cbc_stream, NULL), TEST_DECL(test_aes256_cbc_multiple, NULL), + TEST_DECL(test_aes256_cbc_bad_pad, NULL), #endif #ifdef WP_HAVE_AESCTR TEST_DECL(test_aes128_ctr_stream, NULL), @@ -446,6 +449,12 @@ TEST_CASE test_case[] = { defined(WP_HAVE_ECDH) && defined(WP_HAVE_SHA384) TEST_DECL(test_tls12_cbc_ossl, NULL), TEST_DECL(test_tls12_cbc, NULL), + TEST_DECL(test_aes_tls_cbc_bad_pad, NULL), +#endif +#ifdef WP_HAVE_DES3CBC + #if !defined(HAVE_FIPS) || defined(WP_ALLOW_NON_FIPS) + TEST_DECL(test_des3_tls_cbc_bad_pad, NULL), + #endif #endif }; #define TEST_CASE_CNT (int)(sizeof(test_case) / sizeof(*test_case)) diff --git a/test/unit.h b/test/unit.h index 25103463..41557ebb 100644 --- a/test/unit.h +++ b/test/unit.h @@ -87,6 +87,7 @@ typedef struct TEST_CASE { } TEST_CASE; int test_logging(void *data); +int test_ct_masks(void *data); #define WP_VALGRIND_TEST 0x1 @@ -147,6 +148,7 @@ int test_krb5kdf(void *data); #ifdef WP_HAVE_DES3CBC int test_des3_cbc(void *data); int test_des3_cbc_stream(void *data); +int test_des3_cbc_bad_pad(void *data); #endif #ifdef WP_HAVE_AESECB @@ -169,6 +171,7 @@ int test_aes128_cbc_stream(void *data); int test_aes192_cbc_stream(void *data); int test_aes256_cbc_stream(void *data); int test_aes256_cbc_multiple(void *data); +int test_aes256_cbc_bad_pad(void *data); #endif @@ -437,6 +440,13 @@ int test_x509_cert(void *data); defined(WP_HAVE_ECDH) && defined(WP_HAVE_SHA384) int test_tls12_cbc(void *data); int test_tls12_cbc_ossl(void *data); +int test_aes_tls_cbc_bad_pad(void *data); +#endif + +#ifdef WP_HAVE_DES3CBC +#if !defined(HAVE_FIPS) || defined(WP_ALLOW_NON_FIPS) +int test_des3_tls_cbc_bad_pad(void *data); +#endif #endif #endif /* UNIT_H */