Skip to content

Commit f96f04d

Browse files
authored
Initial implementation of proper TLS MAC handling (#360)
* Initial implementation of proper TLS MAC handling * Also handle encrypt then mac case * Break out new decryption logic into its own function * Updates per review comments
1 parent d7c1942 commit f96f04d

File tree

7 files changed

+564
-13
lines changed

7 files changed

+564
-13
lines changed

include/wolfprovider/internal.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ BIO* wp_corebio_get_bio(WOLFPROV_CTX* provCtx, OSSL_CORE_BIO *coreBio);
252252
byte wp_ct_byte_mask_eq(byte a, byte b);
253253
byte wp_ct_byte_mask_ne(byte a, byte b);
254254
byte wp_ct_int_mask_gte(int a, int b);
255+
byte wp_ct_int_mask_eq(int a, int b);
256+
byte wp_ct_int_mask_lt(int a, int b);
257+
byte wp_ct_byte_mask_sel(byte mask, byte a, byte b);
255258

256259
#endif /* WP_INTERNAL_H */
257260

src/wp_aes_block.c

Lines changed: 265 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,21 @@ typedef struct wp_AesBlockCtx {
3939
/** wolfSSL AES object. */
4040
Aes aes;
4141

42+
/** Provider context - needed for wolfCrypt RNG access. */
43+
WOLFPROV_CTX *provCtx;
44+
4245
/** Cipher mode - CBC or ECB. */
4346
int mode;
4447

4548
unsigned int tls_version;
4649

50+
/** Pointer to the MAC extracted from a decrypted TLS record. */
51+
unsigned char *tlsmac;
52+
/** Size of the MAC expected in TLS records. */
53+
size_t tlsmacsize;
54+
/** Whether tlsmac was separately allocated. */
55+
int tlsmacAlloced;
56+
4757
/** Length of key in bytes. */
4858
size_t keyLen;
4959
/** Length of IV in bytes */
@@ -79,6 +89,9 @@ static int wp_aes_block_set_ctx_params(wp_AesBlockCtx *ctx,
7989
*/
8090
static void wp_aes_block_freectx(wp_AesBlockCtx *ctx)
8191
{
92+
if (ctx->tlsmacAlloced) {
93+
OPENSSL_free(ctx->tlsmac);
94+
}
8295
wc_AesFree(&ctx->aes);
8396
OPENSSL_clear_free(ctx, sizeof(*ctx));
8497
}
@@ -100,6 +113,20 @@ static void *wp_aes_block_dupctx(wp_AesBlockCtx *src)
100113
if (dst != NULL) {
101114
/* TODO: copying Aes may not work if it has pointers in it. */
102115
XMEMCPY(dst, src, sizeof(*src));
116+
/* Deep-copy tlsmac to avoid double-free between src and dst. */
117+
if (src->tlsmacAlloced && src->tlsmac != NULL) {
118+
dst->tlsmac = OPENSSL_malloc(src->tlsmacsize);
119+
if (dst->tlsmac == NULL) {
120+
OPENSSL_free(dst);
121+
return NULL;
122+
}
123+
XMEMCPY(dst->tlsmac, src->tlsmac, src->tlsmacsize);
124+
dst->tlsmacAlloced = 1;
125+
}
126+
else {
127+
dst->tlsmac = NULL;
128+
dst->tlsmacAlloced = 0;
129+
}
103130
}
104131

105132
return dst;
@@ -207,6 +234,8 @@ static const OSSL_PARAM* wp_cipher_gettable_ctx_params(wp_AesBlockCtx* ctx,
207234
OSSL_PARAM_uint(OSSL_CIPHER_PARAM_NUM, NULL),
208235
OSSL_PARAM_octet_string(OSSL_CIPHER_PARAM_IV, NULL, 0),
209236
OSSL_PARAM_octet_string(OSSL_CIPHER_PARAM_UPDATED_IV, NULL, 0),
237+
{ OSSL_CIPHER_PARAM_TLS_MAC, OSSL_PARAM_OCTET_PTR, NULL, 0,
238+
OSSL_PARAM_UNMODIFIED },
210239
OSSL_PARAM_END
211240
};
212241
(void)ctx;
@@ -232,6 +261,7 @@ static const OSSL_PARAM* wp_cipher_settable_ctx_params(wp_AesBlockCtx* ctx,
232261
OSSL_PARAM_uint(OSSL_CIPHER_PARAM_NUM, NULL),
233262
OSSL_PARAM_uint(OSSL_CIPHER_PARAM_USE_BITS, NULL),
234263
OSSL_PARAM_uint(OSSL_CIPHER_PARAM_TLS_VERSION, NULL),
264+
OSSL_PARAM_size_t(OSSL_CIPHER_PARAM_TLS_MAC_SIZE, NULL),
235265
OSSL_PARAM_END
236266
};
237267
(void)ctx;
@@ -445,6 +475,223 @@ static int wp_aes_block_doit(wp_AesBlockCtx *ctx, unsigned char *out,
445475
return rc == 0;
446476
}
447477

478+
/**
479+
* TLS 1.2 CBC decryption record post-processing.
480+
*
481+
* Performs constant-time padding validation and MAC extraction after
482+
* CBC decryption. For ETM (Encrypt-then-MAC) or no-MAC modes, strips the
483+
* explicit IV and validates/removes padding. For MtE (MAC-then-Encrypt),
484+
* also extracts the MAC using a constant-time rotation pattern.
485+
*
486+
* @param [in] ctx AES block context object.
487+
* @param [in] out Decrypted output buffer.
488+
* @param [in] oLen Length of decrypted data in bytes.
489+
* @param [in, out] outLen Updated with length after padding/MAC removal.
490+
* @return 1 on success.
491+
* @return 0 on failure.
492+
*/
493+
static int wp_aes_block_tls_dec_record(wp_AesBlockCtx *ctx,
494+
unsigned char *out, size_t oLen, size_t *outLen)
495+
{
496+
int ok = 1;
497+
498+
WOLFPROV_ENTER(WP_LOG_COMP_AES, "wp_aes_block_tls_dec_record");
499+
500+
/*
501+
* TLS 1.2 CBC padding removal and MAC extraction.
502+
* Buffer: [explicit_IV(BS)][payload][MAC(macsize)][padding(pad+1)]
503+
*
504+
* Constant-time padding validation based on OpenSSL's
505+
* tls1_cbc_remove_padding_and_mac() (ssl/record/methods/tls_pad.c)
506+
*
507+
* Constant-time MAC extraction based on OpenSSL's
508+
* ssl3_cbc_copy_mac() rotation pattern. On bad padding the MAC
509+
* is replaced with random bytes via ct select.
510+
*/
511+
unsigned char *rec;
512+
size_t recLen;
513+
size_t origRecLen;
514+
unsigned char padVal;
515+
size_t overhead;
516+
size_t toCheck;
517+
size_t good;
518+
size_t i, j;
519+
size_t macSize = ctx->tlsmacsize;
520+
521+
/* Free any previously allocated MAC */
522+
if (ctx->tlsmacAlloced) {
523+
OPENSSL_free(ctx->tlsmac);
524+
ctx->tlsmacAlloced = 0;
525+
ctx->tlsmac = NULL;
526+
}
527+
528+
if (macSize > EVP_MAX_MD_SIZE ||
529+
oLen < AES_BLOCK_SIZE + macSize + 1) {
530+
ok = 0;
531+
}
532+
533+
if (ok && macSize == 0) {
534+
/* ETM (Encrypt-then-MAC) or no MAC: the record layer already
535+
* handled the MAC. We only need to strip the explicit IV and
536+
* validate+remove padding (same as OpenSSL ssl3_cbc_copy_mac
537+
* returning early when mac_size == 0). */
538+
unsigned char *ivRec = out + AES_BLOCK_SIZE;
539+
size_t ivRecLen = oLen - AES_BLOCK_SIZE;
540+
unsigned char padV = ivRec[ivRecLen - 1];
541+
size_t gd = (size_t)0 - ((size_t)(
542+
wp_ct_int_mask_gte((int)ivRecLen, (int)padV + 1) & 1));
543+
size_t tc = 256;
544+
if (tc > ivRecLen)
545+
tc = ivRecLen;
546+
547+
for (i = 0; i < tc; i++) {
548+
byte m = wp_ct_int_mask_gte((int)padV, (int)i);
549+
unsigned char bv = ivRec[ivRecLen - 1 - i];
550+
gd &= ~((size_t)(m & (padV ^ bv)));
551+
}
552+
{
553+
size_t d = (gd & 0xff) ^ 0xff;
554+
d |= (0 - d);
555+
d >>= (sizeof(size_t) * 8 - 1);
556+
gd = d - 1;
557+
}
558+
ivRecLen -= gd & ((size_t)padV + 1);
559+
*outLen = ivRecLen;
560+
/* No MAC to extract */
561+
ctx->tlsmac = NULL;
562+
ctx->tlsmacAlloced = 0;
563+
/* With ETM/no-MAC, bad padding is a real error (the MAC was
564+
* already verified by the record layer, so there is no padding
565+
* oracle concern). Matches OpenSSL ssl3_cbc_copy_mac returning
566+
* 0 when good==0 and mac_size==0. */
567+
if (gd == 0)
568+
ok = 0;
569+
}
570+
else if (ok) {
571+
/* 64-byte aligned buffer for cache-line-aware MAC rotation */
572+
unsigned char rotatedMacBuf[64 + EVP_MAX_MD_SIZE];
573+
unsigned char *rotatedMac;
574+
unsigned char randMac[EVP_MAX_MD_SIZE];
575+
size_t macEnd;
576+
size_t macStart;
577+
size_t scanStart = 0;
578+
byte inMac;
579+
size_t rotateOff;
580+
581+
/* Align rotatedMac to 64-byte boundary so the entire MAC
582+
* buffer (up to EVP_MAX_MD_SIZE=64) sits within one or two
583+
* 32-byte cache lines at known positions. */
584+
rotatedMac = rotatedMacBuf +
585+
((0 - (size_t)rotatedMacBuf) & 63);
586+
587+
/* For TLS 1.1+/DTLS: skip explicit IV */
588+
rec = out + AES_BLOCK_SIZE;
589+
recLen = oLen - AES_BLOCK_SIZE;
590+
origRecLen = recLen;
591+
592+
padVal = rec[recLen - 1];
593+
overhead = macSize + (size_t)padVal + 1;
594+
595+
/* CT overhead check: recLen >= overhead.
596+
* No branch on padVal — fold into good mask instead. */
597+
good = (size_t)0 -
598+
((size_t)(wp_ct_int_mask_gte((int)recLen, (int)overhead) & 1));
599+
600+
/* Validate padding bytes in constant time.
601+
* Check up to 256 bytes (max TLS padding). */
602+
toCheck = 256;
603+
if (toCheck > recLen)
604+
toCheck = recLen;
605+
606+
for (i = 0; i < toCheck; i++) {
607+
byte mask = wp_ct_int_mask_gte((int)padVal, (int)i);
608+
unsigned char b = rec[recLen - 1 - i];
609+
good &= ~((size_t)(mask & (padVal ^ b)));
610+
}
611+
{
612+
/* Collapse lower 8 bits to full-width size_t mask.
613+
* Same technique as OpenSSL constant_time_eq_s. */
614+
size_t diff = (good & 0xff) ^ 0xff;
615+
diff |= (0 - diff);
616+
diff >>= (sizeof(size_t) * 8 - 1);
617+
good = diff - 1;
618+
}
619+
620+
/* Remove padding (only if valid) */
621+
recLen -= good & ((size_t)padVal + 1);
622+
623+
macEnd = recLen;
624+
macStart = macEnd - macSize;
625+
626+
recLen -= macSize;
627+
*outLen = recLen;
628+
629+
/* Generate random MAC to use if padding was bad */
630+
#ifndef WP_SINGLE_THREADED
631+
wp_provctx_lock_rng(ctx->provCtx);
632+
#endif
633+
if (wc_RNG_GenerateBlock(wp_provctx_get_rng(ctx->provCtx),
634+
randMac, (word32)macSize) != 0) {
635+
ok = 0;
636+
}
637+
#ifndef WP_SINGLE_THREADED
638+
wp_provctx_unlock_rng(ctx->provCtx);
639+
#endif
640+
641+
ctx->tlsmac = OPENSSL_malloc(macSize);
642+
if (ctx->tlsmac == NULL) {
643+
ok = 0;
644+
}
645+
else {
646+
ctx->tlsmacAlloced = 1;
647+
648+
/* Constant-time MAC extraction: scan all bytes that
649+
* could contain the MAC (position varies by up to 255). */
650+
if (origRecLen > macSize + 255 + 1)
651+
scanStart = origRecLen - (macSize + 255 + 1);
652+
653+
XMEMSET(rotatedMac, 0, EVP_MAX_MD_SIZE);
654+
inMac = 0;
655+
rotateOff = 0;
656+
for (i = scanStart, j = 0; i < origRecLen; i++) {
657+
byte started = wp_ct_int_mask_eq((int)i, (int)macStart);
658+
byte ended = wp_ct_int_mask_lt((int)i, (int)macEnd);
659+
unsigned char b = rec[i];
660+
661+
inMac |= started;
662+
inMac &= ended;
663+
rotateOff |= j & (size_t)started;
664+
rotatedMac[j++] |= b & inMac;
665+
j &= (size_t)wp_ct_int_mask_lt((int)j, (int)macSize);
666+
}
667+
668+
/* Cache-line-aware un-rotation: always load from both
669+
* 32-byte halves and ct-select to avoid leaking
670+
* rotateOff through cache access patterns. Same
671+
* technique as OpenSSL's CBC_MAC_ROTATE_IN_PLACE. */
672+
for (i = 0; i < macSize; i++) {
673+
char aux1 = rotatedMac[rotateOff & ~32];
674+
char aux2 = rotatedMac[rotateOff | 32];
675+
byte eqMask = wp_ct_int_mask_eq(
676+
(int)(rotateOff & ~32), (int)rotateOff);
677+
unsigned char real = wp_ct_byte_mask_sel(
678+
eqMask, (byte)aux1, (byte)aux2);
679+
byte goodMask = (byte)(good & 0xff);
680+
681+
ctx->tlsmac[i] = wp_ct_byte_mask_sel(goodMask, real,
682+
randMac[i]);
683+
rotateOff++;
684+
rotateOff &= (size_t)wp_ct_int_mask_lt(
685+
(int)rotateOff, (int)macSize);
686+
}
687+
}
688+
}
689+
690+
WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);
691+
692+
return ok;
693+
}
694+
448695
/**
449696
* Update encryption/decryption with more data.
450697
*
@@ -528,18 +775,7 @@ static int wp_aes_block_update(wp_AesBlockCtx *ctx, unsigned char *out,
528775
*outLen = oLen;
529776
}
530777
if (ok && (ctx->tls_version > 0) && (!ctx->enc)) {
531-
unsigned char pad = out[oLen-1];
532-
int padStart = AES_BLOCK_SIZE - pad - 1;
533-
unsigned char invalid = (pad < AES_BLOCK_SIZE) - 1;
534-
int i;
535-
536-
for (i = AES_BLOCK_SIZE - 1; i >= 0; i--) {
537-
byte check = wp_ct_int_mask_gte(i, padStart);
538-
check &= wp_ct_byte_mask_ne(out[oLen - AES_BLOCK_SIZE + i], pad);
539-
invalid |= check;
540-
}
541-
*outLen = oLen - pad - 1 - AES_BLOCK_SIZE;
542-
ok = invalid == 0;
778+
ok = wp_aes_block_tls_dec_record(ctx, out, oLen, outLen);
543779
}
544780

545781
WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);
@@ -790,6 +1026,13 @@ static int wp_aes_block_get_ctx_params(wp_AesBlockCtx* ctx, OSSL_PARAM params[])
7901026
ok = 0;
7911027
}
7921028
}
1029+
if (ok) {
1030+
p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_TLS_MAC);
1031+
if ((p != NULL) &&
1032+
(!OSSL_PARAM_set_octet_ptr(p, ctx->tlsmac, ctx->tlsmacsize))) {
1033+
ok = 0;
1034+
}
1035+
}
7931036

7941037
WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);
7951038
return ok;
@@ -838,6 +1081,15 @@ static int wp_aes_block_set_ctx_params(wp_AesBlockCtx *ctx,
8381081
&ctx->tls_version, NULL))) {
8391082
ok = 0;
8401083
}
1084+
if (ok) {
1085+
const OSSL_PARAM *pmac = OSSL_PARAM_locate_const(params,
1086+
OSSL_CIPHER_PARAM_TLS_MAC_SIZE);
1087+
if (pmac != NULL) {
1088+
if (!OSSL_PARAM_get_size_t(pmac, &ctx->tlsmacsize)) {
1089+
ok = 0;
1090+
}
1091+
}
1092+
}
8411093
}
8421094

8431095
WOLFPROV_LEAVE(WP_LOG_COMP_AES, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);
@@ -891,11 +1143,11 @@ static wp_AesBlockCtx* wp_aes_block_##kBits##_##lcmode##_newctx( \
8911143
WOLFPROV_CTX *provCtx) \
8921144
{ \
8931145
wp_AesBlockCtx *ctx = NULL; \
894-
(void)provCtx; \
8951146
if (wolfssl_prov_is_running()) { \
8961147
ctx = OPENSSL_zalloc(sizeof(*ctx)); \
8971148
} \
8981149
if (ctx != NULL) { \
1150+
ctx->provCtx = provCtx; \
8991151
wp_aes_block_init_ctx(ctx, kBits, ivBits, EVP_CIPH_##UCMODE##_MODE); \
9001152
} \
9011153
return ctx; \

0 commit comments

Comments
 (0)