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;
 };
 
 
openSUSE Build Service is sponsored by