LCOV - code coverage report
Current view: top level - tls - s2n_fingerprint_ja4.c (source / functions) Hit Total Coverage
Test: unit_test_coverage.info Lines: 279 281 99.3 %
Date: 2026-10-06 07:26:09 Functions: 14 14 100.0 %
Branches: 156 288 54.2 %

           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                 :        158 : #define S2N_JA4_COUNT_SIZE 2
      39                 :            : 
      40                 :      22706 : #define S2N_HEX_PER_BYTE              2
      41                 :         93 : #define S2N_JA4_DIGEST_HEX_CHAR_LIMIT 12
      42                 :         93 : #define S2N_JA4_DIGEST_BYTE_LIMIT     (S2N_JA4_DIGEST_HEX_CHAR_LIMIT / S2N_HEX_PER_BYTE)
      43                 :            : 
      44                 :         70 : #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                 :       2365 : #define S2N_JA4_LIST_LIMIT      99
      50                 :      22613 : #define S2N_JA4_IANA_HEX_SIZE   (S2N_HEX_PER_BYTE * sizeof(uint16_t))
      51                 :        264 : #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                 :         65 : #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                 :       5347 : {
      85                 :       5347 :     const uint8_t *iana_a = (const uint8_t *) a;
      86                 :       5347 :     const uint8_t *iana_b = (const uint8_t *) b;
      87         [ +  + ]:      22349 :     for (size_t i = 0; i < S2N_JA4_IANA_HEX_SIZE; i++) {
      88         [ +  + ]:      20005 :         if (iana_a[i] != iana_b[i]) {
      89                 :       3003 :             return iana_a[i] - iana_b[i];
      90                 :       3003 :         }
      91                 :      20005 :     }
      92                 :       2344 :     return 0;
      93                 :       5347 : }
      94                 :            : 
      95                 :            : static S2N_RESULT s2n_fingerprint_ja4_digest(struct s2n_fingerprint_hash *hash,
      96                 :            :         struct s2n_stuffer *out)
      97                 :        158 : {
      98 [ -  + ][ #  # ]:        158 :     RESULT_ENSURE_REF(hash);
      99         [ +  + ]:        158 :     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                 :        140 :     uint64_t bytes = 0;
     115         [ -  + ]:        140 :     RESULT_GUARD_POSIX(s2n_hash_get_currently_in_hash_total(hash->hash, &bytes));
     116         [ +  + ]:        140 :     if (bytes == 0) {
     117         [ -  + ]:         47 :         RESULT_GUARD_POSIX(s2n_stuffer_write_str(out, "000000000000"));
     118                 :         47 :         return S2N_RESULT_OK;
     119                 :         47 :     }
     120                 :            : 
     121                 :         93 :     uint8_t digest_bytes[SHA256_DIGEST_LENGTH] = { 0 };
     122                 :         93 :     struct s2n_blob digest = { 0 };
     123         [ -  + ]:         93 :     RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes)));
     124         [ -  + ]:         93 :     RESULT_GUARD(s2n_fingerprint_hash_digest(hash, &digest));
     125                 :            : 
     126                 :            :     /* JA4 digests are truncated */
     127 [ #  # ][ -  + ]:         93 :     RESULT_ENSURE_LTE(S2N_JA4_DIGEST_BYTE_LIMIT, digest.size);
     128                 :         93 :     digest.size = S2N_JA4_DIGEST_BYTE_LIMIT;
     129         [ -  + ]:         93 :     RESULT_GUARD(s2n_stuffer_write_hex(out, &digest));
     130                 :         93 :     return S2N_RESULT_OK;
     131                 :         93 : }
     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                 :        158 : {
     143 [ #  # ][ -  + ]:        158 :     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         [ +  + ]:        158 :     count = S2N_MIN(count, 99);
     153                 :            : 
     154 [ -  + ][ #  # ]:        158 :     RESULT_ENSURE_EQ(output->size, 2);
     155                 :        158 :     output->data[0] = (count / 10) + '0';
     156                 :        158 :     output->data[1] = (count % 10) + '0';
     157                 :        158 :     return S2N_RESULT_OK;
     158                 :        158 : }
     159                 :            : 
     160                 :            : static S2N_RESULT s2n_fingerprint_get_extension_version(struct s2n_client_hello *ch,
     161                 :            :         uint16_t *client_version)
     162                 :         79 : {
     163 [ -  + ][ #  # ]:         79 :     RESULT_ENSURE_REF(ch);
     164 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(client_version);
     165                 :            : 
     166                 :         79 :     s2n_parsed_extension *extension = NULL;
     167         [ +  + ]:         79 :     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 = S2N_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                 :         79 : {
     198                 :         79 :     uint16_t client_version = 0;
     199         [ +  + ]:         79 :     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         [ -  + ]:         67 :         RESULT_GUARD(s2n_fingerprint_get_legacy_version(ch, &client_version));
     206                 :         67 :     }
     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                 :         79 :     const char *version_str = NULL;
     214         [ +  + ]:         79 :     if (client_version < s2n_array_len(s2n_ja4_version_strings)) {
     215                 :         76 :         version_str = s2n_ja4_version_strings[client_version];
     216                 :         76 :     }
     217         [ +  + ]:         79 :     if (version_str == NULL) {
     218                 :         65 :         version_str = S2N_JA4_UNKNOWN_STR;
     219                 :         65 :     }
     220         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_write_str(output, version_str));
     221                 :            : 
     222                 :         79 :     return S2N_RESULT_OK;
     223                 :         79 : }
     224                 :            : 
     225                 :            : static S2N_RESULT s2n_client_hello_get_first_alpn(struct s2n_client_hello *ch, struct s2n_blob *first)
     226                 :         79 : {
     227 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(ch);
     228                 :            : 
     229                 :         79 :     s2n_parsed_extension *extension = NULL;
     230         [ +  + ]:         79 :     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                 :         79 : {
     252                 :         79 :     struct s2n_blob protocol = { 0 };
     253         [ +  + ]:         79 :     if (s2n_result_is_error(s2n_client_hello_get_first_alpn(ch, &protocol))) {
     254                 :         70 :         protocol.size = 0;
     255                 :         70 :     }
     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                 :         79 :     uint8_t first_char = '0', last_char = '0';
     267         [ +  + ]:         79 :     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 [ +  + ][ +  + ]:         79 :     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         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, first_char));
     284         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, last_char));
     285                 :         79 :     return S2N_RESULT_OK;
     286                 :         79 : }
     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                 :         79 : {
     301 [ #  # ][ -  + ]:         79 :     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                 :         79 :     bool is_quic = false;
     311         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello,
     312                 :         79 :             TLS_EXTENSION_QUIC_TRANSPORT_PARAMETERS, &is_quic));
     313         [ +  + ]:         79 :     char protocol_char = (is_quic) ? 'q' : 't';
     314         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, protocol_char));
     315                 :            : 
     316         [ -  + ]:         79 :     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                 :         79 :     bool has_sni = false;
     325         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello,
     326                 :         79 :             TLS_EXTENSION_SERVER_NAME, &has_sni));
     327         [ +  + ]:         79 :     char sni_char = (has_sni) ? 'd' : 'i';
     328         [ -  + ]:         79 :     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                 :         79 :     uint8_t *ciphers_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE);
     334         [ -  + ]:         79 :     RESULT_GUARD_PTR(ciphers_count_mem);
     335         [ -  + ]:         79 :     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                 :         79 :     uint8_t *extensions_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE);
     341         [ -  + ]:         79 :     RESULT_GUARD_PTR(extensions_count_mem);
     342         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_blob_init(extensions_count, extensions_count_mem, S2N_JA4_COUNT_SIZE));
     343                 :            : 
     344         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_alpn(output, fingerprint->client_hello));
     345                 :            : 
     346                 :         79 :     return S2N_RESULT_OK;
     347                 :         79 : }
     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                 :         79 : {
     357 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(ch);
     358 [ -  + ][ #  # ]:         79 :     RESULT_ENSURE_REF(sort_space);
     359 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(ciphers_count);
     360                 :            : 
     361                 :         79 :     struct s2n_stuffer cipher_suites = { 0 };
     362         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&cipher_suites, &ch->cipher_suites));
     363                 :            : 
     364                 :         79 :     DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer);
     365                 :         79 :     size_t written_count = 0;
     366         [ +  + ]:       1302 :     while (s2n_stuffer_data_available(&cipher_suites)) {
     367                 :       1223 :         uint16_t iana = 0;
     368         [ -  + ]:       1223 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&cipher_suites, &iana));
     369                 :            :         /**
     370                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
     371                 :            :          *# Remember, ignore GREASE values. They don’t count.
     372                 :            :          */
     373         [ +  + ]:       1223 :         if (s2n_fingerprint_is_grease_value(iana)) {
     374                 :          3 :             continue;
     375                 :          3 :         }
     376                 :            :         /* The count is capped at 99 (see s2n_fingerprint_ja4_count), so there
     377                 :            :          * is no need to collect more than S2N_JA4_LIST_LIMIT entries. This keeps
     378                 :            :          * the workspace within its pre-sized bounds.
     379                 :            :          */
     380         [ +  + ]:       1220 :         if (written_count >= S2N_JA4_LIST_LIMIT) {
     381                 :        314 :             continue;
     382                 :        314 :         }
     383                 :        906 :         written_count++;
     384         [ -  + ]:        906 :         RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana));
     385         [ -  + ]:        906 :         RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV));
     386                 :        906 :     }
     387                 :            : 
     388                 :         79 :     size_t iana_list_size = s2n_stuffer_data_available(iana_list);
     389                 :         79 :     size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE;
     390                 :         79 :     *ciphers_count = iana_count;
     391         [ +  + ]:         79 :     if (iana_count == 0) {
     392                 :          1 :         return S2N_RESULT_OK;
     393                 :          1 :     }
     394                 :            : 
     395                 :         78 :     uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size);
     396 [ #  # ][ -  + ]:         78 :     RESULT_ENSURE_REF(ianas);
     397                 :         78 :     qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare);
     398         [ -  + ]:         78 :     RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1));
     399                 :         78 :     return S2N_RESULT_OK;
     400                 :         78 : }
     401                 :            : 
     402                 :            : /**
     403                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash
     404                 :            :  *# A 12 character truncated sha256 hash of the list of ciphers sorted in hex order,
     405                 :            :  *# first 12 characters.
     406                 :            :  */
     407                 :            : static S2N_RESULT s2n_fingerprint_ja4_b(struct s2n_fingerprint *fingerprint,
     408                 :            :         struct s2n_fingerprint_hash *hash, struct s2n_blob *ciphers_count,
     409                 :            :         struct s2n_stuffer *output)
     410                 :         79 : {
     411 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(fingerprint);
     412                 :            : 
     413                 :         79 :     uint16_t ciphers_count_value = 0;
     414         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_ciphers(hash, fingerprint->client_hello,
     415                 :         79 :             &fingerprint->workspace, &ciphers_count_value));
     416                 :            : 
     417         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output));
     418         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_count(ciphers_count, ciphers_count_value));
     419                 :         79 :     return S2N_RESULT_OK;
     420                 :         79 : }
     421                 :            : 
     422                 :            : /**
     423                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     424                 :            :  *# The extension list is created using the 4 character hex values of the extensions,
     425                 :            :  *# lower case, comma delimited, sorted (not in the order they appear).
     426                 :            :  */
     427                 :            : static S2N_RESULT s2n_fingerprint_ja4_extensions(struct s2n_fingerprint_hash *hash,
     428                 :            :         struct s2n_client_hello *ch, struct s2n_stuffer *sort_space, uint16_t *extensions_count)
     429                 :         79 : {
     430 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(ch);
     431 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(sort_space);
     432 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(extensions_count);
     433                 :            : 
     434                 :         79 :     struct s2n_stuffer extensions = { 0 };
     435         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&extensions, &ch->extensions.raw));
     436                 :            : 
     437                 :         79 :     DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer);
     438                 :         79 :     size_t written_count = 0;
     439         [ +  + ]:       1250 :     while (s2n_stuffer_data_available(&extensions)) {
     440                 :       1171 :         uint16_t iana = 0;
     441         [ -  + ]:       1171 :         RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana));
     442                 :            : 
     443                 :            :         /**
     444                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
     445                 :            :          *# Ignore GREASE.
     446                 :            :          */
     447         [ +  + ]:       1171 :         if (s2n_fingerprint_is_grease_value(iana)) {
     448                 :          3 :             continue;
     449                 :          3 :         }
     450                 :            : 
     451                 :            :         /* SNI and ALPN are included in the extension count, but not in the extension list.
     452                 :            :          *
     453                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     454                 :            :          *# Ignore the SNI extension (0000) and the ALPN extension (0010)
     455                 :            :          *# as we’ve already captured them in the _a_ section of the fingerprint.
     456                 :            :          *
     457                 :            :          *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
     458                 :            :          *# Include SNI and ALPN.
     459                 :            :          */
     460                 :       1168 :         (*extensions_count)++;
     461 [ +  + ][ +  + ]:       1168 :         if (iana == TLS_EXTENSION_SERVER_NAME || iana == S2N_EXTENSION_ALPN) {
     462                 :         23 :             continue;
     463                 :         23 :         }
     464                 :            :         /* The count is capped at 99 (see s2n_fingerprint_ja4_count), so there
     465                 :            :          * is no need to collect more than S2N_JA4_LIST_LIMIT entries. This keeps
     466                 :            :          * the workspace within its pre-sized bounds. extensions_count is still
     467                 :            :          * incremented above for every extension.
     468                 :            :          */
     469         [ +  + ]:       1145 :         if (written_count >= S2N_JA4_LIST_LIMIT) {
     470                 :        314 :             continue;
     471                 :        314 :         }
     472                 :        831 :         written_count++;
     473         [ -  + ]:        831 :         RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana));
     474         [ -  + ]:        831 :         RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV));
     475                 :        831 :     }
     476                 :            : 
     477                 :         79 :     size_t iana_list_size = s2n_stuffer_data_available(iana_list);
     478                 :         79 :     size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE;
     479         [ +  + ]:         79 :     if (iana_count == 0) {
     480                 :         51 :         return S2N_RESULT_OK;
     481                 :         51 :     }
     482                 :            : 
     483                 :         28 :     uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size);
     484 [ #  # ][ -  + ]:         28 :     RESULT_ENSURE_REF(ianas);
     485                 :         28 :     qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare);
     486         [ -  + ]:         28 :     RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1));
     487                 :         28 :     return S2N_RESULT_OK;
     488                 :         28 : }
     489                 :            : 
     490                 :            : static S2N_RESULT s2n_fingerprint_ja4_sig_algs(struct s2n_fingerprint_hash *hash,
     491                 :            :         struct s2n_client_hello *ch)
     492                 :         79 : {
     493 [ #  # ][ -  + ]:         79 :     RESULT_ENSURE_REF(ch);
     494                 :            : 
     495                 :         79 :     s2n_parsed_extension *extension = NULL;
     496                 :         79 :     int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_SIGNATURE_ALGORITHMS,
     497                 :         79 :             &ch->extensions, &extension);
     498         [ +  + ]:         79 :     if (result != S2N_SUCCESS) {
     499                 :         74 :         return S2N_RESULT_OK;
     500                 :         74 :     }
     501 [ -  + ][ #  # ]:          5 :     RESULT_ENSURE_REF(extension);
     502                 :            : 
     503                 :          5 :     struct s2n_stuffer sig_algs = { 0 };
     504         [ -  + ]:          5 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&sig_algs, &extension->extension));
     505                 :            : 
     506                 :          5 :     uint8_t entry_bytes[S2N_JA4_IANA_ENTRY_SIZE] = { 0 };
     507                 :          5 :     struct s2n_stuffer entry = { 0 };
     508         [ -  + ]:          5 :     RESULT_GUARD_POSIX(s2n_blob_init(&entry.blob, entry_bytes, sizeof(entry_bytes)));
     509                 :            : 
     510                 :          5 :     bool is_first = true;
     511         [ +  + ]:          5 :     if (s2n_stuffer_skip_read(&sig_algs, sizeof(uint16_t)) != S2N_SUCCESS) {
     512                 :          1 :         return S2N_RESULT_OK;
     513                 :          1 :     }
     514         [ +  + ]:         39 :     while (s2n_stuffer_data_available(&sig_algs)) {
     515                 :         35 :         uint16_t iana = 0;
     516         [ -  + ]:         35 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&sig_algs, &iana));
     517         [ -  + ]:         35 :         if (s2n_fingerprint_is_grease_value(iana)) {
     518                 :          0 :             continue;
     519                 :          0 :         }
     520         [ +  + ]:         35 :         if (is_first) {
     521         [ -  + ]:          4 :             RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA4_PART_DIV));
     522                 :         31 :         } else {
     523         [ -  + ]:         31 :             RESULT_GUARD_POSIX(s2n_stuffer_write_char(&entry, S2N_JA4_LIST_DIV));
     524                 :         31 :         }
     525         [ -  + ]:         35 :         RESULT_GUARD(s2n_stuffer_write_uint16_hex(&entry, iana));
     526         [ -  + ]:         35 :         RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, entry_bytes,
     527                 :         35 :                 s2n_stuffer_data_available(&entry)));
     528         [ -  + ]:         35 :         RESULT_GUARD_POSIX(s2n_stuffer_rewrite(&entry));
     529                 :         35 :         is_first = false;
     530                 :         35 :     }
     531                 :          4 :     return S2N_RESULT_OK;
     532                 :          4 : }
     533                 :            : 
     534                 :            : /**
     535                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     536                 :            :  *# A 12 character truncated sha256 hash of the list of extensions, sorted by
     537                 :            :  *# hex value, followed by the list of signature algorithms, in the order that
     538                 :            :  *# they appear (not sorted).
     539                 :            :  */
     540                 :            : static S2N_RESULT s2n_fingerprint_ja4_c(struct s2n_fingerprint *fingerprint,
     541                 :            :         struct s2n_fingerprint_hash *hash, struct s2n_blob *extensions_count,
     542                 :            :         struct s2n_stuffer *output)
     543                 :         79 : {
     544 [ -  + ][ #  # ]:         79 :     RESULT_ENSURE_REF(fingerprint);
     545                 :            : 
     546                 :         79 :     uint16_t extensions_count_value = 0;
     547         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_extensions(hash, fingerprint->client_hello,
     548                 :         79 :             &fingerprint->workspace, &extensions_count_value));
     549                 :            : 
     550                 :            :     /**
     551                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     552                 :            :      *# The signature algorithm hex values are then added to the end of the list
     553                 :            :      *# in the order that they appear (not sorted) with an underscore delimiting
     554                 :            :      *# the two lists.
     555                 :            :      *
     556                 :            :      *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
     557                 :            :      *# If there are no signature algorithms in the hello packet,
     558                 :            :      *# then the string ends without an underscore and is hashed.
     559                 :            :      *
     560                 :            :      * s2n_fingerprint_ja4_sig_algs handles writing the underscore because we
     561                 :            :      * need to skip writing it if there are no signature algorithms.
     562                 :            :      */
     563         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_sig_algs(hash, fingerprint->client_hello));
     564                 :            : 
     565         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output));
     566         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_count(extensions_count, extensions_count_value));
     567                 :         79 :     return S2N_RESULT_OK;
     568                 :         79 : }
     569                 :            : 
     570                 :            : /* JA4 fingerprints are basically of the form a_b_c:
     571                 :            :  *
     572                 :            :  *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#ja4-algorithm
     573                 :            :  *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”)
     574                 :            :  *# (2 character TLS version)
     575                 :            :  *# (SNI=”d” or no SNI=”i”)
     576                 :            :  *# (2 character count of ciphers)
     577                 :            :  *# (2 character count of extensions)
     578                 :            :  *# (first and last characters of first ALPN extension value)
     579                 :            :  *# _
     580                 :            :  *# (sha256 hash of the list of cipher hex codes sorted in hex order, truncated to 12 characters)
     581                 :            :  *# _
     582                 :            :  *# (sha256 hash of (the list of extension hex codes sorted in hex order)_(the list of signature algorithms), truncated to 12 characters)
     583                 :            :  *#
     584                 :            :  *# The end result is a fingerprint that looks like:
     585                 :            :  *# t13d1516h2_8daaf6152771_b186095e22b6
     586                 :            :  */
     587                 :            : static S2N_RESULT s2n_fingerprint_ja4(struct s2n_fingerprint *fingerprint,
     588                 :            :         struct s2n_fingerprint_hash *hash, struct s2n_stuffer *output)
     589                 :         79 : {
     590 [ -  + ][ #  # ]:         79 :     RESULT_ENSURE_REF(fingerprint);
     591 [ -  + ][ #  # ]:         79 :     RESULT_ENSURE_REF(hash);
     592 [ -  + ][ #  # ]:         79 :     RESULT_ENSURE_REF(output);
     593                 :            : 
     594         [ +  - ]:         79 :     if (s2n_stuffer_is_freed(&fingerprint->workspace)) {
     595         [ -  + ]:         79 :         RESULT_GUARD_POSIX(s2n_stuffer_growable_alloc(&fingerprint->workspace, S2N_JA4_WORKSPACE_SIZE));
     596                 :         79 :     }
     597                 :            : 
     598                 :         79 :     struct s2n_blob ciphers_count = { 0 };
     599                 :         79 :     struct s2n_blob extensions_count = { 0 };
     600         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_a(fingerprint, output, &ciphers_count, &extensions_count));
     601         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV));
     602         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_b(fingerprint, hash, &ciphers_count, output));
     603         [ -  + ]:         79 :     RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV));
     604         [ -  + ]:         79 :     RESULT_GUARD(s2n_fingerprint_ja4_c(fingerprint, hash, &extensions_count, output));
     605                 :            : 
     606         [ +  + ]:         79 :     if (s2n_fingerprint_hash_do_digest(hash)) {
     607                 :            :         /* The extra two bytes are for the characters separating the parts */
     608                 :         70 :         fingerprint->raw_size = hash->bytes_digested + S2N_JA4_A_SIZE + 2;
     609                 :         70 :     } else {
     610                 :          9 :         fingerprint->raw_size = s2n_stuffer_data_available(output);
     611                 :          9 :     }
     612                 :            : 
     613                 :         79 :     return S2N_RESULT_OK;
     614                 :         79 : }
     615                 :            : 
     616                 :            : struct s2n_fingerprint_method ja4_fingerprint = {
     617                 :            :     .hash = S2N_HASH_SHA256,
     618                 :            :     .hash_str_size = S2N_JA4_SIZE,
     619                 :            :     .fingerprint = s2n_fingerprint_ja4,
     620                 :            : };

Generated by: LCOV version 1.14