LCOV - code coverage report
Current view: top level - tls - s2n_fingerprint_ja3.c (source / functions) Hit Total Coverage
Test: unit_test_coverage.info Lines: 119 119 100.0 %
Date: 2025-08-15 07:28:39 Functions: 9 9 100.0 %
Branches: 70 112 62.5 %

           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 "tls/extensions/s2n_extension_list.h"
      17                 :            : #include "tls/s2n_fingerprint.h"
      18                 :            : #include "utils/s2n_blob.h"
      19                 :            : #include "utils/s2n_safety.h"
      20                 :            : 
      21                 :            : #define S2N_JA3_FIELD_DIV ','
      22                 :            : #define S2N_JA3_LIST_DIV  '-'
      23                 :            : 
      24                 :            : /* UINT16_MAX == 65535 */
      25                 :            : #define S2N_UINT16_STR_MAX_SIZE 5
      26                 :            : 
      27                 :            : static S2N_RESULT s2n_fingerprint_ja3_digest(struct s2n_fingerprint_hash *hash,
      28                 :            :         struct s2n_stuffer *out)
      29                 :         67 : {
      30         [ +  + ]:         67 :     if (!s2n_fingerprint_hash_do_digest(hash)) {
      31                 :         40 :         return S2N_RESULT_OK;
      32                 :         40 :     }
      33                 :            : 
      34                 :         27 :     uint8_t digest_bytes[MD5_DIGEST_LENGTH] = { 0 };
      35                 :         27 :     struct s2n_blob digest = { 0 };
      36         [ -  + ]:         27 :     RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes)));
      37         [ -  + ]:         27 :     RESULT_GUARD(s2n_fingerprint_hash_digest(hash, &digest));
      38         [ -  + ]:         27 :     RESULT_GUARD(s2n_stuffer_write_hex(out, &digest));
      39                 :            : 
      40                 :         27 :     return S2N_RESULT_OK;
      41                 :         27 : }
      42                 :            : 
      43                 :            : static S2N_RESULT s2n_fingerprint_ja3_iana(struct s2n_fingerprint_hash *hash,
      44                 :            :         bool *is_list, uint16_t iana)
      45                 :       1577 : {
      46         [ +  + ]:       1577 :     if (s2n_fingerprint_is_grease_value(iana)) {
      47                 :         22 :         return S2N_RESULT_OK;
      48                 :         22 :     }
      49                 :            : 
      50                 :            :     /* If we have already written at least one value for this field,
      51                 :            :      * then we are writing a list and need to prepend a list divider before
      52                 :            :      * writing the next value.
      53                 :            :      */
      54         [ +  + ]:       1555 :     if (*is_list) {
      55         [ +  + ]:       1313 :         RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_LIST_DIV));
      56                 :       1313 :     } else {
      57                 :        242 :         *is_list = true;
      58                 :        242 :     }
      59                 :            : 
      60                 :            :     /* snprintf always appends a '\0' to the output,
      61                 :            :      * but that extra '\0' is not included in the return value */
      62                 :       1554 :     char str[S2N_UINT16_STR_MAX_SIZE + 1] = { 0 };
      63                 :       1554 :     int written = snprintf(str, sizeof(str), "%u", iana);
      64 [ #  # ][ -  + ]:       1554 :     RESULT_ENSURE_GT(written, 0);
      65 [ #  # ][ -  + ]:       1554 :     RESULT_ENSURE_LTE(written, S2N_UINT16_STR_MAX_SIZE);
      66                 :            : 
      67         [ +  + ]:       1554 :     RESULT_GUARD(s2n_fingerprint_hash_add_str(hash, str, written));
      68                 :       1546 :     return S2N_RESULT_OK;
      69                 :       1554 : }
      70                 :            : 
      71                 :            : static S2N_RESULT s2n_fingerprint_ja3_version(struct s2n_fingerprint_hash *hash,
      72                 :            :         struct s2n_client_hello *ch)
      73                 :         78 : {
      74                 :         78 :     uint16_t version = 0;
      75         [ -  + ]:         78 :     RESULT_GUARD(s2n_fingerprint_get_legacy_version(ch, &version));
      76                 :            : 
      77                 :         78 :     bool is_list = false;
      78         [ +  + ]:         78 :     RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &is_list, version));
      79                 :         76 :     return S2N_RESULT_OK;
      80                 :         78 : }
      81                 :            : 
      82                 :            : static S2N_RESULT s2n_fingerprint_ja3_cipher_suites(struct s2n_fingerprint_hash *hash,
      83                 :            :         struct s2n_client_hello *ch)
      84                 :         75 : {
      85 [ -  + ][ #  # ]:         75 :     RESULT_ENSURE_REF(ch);
      86                 :            : 
      87                 :         75 :     struct s2n_stuffer ciphers = { 0 };
      88         [ -  + ]:         75 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&ciphers, &ch->cipher_suites));
      89                 :            : 
      90                 :         75 :     bool found = false;
      91         [ +  + ]:       1151 :     while (s2n_stuffer_data_available(&ciphers)) {
      92                 :       1082 :         uint16_t iana = 0;
      93         [ -  + ]:       1082 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&ciphers, &iana));
      94         [ +  + ]:       1082 :         RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
      95                 :       1082 :     }
      96                 :         69 :     return S2N_RESULT_OK;
      97                 :         75 : }
      98                 :            : 
      99                 :            : static S2N_RESULT s2n_fingerprint_ja3_extensions(struct s2n_fingerprint_hash *hash,
     100                 :            :         struct s2n_client_hello *ch)
     101                 :         69 : {
     102 [ #  # ][ -  + ]:         69 :     RESULT_ENSURE_REF(ch);
     103                 :            : 
     104                 :            :     /* We have to use the raw extensions instead of the parsed extensions
     105                 :            :      * because s2n-tls both intentionally ignores any unknown extensions
     106                 :            :      * and reorders the extensions when parsing the list.
     107                 :            :      */
     108                 :         69 :     struct s2n_stuffer extensions = { 0 };
     109         [ -  + ]:         69 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&extensions, &ch->extensions.raw));
     110                 :            : 
     111                 :         69 :     bool found = false;
     112         [ +  + ]:        321 :     while (s2n_stuffer_data_available(&extensions)) {
     113                 :        252 :         uint16_t iana = 0;
     114         [ -  + ]:        252 :         RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana));
     115         [ -  + ]:        252 :         RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
     116                 :        252 :     }
     117                 :         69 :     return S2N_RESULT_OK;
     118                 :         69 : }
     119                 :            : 
     120                 :            : static S2N_RESULT s2n_fingerprint_ja3_elliptic_curves(struct s2n_fingerprint_hash *hash,
     121                 :            :         struct s2n_client_hello *ch)
     122                 :         69 : {
     123 [ #  # ][ -  + ]:         69 :     RESULT_ENSURE_REF(ch);
     124                 :            : 
     125                 :         69 :     s2n_parsed_extension *extension = NULL;
     126                 :         69 :     int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_SUPPORTED_GROUPS,
     127                 :         69 :             &ch->extensions, &extension);
     128         [ +  + ]:         69 :     if (result != S2N_SUCCESS) {
     129                 :         38 :         return S2N_RESULT_OK;
     130                 :         38 :     }
     131                 :            : 
     132                 :         31 :     struct s2n_stuffer elliptic_curves = { 0 };
     133         [ -  + ]:         31 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&elliptic_curves, &extension->extension));
     134         [ -  + ]:         31 :     RESULT_GUARD_POSIX(s2n_stuffer_skip_read(&elliptic_curves, sizeof(uint16_t)));
     135                 :            : 
     136                 :         31 :     bool found = false;
     137         [ +  + ]:        141 :     while (s2n_stuffer_data_available(&elliptic_curves)) {
     138                 :        110 :         uint16_t iana = 0;
     139         [ -  + ]:        110 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&elliptic_curves, &iana));
     140         [ -  + ]:        110 :         RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
     141                 :        110 :     }
     142                 :         31 :     return S2N_RESULT_OK;
     143                 :         31 : }
     144                 :            : 
     145                 :            : static S2N_RESULT s2n_fingerprint_ja3_point_formats(struct s2n_fingerprint_hash *hash,
     146                 :            :         struct s2n_client_hello *ch)
     147                 :         68 : {
     148 [ #  # ][ -  + ]:         68 :     RESULT_ENSURE_REF(ch);
     149                 :            : 
     150                 :         68 :     s2n_parsed_extension *extension = NULL;
     151                 :         68 :     int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_EC_POINT_FORMATS,
     152                 :         68 :             &ch->extensions, &extension);
     153         [ +  + ]:         68 :     if (result != S2N_SUCCESS) {
     154                 :         41 :         return S2N_RESULT_OK;
     155                 :         41 :     }
     156                 :            : 
     157                 :         27 :     struct s2n_stuffer point_formats = { 0 };
     158         [ -  + ]:         27 :     RESULT_GUARD_POSIX(s2n_stuffer_init_written(&point_formats, &extension->extension));
     159         [ -  + ]:         27 :     RESULT_GUARD_POSIX(s2n_stuffer_skip_read(&point_formats, sizeof(uint8_t)));
     160                 :            : 
     161                 :         27 :     bool found = false;
     162         [ +  + ]:         81 :     while (s2n_stuffer_data_available(&point_formats)) {
     163                 :         55 :         uint8_t iana = 0;
     164         [ -  + ]:         55 :         RESULT_GUARD_POSIX(s2n_stuffer_read_uint8(&point_formats, &iana));
     165         [ +  + ]:         55 :         RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
     166                 :         55 :     }
     167                 :         26 :     return S2N_RESULT_OK;
     168                 :         27 : }
     169                 :            : 
     170                 :            : /* JA3 involves concatenating a set of fields from the ClientHello:
     171                 :            :  *      SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
     172                 :            :  * For example:
     173                 :            :  *      "769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0"
     174                 :            :  * See https://github.com/salesforce/ja3
     175                 :            :  */
     176                 :            : static S2N_RESULT s2n_fingerprint_ja3_write(struct s2n_fingerprint_hash *hash,
     177                 :            :         struct s2n_client_hello *ch)
     178                 :         78 : {
     179         [ +  + ]:         78 :     RESULT_GUARD(s2n_fingerprint_ja3_version(hash, ch));
     180         [ +  + ]:         76 :     RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
     181         [ +  + ]:         75 :     RESULT_GUARD(s2n_fingerprint_ja3_cipher_suites(hash, ch));
     182         [ -  + ]:         69 :     RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
     183         [ -  + ]:         69 :     RESULT_GUARD(s2n_fingerprint_ja3_extensions(hash, ch));
     184         [ -  + ]:         69 :     RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
     185         [ -  + ]:         69 :     RESULT_GUARD(s2n_fingerprint_ja3_elliptic_curves(hash, ch));
     186         [ +  + ]:         69 :     RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
     187         [ +  + ]:         68 :     RESULT_GUARD(s2n_fingerprint_ja3_point_formats(hash, ch));
     188                 :         67 :     return S2N_RESULT_OK;
     189                 :         68 : }
     190                 :            : 
     191                 :            : S2N_RESULT s2n_fingerprint_ja3(struct s2n_fingerprint *fingerprint,
     192                 :            :         struct s2n_fingerprint_hash *hash, struct s2n_stuffer *output)
     193                 :         78 : {
     194 [ #  # ][ -  + ]:         78 :     RESULT_ENSURE_REF(fingerprint);
     195         [ +  + ]:         78 :     RESULT_GUARD(s2n_fingerprint_ja3_write(hash, fingerprint->client_hello));
     196         [ -  + ]:         67 :     RESULT_GUARD(s2n_fingerprint_ja3_digest(hash, output));
     197                 :            : 
     198         [ +  + ]:         67 :     if (s2n_fingerprint_hash_do_digest(hash)) {
     199                 :         27 :         fingerprint->raw_size = hash->bytes_digested;
     200                 :         40 :     } else {
     201                 :         40 :         fingerprint->raw_size = s2n_stuffer_data_available(output);
     202                 :         40 :     }
     203                 :         67 :     return S2N_RESULT_OK;
     204                 :         67 : }
     205                 :            : 
     206                 :            : struct s2n_fingerprint_method ja3_fingerprint = {
     207                 :            :     /* The hash doesn't have to be cryptographically secure,
     208                 :            :      * so the weakness of MD5 shouldn't be a problem. */
     209                 :            :     .hash = S2N_HASH_MD5,
     210                 :            :     /* The hash string is a single MD5 digest represented as hex */
     211                 :            :     .hash_str_size = S2N_JA3_HASH_STR_SIZE,
     212                 :            :     .fingerprint = s2n_fingerprint_ja3,
     213                 :            : };

Generated by: LCOV version 1.14