mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-08-14 13:01:27 +02:00
Resume the TLS 1.3 certificate chain send after a WANT_WRITE
SendTls13Certificate keeps its chain walk cursor in the function locals len, idx, offset and p, but the only state that survives the WANT_WRITE return of a non-blocking send is ssl->fragOffset, and that is consulted for the leaf certificate alone. A send that blocked part way through the chain therefore re-primed the walk on the next call and copied the chain from its first byte again. The byte count still matched the announced payload size, so the message stayed well formed on the wire while the tail of the chain was replaced by a repeat of its head, and the peer rejected it with BUFFER_ERROR. Rebuild the cursor from ssl->fragOffset when a resume lands inside the chain. NextCert reads each entry's three byte length prefix and skips it, so passing over the entries already sent costs one hop per certificate and only happens on a resume. Guard the extension index bump the same way the send loop does, so builds without certificate status request keep the leaf extension size. Track the size of the chain entry being written in its own variable rather than folding the extension size into len once the entry completes. The send loop detected completion with offset == len + OPAQUE16_LEN and kept that check honest by adding extSz[extIdx] - OPAQUE16_LEN to len at the end of an entry, so until then len held the raw certificate length and the check read as complete whenever a fragment boundary landed exactly OPAQUE16_LEN bytes into a real extension. The walk then jumped to the next certificate in the middle of the current one. entrySz records len + extSz[extIdx] when the entry is picked up, len keeps the raw certificate length AddCertExt expects, and both the resume and the ordinary multi fragment path test the same condition. The stapled chain in scripts/ocsp-stapling_tls13multi.test reproduces the entry size case with the server records held to 1482 bytes: the boundary falls two bytes into an 1837 byte OCSP extension and the handshake fails, while 1480, 1481, 1483 and 1484 all pass. Reaching this needs a certificate message larger than one record, which is why it stayed dormant with classic certificates. Add SLH-DSA scenarios with simulated WANT_WRITE, for server and for mutual authentication, to tests/test-tls13-slhdsa-entity-128s.conf. The same resume path mishandles the stapled OCSP responses. WriteCSRToBuffer fills extSz[] only for the entries whose buffer it allocates, so on a resumed call every entry that still held a buffer, the one being written and all that follow it, kept the OPAQUE16_LEN default of an empty extension. The message length, the entry sizes and the extension bytes written for those entries were all derived from that default. Recover the size from the extension length already written into the buffer instead. SetupOcspResp appends a fresh request per certificate on every call, so a message that resumed often enough exhausted the extension array and the handshake ended with MAX_CERT_EXTENSIONS_ERR. Look the responses up once, when the message starts, and reuse them for the rest of it. A resumed call also reallocates the extension buffers of the entries it has already sent, and the walk passes over those entries without writing them again, so free them there. Free the array in wolfSSL_ResourceFree as well: nothing released it when a connection ended part way through a Certificate message, which leaked one OCSP response per unsent entry. Test case 8 of scripts/ocsp-stapling_tls13multi.test covers all three. A maximum fragment length of 512 bytes splits the stapled message over about twenty records and the server blocks on every one of them; without these fixes the handshake fails with MAX_CERT_EXTENSIONS_ERR.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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). */
|
||||
|
||||
+96
-33
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user