File nginx-ja4.patch of Package nginx
diff -ru nginx-1.29.5.old/src/event/ngx_event_openssl.c nginx-1.29.5.new/src/event/ngx_event_openssl.c
--- nginx-1.29.5.old/src/event/ngx_event_openssl.c 2026-02-04 16:12:20.000000000 +0100
+++ nginx-1.29.5.new/src/event/ngx_event_openssl.c 2026-03-14 01:25:40.445705643 +0100
@@ -1971,6 +1971,417 @@
}
+#ifdef OPENSSL_IS_BORINGSSL
+#define __DEFER__(F, V) [[gnu::always_inline]] inline auto void F(int*); \
+ [[gnu::cleanup(F)]] int V; auto void F(int*)
+
+#define defer __DEFER(__COUNTER__)
+#define __DEFER(N) __DEFER_(N)
+#define __DEFER_(N) __DEFER__(__DEFER_FUNCTION_ ## N, __DEFER_VARIABLE_ ## N)
+
+#define noticef(fmt, ...) \
+ ngx_log_error(NGX_LOG_NOTICE, conn->log, 0, fmt __VA_OPT__(,) __VA_ARGS__)
+#define errf(fmt, ...) \
+ ngx_log_error(NGX_LOG_ERR, conn->log, 0, fmt __VA_OPT__(,) __VA_ARGS__)
+
+[[gnu::always_inline]]
+inline static bool
+fp_isgrease(uint16_t id)
+{
+ return (((id) & 0x0f0f) == 0x0a0a && ((id) & 0xff) == ((id) >> 8));
+}
+
+static const u_char hex[] = "0123456789abcdef";
+
+static int
+fp_sort_uint16(const void *p1, const void *p2)
+{
+ uint16_t u1 = *(const uint16_t *)p1;
+ uint16_t u2 = *(const uint16_t *)p2;
+
+ if (u1 > u2)
+ return 1;
+ if (u1 < u2)
+ return -1;
+ return 0;
+}
+
+typedef struct fp_hash_ctx_t {
+ ngx_connection_t *conn;
+ EVP_MD_CTX *md_ctx;
+ const EVP_MD *digest;
+ bool init;
+} fp_hash_ctx_t;
+
+
+inline static int
+fp_digest(fp_hash_ctx_t *ctx, const uint16_t *data, size_t num_data,
+ u_char **out, bool finalize)
+{
+ bool init = ctx->init;
+ ngx_connection_t *conn = ctx->conn;
+ if (!init) {
+ if (!EVP_DigestInit_ex(ctx->md_ctx, ctx->digest, NULL)) {
+ errf("could not initialize digest context");
+ return NGX_ERROR;
+ }
+ ctx->init = true;
+ }
+
+ for (size_t i = 0; i < num_data; i++) {
+ uint16_t u = data[i];
+ const u_char s[5] = {
+ (!init || i > 0) ? ',' : '_',
+ hex[(u>>12)&0xf],
+ hex[(u>>8)&0xf],
+ hex[(u>>4)&0xf],
+ hex[u&0xf]
+ };
+ size_t start = (i == 0 && !init), len = 5 - (i == 0 && !init);
+
+ if (!EVP_DigestUpdate(ctx->md_ctx, s + start, len)) {
+ errf("digest update failed (%uz / %uz)", i, num_data);
+ return NGX_ERROR;
+ }
+ }
+
+ if (finalize) {
+ u_char digest_buf[EVP_MAX_MD_SIZE];
+ unsigned int digest_len;
+ if (!EVP_DigestFinal_ex(ctx->md_ctx, digest_buf, &digest_len)) {
+ errf("could not finalize digest (had %uz items)", num_data);
+ return NGX_ERROR;
+ }
+
+ ctx->init = false;
+ ngx_hex_dump(*out, digest_buf, 6);
+ *out += 12;
+ }
+
+ return NGX_OK;
+}
+
+inline static size_t
+fp_count_cipher_suites(const SSL_CLIENT_HELLO *hello)
+{
+ ngx_connection_t *conn = ngx_ssl_get_connection(hello->ssl);
+ CBS cbs;
+ size_t num_suites = 0;
+
+ CBS_init(&cbs, hello->cipher_suites, hello->cipher_suites_len);
+ while (CBS_len(&cbs) > 0) {
+ uint16_t id;
+ if (!CBS_get_u16(&cbs, &id))
+ break;
+ num_suites += !fp_isgrease(id);
+ }
+
+ return num_suites;
+}
+
+inline static int
+fp_hash_suites(const SSL_CLIENT_HELLO *hello, fp_hash_ctx_t *ctx,
+ size_t num_suites, u_char **out, u_char **out_o)
+{
+ CBS cbs;
+ CBS_init(&cbs, hello->cipher_suites, hello->cipher_suites_len);
+ uint16_t suites[num_suites], suites_o[num_suites];
+ for (size_t i = 0; CBS_len(&cbs) > 0;) {
+ uint16_t id;
+ if (!CBS_get_u16(&cbs, &id))
+ break;
+ if (!fp_isgrease(id)) {
+ suites[i] = id;
+ suites_o[i] = id;
+ i++;
+ }
+ }
+ ngx_qsort(suites, num_suites, sizeof(uint16_t), fp_sort_uint16);
+
+ if (fp_digest(ctx, suites, num_suites, out, true) != NGX_OK)
+ return NGX_ERROR;
+ if (fp_digest(ctx, suites_o, num_suites, out_o, true) != NGX_OK)
+ return NGX_ERROR;
+
+ return NGX_OK;
+}
+
+inline static void
+fp_count_exts(const SSL_CLIENT_HELLO *hello,
+ size_t *num_exts, size_t *num_exts_o)
+{
+ CBS cbs;
+ *num_exts = 0;
+ *num_exts_o = 0;
+
+ CBS_init(&cbs, hello->extensions, hello->extensions_len);
+ while (CBS_len(&cbs) > 0) {
+ uint16_t id;
+ CBS ext;
+
+ if (!CBS_get_u16(&cbs, &id)
+ || !CBS_get_u16_length_prefixed(&cbs, &ext))
+ {
+ break;
+ }
+
+ *num_exts_o += !fp_isgrease(id);
+ *num_exts += !fp_isgrease(id)
+ && id != TLSEXT_TYPE_application_layer_protocol_negotiation
+ && id != TLSEXT_TYPE_server_name;
+ }
+}
+
+inline static void
+fp_get_exts(const SSL_CLIENT_HELLO *hello,
+ uint16_t *exts, size_t num_exts,
+ uint16_t *exts_o, size_t num_exts_o)
+{
+ CBS cbs;
+ CBS_init(&cbs, hello->extensions, hello->extensions_len);
+ size_t ie = 0, ieo = 0;
+ while (CBS_len(&cbs) > 0) {
+ uint16_t id;
+ CBS ext;
+
+ if (!CBS_get_u16(&cbs, &id)
+ || !CBS_get_u16_length_prefixed(&cbs, &ext))
+ {
+ break;
+ }
+
+ if (!fp_isgrease(id)) {
+ if (id != TLSEXT_TYPE_application_layer_protocol_negotiation
+ && id != TLSEXT_TYPE_server_name) {
+ exts[ie++] = id;
+ }
+ exts_o[ieo++] = id;
+ }
+ }
+ if (num_exts > 0) {
+ ngx_qsort(exts, num_exts, sizeof(uint16_t), fp_sort_uint16);
+ }
+}
+
+inline static const u_char*
+fp_get_version(ngx_connection_t *conn, const SSL_CLIENT_HELLO *hello)
+{
+ const u_char *versions_ext = nullptr;
+ size_t versions_ext_len = 0;
+ uint16_t version = hello->version;
+
+ if (SSL_early_callback_ctx_extension_get(
+ hello, TLSEXT_TYPE_supported_versions,
+ &versions_ext, &versions_ext_len))
+ {
+ CBS ext, vers;
+ CBS_init(&ext, versions_ext, versions_ext_len);
+ if (CBS_get_u8_length_prefixed(&ext, &vers)) {
+ while (CBS_len(&vers) > 0) {
+ uint16_t v;
+ if (!CBS_get_u16(&vers, &v))
+ break;
+
+ if (!fp_isgrease(v) && v > version)
+ version = v;
+ }
+ }
+ }
+
+ const u_char *tls_version = (u_char*)"00";
+ switch (version) {
+#define TV(val, ver) case val: tls_version = (u_char*) ver; break;
+ TV(SSL3_VERSION, "s3")
+ TV(TLS1_VERSION, "10")
+ TV(TLS1_1_VERSION, "11")
+ TV(TLS1_2_VERSION, "12")
+ TV(TLS1_3_VERSION, "13")
+ TV(DTLS1_VERSION, "d1")
+ TV(DTLS1_2_VERSION, "d2")
+ TV(DTLS1_3_VERSION, "d3")
+#undef TV
+ default:
+ noticef("unexpected TLS version (%04uxd)", version);
+ }
+
+ return tls_version;
+}
+
+inline static void
+fp_get_alpn(const SSL_CLIENT_HELLO *hello, u_char *first_alpn)
+{
+ const u_char *alpn_ext = nullptr;
+ size_t alpn_ext_len = 0;
+ first_alpn[0] = '0';
+ first_alpn[1] = '0';
+ if (SSL_early_callback_ctx_extension_get(
+ hello, TLSEXT_TYPE_application_layer_protocol_negotiation,
+ &alpn_ext, &alpn_ext_len))
+ {
+ CBS ext, alpn;
+ CBS_init(&ext, alpn_ext, alpn_ext_len);
+ if (CBS_get_u16_length_prefixed(&ext, &alpn)) {
+ CBS str;
+ if (CBS_get_u8_length_prefixed(&alpn, &str)) {
+ u_char af = (u_char)str.data[0];
+ u_char al = (u_char)str.data[str.len-1];
+ if (isalnum(af) && isalnum(al)){
+ first_alpn[0] = af;
+ first_alpn[1] = al;
+ } else {
+ first_alpn[0] = hex[(af >> 4) & 0xf];
+ first_alpn[1] = hex[al & 0xf];
+ }
+ }
+ }
+ }
+}
+
+int
+ngx_client_hello_fprint(const SSL_CLIENT_HELLO *hello)
+{
+ ngx_connection_t *conn = ngx_ssl_get_connection(hello->ssl);
+ if (conn == nullptr || conn->ssl == nullptr) {
+ return 1;
+ }
+ char *log_prev = conn->log->action;
+ defer { conn->log->action = log_prev; }
+ conn->log->action = "JA4 TLS client fingerprinting";
+
+ static const
+ ngx_str_t init = ngx_string("0000000000_000000000000_000000000000");
+
+ ngx_str_t *ja4 = &conn->ssl->fp_ja4;
+ ja4->len = init.len;
+ ja4->data = ngx_pnalloc(conn->pool, ja4->len);
+ if (ja4->data == nullptr) {
+ errf("could not allocate JA4 string (size %uz)", ja4->len);
+ return 1;
+ }
+ ngx_memcpy(ja4->data, init.data, init.len);
+
+ ngx_str_t *ja4o = &conn->ssl->fp_ja4_o;
+ ja4o->len = init.len;
+ ja4o->data = ngx_pnalloc(conn->pool, ja4o->len);
+ if (ja4o->data == nullptr) {
+ errf("could not allocate JA4_o string (size %uz)", ja4o->len);
+ return 1;
+ }
+ ngx_memcpy(ja4o->data, init.data, init.len);
+
+ u_char *p = ja4->data, *po = ja4o->data;
+
+ // protocol. we don’t know if it’s QUIC at this stage
+ *p++ = (u_char)(SSL_is_dtls(hello->ssl) ? 'd' : 't');
+
+ // version
+ p = ngx_cpymem(p, fp_get_version(conn, hello), 2);
+
+ // sni
+ const u_char *out;
+ size_t out_len;
+ if (SSL_early_callback_ctx_extension_get(
+ hello, TLSEXT_TYPE_server_name, &out, &out_len)) {
+ *p++ = 'd';
+ } else {
+ *p++ = 'i';
+ }
+
+ // cipher suites and extensions
+ size_t num_suites = fp_count_cipher_suites(hello);
+ size_t num_exts, num_exts_o;
+ fp_count_exts(hello, &num_exts, &num_exts_o);
+
+ ngx_snprintf(p, 4, "%02uz%02uz",
+ num_suites < 100 ? num_suites : 0,
+ num_exts < 100 ? num_exts : 0);
+ p += 4;
+
+ // alpn
+ u_char alpn[2];
+ fp_get_alpn(hello, alpn);
+ p = ngx_cpymem(p, alpn, 2);
+
+ // _
+ p++;
+ po = ngx_cpymem(po, ja4->data, 11);
+
+ fp_hash_ctx_t hctx = {
+ .conn = conn,
+ .md_ctx = EVP_MD_CTX_new(),
+ .digest = EVP_sha256(),
+ .init = false
+ };
+ if (hctx.md_ctx == nullptr) {
+ errf("failed to create digest context");
+ return 1;
+ }
+
+ defer { EVP_MD_CTX_free(hctx.md_ctx); }
+
+ // cipher suites, hashed
+ if (num_suites > 0)
+ fp_hash_suites(hello, &hctx, num_suites, &p, &po);
+
+ p++;
+ po++;
+
+ uint16_t exts[num_exts], exts_o[num_exts_o];
+ if (num_exts_o > 0) {
+ fp_get_exts(hello, exts, num_exts, exts_o, num_exts_o);
+ }
+
+ const u_char *sigalgs_ext = nullptr;
+ size_t sigalgs_ext_len = 0, num_sigalgs = 0;
+ CBS ext, algs;
+ if (SSL_early_callback_ctx_extension_get(
+ hello, TLSEXT_TYPE_signature_algorithms,
+ &sigalgs_ext, &sigalgs_ext_len))
+ {
+ CBS_init(&ext, sigalgs_ext, sigalgs_ext_len);
+ if (CBS_get_u16_length_prefixed(&ext, &algs)) {
+ while (CBS_len(&algs) > 0) {
+ uint16_t id;
+ if (!CBS_get_u16(&algs, &id))
+ break;
+ num_sigalgs += !fp_isgrease(id);
+ }
+ }
+ }
+ uint16_t sigalgs[num_sigalgs];
+ if (num_sigalgs > 0) {
+ CBS_init(&ext, sigalgs_ext, sigalgs_ext_len);
+ if (CBS_get_u16_length_prefixed(&ext, &algs)) {
+ for (size_t i = 0; CBS_len(&algs) > 0;) {
+ uint16_t id;
+ if (!CBS_get_u16(&algs, &id))
+ break;
+ if (!fp_isgrease(id))
+ sigalgs[i++] = id;
+ }
+ }
+ }
+
+ if (num_exts > 0 || num_sigalgs > 0) {
+ if (fp_digest(&hctx, exts, num_exts, &p, false))
+ return 1;
+ if (fp_digest(&hctx, sigalgs, num_sigalgs, &p, true))
+ return 1;
+ }
+ if (num_exts_o > 0 || num_sigalgs > 0) {
+ if (fp_digest(&hctx, exts_o, num_exts_o, &po, false))
+ return 1;
+ if (fp_digest(&hctx, sigalgs, num_sigalgs, &po, true))
+ return 1;
+ }
+
+ return 1;
+}
+#else
+#error JA4 patch requires BoringSSL
+#endif
+
+
ngx_int_t
ngx_ssl_set_client_hello_callback(ngx_ssl_t *ssl, ngx_ssl_client_hello_arg *cb)
{
@@ -1987,6 +2398,7 @@
#elif defined OPENSSL_IS_BORINGSSL
+ SSL_CTX_set_dos_protection_cb(ssl->ctx, ngx_client_hello_fprint);
SSL_CTX_set_select_certificate_cb(ssl->ctx, ngx_ssl_select_certificate);
if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_client_hello_arg_index, cb) == 0)
Only in nginx-1.29.5.new/src/event: ngx_event_openssl.c.orig
diff -ru nginx-1.29.5.old/src/event/ngx_event_openssl.h nginx-1.29.5.new/src/event/ngx_event_openssl.h
--- nginx-1.29.5.old/src/event/ngx_event_openssl.h 2026-02-04 16:12:20.000000000 +0100
+++ nginx-1.29.5.new/src/event/ngx_event_openssl.h 2026-03-11 23:29:33.711007155 +0100
@@ -144,6 +144,9 @@
unsigned early_preread:1;
unsigned write_blocked:1;
unsigned sni_accepted:1;
+
+ ngx_str_t fp_ja4;
+ ngx_str_t fp_ja4_o;
};