Fix sniffer decryption, threaded snifftest, and the shared-state teardown - #11319
Conversation
|
Can one of the admins verify this patch? |
9450c24 to
1ecc9b8
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11319
Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
1ecc9b8 to
715d663
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11319
Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
715d663 to
43b485c
Compare
The sniffer ignored the encrypt_then_mac extension (RFC 7366) in the ServerHello. For a CBC suite that meant the trailing MAC was handed to the block decrypt along with the ciphertext, so the length was not a multiple of the block size and every record failed with BAD_FUNC_ARG. wolfSSL peers negotiate Encrypt-Then-MAC by default, so no TLS 1.2 CBC capture taken from one could be decrypted. Record the extension and remove the MAC before decrypting. The plaintext length returned to the caller, printed by snifftest as SSL App Data(packet:length), came from the record size without subtracting what DecryptMessage() had already worked out in ssl->keys.padSz. A 14 byte payload was reported as 30 under TLS 1.2 AES-GCM and as 31 under TLS 1.3. Subtract padSz so the sniffer removes the same bytes the ordinary read path removes from ssl->curSize. Both failures were invisible because the tests could not fail. snifftest overwrote hadBadPacket on every packet instead of accumulating it, so errors early in a capture were erased by later good packets and the process still exited 0. The keylog leg read $? after a pipe into tee, which reports tee's status rather than snifftest's. Accumulate the flag, use PIPESTATUS, and treat a saved capture that yields no plaintext at all as a failure. The two static RSA captures decode correctly again and the keylog reference output is regenerated from the checked-in pcaps, which had drifted by a packet since the files were last produced on 5.6.3. The X25519 legs are skipped: both captures are in the tree as a bare 24 byte pcap header with no packets. sniffer-gen.sh could never produce them because its configure line for those two builds without ed25519, so the example client and server fail to load their certificates and tcpdump writes an empty file. Add the missing option, and pin the group with --disable-mlkem so the capture negotiates X25519 rather than the X25519MLKEM768 hybrid the static key does not match. Regenerating them is left out of this change: the sniffer cannot yet derive the shared secret for an X25519 static ephemeral key, which is a separate defect.
Hand the corrected plaintext length to the WOLFSSL_SNIFFER_STORE_DATA_CB callback. The previous commit fixed the length only for the branch that copies into the caller's buffer; the callback branch still passed the raw record size, which spans the explicit IV, the tag or MAC and any padding. The callback pointer already points at the start of the plaintext, so the extra bytes run off the end of the decrypt output buffer, which leaves only a record header of slack. AddressSanitizer reports a heap-buffer-overflow read of 68 bytes on a TLS 1.2 CBC capture before this change and is clean after it. Reject a non-empty encrypt_then_mac extension in the ServerHello. RFC 7366 section 3.1 specifies empty extension_data and TLSX_EncryptThenMac_Parse enforces it, as do the neighbouring length-constrained cases in the same switch. Clear the Encrypt-Then-MAC decision at the start of every ServerHello. Nothing reset it, so a renegotiation that dropped the extension kept stripping a MAC that was no longer there, and one that added it started stripping while the previous cipher was still active. Reset the decrypted flag at doMessage and gate the padSz subtraction on it. The flag is set inside the decrypt block and was never cleared, so it described whichever earlier record in the packet had last been decrypted. The handshake case already guards the same value with it. Set the decoded-data flag on the WOLFSSL_ASYNC_CRYPT drain path too. A record that goes pending is completed inside SnifferAsyncPollQueue rather than DecodePacket, so an async build could decrypt a capture correctly and still report that nothing was decrypted. Send that diagnostic to stderr rather than into the decoded output, and note the exit status in the usage text so a wrapper is not surprised by it. Add a TLS 1.2 CBC Encrypt-Then-MAC capture and the sniffer-gen.sh recipe that produces it, so the fix has a regression test that can be regenerated. The existing static RSA and IPv6 captures exercise the same path but no script in the tree can rebuild them. The new capture covers both failure modes: with an HMAC-SHA1 suite the old code fails the decrypt outright, and with HMAC-SHA256, whose MAC is a multiple of the block size, it silently decrypts the wrong byte range and emits 48 bytes where 14 are correct. Warn when the Encrypt-Then-MAC gate drops the TLS 1.2 CBC captures, so a build configuration that loses coverage says so, and regenerate the keylog reference output with a binary that carries the new feature token.
curve25519 blinding is enabled by default for the C implementation, in settings.h, and draws from the private key's own RNG on every shared secret. The sniffer built its key with wc_curve25519_init_ex() followed by wc_Curve25519PrivateKeyDecode(), and neither sets one, so the shared secret reached wc_RNG_GenerateBlock() with a NULL RNG and came back BAD_FUNC_ARG while every argument it was given was valid. SetupKeys() then reported a server and client key mismatch and no X25519 session could be read at all. The library's own static ephemeral path already calls wc_curve25519_set_rng() under the same guard, and the sniffer already calls wc_ecc_set_rng() for the ECC key a few lines above. Only X25519 was missed. Regenerate the two X25519 captures. Both were committed as a bare 24 byte pcap header with no packets in them, so those legs had never tested anything; they now hold real traffic and fail against the unfixed sniffer. Drop the has_packets guard added earlier in this branch. It existed to skip the empty captures, and with every capture holding packets it is dead code. Removing it is also the better behaviour: snifftest already fails a capture that yields no plaintext, so an empty file now fails the suite loudly instead of being skipped, which is what a missing fixture deserves.
snifferWorker() looped while the shutdown flag was clear and only then drained its queue. Reading a capture file, main() enqueues every packet and then sets that flag and joins, normally all before the worker thread is scheduled for the first time, so the worker woke up, saw the flag and returned without decoding a single packet. Instrumenting the loop shows it exiting with zero iterations and a non-empty queue, at -O0 as well as -O1, so this is an ordering race rather than a hoisted load. Keep going after shutdown for as long as packets are queued. The check has to be its own helper that takes the worker's semaphore, because a placeholder head means an empty queue: looping on worker->head alone would spin forever on a placeholder nothing will ever fill. shutdown and unused are polled across threads, so mark them volatile. That alone left -keylogfile broken in this build. The sniffer's server and secret tables are thread local, so every worker has to repeat the setup main() did for itself, but the workers only ever called load_key() and so reported every packet as coming from an unregistered server. Carry the keylog path in SnifferWorker and load the secrets and create the keylog sniffer server per worker. Verified with --enable-sniffer CFLAGS=-DTHREADED_SNIFFTEST: the whole sniffer test suite passes, and the multi-session captures decode the same application data at 1, 2, 4 and 8 threads as they do without threading.
The sniffer's session, server and secret tables are qualified THREAD_LS_T, so each thread that calls ssl_InitSniffer*() owns its own. Its trace file, its mutexes and its crypto device are not: they are shared by the whole process. ssl_FreeSniffer() released all of it on every call, so in a build such as snifftest with THREADED_SNIFFTEST, where every worker thread initializes and frees the sniffer for itself, a thread that finished early closed the trace file and freed the mutexes under the threads still running. A worker that was still registering its key then crashed inside ssl_SetNamedEphemeralKey(), writing to a file another thread had closed: flockfile -> vfprintf -> fprintf -> ssl_SetNamedEphemeralKey -> load_key Count the callers of ssl_InitSniffer_ex(), which ssl_InitSniffer() and ssl_InitSniffer_ex2() both funnel through, and only release the shared state once the count reaches zero. The count is atomic where the platform offers atomics and a plain int otherwise, which covers the single threaded build. The per-thread tables are still freed on every call, and wolfSSL_Cleanup() is still called every time, since it keeps a count of its own. Verified with two ssl_InitSniffer() callers and one ssl_FreeSniffer(): before this the trace file was closed by the early free and everything traced after it was lost, now both entries are present. The full sniffer test suite passes with and without THREADED_SNIFFTEST.
With WOLFSSL_SNIFFER defined, the example client and server pin themselves to a static RSA and static ECC cipher list so a capture can be decrypted. The guard for that read "version < 4", meant to leave TLS 1.3 alone, but the examples encode a DTLS version as a negative number and DTLS 1.3 is -4, so a "-u -v 4" run was handed a TLS 1.2 only cipher list. The server then found no common TLS 1.3 suite in MatchSuite_ex() while processing the ClientHello and failed with MATCH_SUITE_ERROR, which reached the client as a missing_extension alert. Every DTLS 1.3 handshake between the two examples failed in a sniffer build, which also took out the DTLS cases of scripts/ocsp-stapling_tls13multi.test. Only the examples were affected; an application that does not set that cipher list was always able to handshake.
3393290 to
4c4856f
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11319
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.
Fenrir's latest completed scan found no issues; clearing the prior automated change request.
Description
Nine sniffer fixes. Three of them mean whole classes of capture could not be decrypted at all, and one means a sniffer build could not complete a DTLS 1.3 handshake at all.
Encrypt-Then-MAC was never implemented.
EXT_ENCRYPT_THEN_MACwas defined insrc/sniffer.cbut never handled in theProcessServerHelloextension switch, sostartedETMReadwas never set and the trailing MAC was passed towc_AesCbcDecrypt()along with the ciphertext. The length is then not a block multiple and every record fails withBAD_FUNC_ARG. wolfSSL peers negotiate RFC 7366 by default, so no TLS 1.2 CBC capture between wolfSSL peers could be decrypted. A build without Encrypt-Then-MAC support now says so through a new error instead of failing every record generically; the complaint is deferred until the suite is known, since RFC 7366 covers only block ciphers.Reported plaintext lengths included the MAC or AEAD tag. The length came from the record size without removing
ssl->keys.padSz, so 14 bytes were reported as 30 under TLS 1.2 AES-GCM. The plaintext was correct, which is why this survived. ForWOLFSSL_SNIFFER_STORE_DATA_CBthis is a heap buffer overread - the callback pointer already sits at the plaintext start, so the extra bytes run off the decrypt output buffer. ASan reports aREAD of size 68past the allocation before the fix, clean after.The X25519 static ephemeral key had no RNG. curve25519 blinding is on by default for the C implementation and draws from the key's own RNG, but the sniffer built its key with
wc_curve25519_init_ex()andwc_Curve25519PrivateKeyDecode(), neither of which sets one.wc_RNG_GenerateBlock()then returnedBAD_FUNC_ARGwith every argument valid, and no X25519 traffic could be read. The library's own static ephemeral path and the sniffer's ECC path already called the matching setter; only X25519 was missed.The rest:
client_key_exchangehandler and the TLS 1.3 ServerHello path overwrote whateverSetupKeys()had reported with the generic "Server Client Key Mismatch", discarding the real reason every time. This is what made the X25519 failure look like a key problem.snifftestcould not report a failed capture. The read loop assignedhadBadPacketinstead of accumulating it, so an early error was erased by any later clean packet. A new opt-in-expectdataadditionally fails a run that decrypted nothing at all.THREADED_SNIFFTESTdecoded nothing. The worker checked its shutdown flag before draining its queue, and reading a capture file sets that flag before the worker is first scheduled. Separately, the server and secret tables are thread-local, so workers had to repeatmain()'s keylog setup and never did.ssl_FreeSniffer()tore down process-wide state from every thread. The trace file, mutexes and crypto device are shared, but every call released them, so a worker finishing early closed the trace file under threads still running. The init entry points now count their callers and only the last free releases the shared state.WOLFSSL_SNIFFERdefined, the example client and server pin themselves to a static RSA and static ECC cipher list so a capture can be decrypted. The guard for that readversion < 4, meant to leave TLS 1.3 alone, but the examples encode a DTLS version as a negative number and DTLS 1.3 is-4, so a-u -v 4run was handed a TLS 1.2 only cipher list. The server then found no common TLS 1.3 suite inMatchSuite_ex()while processing the ClientHello and failed withMATCH_SUITE_ERROR, reaching the client as amissing_extensionalert. This also took out the DTLS cases ofscripts/ocsp-stapling_tls13multi.test. Only the examples were affected; an application that does not set that cipher list was always able to handshake.Testing
Nothing failed before because three things swallowed it:
hadBadPacketwas assigned rather than accumulated, the keylog leg readtee's exit status instead of snifftest's, and every other leg checked only the exit code, never the lengths. On top of that bothsniffer-tls13-x25519*.pcapwere checked in at 24 bytes - a bare pcap header with no packets - so those legs had always tested nothing.sniffer-tls12-etm.pcapand-etm-keylog.pcap, with atls12-etmbranch insniffer-gen.sh. Two suites deliberately: HMAC-SHA1 makes the pre-fix decrypt fail outright, HMAC-SHA256 silently decrypts the wrong byte range.sniffer-static-rsa.pcapandsniffer-ipv6.pcapregenerated Encrypt-Then-MAC-free, so IPv6 is no longer gated on an unrelated feature..outoracle. Reverting thepadSzfix makes the EtM leg report 36/52/48/64 where 14/22/14/22 are correct.sniffer-gen.shregenerates the oracles itself, takes capture names, and fails on a name that matched nothing. Six keylog assets had never been in any.am, somake distshipped the test but not its captures; fixed.Three CI entries added to
.github/configs/os-check-linux.json, each covering a path no other entry compiles:sniffer-keylog-storedata(WOLFSSL_SNIFFER_KEYLOGFILEandWOLFSSL_SNIFFER_STORE_DATA_CB),sniffer-no-enc-then-mac(the new diagnostic and its assertion), andsniffer-threaded(THREADED_SNIFFTEST, session tickets off so the concurrent path is actually exercised).The DTLS 1.3 fix is verified separately, since no CI entry reaches it - the only
--enable-snifferleg has no--enable-dtls13, soocsp-stapling_tls13multi.testskips its DTLS cases there. Built--enable-all --enable-debug --enable-sniffer: before the fix a plainserver -u -v 4/client -u -v 4pair fails deterministically andocsp-stapling_tls13multi.testfails atTEST CASE DTLS-1; after it, both pass, TLS 1.3 is unchanged, andsniffer-testsuite.teststill passes.