diff --git a/changelogs/current/bug_fixes/tls__fixed-ja4-grease-filtering-on-signature-algorithms.rst b/changelogs/current/bug_fixes/tls__fixed-ja4-grease-filtering-on-signature-algorithms.rst new file mode 100644 index 000000000000..452f50038ab8 --- /dev/null +++ b/changelogs/current/bug_fixes/tls__fixed-ja4-grease-filtering-on-signature-algorithms.rst @@ -0,0 +1,5 @@ +Fixed a bug in the TLS inspector's ``JA4`` fingerprint implementation where GREASE values in the +``signature_algorithms`` extension were included in the ``JA4_c`` hash input, contrary to the +`JA4 specification `_. +GREASE values in ``signature_algorithms`` are now excluded, matching the treatment already applied +to cipher suites, extension type IDs, and supported versions. diff --git a/source/extensions/filters/listener/tls_inspector/ja4_fingerprint.cc b/source/extensions/filters/listener/tls_inspector/ja4_fingerprint.cc index 454c6288be7a..235bf9a9f6af 100644 --- a/source/extensions/filters/listener/tls_inspector/ja4_fingerprint.cc +++ b/source/extensions/filters/listener/tls_inspector/ja4_fingerprint.cc @@ -242,7 +242,9 @@ std::string JA4Fingerprinter::getJA4ExtensionHash(const SSL_CLIENT_HELLO* ssl_cl if (!CBS_get_u16(&sig_alg_data, &sig_alg)) { break; } - sig_algs.push_back(sig_alg); + if (isNotGrease(sig_alg)) { + sig_algs.push_back(sig_alg); + } } } } diff --git a/test/extensions/filters/listener/tls_inspector/BUILD b/test/extensions/filters/listener/tls_inspector/BUILD index 2e3b66fcb16f..e1261ba73311 100644 --- a/test/extensions/filters/listener/tls_inspector/BUILD +++ b/test/extensions/filters/listener/tls_inspector/BUILD @@ -19,10 +19,12 @@ envoy_package() envoy_cc_test( name = "ja4_fingerprint_test", srcs = ["ja4_fingerprint_test.cc"], + external_deps = ["ssl"], rbe_pool = "6gig", deps = [ ":tls_utility_lib", "//source/common/common:hex_lib", + "//source/common/ssl:ssl_lib", "//source/extensions/filters/listener/tls_inspector:ja4_fingerprint_lib", ], ) diff --git a/test/extensions/filters/listener/tls_inspector/ja4_fingerprint_test.cc b/test/extensions/filters/listener/tls_inspector/ja4_fingerprint_test.cc index 4bf0a2fa8d7e..102c2592fb92 100644 --- a/test/extensions/filters/listener/tls_inspector/ja4_fingerprint_test.cc +++ b/test/extensions/filters/listener/tls_inspector/ja4_fingerprint_test.cc @@ -1,11 +1,15 @@ #include "source/extensions/filters/listener/tls_inspector/ja4_fingerprint.h" +#include "source/common/common/hex.h" +#include "source/common/ssl/ssl.h" + #include "test/extensions/filters/listener/tls_inspector/tls_utility.h" #include "absl/strings/match.h" #include "absl/strings/string_view.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "openssl/ssl.h" namespace Envoy { namespace Extensions { @@ -25,6 +29,118 @@ TEST(JA4Fingerprinter, GreaseValueFiltering) { EXPECT_TRUE(JA4Fingerprinter::isNotGrease(0xffff)); // Not a GREASE value } +// Build a signature_algorithms extension body containing the given list of +// 16-bit sig-alg codepoints. Layout: 2-byte list length prefix followed by +// the codepoints (big-endian). +std::vector buildSigAlgsExtensionBody(const std::vector& sig_algs) { + const uint16_t list_len = static_cast(sig_algs.size() * 2); + std::vector body; + body.push_back(static_cast(list_len >> 8)); + body.push_back(static_cast(list_len & 0xff)); + for (uint16_t s : sig_algs) { + body.push_back(static_cast(s >> 8)); + body.push_back(static_cast(s & 0xff)); + } + return body; +} + +// Build a TLS extensions blob containing a single signature_algorithms extension +// (type 0x000d) whose body is |sig_algs_body|. +std::vector buildExtensionsWithSigAlgs(const std::vector& sig_algs_body) { + const uint16_t ext_type = TLSEXT_TYPE_signature_algorithms; + const uint16_t ext_len = static_cast(sig_algs_body.size()); + std::vector exts; + exts.push_back(static_cast(ext_type >> 8)); + exts.push_back(static_cast(ext_type & 0xff)); + exts.push_back(static_cast(ext_len >> 8)); + exts.push_back(static_cast(ext_len & 0xff)); + exts.insert(exts.end(), sig_algs_body.begin(), sig_algs_body.end()); + return exts; +} + +// End-to-end wire-format regression test for the GREASE-in-signature_algorithms +// bug. Parses a captured TLS record + handshake header + ClientHello body from +// hex using BoringSSL's SSL_parse_client_hello (the same code path Envoy hits +// when it feeds ClientHello bytes into JA4Fingerprinter::create through the +// listener filter), then asserts the resulting JA4 fingerprint matches the +// expected spec-conformant value. +// +// The vector is Browser-1's captured ClientHello from tls_inspector_ja4_test.cc +// with a single GREASE codepoint (0x0a0a) injected into the signature_algorithms +// extension body and the padding extension shrunk by the same 2 bytes to keep +// outer record/handshake lengths intact. Post-fix, JA4_c must match Browser-1's +// exactly, because GREASE stripping makes the sig-alg preimage identical. +TEST(JA4Fingerprinter, EndToEndGreaseSigAlgProducesExpectedFingerprint) { + // TLS record header (5 bytes) + handshake header (4 bytes) + ClientHello body. + const std::string wire_hex = + "1603010200010001fc0303528b4e00213672e534980dfed836dd5b375ab164dcc65ba6a3c87e7e2a1f9d61201bf29c9" + "dffaa31ed2df524d3a113edb4e6fd3b7fb3d6d57d5d9aafb213e83c420020aaaa130113021303c02bc02fc02cc030cc" + "a9cca8c013c014009c009d002f0035010001936a6a0000000d001400120a0a040308040401050308050501080606010" + "00000170015000012656467652e6d6963726f736f66742e636f6d000a000a00084a4a001d0017001800050005010000" + "0000000b00020100002b0007069a9a03040303001b0003020002ff010001000033002b00294a4a000100001d0020c4c" + "e4268d58f0c703855163f4754b883742487a5ce87a6016a30208c18e07f69446900050003026832002d000201010023" + "000000170000001200000010000e000c02683208687474702f312e310a0a000100001500c3000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + const std::vector wire = Hex::decode(wire_hex); + + // ClientHello body starts after the 5-byte TLS record header and 4-byte + // handshake header. SSL_parse_client_hello expects the body starting at the + // legacy_version field. + ASSERT_GT(wire.size(), 9u); + const uint8_t* body = wire.data() + 9; + const size_t body_len = wire.size() - 9; + + bssl::UniquePtr ctx(SSL_CTX_new(TLS_method())); + ASSERT_TRUE(ctx != nullptr); + bssl::UniquePtr ssl(SSL_new(ctx.get())); + ASSERT_TRUE(ssl != nullptr); + + SSL_CLIENT_HELLO client_hello; + ASSERT_EQ(1, SSL_parse_client_hello(ssl.get(), &client_hello, body, body_len)); + + const std::string fingerprint = JA4Fingerprinter::create(&client_hello); + + // Same expected fingerprint as Browser-1 in tls_inspector_ja4_test.cc: the + // injected GREASE codepoint must not affect JA4_c. + EXPECT_EQ(fingerprint, SSL_SELECT("t13d1516h2_8daaf6152771_e5627efa2ab1", + "t13d1515h2_8daaf6152771_de4a06bb82e3")); +} + +// Regression test for the JA4 spec's requirement that GREASE values be excluded +// from the signature_algorithms input to the JA4_c hash. A ClientHello with a +// GREASE codepoint inserted into signature_algorithms must produce the same +// JA4 fingerprint as an otherwise-identical ClientHello without it. See +// https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md +TEST(JA4Fingerprinter, GreaseValueFilteredFromSignatureAlgorithms) { + const std::vector sig_algs_no_grease = {0x0403, 0x0804}; + const std::vector sig_algs_with_grease = {0x0a0a, 0x0403, 0x0804}; + + const auto exts_no_grease = + buildExtensionsWithSigAlgs(buildSigAlgsExtensionBody(sig_algs_no_grease)); + const auto exts_with_grease = + buildExtensionsWithSigAlgs(buildSigAlgsExtensionBody(sig_algs_with_grease)); + + // Two 16-bit codepoints for TLS 1.2 ECDHE-RSA-AES-128-GCM-SHA256 and -AES-256-GCM-SHA384. + const std::vector ciphers = {0xc0, 0x2f, 0xc0, 0x30}; + + SSL_CLIENT_HELLO hello_no_grease{}; + hello_no_grease.version = TLS1_2_VERSION; + hello_no_grease.cipher_suites = ciphers.data(); + hello_no_grease.cipher_suites_len = ciphers.size(); + hello_no_grease.extensions = exts_no_grease.data(); + hello_no_grease.extensions_len = exts_no_grease.size(); + + SSL_CLIENT_HELLO hello_with_grease = hello_no_grease; + hello_with_grease.extensions = exts_with_grease.data(); + hello_with_grease.extensions_len = exts_with_grease.size(); + + EXPECT_EQ(JA4Fingerprinter::create(&hello_no_grease), + JA4Fingerprinter::create(&hello_with_grease)); +} + // This will test the ``JA4`` fingerprinting integration with the TLS Inspector code class TlsInspectorJA4IntegrationTest : public testing::Test { public: diff --git a/test/extensions/filters/listener/tls_inspector/tls_inspector_ja4_test.cc b/test/extensions/filters/listener/tls_inspector/tls_inspector_ja4_test.cc index bf20a218687a..58959ea20b14 100644 --- a/test/extensions/filters/listener/tls_inspector/tls_inspector_ja4_test.cc +++ b/test/extensions/filters/listener/tls_inspector/tls_inspector_ja4_test.cc @@ -256,6 +256,30 @@ const std::vector> JA4_TEST_VE "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" "00000000000", SSL_SELECT("t13d1516bd_8daaf6152771_e5627efa2ab1", "t13d1515bd_8daaf6152771_de4a06bb82e3")}, + + // Regression coverage for the GREASE-in-signature_algorithms bug (see + // https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md). This is + // Browser-1's captured ClientHello with a single GREASE codepoint (0x0a0a) + // injected at the head of the signature_algorithms list. The trailing padding + // extension is shrunk by 2 bytes to keep outer record/handshake lengths intact. + // + // Since the JA4 spec requires GREASE codepoints to be excluded everywhere they + // appear, the resulting fingerprint (including JA4_c) must exactly match the + // baseline Browser-1 fingerprint above -- the GREASE sig-alg must not affect + // the hash. Without the fix, JA4_c would drift to a different value. + {"Browser-1-Grease-SigAlg", + "1603010200010001fc0303528b4e00213672e534980dfed836dd5b375ab164dcc65ba6a3c87e7e2a1f9d61201bf29c9" + "dffaa31ed2df524d3a113edb4e6fd3b7fb3d6d57d5d9aafb213e83c420020aaaa130113021303c02bc02fc02cc030cc" + "a9cca8c013c014009c009d002f0035010001936a6a0000000d001400120a0a040308040401050308050501080606010" + "00000170015000012656467652e6d6963726f736f66742e636f6d000a000a00084a4a001d0017001800050005010000" + "0000000b00020100002b0007069a9a03040303001b0003020002ff010001000033002b00294a4a000100001d0020c4c" + "e4268d58f0c703855163f4754b883742487a5ce87a6016a30208c18e07f69446900050003026832002d000201010023" + "000000170000001200000010000e000c02683208687474702f312e310a0a000100001500c3000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + SSL_SELECT("t13d1516h2_8daaf6152771_e5627efa2ab1", "t13d1515h2_8daaf6152771_de4a06bb82e3")}, }; class TlsInspectorJA4Test