More and more TLS traffic runs over QUIC (HTTP/3), and the classic JA3 fingerprint was never defined for it. Its successor, JA4, handles QUIC explicitly: the fingerprint gets a q prefix instead of t. Wireshark 4.2+ computes JA4 natively, but only the client side — there is no built-in JA4S (server fingerprint). I wrote a Lua plugin that fills that gap: ja4_quic.lua.
What Wireshark already gives you
Since 4.2, the TLS dissector exposes:
tls.handshake.ja3 tls.handshake.ja3s
tls.handshake.ja4 tls.handshake.ja4_r
Because the QUIC dissector hands the ClientHello to the TLS dissector, tls.handshake.ja4 also populates for QUIC Initial packets, with the correct q prefix:
$ tshark -r quic-tls-handshake.pcapng -T fields -e tls.handshake.ja4
q13d0310h3_55b375c5d22e_cd85d2d88918
What's missing from Wireshark's built-in TLS fields is JA4S. JA4 is available under the BSD 3-Clause License, while JA4S is part of the separately licensed JA4+ suite. FoxIO does provide its own JA4+ Wireshark plugin, but JA4S is not implemented by Wireshark's core TLS dissector.
The plugin
ja4_quic.lua is a postdissector that:
- reads JA4 from the built-in field (no point recomputing it — GREASE handling and the QUIC detection are already done),
- computes JA4S from the ServerHello,
- tracks sessions by
quic.connection.number(which survives QUIC connection migration) ortcp.stream, - adds filterable fields:
ja4_quic.ja4,ja4_quic.ja4s,ja4_quic.ja4s_r,ja4_quic.sni,ja4_quic.alpn, - exports everything to CSV via Tools → Export JA4 Analysis.
JA4S in one minute
q 13 02 00 _ 1301 _ 234ea6891581
| | | | | |
| | | | | truncated SHA-256 of the extension list, in order
| | | | cipher suite chosen by the server
| | | ALPN chosen ("00" if none visible)
| | number of extensions
| TLS version (from supported_versions if present)
q = QUIC, t = TCP
One caveat worth knowing: in TLS 1.3 (and therefore in QUIC) the server sends its ALPN choice inside EncryptedExtensions, which is encrypted. Without TLS decryption secrets, a passive analyzer cannot recover that value, so the ALPN part is 00. This is a property of passive TLS 1.3 analysis rather than a limitation specific to the plugin.
Implementation notes
Wireshark doesn't expose a cryptographic hashing API to Lua, so the plugin carries its own SHA-256 implementation. It requires Wireshark 4.4 or newer, which ships with Lua 5.4 and its native bitwise operators. One important detail is that Lua integers are 64-bit, so every 32-bit rotation needs masking:
local function rotr32(x, n)
return ((x >> n) | (x << (32 - n))) & 0xffffffff
end
The hash self-tests against a known vector at load time — a silently broken hash producing plausible-looking fingerprints would be worse than a crash.
Another Wireshark-specific gotcha: a postdissector runs again every time the GUI re-dissects a packet (clicking it in the packet list), so byte counters must only accumulate when pinfo.visited is false.
Verification
I compared the output against the FoxIO reference implementation (python/ja4.py from their repo) on their own sample pcaps. Every JA4S the reference emitted matches the plugin:
| pcap | plugin | FoxIO reference |
|---|---|---|
latest.pcapng |
t1206h2_c02f_3603f09c43ba |
t1206h2_c02f_3603f09c43ba |
latest.pcapng |
t130300_1301_6bbbaf601ed8 |
t130300_1301_6bbbaf601ed8 |
chrome-cloudflare-quic (TCP stream) |
t130200_1301_234ea6891581 |
t130200_1301_234ea6891581 |
The reference script didn't emit a JA4S for the QUIC stream in that last capture, so the TCP row is the directly comparable one. On the QUIC side, JA4 matches Wireshark's built-in implementation, and the plugin's QUIC JA4S (q130200_1301_234ea6891581) differs from the verified TCP fingerprint only by the q prefix — expected, since it's the same server configuration.
Usage
Install Wireshark 4.4 or newer. The plugin uses Lua 5.4 syntax and will not load in Wireshark 4.2, even though that version already exposes the built-in JA4 fields.
Drop the file into your personal Lua plugins directory (About Wireshark → Folders → Personal Lua Plugins, e.g. ~/.local/lib/wireshark/plugins/), reload with Ctrl+Shift+L, and filter away:
ja4_quic.ja4s == "q130200_1301_234ea6891581"
ja4_quic.sni contains "example.com"
Licensing
JA4 (the client fingerprint) is available under the BSD 3-Clause License. JA4S and the rest of the JA4+ suite are specified by FoxIO and covered by the FoxIO License 1.1.
That license permits personal and academic use, as well as internal business use that does not directly monetize JA4+. Direct or indirect monetization — including incorporating JA4S into a paid product, hosted service, managed service, or other offering that provides value to paying customers — requires a separate license from FoxIO. The plugin follows the FoxIO specification and reference implementation and is published for non-commercial personal, academic and internal analysis. Review the full license before redistributing the plugin or using it in a commercial context.
Top comments (0)