diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake index 487e0bfed16..c546e7ebb5f 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -41,15 +41,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 8de06d97242..251647a40b2 100644 --- a/doc/admin-guide/plugins/index.en.rst +++ b/doc/admin-guide/plugins/index.en.rst @@ -177,6 +177,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi Header Frequency Hook Trace ICAP + JA4 Fingerprint Maxmind ACL Memcache Memory Profile @@ -228,6 +229,9 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi :doc:`ICAP ` Pass response data to external server for further processing using the ICAP protocol. +:doc:`JA4 Fingerprint ` + Calculates JA4 Fingerprints for incoming TLS traffic. + :doc:`MaxMind ACL ` 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 00000000000..b8e5e5e37ad --- /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: JA4: + +**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 00000000000..5d8b1b8757d --- /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 + +.. function:: TSClientHello TSVConnClientHelloGet(TSVConn sslp) +.. function:: void TSClientHelloDestroy(TSClientHello ch) +.. 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`. This function is typically called from the +``TS_EVENT_SSL_CLIENT_HELLO`` hook. Returns ``nullptr`` if +:arg:`sslp` is invalid or not a TLS connection. + +The caller must call :func:`TSClientHelloDestroy` to free the returned object. + +:func:`TSClientHelloDestroy` frees the :type:`TSClientHello` object :arg:`ch`. + +: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 00000000000..e3d2f4fcf4f --- /dev/null +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -0,0 +1,43 @@ +.. 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 + +.. type:: TSClientHello + + +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. + +Objects of this type are obtained via :func:`TSVConnClientHelloGet` and must +be freed using :func:`TSClientHelloDestroy`. The implementation abstracts +differences between OpenSSL and BoringSSL to provide a consistent interface. diff --git a/include/iocore/net/TLSSNISupport.h b/include/iocore/net/TLSSNISupport.h index 6897cce36a4..f56223d23b0 100644 --- a/include/iocore/net/TLSSNISupport.h +++ b/include/iocore/net/TLSSNISupport.h @@ -43,7 +43,8 @@ class TLSSNISupport /** * @return 1 if successful */ - int getExtension(int type, const uint8_t **out, size_t *outlen); + int getExtension(int type, const uint8_t **out, size_t *outlen); + ClientHelloContainer get_client_hello_container(); private: ClientHelloContainer _chc; @@ -55,8 +56,9 @@ class TLSSNISupport static TLSSNISupport *getInstance(SSL *ssl); static void bind(SSL *ssl, TLSSNISupport *snis); static void unbind(SSL *ssl); - - int perform_sni_action(SSL &ssl); + int perform_sni_action(SSL &ssl); + ClientHelloContainer get_client_hello_container() const; + void set_client_hello_container(ClientHelloContainer container); // Callback functions for OpenSSL libraries /** Process a CLIENT_HELLO from a client. @@ -114,5 +116,6 @@ class TLSSNISupport // Null-terminated string, or nullptr if there is no SNI server name. std::unique_ptr _sni_server_name; - void _set_sni_server_name_buffer(std::string_view name); + void _set_sni_server_name_buffer(std::string_view name); + ClientHelloContainer _chc = nullptr; }; diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 078ce0eb69c..44a7395c6f4 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -43,6 +43,8 @@ */ #include +#include +#include #include #include #include @@ -1044,6 +1046,122 @@ struct TSHttp2Priority { * or -1 if the stream has no dependency. */ int32_t stream_dependency; }; +/** + * A structure for SSL Client Hello data + */ +struct tsapi_ssl_client_hello { + uint16_t version{0}; + const uint8_t *cipher_suites{nullptr}; + size_t cipher_suites_len{0}; + const uint8_t *extensions{nullptr}; + size_t extensions_len{0}; + int *extension_ids{nullptr}; + size_t extension_ids_len{0}; + void *ssl_ptr{nullptr}; +}; + +// Wrapper class that provides controlled access to client hello data +class TSClientHelloImpl +{ +public: + TSClientHelloImpl(std::unique_ptr ch) : _ssl_client_hello(std::move(ch)) {} + + ~TSClientHelloImpl() = default; + + uint16_t + get_version() const + { + return _ssl_client_hello->version; + } + + const uint8_t * + get_cipher_suites() const + { + return _ssl_client_hello->cipher_suites; + } + + size_t + get_cipher_suites_len() const + { + return _ssl_client_hello->cipher_suites_len; + } + + const uint8_t * + get_extensions() const + { + return _ssl_client_hello->extensions; + } + + size_t + get_extensions_len() const + { + return _ssl_client_hello->extensions_len; + } + + const int * + get_extension_ids() const + { + return _ssl_client_hello->extension_ids; + } + + size_t + get_extension_ids_len() const + { + return _ssl_client_hello->extension_ids_len; + } + + void * + get_ssl_ptr() const + { + return _ssl_client_hello->ssl_ptr; + } + + // Returns a vector of extension type IDs + // This abstracts the difference between BoringSSL (extensions buffer) and OpenSSL (extension_ids array) + std::vector + get_extension_types() const + { + std::vector result; + + // For BoringSSL, parse the extensions buffer + if (_ssl_client_hello->extensions != nullptr) { + const uint8_t *ext = _ssl_client_hello->extensions; + size_t remaining = _ssl_client_hello->extensions_len; + + while (remaining >= 4) { + uint16_t ext_type = (ext[0] << 8) | ext[1]; + uint16_t ext_len = (ext[2] << 8) | ext[3]; + size_t total_ext_size = 4 + ext_len; + + result.push_back(ext_type); + + if (total_ext_size > remaining) { + break; + } + ext += total_ext_size; + remaining -= total_ext_size; + } + } + // For OpenSSL, use the extension IDs array + else if (_ssl_client_hello->extension_ids != nullptr) { + for (size_t i = 0; i < _ssl_client_hello->extension_ids_len; i++) { + result.push_back(static_cast(_ssl_client_hello->extension_ids[i])); + } + } + + return result; + } + + // Internal accessor for API implementation + tsapi_ssl_client_hello * + _get_internal() const + { + return _ssl_client_hello.get(); + } + +private: + std::unique_ptr _ssl_client_hello; +}; using TSFile = struct tsapi_file *; @@ -1080,6 +1198,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; +using TSClientHello = TSClientHelloImpl *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 3227f1cf18d..519dbf16f79 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1334,6 +1334,78 @@ 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 nullptr being returned. + + For BoringSSL, the Client Hello data is copied from the SSL_CLIENT_HELLO + structure. For OpenSSL, cipher suites and extension IDs are extracted using + SSL_client_hello_get0_* functions. + + Memory Management: The caller must call TSClientHelloDestroy() to free the + returned object when it is no longer needed. Failure to do so will result + in memory leaks, especially for OpenSSL which allocates memory for the + extension IDs array. + + @param sslp The SSL virtual connection handle. Must not be nullptr. + @return Pointer to TSClientHello object containing Client Hello data, or + nullptr if the client hello is not available or if an error occurs. + + @see TSClientHelloDestroy + @see TSClientHelloExtensionGet + */ +TSClientHello TSVConnClientHelloGet(TSVConn sslp); +/** + Destroys a Client Hello object and frees associated memory. + + This function must be called to properly free a TSClientHello object + obtained from TSVConnClientHelloGet(). It handles SSL library-specific + cleanup, including freeing the extension IDs array allocated by OpenSSL's + SSL_client_hello_get1_extensions_present() function. + + @param ch The Client Hello object to destroy. + + @see TSVConnClientHelloGet + */ +void TSClientHelloDestroy(TSClientHello ch); + +/** + 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 as long as the + TSClientHello object has not been destroyed. + + @param ch The Client Hello object obtained from TSVConnClientHelloGet(). + Must not be nullptr. + @param type The TLS extension type to retrieve. + @param out Pointer to receive the extension data buffer. Must not be nullptr. + @param 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 + @see TSClientHelloDestroy + */ +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 d45ddf00785..b1b4dd55c7d 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. +Ja4 now supports boringssl + ## 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 ada54ed1c98..5ddc4cbefdd 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -52,13 +52,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); @@ -75,7 +75,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}; @@ -163,15 +162,20 @@ 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(edata)}; - TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; - if (nullptr == ssl) { - Dbg(dbg_ctl, "Could not get SSL object."); + + TSVConn const ssl_vc{static_cast(edata)}; + + TSClientHello ch = TSVConnClientHelloGet(ssl_vc); + + if (nullptr == ch) { + Dbg(dbg_ctl, "Could not get TSClientHello object."); } else { auto data{std::make_unique()}; - data->fingerprint = get_fingerprint(reinterpret_cast(ssl)); + data->fingerprint = get_fingerprint(ch); get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); log_fingerprint(data.get()); + // Clean up the TSClientHello structure + TSClientHelloDestroy(ch); // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } @@ -180,14 +184,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; } @@ -229,49 +233,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 list_len = buf[0]; + for (size_t i = 1; i + 1 < buflen && i < list_len + 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."); @@ -279,16 +286,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 @@ -387,7 +389,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(edata)}; delete static_cast(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 cabfb5309d6..c3e05227585 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,6 +7890,112 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } +TSClientHello +TSVConnClientHelloGet(TSVConn sslp) +{ + NetVConnection *netvc = reinterpret_cast(sslp); + if (netvc == nullptr) { + return nullptr; + } + + if (auto snis = netvc->get_service(); snis) { + auto ch = std::make_unique(); + +#ifdef OPENSSL_IS_BORINGSSL + // Get the BoringSSL client hello container + ClientHelloContainer client_hello = snis->get_client_hello_container(); + if (client_hello == nullptr) { + return nullptr; + } + + // Populate from BoringSSL SSL_CLIENT_HELLO structure + ch->version = client_hello->version; + ch->cipher_suites = client_hello->cipher_suites; + ch->cipher_suites_len = client_hello->cipher_suites_len; + ch->extensions = client_hello->extensions; + ch->extensions_len = client_hello->extensions_len; + ch->ssl_ptr = const_cast(client_hello); +#else + // Get the OpenSSL SSL* object + auto tbs = netvc->get_service(); + if (!tbs) { + return nullptr; + } + SSL *ssl = tbs->get_tls_handle(); + if (ssl == nullptr) { + return nullptr; + } + + // Get legacy version (OpenSSL doesn't expose the direct version field from client hello) + ch->version = SSL_client_hello_get0_legacy_version(ssl); + + // Get cipher suites + const unsigned char *cipher_buf = nullptr; + size_t cipher_buf_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); + ch->cipher_suites = cipher_buf; + ch->cipher_suites_len = cipher_buf_len; + + // For OpenSSL, we can't get direct access to the raw extensions buffer + // Instead, get the list of extension IDs + int *ext_ids = nullptr; + size_t ext_count; + if (SSL_client_hello_get1_extensions_present(ssl, &ext_ids, &ext_count) == 1) { + ch->extension_ids = ext_ids; + ch->extension_ids_len = ext_count; + } + ch->ssl_ptr = ssl; +#endif + + // Wrap the POD structure in the wrapper class and return + return new TSClientHelloImpl(std::move(ch)); + } + + return nullptr; +} + +void +TSClientHelloDestroy(TSClientHello ch) +{ +#ifndef OPENSSL_IS_BORINGSSL + // For OpenSSL, we need to free the extension IDs array that was allocated + // by SSL_client_hello_get1_extensions_present + if (ch->get_extension_ids() != nullptr) { + OPENSSL_free(const_cast(ch->get_extension_ids())); + } +#endif + delete ch; +} + +TSReturnCode +TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) +{ + if (ch == nullptr || out == nullptr || outlen == nullptr) { + return TS_ERROR; + } + +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = static_cast(ch->get_ssl_ptr()); + if (client_hello == nullptr) { + return TS_ERROR; + } + + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#else + SSL *ssl = static_cast(ch->get_ssl_ptr()); + if (ssl == nullptr) { + return TS_ERROR; + } + + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#endif + + return TS_ERROR; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc index 0b7bc982ad4..ede8f20543f 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -306,6 +306,7 @@ ssl_client_hello_callback(const SSL_CLIENT_HELLO *client_hello) TLSSNISupport *snis = TLSSNISupport::getInstance(s); if (snis) { + snis->set_client_hello_container(ch.get_client_hello_container()); snis->on_client_hello(ch); int ret = snis->perform_sni_action(*s); if (ret != SSL_TLSEXT_ERR_OK) { diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc index ee5e4a8c441..b4ced5d632a 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -50,6 +50,25 @@ TLSSNISupport::getInstance(SSL *ssl) return static_cast(SSL_get_ex_data(ssl, _ex_data_index)); } +ClientHelloContainer +TLSSNISupport::ClientHello::get_client_hello_container() +{ + return this->_chc; +} + +// In TLSSNISupport.h +ClientHelloContainer +TLSSNISupport::get_client_hello_container() const +{ + return this->_chc; +} + +void +TLSSNISupport::set_client_hello_container(ClientHelloContainer container) +{ + this->_chc = container; +} + void TLSSNISupport::bind(SSL *ssl, TLSSNISupport *snis) {