From 2107aaad97c12edbeb5f2942d2949655ea93c612 Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Wed, 11 Feb 2026 14:14:42 -0800 Subject: [PATCH 1/3] Initial implementation of proper TLS MAC handling --- include/wolfprovider/internal.h | 3 + src/wp_aes_block.c | 207 +++++++++++++++++++++++++-- src/wp_internal.c | 43 ++++++ test/include.am | 1 + test/test_tls_cbc.c | 240 ++++++++++++++++++++++++++++++++ test/unit.c | 6 + test/unit.h | 6 + 7 files changed, 495 insertions(+), 11 deletions(-) create mode 100644 test/test_tls_cbc.c diff --git a/include/wolfprovider/internal.h b/include/wolfprovider/internal.h index a7f41f91..3f570f4b 100644 --- a/include/wolfprovider/internal.h +++ b/include/wolfprovider/internal.h @@ -252,6 +252,9 @@ BIO* wp_corebio_get_bio(WOLFPROV_CTX* provCtx, OSSL_CORE_BIO *coreBio); byte wp_ct_byte_mask_eq(byte a, byte b); byte wp_ct_byte_mask_ne(byte a, byte b); byte wp_ct_int_mask_gte(int a, int b); +byte wp_ct_int_mask_eq(int a, int b); +byte wp_ct_int_mask_lt(int a, int b); +byte wp_ct_byte_mask_sel(byte mask, byte a, byte b); #endif /* WP_INTERNAL_H */ diff --git a/src/wp_aes_block.c b/src/wp_aes_block.c index cd60f5c4..0275d4dc 100644 --- a/src/wp_aes_block.c +++ b/src/wp_aes_block.c @@ -39,11 +39,21 @@ typedef struct wp_AesBlockCtx { /** wolfSSL AES object. */ Aes aes; + /** Provider context - needed for wolfCrypt RNG access. */ + WOLFPROV_CTX *provCtx; + /** Cipher mode - CBC or ECB. */ int mode; unsigned int tls_version; + /** Pointer to the MAC extracted from a decrypted TLS record. */ + unsigned char *tlsmac; + /** Size of the MAC expected in TLS records. */ + size_t tlsmacsize; + /** Whether tlsmac was separately allocated. */ + int tlsmacAlloced; + /** Length of key in bytes. */ size_t keyLen; /** Length of IV in bytes */ @@ -79,6 +89,9 @@ static int wp_aes_block_set_ctx_params(wp_AesBlockCtx *ctx, */ static void wp_aes_block_freectx(wp_AesBlockCtx *ctx) { + if (ctx->tlsmacAlloced) { + OPENSSL_free(ctx->tlsmac); + } wc_AesFree(&ctx->aes); OPENSSL_clear_free(ctx, sizeof(*ctx)); } @@ -100,6 +113,19 @@ static void *wp_aes_block_dupctx(wp_AesBlockCtx *src) if (dst != NULL) { /* TODO: copying Aes may not work if it has pointers in it. */ XMEMCPY(dst, src, sizeof(*src)); + /* Deep-copy tlsmac to avoid double-free between src and dst. */ + if (src->tlsmacAlloced && src->tlsmac != NULL) { + dst->tlsmac = OPENSSL_malloc(src->tlsmacsize); + if (dst->tlsmac == NULL) { + OPENSSL_free(dst); + return NULL; + } + XMEMCPY(dst->tlsmac, src->tlsmac, src->tlsmacsize); + } + else { + dst->tlsmac = NULL; + dst->tlsmacAlloced = 0; + } } return dst; @@ -207,6 +233,8 @@ static const OSSL_PARAM* wp_cipher_gettable_ctx_params(wp_AesBlockCtx* ctx, OSSL_PARAM_uint(OSSL_CIPHER_PARAM_NUM, NULL), OSSL_PARAM_octet_string(OSSL_CIPHER_PARAM_IV, NULL, 0), OSSL_PARAM_octet_string(OSSL_CIPHER_PARAM_UPDATED_IV, NULL, 0), + { OSSL_CIPHER_PARAM_TLS_MAC, OSSL_PARAM_OCTET_PTR, NULL, 0, + OSSL_PARAM_UNMODIFIED }, OSSL_PARAM_END }; (void)ctx; @@ -232,6 +260,7 @@ static const OSSL_PARAM* wp_cipher_settable_ctx_params(wp_AesBlockCtx* ctx, OSSL_PARAM_uint(OSSL_CIPHER_PARAM_NUM, NULL), OSSL_PARAM_uint(OSSL_CIPHER_PARAM_USE_BITS, NULL), OSSL_PARAM_uint(OSSL_CIPHER_PARAM_TLS_VERSION, NULL), + OSSL_PARAM_size_t(OSSL_CIPHER_PARAM_TLS_MAC_SIZE, NULL), OSSL_PARAM_END }; (void)ctx; @@ -528,18 +557,158 @@ static int wp_aes_block_update(wp_AesBlockCtx *ctx, unsigned char *out, *outLen = oLen; } if (ok && (ctx->tls_version > 0) && (!ctx->enc)) { - unsigned char pad = out[oLen-1]; - int padStart = AES_BLOCK_SIZE - pad - 1; - unsigned char invalid = (pad < AES_BLOCK_SIZE) - 1; - int i; + /* + * TLS 1.2 CBC padding removal and MAC extraction. + * Buffer: [explicit_IV(BS)][payload][MAC(macsize)][padding(pad+1)] + * + * Constant-time padding validation based on OpenSSL's + * tls1_cbc_remove_padding_and_mac() (ssl/record/methods/tls_pad.c) + * + * Constant-time MAC extraction based on OpenSSL's + * ssl3_cbc_copy_mac() rotation pattern. On bad padding the MAC + * is replaced with random bytes via ct select. + */ + unsigned char *rec; + size_t recLen; + size_t origRecLen; + unsigned char padVal; + size_t overhead; + size_t toCheck; + size_t good; + size_t i, j; + size_t macSize = ctx->tlsmacsize; + + /* Free any previously allocated MAC */ + if (ctx->tlsmacAlloced) { + OPENSSL_free(ctx->tlsmac); + ctx->tlsmacAlloced = 0; + ctx->tlsmac = NULL; + } - for (i = AES_BLOCK_SIZE - 1; i >= 0; i--) { - byte check = wp_ct_int_mask_gte(i, padStart); - check &= wp_ct_byte_mask_ne(out[oLen - AES_BLOCK_SIZE + i], pad); - invalid |= check; + if (macSize == 0 || macSize > EVP_MAX_MD_SIZE || + oLen < AES_BLOCK_SIZE + macSize + 1) { + ok = 0; + } + + if (ok) { + /* 64-byte aligned buffer for cache-line-aware MAC rotation */ + unsigned char rotatedMacBuf[64 + EVP_MAX_MD_SIZE]; + unsigned char *rotatedMac; + unsigned char randMac[EVP_MAX_MD_SIZE]; + size_t macEnd; + size_t macStart; + size_t scanStart = 0; + byte inMac; + size_t rotateOff; + + /* Align rotatedMac to 64-byte boundary so the entire MAC + * buffer (up to EVP_MAX_MD_SIZE=64) sits within one or two + * 32-byte cache lines at known positions. */ + rotatedMac = rotatedMacBuf + + ((0 - (size_t)rotatedMacBuf) & 63); + + /* For TLS 1.1+/DTLS: skip explicit IV */ + rec = out + AES_BLOCK_SIZE; + recLen = oLen - AES_BLOCK_SIZE; + origRecLen = recLen; + + padVal = rec[recLen - 1]; + overhead = macSize + (size_t)padVal + 1; + + /* CT overhead check: recLen >= overhead. + * No branch on padVal — fold into good mask instead. */ + good = (size_t)0 - + ((size_t)(wp_ct_int_mask_gte((int)recLen, (int)overhead) & 1)); + + /* Validate padding bytes in constant time. + * Check up to 256 bytes (max TLS padding). */ + toCheck = 256; + if (toCheck > recLen) + toCheck = recLen; + + for (i = 0; i < toCheck; i++) { + byte mask = wp_ct_int_mask_gte((int)padVal, (int)i); + unsigned char b = rec[recLen - 1 - i]; + good &= ~((size_t)(mask & (padVal ^ b))); + } + { + /* Collapse lower 8 bits to full-width size_t mask. + * Same technique as OpenSSL constant_time_eq_s. */ + size_t diff = (good & 0xff) ^ 0xff; + diff |= (0 - diff); + diff >>= (sizeof(size_t) * 8 - 1); + good = diff - 1; + } + + /* Remove padding (only if valid) */ + recLen -= good & ((size_t)padVal + 1); + + macEnd = recLen; + macStart = macEnd - macSize; + + recLen -= macSize; + *outLen = recLen; + + /* Generate random MAC to use if padding was bad */ + #ifndef WP_SINGLE_THREADED + wp_provctx_lock_rng(ctx->provCtx); + #endif + if (wc_RNG_GenerateBlock(wp_provctx_get_rng(ctx->provCtx), + randMac, (word32)macSize) != 0) { + ok = 0; + } + #ifndef WP_SINGLE_THREADED + wp_provctx_unlock_rng(ctx->provCtx); + #endif + + ctx->tlsmac = OPENSSL_malloc(macSize); + if (ctx->tlsmac == NULL) { + ok = 0; + } + else { + ctx->tlsmacAlloced = 1; + + /* Constant-time MAC extraction: scan all bytes that + * could contain the MAC (position varies by up to 255). */ + if (origRecLen > macSize + 255 + 1) + scanStart = origRecLen - (macSize + 255 + 1); + + XMEMSET(rotatedMac, 0, EVP_MAX_MD_SIZE); + inMac = 0; + rotateOff = 0; + for (i = scanStart, j = 0; i < origRecLen; i++) { + byte started = wp_ct_int_mask_eq((int)i, (int)macStart); + byte ended = wp_ct_int_mask_lt((int)i, (int)macEnd); + unsigned char b = rec[i]; + + inMac |= started; + inMac &= ended; + rotateOff |= j & (size_t)started; + rotatedMac[j++] |= b & inMac; + j &= (size_t)wp_ct_int_mask_lt((int)j, (int)macSize); + } + + /* Cache-line-aware un-rotation: always load from both + * 32-byte halves and ct-select to avoid leaking + * rotateOff through cache access patterns. Same + * technique as OpenSSL's CBC_MAC_ROTATE_IN_PLACE. */ + for (i = 0; i < macSize; i++) { + char aux1 = rotatedMac[rotateOff & ~32]; + char aux2 = rotatedMac[rotateOff | 32]; + byte eqMask = wp_ct_int_mask_eq( + (int)(rotateOff & ~32), (int)rotateOff); + unsigned char real = wp_ct_byte_mask_sel( + eqMask, (byte)aux1, (byte)aux2); + byte goodMask = (byte)(good & 0xff); + + ctx->tlsmac[i] = wp_ct_byte_mask_sel(goodMask, real, + randMac[i]); + rotateOff++; + rotateOff &= (size_t)wp_ct_int_mask_lt( + (int)rotateOff, (int)macSize); + } + } } - *outLen = oLen - pad - 1 - AES_BLOCK_SIZE; - ok = invalid == 0; } WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); @@ -790,6 +959,13 @@ static int wp_aes_block_get_ctx_params(wp_AesBlockCtx* ctx, OSSL_PARAM params[]) ok = 0; } } + if (ok) { + p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_TLS_MAC); + if ((p != NULL) && + (!OSSL_PARAM_set_octet_ptr(p, ctx->tlsmac, ctx->tlsmacsize))) { + ok = 0; + } + } WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; @@ -838,6 +1014,15 @@ static int wp_aes_block_set_ctx_params(wp_AesBlockCtx *ctx, &ctx->tls_version, NULL))) { ok = 0; } + if (ok) { + const OSSL_PARAM *pmac = OSSL_PARAM_locate_const(params, + OSSL_CIPHER_PARAM_TLS_MAC_SIZE); + if (pmac != NULL) { + if (!OSSL_PARAM_get_size_t(pmac, &ctx->tlsmacsize)) { + ok = 0; + } + } + } } WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); @@ -891,11 +1076,11 @@ static wp_AesBlockCtx* wp_aes_block_##kBits##_##lcmode##_newctx( \ WOLFPROV_CTX *provCtx) \ { \ wp_AesBlockCtx *ctx = NULL; \ - (void)provCtx; \ if (wolfssl_prov_is_running()) { \ ctx = OPENSSL_zalloc(sizeof(*ctx)); \ } \ if (ctx != NULL) { \ + ctx->provCtx = provCtx; \ wp_aes_block_init_ctx(ctx, kBits, ivBits, EVP_CIPH_##UCMODE##_MODE); \ } \ return ctx; \ diff --git a/src/wp_internal.c b/src/wp_internal.c index de4aa7e0..9be14df7 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -1183,3 +1183,46 @@ byte wp_ct_int_mask_gte(int a, int b) return ((((uint32_t)a - (uint32_t)b) >> 31) - 1); } +/** + * Constant time, set mask when first value is equal to second. + * + * @param [in] a First value. + * @param [in] b Second value. + * @return All bits set when a == b. + * @return 0 when a != b. + */ +byte wp_ct_int_mask_eq(int a, int b) +{ + /* Same as wolfSSL ctMaskEq: ~GT & ~LT */ + byte gt = (byte)((((uint32_t)a - (uint32_t)b - 1) >> 31) - 1); + byte lt = (byte)((((uint32_t)b - (uint32_t)a - 1) >> 31) - 1); + return (byte)((byte)(~gt) & (byte)(~lt)); +} + +/** + * Constant time, set mask when first value is less than second. + * + * @param [in] a First value. + * @param [in] b Second value. + * @return All bits set when a < b. + * @return 0 when a >= b. + */ +byte wp_ct_int_mask_lt(int a, int b) +{ + /* Same as wolfSSL ctMaskLT */ + return (byte)((((uint32_t)b - (uint32_t)a - 1) >> 31) - 1); +} + +/** + * Constant time byte select: returns a when mask is 0xff, b when mask is 0x00. + * + * @param [in] mask Selection mask (0xff or 0x00). + * @param [in] a Value returned when mask is all-ones. + * @param [in] b Value returned when mask is all-zeros. + * @return Selected byte value. + */ +byte wp_ct_byte_mask_sel(byte mask, byte a, byte b) +{ + return (byte)((mask & a) | (~mask & b)); +} + diff --git a/test/include.am b/test/include.am index cc6a31d7..9651d250 100644 --- a/test/include.am +++ b/test/include.am @@ -33,6 +33,7 @@ test_unit_test_SOURCES = \ test/test_rsa.c \ test/test_seccomp_sandbox.c \ test/test_tls1_prf.c \ + test/test_tls_cbc.c \ test/unit.c test_unit_test_LDADD = libwolfprov.la noinst_HEADERS += test/unit.h diff --git a/test/test_tls_cbc.c b/test/test_tls_cbc.c new file mode 100644 index 00000000..4e514951 --- /dev/null +++ b/test/test_tls_cbc.c @@ -0,0 +1,240 @@ +/* test_tls_cbc.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 "unit.h" + +#if defined(WP_HAVE_AESCBC) && defined(WP_HAVE_RSA) && \ + defined(WP_HAVE_ECDH) && defined(WP_HAVE_SHA384) + +/* + * Direct EVP-level test for TLS 1.2 CBC OSSL_CIPHER_PARAM_TLS_MAC handling. + * Exercises the same EVP calls the TLS record layer makes for provided CBC + * ciphers: set TLS_VERSION + TLS_MAC_SIZE, encrypt/decrypt in-place, then + * retrieve the MAC via TLS_MAC get_params. + */ + +/* 37-byte plaintext, intentionally not block-aligned */ +static const unsigned char testPlain[] = "Hello TLS 1.2 CBC test from wolfProv!"; + +#define BS AES_BLOCK_SIZE + +/* Encrypt a TLS 1.2 CBC record in-place. Returns 0 on success. */ +static int test_tls_cbc_enc(EVP_CIPHER *cipher, const unsigned char *key, + const unsigned char *iv, const unsigned char *pt, int ptLen, + const unsigned char *mac, int macSize, unsigned char *buf, int *outLen) +{ + int err = 0; + EVP_CIPHER_CTX *ctx = NULL; + OSSL_PARAM params[3]; + unsigned int tlsVer = TLS1_2_VERSION; + size_t macSz = (size_t)macSize; + int inLen = BS + ptLen + macSize; + + /* Build in-place buffer: [explicit_IV][plaintext][MAC] */ + memcpy(buf, iv, BS); + memcpy(buf + BS, pt, ptLen); + memcpy(buf + BS + ptLen, mac, macSize); + + 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_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, outLen, buf, inLen) != 1; + } + + EVP_CIPHER_CTX_free(ctx); + return err; +} + +/* + * Decrypt a TLS 1.2 CBC record in-place and verify: + * - outLen == ptLen + * - plaintext at buf+BS matches original + * - TLS_MAC get_params returns non-NULL pointer matching original MAC + */ +static int test_tls_cbc_dec(EVP_CIPHER *cipher, const unsigned char *key, + const unsigned char *iv, const unsigned char *origPt, int ptLen, + const unsigned char *origMac, int macSize, unsigned char *buf, int encLen) +{ + int err = 0; + 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; + int outLen = 0; + unsigned char *tlsMac = NULL; + + 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, &outLen, buf, encLen) != 1; + } + + /* Verify output length matches plaintext length */ + if (err == 0 && outLen != ptLen) { + PRINT_ERR_MSG("dec outLen mismatch: got %d, expected %d", outLen, + ptLen); + err = 1; + } + + /* Verify plaintext at buf+BS (past explicit IV) */ + if (err == 0 && memcmp(buf + BS, origPt, ptLen) != 0) { + PRINT_ERR_MSG("dec plaintext mismatch"); + err = 1; + } + + /* Retrieve and verify TLS_MAC */ + 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 && (tlsMac == NULL || memcmp(tlsMac, origMac, macSize) != 0)) { + PRINT_ERR_MSG("dec MAC mismatch or NULL"); + err = 1; + } + + EVP_CIPHER_CTX_free(ctx); + return err; +} + +/* Encrypt with encCtx provider, decrypt with decCtx provider. */ +static int test_tls_cbc_interop(OSSL_LIB_CTX *encCtx, OSSL_LIB_CTX *decCtx, + const char *cipherName, int keyLen, int macSize) +{ + int err = 0; + EVP_CIPHER *encCipher = NULL; + EVP_CIPHER *decCipher = NULL; + unsigned char key[32]; + unsigned char iv[BS]; + unsigned char mac[48]; + unsigned char buf[BS + sizeof(testPlain) + 48 + BS]; + int encLen = 0; + + encCipher = EVP_CIPHER_fetch(encCtx, cipherName, ""); + decCipher = EVP_CIPHER_fetch(decCtx, cipherName, ""); + if (encCipher == NULL || decCipher == NULL) { + err = 1; + } + + if (err == 0) { + memset(key, 0xAA, keyLen); + memset(iv, 0xBB, BS); + memset(mac, 0xCC, macSize); + } + if (err == 0) { + err = test_tls_cbc_enc(encCipher, key, iv, testPlain, + sizeof(testPlain), mac, macSize, buf, &encLen); + } + if (err == 0) { + err = test_tls_cbc_dec(decCipher, key, iv, testPlain, + sizeof(testPlain), mac, macSize, buf, encLen); + } + + EVP_CIPHER_free(encCipher); + EVP_CIPHER_free(decCipher); + return err; +} + +static const struct { + const char *cipher; + int keyLen; + int macSize; +} tlsCbcTests[] = { + { "AES-256-CBC", 32, 48 }, /* ECDHE-RSA-AES256-SHA384 */ + { "AES-128-CBC", 16, 32 }, /* ECDHE-RSA-AES128-SHA256 */ +}; +#define TLS_CBC_TEST_CNT \ + (int)(sizeof(tlsCbcTests) / sizeof(tlsCbcTests[0])) + +int test_tls12_cbc(void *data) +{ + int err = 0; + int i; + + (void)data; + + for (i = 0; i < TLS_CBC_TEST_CNT && err == 0; i++) { + PRINT_MSG("TLS 1.2 CBC (OpenSSL -> wolfProvider): %s mac=%d", + tlsCbcTests[i].cipher, tlsCbcTests[i].macSize); + err = test_tls_cbc_interop(osslLibCtx, wpLibCtx, + tlsCbcTests[i].cipher, + tlsCbcTests[i].keyLen, + tlsCbcTests[i].macSize); + if (err == 0) { + PRINT_MSG("TLS 1.2 CBC (wolfProvider -> OpenSSL): %s mac=%d", + tlsCbcTests[i].cipher, tlsCbcTests[i].macSize); + err = test_tls_cbc_interop(wpLibCtx, osslLibCtx, + tlsCbcTests[i].cipher, + tlsCbcTests[i].keyLen, + tlsCbcTests[i].macSize); + } + } + + return err; +} + +int test_tls12_cbc_ossl(void *data) +{ + int err = 0; + int i; + + (void)data; + + for (i = 0; i < TLS_CBC_TEST_CNT && err == 0; i++) { + PRINT_MSG("TLS 1.2 CBC (OpenSSL baseline): %s mac=%d", + tlsCbcTests[i].cipher, tlsCbcTests[i].macSize); + err = test_tls_cbc_interop(osslLibCtx, osslLibCtx, + tlsCbcTests[i].cipher, + tlsCbcTests[i].keyLen, + tlsCbcTests[i].macSize); + } + + return err; +} + +#endif /* WP_HAVE_AESCBC && WP_HAVE_RSA && WP_HAVE_ECDH && WP_HAVE_SHA384 */ diff --git a/test/unit.c b/test/unit.c index 5c57f044..8e67b467 100644 --- a/test/unit.c +++ b/test/unit.c @@ -437,6 +437,12 @@ TEST_CASE test_case[] = { TEST_DECL(test_pkcs7_x509_sign_verify, NULL), TEST_DECL(test_x509_cert, NULL), + +#if defined(WP_HAVE_AESCBC) && defined(WP_HAVE_RSA) && \ + defined(WP_HAVE_ECDH) && defined(WP_HAVE_SHA384) + TEST_DECL(test_tls12_cbc_ossl, NULL), + TEST_DECL(test_tls12_cbc, NULL), +#endif }; #define TEST_CASE_CNT (int)(sizeof(test_case) / sizeof(*test_case)) diff --git a/test/unit.h b/test/unit.h index 115d4ff3..cc968fcb 100644 --- a/test/unit.h +++ b/test/unit.h @@ -426,4 +426,10 @@ int test_ecx_null_init(void *data); int test_pkcs7_x509_sign_verify(void *data); int test_x509_cert(void *data); +#if defined(WP_HAVE_AESCBC) && defined(WP_HAVE_RSA) && \ + defined(WP_HAVE_ECDH) && defined(WP_HAVE_SHA384) +int test_tls12_cbc(void *data); +int test_tls12_cbc_ossl(void *data); +#endif + #endif /* UNIT_H */ From 9abd2f6238b79c81eda8360c84feef643cd8012c Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Wed, 11 Feb 2026 15:59:06 -0800 Subject: [PATCH 2/3] Also handle encrypt then mac case --- src/wp_aes_block.c | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/wp_aes_block.c b/src/wp_aes_block.c index 0275d4dc..072cff15 100644 --- a/src/wp_aes_block.c +++ b/src/wp_aes_block.c @@ -585,12 +585,49 @@ static int wp_aes_block_update(wp_AesBlockCtx *ctx, unsigned char *out, ctx->tlsmac = NULL; } - if (macSize == 0 || macSize > EVP_MAX_MD_SIZE || + if (macSize > EVP_MAX_MD_SIZE || oLen < AES_BLOCK_SIZE + macSize + 1) { ok = 0; } - if (ok) { + if (ok && macSize == 0) { + /* ETM (Encrypt-then-MAC) or no MAC: the record layer already + * handled the MAC. We only need to strip the explicit IV and + * validate+remove padding (same as OpenSSL ssl3_cbc_copy_mac + * returning early when mac_size == 0). */ + unsigned char *ivRec = out + AES_BLOCK_SIZE; + size_t ivRecLen = oLen - AES_BLOCK_SIZE; + unsigned char padV = ivRec[ivRecLen - 1]; + size_t gd = (size_t)0 - ((size_t)( + wp_ct_int_mask_gte((int)ivRecLen, (int)padV + 1) & 1)); + size_t tc = 256; + if (tc > ivRecLen) + tc = ivRecLen; + + for (i = 0; i < tc; i++) { + byte m = wp_ct_int_mask_gte((int)padV, (int)i); + unsigned char bv = ivRec[ivRecLen - 1 - i]; + gd &= ~((size_t)(m & (padV ^ bv))); + } + { + size_t d = (gd & 0xff) ^ 0xff; + d |= (0 - d); + d >>= (sizeof(size_t) * 8 - 1); + gd = d - 1; + } + ivRecLen -= gd & ((size_t)padV + 1); + *outLen = ivRecLen; + /* No MAC to extract */ + ctx->tlsmac = NULL; + ctx->tlsmacAlloced = 0; + /* With ETM/no-MAC, bad padding is a real error (the MAC was + * already verified by the record layer, so there is no padding + * oracle concern). Matches OpenSSL ssl3_cbc_copy_mac returning + * 0 when good==0 and mac_size==0. */ + if (gd == 0) + ok = 0; + } + else if (ok) { /* 64-byte aligned buffer for cache-line-aware MAC rotation */ unsigned char rotatedMacBuf[64 + EVP_MAX_MD_SIZE]; unsigned char *rotatedMac; From 8df1b8b8974be67bf674895c994a870ec58119e1 Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Fri, 13 Feb 2026 13:13:28 -0800 Subject: [PATCH 3/3] Break out new decryption logic into its own function --- src/wp_aes_block.c | 402 ++++++++++++++++++++++++--------------------- 1 file changed, 213 insertions(+), 189 deletions(-) diff --git a/src/wp_aes_block.c b/src/wp_aes_block.c index 072cff15..2907e247 100644 --- a/src/wp_aes_block.c +++ b/src/wp_aes_block.c @@ -474,6 +474,218 @@ static int wp_aes_block_doit(wp_AesBlockCtx *ctx, unsigned char *out, return rc == 0; } +/** + * TLS 1.2 CBC decryption record post-processing. + * + * Performs constant-time padding validation and MAC extraction after + * CBC decryption. For ETM (Encrypt-then-MAC) or no-MAC modes, strips the + * explicit IV and validates/removes padding. For MtE (MAC-then-Encrypt), + * also extracts the MAC using a constant-time rotation pattern. + * + * @param [in] ctx AES block context object. + * @param [in] out Decrypted output buffer. + * @param [in] oLen Length of decrypted data in bytes. + * @param [in, out] outLen Updated with length after padding/MAC removal. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_aes_block_tls_dec_record(wp_AesBlockCtx *ctx, + unsigned char *out, size_t oLen, size_t *outLen) +{ + int ok = 1; + /* + * TLS 1.2 CBC padding removal and MAC extraction. + * Buffer: [explicit_IV(BS)][payload][MAC(macsize)][padding(pad+1)] + * + * Constant-time padding validation based on OpenSSL's + * tls1_cbc_remove_padding_and_mac() (ssl/record/methods/tls_pad.c) + * + * Constant-time MAC extraction based on OpenSSL's + * ssl3_cbc_copy_mac() rotation pattern. On bad padding the MAC + * is replaced with random bytes via ct select. + */ + unsigned char *rec; + size_t recLen; + size_t origRecLen; + unsigned char padVal; + size_t overhead; + size_t toCheck; + size_t good; + size_t i, j; + size_t macSize = ctx->tlsmacsize; + + /* Free any previously allocated MAC */ + if (ctx->tlsmacAlloced) { + OPENSSL_free(ctx->tlsmac); + ctx->tlsmacAlloced = 0; + ctx->tlsmac = NULL; + } + + if (macSize > EVP_MAX_MD_SIZE || + oLen < AES_BLOCK_SIZE + macSize + 1) { + ok = 0; + } + + if (ok && macSize == 0) { + /* ETM (Encrypt-then-MAC) or no MAC: the record layer already + * handled the MAC. We only need to strip the explicit IV and + * validate+remove padding (same as OpenSSL ssl3_cbc_copy_mac + * returning early when mac_size == 0). */ + unsigned char *ivRec = out + AES_BLOCK_SIZE; + size_t ivRecLen = oLen - AES_BLOCK_SIZE; + unsigned char padV = ivRec[ivRecLen - 1]; + size_t gd = (size_t)0 - ((size_t)( + wp_ct_int_mask_gte((int)ivRecLen, (int)padV + 1) & 1)); + size_t tc = 256; + if (tc > ivRecLen) + tc = ivRecLen; + + for (i = 0; i < tc; i++) { + byte m = wp_ct_int_mask_gte((int)padV, (int)i); + unsigned char bv = ivRec[ivRecLen - 1 - i]; + gd &= ~((size_t)(m & (padV ^ bv))); + } + { + size_t d = (gd & 0xff) ^ 0xff; + d |= (0 - d); + d >>= (sizeof(size_t) * 8 - 1); + gd = d - 1; + } + ivRecLen -= gd & ((size_t)padV + 1); + *outLen = ivRecLen; + /* No MAC to extract */ + ctx->tlsmac = NULL; + ctx->tlsmacAlloced = 0; + /* With ETM/no-MAC, bad padding is a real error (the MAC was + * already verified by the record layer, so there is no padding + * oracle concern). Matches OpenSSL ssl3_cbc_copy_mac returning + * 0 when good==0 and mac_size==0. */ + if (gd == 0) + ok = 0; + } + else if (ok) { + /* 64-byte aligned buffer for cache-line-aware MAC rotation */ + unsigned char rotatedMacBuf[64 + EVP_MAX_MD_SIZE]; + unsigned char *rotatedMac; + unsigned char randMac[EVP_MAX_MD_SIZE]; + size_t macEnd; + size_t macStart; + size_t scanStart = 0; + byte inMac; + size_t rotateOff; + + /* Align rotatedMac to 64-byte boundary so the entire MAC + * buffer (up to EVP_MAX_MD_SIZE=64) sits within one or two + * 32-byte cache lines at known positions. */ + rotatedMac = rotatedMacBuf + + ((0 - (size_t)rotatedMacBuf) & 63); + + /* For TLS 1.1+/DTLS: skip explicit IV */ + rec = out + AES_BLOCK_SIZE; + recLen = oLen - AES_BLOCK_SIZE; + origRecLen = recLen; + + padVal = rec[recLen - 1]; + overhead = macSize + (size_t)padVal + 1; + + /* CT overhead check: recLen >= overhead. + * No branch on padVal — fold into good mask instead. */ + good = (size_t)0 - + ((size_t)(wp_ct_int_mask_gte((int)recLen, (int)overhead) & 1)); + + /* Validate padding bytes in constant time. + * Check up to 256 bytes (max TLS padding). */ + toCheck = 256; + if (toCheck > recLen) + toCheck = recLen; + + for (i = 0; i < toCheck; i++) { + byte mask = wp_ct_int_mask_gte((int)padVal, (int)i); + unsigned char b = rec[recLen - 1 - i]; + good &= ~((size_t)(mask & (padVal ^ b))); + } + { + /* Collapse lower 8 bits to full-width size_t mask. + * Same technique as OpenSSL constant_time_eq_s. */ + size_t diff = (good & 0xff) ^ 0xff; + diff |= (0 - diff); + diff >>= (sizeof(size_t) * 8 - 1); + good = diff - 1; + } + + /* Remove padding (only if valid) */ + recLen -= good & ((size_t)padVal + 1); + + macEnd = recLen; + macStart = macEnd - macSize; + + recLen -= macSize; + *outLen = recLen; + + /* Generate random MAC to use if padding was bad */ + #ifndef WP_SINGLE_THREADED + wp_provctx_lock_rng(ctx->provCtx); + #endif + if (wc_RNG_GenerateBlock(wp_provctx_get_rng(ctx->provCtx), + randMac, (word32)macSize) != 0) { + ok = 0; + } + #ifndef WP_SINGLE_THREADED + wp_provctx_unlock_rng(ctx->provCtx); + #endif + + ctx->tlsmac = OPENSSL_malloc(macSize); + if (ctx->tlsmac == NULL) { + ok = 0; + } + else { + ctx->tlsmacAlloced = 1; + + /* Constant-time MAC extraction: scan all bytes that + * could contain the MAC (position varies by up to 255). */ + if (origRecLen > macSize + 255 + 1) + scanStart = origRecLen - (macSize + 255 + 1); + + XMEMSET(rotatedMac, 0, EVP_MAX_MD_SIZE); + inMac = 0; + rotateOff = 0; + for (i = scanStart, j = 0; i < origRecLen; i++) { + byte started = wp_ct_int_mask_eq((int)i, (int)macStart); + byte ended = wp_ct_int_mask_lt((int)i, (int)macEnd); + unsigned char b = rec[i]; + + inMac |= started; + inMac &= ended; + rotateOff |= j & (size_t)started; + rotatedMac[j++] |= b & inMac; + j &= (size_t)wp_ct_int_mask_lt((int)j, (int)macSize); + } + + /* Cache-line-aware un-rotation: always load from both + * 32-byte halves and ct-select to avoid leaking + * rotateOff through cache access patterns. Same + * technique as OpenSSL's CBC_MAC_ROTATE_IN_PLACE. */ + for (i = 0; i < macSize; i++) { + char aux1 = rotatedMac[rotateOff & ~32]; + char aux2 = rotatedMac[rotateOff | 32]; + byte eqMask = wp_ct_int_mask_eq( + (int)(rotateOff & ~32), (int)rotateOff); + unsigned char real = wp_ct_byte_mask_sel( + eqMask, (byte)aux1, (byte)aux2); + byte goodMask = (byte)(good & 0xff); + + ctx->tlsmac[i] = wp_ct_byte_mask_sel(goodMask, real, + randMac[i]); + rotateOff++; + rotateOff &= (size_t)wp_ct_int_mask_lt( + (int)rotateOff, (int)macSize); + } + } + } + + return ok; +} + /** * Update encryption/decryption with more data. * @@ -557,195 +769,7 @@ static int wp_aes_block_update(wp_AesBlockCtx *ctx, unsigned char *out, *outLen = oLen; } if (ok && (ctx->tls_version > 0) && (!ctx->enc)) { - /* - * TLS 1.2 CBC padding removal and MAC extraction. - * Buffer: [explicit_IV(BS)][payload][MAC(macsize)][padding(pad+1)] - * - * Constant-time padding validation based on OpenSSL's - * tls1_cbc_remove_padding_and_mac() (ssl/record/methods/tls_pad.c) - * - * Constant-time MAC extraction based on OpenSSL's - * ssl3_cbc_copy_mac() rotation pattern. On bad padding the MAC - * is replaced with random bytes via ct select. - */ - unsigned char *rec; - size_t recLen; - size_t origRecLen; - unsigned char padVal; - size_t overhead; - size_t toCheck; - size_t good; - size_t i, j; - size_t macSize = ctx->tlsmacsize; - - /* Free any previously allocated MAC */ - if (ctx->tlsmacAlloced) { - OPENSSL_free(ctx->tlsmac); - ctx->tlsmacAlloced = 0; - ctx->tlsmac = NULL; - } - - if (macSize > EVP_MAX_MD_SIZE || - oLen < AES_BLOCK_SIZE + macSize + 1) { - ok = 0; - } - - if (ok && macSize == 0) { - /* ETM (Encrypt-then-MAC) or no MAC: the record layer already - * handled the MAC. We only need to strip the explicit IV and - * validate+remove padding (same as OpenSSL ssl3_cbc_copy_mac - * returning early when mac_size == 0). */ - unsigned char *ivRec = out + AES_BLOCK_SIZE; - size_t ivRecLen = oLen - AES_BLOCK_SIZE; - unsigned char padV = ivRec[ivRecLen - 1]; - size_t gd = (size_t)0 - ((size_t)( - wp_ct_int_mask_gte((int)ivRecLen, (int)padV + 1) & 1)); - size_t tc = 256; - if (tc > ivRecLen) - tc = ivRecLen; - - for (i = 0; i < tc; i++) { - byte m = wp_ct_int_mask_gte((int)padV, (int)i); - unsigned char bv = ivRec[ivRecLen - 1 - i]; - gd &= ~((size_t)(m & (padV ^ bv))); - } - { - size_t d = (gd & 0xff) ^ 0xff; - d |= (0 - d); - d >>= (sizeof(size_t) * 8 - 1); - gd = d - 1; - } - ivRecLen -= gd & ((size_t)padV + 1); - *outLen = ivRecLen; - /* No MAC to extract */ - ctx->tlsmac = NULL; - ctx->tlsmacAlloced = 0; - /* With ETM/no-MAC, bad padding is a real error (the MAC was - * already verified by the record layer, so there is no padding - * oracle concern). Matches OpenSSL ssl3_cbc_copy_mac returning - * 0 when good==0 and mac_size==0. */ - if (gd == 0) - ok = 0; - } - else if (ok) { - /* 64-byte aligned buffer for cache-line-aware MAC rotation */ - unsigned char rotatedMacBuf[64 + EVP_MAX_MD_SIZE]; - unsigned char *rotatedMac; - unsigned char randMac[EVP_MAX_MD_SIZE]; - size_t macEnd; - size_t macStart; - size_t scanStart = 0; - byte inMac; - size_t rotateOff; - - /* Align rotatedMac to 64-byte boundary so the entire MAC - * buffer (up to EVP_MAX_MD_SIZE=64) sits within one or two - * 32-byte cache lines at known positions. */ - rotatedMac = rotatedMacBuf + - ((0 - (size_t)rotatedMacBuf) & 63); - - /* For TLS 1.1+/DTLS: skip explicit IV */ - rec = out + AES_BLOCK_SIZE; - recLen = oLen - AES_BLOCK_SIZE; - origRecLen = recLen; - - padVal = rec[recLen - 1]; - overhead = macSize + (size_t)padVal + 1; - - /* CT overhead check: recLen >= overhead. - * No branch on padVal — fold into good mask instead. */ - good = (size_t)0 - - ((size_t)(wp_ct_int_mask_gte((int)recLen, (int)overhead) & 1)); - - /* Validate padding bytes in constant time. - * Check up to 256 bytes (max TLS padding). */ - toCheck = 256; - if (toCheck > recLen) - toCheck = recLen; - - for (i = 0; i < toCheck; i++) { - byte mask = wp_ct_int_mask_gte((int)padVal, (int)i); - unsigned char b = rec[recLen - 1 - i]; - good &= ~((size_t)(mask & (padVal ^ b))); - } - { - /* Collapse lower 8 bits to full-width size_t mask. - * Same technique as OpenSSL constant_time_eq_s. */ - size_t diff = (good & 0xff) ^ 0xff; - diff |= (0 - diff); - diff >>= (sizeof(size_t) * 8 - 1); - good = diff - 1; - } - - /* Remove padding (only if valid) */ - recLen -= good & ((size_t)padVal + 1); - - macEnd = recLen; - macStart = macEnd - macSize; - - recLen -= macSize; - *outLen = recLen; - - /* Generate random MAC to use if padding was bad */ - #ifndef WP_SINGLE_THREADED - wp_provctx_lock_rng(ctx->provCtx); - #endif - if (wc_RNG_GenerateBlock(wp_provctx_get_rng(ctx->provCtx), - randMac, (word32)macSize) != 0) { - ok = 0; - } - #ifndef WP_SINGLE_THREADED - wp_provctx_unlock_rng(ctx->provCtx); - #endif - - ctx->tlsmac = OPENSSL_malloc(macSize); - if (ctx->tlsmac == NULL) { - ok = 0; - } - else { - ctx->tlsmacAlloced = 1; - - /* Constant-time MAC extraction: scan all bytes that - * could contain the MAC (position varies by up to 255). */ - if (origRecLen > macSize + 255 + 1) - scanStart = origRecLen - (macSize + 255 + 1); - - XMEMSET(rotatedMac, 0, EVP_MAX_MD_SIZE); - inMac = 0; - rotateOff = 0; - for (i = scanStart, j = 0; i < origRecLen; i++) { - byte started = wp_ct_int_mask_eq((int)i, (int)macStart); - byte ended = wp_ct_int_mask_lt((int)i, (int)macEnd); - unsigned char b = rec[i]; - - inMac |= started; - inMac &= ended; - rotateOff |= j & (size_t)started; - rotatedMac[j++] |= b & inMac; - j &= (size_t)wp_ct_int_mask_lt((int)j, (int)macSize); - } - - /* Cache-line-aware un-rotation: always load from both - * 32-byte halves and ct-select to avoid leaking - * rotateOff through cache access patterns. Same - * technique as OpenSSL's CBC_MAC_ROTATE_IN_PLACE. */ - for (i = 0; i < macSize; i++) { - char aux1 = rotatedMac[rotateOff & ~32]; - char aux2 = rotatedMac[rotateOff | 32]; - byte eqMask = wp_ct_int_mask_eq( - (int)(rotateOff & ~32), (int)rotateOff); - unsigned char real = wp_ct_byte_mask_sel( - eqMask, (byte)aux1, (byte)aux2); - byte goodMask = (byte)(good & 0xff); - - ctx->tlsmac[i] = wp_ct_byte_mask_sel(goodMask, real, - randMac[i]); - rotateOff++; - rotateOff &= (size_t)wp_ct_int_mask_lt( - (int)rotateOff, (int)macSize); - } - } - } + ok = wp_aes_block_tls_dec_record(ctx, out, oLen, outLen); } WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);