From 7a61b2e087822be219fbbc72f6fc1ca39ef78eb1 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 7 Apr 2026 14:56:59 +0200 Subject: [PATCH 01/11] Refactor record padding handling to eliminate middle padding pattern Move padSz index advancement from individual message handlers to a single location at the end of record processing in ProcessReply. Previously, each handler (DoFinished, DoApplicationData, DoAlert, DoCertificate, DoServerHello, etc.) advanced the index by padSz, which then had to be corrected when processing multiple messages in one record. This "middle padding" pattern was error-prone. Now curSize is reduced by padSz after decryption/verification so it reflects content size only. Message handlers advance the index by content size only. padSz is added once when the record is fully processed. The correction code for multi-message records is removed. --- src/internal.c | 205 ++++++++++++++----------------------------------- src/tls13.c | 28 ++----- 2 files changed, 62 insertions(+), 171 deletions(-) diff --git a/src/internal.c b/src/internal.c index f19d8fa10d..c7656e0baa 100644 --- a/src/internal.c +++ b/src/internal.c @@ -11905,10 +11905,7 @@ int MsgCheckEncryption(WOLFSSL* ssl, byte type, byte encrypted) static WC_INLINE int isLastMsg(const WOLFSSL* ssl, word32 msgSz) { - word32 extra = 0; - if (IsEncryptionOn(ssl, 0)) - extra = ssl->keys.padSz; - return (ssl->buffers.inputBuffer.idx - ssl->curStartIdx) + msgSz + extra + return (ssl->buffers.inputBuffer.idx - ssl->curStartIdx) + msgSz == ssl->curSize; } @@ -17698,9 +17695,6 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, ssl->options.serverState = SERVER_CERT_COMPLETE; } - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - /* Advance state and proceed */ ssl->options.asyncState = TLS_ASYNC_END; } /* case TLS_ASYNC_FINALIZE */ @@ -17964,12 +17958,6 @@ static int DoCertificateStatus(WOLFSSL* ssl, byte* input, word32* inOutIdx, SendAlert(ssl, alert_fatal, bad_certificate_status_response); } - if (IsEncryptionOn(ssl, 0)) { - if (*inOutIdx + ssl->keys.padSz > size) - return BUFFER_E; - *inOutIdx += ssl->keys.padSz; - } - WOLFSSL_LEAVE("DoCertificateStatus", ret); WOLFSSL_END(WC_FUNC_CERTIFICATE_STATUS_DO); @@ -17984,10 +17972,9 @@ static int DoCertificateStatus(WOLFSSL* ssl, byte* input, word32* inOutIdx, #if !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) -static int DoHelloRequest(WOLFSSL* ssl, const byte* input, word32* inOutIdx, - word32 size, word32 totalSz) +static int DoHelloRequest(WOLFSSL* ssl, word32 size) { - (void)input; + (void)size; WOLFSSL_START(WC_FUNC_HELLO_REQUEST_DO); WOLFSSL_ENTER("DoHelloRequest"); @@ -17995,17 +17982,6 @@ static int DoHelloRequest(WOLFSSL* ssl, const byte* input, word32* inOutIdx, if (size) /* must be 0 */ return BUFFER_ERROR; - if (IsEncryptionOn(ssl, 0)) { - /* If size == totalSz then we are in DtlsMsgDrain so no need to worry - * about padding */ - /* access beyond input + size should be checked against totalSz */ - if (size != totalSz && - *inOutIdx + ssl->keys.padSz > totalSz) - return BUFFER_E; - - *inOutIdx += ssl->keys.padSz; - } - if (ssl->options.side == WOLFSSL_SERVER_END) { SendAlert(ssl, alert_fatal, unexpected_message); /* try */ WOLFSSL_ERROR_VERBOSE(FATAL_ERROR); @@ -18040,7 +18016,7 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, * If size == totalSz then we are in DtlsMsgDrain so no need to worry about * padding */ if (size != totalSz) { - if (*inOutIdx + size + ssl->keys.padSz > totalSz) + if (*inOutIdx + size > totalSz) return BUFFER_E; } @@ -18083,8 +18059,7 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, } #endif - /* force input exhaustion at ProcessReply consuming padSz */ - *inOutIdx += size + ssl->keys.padSz; + *inOutIdx += size; if (ssl->options.side == WOLFSSL_CLIENT_END) { ssl->options.serverState = SERVER_FINISHED_COMPLETE; @@ -18737,8 +18712,7 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, return INCOMPLETE_DATA; } - expectedIdx = *inOutIdx + size + - (ssl->keys.encryptionOn ? ssl->keys.padSz : 0); + expectedIdx = *inOutIdx + size; #if !defined(NO_WOLFSSL_SERVER) && \ defined(HAVE_SECURE_RENEGOTIATION) && \ @@ -18903,21 +18877,13 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, case hello_request: WOLFSSL_MSG("processing hello request"); - ret = DoHelloRequest(ssl, input, inOutIdx, size, totalSz); + ret = DoHelloRequest(ssl, size); break; #ifndef NO_WOLFSSL_CLIENT case hello_verify_request: WOLFSSL_MSG("processing hello verify request"); ret = DoHelloVerifyRequest(ssl, input,inOutIdx, size); - if (IsEncryptionOn(ssl, 0)) { - /* access beyond input + size should be checked against totalSz - */ - if (*inOutIdx + ssl->keys.padSz > totalSz) - return BUFFER_E; - - *inOutIdx += ssl->keys.padSz; - } break; case server_hello: @@ -18989,8 +18955,6 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, AddLateName("ServerHelloDone", &ssl->timeoutInfo); #endif ssl->options.serverState = SERVER_HELLODONE_COMPLETE; - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; break; case finished: @@ -19022,16 +18986,6 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, } } #endif - /* If size == totalSz then we are in DtlsMsgDrain so no need to worry - * about padding */ - if (IsEncryptionOn(ssl, 0)) { - /* access beyond input + size should be checked against totalSz - */ - if (size != totalSz && - *inOutIdx + ssl->keys.padSz > totalSz) - return BUFFER_E; - *inOutIdx += ssl->keys.padSz; - } break; case client_key_exchange: @@ -19133,6 +19087,8 @@ static int DoHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, } inputLength = ssl->buffers.inputBuffer.length - *inOutIdx; + if (IsEncryptionOn(ssl, 0)) + inputLength -= ssl->keys.padSz; /* If there is a pending fragmented handshake message, * pending message size will be non-zero. */ @@ -19893,11 +19849,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, input + *inOutIdx, size, type, fragOffset, fragSz, ssl->heap); *inOutIdx += fragSz; - if (*inOutIdx + ssl->keys.padSz > totalSz) { - WOLFSSL_ERROR(BUFFER_E); - return BUFFER_E; - } - *inOutIdx += ssl->keys.padSz; ret = 0; #ifndef WOLFSSL_DTLS_RESEND_ONLY_TIMEOUT /* If we receive an out of order last flight msg then retransmit */ @@ -19936,10 +19887,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* Already saw this message and processed it. It can be ignored. */ WOLFSSL_MSG("Already saw this message and processed it"); *inOutIdx += fragSz; - if (*inOutIdx + ssl->keys.padSz > totalSz) { - WOLFSSL_ERROR(BUFFER_E); - return BUFFER_E; - } #ifndef WOLFSSL_DTLS_RESEND_ONLY_TIMEOUT if (IsDtlsNotSctpMode(ssl) && VerifyForDtlsMsgPoolSend(ssl, type, fragOffset)) { @@ -19947,7 +19894,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, ret = DtlsMsgPoolSend(ssl, 0); } #endif - *inOutIdx += ssl->keys.padSz; } else if (fragSz < size) { /* Since this branch is in order, but fragmented, dtls_rx_msg_list will @@ -19971,11 +19917,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, input + *inOutIdx, size, type, fragOffset, fragSz, ssl->heap); *inOutIdx += fragSz; - if (*inOutIdx + ssl->keys.padSz > totalSz) { - WOLFSSL_ERROR(BUFFER_E); - return BUFFER_E; - } - *inOutIdx += ssl->keys.padSz; ret = 0; if (ssl->dtls_rx_msg_list != NULL && ssl->dtls_rx_msg_list->ready) ret = DtlsMsgDrain(ssl); @@ -19992,9 +19933,9 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, WOLFSSL_ERROR(BUFFER_ERROR); return BUFFER_ERROR; } - if (idx + fragSz + ssl->keys.padSz > totalSz) + if (idx + fragSz > totalSz) return BUFFER_E; - *inOutIdx = idx + fragSz + ssl->keys.padSz; + *inOutIdx = idx + fragSz; /* In async mode always store the message and process it with * DtlsMsgDrain because in case of a WC_PENDING_E it will be * easier this way. */ @@ -22147,7 +22088,7 @@ int DoApplicationData(WOLFSSL* ssl, byte* input, word32* inOutIdx, int sniff) } #endif - dataSz = (int)(msgSz - ssl->keys.padSz); + dataSz = (int)msgSz; if (dataSz < 0) { WOLFSSL_MSG("App data buffer error, malicious input?"); if (sniff == NO_SNIFF) { @@ -22202,8 +22143,6 @@ int DoApplicationData(WOLFSSL* ssl, byte* input, word32* inOutIdx, int sniff) ssl->buffers.clearOutputBuffer.length = (unsigned int)dataSz; } - idx += ssl->keys.padSz; - #ifdef HAVE_LIBZ /* decompress could be bigger, overwrite after verify */ if (ssl->options.usingCompression) @@ -22494,9 +22433,6 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type) } #endif - if (IsEncryptionOn(ssl, 0)) - dataSz -= ssl->keys.padSz; - /* make sure can read the message */ if (dataSz != ALERT_SIZE) { #ifdef WOLFSSL_EXTRA_ALERTS @@ -22565,9 +22501,6 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type) WOLFSSL_ERROR(*type); } } - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - return level; } @@ -23575,6 +23508,11 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) } + /* Reduce curSize to content only, excluding padding/MAC overhead. + * padSz is accounted for once at the end of record processing. */ + if (IsEncryptionOn(ssl, 0)) + ssl->curSize -= (word16)ssl->keys.padSz; + /* in case > 1 msg per record */ ssl->curStartIdx = ssl->buffers.inputBuffer.idx; @@ -23632,7 +23570,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) } #if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY) if (IsEncryptionOn(ssl, 0) && ssl->options.startedETMRead) { - if ((ssl->curSize - ssl->keys.padSz > MAX_PLAINTEXT_SZ) + if ((ssl->curSize > MAX_PLAINTEXT_SZ) #ifdef WOLFSSL_ASYNC_CRYPT && ssl->buffers.inputBuffer.length != ssl->buffers.inputBuffer.idx @@ -23650,7 +23588,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) #endif /* TLS13 plaintext limit is checked earlier before decryption */ if (!IsAtLeastTLSv1_3(ssl->version) - && ssl->curSize - ssl->keys.padSz > MAX_PLAINTEXT_SZ + && ssl->curSize > MAX_PLAINTEXT_SZ #ifdef WOLFSSL_ASYNC_CRYPT && ssl->buffers.inputBuffer.length != ssl->buffers.inputBuffer.idx @@ -23855,11 +23793,6 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) return LENGTH_ERROR; } - if (IsEncryptionOn(ssl, 0) && ssl->options.handShakeDone) { - ssl->buffers.inputBuffer.idx += ssl->keys.padSz; - ssl->curSize -= (word16)ssl->keys.padSz; - } - if (ssl->curSize != 1) { WOLFSSL_MSG("Malicious or corrupted ChangeCipher msg"); WOLFSSL_ERROR_VERBOSE(LENGTH_ERROR); @@ -24027,7 +23960,6 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ssl->buffers.inputBuffer.idx - ssl->keys.padSz, &processedSize); ssl->buffers.inputBuffer.idx += processedSize; - ssl->buffers.inputBuffer.idx += ssl->keys.padSz; if (ret != 0) return ret; break; @@ -24041,53 +23973,44 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ssl->options.processReply = doProcessInit; - /* input exhausted */ - if (ssl->buffers.inputBuffer.idx >= ssl->buffers.inputBuffer.length -#ifdef WOLFSSL_DTLS - || (ssl->options.dtls && - /* If app data was processed then return now to avoid - * dropping any app data. */ - (ssl->curRL.type == application_data || - /* client: if we processed a finished message, return to - * allow higher layers to establish the crypto - * parameters of the connection. The remaining data - * may be app data that we would drop without the - * crypto setup. */ - (ssl->options.side == WOLFSSL_CLIENT_END && - ssl->options.serverState == SERVER_FINISHED_COMPLETE && - ssl->options.handShakeState != HANDSHAKE_DONE))) -#endif - ) { - /* Shrink input buffer when we successfully finish record - * processing */ - if ((ret == 0) && ssl->buffers.inputBuffer.dynamicFlag) - ShrinkInputBuffer(ssl, NO_FORCED_FREE); - return ret; - } /* more messages per record */ - else if ((ssl->buffers.inputBuffer.idx - ssl->curStartIdx) + if ((ssl->buffers.inputBuffer.idx - ssl->curStartIdx) < ssl->curSize) { WOLFSSL_MSG("More messages in record"); ssl->options.processReply = runProcessingOneMessage; - - if (IsEncryptionOn(ssl, 0)) { - /* With encryption on, we advance the index by the value - * of ssl->keys.padSz. Since padding only appears once, we - * only can do this at the end of record parsing. We have to - * reset the index to the start of the next message here. */ - if (ssl->buffers.inputBuffer.idx >= ssl->keys.padSz) { - ssl->buffers.inputBuffer.idx -= ssl->keys.padSz; - } - else { - WOLFSSL_MSG("\tBuffer advanced not enough error"); - WOLFSSL_ERROR_VERBOSE(FATAL_ERROR); - return FATAL_ERROR; - } - } } - /* more records */ else { + /* Done with this record. Advance past padding/MAC. */ + if (IsEncryptionOn(ssl, 0)) + ssl->buffers.inputBuffer.idx += ssl->keys.padSz; + + /* input exhausted */ + if (ssl->buffers.inputBuffer.idx >= + ssl->buffers.inputBuffer.length +#ifdef WOLFSSL_DTLS + || (ssl->options.dtls && + /* If app data was processed then return now to avoid + * dropping any app data. */ + (ssl->curRL.type == application_data || + /* client: if we processed a finished message, return + * to allow higher layers to establish the + * crypto parameters of the connection. The + * remaining data may be app data that we would + * drop without the crypto setup. */ + (ssl->options.side == WOLFSSL_CLIENT_END && + ssl->options.serverState == + SERVER_FINISHED_COMPLETE && + ssl->options.handShakeState != HANDSHAKE_DONE))) +#endif + ) { + /* Shrink input buffer when we successfully finish record + * processing */ + if ((ret == 0) && ssl->buffers.inputBuffer.dynamicFlag) + ShrinkInputBuffer(ssl, NO_FORCED_FREE); + return ret; + } + /* more records */ WOLFSSL_MSG("More records in input"); } #ifdef WOLFSSL_ASYNC_CRYPT @@ -24102,10 +24025,14 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) if (ret == WC_NO_ERR_TRACE(APP_DATA_READY)) return ret; #endif - /* It is safe to shrink the input buffer here now. local vars will - * be reset to the new starting value. */ - if (ret == 0 && ssl->buffers.inputBuffer.dynamicFlag) + /* It is safe to shrink the input buffer here now, but only + * when done with the current record. During multi-message + * record processing, shrinking would invalidate curStartIdx + * and curSize. */ + if (ssl->options.processReply != runProcessingOneMessage + && ret == 0 && ssl->buffers.inputBuffer.dynamicFlag) { ShrinkInputBuffer(ssl, NO_FORCED_FREE); + } continue; default: WOLFSSL_MSG("Bad process input state, programming error"); @@ -32163,9 +32090,6 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) ssl->options.serverState = SERVER_HELLO_COMPLETE; - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - #ifdef HAVE_SECRET_CALLBACK if (ssl->sessionSecretCb != NULL #ifdef HAVE_SESSION_TICKET @@ -32497,9 +32421,6 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) ssl->options.sendVerify = SEND_BLANK_CERT; } - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - WOLFSSL_LEAVE("DoCertificateRequest", 0); WOLFSSL_END(WC_FUNC_CERTIFICATE_REQUEST_DO); @@ -33739,9 +33660,6 @@ static int DoServerKeyExchange(WOLFSSL* ssl, const byte* input, case TLS_ASYNC_FINALIZE: { - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - /* Advance state and proceed */ ssl->options.asyncState = TLS_ASYNC_END; } /* case TLS_ASYNC_FINALIZE */ @@ -35509,9 +35427,6 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #endif } - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - ssl->expect_session_ticket = 0; return 0; @@ -39340,9 +39255,6 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) case TLS_ASYNC_FINALIZE: { - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - ssl->options.havePeerVerify = 1; /* Set final index */ @@ -42404,9 +42316,6 @@ static int DefTicketEncCb(WOLFSSL* ssl, byte key_name[WOLFSSL_TICKET_NAME_SZ], case TLS_ASYNC_FINALIZE: { - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - ret = MakeMasterSecret(ssl); /* Check for error */ diff --git a/src/tls13.c b/src/tls13.c index 1d7e0fd3e1..2b8cdfe189 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6005,9 +6005,6 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input, /* Move index to byte after message. */ *inOutIdx = i + totalExtSz; - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; - #ifdef WOLFSSL_EARLY_DATA if (ssl->earlyData != no_early_data) { TLSX* ext = TLSX_Find(ssl->extensions, TLSX_EARLY_DATA); @@ -6148,9 +6145,6 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, #endif } - /* This message is always encrypted so add encryption padding. */ - *inOutIdx += ssl->keys.padSz; - #ifdef WOLFSSL_POST_HANDSHAKE_AUTH { /* CertReqCtx has one byte at end for context value. @@ -11619,9 +11613,6 @@ static int DoTls13CertificateVerify(WOLFSSL* ssl, byte* input, args->idx += args->sz; *inOutIdx = args->idx; - /* Encryption is always on: add padding */ - *inOutIdx += ssl->keys.padSz; - /* Advance state and proceed */ ssl->options.asyncState = TLS_ASYNC_END; @@ -11819,8 +11810,7 @@ int DoTls13Finished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, } } - /* Force input exhaustion at ProcessReply by consuming padSz. */ - *inOutIdx += size + ssl->keys.padSz; + *inOutIdx += size; #ifndef NO_WOLFSSL_SERVER if (ssl->options.side == WOLFSSL_SERVER_END && @@ -12273,8 +12263,6 @@ static int DoTls13KeyUpdate(WOLFSSL* ssl, const byte* input, word32* inOutIdx, /* Move index to byte after message. */ *inOutIdx += totalSz; - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; /* Future traffic uses new decryption keys. */ if ((ret = DeriveTls13Keys(ssl, update_traffic_key, DECRYPT_SIDE_ONLY, 1)) @@ -12425,9 +12413,6 @@ static int DoTls13EndOfEarlyData(WOLFSSL* ssl, const byte* input, ssl->earlyData = done_early_data; - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; - ret = SetKeysSide(ssl, DECRYPT_SIDE_ONLY); WOLFSSL_LEAVE("DoTls13EndOfEarlyData", ret); @@ -12599,9 +12584,6 @@ static int DoTls13NewSessionTicket(WOLFSSL* ssl, const byte* input, AddSession(ssl); #endif - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; - ssl->expect_session_ticket = 0; #else (void)ssl; @@ -12609,7 +12591,7 @@ static int DoTls13NewSessionTicket(WOLFSSL* ssl, const byte* input, WOLFSSL_ENTER("DoTls13NewSessionTicket"); - *inOutIdx += size + ssl->keys.padSz; + *inOutIdx += size; #endif /* HAVE_SESSION_TICKET */ WOLFSSL_LEAVE("DoTls13NewSessionTicket", 0); @@ -13994,7 +13976,7 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, input + *inOutIdx - HANDSHAKE_HEADER_SZ, inputLength); ssl->arrays->pendingMsgOffset = inputLength; - *inOutIdx += inputLength + ssl->keys.padSz - HANDSHAKE_HEADER_SZ; + *inOutIdx += inputLength - HANDSHAKE_HEADER_SZ; return 0; } @@ -14018,7 +14000,7 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, XMEMCPY(ssl->arrays->pendingMsg + ssl->arrays->pendingMsgOffset, input + *inOutIdx, inputLength); ssl->arrays->pendingMsgOffset += inputLength; - *inOutIdx += inputLength + ssl->keys.padSz; + *inOutIdx += inputLength; if (ssl->arrays->pendingMsgOffset == ssl->arrays->pendingMsgSz) { @@ -14033,7 +14015,7 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, ret == WC_NO_ERR_TRACE(OCSP_WANT_READ)) { /* setup to process fragment again */ ssl->arrays->pendingMsgOffset -= inputLength; - *inOutIdx -= inputLength + ssl->keys.padSz; + *inOutIdx -= inputLength; } else #endif From 7fe8f4d5c695d28b4f05918bea5e66376e2d4545 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 7 Apr 2026 16:29:48 +0200 Subject: [PATCH 02/11] Fix CI --- src/dtls13.c | 6 +++--- src/internal.c | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/dtls13.c b/src/dtls13.c index baec84569e..893f6813e7 100644 --- a/src/dtls13.c +++ b/src/dtls13.c @@ -1904,7 +1904,7 @@ static int _Dtls13HandshakeRecv(WOLFSSL* ssl, byte* input, word32 size, #endif /* WOLFSSL_DEBUG_TLS */ /* ignore the message */ - *processedSize = idx + fragLength + ssl->keys.padSz; + *processedSize = idx + fragLength; return 0; } @@ -1938,7 +1938,7 @@ static int _Dtls13HandshakeRecv(WOLFSSL* ssl, byte* input, word32 size, WOLFSSL_MSG("DTLS1.3 not accepting fragmented plaintext message"); #endif /* WOLFSSL_DEBUG_TLS */ /* ignore the message */ - *processedSize = idx + fragLength + ssl->keys.padSz; + *processedSize = idx + fragLength; return 0; } } @@ -1966,7 +1966,7 @@ static int _Dtls13HandshakeRecv(WOLFSSL* ssl, byte* input, word32 size, return DTLS_TOO_MANY_FRAGMENTS_E; } - *processedSize = idx + fragLength + ssl->keys.padSz; + *processedSize = idx + fragLength; if (Dtls13NextMessageComplete(ssl)) return Dtls13ProcessBufferedMessages(ssl); diff --git a/src/internal.c b/src/internal.c index c7656e0baa..49b72ba496 100644 --- a/src/internal.c +++ b/src/internal.c @@ -23530,7 +23530,8 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) WOLFSSL_MSG("Dropping DTLS record outside receiving " "window"); ssl->options.processReply = doProcessInit; - ssl->buffers.inputBuffer.idx += ssl->curSize; + ssl->buffers.inputBuffer.idx += ssl->curSize + + ssl->keys.padSz; if (ssl->buffers.inputBuffer.idx > ssl->buffers.inputBuffer.length) return BUFFER_E; @@ -23645,8 +23646,12 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) exit */ ssl->earlyData = no_early_data; ssl->options.processReply = doProcessInit; - if (ssl->options.clientInEarlyData) + if (ssl->options.clientInEarlyData) { + if (IsEncryptionOn(ssl, 0)) + ssl->buffers.inputBuffer.idx += + ssl->keys.padSz; return APP_DATA_READY; + } } #endif /* WOLFSSL_EARLY_DATA */ if (ret == 0 || @@ -23692,8 +23697,12 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ssl->options.handShakeState == HANDSHAKE_DONE) { ssl->earlyData = no_early_data; ssl->options.processReply = doProcessInit; - if (ssl->options.clientInEarlyData) + if (ssl->options.clientInEarlyData) { + if (IsEncryptionOn(ssl, 0)) + ssl->buffers.inputBuffer.idx += + ssl->keys.padSz; return APP_DATA_READY; + } } #endif #else From 38bd87591f25a0f70d6790b7c566602c1041bb0e Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 8 Apr 2026 09:52:13 +0000 Subject: [PATCH 03/11] Use curSize for content-only input length in handshake/ack handlers Since ProcessReply already reduces curSize by padSz after decryption, use curStartIdx + curSize to bound content data instead of recomputing it from buffer.length - padSz. This removes three more padSz references from message processing code. --- src/internal.c | 12 ++++++------ src/tls13.c | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/internal.c b/src/internal.c index 49b72ba496..fd82be8738 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19086,9 +19086,9 @@ static int DoHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, return DoHandShakeMsgType(ssl, input, inOutIdx, type, size, totalSz); } - inputLength = ssl->buffers.inputBuffer.length - *inOutIdx; - if (IsEncryptionOn(ssl, 0)) - inputLength -= ssl->keys.padSz; + /* curSize has already been reduced to content-only (padSz subtracted) + * in ProcessReply, so curStartIdx + curSize bounds the content. */ + inputLength = ssl->curStartIdx + ssl->curSize - *inOutIdx; /* If there is a pending fragmented handshake message, * pending message size will be non-zero. */ @@ -23965,9 +23965,9 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) word32 processedSize = 0; ret = DoDtls13Ack(ssl, ssl->buffers.inputBuffer.buffer + ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length - - ssl->buffers.inputBuffer.idx - - ssl->keys.padSz, &processedSize); + ssl->curStartIdx + ssl->curSize - + ssl->buffers.inputBuffer.idx, + &processedSize); ssl->buffers.inputBuffer.idx += processedSize; if (ret != 0) return ret; diff --git a/src/tls13.c b/src/tls13.c index 2b8cdfe189..5137ad8133 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13934,7 +13934,9 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, totalSz); } - inputLength = ssl->buffers.inputBuffer.length - *inOutIdx - ssl->keys.padSz; + /* curSize has already been reduced to content-only (padSz subtracted) + * in ProcessReply, so curStartIdx + curSize bounds the content. */ + inputLength = ssl->curStartIdx + ssl->curSize - *inOutIdx; /* If there is a pending fragmented handshake message, * pending message size will be non-zero. */ From b88eb32c1da8d08802276e2fab9323330da2f63c Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 8 Apr 2026 14:07:30 +0000 Subject: [PATCH 04/11] Guard against unsigned underflow in inputLength calculation Add bounds check before computing inputLength from curStartIdx + curSize to prevent unsigned underflow if *inOutIdx ever exceeds the record content boundary. --- src/internal.c | 2 ++ src/tls13.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/internal.c b/src/internal.c index fd82be8738..c009452d62 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19088,6 +19088,8 @@ static int DoHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* curSize has already been reduced to content-only (padSz subtracted) * in ProcessReply, so curStartIdx + curSize bounds the content. */ + if (*inOutIdx > (word32)ssl->curStartIdx + ssl->curSize) + return BUFFER_ERROR; inputLength = ssl->curStartIdx + ssl->curSize - *inOutIdx; /* If there is a pending fragmented handshake message, diff --git a/src/tls13.c b/src/tls13.c index 5137ad8133..a3ad1eacf5 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13936,6 +13936,8 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* curSize has already been reduced to content-only (padSz subtracted) * in ProcessReply, so curStartIdx + curSize bounds the content. */ + if (*inOutIdx > (word32)ssl->curStartIdx + ssl->curSize) + return BUFFER_ERROR; inputLength = ssl->curStartIdx + ssl->curSize - *inOutIdx; /* If there is a pending fragmented handshake message, From 0b1b158fe27ded731955e1ed496869d1a636bc84 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 10 Apr 2026 16:32:35 +0000 Subject: [PATCH 05/11] Add a test for multi-message TLS records --- .gitignore | 1 + scripts/include.am | 13 + scripts/multi-msg-record.py | 611 ++++++++++++++++++++++++++++++++++ scripts/multi-msg-record.test | 37 ++ src/internal.c | 21 +- src/tls13.c | 8 +- 6 files changed, 678 insertions(+), 13 deletions(-) create mode 100755 scripts/multi-msg-record.py create mode 100755 scripts/multi-msg-record.test diff --git a/.gitignore b/.gitignore index 7ea896eb2c..93388d03bf 100644 --- a/.gitignore +++ b/.gitignore @@ -200,6 +200,7 @@ tracefile.txt *.bak *.dummy *.xcworkspace +workspace.pid*/ xcuserdata compile NTRU_algorithm/ diff --git a/scripts/include.am b/scripts/include.am index f7a0bb37c8..38dc071466 100644 --- a/scripts/include.am +++ b/scripts/include.am @@ -106,9 +106,22 @@ endif dist_noinst_SCRIPTS+= scripts/unit.test noinst_SCRIPTS+= scripts/unit.test.in +# multi-msg-record.test drives the wolfSSL example client against a +# Python (tlslite-ng) peer to verify parsing of TLS records that carry +# multiple handshake messages. The script probes the client binary +# at runtime for TLS 1.2, TLS 1.3 and secure-renegotiation support +# and skips phases that are not compiled in. The whole test exits 77 +# (SKIP) if python3 or tlslite-ng is missing or if nothing is +# runnable. +dist_noinst_SCRIPTS+= scripts/multi-msg-record.test + endif endif +# The Python half of multi-msg-record.test always ships in tarballs so +# the wrapper can find it on the installed side. +EXTRA_DIST+= scripts/multi-msg-record.py + dist_noinst_SCRIPTS+= scripts/pem.test EXTRA_DIST += scripts/sniffer-static-rsa.pcap \ diff --git a/scripts/multi-msg-record.py b/scripts/multi-msg-record.py new file mode 100755 index 0000000000..fe19730eae --- /dev/null +++ b/scripts/multi-msg-record.py @@ -0,0 +1,611 @@ +#!/usr/bin/env python3 +# +# multi-msg-record.py +# +# Python half of scripts/multi-msg-record.test (the bash wrapper handles +# NETWORK_UNSHARE_HELPER / AM_BWRAPPED and the python3 availability +# check, then execs this script). +# +# Tests that wolfSSL correctly processes TLS records containing multiple +# handshake messages packed into a single record. +# +# Uses tlslite-ng as the TLS peer to craft multi-message records: +# +# TLS 1.2 – Each connection tests TWO code paths back-to-back: +# 1. Initial handshake: RecordMergingSocket rewrites separate +# plaintext ServerHello + Certificate + ServerKeyExchange + +# ServerHelloDone records into one multi-message TLS +# record before forwarding to the wolfSSL client. +# 2. Renegotiation on the same connection: tlslite-ng is +# monkey-patched to coalesce SH+Cert+SKE+SHD into ONE +# encrypted handshake record (exercises the +# curSize -= padSz CBC-padding path and the AEAD path). +# +# TLS 1.3 – tlslite-ng's _queue_message / _queue_flush mechanism already +# coalesces EncryptedExtensions + Certificate + CertificateVerify +# + Finished into a single encrypted record. The test verifies +# that wolfSSL parses this correctly. +# +# Multiple cipher suites are tested for both protocol versions. +# +# Requirements: python3, tlslite-ng (pip install tlslite-ng) + +import socket +import struct +import subprocess +import os +import sys +import threading +import time +import types + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +WOLFSSL_DIR = os.path.dirname(SCRIPT_DIR) +WOLF_CLIENT = os.path.join(WOLFSSL_DIR, "examples", "client", "client") +CERT_DIR = os.path.join(WOLFSSL_DIR, "certs") + +# --------------------------------------------------------------------------- +# Bypass a strict tlslite-ng validation that rejects wolfSSL's ClientHello +# when the client advertises FFDHE groups in a TLS-1.3-only hello. +# This must happen before importing TLSConnection. +# +# If tlslite-ng isn't installed we exit 77 so automake marks the test +# SKIPped instead of FAILed. +# --------------------------------------------------------------------------- +try: + import tlslite.tlsconnection # noqa: E402 + import tlslite.recordlayer # noqa: E402 + tlslite.tlsconnection.TLS_1_3_FORBIDDEN_GROUPS = frozenset() + + from tlslite import ( # noqa: E402 + TLSConnection, HandshakeSettings, X509CertChain, parsePEMKey, + ) + from tlslite.constants import ContentType # noqa: E402 + from tlslite.extensions import RenegotiationInfoExtension # noqa: E402 + from tlslite.constants import ExtensionType # noqa: E402 + from tlslite.messages import HelloMessage, Message as TLSMessage # noqa: E402 +except ImportError as e: + sys.stdout.write( + "tlslite-ng not installed ({}); skipping multi-msg-record test\n" + " (install with: pip install tlslite-ng)\n".format(e)) + sys.exit(77) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +HS_NAMES = { + 2: "SH", 4: "NST", 8: "EE", 11: "Cert", 12: "SKE", + 13: "CR", 14: "SHD", 15: "CV", 16: "CKE", 20: "Fin", +} + +PASS_COUNT = 0 +FAIL_COUNT = 0 +SKIP_COUNT = 0 + + +def passed(label): + global PASS_COUNT + PASS_COUNT += 1 + print(f" PASS: {label}") + + +def failed(label): + global FAIL_COUNT + FAIL_COUNT += 1 + print(f" FAIL: {label}") + + +def skipped(label): + global SKIP_COUNT + SKIP_COUNT += 1 + print(f" SKIP: {label}") + + +def detect_wolf_features(): + """Probe the wolfSSL client binary to find which features are + compiled in. Used to decide which test phases to run. + + Returns dict with boolean keys: tls12, tls13, secure_reneg. + """ + feats = {"tls12": False, "tls13": False, "secure_reneg": False} + + # ./client -V -> e.g. "3:4:d(downgrade):e(either):" + try: + r = subprocess.run([WOLF_CLIENT, "-V"], + capture_output=True, timeout=5) + parts = r.stdout.decode("utf-8", errors="replace").strip().split(":") + feats["tls12"] = "3" in parts + feats["tls13"] = "4" in parts + except Exception: + pass + + # ./client -? -> help text includes "-R" only when + # HAVE_SECURE_RENEGOTIATION is defined. + try: + r = subprocess.run([WOLF_CLIENT, "-?"], + capture_output=True, timeout=5) + htxt = r.stdout.decode("utf-8", errors="replace") + feats["secure_reneg"] = ("Allow Secure Renegotiation" in htxt) + except Exception: + pass + + return feats + + +def _load_chain(cert_file): + with open(cert_file) as f: + chain = X509CertChain() + chain.parsePemList(f.read()) + return chain + + +def _load_key(key_file): + with open(key_file) as f: + return parsePEMKey(f.read(), private=True) + + +def _parse_hs_types(data): + """Parse handshake message types from raw handshake content.""" + msgs = [] + off = 0 + while off + 4 <= len(data): + ht = data[off] + hl = struct.unpack("!I", b"\x00" + bytes(data[off + 1 : off + 4]))[0] + msgs.append(HS_NAMES.get(ht, f"T{ht}")) + off += 4 + hl + return msgs + + +def _get_free_port(): + """Get an available TCP port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +def _listen_socket(): + """Bind a listening TCP socket on localhost with the standard test timeout.""" + port = _get_free_port() + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind(("127.0.0.1", port)) + srv.listen(1) + srv.settimeout(15) + return srv, port + + +def _run_wolf_client(port, version, cipher, extra=()): + """Invoke the wolfSSL example client against 127.0.0.1:port.""" + cmd = [WOLF_CLIENT, "-h", "127.0.0.1", "-p", str(port), + "-v", version, "-A", os.path.join(CERT_DIR, "ca-cert.pem"), + "-g", *extra] + if cipher: + cmd.extend(["-l", cipher]) + return subprocess.run(cmd, capture_output=True, timeout=15) + + +class _SendRecordTrace: + """Context manager that wraps RecordLayer.sendRecord to log every record.""" + + def __init__(self): + self.log = [] + self._orig = None + + def __enter__(self): + self._orig = tlslite.recordlayer.RecordLayer.sendRecord + log = self.log + orig = self._orig + + def wrapper(self_rl, msg): + data = msg.write() + ct = msg.contentType + encrypted = bool(self_rl._writeState + and self_rl._writeState.encContext) + hs_msgs = [] + if ct == ContentType.handshake: + hs_msgs = _parse_hs_types(data) + log.append((ct, encrypted, len(data), hs_msgs)) + yield from orig(self_rl, msg) + + tlslite.recordlayer.RecordLayer.sendRecord = wrapper + return self.log + + def __exit__(self, *exc): + tlslite.recordlayer.RecordLayer.sendRecord = self._orig + + +# --------------------------------------------------------------------------- +# RecordMergingSocket (TLS 1.2 plaintext record merging) +# --------------------------------------------------------------------------- +class RecordMergingSocket: + """Socket wrapper that rewrites consecutive TLS handshake records into + a single multi-message record. Only merges plaintext records that + precede ChangeCipherSpec.""" + + def __init__(self, sock): + self._sock = sock + self._pending = bytearray() + self._ver = 0x0303 + self._after_ccs = False + self.merged_msgs = [] # [(n_msgs, [names], size)] + + def _flush(self): + if not self._pending: + return + msgs = _parse_hs_types(self._pending) + n = len(msgs) + hdr = struct.pack("!BHH", 22, self._ver, len(self._pending)) + self._sock.sendall(hdr + bytes(self._pending)) + self.merged_msgs.append((n, msgs, len(self._pending))) + self._pending = bytearray() + + # Called by BufferedSocket (one record per call, or multiple from flush) + def _process(self, data): + data = bytearray(data) + off = 0 + while off + 5 <= len(data): + ct = data[off] + ver = struct.unpack("!H", data[off + 1 : off + 3])[0] + rlen = struct.unpack("!H", data[off + 3 : off + 5])[0] + if off + 5 + rlen > len(data): + break + payload = data[off + 5 : off + 5 + rlen] + if not self._after_ccs and ct == 22: + self._pending.extend(payload) + self._ver = ver + else: + if ct == 20: + self._after_ccs = True + self._flush() + self._sock.sendall(bytes(data[off : off + 5 + rlen])) + off += 5 + rlen + + def send(self, data): + self._process(data) + return len(data) + + def sendall(self, data): + self._process(data) + + def recv(self, bufsize): + self._flush() + return self._sock.recv(bufsize) + + def __getattr__(self, name): + return getattr(self._sock, name) + + +# --------------------------------------------------------------------------- +# Test runners +# --------------------------------------------------------------------------- +def run_tls12_test(cipher_wolf, cert_chain, priv_key, label, + do_reneg=True): + """TLS 1.2 test – one connection optionally exercises two code paths: + + Phase 1 (plaintext grouping, initial handshake): + RecordMergingSocket rewrites separate plaintext ServerHello, + Certificate, ServerKeyExchange and ServerHelloDone records into + one multi-message TLS record before delivery to wolfSSL. + + Phase 2 (encrypted grouping, renegotiation on same connection): + tlslite-ng server is monkey-patched to coalesce SH+Cert+SKE+SHD + into a single encrypted handshake record inside the renegotiation + (exercises wolfSSL's encrypted multi-message parsing including + curSize -= padSz for CBC padding). + + Phase 2 is skipped when do_reneg=False (e.g. the wolfSSL client was + built without HAVE_SECURE_RENEGOTIATION). + """ + srv, port = _listen_socket() + + result = {"ok": False, "error": ""} + msock_ref = [None] + trace_log = [] + reneg_active = [False] + verify_data = {'client': None, 'server': None} + + # --- monkey-patches (used only during this connection) ---------------- + orig_calc_key = tlslite.tlsconnection.calc_key + + def capturing_calc_key(*args, **kwargs): + res = orig_calc_key(*args, **kwargs) + lbl = args[3] if len(args) > 3 else kwargs.get('label', b'') + if lbl == b"client finished" and verify_data['client'] is None: + verify_data['client'] = bytearray(res) + elif lbl == b"server finished" and verify_data['server'] is None: + verify_data['server'] = bytearray(res) + return res + + orig_getExt = HelloMessage.getExtension + + def patched_getExt(self, ext_type): + ext = orig_getExt(self, ext_type) + if (ext_type == ExtensionType.renegotiation_info + and ext is not None and reneg_active[0]): + ext._internal_value = bytearray(0) + return ext + + orig_rie_create = RenegotiationInfoExtension.create + + def patched_rie_create(self, data): + if reneg_active[0] and data == bytearray(0): + combined = (bytearray(verify_data['client']) + + bytearray(verify_data['server'])) + return orig_rie_create(self, combined) + return orig_rie_create(self, data) + + # ---------------------------------------------------------------------- + def server(): + try: + tlslite.tlsconnection.calc_key = capturing_calc_key + HelloMessage.getExtension = patched_getExt + RenegotiationInfoExtension.create = patched_rie_create + + conn, _ = srv.accept() + conn.settimeout(15) + msock = RecordMergingSocket(conn) + msock_ref[0] = msock + tls = TLSConnection(msock) + settings = HandshakeSettings() + settings.minVersion = (3, 3) + settings.maxVersion = (3, 3) + + # ---------- Phase 1: initial handshake (plaintext grouping) ---- + tls.handshakeServer(certChain=cert_chain, privateKey=priv_key, + settings=settings) + tlslite.tlsconnection.calc_key = orig_calc_key + + data = tls.recv(4096) + + if do_reneg: + # ---------- Phase 2: trigger + run renegotiation ---------- + hr = TLSMessage(ContentType.handshake, + bytearray([0, 0, 0, 0])) + for _ in tls._sendMsg(hr, randomizeFirstBlock=False, + update_hashes=False): + pass + + # Bypass tlslite-ng renegotiation guards + tls.closed = True + tls.session = None + reneg_active[0] = True + + # Coalesce handshake messages into ONE encrypted TLS record + def coalescing_sendMsgs(self, msgs): + for msg in msgs: + self._queue_message(msg) + yield from self._queue_flush() + tls._sendMsgs = types.MethodType(coalescing_sendMsgs, tls) + + with _SendRecordTrace() as log: + tls.handshakeServer(certChain=cert_chain, + privateKey=priv_key, + settings=settings) + reneg_active[0] = False + trace_log.extend(log) + + if data: + tls.send(data) + tls.close() + result["ok"] = True + except Exception as e: + import traceback + result["error"] = traceback.format_exc() + finally: + tlslite.tlsconnection.calc_key = orig_calc_key + HelloMessage.getExtension = orig_getExt + RenegotiationInfoExtension.create = orig_rie_create + reneg_active[0] = False + srv.close() + + st = threading.Thread(target=server, daemon=True) + st.start() + time.sleep(0.1) + + proc = _run_wolf_client(port, "3", cipher_wolf, + extra=("-R",) if do_reneg else ()) + st.join(timeout=5) + + if proc.returncode != 0 or not result["ok"]: + err = (result["error"] + or proc.stderr.decode("utf-8", errors="replace")[:400]) + failed(f"{label}: connection failed ({err})") + return False + + ok = True + + # Phase 1 verification: plaintext multi-message record + msock = msock_ref[0] + has_pt_grouped = False + for n, msgs, sz in (msock.merged_msgs if msock else []): + if n > 1: + has_pt_grouped = True + passed(f"{label} [plaintext]: {n} msgs " + f"[{'+'.join(msgs)}] in one record ({sz} bytes)") + if not has_pt_grouped: + failed(f"{label} [plaintext]: no multi-message record detected") + ok = False + + # Phase 2 verification: encrypted multi-message record (renego) + if do_reneg: + has_enc_grouped = False + for ct, enc, sz, msgs in trace_log: + if ct == ContentType.handshake and enc and len(msgs) > 1: + has_enc_grouped = True + passed(f"{label} [encrypted]: {len(msgs)} msgs " + f"[{'+'.join(msgs)}] in one record ({sz} bytes)") + if not has_enc_grouped: + failed(f"{label} [encrypted]: no multi-message " + f"encrypted record") + ok = False + + return ok + + +def run_tls13_test(cipher_wolf, cert_chain, priv_key, label): + """TLS 1.3: verify tlslite-ng sends multi-msg encrypted record and + wolfSSL client processes it.""" + srv, port = _listen_socket() + + result = {"ok": False, "error": ""} + + def server(): + try: + conn, _ = srv.accept() + conn.settimeout(15) + tls = TLSConnection(conn) + settings = HandshakeSettings() + settings.minVersion = (3, 4) + settings.maxVersion = (3, 4) + tls.handshakeServer(certChain=cert_chain, privateKey=priv_key, + settings=settings) + data = tls.recv(4096) + if data: + tls.send(data) + tls.close() + result["ok"] = True + except Exception as e: + result["error"] = str(e) + finally: + srv.close() + + with _SendRecordTrace() as log: + st = threading.Thread(target=server, daemon=True) + st.start() + time.sleep(0.1) + + proc = _run_wolf_client(port, "4", cipher_wolf) + st.join(timeout=5) + + if proc.returncode != 0 or not result["ok"]: + err = result["error"] or proc.stderr.decode("utf-8", errors="replace")[:200] + failed(f"{label}: handshake failed ({err})") + return False + + # Check that at least one encrypted handshake record has multiple messages + has_multi = False + for ct, enc, sz, msgs in log: + if ct == ContentType.handshake and enc and len(msgs) > 1: + has_multi = True + passed(f"{label}: {len(msgs)} encrypted msgs " + f"[{'+'.join(msgs)}] in one record ({sz} bytes)") + if not has_multi: + failed(f"{label}: no multi-message encrypted records") + return False + return True + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +def main(): + if not os.path.isfile(WOLF_CLIENT): + print(f"ERROR: wolfSSL client not found: {WOLF_CLIENT}") + print(" Build wolfSSL first (./configure && make)") + sys.exit(1) + + # Probe the client to see which features are compiled in so each + # phase of the test is only run when it can succeed. + feats = detect_wolf_features() + + # Load certificate / key pairs + rsa_chain = _load_chain(os.path.join(CERT_DIR, "server-cert.pem")) + rsa_key = _load_key(os.path.join(CERT_DIR, "server-key.pem")) + + print("=" * 60) + print(" Multi-Message TLS Record Test") + print("=" * 60) + print(f" wolfSSL features: TLS1.2={feats['tls12']} " + f"TLS1.3={feats['tls13']} " + f"secure_reneg={feats['secure_reneg']}") + + # ------------------------------------------------------------------ + # TLS 1.2 – plaintext (initial HS) + optional encrypted (renegotiation) + # multi-message records, same connection per cipher suite. + # ------------------------------------------------------------------ + tls12_suites = [ + # (wolfSSL cipher name, description) + (None, "default negotiated"), + # AEAD (GCM) + ("ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-RSA AES128-GCM"), + ("ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-RSA AES256-GCM"), + ("DHE-RSA-AES128-GCM-SHA256", "DHE-RSA AES128-GCM"), + ("DHE-RSA-AES256-GCM-SHA384", "DHE-RSA AES256-GCM"), + # CBC + HMAC (exercises padding path) + ("ECDHE-RSA-AES128-SHA256", "ECDHE-RSA AES128-CBC-SHA256"), + ("ECDHE-RSA-AES256-SHA384", "ECDHE-RSA AES256-CBC-SHA384"), + ("DHE-RSA-AES128-SHA256", "DHE-RSA AES128-CBC-SHA256"), + ("DHE-RSA-AES256-SHA256", "DHE-RSA AES256-CBC-SHA256"), + # AEAD (ChaCha20-Poly1305) + ("ECDHE-RSA-CHACHA20-POLY1305", "ECDHE-RSA CHACHA20-POLY1305"), + ("DHE-RSA-CHACHA20-POLY1305", "DHE-RSA CHACHA20-POLY1305"), + ] + + if feats["tls12"]: + if feats["secure_reneg"]: + print("\n--- TLS 1.2: plaintext + encrypted multi-message " + "records ---") + print(" Each connection verifies BOTH code paths:") + print(" * initial handshake -> plaintext SH+Cert+SKE+SHD") + print(" * renegotiation -> encrypted SH+Cert+SKE+SHD") + else: + print("\n--- TLS 1.2: plaintext multi-message records ---") + print(" wolfSSL built without HAVE_SECURE_RENEGOTIATION;") + print(" skipping the encrypted (renegotiation) half.") + print(" Covers multiple key-exchanges, ciphers and MAC " + "families.\n") + + for cipher, desc in tls12_suites: + run_tls12_test(cipher, rsa_chain, rsa_key, + f"TLS1.2 {desc}", + do_reneg=feats["secure_reneg"]) + if not feats["secure_reneg"]: + skipped("TLS1.2 encrypted multi-msg record " + "(requires HAVE_SECURE_RENEGOTIATION)") + else: + skipped(f"TLS 1.2 tests ({len(tls12_suites)} suites) - " + "wolfSSL built without TLS 1.2") + + # ------------------------------------------------------------------ + # TLS 1.3 – encrypted multi-message records + # ------------------------------------------------------------------ + tls13_suites = [ + # (wolfSSL cipher name, description) + (None, "default negotiated"), + ("TLS13-AES128-GCM-SHA256", "AES-128-GCM"), + ("TLS13-AES256-GCM-SHA384", "AES-256-GCM"), + ("TLS13-CHACHA20-POLY1305-SHA256", "CHACHA20-POLY1305"), + ] + + if feats["tls13"]: + print("\n--- TLS 1.3: encrypted multi-message records ---") + print(" Server sends EE+Cert+CV+Fin in a single encrypted " + "record;") + print(" wolfSSL client must decrypt and parse.\n") + + for cipher, desc in tls13_suites: + run_tls13_test(cipher, rsa_chain, rsa_key, + f"TLS1.3 {desc}") + else: + skipped(f"TLS 1.3 tests ({len(tls13_suites)} suites) - " + "wolfSSL built without TLS 1.3") + + # ------------------------------------------------------------------ + # Summary + # ------------------------------------------------------------------ + print() + print("=" * 60) + print(f" Results: {PASS_COUNT} passed, {FAIL_COUNT} failed, " + f"{SKIP_COUNT} skipped") + print("=" * 60) + + # If nothing at all could run, signal SKIP (exit 77) so automake + # records the test as skipped rather than passed-with-nothing. + if PASS_COUNT == 0 and FAIL_COUNT == 0: + sys.exit(77) + + return FAIL_COUNT == 0 + + +if __name__ == "__main__": + sys.exit(0 if main() else 1) diff --git a/scripts/multi-msg-record.test b/scripts/multi-msg-record.test new file mode 100755 index 0000000000..984918f0a4 --- /dev/null +++ b/scripts/multi-msg-record.test @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# +# multi-msg-record.test +# +# Wrapper around scripts/multi-msg-record.py, invoked by automake via +# `make check`. Verifies that the wolfSSL client correctly processes +# TLS records that carry multiple handshake messages in a single record +# (TLS 1.2 plaintext + renegotiation, and TLS 1.3 encrypted flight). +# +# The test is SKIPped (exit 77) when python3 or tlslite-ng is not +# installed. +# + +# if we can, isolate the network namespace to eliminate port collisions. +if [[ -n "$NETWORK_UNSHARE_HELPER" ]]; then + if [[ -z "$NETWORK_UNSHARE_HELPER_CALLED" ]]; then + export NETWORK_UNSHARE_HELPER_CALLED=yes + exec "$NETWORK_UNSHARE_HELPER" "$0" "$@" || exit $? + fi +elif [ "${AM_BWRAPPED-}" != "yes" ]; then + bwrap_path="$(command -v bwrap)" + if [ -n "$bwrap_path" ]; then + export AM_BWRAPPED=yes + exec "$bwrap_path" --unshare-net --dev-bind / / "$0" "$@" + fi + unset AM_BWRAPPED +fi + +# Locate python3 – skip (exit 77) if unavailable. +PYTHON="$(command -v python3)" +if [ -z "$PYTHON" ]; then + echo "python3 not found, skipping multi-msg-record test" + exit 77 +fi + +SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" +exec "$PYTHON" "$SCRIPT_DIR/multi-msg-record.py" "$@" diff --git a/src/internal.c b/src/internal.c index c009452d62..d772797424 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19086,11 +19086,11 @@ static int DoHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, return DoHandShakeMsgType(ssl, input, inOutIdx, type, size, totalSz); } - /* curSize has already been reduced to content-only (padSz subtracted) - * in ProcessReply, so curStartIdx + curSize bounds the content. */ - if (*inOutIdx > (word32)ssl->curStartIdx + ssl->curSize) + /* totalSz is now curStartIdx + curSize (content-only, padSz already + * subtracted in ProcessReply). */ + if (*inOutIdx > totalSz) return BUFFER_ERROR; - inputLength = ssl->curStartIdx + ssl->curSize - *inOutIdx; + inputLength = totalSz - *inOutIdx; /* If there is a pending fragmented handshake message, * pending message size will be non-zero. */ @@ -23512,8 +23512,11 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) /* Reduce curSize to content only, excluding padding/MAC overhead. * padSz is accounted for once at the end of record processing. */ - if (IsEncryptionOn(ssl, 0)) + if (IsEncryptionOn(ssl, 0)) { + if (ssl->keys.padSz > ssl->curSize) + return BUFFER_E; ssl->curSize -= (word16)ssl->keys.padSz; + } /* in case > 1 msg per record */ ssl->curStartIdx = ssl->buffers.inputBuffer.idx; @@ -23617,7 +23620,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = DoDtlsHandShakeMsg(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); if (ret == 0 || ret == WC_NO_ERR_TRACE(WC_PENDING_E)) { /* Reset timeout as we have received a valid @@ -23637,7 +23640,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = Dtls13HandshakeRecv(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); #ifdef WOLFSSL_EARLY_DATA if (ret == 0 && ssl->options.side == WOLFSSL_SERVER_END && @@ -23674,7 +23677,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = DoHandShakeMsg(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); if (ret != 0) { if (SendFatalAlertOnly(ssl, ret) == WC_NO_ERR_TRACE(SOCKET_ERROR_E)) @@ -23690,7 +23693,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = DoTls13HandShakeMsg(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); #ifdef WOLFSSL_EARLY_DATA if (ret != 0) return ret; diff --git a/src/tls13.c b/src/tls13.c index a3ad1eacf5..db0d352db8 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13934,11 +13934,11 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, totalSz); } - /* curSize has already been reduced to content-only (padSz subtracted) - * in ProcessReply, so curStartIdx + curSize bounds the content. */ - if (*inOutIdx > (word32)ssl->curStartIdx + ssl->curSize) + /* totalSz is now curStartIdx + curSize (content-only, padSz already + * subtracted in ProcessReply). */ + if (*inOutIdx > totalSz) return BUFFER_ERROR; - inputLength = ssl->curStartIdx + ssl->curSize - *inOutIdx; + inputLength = totalSz - *inOutIdx; /* If there is a pending fragmented handshake message, * pending message size will be non-zero. */ From dab6461db1d7a0c43536a0b53119e79fdefd7ec1 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 12 May 2026 22:18:21 +0200 Subject: [PATCH 06/11] Fix comment dash --- scripts/multi-msg-record.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/multi-msg-record.test b/scripts/multi-msg-record.test index 984918f0a4..3bb70b9ff6 100755 --- a/scripts/multi-msg-record.test +++ b/scripts/multi-msg-record.test @@ -26,7 +26,7 @@ elif [ "${AM_BWRAPPED-}" != "yes" ]; then unset AM_BWRAPPED fi -# Locate python3 – skip (exit 77) if unavailable. +# Locate python3 - skip (exit 77) if unavailable. PYTHON="$(command -v python3)" if [ -z "$PYTHON" ]; then echo "python3 not found, skipping multi-msg-record test" From b9fad30beea88d3c1bd645096e76fcf07de1f8d7 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 12 May 2026 22:40:39 +0200 Subject: [PATCH 07/11] Install tlslite-ng in os-check workflow so multi-msg-record test runs --- .github/workflows/os-check.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/os-check.yml b/.github/workflows/os-check.yml index 39b4c65585..1738340cf0 100644 --- a/.github/workflows/os-check.yml +++ b/.github/workflows/os-check.yml @@ -137,6 +137,13 @@ jobs: # This should be a safe limit for the tests to run. timeout-minutes: 14 steps: + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng + - name: Build and test wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 with: @@ -181,6 +188,13 @@ jobs: # This should be a safe limit for the tests to run. timeout-minutes: 14 steps: + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng + - name: Build and test wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 with: @@ -207,6 +221,13 @@ jobs: # This should be a safe limit for the tests to run. timeout-minutes: 14 steps: + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng + - name: Build and test wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 with: @@ -269,6 +290,12 @@ jobs: timeout-minutes: 14 steps: - uses: actions/checkout@v4 + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng - run: ./autogen.sh - name: user_settings_all.h with compatibility layer run: | From 6357a0e5cfd443fc1ac5cf7bdb702d39d9f19e80 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 13 May 2026 12:22:56 +0200 Subject: [PATCH 08/11] Skip multi-msg-record ciphers not built into wolfSSL client Probe ./client -e for the supported cipher list and skip suites that aren't compiled in instead of reporting them as failures. --- scripts/multi-msg-record.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/scripts/multi-msg-record.py b/scripts/multi-msg-record.py index fe19730eae..5f40430773 100755 --- a/scripts/multi-msg-record.py +++ b/scripts/multi-msg-record.py @@ -105,9 +105,11 @@ def detect_wolf_features(): """Probe the wolfSSL client binary to find which features are compiled in. Used to decide which test phases to run. - Returns dict with boolean keys: tls12, tls13, secure_reneg. + Returns dict with keys: tls12 (bool), tls13 (bool), + secure_reneg (bool), ciphers (set[str]). """ - feats = {"tls12": False, "tls13": False, "secure_reneg": False} + feats = {"tls12": False, "tls13": False, "secure_reneg": False, + "ciphers": set()} # ./client -V -> e.g. "3:4:d(downgrade):e(either):" try: @@ -129,6 +131,15 @@ def detect_wolf_features(): except Exception: pass + # ./client -e -> colon-separated list of supported cipher suites. + try: + r = subprocess.run([WOLF_CLIENT, "-e"], + capture_output=True, timeout=5) + ctxt = r.stdout.decode("utf-8", errors="replace").strip() + feats["ciphers"] = {c for c in ctxt.split(":") if c} + except Exception: + pass + return feats @@ -556,6 +567,9 @@ def main(): "families.\n") for cipher, desc in tls12_suites: + if cipher and cipher not in feats["ciphers"]: + skipped(f"TLS1.2 {desc} - cipher not in wolfSSL build") + continue run_tls12_test(cipher, rsa_chain, rsa_key, f"TLS1.2 {desc}", do_reneg=feats["secure_reneg"]) @@ -584,6 +598,9 @@ def main(): print(" wolfSSL client must decrypt and parse.\n") for cipher, desc in tls13_suites: + if cipher and cipher not in feats["ciphers"]: + skipped(f"TLS1.3 {desc} - cipher not in wolfSSL build") + continue run_tls13_test(cipher, rsa_chain, rsa_key, f"TLS1.3 {desc}") else: From 7cc972d5c74f569e45b043282bc9001682f48417 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 13 May 2026 13:22:31 +0200 Subject: [PATCH 09/11] Use DER CA cert in multi-msg-record test for NO_CODING builds wolfSSL builds configured with --enable-coding=no cannot parse PEM because base64 decoding is disabled. Switch the example client's -A argument to ca-cert.der so the test works in both PEM-enabled and PEM-disabled builds. --- scripts/multi-msg-record.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/multi-msg-record.py b/scripts/multi-msg-record.py index 5f40430773..017934deb2 100755 --- a/scripts/multi-msg-record.py +++ b/scripts/multi-msg-record.py @@ -186,9 +186,13 @@ def _listen_socket(): def _run_wolf_client(port, version, cipher, extra=()): - """Invoke the wolfSSL example client against 127.0.0.1:port.""" + """Invoke the wolfSSL example client against 127.0.0.1:port. + + Uses the DER-encoded CA cert so the test works with wolfSSL builds + configured with NO_CODING (base64 decode disabled, no PEM support). + """ cmd = [WOLF_CLIENT, "-h", "127.0.0.1", "-p", str(port), - "-v", version, "-A", os.path.join(CERT_DIR, "ca-cert.pem"), + "-v", version, "-A", os.path.join(CERT_DIR, "ca-cert.der"), "-g", *extra] if cipher: cmd.extend(["-l", cipher]) From d2f45f614fa9e08c6c534946dab55fd7949f78b2 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 13 May 2026 14:50:47 +0200 Subject: [PATCH 10/11] Make test scripts work in sandboxed/restricted environments multi-msg-record.py: auto-detect the CA cert format the wolfSSL client build accepts (PEM or DER) from the default shown in client -? help. OPENSSL_EXTRA-style builds need PEM; NO_CODING builds need DER. ocsp-stapling.test: skip the external login.live.com connection unless WOLFSSL_EXTERNAL_TEST is explicitly enabled (matches external.test / google.test convention). Local OCSP tests still run. ocsp-responder-openssl-interop.test: use ${TMPDIR:-/tmp} for mktemp templates so the test works when /tmp is not writable. --- scripts/multi-msg-record.py | 23 +++++++++++++++------ scripts/ocsp-responder-openssl-interop.test | 20 +++++++++--------- scripts/ocsp-stapling.test | 4 +++- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/scripts/multi-msg-record.py b/scripts/multi-msg-record.py index 017934deb2..a1216d8e3d 100755 --- a/scripts/multi-msg-record.py +++ b/scripts/multi-msg-record.py @@ -44,6 +44,10 @@ WOLF_CLIENT = os.path.join(WOLFSSL_DIR, "examples", "client", "client") CERT_DIR = os.path.join(WOLFSSL_DIR, "certs") +# CA cert path passed to the wolfSSL client via -A. Set in main() after +# detect_wolf_features() determines whether the build accepts PEM or DER. +WOLF_CA_CERT = os.path.join(CERT_DIR, "ca-cert.pem") + # --------------------------------------------------------------------------- # Bypass a strict tlslite-ng validation that rejects wolfSSL's ClientHello # when the client advertises FFDHE groups in a TLS-1.3-only hello. @@ -106,10 +110,11 @@ def detect_wolf_features(): compiled in. Used to decide which test phases to run. Returns dict with keys: tls12 (bool), tls13 (bool), - secure_reneg (bool), ciphers (set[str]). + secure_reneg (bool), ciphers (set[str]), ca_cert (str). """ feats = {"tls12": False, "tls13": False, "secure_reneg": False, - "ciphers": set()} + "ciphers": set(), + "ca_cert": os.path.join(CERT_DIR, "ca-cert.pem")} # ./client -V -> e.g. "3:4:d(downgrade):e(either):" try: @@ -122,12 +127,16 @@ def detect_wolf_features(): pass # ./client -? -> help text includes "-R" only when - # HAVE_SECURE_RENEGOTIATION is defined. + # HAVE_SECURE_RENEGOTIATION is defined. The default -A path + # ("ca-cert.pem" vs "ca-cert.der") also tells us which CA file + # format the build can load. try: r = subprocess.run([WOLF_CLIENT, "-?"], capture_output=True, timeout=5) htxt = r.stdout.decode("utf-8", errors="replace") feats["secure_reneg"] = ("Allow Secure Renegotiation" in htxt) + if "ca-cert.der" in htxt and "ca-cert.pem" not in htxt: + feats["ca_cert"] = os.path.join(CERT_DIR, "ca-cert.der") except Exception: pass @@ -188,11 +197,11 @@ def _listen_socket(): def _run_wolf_client(port, version, cipher, extra=()): """Invoke the wolfSSL example client against 127.0.0.1:port. - Uses the DER-encoded CA cert so the test works with wolfSSL builds - configured with NO_CODING (base64 decode disabled, no PEM support). + WOLF_CA_CERT is PEM or DER depending on the build (NO_CODING / + OPENSSL_EXTRA builds don't both support PEM). """ cmd = [WOLF_CLIENT, "-h", "127.0.0.1", "-p", str(port), - "-v", version, "-A", os.path.join(CERT_DIR, "ca-cert.der"), + "-v", version, "-A", WOLF_CA_CERT, "-g", *extra] if cipher: cmd.extend(["-l", cipher]) @@ -522,6 +531,8 @@ def main(): # Probe the client to see which features are compiled in so each # phase of the test is only run when it can succeed. feats = detect_wolf_features() + global WOLF_CA_CERT + WOLF_CA_CERT = feats["ca_cert"] # Load certificate / key pairs rsa_chain = _load_chain(os.path.join(CERT_DIR, "server-cert.pem")) diff --git a/scripts/ocsp-responder-openssl-interop.test b/scripts/ocsp-responder-openssl-interop.test index ed62c19951..8e0bd3ac25 100755 --- a/scripts/ocsp-responder-openssl-interop.test +++ b/scripts/ocsp-responder-openssl-interop.test @@ -219,9 +219,9 @@ port4=$(get_first_free_port $((port3 + 1))) # OCSP responder: root-ca port5=$(get_first_free_port $((port4 + 1))) # TLS server # Responder 1: intermediate1-ca (server1=valid, server2=revoked) -log1=$(mktemp /tmp/ocsp_resp1.XXXXXX) +log1=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp1.XXXXXX") resp_logs="$resp_logs $log1" -ready1=$(mktemp /tmp/ocsp_ready1.XXXXXX) +ready1=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready1.XXXXXX") ready_files="$ready_files $ready1" $OCSP_RESPONDER -p $port1 -v -R "$ready1" \ -c $OCSP_DIR/intermediate1-ca-cert.pem \ @@ -232,9 +232,9 @@ pid1=$! resp_pids="$resp_pids $pid1" # Responder 2: intermediate2-ca (server3=valid, server4=revoked) -log2=$(mktemp /tmp/ocsp_resp2.XXXXXX) +log2=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp2.XXXXXX") resp_logs="$resp_logs $log2" -ready2=$(mktemp /tmp/ocsp_ready2.XXXXXX) +ready2=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready2.XXXXXX") ready_files="$ready_files $ready2" $OCSP_RESPONDER -p $port2 -v -R "$ready2" \ -c $OCSP_DIR/intermediate2-ca-cert.pem \ @@ -245,9 +245,9 @@ pid2=$! resp_pids="$resp_pids $pid2" # Responder 3: intermediate3-ca (server5=valid) -log3=$(mktemp /tmp/ocsp_resp3.XXXXXX) +log3=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp3.XXXXXX") resp_logs="$resp_logs $log3" -ready3=$(mktemp /tmp/ocsp_ready3.XXXXXX) +ready3=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready3.XXXXXX") ready_files="$ready_files $ready3" $OCSP_RESPONDER -p $port3 -v -R "$ready3" \ -c $OCSP_DIR/intermediate3-ca-cert.pem \ @@ -258,9 +258,9 @@ pid3=$! resp_pids="$resp_pids $pid3" # Responder 4: root-ca (intermediate CAs: 1=valid, 2=valid, 3=revoked) -log4=$(mktemp /tmp/ocsp_resp4.XXXXXX) +log4=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp4.XXXXXX") resp_logs="$resp_logs $log4" -ready4=$(mktemp /tmp/ocsp_ready4.XXXXXX) +ready4=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready4.XXXXXX") ready_files="$ready_files $ready4" $OCSP_RESPONDER -p $port4 -v -R "$ready4" \ -c $OCSP_DIR/root-ca-cert.pem \ @@ -271,9 +271,9 @@ pid4=$! resp_pids="$resp_pids $pid4" # Responder 5: authorized responder (delegated OCSP signer with id-kp-OCSPSigning) -log5=$(mktemp /tmp/ocsp_resp5.XXXXXX) +log5=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp5.XXXXXX") resp_logs="$resp_logs $log5" -ready5=$(mktemp /tmp/ocsp_ready5.XXXXXX) +ready5=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready5.XXXXXX") ready_files="$ready_files $ready5" $OCSP_RESPONDER -p $port5 -v -R "$ready5" \ -c $OCSP_DIR/root-ca-cert.pem \ diff --git a/scripts/ocsp-stapling.test b/scripts/ocsp-stapling.test index 8f6ed717cf..55ead3e27f 100755 --- a/scripts/ocsp-stapling.test +++ b/scripts/ocsp-stapling.test @@ -341,7 +341,9 @@ server=login.live.com #ca=certs/external/DigiCertGlobalRootCA.pem ca=./certs/external/ca_collection.pem -if [[ "$V4V6" == "4" ]]; then +if [[ -z "${WOLFSSL_EXTERNAL_TEST-}" || "$WOLFSSL_EXTERNAL_TEST" == "0" ]]; then + echo "Skipping OCSP test on $server (set WOLFSSL_EXTERNAL_TEST=1 to run)" +elif [[ "$V4V6" == "4" ]]; then retry_with_backoff 3 ./examples/client/client -C -h "$server" -p 443 -A "$ca" -g -W 1 RESULT=$? [ $RESULT -ne 0 ] && echo -e "\n\nClient connection failed" && exit 1 From 39642d5ad3a9579f78527dfcd87a9792f588c6b3 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 13 May 2026 18:10:24 +0200 Subject: [PATCH 11/11] Skip multi-msg-record test when wolfSSL built without RSA The test certs are RSA; if NO_RSA is defined the client can neither load nor verify them. Detect "RSA not supported" in client -? help and exit 77 (SKIP) before tlslite-ng tries to use the RSA chain. --- scripts/multi-msg-record.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/multi-msg-record.py b/scripts/multi-msg-record.py index a1216d8e3d..ae035d5605 100755 --- a/scripts/multi-msg-record.py +++ b/scripts/multi-msg-record.py @@ -110,9 +110,10 @@ def detect_wolf_features(): compiled in. Used to decide which test phases to run. Returns dict with keys: tls12 (bool), tls13 (bool), - secure_reneg (bool), ciphers (set[str]), ca_cert (str). + secure_reneg (bool), rsa (bool), ciphers (set[str]), ca_cert (str). """ feats = {"tls12": False, "tls13": False, "secure_reneg": False, + "rsa": True, "ciphers": set(), "ca_cert": os.path.join(CERT_DIR, "ca-cert.pem")} @@ -129,7 +130,8 @@ def detect_wolf_features(): # ./client -? -> help text includes "-R" only when # HAVE_SECURE_RENEGOTIATION is defined. The default -A path # ("ca-cert.pem" vs "ca-cert.der") also tells us which CA file - # format the build can load. + # format the build can load. The RSA key-size line reports + # "RSA not supported" when NO_RSA is defined. try: r = subprocess.run([WOLF_CLIENT, "-?"], capture_output=True, timeout=5) @@ -137,6 +139,8 @@ def detect_wolf_features(): feats["secure_reneg"] = ("Allow Secure Renegotiation" in htxt) if "ca-cert.der" in htxt and "ca-cert.pem" not in htxt: feats["ca_cert"] = os.path.join(CERT_DIR, "ca-cert.der") + if "RSA not supported" in htxt: + feats["rsa"] = False except Exception: pass @@ -534,16 +538,24 @@ def main(): global WOLF_CA_CERT WOLF_CA_CERT = feats["ca_cert"] - # Load certificate / key pairs - rsa_chain = _load_chain(os.path.join(CERT_DIR, "server-cert.pem")) - rsa_key = _load_key(os.path.join(CERT_DIR, "server-key.pem")) - print("=" * 60) print(" Multi-Message TLS Record Test") print("=" * 60) print(f" wolfSSL features: TLS1.2={feats['tls12']} " f"TLS1.3={feats['tls13']} " - f"secure_reneg={feats['secure_reneg']}") + f"secure_reneg={feats['secure_reneg']} " + f"rsa={feats['rsa']}") + + # The test certs are RSA; skip the whole test when the wolfSSL build + # has no RSA support (the client can't load or verify them). + if not feats["rsa"]: + print("\n wolfSSL built without RSA; skipping multi-msg-record " + "test (RSA test certs cannot be verified).") + sys.exit(77) + + # Load certificate / key pairs + rsa_chain = _load_chain(os.path.join(CERT_DIR, "server-cert.pem")) + rsa_key = _load_key(os.path.join(CERT_DIR, "server-key.pem")) # ------------------------------------------------------------------ # TLS 1.2 – plaintext (initial HS) + optional encrypted (renegotiation)