LCOV - code coverage report
Current view: top level - tls - s2n_fingerprint_ja4.c (source / functions) Hit Total Coverage
Test: unit_test_coverage.info Lines: 268 270 99.3 %
Date: 2025-08-15 07:28:39 Functions: 14 14 100.0 %
Branches: 148 280 52.9 %

           Branch data     Line data    Source code
       1                 :            : /*
       2                 :            :  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
       3                 :            :  *
       4                 :            :  * Licensed under the Apache License, Version 2.0 (the "License").
       5                 :            :  * You may not use this file except in compliance with the License.
       6                 :            :  * A copy of the License is located at
       7                 :            :  *
       8                 :            :  *  http://aws.amazon.com/apache2.0
       9                 :            :  *
      10                 :            :  * or in the "license" file accompanying this file. This file is distributed
      11                 :            :  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
      12                 :            :  * express or implied. See the License for the specific language governing
      13                 :            :  * permissions and limitations under the License.
      14                 :            :  */
      15                 :            : 
      16                 :            : #include <ctype.h>
      17                 :            : 
      18                 :            : #include "crypto/s2n_hash.h"
      19                 :            : #include "stuffer/s2n_stuffer.h"
      20                 :            : #include "tls/extensions/s2n_client_supported_versions.h"
      21                 :            : #include "tls/extensions/s2n_extension_list.h"
      22                 :            : #include "tls/s2n_client_hello.h"
      23                 :            : #include "tls/s2n_fingerprint.h"
      24                 :            : #include "tls/s2n_protocol_preferences.h"
      25                 :            : #include "utils/s2n_blob.h"
      26                 :            : #include "utils/s2n_safety.h"
      27                 :            : 
      28                 :            : #define S2N_JA4_LIST_DIV ','
      29                 :            : #define S2N_JA4_PART_DIV '_'
      30                 :            : 
      31                 :            : /**
      32                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
      33                 :            :  *# 2 character number of cipher suites
      34                 :            :  *
      35                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
      36                 :            :  *# Same as counting ciphers.
      37                 :            :  */
      38                 :        142 : #define S2N_JA4_COUNT_SIZE 2
      39                 :            : 
      40                 :      17130 : #define S2N_HEX_PER_BYTE              2
      41                 :         81 : #define S2N_JA4_DIGEST_HEX_CHAR_LIMIT 12
      42                 :         81 : #define S2N_JA4_DIGEST_BYTE_LIMIT     (S2N_JA4_DIGEST_HEX_CHAR_LIMIT / S2N_HEX_PER_BYTE)
      43                 :            : 
      44                 :         62 : #define S2N_JA4_A_SIZE 10
      45                 :            : #define S2N_JA4_B_SIZE S2N_JA4_DIGEST_HEX_CHAR_LIMIT
      46                 :            : #define S2N_JA4_C_SIZE S2N_JA4_DIGEST_HEX_CHAR_LIMIT
      47                 :            : #define S2N_JA4_SIZE   (S2N_JA4_A_SIZE + 1 + S2N_JA4_B_SIZE + 1 + S2N_JA4_C_SIZE)
      48                 :            : 
      49                 :            : #define S2N_JA4_LIST_LIMIT      99
      50                 :      17049 : #define S2N_JA4_IANA_HEX_SIZE   (S2N_HEX_PER_BYTE * sizeof(uint16_t))
      51                 :        236 : #define S2N_JA4_IANA_ENTRY_SIZE (S2N_JA4_IANA_HEX_SIZE + 1)
      52                 :            : #define S2N_JA4_WORKSPACE_SIZE  ((S2N_JA4_LIST_LIMIT * (S2N_JA4_IANA_ENTRY_SIZE)))
      53                 :            : 
      54                 :            : const char *s2n_ja4_version_strings[] = {
      55                 :            :     /**
      56                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
      57                 :            :      *# 0x0304 = TLS 1.3 = “13”
      58                 :            :      *# 0x0303 = TLS 1.2 = “12”
      59                 :            :      *# 0x0302 = TLS 1.1 = “11”
      60                 :            :      *# 0x0301 = TLS 1.0 = “10”
      61                 :            :      */
      62                 :            :     [0x0304] = "13",
      63                 :            :     [0x0303] = "12",
      64                 :            :     [0x0302] = "11",
      65                 :            :     [0x0301] = "10",
      66                 :            :     /**
      67                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
      68                 :            :      *# 0x0300 = SSL 3.0 = “s3”
      69                 :            :      *# 0x0002 = SSL 2.0 = “s2”
      70                 :            :      */
      71                 :            :     [0x0300] = "s3",
      72                 :            :     [0x0002] = "s2",
      73                 :            : };
      74                 :            : 
      75                 :            : /**
      76                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
      77                 :            :  *# Unknown = “00”
      78                 :            :  */
      79                 :         57 : #define S2N_JA4_UNKNOWN_STR "00"
      80                 :            : 
      81                 :            : DEFINE_POINTER_CLEANUP_FUNC(struct s2n_stuffer *, s2n_stuffer_wipe);
      82                 :            : 
      83                 :            : static int s2n_fingerprint_ja4_iana_compare(const void *a, const void *b)
      84                 :       4074 : {
      85                 :       4074 :     const uint8_t *iana_a = (const uint8_t *) a;
      86                 :       4074 :     const uint8_t *iana_b = (const uint8_t *) b;
      87         [ +  + ]:      16813 :     for (size_t i = 0; i < S2N_JA4_IANA_HEX_SIZE; i++) {
      88         [ +  + ]:      15009 :         if (iana_a[i] != iana_b[i]) {
      89                 :       2270 :             return iana_a[i] - iana_b[i];
      90                 :       2270 :         }
      91                 :      15009 :     }
      92                 :       1804 :     return 0;
      93                 :       4074 : }
      94                 :            : 
      95                 :            : static S2N_RESULT s2n_fingerprint_ja4_digest(struct s2n_fingerprint_hash *hash,
      96                 :            :         struct s2n_stuffer *out)
      97                 :        142 : {
      98 [ -  + ][ #  # ]:        142 :     RESULT_ENSURE_REF(hash);
      99         [ +  + ]:        142 :     if (!s2n_fingerprint_hash_do_digest(hash)) {
     100                 :         18 :         return S2N_RESULT_OK;
     101                 :         18 :     }
     102                 :            : 
     103                 :            :     /* Instead of hashing empty inputs, JA4 sets the output to a string of all zeroes.
     104                 :            :      * (Actually hashing an empty input doesn't produce a digest of all zeroes)
     105                 :            :      *
     106                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash
     107                 :            :      *# If there are no ciphers in the sorted cipher list, then the value of
     108                 :            :      *# JA4_b is set to `000000000000`
     109                 :            :      *
     110                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     111                 :            :      *# If there are no extensions in the sorted extensions list, then the value of
     112                 :            :      *# JA4_c is set to `000000000000`
     113                 :            :      */
     114                 :        124 :     uint64_t bytes = 0;
     115         [ -  + ]:        124 :     RESULT_GUARD_POSIX(s2n_hash_get_currently_in_hash_total(hash->hash, &bytes));
     116         [ +  + ]:        124 :     if (bytes == 0) {
     117         [ -  + ]:         43 :         RESULT_GUARD_POSIX(s2n_stuffer_write_str(out, "000000000000"));
     118                 :         43 :         return S2N_RESULT_OK;
     119                 :         43 :     }
     120                 :            : 
     121                 :         81 :     uint8_t digest_bytes[SHA256_DIGEST_LENGTH] = { 0 };
     122                 :         81 :     struct s2n_blob digest = { 0 };
     123         [ -  + ]:         81 :     RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes)));
     124         [ -  + ]:         81 :     RESULT_GUARD(s2n_fingerprint_hash_digest(hash, &digest));
     125                 :            : 
     126                 :            :     /* JA4 digests are truncated */
     127 [ -  + ][ #  # ]:         81 :     RESULT_ENSURE_LTE(S2N_JA4_DIGEST_BYTE_LIMIT, digest.size);
     128                 :         81 :     digest.size = S2N_JA4_DIGEST_BYTE_LIMIT;
     129         [ -  + ]:         81 :     RESULT_GUARD(s2n_stuffer_write_hex(out, &digest));
     130                 :         81 :     return S2N_RESULT_OK;
     131                 :         81 : }
     132                 :            : 
     133                 :            : /**
     134                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
     135                 :            :  *# 2 character number of cipher suites, so if there’s 6 cipher suites
     136                 :            :  *# in the hello packet, then the value should be “06”.
     137                 :            :  *
     138                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
     139                 :            :  *# Same as counting ciphers.
     140                 :            :  */
     141                 :            : static S2N_RESULT s2n_fingerprint_ja4_count(struct s2n_blob *output, uint16_t count)
     142                 :        142 : {
     143 [ -  + ][ #  # ]:        142 :     RESULT_ENSURE_REF(output);
     144                 :            : 
     145                 :            :     /**
     146                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
     147                 :            :      *# If there’s > 99, which there should never be, then output “99”.
     148                 :            :      *
     149                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
     150                 :            :      *# Same as counting ciphers.
     151                 :            :      */
     152                 :        142 :     count = MIN(count, 99);
     153                 :            : 
     154 [ -  + ][ #  # ]:        142 :     RESULT_ENSURE_EQ(output->size, 2);
     155                 :        142 :     output->data[0] = (count / 10) + '0';
     156                 :        142 :     output->data[1] = (count % 10) + '0';
     157                 :        142 :     return S2N_RESULT_OK;
     158                 :        142 : }
     159                 :            : 
     160                 :            : static S2N_RESULT s2n_fingerprint_get_extension_version(struct s2n_client_hello *ch,
     161                 :            :         uint16_t *client_version)
     162                 :         71 : {
     163 [ -  + ][ #  # ]:         71 :     RESULT_ENSURE_REF(ch);
     164 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(client_version);
     165                 :            : 
     166                 :         71 :     s2n_parsed_extension *extension = NULL;
     167         [ +  + ]:         71 :     RESULT_GUARD_POSIX(s2n_client_hello_get_parsed_extension(
     168                 :         16 :             S2N_EXTENSION_SUPPORTED_VERSIONS, &ch->extensions, &extension));
     169 [ -  + ][ #  # ]:         16 :     RESULT_ENSURE_REF(extension);
     170                 :            : 
     171                 :         16 :     struct s2n_stuffer supported_versions = { 0 };
     172         [ -  + ]:         16 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&supported_versions, &extension->extension));
     173                 :            : 
     174         [ +  + ]:         16 :     RESULT_GUARD_POSIX(s2n_stuffer_skip_read(&supported_versions, sizeof(uint8_t)));
     175         [ +  + ]:         28 :     while (s2n_stuffer_data_available(&supported_versions)) {
     176                 :         16 :         uint16_t version = 0;
     177         [ -  + ]:         16 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&supported_versions, &version));
     178                 :            :         /**
     179                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
     180                 :            :          *# Remember to ignore GREASE values.
     181                 :            :          */
     182         [ +  + ]:         16 :         if (s2n_fingerprint_is_grease_value(version)) {
     183                 :          3 :             continue;
     184                 :          3 :         }
     185                 :            :         /**
     186                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
     187                 :            :          *# If extension 0x002b exists (supported_versions), then the version is
     188                 :            :          *# the highest value in the extension.
     189                 :            :          */
     190                 :         13 :         *client_version = MAX(*client_version, version);
     191                 :         13 :     }
     192                 :         12 :     return S2N_RESULT_OK;
     193                 :         12 : }
     194                 :            : 
     195                 :            : static S2N_RESULT s2n_fingerprint_ja4_version(struct s2n_stuffer *output,
     196                 :            :         struct s2n_client_hello *ch)
     197                 :         71 : {
     198                 :         71 :     uint16_t client_version = 0;
     199         [ +  + ]:         71 :     if (s2n_result_is_error(s2n_fingerprint_get_extension_version(ch, &client_version))) {
     200                 :            :         /**
     201                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
     202                 :            :          *# If the extension doesn’t exist, then the TLS version is the value of
     203                 :            :          *# the Protocol Version.
     204                 :            :          */
     205         [ -  + ]:         59 :         RESULT_GUARD(s2n_fingerprint_get_legacy_version(ch, &client_version));
     206                 :         59 :     }
     207                 :            : 
     208                 :            :     /**
     209                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
     210                 :            :      *# Handshake version (located at the top of the packet) should be ignored.
     211                 :            :      */
     212                 :            : 
     213                 :         71 :     const char *version_str = NULL;
     214         [ +  + ]:         71 :     if (client_version < s2n_array_len(s2n_ja4_version_strings)) {
     215                 :         68 :         version_str = s2n_ja4_version_strings[client_version];
     216                 :         68 :     }
     217         [ +  + ]:         71 :     if (version_str == NULL) {
     218                 :         57 :         version_str = S2N_JA4_UNKNOWN_STR;
     219                 :         57 :     }
     220         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_write_str(output, version_str));
     221                 :            : 
     222                 :         71 :     return S2N_RESULT_OK;
     223                 :         71 : }
     224                 :            : 
     225                 :            : static S2N_RESULT s2n_client_hello_get_first_alpn(struct s2n_client_hello *ch, struct s2n_blob *first)
     226                 :         71 : {
     227 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(ch);
     228                 :            : 
     229                 :         71 :     s2n_parsed_extension *extension = NULL;
     230         [ +  + ]:         71 :     RESULT_GUARD_POSIX(s2n_client_hello_get_parsed_extension(S2N_EXTENSION_ALPN,
     231                 :         16 :             &ch->extensions, &extension));
     232 [ -  + ][ #  # ]:         16 :     RESULT_ENSURE_REF(extension);
     233                 :            : 
     234                 :         16 :     struct s2n_stuffer protocols = { 0 };
     235         [ -  + ]:         16 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&protocols, &extension->extension));
     236                 :            : 
     237                 :         16 :     uint16_t list_size = 0;
     238         [ +  + ]:         16 :     RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&protocols, &list_size));
     239                 :            : 
     240         [ +  + ]:         11 :     RESULT_GUARD(s2n_protocol_preferences_read(&protocols, first));
     241                 :          9 :     return S2N_RESULT_OK;
     242                 :         11 : }
     243                 :            : 
     244                 :            : /**
     245                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
     246                 :            :  *# The first and last alphanumeric characters of the ALPN (Application-Layer
     247                 :            :  *# Protocol Negotiation) first value.
     248                 :            :  */
     249                 :            : static S2N_RESULT s2n_fingerprint_ja4_alpn(struct s2n_stuffer *output,
     250                 :            :         struct s2n_client_hello *ch)
     251                 :         71 : {
     252                 :         71 :     struct s2n_blob protocol = { 0 };
     253         [ +  + ]:         71 :     if (s2n_result_is_error(s2n_client_hello_get_first_alpn(ch, &protocol))) {
     254                 :         62 :         protocol.size = 0;
     255                 :         62 :     }
     256                 :            : 
     257                 :            :     /**
     258                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
     259                 :            :      *# If there is no ALPN extension, no ALPN values, or the first ALPN value
     260                 :            :      *# is empty, then we print "00" as the value in the fingerprint.
     261                 :            :      *
     262                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
     263                 :            :      *# If the first ALPN value is only a single character, then that character
     264                 :            :      *# is treated as both the first and last character.
     265                 :            :      */
     266                 :         71 :     uint8_t first_char = '0', last_char = '0';
     267         [ +  + ]:         71 :     if (protocol.size > 0) {
     268                 :          9 :         first_char = protocol.data[0];
     269                 :          9 :         last_char = protocol.data[protocol.size - 1];
     270                 :          9 :     }
     271                 :            : 
     272                 :            :     /**
     273                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
     274                 :            :      *# If the first or last byte of the first ALPN is non-alphanumeric (meaning
     275                 :            :      *# not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we print the first and
     276                 :            :      *# last characters of the hex representation of the first ALPN instead.
     277                 :            :      */
     278 [ +  + ][ +  + ]:         71 :     if (!isalnum(first_char) || !isalnum(last_char)) {
     279         [ -  + ]:          4 :         RESULT_GUARD(s2n_hex_digit((first_char >> 4), &first_char));
     280         [ -  + ]:          4 :         RESULT_GUARD(s2n_hex_digit((last_char & 0x0F), &last_char));
     281                 :          4 :     }
     282                 :            : 
     283         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, first_char));
     284         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, last_char));
     285                 :         71 :     return S2N_RESULT_OK;
     286                 :         71 : }
     287                 :            : 
     288                 :            : /* Part "a" of the fingerprint is a descriptive prefix.
     289                 :            :  *
     290                 :            :  * https://github.com/FoxIO-LLC/ja4/main/technical_details/JA4.md
     291                 :            :  *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”)
     292                 :            :  *# (2 character TLS version)
     293                 :            :  *# (SNI=”d” or no SNI=”i”)
     294                 :            :  *# (2 character count of ciphers)
     295                 :            :  *# (2 character count of extensions)
     296                 :            :  *# (first and last characters of first ALPN extension value)
     297                 :            :  */
     298                 :            : static S2N_RESULT s2n_fingerprint_ja4_a(struct s2n_fingerprint *fingerprint,
     299                 :            :         struct s2n_stuffer *output, struct s2n_blob *ciphers_count, struct s2n_blob *extensions_count)
     300                 :         71 : {
     301 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(fingerprint);
     302                 :            : 
     303                 :            :     /**
     304                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#quic-and-dtls
     305                 :            :      *# If the protocol is QUIC then the first character of the fingerprint is “q”,
     306                 :            :      *# if DTLS it is "d", else it is “t”.
     307                 :            :      *
     308                 :            :      * s2n-tls only supports TLS and QUIC. DTLS is not supported.
     309                 :            :      */
     310                 :         71 :     bool is_quic = false;
     311         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello,
     312                 :         71 :             TLS_EXTENSION_QUIC_TRANSPORT_PARAMETERS, &is_quic));
     313         [ +  + ]:         71 :     char protocol_char = (is_quic) ? 'q' : 't';
     314         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, protocol_char));
     315                 :            : 
     316         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_version(output, fingerprint->client_hello));
     317                 :            : 
     318                 :            :     /**
     319                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#sni
     320                 :            :      *# If the SNI extension (0x0000) exists, then the destination of the connection
     321                 :            :      *# is a domain, or “d” in the fingerprint.
     322                 :            :      *# If the SNI does not exist, then the destination is an IP address, or “i”.
     323                 :            :      */
     324                 :         71 :     bool has_sni = false;
     325         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello,
     326                 :         71 :             TLS_EXTENSION_SERVER_NAME, &has_sni));
     327         [ +  + ]:         71 :     char sni_char = (has_sni) ? 'd' : 'i';
     328         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, sni_char));
     329                 :            : 
     330                 :            :     /* Reserve two characters for the "count of ciphers".
     331                 :            :      * We'll calculate it later when we handle the cipher suite list for JA4_b.
     332                 :            :      */
     333                 :         71 :     uint8_t *ciphers_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE);
     334         [ -  + ]:         71 :     RESULT_GUARD_PTR(ciphers_count_mem);
     335         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_blob_init(ciphers_count, ciphers_count_mem, S2N_JA4_COUNT_SIZE));
     336                 :            : 
     337                 :            :     /* Reserve two characters for the "count of extensions".
     338                 :            :      * We'll calculate it later when we handle the extensions list for JA4_c.
     339                 :            :      */
     340                 :         71 :     uint8_t *extensions_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE);
     341         [ -  + ]:         71 :     RESULT_GUARD_PTR(extensions_count_mem);
     342         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_blob_init(extensions_count, extensions_count_mem, S2N_JA4_COUNT_SIZE));
     343                 :            : 
     344         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_alpn(output, fingerprint->client_hello));
     345                 :            : 
     346                 :         71 :     return S2N_RESULT_OK;
     347                 :         71 : }
     348                 :            : 
     349                 :            : /**
     350                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash
     351                 :            :  *# The list is created using the 4 character hex values of the ciphers,
     352                 :            :  *# lower case, comma delimited, ignoring GREASE.
     353                 :            :  */
     354                 :            : static S2N_RESULT s2n_fingerprint_ja4_ciphers(struct s2n_fingerprint_hash *hash,
     355                 :            :         struct s2n_client_hello *ch, struct s2n_stuffer *sort_space, uint16_t *ciphers_count)
     356                 :         71 : {
     357 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(ch);
     358 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(sort_space);
     359 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(ciphers_count);
     360                 :            : 
     361                 :         71 :     struct s2n_stuffer cipher_suites = { 0 };
     362         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&cipher_suites, &ch->cipher_suites));
     363                 :            : 
     364                 :         71 :     DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer);
     365         [ +  + ]:        737 :     while (s2n_stuffer_data_available(&cipher_suites)) {
     366                 :        666 :         uint16_t iana = 0;
     367         [ -  + ]:        666 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&cipher_suites, &iana));
     368                 :            :         /**
     369                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
     370                 :            :          *# Remember, ignore GREASE values. They don’t count.
     371                 :            :          */
     372         [ +  + ]:        666 :         if (s2n_fingerprint_is_grease_value(iana)) {
     373                 :          3 :             continue;
     374                 :          3 :         }
     375         [ -  + ]:        663 :         RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana));
     376         [ -  + ]:        663 :         RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV));
     377                 :        663 :     }
     378                 :            : 
     379                 :         71 :     size_t iana_list_size = s2n_stuffer_data_available(iana_list);
     380                 :         71 :     size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE;
     381                 :         71 :     *ciphers_count = iana_count;
     382         [ +  + ]:         71 :     if (iana_count == 0) {
     383                 :          1 :         return S2N_RESULT_OK;
     384                 :          1 :     }
     385                 :            : 
     386                 :         70 :     uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size);
     387 [ -  + ][ #  # ]:         70 :     RESULT_ENSURE_REF(ianas);
     388                 :         70 :     qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare);
     389         [ -  + ]:         70 :     RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1));
     390                 :         70 :     return S2N_RESULT_OK;
     391                 :         70 : }
     392                 :            : 
     393                 :            : /**
     394                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash
     395                 :            :  *# A 12 character truncated sha256 hash of the list of ciphers sorted in hex order,
     396                 :            :  *# first 12 characters.
     397                 :            :  */
     398                 :            : static S2N_RESULT s2n_fingerprint_ja4_b(struct s2n_fingerprint *fingerprint,
     399                 :            :         struct s2n_fingerprint_hash *hash, struct s2n_blob *ciphers_count,
     400                 :            :         struct s2n_stuffer *output)
     401                 :         71 : {
     402 [ -  + ][ #  # ]:         71 :     RESULT_ENSURE_REF(fingerprint);
     403                 :            : 
     404                 :         71 :     uint16_t ciphers_count_value = 0;
     405         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_ciphers(hash, fingerprint->client_hello,
     406                 :         71 :             &fingerprint->workspace, &ciphers_count_value));
     407                 :            : 
     408         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output));
     409         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_count(ciphers_count, ciphers_count_value));
     410                 :         71 :     return S2N_RESULT_OK;
     411                 :         71 : }
     412                 :            : 
     413                 :            : /**
     414                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     415                 :            :  *# The extension list is created using the 4 character hex values of the extensions,
     416                 :            :  *# lower case, comma delimited, sorted (not in the order they appear).
     417                 :            :  */
     418                 :            : static S2N_RESULT s2n_fingerprint_ja4_extensions(struct s2n_fingerprint_hash *hash,
     419                 :            :         struct s2n_client_hello *ch, struct s2n_stuffer *sort_space, uint16_t *extensions_count)
     420                 :         71 : {
     421 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(ch);
     422 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(sort_space);
     423 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(extensions_count);
     424                 :            : 
     425                 :         71 :     struct s2n_stuffer extensions = { 0 };
     426         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&extensions, &ch->extensions.raw));
     427                 :            : 
     428                 :         71 :     DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer);
     429         [ +  + ]:        689 :     while (s2n_stuffer_data_available(&extensions)) {
     430                 :        618 :         uint16_t iana = 0;
     431         [ -  + ]:        618 :         RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana));
     432                 :            : 
     433                 :            :         /**
     434                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
     435                 :            :          *# Ignore GREASE.
     436                 :            :          */
     437         [ +  + ]:        618 :         if (s2n_fingerprint_is_grease_value(iana)) {
     438                 :          3 :             continue;
     439                 :          3 :         }
     440                 :            : 
     441                 :            :         /* SNI and ALPN are included in the extension count, but not in the extension list.
     442                 :            :          *
     443                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     444                 :            :          *# Ignore the SNI extension (0000) and the ALPN extension (0010)
     445                 :            :          *# as we’ve already captured them in the _a_ section of the fingerprint.
     446                 :            :          *
     447                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
     448                 :            :          *# Include SNI and ALPN.
     449                 :            :          */
     450                 :        615 :         (*extensions_count)++;
     451 [ +  + ][ +  + ]:        615 :         if (iana == TLS_EXTENSION_SERVER_NAME || iana == S2N_EXTENSION_ALPN) {
     452                 :         23 :             continue;
     453                 :         23 :         }
     454         [ -  + ]:        592 :         RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana));
     455         [ -  + ]:        592 :         RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV));
     456                 :        592 :     }
     457                 :            : 
     458                 :         71 :     size_t iana_list_size = s2n_stuffer_data_available(iana_list);
     459                 :         71 :     size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE;
     460         [ +  + ]:         71 :     if (iana_count == 0) {
     461                 :         47 :         return S2N_RESULT_OK;
     462                 :         47 :     }
     463                 :            : 
     464                 :         24 :     uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size);
     465 [ -  + ][ #  # ]:         24 :     RESULT_ENSURE_REF(ianas);
     466                 :         24 :     qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare);
     467         [ -  + ]:         24 :     RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1));
     468                 :         24 :     return S2N_RESULT_OK;
     469                 :         24 : }
     470                 :            : 
     471                 :            : static S2N_RESULT s2n_fingerprint_ja4_sig_algs(struct s2n_fingerprint_hash *hash,
     472                 :            :         struct s2n_client_hello *ch)
     473                 :         71 : {
     474 [ #  # ][ -  + ]:         71 :     RESULT_ENSURE_REF(ch);
     475                 :            : 
     476                 :         71 :     s2n_parsed_extension *extension = NULL;
     477                 :         71 :     int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_SIGNATURE_ALGORITHMS,
     478                 :         71 :             &ch->extensions, &extension);
     479         [ +  + ]:         71 :     if (result != S2N_SUCCESS) {
     480                 :         66 :         return S2N_RESULT_OK;
     481                 :         66 :     }
     482 [ -  + ][ #  # ]:          5 :     RESULT_ENSURE_REF(extension);
     483                 :            : 
     484                 :          5 :     struct s2n_stuffer sig_algs = { 0 };
     485         [ -  + ]:          5 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&sig_algs, &extension->extension));
     486                 :            : 
     487                 :          5 :     uint8_t entry_bytes[S2N_JA4_IANA_ENTRY_SIZE] = { 0 };
     488                 :          5 :     struct s2n_stuffer entry = { 0 };
     489         [ -  + ]:          5 :     RESULT_GUARD_POSIX(s2n_blob_init(&entry.blob, entry_bytes, sizeof(entry_bytes)));
     490                 :            : 
     491                 :          5 :     bool is_first = true;
     492         [ +  + ]:          5 :     if (s2n_stuffer_skip_read(&sig_algs, sizeof(uint16_t)) != S2N_SUCCESS) {
     493                 :          1 :         return S2N_RESULT_OK;
     494                 :          1 :     }
     495         [ +  + ]:         39 :     while (s2n_stuffer_data_available(&sig_algs)) {
     496                 :         35 :         uint16_t iana = 0;
     497         [ -  + ]:         35 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&sig_algs, &iana));
     498         [ -  + ]:         35 :         if (s2n_fingerprint_is_grease_value(iana)) {
     499                 :          0 :             continue;
     500                 :          0 :         }
     501         [ +  + ]:         35 :         if (is_first) {
     502         [ -  + ]:          4 :             RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA4_PART_DIV));
     503                 :         31 :         } else {
     504         [ -  + ]:         31 :             RESULT_GUARD_POSIX(s2n_stuffer_write_char(&entry, S2N_JA4_LIST_DIV));
     505                 :         31 :         }
     506         [ -  + ]:         35 :         RESULT_GUARD(s2n_stuffer_write_uint16_hex(&entry, iana));
     507         [ -  + ]:         35 :         RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, entry_bytes,
     508                 :         35 :                 s2n_stuffer_data_available(&entry)));
     509         [ -  + ]:         35 :         RESULT_GUARD_POSIX(s2n_stuffer_rewrite(&entry));
     510                 :         35 :         is_first = false;
     511                 :         35 :     }
     512                 :          4 :     return S2N_RESULT_OK;
     513                 :          4 : }
     514                 :            : 
     515                 :            : /**
     516                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     517                 :            :  *# A 12 character truncated sha256 hash of the list of extensions, sorted by
     518                 :            :  *# hex value, followed by the list of signature algorithms, in the order that
     519                 :            :  *# they appear (not sorted).
     520                 :            :  */
     521                 :            : static S2N_RESULT s2n_fingerprint_ja4_c(struct s2n_fingerprint *fingerprint,
     522                 :            :         struct s2n_fingerprint_hash *hash, struct s2n_blob *extensions_count,
     523                 :            :         struct s2n_stuffer *output)
     524                 :         71 : {
     525 [ -  + ][ #  # ]:         71 :     RESULT_ENSURE_REF(fingerprint);
     526                 :            : 
     527                 :         71 :     uint16_t extensions_count_value = 0;
     528         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_extensions(hash, fingerprint->client_hello,
     529                 :         71 :             &fingerprint->workspace, &extensions_count_value));
     530                 :            : 
     531                 :            :     /**
     532                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     533                 :            :      *# The signature algorithm hex values are then added to the end of the list
     534                 :            :      *# in the order that they appear (not sorted) with an underscore delimiting
     535                 :            :      *# the two lists.
     536                 :            :      *
     537                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     538                 :            :      *# If there are no signature algorithms in the hello packet,
     539                 :            :      *# then the string ends without an underscore and is hashed.
     540                 :            :      *
     541                 :            :      * s2n_fingerprint_ja4_sig_algs handles writing the underscore because we
     542                 :            :      * need to skip writing it if there are no signature algorithms.
     543                 :            :      */
     544         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_sig_algs(hash, fingerprint->client_hello));
     545                 :            : 
     546         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output));
     547         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_count(extensions_count, extensions_count_value));
     548                 :         71 :     return S2N_RESULT_OK;
     549                 :         71 : }
     550                 :            : 
     551                 :            : /* JA4 fingerprints are basically of the form a_b_c:
     552                 :            :  *
     553                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#ja4-algorithm
     554                 :            :  *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”)
     555                 :            :  *# (2 character TLS version)
     556                 :            :  *# (SNI=”d” or no SNI=”i”)
     557                 :            :  *# (2 character count of ciphers)
     558                 :            :  *# (2 character count of extensions)
     559                 :            :  *# (first and last characters of first ALPN extension value)
     560                 :            :  *# _
     561                 :            :  *# (sha256 hash of the list of cipher hex codes sorted in hex order, truncated to 12 characters)
     562                 :            :  *# _
     563                 :            :  *# (sha256 hash of (the list of extension hex codes sorted in hex order)_(the list of signature algorithms), truncated to 12 characters)
     564                 :            :  *#
     565                 :            :  *# The end result is a fingerprint that looks like:
     566                 :            :  *# t13d1516h2_8daaf6152771_b186095e22b6
     567                 :            :  */
     568                 :            : static S2N_RESULT s2n_fingerprint_ja4(struct s2n_fingerprint *fingerprint,
     569                 :            :         struct s2n_fingerprint_hash *hash, struct s2n_stuffer *output)
     570                 :         71 : {
     571 [ -  + ][ #  # ]:         71 :     RESULT_ENSURE_REF(fingerprint);
     572 [ -  + ][ #  # ]:         71 :     RESULT_ENSURE_REF(hash);
     573 [ -  + ][ #  # ]:         71 :     RESULT_ENSURE_REF(output);
     574                 :            : 
     575         [ +  - ]:         71 :     if (s2n_stuffer_is_freed(&fingerprint->workspace)) {
     576         [ -  + ]:         71 :         RESULT_GUARD_POSIX(s2n_stuffer_growable_alloc(&fingerprint->workspace, S2N_JA4_WORKSPACE_SIZE));
     577                 :         71 :     }
     578                 :            : 
     579                 :         71 :     struct s2n_blob ciphers_count = { 0 };
     580                 :         71 :     struct s2n_blob extensions_count = { 0 };
     581         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_a(fingerprint, output, &ciphers_count, &extensions_count));
     582         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV));
     583         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_b(fingerprint, hash, &ciphers_count, output));
     584         [ -  + ]:         71 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV));
     585         [ -  + ]:         71 :     RESULT_GUARD(s2n_fingerprint_ja4_c(fingerprint, hash, &extensions_count, output));
     586                 :            : 
     587         [ +  + ]:         71 :     if (s2n_fingerprint_hash_do_digest(hash)) {
     588                 :            :         /* The extra two bytes are for the characters separating the parts */
     589                 :         62 :         fingerprint->raw_size = hash->bytes_digested + S2N_JA4_A_SIZE + 2;
     590                 :         62 :     } else {
     591                 :          9 :         fingerprint->raw_size = s2n_stuffer_data_available(output);
     592                 :          9 :     }
     593                 :            : 
     594                 :         71 :     return S2N_RESULT_OK;
     595                 :         71 : }
     596                 :            : 
     597                 :            : struct s2n_fingerprint_method ja4_fingerprint = {
     598                 :            :     .hash = S2N_HASH_SHA256,
     599                 :            :     .hash_str_size = S2N_JA4_SIZE,
     600                 :            :     .fingerprint = s2n_fingerprint_ja4,
     601                 :            : };

Generated by: LCOV version 1.14