@@ -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 */
8090static 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