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/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..4a254ced081 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1081,6 +1081,19 @@ using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; +struct tsapi_ssl_client_hello { + uint16_t version; + const uint8_t *cipher_suites; + size_t cipher_suites_len; + const uint8_t *extensions; + size_t extensions_len; + int *extension_ids; + size_t extension_ids_len; + void *ssl_ptr; +}; + +using TSClientHello = struct tsapi_ssl_client_hello *; + using TSFetchSM = struct tsapi_fetchsm *; using TSThreadFunc = void *(*)(void *data); diff --git a/include/ts/ts.h b/include/ts/ts.h index 3227f1cf18d..514470d9431 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1331,8 +1331,10 @@ TSReturnCode TSVConnProtocolEnable(TSVConn connp, const char *protocol_name); int TSVConnIsSsl(TSVConn sslp); /* Returns 1 if a certificate was provided in the TLS handshake, 0 otherwise. */ -int TSVConnProvidedSslCert(TSVConn sslp); -const char *TSVConnSslSniGet(TSVConn sslp, int *length); +int TSVConnProvidedSslCert(TSVConn sslp); +const char *TSVConnSslSniGet(TSVConn sslp, int *length); +TSClientHello TSVConnClientHelloGet(TSVConn sslp); +void TSClientHelloDestroy(TSClientHello ch); TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); 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..aa8c9266ba4 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -52,18 +52,19 @@ 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); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); +int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); namespace { @@ -75,7 +76,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 +163,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 +185,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 +234,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 == client_hello_ext_get(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->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 == client_hello_ext_get(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->cipher_suites; + size_t buflen = ch->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 +287,30 @@ 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])); + if (ch->extensions != nullptr) { + const uint8_t *ext = ch->extensions; + size_t remaining = ch->extensions_len; + + while (remaining >= 4) { + uint16_t ext_type = (ext[0] << 8) | ext[1]; + uint16_t ext_len = (ext[2] << 8) | ext[3]; + summary.add_extension(ext_type); + size_t total_ext_size = 4 + ext_len; + if (total_ext_size > remaining) { + break; + } + + ext += total_ext_size; + remaining -= total_ext_size; + } + } else if (ch->extension_ids != nullptr) { + // OpenSSL's extension_ids is an array of ints, each element is a complete extension ID + for (std::size_t i = 0; i < ch->extension_ids_len; i++) { + summary.add_extension(static_cast(ch->extension_ids[i])); } } - OPENSSL_free(buf); } std::string @@ -394,3 +416,33 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConnReenable(ssl_vc); return TS_SUCCESS; } + +int +client_hello_ext_get(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->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->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; +} diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index cabfb5309d6..2ec6b8be7ef 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,6 +7890,96 @@ 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) { + // Allocate the TSClientHello structure + auto ch = new tsapi_ssl_client_hello(); + +#ifdef OPENSSL_IS_BORINGSSL + // Get the BoringSSL client hello container + ClientHelloContainer client_hello = snis->get_client_hello_container(); + if (client_hello == nullptr) { + delete ch; + 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->extension_ids = nullptr; + ch->extension_ids_len = 0; + ch->ssl_ptr = const_cast(client_hello); +#else + // Get the OpenSSL SSL* object + auto tbs = netvc->get_service(); + if (!tbs) { + delete ch; + return nullptr; + } + SSL *ssl = tbs->get_tls_handle(); + if (ssl == nullptr) { + delete ch; + 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; + ch->cipher_suites_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); + ch->cipher_suites = cipher_buf; + + // For OpenSSL, we can't get direct access to the raw extensions buffer + // Instead, get the list of extension IDs + ch->extensions = nullptr; + ch->extensions_len = 0; + 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; + } else { + ch->extension_ids = nullptr; + ch->extension_ids_len = 0; + } + ch->ssl_ptr = ssl; +#endif + + return ch; + } + + return nullptr; +} + +void +TSClientHelloDestroy(TSClientHello ch) +{ + if (ch == nullptr) { + return; + } + +#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->extension_ids != nullptr) { + OPENSSL_free(ch->extension_ids); + } +#endif + + delete ch; +} + 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) {