/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 #include "crypto/s2n_hash.h" #include "stuffer/s2n_stuffer.h" #include "tls/extensions/s2n_client_supported_versions.h" #include "tls/extensions/s2n_extension_list.h" #include "tls/s2n_client_hello.h" #include "tls/s2n_fingerprint.h" #include "tls/s2n_protocol_preferences.h" #include "utils/s2n_blob.h" #include "utils/s2n_safety.h" #define S2N_JA4_LIST_DIV ',' #define S2N_JA4_PART_DIV '_' /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# 2 character number of cipher suites * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Same as counting ciphers. */ #define S2N_JA4_COUNT_SIZE 2 #define S2N_HEX_PER_BYTE 2 #define S2N_JA4_DIGEST_HEX_CHAR_LIMIT 12 #define S2N_JA4_DIGEST_BYTE_LIMIT (S2N_JA4_DIGEST_HEX_CHAR_LIMIT / S2N_HEX_PER_BYTE) #define S2N_JA4_A_SIZE 10 #define S2N_JA4_B_SIZE S2N_JA4_DIGEST_HEX_CHAR_LIMIT #define S2N_JA4_C_SIZE S2N_JA4_DIGEST_HEX_CHAR_LIMIT #define S2N_JA4_SIZE (S2N_JA4_A_SIZE + 1 + S2N_JA4_B_SIZE + 1 + S2N_JA4_C_SIZE) #define S2N_JA4_LIST_LIMIT 99 #define S2N_JA4_IANA_HEX_SIZE (S2N_HEX_PER_BYTE * sizeof(uint16_t)) #define S2N_JA4_IANA_ENTRY_SIZE (S2N_JA4_IANA_HEX_SIZE + 1) #define S2N_JA4_WORKSPACE_SIZE ((S2N_JA4_LIST_LIMIT * (S2N_JA4_IANA_ENTRY_SIZE))) const char *s2n_ja4_version_strings[] = { /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# 0x0304 = TLS 1.3 = “13” *# 0x0303 = TLS 1.2 = “12” *# 0x0302 = TLS 1.1 = “11” *# 0x0301 = TLS 1.0 = “10” */ [0x0304] = "13", [0x0303] = "12", [0x0302] = "11", [0x0301] = "10", /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# 0x0300 = SSL 3.0 = “s3” *# 0x0002 = SSL 2.0 = “s2” */ [0x0300] = "s3", [0x0002] = "s2", }; /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# Unknown = “00” */ #define S2N_JA4_UNKNOWN_STR "00" DEFINE_POINTER_CLEANUP_FUNC(struct s2n_stuffer *, s2n_stuffer_wipe); static int s2n_fingerprint_ja4_iana_compare(const void *a, const void *b) { const uint8_t *iana_a = (const uint8_t *) a; const uint8_t *iana_b = (const uint8_t *) b; for (size_t i = 0; i < S2N_JA4_IANA_HEX_SIZE; i++) { if (iana_a[i] != iana_b[i]) { return iana_a[i] - iana_b[i]; } } return 0; } static S2N_RESULT s2n_fingerprint_ja4_digest(struct s2n_fingerprint_hash *hash, struct s2n_stuffer *out) { RESULT_ENSURE_REF(hash); if (!s2n_fingerprint_hash_do_digest(hash)) { return S2N_RESULT_OK; } /* Instead of hashing empty inputs, JA4 sets the output to a string of all zeroes. * (Actually hashing an empty input doesn't produce a digest of all zeroes) * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *# If there are no ciphers in the sorted cipher list, then the value of *# JA4_b is set to `000000000000` * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# If there are no extensions in the sorted extensions list, then the value of *# JA4_c is set to `000000000000` */ uint64_t bytes = 0; RESULT_GUARD_POSIX(s2n_hash_get_currently_in_hash_total(hash->hash, &bytes)); if (bytes == 0) { RESULT_GUARD_POSIX(s2n_stuffer_write_str(out, "000000000000")); return S2N_RESULT_OK; } uint8_t digest_bytes[SHA256_DIGEST_LENGTH] = { 0 }; struct s2n_blob digest = { 0 }; RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes))); RESULT_GUARD(s2n_fingerprint_hash_digest(hash, &digest)); /* JA4 digests are truncated */ RESULT_ENSURE_LTE(S2N_JA4_DIGEST_BYTE_LIMIT, digest.size); digest.size = S2N_JA4_DIGEST_BYTE_LIMIT; RESULT_GUARD(s2n_stuffer_write_hex(out, &digest)); return S2N_RESULT_OK; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# 2 character number of cipher suites, so if there’s 6 cipher suites *# in the hello packet, then the value should be “06”. * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Same as counting ciphers. */ static S2N_RESULT s2n_fingerprint_ja4_count(struct s2n_blob *output, uint16_t count) { RESULT_ENSURE_REF(output); /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# If there’s > 99, which there should never be, then output “99”. * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Same as counting ciphers. */ count = MIN(count, 99); RESULT_ENSURE_EQ(output->size, 2); output->data[0] = (count / 10) + '0'; output->data[1] = (count % 10) + '0'; return S2N_RESULT_OK; } static S2N_RESULT s2n_fingerprint_get_extension_version(struct s2n_client_hello *ch, uint16_t *client_version) { RESULT_ENSURE_REF(ch); RESULT_ENSURE_REF(client_version); s2n_parsed_extension *extension = NULL; RESULT_GUARD_POSIX(s2n_client_hello_get_parsed_extension( S2N_EXTENSION_SUPPORTED_VERSIONS, &ch->extensions, &extension)); RESULT_ENSURE_REF(extension); struct s2n_stuffer supported_versions = { 0 }; RESULT_GUARD_POSIX(s2n_stuffer_init_written(&supported_versions, &extension->extension)); RESULT_GUARD_POSIX(s2n_stuffer_skip_read(&supported_versions, sizeof(uint8_t))); while (s2n_stuffer_data_available(&supported_versions)) { uint16_t version = 0; RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&supported_versions, &version)); /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# Remember to ignore GREASE values. */ if (s2n_fingerprint_is_grease_value(version)) { continue; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# If extension 0x002b exists (supported_versions), then the version is *# the highest value in the extension. */ *client_version = MAX(*client_version, version); } return S2N_RESULT_OK; } static S2N_RESULT s2n_fingerprint_ja4_version(struct s2n_stuffer *output, struct s2n_client_hello *ch) { uint16_t client_version = 0; if (s2n_result_is_error(s2n_fingerprint_get_extension_version(ch, &client_version))) { /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# If the extension doesn’t exist, then the TLS version is the value of *# the Protocol Version. */ RESULT_GUARD(s2n_fingerprint_get_legacy_version(ch, &client_version)); } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# Handshake version (located at the top of the packet) should be ignored. */ const char *version_str = NULL; if (client_version < s2n_array_len(s2n_ja4_version_strings)) { version_str = s2n_ja4_version_strings[client_version]; } if (version_str == NULL) { version_str = S2N_JA4_UNKNOWN_STR; } RESULT_GUARD_POSIX(s2n_stuffer_write_str(output, version_str)); return S2N_RESULT_OK; } static S2N_RESULT s2n_client_hello_get_first_alpn(struct s2n_client_hello *ch, struct s2n_blob *first) { RESULT_ENSURE_REF(ch); s2n_parsed_extension *extension = NULL; RESULT_GUARD_POSIX(s2n_client_hello_get_parsed_extension(S2N_EXTENSION_ALPN, &ch->extensions, &extension)); RESULT_ENSURE_REF(extension); struct s2n_stuffer protocols = { 0 }; RESULT_GUARD_POSIX(s2n_stuffer_init_written(&protocols, &extension->extension)); uint16_t list_size = 0; RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&protocols, &list_size)); RESULT_GUARD(s2n_protocol_preferences_read(&protocols, first)); return S2N_RESULT_OK; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *# The first and last alphanumeric characters of the ALPN (Application-Layer *# Protocol Negotiation) first value. */ static S2N_RESULT s2n_fingerprint_ja4_alpn(struct s2n_stuffer *output, struct s2n_client_hello *ch) { struct s2n_blob protocol = { 0 }; if (s2n_result_is_error(s2n_client_hello_get_first_alpn(ch, &protocol))) { protocol.size = 0; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *# If there is no ALPN extension, no ALPN values, or the first ALPN value *# is empty, then we print "00" as the value in the fingerprint. * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *# If the first ALPN value is only a single character, then that character *# is treated as both the first and last character. */ uint8_t first_char = '0', last_char = '0'; if (protocol.size > 0) { first_char = protocol.data[0]; last_char = protocol.data[protocol.size - 1]; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *# If the first or last byte of the first ALPN is non-alphanumeric (meaning *# not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we print the first and *# last characters of the hex representation of the first ALPN instead. */ if (!isalnum(first_char) || !isalnum(last_char)) { RESULT_GUARD(s2n_hex_digit((first_char >> 4), &first_char)); RESULT_GUARD(s2n_hex_digit((last_char & 0x0F), &last_char)); } RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, first_char)); RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, last_char)); return S2N_RESULT_OK; } /* Part "a" of the fingerprint is a descriptive prefix. * * https://github.com/FoxIO-LLC/ja4/main/technical_details/JA4.md *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”) *# (2 character TLS version) *# (SNI=”d” or no SNI=”i”) *# (2 character count of ciphers) *# (2 character count of extensions) *# (first and last characters of first ALPN extension value) */ static S2N_RESULT s2n_fingerprint_ja4_a(struct s2n_fingerprint *fingerprint, struct s2n_stuffer *output, struct s2n_blob *ciphers_count, struct s2n_blob *extensions_count) { RESULT_ENSURE_REF(fingerprint); /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#quic-and-dtls *# If the protocol is QUIC then the first character of the fingerprint is “q”, *# if DTLS it is "d", else it is “t”. * * s2n-tls only supports TLS and QUIC. DTLS is not supported. */ bool is_quic = false; RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello, TLS_EXTENSION_QUIC_TRANSPORT_PARAMETERS, &is_quic)); char protocol_char = (is_quic) ? 'q' : 't'; RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, protocol_char)); RESULT_GUARD(s2n_fingerprint_ja4_version(output, fingerprint->client_hello)); /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#sni *# If the SNI extension (0x0000) exists, then the destination of the connection *# is a domain, or “d” in the fingerprint. *# If the SNI does not exist, then the destination is an IP address, or “i”. */ bool has_sni = false; RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello, TLS_EXTENSION_SERVER_NAME, &has_sni)); char sni_char = (has_sni) ? 'd' : 'i'; RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, sni_char)); /* Reserve two characters for the "count of ciphers". * We'll calculate it later when we handle the cipher suite list for JA4_b. */ uint8_t *ciphers_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE); RESULT_GUARD_PTR(ciphers_count_mem); RESULT_GUARD_POSIX(s2n_blob_init(ciphers_count, ciphers_count_mem, S2N_JA4_COUNT_SIZE)); /* Reserve two characters for the "count of extensions". * We'll calculate it later when we handle the extensions list for JA4_c. */ uint8_t *extensions_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE); RESULT_GUARD_PTR(extensions_count_mem); RESULT_GUARD_POSIX(s2n_blob_init(extensions_count, extensions_count_mem, S2N_JA4_COUNT_SIZE)); RESULT_GUARD(s2n_fingerprint_ja4_alpn(output, fingerprint->client_hello)); return S2N_RESULT_OK; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *# The list is created using the 4 character hex values of the ciphers, *# lower case, comma delimited, ignoring GREASE. */ static S2N_RESULT s2n_fingerprint_ja4_ciphers(struct s2n_fingerprint_hash *hash, struct s2n_client_hello *ch, struct s2n_stuffer *sort_space, uint16_t *ciphers_count) { RESULT_ENSURE_REF(ch); RESULT_ENSURE_REF(sort_space); RESULT_ENSURE_REF(ciphers_count); struct s2n_stuffer cipher_suites = { 0 }; RESULT_GUARD_POSIX(s2n_stuffer_init_written(&cipher_suites, &ch->cipher_suites)); DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer); while (s2n_stuffer_data_available(&cipher_suites)) { uint16_t iana = 0; RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&cipher_suites, &iana)); /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# Remember, ignore GREASE values. They don’t count. */ if (s2n_fingerprint_is_grease_value(iana)) { continue; } RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana)); RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV)); } size_t iana_list_size = s2n_stuffer_data_available(iana_list); size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE; *ciphers_count = iana_count; if (iana_count == 0) { return S2N_RESULT_OK; } uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size); RESULT_ENSURE_REF(ianas); qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare); RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1)); return S2N_RESULT_OK; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *# A 12 character truncated sha256 hash of the list of ciphers sorted in hex order, *# first 12 characters. */ static S2N_RESULT s2n_fingerprint_ja4_b(struct s2n_fingerprint *fingerprint, struct s2n_fingerprint_hash *hash, struct s2n_blob *ciphers_count, struct s2n_stuffer *output) { RESULT_ENSURE_REF(fingerprint); uint16_t ciphers_count_value = 0; RESULT_GUARD(s2n_fingerprint_ja4_ciphers(hash, fingerprint->client_hello, &fingerprint->workspace, &ciphers_count_value)); RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output)); RESULT_GUARD(s2n_fingerprint_ja4_count(ciphers_count, ciphers_count_value)); return S2N_RESULT_OK; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# The extension list is created using the 4 character hex values of the extensions, *# lower case, comma delimited, sorted (not in the order they appear). */ static S2N_RESULT s2n_fingerprint_ja4_extensions(struct s2n_fingerprint_hash *hash, struct s2n_client_hello *ch, struct s2n_stuffer *sort_space, uint16_t *extensions_count) { RESULT_ENSURE_REF(ch); RESULT_ENSURE_REF(sort_space); RESULT_ENSURE_REF(extensions_count); struct s2n_stuffer extensions = { 0 }; RESULT_GUARD_POSIX(s2n_stuffer_init_written(&extensions, &ch->extensions.raw)); DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer); while (s2n_stuffer_data_available(&extensions)) { uint16_t iana = 0; RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana)); /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Ignore GREASE. */ if (s2n_fingerprint_is_grease_value(iana)) { continue; } /* SNI and ALPN are included in the extension count, but not in the extension list. * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# Ignore the SNI extension (0000) and the ALPN extension (0010) *# as we’ve already captured them in the _a_ section of the fingerprint. * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Include SNI and ALPN. */ (*extensions_count)++; if (iana == TLS_EXTENSION_SERVER_NAME || iana == S2N_EXTENSION_ALPN) { continue; } RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana)); RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV)); } size_t iana_list_size = s2n_stuffer_data_available(iana_list); size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE; if (iana_count == 0) { return S2N_RESULT_OK; } uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size); RESULT_ENSURE_REF(ianas); qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare); RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1)); return S2N_RESULT_OK; } static S2N_RESULT s2n_fingerprint_ja4_sig_algs(struct s2n_fingerprint_hash *hash, struct s2n_client_hello *ch) { RESULT_ENSURE_REF(ch); s2n_parsed_extension *extension = NULL; int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_SIGNATURE_ALGORITHMS, &ch->extensions, &extension); if (result != S2N_SUCCESS) { return S2N_RESULT_OK; } RESULT_ENSURE_REF(extension); struct s2n_stuffer sig_algs = { 0 }; RESULT_GUARD_POSIX(s2n_stuffer_init_written(&sig_algs, &extension->extension)); uint8_t entry_bytes[S2N_JA4_IANA_ENTRY_SIZE] = { 0 }; struct s2n_stuffer entry = { 0 }; RESULT_GUARD_POSIX(s2n_blob_init(&entry.blob, entry_bytes, sizeof(entry_bytes))); bool is_first = true; if (s2n_stuffer_skip_read(&sig_algs, sizeof(uint16_t)) != S2N_SUCCESS) { return S2N_RESULT_OK; } while (s2n_stuffer_data_available(&sig_algs)) { uint16_t iana = 0; RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&sig_algs, &iana)); if (s2n_fingerprint_is_grease_value(iana)) { continue; } if (is_first) { RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA4_PART_DIV)); } else { RESULT_GUARD_POSIX(s2n_stuffer_write_char(&entry, S2N_JA4_LIST_DIV)); } RESULT_GUARD(s2n_stuffer_write_uint16_hex(&entry, iana)); RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, entry_bytes, s2n_stuffer_data_available(&entry))); RESULT_GUARD_POSIX(s2n_stuffer_rewrite(&entry)); is_first = false; } return S2N_RESULT_OK; } /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# A 12 character truncated sha256 hash of the list of extensions, sorted by *# hex value, followed by the list of signature algorithms, in the order that *# they appear (not sorted). */ static S2N_RESULT s2n_fingerprint_ja4_c(struct s2n_fingerprint *fingerprint, struct s2n_fingerprint_hash *hash, struct s2n_blob *extensions_count, struct s2n_stuffer *output) { RESULT_ENSURE_REF(fingerprint); uint16_t extensions_count_value = 0; RESULT_GUARD(s2n_fingerprint_ja4_extensions(hash, fingerprint->client_hello, &fingerprint->workspace, &extensions_count_value)); /** *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# The signature algorithm hex values are then added to the end of the list *# in the order that they appear (not sorted) with an underscore delimiting *# the two lists. * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# If there are no signature algorithms in the hello packet, *# then the string ends without an underscore and is hashed. * * s2n_fingerprint_ja4_sig_algs handles writing the underscore because we * need to skip writing it if there are no signature algorithms. */ RESULT_GUARD(s2n_fingerprint_ja4_sig_algs(hash, fingerprint->client_hello)); RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output)); RESULT_GUARD(s2n_fingerprint_ja4_count(extensions_count, extensions_count_value)); return S2N_RESULT_OK; } /* JA4 fingerprints are basically of the form a_b_c: * *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#ja4-algorithm *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”) *# (2 character TLS version) *# (SNI=”d” or no SNI=”i”) *# (2 character count of ciphers) *# (2 character count of extensions) *# (first and last characters of first ALPN extension value) *# _ *# (sha256 hash of the list of cipher hex codes sorted in hex order, truncated to 12 characters) *# _ *# (sha256 hash of (the list of extension hex codes sorted in hex order)_(the list of signature algorithms), truncated to 12 characters) *# *# The end result is a fingerprint that looks like: *# t13d1516h2_8daaf6152771_b186095e22b6 */ static S2N_RESULT s2n_fingerprint_ja4(struct s2n_fingerprint *fingerprint, struct s2n_fingerprint_hash *hash, struct s2n_stuffer *output) { RESULT_ENSURE_REF(fingerprint); RESULT_ENSURE_REF(hash); RESULT_ENSURE_REF(output); if (s2n_stuffer_is_freed(&fingerprint->workspace)) { RESULT_GUARD_POSIX(s2n_stuffer_growable_alloc(&fingerprint->workspace, S2N_JA4_WORKSPACE_SIZE)); } struct s2n_blob ciphers_count = { 0 }; struct s2n_blob extensions_count = { 0 }; RESULT_GUARD(s2n_fingerprint_ja4_a(fingerprint, output, &ciphers_count, &extensions_count)); RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV)); RESULT_GUARD(s2n_fingerprint_ja4_b(fingerprint, hash, &ciphers_count, output)); RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV)); RESULT_GUARD(s2n_fingerprint_ja4_c(fingerprint, hash, &extensions_count, output)); if (s2n_fingerprint_hash_do_digest(hash)) { /* The extra two bytes are for the characters separating the parts */ fingerprint->raw_size = hash->bytes_digested + S2N_JA4_A_SIZE + 2; } else { fingerprint->raw_size = s2n_stuffer_data_available(output); } return S2N_RESULT_OK; } struct s2n_fingerprint_method ja4_fingerprint = { .hash = S2N_HASH_SHA256, .hash_str_size = S2N_JA4_SIZE, .fingerprint = s2n_fingerprint_ja4, };