This is an automated email from the ASF dual-hosted git repository.

maskit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 3f6f207e7e Add BoringSSL support to the ja4_fingerprint plugin (#12914)
3f6f207e7e is described below

commit 3f6f207e7ee849cd97bedf78ce010a2edd22bf53
Author: Masakazu Kitajo <[email protected]>
AuthorDate: Tue Mar 3 18:23:31 2026 -0700

    Add BoringSSL support to the ja4_fingerprint plugin (#12914)
    
    * got client hello routed to plugins
    
    * Creates ja4 fingerprint with boringssl
    
    * cleanup a bit
    
    * make ssl_client_hello const
    
    * spaces cleanup
    
    * cleanup code
    
    * more cleanup
    
    * Update plugin.cc
    
    * Update plugin.cc
    
    * Update ts.h
    
    * Update apidefs.h.in
    
    * Update to make more clean
    
    * Update data
    
    * address comments
    
    * Update ja4_fingerprint.en.rst
    
    * Add docs
    
    * Update TSVConnClientHelloGet.en.rst
    
    * Update TSVConnClientHelloGet.en.rst
    
    * Address comments
    
    * Update TSClientHello.en.rst
    
    * Update TSClientHello.en.rst
    
    * Eliminate heap allocations
    
    * Address Copilot comments
    
    * Address Copilot comments
    
    * Update doxygen comments
    
    ---------
    
    Co-authored-by: Jasmine Emanouel <[email protected]>
---
 cmake/ExperimentalPlugins.cmake                    |  10 +-
 doc/admin-guide/plugins/index.en.rst               |   4 +
 doc/admin-guide/plugins/ja4_fingerprint.en.rst     | 209 +++++++++++++++++++++
 .../api/functions/TSVConnClientHelloGet.en.rst     |  50 +++++
 doc/developer-guide/api/types/TSClientHello.en.rst |  83 ++++++++
 include/iocore/net/TLSSNISupport.h                 |  45 ++++-
 include/ts/apidefs.h.in                            |  69 +++++++
 include/ts/ts.h                                    |  46 +++++
 plugins/experimental/ja4_fingerprint/README.md     |   2 +
 plugins/experimental/ja4_fingerprint/plugin.cc     |  75 ++++----
 src/api/InkAPI.cc                                  | 102 ++++++++++
 src/iocore/net/TLSSNISupport.cc                    | 102 ++++++++++
 12 files changed, 748 insertions(+), 49 deletions(-)

diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake
index 52313acfdd..764571546c 100644
--- a/cmake/ExperimentalPlugins.cmake
+++ b/cmake/ExperimentalPlugins.cmake
@@ -42,15 +42,7 @@ auto_option(HOOK_TRACE FEATURE_VAR BUILD_HOOK_TRACE DEFAULT 
${_DEFAULT})
 auto_option(HTTP_STATS FEATURE_VAR BUILD_HTTP_STATS DEFAULT ${_DEFAULT})
 auto_option(ICAP FEATURE_VAR BUILD_ICAP DEFAULT ${_DEFAULT})
 auto_option(INLINER FEATURE_VAR BUILD_INLINER DEFAULT ${_DEFAULT})
-auto_option(
-  JA4_FINGERPRINT
-  FEATURE_VAR
-  BUILD_JA4_FINGERPRINT
-  VAR_DEPENDS
-  HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
-  DEFAULT
-  ${_DEFAULT}
-)
+auto_option(JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS 
DEFAULT ${_DEFAULT})
 auto_option(
   MAGICK
   FEATURE_VAR
diff --git a/doc/admin-guide/plugins/index.en.rst 
b/doc/admin-guide/plugins/index.en.rst
index 1def2223a7..5379c9a72e 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -178,6 +178,7 @@ directory of the |TS| source tree. Experimental plugins can 
be compiled by passi
    Header Frequency <header_freq.en>
    Hook Trace <hook-trace.en>
    ICAP <icap.en>
+   JA4 Fingerprint <ja4_fingerprint.en>
    Maxmind ACL <maxmind_acl.en>
    Memcache <memcache.en>
    Memory Profile <memory_profile.en>
@@ -232,6 +233,9 @@ directory of the |TS| source tree. Experimental plugins can 
be compiled by passi
 :doc:`ICAP <icap.en>`
    Pass response data to external server for further processing using the ICAP 
protocol.
 
+:doc:`JA4 Fingerprint <ja4_fingerprint.en>`
+   Calculates JA4 Fingerprints for incoming TLS traffic.
+
 :doc:`MaxMind ACL <maxmind_acl.en>`
    ACL based on the maxmind geo databases (GeoIP2 mmdb and libmaxminddb)
 
diff --git a/doc/admin-guide/plugins/ja4_fingerprint.en.rst 
b/doc/admin-guide/plugins/ja4_fingerprint.en.rst
new file mode 100644
index 0000000000..b8e5e5e37a
--- /dev/null
+++ b/doc/admin-guide/plugins/ja4_fingerprint.en.rst
@@ -0,0 +1,209 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+
+.. include:: ../../common.defs
+
+.. _admin-plugins-ja4-fingerprint:
+
+JA4 Fingerprint Plugin
+**********************
+
+Description
+===========
+
+The JA4 Fingerprint plugin generates TLS client fingerprints based on the JA4
+algorithm designed by John Althouse. JA4 is the successor to the JA3
+fingerprinting algorithm and provides improved client identification for TLS
+connections.
+
+A JA4 fingerprint uniquely identifies TLS clients based on characteristics of
+their TLS ClientHello messages, including:
+
+* TLS version
+* ALPN (Application-Layer Protocol Negotiation) preferences
+* Cipher suites offered
+* TLS extensions present
+
+This information can be used for:
+
+* Client identification and tracking
+* Bot detection and mitigation
+* Security analytics and threat intelligence
+* Understanding client TLS implementation patterns
+
+How It Works
+============
+
+The plugin intercepts TLS ClientHello messages during the TLS handshake and
+generates a JA4 fingerprint consisting of three sections separated by 
underscores:
+
+**Section a (unhashed)**: Basic information about the client including:
+
+  * Protocol (``t`` for TCP, ``q`` for QUIC)
+  * TLS version
+  * SNI (Server Name Indication) status
+  * Number of cipher suites
+  * Number of extensions
+  * First ALPN value
+
+**Section b (hashed)**: A SHA-256 hash of the sorted cipher suite list
+
+**Section c (hashed)**: A SHA-256 hash of the sorted extension list
+
+Example fingerprint::
+
+    t13d1516h2_8daaf6152771_b186095e22b6
+
+Key Differences from JA3
+-------------------------
+
+* Cipher suites and extensions are sorted before hashing for consistency
+* SNI and ALPN information is included in the fingerprint
+* More resistant to fingerprint randomization
+
+Plugin Configuration
+====================
+
+The plugin operates as a global plugin and has no configuration options.
+
+To enable the plugin, add the following line to :file:`plugin.config`::
+
+    ja4_fingerprint.so
+
+No additional parameters are required or supported.
+
+Plugin Behavior
+===============
+
+When loaded, the plugin will:
+
+1. **Capture TLS ClientHello**: Intercepts all incoming TLS connections during
+   the ClientHello phase
+
+2. **Generate Fingerprint**: Calculates the JA4 fingerprint from the
+   ClientHello data
+
+3. **Log to File**: Writes the fingerprint and client IP address to
+   ``ja4_fingerprint.log``
+
+4. **Add HTTP Headers**: Injects the following headers into subsequent HTTP
+   requests on the same connection:
+
+   * ``ja4``: Contains the JA4 fingerprint
+   * ``x-ja4-via``: Contains the proxy name (from ``proxy.config.proxy_name``)
+
+Log Output
+==========
+
+The plugin writes to ``ja4_fingerprint.log`` in the Traffic Server log
+directory (typically ``/var/log/trafficserver/``).
+
+**Log Format**::
+
+    [timestamp] Client IP: <ip_address>    JA4: <fingerprint>
+
+**Example**::
+
+    [Jan 29 10:15:23.456] Client IP: 192.168.1.100    JA4: 
t13d1516h2_8daaf6152771_b186095e22b6
+    [Jan 29 10:15:24.123] Client IP: 10.0.0.50        JA4: 
t13d1715h2_8daaf6152771_02713d6af862
+
+Using JA4 Headers in Origin Requests
+=====================================
+
+Origin servers can access the JA4 fingerprint through the injected HTTP header.
+This allows the origin to:
+
+* Make access control decisions based on client fingerprints
+* Log fingerprints for security analysis
+* Track client populations and TLS implementation patterns
+
+The ``x-ja4-via`` header allows origin servers to track which Traffic Server
+proxy handled the request when multiple proxies are deployed.
+
+Debugging
+=========
+
+To enable debug logging for the plugin, set the following in 
:file:`records.yaml`::
+
+    records:
+      diags:
+        debug:
+          enabled: 1
+          tags: ja4_fingerprint
+
+Debug output will appear in :file:`diags.log` and includes:
+
+* ClientHello processing events
+* Fingerprint generation details
+* Header injection operations
+
+Requirements
+============
+
+* Traffic Server must be built with TLS support (OpenSSL or BoringSSL)
+* The plugin operates on all TLS connections
+
+Configuration Settings
+======================
+
+The plugin requires the ``proxy.config.proxy_name`` setting to be configured
+for the ``x-ja4-via`` header. If not set, the plugin will log an error and use
+"unknown" as the proxy name.
+
+To set the proxy name in :file:`records.yaml`::
+
+    records:
+      proxy:
+        config:
+          proxy_name: proxy01
+
+Limitations
+===========
+
+* The plugin only operates in global mode (no per-remap configuration)
+* Logging cannot be disabled
+* Raw (unhashed) cipher and extension lists are not logged
+* Non-TLS connections do not generate fingerprints
+
+See Also
+========
+
+* JA4 Technical Specification: 
https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md
+* JA4 is licensed under the BSD 3-Clause license
+
+Example Configuration
+=====================
+
+Complete example configuration for enabling JA4 fingerprinting:
+
+**plugin.config**::
+
+    ja4_fingerprint.so
+
+**records.yaml**::
+
+    records:
+      proxy:
+        config:
+          proxy_name: proxy-01
+      diags:
+        debug:
+          enabled: 1
+          tags: ja4_fingerprint
+
+After restarting Traffic Server, the plugin will begin fingerprinting TLS
+connections and logging to ``ja4_fingerprint.log``.
diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst 
b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst
new file mode 100644
index 0000000000..acfd96a91d
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst
@@ -0,0 +1,50 @@
+.. Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed
+   with this work for additional information regarding copyright
+   ownership.  The ASF licenses this file to you under the Apache
+   License, Version 2.0 (the "License"); you may not use this file
+   except in compliance with the License.  You may obtain a copy of
+   the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied.  See the License for the specific language governing
+   permissions and limitations under the License.
+
+.. include:: ../../../common.defs
+
+.. default-domain:: cpp
+
+TSVConnClientHelloGet
+*********************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/ts.h>
+
+.. function:: TSClientHello TSVConnClientHelloGet(TSVConn sslp)
+.. function:: TSReturnCode TSClientHelloExtensionGet(TSClientHello ch, 
unsigned int type, const unsigned char **out, size_t *outlen)
+
+Description
+===========
+
+:func:`TSVConnClientHelloGet` retrieves ClientHello message data from the TLS
+virtual connection :arg:`sslp`. Returns a :type:`TSClientHello` always. The 
availability
+of the returned object must be checked before use.
+
+.. important::
+
+   This function should only be called from the ``TS_EVENT_SSL_CLIENT_HELLO`` 
hook.
+   The returned :type:`TSClientHello` is only valid during the SSL ClientHello 
event processing.
+   Using this function from other hooks may result in accessing invalid or 
stale data.
+
+:func:`TSClientHelloExtensionGet` retrieves extension data for the specified
+:arg:`type` (e.g., ``0x10`` for ALPN). Returns :enumerator:`TS_SUCCESS` if
+found, :enumerator:`TS_ERROR` otherwise. The returned pointer in :arg:`out` is
+valid only while :arg:`ch` exists.
diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst 
b/doc/developer-guide/api/types/TSClientHello.en.rst
new file mode 100644
index 0000000000..a9fbcc981b
--- /dev/null
+++ b/doc/developer-guide/api/types/TSClientHello.en.rst
@@ -0,0 +1,83 @@
+.. Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed
+   with this work for additional information regarding copyright
+   ownership.  The ASF licenses this file to you under the Apache
+   License, Version 2.0 (the "License"); you may not use this file
+   except in compliance with the License.  You may obtain a copy of
+   the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied.  See the License for the specific language governing
+   permissions and limitations under the License.
+
+.. include:: ../../../common.defs
+
+.. default-domain:: cpp
+
+TSClientHello
+*************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+    #include <ts/apidefs.h>
+
+.. type:: TSClientHello
+
+.. type:: TSClientHello::TSExtensionTypeList
+
+   A type alias for an iterable container of extension type IDs.
+
+
+Description
+===========
+
+:type:`TSClientHello` is an opaque handle to a TLS ClientHello message sent by
+a client during the TLS handshake. It provides access to the client's TLS
+version, cipher suites, and extensions.
+
+The implementation abstracts differences between OpenSSL and BoringSSL to
+provide a consistent interface.
+
+Accessor Methods
+================
+
+The following methods are available to access ClientHello data:
+
+.. function:: bool is_available() const
+
+   Returns whether the object contains valid values. As long as
+   :func:`TSVConnClientHelloGet` is called for a TLS connection, the return
+   value should be `true`.
+
+.. function:: uint16_t get_version() const
+
+   Returns the TLS version from the ClientHello message.
+
+.. function:: const uint8_t* get_cipher_suites() const
+
+   Returns a pointer to the cipher suites buffer. The length is available via
+   :func:`get_cipher_suites_len()`.
+
+.. function:: size_t get_cipher_suites_len() const
+
+   Returns the length of the cipher suites buffer in bytes.
+
+.. function:: TSClientHello::TSExtensionTypeList get_extension_types() const
+
+   Returns an iterable container of extension type IDs present in the 
ClientHello.
+   This method abstracts the differences between BoringSSL (which uses an 
extensions
+   buffer) and OpenSSL (which uses an extension_ids array), providing a 
consistent
+   interface regardless of the SSL library in use.
+
+.. function:: void* _get_internal() const
+
+   Returns a pointer to internal implementation data. This is an internal 
accessor for advanced use
+   cases. This accessor is not part of the stable public API, and plugins must 
not cast or rely
+   on the returned pointer type.
diff --git a/include/iocore/net/TLSSNISupport.h 
b/include/iocore/net/TLSSNISupport.h
index 6897cce36a..22f3d751a8 100644
--- a/include/iocore/net/TLSSNISupport.h
+++ b/include/iocore/net/TLSSNISupport.h
@@ -23,6 +23,7 @@
  */
 #pragma once
 
+#include "tscore/ink_config.h"
 #include "tscore/ink_memory.h"
 #include "SSLTypes.h"
 
@@ -40,6 +41,40 @@ public:
   {
   public:
     ClientHello(ClientHelloContainer chc) : _chc(chc) {}
+    ~ClientHello();
+
+    class ExtensionIdIterator
+    {
+    public:
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+      ExtensionIdIterator(int *ids, size_t len, size_t offset) : 
_extensions(ids), _ext_len(len), _offset(offset) {}
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+      ExtensionIdIterator(const uint8_t *extensions, size_t len, size_t offset)
+        : _extensions(extensions), _ext_len(len), _offset(offset)
+      {
+      }
+#endif
+      ~ExtensionIdIterator() = default;
+
+      ExtensionIdIterator &operator++();
+      bool                 operator==(const ExtensionIdIterator &b) const;
+      int                  operator*() const;
+
+    private:
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+      int *_extensions;
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+      const uint8_t *_extensions;
+#endif
+      size_t _ext_len = 0;
+      size_t _offset  = 0;
+    };
+
+    uint16_t            getVersion();
+    std::string_view    getCipherSuites();
+    ExtensionIdIterator begin();
+    ExtensionIdIterator end();
+
     /**
      * @return 1 if successful
      */
@@ -47,6 +82,10 @@ public:
 
   private:
     ClientHelloContainer _chc;
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+    int   *_ext_ids = nullptr;
+    size_t _ext_len;
+#endif
   };
 
   virtual ~TLSSNISupport() = default;
@@ -56,7 +95,8 @@ public:
   static void           bind(SSL *ssl, TLSSNISupport *snis);
   static void           unbind(SSL *ssl);
 
-  int perform_sni_action(SSL &ssl);
+  int          perform_sni_action(SSL &ssl);
+  ClientHello *get_client_hello() const;
   // Callback functions for OpenSSL libraries
 
   /** Process a CLIENT_HELLO from a client.
@@ -114,5 +154,6 @@ private:
   // Null-terminated string, or nullptr if there is no SNI server name.
   std::unique_ptr<char[]> _sni_server_name;
 
-  void _set_sni_server_name_buffer(std::string_view name);
+  void         _set_sni_server_name_buffer(std::string_view name);
+  ClientHello *_ch = nullptr;
 };
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index c979ac8e94..86aff5202f 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -43,10 +43,13 @@
  */
 
 #include <cstdint>
+#include <memory>
+#include <vector>
 #include <netinet/in.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <tuple>
+#include <type_traits>
 
 /** Apply printf format string compile-time argument checking to a function.
  *
@@ -1049,6 +1052,72 @@ struct TSHttp2Priority {
   int32_t stream_dependency;
 };
 
+// Wrapper class that provides controlled access to client hello data
+class TSClientHello
+{
+public:
+  class TSExtensionTypeList
+  {
+  public:
+    TSExtensionTypeList(void *ch) : _ch(ch) {}
+
+    class Iterator
+    {
+    public:
+      Iterator(const void *ite);
+      Iterator &operator++();
+      bool      operator==(const Iterator &b) const;
+      int       operator*() const;
+
+    private:
+      char _real_iterator[24];
+    };
+
+    Iterator begin();
+    Iterator end();
+
+  private:
+    void *_ch;
+  };
+
+  TSClientHello(void *ch) : _client_hello(ch) {}
+
+  ~TSClientHello() = default;
+
+  explicit
+  operator bool() const
+  {
+    return _client_hello != nullptr;
+  }
+
+  bool is_available() const;
+
+  uint16_t get_version() const;
+
+  const uint8_t *get_cipher_suites() const;
+
+  size_t get_cipher_suites_len() const;
+
+  // Returns an iterable container of extension type IDs
+  // This abstracts the difference between BoringSSL (extensions buffer) and 
OpenSSL (extension_ids array)
+  TSExtensionTypeList
+  get_extension_types() const
+  {
+    return TSExtensionTypeList(_client_hello);
+  }
+
+  // Internal accessor for API implementation
+  void *
+  _get_internal() const
+  {
+    return _client_hello;
+  }
+
+private:
+  void *_client_hello;
+};
+static_assert(std::is_trivially_copyable_v<TSClientHello> == true);
+
 using TSFile = struct tsapi_file *;
 
 using TSMLoc          = struct tsapi_mloc *;
diff --git a/include/ts/ts.h b/include/ts/ts.h
index c63febb360..9b62d64fe8 100644
--- a/include/ts/ts.h
+++ b/include/ts/ts.h
@@ -1334,6 +1334,52 @@ int TSVConnIsSsl(TSVConn sslp);
 int         TSVConnProvidedSslCert(TSVConn sslp);
 const char *TSVConnSslSniGet(TSVConn sslp, int *length);
 
+/**
+    Retrieve TLS Client Hello information from an SSL virtual connection.
+
+    This function extracts TLS Client Hello data from a TLS handshake.
+    The returned object provides access to version, cipher suites, and 
extensions
+    in a way that is portable across both BoringSSL and OpenSSL 
implementations.
+
+    IMPORTANT: This function must be called during the 
TS_SSL_CLIENT_HELLO_HOOK.
+    The underlying SSL context may not be available at other hooks, 
particularly
+    for BoringSSL where the SSL_CLIENT_HELLO structure is only valid during
+    specific callback functions. Calling this function outside of the client
+    hello hook may result in unavailable object being returned.
+
+    @param[in] sslp The SSL virtual connection handle. Must not be nullptr.
+    @return A TSClientHello object containing Client Hello data.
+
+    @see TSClientHelloExtensionGet
+ */
+TSClientHello TSVConnClientHelloGet(TSVConn sslp);
+
+/**
+    Retrieve a specific TLS extension from the Client Hello.
+
+    This function looks up a TLS extension by its type (e.g., 0x10 for ALPN,
+    0x00 for SNI) and returns a pointer to its data. The lookup is performed
+    using SSL library-specific functions that work with both BoringSSL and
+    OpenSSL without requiring conditional compilation in the plugin.
+
+    The returned buffer is still owned by the underlying SSL context and must
+    not be freed by the caller. The buffer is valid only in the condition where
+    you can get a TSClientHello object from an SSL virtual connection.
+
+    @param[in]  ch The Client Hello object obtained from 
TSVConnClientHelloGet().
+    @param[in]  type The TLS extension type to retrieve.
+    @param[out] out Pointer to receive the extension data buffer. Must not be 
nullptr.
+    @param[out] outlen Pointer to receive the length of the extension data in 
bytes.
+                  Must not be nullptr.
+
+    @return TS_SUCCESS if the extension was found and retrieved successfully.
+            TS_ERROR if the extension is not present, or if any parameter is 
nullptr,
+            or if an error occurred during lookup.
+
+    @see TSVConnClientHelloGet
+ */
+TSReturnCode TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, 
const unsigned char **out, size_t *outlen);
+
 TSSslSession TSSslSessionGet(const TSSslSessionID *session_id);
 int          TSSslSessionGetBuffer(const TSSslSessionID *session_id, char 
*buffer, int *len_ptr);
 TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession 
add_session, TSSslConnection ssl_conn);
diff --git a/plugins/experimental/ja4_fingerprint/README.md 
b/plugins/experimental/ja4_fingerprint/README.md
index d45ddf0078..3de07907d2 100644
--- a/plugins/experimental/ja4_fingerprint/README.md
+++ b/plugins/experimental/ja4_fingerprint/README.md
@@ -21,6 +21,8 @@ The technical specification of the algorithm is available 
[here](https://github.
 
 These changes were made to simplify the plugin as much as possible. The 
missing features are useful and may be implemented in the future.
 
+This plugin works with BoringSSL as well.
+
 ## Logging and Debugging
 
 To get debug information in the traffic log, enable the debug tag 
`ja4_fingerprint`.
diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc 
b/plugins/experimental/ja4_fingerprint/plugin.cc
index 16419d4566..216171280f 100644
--- a/plugins/experimental/ja4_fingerprint/plugin.cc
+++ b/plugins/experimental/ja4_fingerprint/plugin.cc
@@ -54,13 +54,13 @@ static void               reserve_user_arg();
 static bool               create_log_file();
 static void               register_hooks();
 static int                handle_client_hello(TSCont cont, TSEvent event, void 
*edata);
-static std::string        get_fingerprint(SSL *ssl);
 char                     *get_IP(sockaddr const *s_sockaddr, char 
res[INET6_ADDRSTRLEN]);
 static void               log_fingerprint(JA4_data const *data);
-static std::uint16_t      get_version(SSL *ssl);
-static std::string        get_first_ALPN(SSL *ssl);
-static void               add_ciphers(JA4::TLSClientHelloSummary &summary, SSL 
*ssl);
-static void               add_extensions(JA4::TLSClientHelloSummary &summary, 
SSL *ssl);
+static std::string        get_fingerprint(TSClientHello ch);
+static std::uint16_t      get_version(TSClientHello ch);
+static std::string        get_first_ALPN(TSClientHello ch);
+static void               add_ciphers(JA4::TLSClientHelloSummary &summary, 
TSClientHello ch);
+static void               add_extensions(JA4::TLSClientHelloSummary &summary, 
TSClientHello ch);
 static std::string        hash_with_SHA256(std::string_view sv);
 static int                handle_read_request_hdr(TSCont cont, TSEvent event, 
void *edata);
 static void               append_JA4_headers(TSCont cont, TSHttpTxn txnp, 
std::string const *fingerprint);
@@ -77,7 +77,6 @@ constexpr std::string_view JA4_VIA_HEADER{"x-ja4-via"};
 
 constexpr unsigned int EXT_ALPN{0x10};
 constexpr unsigned int EXT_SUPPORTED_VERSIONS{0x2b};
-constexpr int          SSL_SUCCESS{1};
 
 DbgCtl dbg_ctl{PLUGIN_NAME};
 
@@ -198,13 +197,16 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent 
event, void *edata)
     // We ignore the event, but we don't want to reject the connection.
     return TS_SUCCESS;
   }
-  TSVConn const         ssl_vc{static_cast<TSVConn>(edata)};
-  TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)};
-  if (nullptr == ssl) {
-    Dbg(dbg_ctl, "Could not get SSL object.");
+
+  TSVConn const ssl_vc{static_cast<TSVConn>(edata)};
+
+  TSClientHello ch = TSVConnClientHelloGet(ssl_vc);
+
+  if (!ch) {
+    Dbg(dbg_ctl, "Could not get TSClientHello object.");
   } else {
     auto data{std::make_unique<JA4_data>()};
-    data->fingerprint = get_fingerprint(reinterpret_cast<SSL *>(ssl));
+    data->fingerprint = get_fingerprint(ch);
     get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr);
     log_fingerprint(data.get());
     // The VCONN_CLOSE handler is now responsible for freeing the resource.
@@ -215,14 +217,14 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent 
event, void *edata)
 }
 
 std::string
-get_fingerprint(SSL *ssl)
+get_fingerprint(TSClientHello ch)
 {
   JA4::TLSClientHelloSummary summary{};
   summary.protocol    = JA4::Protocol::TLS;
-  summary.TLS_version = get_version(ssl);
-  summary.ALPN        = get_first_ALPN(ssl);
-  add_ciphers(summary, ssl);
-  add_extensions(summary, ssl);
+  summary.TLS_version = get_version(ch);
+  summary.ALPN        = get_first_ALPN(ch);
+  add_ciphers(summary, ch);
+  add_extensions(summary, ch);
   std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)};
   return result;
 }
@@ -264,49 +266,52 @@ log_fingerprint(JA4_data const *data)
 }
 
 std::uint16_t
-get_version(SSL *ssl)
+get_version(TSClientHello ch)
 {
   unsigned char const *buf{};
   std::size_t          buflen{};
-  if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, 
&buf, &buflen)) {
+  if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS, 
&buf, &buflen)) {
     std::uint16_t max_version{0};
-    for (std::size_t i{1}; i < buflen; i += 2) {
-      std::uint16_t version{make_word(buf[i - 1], buf[i])};
-      if ((!JA4::is_GREASE(version)) && version > max_version) {
+    size_t        n_versions = buf[0];
+    for (size_t i = 1; i + 1 < buflen && i < (n_versions * 2) + 1; i += 2) {
+      std::uint16_t version = (buf[i] << 8) | buf[i + 1];
+      if (!JA4::is_GREASE(version) && version > max_version) {
         max_version = version;
       }
     }
     return max_version;
   } else {
     Dbg(dbg_ctl, "No supported_versions extension... using legacy version.");
-    return SSL_client_hello_get0_legacy_version(ssl);
+    return ch.get_version();
   }
 }
 
 std::string
-get_first_ALPN(SSL *ssl)
+get_first_ALPN(TSClientHello ch)
 {
   unsigned char const *buf{};
   std::size_t          buflen{};
   std::string          result{""};
-  if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) {
+  if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_ALPN, &buf, &buflen)) {
     // The first two bytes are a 16bit encoding of the total length.
     unsigned char first_ALPN_length{buf[2]};
     TSAssert(buflen > 4);
     TSAssert(0 != first_ALPN_length);
     result.assign(&buf[3], (&buf[3]) + first_ALPN_length);
   }
+
   return result;
 }
 
 void
-add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl)
+add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch)
 {
-  unsigned char const *buf{};
-  std::size_t          buflen{SSL_client_hello_get0_ciphers(ssl, &buf)};
+  const uint8_t *buf    = ch.get_cipher_suites();
+  size_t         buflen = ch.get_cipher_suites_len();
+
   if (buflen > 0) {
-    for (std::size_t i{1}; i < buflen; i += 2) {
-      summary.add_cipher(make_word(buf[i], buf[i - 1]));
+    for (std::size_t i = 0; i + 1 < buflen; i += 2) {
+      summary.add_cipher(make_word(buf[i], buf[i + 1]));
     }
   } else {
     Dbg(dbg_ctl, "Failed to get ciphers.");
@@ -314,16 +319,11 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl)
 }
 
 void
-add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl)
+add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch)
 {
-  int        *buf{};
-  std::size_t buflen{};
-  if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, 
&buflen)) {
-    for (std::size_t i{1}; i < buflen; i += 2) {
-      summary.add_extension(make_word(buf[i], buf[i - 1]));
-    }
+  for (auto ext_type : ch.get_extension_types()) {
+    summary.add_extension(ext_type);
   }
-  OPENSSL_free(buf);
 }
 
 std::string
@@ -443,7 +443,6 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent 
event, void *edata)
     // We ignore the event, but we don't want to reject the connection.
     return TS_SUCCESS;
   }
-
   TSVConn const ssl_vc{static_cast<TSVConn>(edata)};
   delete static_cast<std::string *>(TSUserArgGet(ssl_vc, 
*get_user_arg_index()));
   TSUserArgSet(ssl_vc, *get_user_arg_index(), nullptr);
diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc
index 1a4b734e28..529ff6d202 100644
--- a/src/api/InkAPI.cc
+++ b/src/api/InkAPI.cc
@@ -31,6 +31,7 @@
 #include "iocore/net/NetVConnection.h"
 #include "iocore/net/NetHandler.h"
 #include "iocore/net/UDPNet.h"
+#include "tscore/ink_config.h"
 #include "tscore/ink_platform.h"
 #include "tscore/ink_base64.h"
 #include "tscore/Encoding.h"
@@ -67,6 +68,7 @@
 #include "iocore/net/SSLAPIHooks.h"
 #include "iocore/net/SSLDiags.h"
 #include "iocore/net/TLSBasicSupport.h"
+#include "iocore/net/TLSSNISupport.h"
 #include "iocore/eventsystem/ConfigProcessor.h"
 #include "proxy/Plugin.h"
 #include "proxy/logging/LogObject.h"
@@ -7920,6 +7922,37 @@ TSVConnSslSniGet(TSVConn sslp, int *length)
   return server_name;
 }
 
+TSClientHello
+TSVConnClientHelloGet(TSVConn sslp)
+{
+  NetVConnection *netvc = reinterpret_cast<NetVConnection *>(sslp);
+  if (netvc == nullptr) {
+    return nullptr;
+  }
+
+  if (auto snis = netvc->get_service<TLSSNISupport>(); snis) {
+    TLSSNISupport::ClientHello *client_hello = snis->get_client_hello();
+    if (client_hello == nullptr) {
+      return nullptr;
+    }
+
+    // Wrap the raw object in the accessor and return
+    return TSClientHello(client_hello);
+  }
+
+  return nullptr;
+}
+
+TSReturnCode
+TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, const unsigned 
char **out, size_t *outlen)
+{
+  if (static_cast<TLSSNISupport::ClientHello 
*>(ch._get_internal())->getExtension(type, out, outlen) == 1) {
+    return TS_SUCCESS;
+  }
+
+  return TS_ERROR;
+}
+
 TSSslVerifyCTX
 TSVConnSslVerifyCTXGet(TSVConn sslp)
 {
@@ -9158,3 +9191,72 @@ TSLogAddrUnmarshal(char **buf, char *dest, int len)
 
   return {-1, -1};
 }
+
+bool
+TSClientHello::is_available() const
+{
+  return static_cast<bool>(*this);
+}
+
+uint16_t
+TSClientHello::get_version() const
+{
+  return static_cast<TLSSNISupport::ClientHello 
*>(_client_hello)->getVersion();
+}
+
+const uint8_t *
+TSClientHello::get_cipher_suites() const
+{
+  return reinterpret_cast<const uint8_t 
*>(static_cast<TLSSNISupport::ClientHello 
*>(_client_hello)->getCipherSuites().data());
+}
+
+size_t
+TSClientHello::get_cipher_suites_len() const
+{
+  return static_cast<TLSSNISupport::ClientHello 
*>(_client_hello)->getCipherSuites().length();
+}
+
+TSClientHello::TSExtensionTypeList::Iterator::Iterator(const void *ite)
+{
+  static_assert(sizeof(_real_iterator) >= 
sizeof(TLSSNISupport::ClientHello::ExtensionIdIterator));
+
+  ink_assert(_real_iterator);
+  ink_assert(ite);
+  memcpy(_real_iterator, ite, 
sizeof(TLSSNISupport::ClientHello::ExtensionIdIterator));
+}
+
+TSClientHello::TSExtensionTypeList::Iterator
+TSClientHello::TSExtensionTypeList::begin()
+{
+  ink_assert(_ch);
+  auto ch  = static_cast<TLSSNISupport::ClientHello *>(_ch);
+  auto ite = ch->begin();
+  // The temporal pointer is for the memcpy in the constructor. It's only used 
in the constructor.
+  return TSClientHello::TSExtensionTypeList::Iterator(&ite);
+}
+
+TSClientHello::TSExtensionTypeList::Iterator
+TSClientHello::TSExtensionTypeList::end()
+{
+  auto ite = static_cast<TLSSNISupport::ClientHello *>(_ch)->end();
+  // The temporal pointer is for the memcpy in the constructor. It's only used 
in the constructor.
+  return TSClientHello::TSExtensionTypeList::Iterator(&ite);
+}
+
+TSClientHello::TSExtensionTypeList::Iterator &
+TSClientHello::TSExtensionTypeList::Iterator::operator++()
+{
+  ++(*reinterpret_cast<TLSSNISupport::ClientHello::ExtensionIdIterator 
*>(_real_iterator));
+  return *this;
+}
+
+bool
+TSClientHello::TSExtensionTypeList::Iterator::operator==(const 
TSClientHello::TSExtensionTypeList::Iterator &b) const
+{
+  return memcmp(_real_iterator, b._real_iterator, sizeof(_real_iterator)) == 0;
+}
+int
+TSClientHello::TSExtensionTypeList::Iterator::operator*() const
+{
+  return *(*reinterpret_cast<const 
TLSSNISupport::ClientHello::ExtensionIdIterator *>(_real_iterator));
+}
diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc
index ee5e4a8c44..9b56c9129b 100644
--- a/src/iocore/net/TLSSNISupport.cc
+++ b/src/iocore/net/TLSSNISupport.cc
@@ -50,6 +50,13 @@ TLSSNISupport::getInstance(SSL *ssl)
   return static_cast<TLSSNISupport *>(SSL_get_ex_data(ssl, _ex_data_index));
 }
 
+// In TLSSNISupport.h
+TLSSNISupport::ClientHello *
+TLSSNISupport::get_client_hello() const
+{
+  return this->_ch;
+}
+
 void
 TLSSNISupport::bind(SSL *ssl, TLSSNISupport *snis)
 {
@@ -95,6 +102,9 @@ TLSSNISupport::perform_sni_action(SSL &ssl)
 void
 TLSSNISupport::on_client_hello(ClientHello &client_hello)
 {
+  // Save local copy for later use;
+  _ch = &client_hello;
+
   const char          *servername = nullptr;
   const unsigned char *p;
   size_t               remaining, len;
@@ -203,3 +213,95 @@ TLSSNISupport::ClientHello::getExtension(int type, const 
uint8_t **out, size_t *
   return SSL_early_callback_ctx_extension_get(this->_chc, type, out, outlen);
 #endif
 }
+
+uint16_t
+TLSSNISupport::ClientHello::getVersion()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+  // Get legacy version (OpenSSL doesn't expose the direct version field from 
client hello)
+  return SSL_client_hello_get0_legacy_version(_chc);
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+  return _chc->version;
+#endif
+}
+
+std::string_view
+TLSSNISupport::ClientHello::getCipherSuites()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+  const unsigned char *cipher_buf     = nullptr;
+  size_t               cipher_buf_len = SSL_client_hello_get0_ciphers(_chc, 
&cipher_buf);
+  return {reinterpret_cast<const char *>(cipher_buf), cipher_buf_len};
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+  return {reinterpret_cast<const char *>(_chc->cipher_suites), 
_chc->cipher_suites_len};
+#endif
+}
+
+TLSSNISupport::ClientHello::ExtensionIdIterator
+TLSSNISupport::ClientHello::begin()
+{
+  ink_assert(_chc);
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+  if (_ext_ids == nullptr) {
+    SSL_client_hello_get1_extensions_present(_chc, &_ext_ids, &_ext_len);
+  }
+  return ExtensionIdIterator(_ext_ids, _ext_len, 0);
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+  return ExtensionIdIterator(_chc->extensions, _chc->extensions_len, 0);
+#endif
+}
+
+TLSSNISupport::ClientHello::ExtensionIdIterator
+TLSSNISupport::ClientHello::end()
+{
+  ink_assert(_chc);
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+  if (_ext_ids == nullptr) {
+    SSL_client_hello_get1_extensions_present(_chc, &_ext_ids, &_ext_len);
+  }
+  return ExtensionIdIterator(_ext_ids, _ext_len, _ext_len);
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+  return ExtensionIdIterator(_chc->extensions, _chc->extensions_len, 
_chc->extensions_len);
+#endif
+}
+
+TLSSNISupport::ClientHello::~ClientHello()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+  if (_ext_ids) {
+    OPENSSL_free(_ext_ids);
+  }
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+  // Nothing to do
+#endif
+}
+
+TLSSNISupport::ClientHello::ExtensionIdIterator &
+TLSSNISupport::ClientHello::ExtensionIdIterator::operator++()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+  _offset++;
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+  uint16_t ext_len  = (_extensions[_offset + 2] << 8) + _extensions[_offset + 
3];
+  _offset          += 2 + 2 + ext_len;
+  ink_assert(_offset <= _ext_len);
+#endif
+  return *this;
+}
+
+bool
+TLSSNISupport::ClientHello::ExtensionIdIterator::operator==(const 
ExtensionIdIterator &b) const
+{
+  return _extensions == b._extensions && _offset == b._offset;
+}
+
+int
+TLSSNISupport::ClientHello::ExtensionIdIterator::operator*() const
+{
+  ink_assert(_offset < _ext_len);
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+  return _extensions[_offset];
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+  return (_extensions[_offset] << 8) + _extensions[_offset + 1];
+#endif
+}


Reply via email to