DEV Community

Marcin
Marcin

Posted on

JA4 and JA4S fingerprints for QUIC traffic in Wireshark, with a Lua plugin

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) or tcp.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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)