diff --git a/scripts/ocsp-stapling_tls13multi.test b/scripts/ocsp-stapling_tls13multi.test index 2b05a85498..b4ed1a47e9 100755 --- a/scripts/ocsp-stapling_tls13multi.test +++ b/scripts/ocsp-stapling_tls13multi.test @@ -60,6 +60,18 @@ else dtls13=yes fi +# The fragmented-send case needs a client that can request a small maximum +# fragment length and a server that can simulate WANT_WRITE. Both options are +# silently ignored otherwise, which would turn the case into a no-op that still +# reports PASSED. +if ! ./examples/client/client -? 2>&1 | grep -q 'Maximum Fragment Length'; then + fragmented_send=no +elif ./examples/server/server -6 -? 2>&1 | grep -q 'Ignoring -6'; then + fragmented_send=no +else + fragmented_send=yes +fi + if [[ ("$tls13" == "no") && ("$dtls13" == "no") ]]; then echo 'skipping ocsp-stapling_tls13multi.test because TLS1.3 is not available.' 1>&2 exit 77 @@ -492,6 +504,27 @@ if [ "$tls13" == "yes" ]; then exit 1 fi printf '%s\n\n' "Test successfully REVOKED!" + + printf '%s\n\n' "------------- TEST CASE 8 FRAGMENTED SEND --------------------" + if [ "$fragmented_send" == "no" ]; then + printf '%s\n\n' "Test SKIPPED: needs HAVE_MAX_FRAGMENT and async I/O." + else + # A small maximum fragment length (-F 1) splits the stapled Certificate + # message over many records, and the server (-6) blocks on every one of + # them, so the send resumes from a WANT_WRITE inside the chain. + remove_single_rF "$ready_file5" + ./examples/server/server -c certs/ocsp/server3-cert.pem \ + -k certs/ocsp/server3-key.pem -R "$ready_file5" \ + -p "$port5" -v 4 -6 & + server_pid5=$! + wait_for_readyFile "$ready_file5" "$server_pid5" "$port5" + ./examples/client/client -C -A certs/ocsp/root-ca-cert.pem -W 1 -v 4 \ + -F 1 -p "$port5" + RESULT=$? + [ "$RESULT" -ne 0 ] && printf '\n\n%s\n' "Client connection 8 failed" \ + && exit 1 + printf '%s\n\n' "Test PASSED!" + fi fi if [ "$dtls13" == "yes" ]; then diff --git a/src/internal.c b/src/internal.c index 953b71ff17..6ecb77c3ec 100644 --- a/src/internal.c +++ b/src/internal.c @@ -9738,6 +9738,17 @@ void wolfSSL_ResourceFree(WOLFSSL* ssl) XFREE(ssl->buffers.tls13CookieSecret.buffer, ssl->heap, DYNAMIC_TYPE_COOKIE_PWD); #endif +#if !defined(NO_CERTS) && defined(WOLFSSL_TLS13) && \ + defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) + { + /* Release the certificate status extensions of a Certificate message + * that was never sent in full. */ + int extIdx; + + for (extIdx = 0; extIdx < MAX_CERT_EXTENSIONS; extIdx++) + FreeDer(&ssl->buffers.certExts[extIdx]); + } +#endif #ifdef WOLFSSL_TLS13_STREAM_CERT_VERIFY /* Release any in-progress streamed CertificateVerify body (e.g. a * connection torn down mid-send). */ diff --git a/src/tls13.c b/src/tls13.c index 83ac9bd39a..410b0054e6 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -9664,33 +9664,46 @@ static int WriteCSRToBuffer(WOLFSSL* ssl, DerBuffer** certExts, for (extIdx = 0; extIdx < (word16)(extSz_num); extIdx++) { tmpSz = TLSX_CSR_GetSize_ex(csr, 0, (int)extIdx); - if (tmpSz > (OPAQUE8_LEN + OPAQUE24_LEN) && - certExts[extIdx] == NULL) { - /* csr extension is not zero */ - if (tmpSz > WOLFSSL_MAX_16BIT) - return BUFFER_E; - extSz[extIdx] = (word16)tmpSz; + if (ssl->fragOffset != 0 && certExts[extIdx] != NULL) { + /* A fragmented send is being resumed and this buffer was + * written by the earlier call. extSz starts over on every + * call, so recover this entry's size from the length written + * into the buffer. */ + ato16(certExts[extIdx]->buffer, &extSz[extIdx]); + extSz[extIdx] += OPAQUE16_LEN; + } + else { + /* Not a resume, so anything still allocated here is left over + * from a completed message and must not be reused. */ + FreeDer(&certExts[extIdx]); - ret = AllocDer(&certExts[extIdx], extSz[extIdx] + ex_offset, - CERT_TYPE, ssl->heap); - if (ret < 0) - return ret; - der = certExts[extIdx]; + if (tmpSz > (OPAQUE8_LEN + OPAQUE24_LEN)) { + /* csr extension is not zero */ + if (tmpSz > WOLFSSL_MAX_16BIT) + return BUFFER_E; + extSz[extIdx] = (word16)tmpSz; - /* write extension type */ - c16toa(ext->type, der->buffer - + OPAQUE16_LEN); - /* writes extension data length. */ - c16toa(extSz[extIdx], der->buffer - + HELLO_EXT_TYPE_SZ + OPAQUE16_LEN); - /* write extension data */ - extSz[extIdx] = (word16)TLSX_CSR_Write_ex(csr, - der->buffer + ex_offset, 0, extIdx); - /* add extension offset */ - extSz[extIdx] += (word16)ex_offset; - /* extension length */ - c16toa(extSz[extIdx] - OPAQUE16_LEN, - der->buffer); + ret = AllocDer(&certExts[extIdx], extSz[extIdx] + ex_offset, + CERT_TYPE, ssl->heap); + if (ret < 0) + return ret; + der = certExts[extIdx]; + + /* write extension type */ + c16toa(ext->type, der->buffer + + OPAQUE16_LEN); + /* writes extension data length. */ + c16toa(extSz[extIdx], der->buffer + + HELLO_EXT_TYPE_SZ + OPAQUE16_LEN); + /* write extension data */ + extSz[extIdx] = (word16)TLSX_CSR_Write_ex(csr, + der->buffer + ex_offset, 0, extIdx); + /* add extension offset */ + extSz[extIdx] += (word16)ex_offset; + /* extension length */ + c16toa(extSz[extIdx] - OPAQUE16_LEN, + der->buffer); + } } totalSz += extSz[extIdx]; } @@ -9877,7 +9890,8 @@ static int SendTls13Certificate(WOLFSSL* ssl) word32 totalextSz = 0; word32 len = 0; word32 idx = 0; - word32 offset = OPAQUE16_LEN; + word32 offset = 0; + word32 entrySz = 0; byte* p = NULL; byte certReqCtxLen = 0; sword32 length; @@ -9957,9 +9971,14 @@ static int SendTls13Certificate(WOLFSSL* ssl) && ssl->options.handShakeDone) #endif ) { - ret = SetupOcspResp(ssl); - if (ret != 0) - return ret; + /* Build the responses once. A resumed send reuses them: looking + * them up again appends another set of requests to the extension + * until it overflows with MAX_CERT_EXTENSIONS_ERR. */ + if (ssl->fragOffset == 0) { + ret = SetupOcspResp(ssl); + if (ret != 0) + return ret; + } if ((1 + ssl->buffers.certChainCnt) > MAX_CERT_EXTENSIONS) ret = MAX_CERT_EXTENSIONS_ERR; @@ -10007,6 +10026,50 @@ static int SendTls13Certificate(WOLFSSL* ssl) extIdx = 0; + /* Only ssl->fragOffset survives a WANT_WRITE, so a resume inside the chain + * has to rebuild the walk cursor from it. */ + if (certChainSz > 0 && ssl->fragOffset >= certSz + extSz[0]) { + word32 chainPos = ssl->fragOffset - (certSz + extSz[0]); + + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) + /* The leaf is behind us and its buffer was rebuilt above. */ + FreeDer(&ssl->buffers.certExts[0]); + #endif + + while (chainPos > 0) { + word32 prevIdx = idx; + + len = NextCert(ssl->buffers.certChain->buffer, + ssl->buffers.certChain->length, &idx); + if (len == 0) + break; + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ + !defined(NO_WOLFSSL_SERVER) + if (extIdx + 1 < MAX_CERT_EXTENSIONS) + extIdx++; + #endif + entrySz = len + extSz[extIdx]; + + if (chainPos < entrySz) { + /* Resume part way through this entry. */ + p = ssl->buffers.certChain->buffer + prevIdx; + offset = chainPos; + chainPos = 0; + } + else { + /* Entry already sent in full; stay primed for the next one. */ + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ + !defined(NO_WOLFSSL_SERVER) + /* Its buffer was rebuilt above and nothing writes it again. */ + FreeDer(&ssl->buffers.certExts[extIdx]); + #endif + chainPos -= entrySz; + offset = 0; + entrySz = 0; + } + } + } + while (length > 0 && ret == 0) { byte* output = NULL; word32 fragSz = 0; @@ -10107,7 +10170,7 @@ static int SendTls13Certificate(WOLFSSL* ssl) while (fragSz > 0) { word32 l; - if (offset == len + OPAQUE16_LEN) { + if (offset == entrySz) { /* Find next CA certificate to write out. */ offset = 0; /* Point to the start of current cert in chain buffer. */ @@ -10121,6 +10184,8 @@ static int SendTls13Certificate(WOLFSSL* ssl) if (extIdx + 1 < MAX_CERT_EXTENSIONS) extIdx++; #endif + /* Certificate and its extensions make up the entry. */ + entrySz = len + extSz[extIdx]; } /* Write out certificate and extension. */ l = AddCertExt(ssl, p, len, extSz[extIdx], offset, fragSz, @@ -10133,10 +10198,8 @@ static int SendTls13Certificate(WOLFSSL* ssl) if (extIdx != 0 && extIdx < MAX_CERT_EXTENSIONS && ssl->buffers.certExts[extIdx] != NULL && - offset == len + extSz[extIdx]) { + offset == entrySz) { FreeDer(&ssl->buffers.certExts[extIdx]); - /* for next chain cert */ - len += extSz[extIdx] - OPAQUE16_LEN; } } } diff --git a/tests/test-tls13-slhdsa-entity-128s.conf b/tests/test-tls13-slhdsa-entity-128s.conf index fe4cd5e9f7..16f11a14db 100644 --- a/tests/test-tls13-slhdsa-entity-128s.conf +++ b/tests/test-tls13-slhdsa-entity-128s.conf @@ -39,3 +39,42 @@ -k ./certs/slhdsa/client-slhdsa-shake-128s-priv.pem -A ./certs/slhdsa/root-slhdsa-shake-128s.pem -C + +# Server auth with simulated WANT_WRITE (-6): leaf plus root push the +# Certificate message past one record, so the send resumes mid chain. + +# server TLSv1.3 TLS13-AES128-GCM-SHA256 +-v 4 +-l TLS13-AES128-GCM-SHA256 +-c ./certs/slhdsa/server-slhdsa-shake-128s.pem +-k ./certs/slhdsa/server-slhdsa-shake-128s-priv.pem +-d +-6 + +# client TLSv1.3 TLS13-AES128-GCM-SHA256 +-v 4 +-l TLS13-AES128-GCM-SHA256 +-A ./certs/slhdsa/root-slhdsa-shake-128s.pem +-C +-6 + +# Mutual auth with simulated WANT_WRITE, so the client's own Certificate +# send resumes mid chain too. + +# server TLSv1.3 TLS13-AES128-GCM-SHA256 +-v 4 +-l TLS13-AES128-GCM-SHA256 +-c ./certs/slhdsa/server-slhdsa-shake-128s.pem +-k ./certs/slhdsa/server-slhdsa-shake-128s-priv.pem +-A ./certs/slhdsa/root-slhdsa-shake-128s.pem +-V +-6 + +# client TLSv1.3 TLS13-AES128-GCM-SHA256 +-v 4 +-l TLS13-AES128-GCM-SHA256 +-c ./certs/slhdsa/client-slhdsa-shake-128s.pem +-k ./certs/slhdsa/client-slhdsa-shake-128s-priv.pem +-A ./certs/slhdsa/root-slhdsa-shake-128s.pem +-C +-6