Send server ChangeCipherSpec when client offers a session id - #11306
Send server ChangeCipherSpec when client offers a session id#11306Frauschi wants to merge 1 commit into
Conversation
|
Can one of the admins verify this patch? |
|
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11306
Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src
Fenrir result: Approved ✅
No new issues found in the changed files.
Advisory only — this automated result does not count as a GitHub approval.
|
Jenkins retest this please. |
philljj
left a comment
There was a problem hiding this comment.
Skoll had a testing coverage nit (see below), otherwise ran clean.
Would like @julek-wolfssl to review this because he also just worked on middlebox-compat.
MEDIUM-1: Exercise the changed early-data CCS retry path
- File:
src/tls13.c:15778-15784 - Function:
wolfSSL_connect_TLSv13
Description: The PR changes the early-data path to guard and set sentChangeCipher before calling SendChangeCipher(), but the new client retry test covers only ordinary and HelloRetryRequest handshakes. It never enables early data or enters this branch. Moreover, the existing early-data WANT_WRITE callback in tests/api/test_tls13.c deliberately lets a standalone CCS write pass, so the default non-grouped configuration cannot detect a duplicate or stranded CCS here.
Code:
if (!ssl->options.dtls && !ssl->options.sentChangeCipher
&& ssl->options.tls13MiddleBoxCompat) {
ssl->options.sentChangeCipher = 1;
if ((ssl->error = SendChangeCipher(ssl)) != 0) {
return WOLFSSL_FATAL_ERROR;
}
}
Recommendation: Cover this third modified client send site with the same retry assertions already applied to the normal and HelloRetryRequest paths.
RFC 8446 Appendix D.4 describes a TLS 1.3 server answering a ClientHello that carries a non-empty legacy_session_id with a ChangeCipherSpec. Both server send paths sat behind WOLFSSL_TLS13_MIDDLEBOX_COMPAT, which only --enable-tls13-middlebox-compat and --enable-jni define and which neither enables by default, so a default-built server never sent one. Peers that require the record, such as Erlang's ssl, abort the handshake with an unexpected_message alert. Compile the two server accept-path sends unconditionally and drive them from the runtime tls13MiddleBoxCompat flag, which the server already clears when the client's session id is empty. This needs the sentChangeCipher option bit compiled in for every TLS 1.3 build, and SendChangeCipher itself, whose definition was guarded on TLS 1.2 or old TLS or middlebox compat and so went missing from a TLS 1.3-only build such as examples/configs/user_settings_tls13.h. That definition is now compiled for a TLS 1.3 server, or a TLS 1.3 client in a middlebox compat build, so a client-only build that can never call it does not carry it. DTLS stays excluded by the existing guard. QUIC clears the flag on its own rather than trusting the peer to honour the empty session id RFC 9001 Section 8.4 calls for, since a ChangeCipherSpec record has no way to travel a QUIC CRYPTO stream. With ECH the outer ClientHello decides on its own, since TLSX_ECH_Parse rebuilds the inner hello carrying the outer session id, so no carve-out is needed for the inner re-parse. wolfSSL_clear() resets the flag and sentChangeCipher, or a reused server object would answer only its first peer. Under TLS 1.3 the record is a dummy and must not move handshake state, so the OPENSSL_EXTRA block no longer assigns serverState for it. It ran before any send could fail, so a retryable send left serverState reading as ChangeCipherSpec complete and the server stopped waiting for the second ClientHello. The info callback still fires on both sides, and the client's clientState is untouched, so nothing an application can observe changes. Both server sends go through one static helper, SendTls13ServerChangeCipher, so the guard, the mark-before-send ordering and the flush are stated once, with the flush an argument because only the HelloRetryRequest needs it. The mark matters on the HelloRetryRequest path, where a resumed accept comes back through the prologue at TLS13_ACCEPT_FIRST_REPLY_DONE and falls into TLS13_SERVER_HELLO_SENT, and that flag is all that stops a second record going out. SendTls13ServerHello flushes a HelloRetryRequest as soon as it is built, so the ChangeCipherSpec queued behind it needs a flush of its own. Without one, a server grouping its messages, which every OPENSSL_COMPATIBLE_DEFAULTS build does, leaves the record in the output buffer and the peer sees a bare HelloRetryRequest. The three client send sites in wolfSSL_connect_TLSv13 take the same ordering. The one answering a HelloRetryRequest also keeps buildingMsg set when the send stops short, since the second ClientHello still has to follow and the connect state machine must stay on that case while the record drains; without it a refused write stranded the handshake. What a client puts on the wire is unchanged. Without WOLFSSL_TLS13_MIDDLEBOX_COMPAT it still sends an empty session id and no ChangeCipherSpec of its own. The test helper counts ChangeCipherSpec records rather than answering whether there is one, so that a second record going out reads differently from the one record that is owed. test_tls13_middlebox_compat_hrr_ccs covers the HelloRetryRequest path with messages grouped and ungrouped, and checks the flight answering the second ClientHello carries no further record. test_tls13_middlebox_compat_server_ccs_retry refuses the write carrying the record and resumes the handshake, so a send that stops part way still leaves exactly one on the wire, and test_tls13_middlebox_compat_hrr_ccs_retry does the same on the retry path, where the flight answering the second ClientHello must carry none. test_tls13_middlebox_compat_client_ccs_retry refuses the client's own record on both of its handshake flights, and test_tls13_middlebox_compat_client_ccs_retry_early_data on the third client send site, the early data one, where the record goes out ahead of the 0-RTT records rather than as part of a handshake flight and a resumed wolfSSL_write_early_data() would otherwise repeat it. Those client-side tests need WOLFSSL_TLS13_MIDDLEBOX_COMPAT, which no os-check-linux entry defined: only --enable-tls13-middlebox-compat and --enable-jni set it, and configure.ac turns it back off when ECH is on, as it is under --enable-all. The all-tls13-middlebox-compat entry is --enable-all with ECH off and the option on, so they run somewhere. Also correct the macro's doc comment, which claimed the feature defaults to on, reword the --enable-tls13-middlebox-compat help text and the matching CMake option description, which described the whole feature rather than the client half they now control, and drop a duplicated dtls test in the same condition.
ed6e678 to
91f6f74
Compare
|
@philljj fixed your finding. |
|
Jenkins retest this please |
|
Might be a real unit test failure: |
|
I cannot reproduce it. Jenkins retest this please. |
Description
RFC 8446 Appendix D.4 says a TLS 1.3 server answers a ClientHello carrying a non-empty
legacy_session_idwith a ChangeCipherSpec. Both server send paths sat behindWOLFSSL_TLS13_MIDDLEBOX_COMPAT, which only--enable-tls13-middlebox-compatand--enable-jnidefine and neither enables by default, so a default-built server never sent one. Peers that require it, such as Erlang'sssl, abort with anunexpected_messagealert.Only wolfSSL-as-server was affected: the wolfSSL client sends an empty session id by default, so no peer is ever obliged to answer back. Reported as an SLH-DSA interop bug, but nothing in this path touches the certificate or sigalg and the regression test reproduces it with the stock test certs.
Changes
Both server sends now compile unconditionally, driven by the runtime
tls13MiddleBoxCompatflag the server already clears for an empty session id, and are factored into one staticSendTls13ServerChangeCipher().sentChangeCipheris compiled in for every TLS 1.3 build.SendChangeCipher()is now compiled for a TLS 1.3 server (or a middlebox-compat client), where before it went missing from a TLS 1.3-only build such asexamples/configs/user_settings_tls13.h.serverStatein theOPENSSL_EXTRAblock. That ran before any send could fail, leaving the server not waiting for the second ClientHello.wolfSSL_clear()resets the flag, or a reused server answers only its first peer.buildingMsgset when the send stops short; without that a refused write stranded the client handshake.DTLS stays excluded, QUIC clears the flag per RFC 9001 Section 8.4, and with ECH the outer ClientHello decides since the inner hello is rebuilt carrying its session id. The configure and CMake option text now says client-side, since only that half stays behind the macro.
Behavioral change
A TLS 1.3 server now sends a ChangeCipherSpec it did not before, whenever the client offered a non-empty
legacy_session_id; an empty one still gets none. One API difference:wolfSSL_get_state()no longer passes throughWOLFSSL_SS_SERVER_CHANGECIPHERSPECorWOLFSSL_SS_CLIENT_CHANGECIPHERSPECon TLS 1.3, though the info callback still fires. What a client puts on the wire is unchanged. Written up inChangeLog.md.Testing
Eight tests in
tests/api/test_tls13.c, seven of them ungated since default-build behaviour is the point: a non-empty session id gets exactly one record, an empty one gets none (#9156), a cleared server still answers its second peer, the HelloRetryRequest flight carries the record grouped and ungrouped while the flight after it carries none, and a refused-then-resumed write leaves exactly one on the wire on the straight-through, retry and client paths.Each was verified to fail without its fix: reverting the source fails the two server tests, dropping the grouped-messages flush fails the HelloRetryRequest test, marking
sentChangeCipherafter a successful send fails the server retry test, and reverting the client HelloRetryRequest site fails the client test with the handshake never completing.Clean in
--enable-all, a default build,--enable-jni,--enable-tls13-middlebox-compat --enable-opensslextra --enable-hrrcookie,--enable-ech --enable-quic --enable-hrrcookie, a client-only TLS 1.3 build,user_settings_tls13.h, and CMake.make checkpasses on--enable-all.Notes for reviewers
Two pre-existing things this surfaces but does not fix:
ssl->session->sessionIDSzis reassigned on every ClientHello parse including the inner one, so with ECH accepted and a non-empty outer session id the ServerHello echoes an emptylegacy_session_id_echoalongside the record. Predates this change, a conformant client rejects on that mismatch alone, and it is unreachable wolfSSL-to-wolfSSL.sniffer.c'schange_cipher_speccase setshandShakeDonewith no TLS 1.3 exclusion, but the branch above it setsserverCipherOn = 0whensrvKs.key_len == 0, so the recovery path it looks like it would trip never fires. Checked against a real capture carrying both a server and a client ChangeCipherSpec.