Skip to content

tls_inspector: fix GREASE filtering in JA4_c signature algorithms - #46658

Open
dangle1 wants to merge 2 commits into
envoyproxy:mainfrom
dangle1:fix/ja4-c-grease-sigalgs
Open

tls_inspector: fix GREASE filtering in JA4_c signature algorithms#46658
dangle1 wants to merge 2 commits into
envoyproxy:mainfrom
dangle1:fix/ja4-c-grease-sigalgs

Conversation

@dangle1

@dangle1 dangle1 commented Aug 11, 2026

Copy link
Copy Markdown

Summary

The JA4 fingerprint's third component hashes the ClientHello's extension list joined to its signature_algorithms list. Per the JA4 spec, GREASE codepoints must be excluded everywhere they appear — including in signature_algorithms.

getJA4ExtensionHash already applies isNotGrease to extension type IDs, and getJA4CipherHash, countCiphers, and countExtensions apply it to their respective inputs. The signature_algorithms collection loop was the sole exception, pushing every 16-bit codepoint from the extension body unconditionally.

Clients that inject a GREASE value into signature_algorithms (e.g. Chrome's per-handshake random GREASE selection, drawn from the 16 reserved code points 0x0a0a, 0x1a1a, …, 0xfafa) therefore produce a different JA4_c hash per connection while all other parts of the fingerprint are identical — resulting in many-to-one hash collapse when compared against spec-compliant implementations.

Fix

Guard the sig_algs.push_back(sig_alg) call in getJA4ExtensionHash with isNotGrease(sig_alg), matching the treatment already applied everywhere else in the file.

Test plan

  • Added TEST(JA4Fingerprinter, GreaseValueFilteredFromSignatureAlgorithms) in ja4_fingerprint_test.cc that constructs two `SSL_CLIENT_HELLO`s identical except for a GREASE codepoint (`0x0a0a`) in `signature_algorithms` and asserts `JA4Fingerprinter::create` produces the same fingerprint for both.
  • `bazel test //test/extensions/filters/listener/tls_inspector:ja4_fingerprint_test` passes locally.

Notes

  • No existing entry in the `JA4_TEST_VECTORS` table in `tls_inspector_ja4_test.cc` contains a GREASE codepoint inside its `signature_algorithms` extension body — all GREASE bytes in those captures live in extensions/versions/keyshare slots, which are already filtered correctly — so its pinned expected hashes remain valid.
  • This change was drafted with AI assistance and reviewed by the author.

The JA4 fingerprint's third component hashes the ClientHello's extension
list joined to its signature_algorithms list. Per the JA4 spec, GREASE
codepoints must be excluded everywhere they appear -- including in
signature_algorithms. `getJA4ExtensionHash` already applies `isNotGrease`
to extension type IDs, and `getJA4CipherHash`, `countCiphers`, and
`countExtensions` apply it to their respective inputs. The
signature_algorithms collection loop was the sole exception, pushing every
16-bit codepoint from the extension body unconditionally.

Clients that inject a GREASE value into signature_algorithms (e.g.
Chrome's per-handshake random GREASE selection) therefore produce a
different JA4_c hash per connection while all other parts of the
fingerprint are identical -- resulting in many-to-one hash collapse when
compared to spec-compliant implementations.

Guard the `sig_algs.push_back` call with `isNotGrease(sig_alg)`, matching
the treatment already applied elsewhere in the file. Adds a regression
test that asserts JA4 fingerprint equality between two ClientHellos that
differ only in the presence of a GREASE codepoint in signature_algorithms.

Spec reference:
https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md

Note: this change was drafted with AI assistance and reviewed by the author.
Signed-off-by: dangle <dangle@pinterest.com>
@repokitteh-read-only

Copy link
Copy Markdown

Hi @dangle1, welcome and thank you for your contribution.

We will try to review your Pull Request as quickly as possible.

In the meantime, please take a look at the contribution guidelines if you have not done so already.

🐱

Caused by: #46658 was opened by dangle1.

see: more, trace.

Extend the JA4 GREASE-in-signature_algorithms fix with two integration
tests that exercise the full wire-bytes -> ClientHello parse -> fingerprint
pipeline:

1. In tls_inspector_ja4_test.cc, add a new JA4_TEST_VECTORS entry
   "Browser-1-Grease-SigAlg" derived from the existing Browser-1 capture.
   It injects one GREASE codepoint (0x0a0a) at the head of the
   signature_algorithms extension body and shrinks the trailing padding
   extension by the same 2 bytes to preserve outer record and handshake
   lengths. The expected fingerprint matches Browser-1's exactly, so the
   parameterized suite proves the GREASE codepoint is stripped at every
   layer of the listener-filter pipeline (mocked recv -> Filter -> ssl
   early callback -> JA4Fingerprinter::create -> setJA4Hash).

2. In ja4_fingerprint_test.cc, add a companion test that decodes the same
   captured hex, uses BoringSSL's SSL_parse_client_hello to populate an
   SSL_CLIENT_HELLO, and asserts JA4Fingerprinter::create returns the
   expected spec-conformant fingerprint. Requires adding an ssl external
   dep and //source/common/ssl:ssl_lib for the SSL_SELECT macro.

Both tests fail without the sig-alg GREASE-filter fix in getJA4ExtensionHash.

Note: this change was drafted with AI assistance and reviewed by the author.
Signed-off-by: dangle <dangle@pinterest.com>
Comment on lines +245 to +247
if (isNotGrease(sig_alg)) {
sig_algs.push_back(sig_alg);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth runtime guarding in-case it breaks anyone operationally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants