diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake index 52313acfddf..764571546c1 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 1def2223a78..5379c9a72e9 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 Hook Trace ICAP + JA4 Fingerprint Maxmind ACL Memcache Memory Profile @@ -232,6 +233,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..acfd96a91d2 --- /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:: 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 00000000000..a9fbcc981b4 --- /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 + +.. 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 6897cce36a4..22f3d751a8b 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 @@ class TLSSNISupport { 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 @@ class TLSSNISupport 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 @@ class TLSSNISupport 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 @@ 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); + ClientHello *_ch = nullptr; }; diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index c979ac8e94c..86aff5202fd 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -43,10 +43,13 @@ */ #include +#include +#include #include #include #include #include +#include /** 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 == true); + using TSFile = struct tsapi_file *; using TSMLoc = struct tsapi_mloc *; diff --git a/include/ts/ts.h b/include/ts/ts.h index c63febb3607..9b62d64fe8c 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 d45ddf00785..3de07907d2f 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 16419d45668..216171280fa 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(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 (!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()); // 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(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 1a4b734e285..529ff6d2029 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(sslp); + if (netvc == nullptr) { + return nullptr; + } + + if (auto snis = netvc->get_service(); 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(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(*this); +} + +uint16_t +TSClientHello::get_version() const +{ + return static_cast(_client_hello)->getVersion(); +} + +const uint8_t * +TSClientHello::get_cipher_suites() const +{ + return reinterpret_cast(static_cast(_client_hello)->getCipherSuites().data()); +} + +size_t +TSClientHello::get_cipher_suites_len() const +{ + return static_cast(_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(_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(_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(_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(_real_iterator)); +} diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc index ee5e4a8c441..9b56c9129be 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -50,6 +50,13 @@ TLSSNISupport::getInstance(SSL *ssl) return static_cast(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(cipher_buf), cipher_buf_len}; +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + return {reinterpret_cast(_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 +}