About-JA4
JA4 Algorithm

JA4 Algorithm

JA4 is a TLS client fingerprinting method that creates a unique identifier based on characteristics of the TLS Client Hello packet. Unlike content-based fingerprinting, JA4 focuses on the structure and metadata of the TLS handshake, making it resistant to randomization and useful for detecting malicious traffic even when encrypted.

JA4 Fingerprint Format

The JA4 fingerprint is constructed using the following format:

(Protocol)(TLS Version)(SNI)(Cipher Count)(Extension Count)(ALPN)_(Cipher Hash)_(Extension Hash)

Example: t13d1516h2_8daaf6152771_e5627efa2ab1

Format Breakdown:

  • Protocol (1 char): t, q, or d
  • TLS Version (2 chars): 13, 12, 11, etc.
  • SNI (1 char): d or i
  • Cipher Count (2 chars): 00 to 99
  • Extension Count (2 chars): 00 to 99
  • ALPN (2 chars): First and last character of first ALPN value, or 00
  • Cipher Hash (12 chars): First 12 characters of SHA-256 hash
  • Extension Hash (12 chars): First 12 characters of SHA-256 hash

Understanding GREASE Values

GREASE (Generate Random Extensions And Sustain Extensibility) values are reserved TLS values used to maintain protocol extensibility. They prevent ossification by ensuring implementations properly ignore unknown values.

GREASE Identification:

  • GREASE values are 2-byte values where both bytes match the pattern: 0x?A?A
  • Range: 0x0A0A to 0xFAFA with the constraint that (value & 0x0F0F) == 0x0A0A
  • Common examples: 0x0A0A, 0x1A1A, 0x2A2A, 0x3A3A, 0x4A4A, 0x5A5A, 0x6A6A, 0x7A7A, 0x8A8A, 0x9A9A, 0xAAAA, 0xBABA, 0xCACA, 0xDADA, 0xEAEA, 0xFAFA

Important: All GREASE values must be ignored when counting ciphers and extensions, and when creating hashes.

def is_grease(hex_value):
    """Check if a value is a GREASE value."""
    value = int(hex_value, 16)
    # GREASE values match the pattern 0x?A?A
    return (value & 0x0F0F) == 0x0A0A

Steps to Construct a JA4 Fingerprint

Step 1: Determine Protocol Type

The first character identifies the transport protocol:

  • t = TLS (Transport Layer Security over TCP)
  • q = QUIC (QUIC encapsulates TLS 1.3 in UDP packets)
  • d = DTLS (Datagram Transport Layer Security over UDP)

Step 2: Extract TLS Version

Extract the 2-character TLS version code:

  1. Check for supported_versions extension (0x002b):

    • If present, use the highest non-GREASE version from the extension
    • If not present, use the Protocol Version field from the Client Hello
  2. Version Mapping:

    Hex ValueCodeVersion
    0x030413TLS 1.3
    0x030312TLS 1.2
    0x030211TLS 1.1
    0x030110TLS 1.0
    0x0300s3SSL 3.0
    0x0200s2SSL 2.0
    0xfeffd1DTLS 1.0
    0xfefdd2DTLS 1.2
    0xfefcd3DTLS 1.3
    Unknown00Unknown

Step 3: SNI Presence Check

Check for the Server Name Indication (SNI) extension (0x0000):

  • d = SNI exists (domain destination)
  • i = SNI does not exist (IP address destination)

Step 4: Count Cipher Suites

  • Count the number of cipher suites in the Client Hello
  • Ignore all GREASE values
  • Format as 2-digit string (e.g., 06, 15)
  • Maximum value: 99 (if count > 99, use 99)

Step 5: Count Extensions

  • Count the number of extensions in the Client Hello
  • Ignore all GREASE values
  • Format as 2-digit string (e.g., 10, 16)
  • Maximum value: 99 (if count > 99, use 99)

Step 6: ALPN Abbreviation

Extract from the Application-Layer Protocol Negotiation (ALPN) extension (0x0010):

  • Take the first ALPN value in the list
  • Use the first and last characters of that value
  • If no ALPN extension exists or the value is empty, use 00

Examples:

  • h2h2
  • http/1.1h1
  • http/1.0h0
  • (empty) → 00

Step 7: Cipher Hash

Create a SHA-256 hash of the sorted cipher suite list:

  1. Extract all cipher suite codes from the Client Hello
  2. Remove all GREASE values
  3. Convert to 4-character lowercase hex strings (e.g., 002f, c02b)
  4. Sort in hexadecimal order (as integers, not strings)
  5. Join with commas: 002f,0035,009c,...
  6. Compute SHA-256 hash of the resulting string
  7. Take the first 12 characters of the hex digest

Step 8: Extension Hash

Create a SHA-256 hash combining sorted extensions and signature algorithms:

  1. Extract all extension codes from the Client Hello
  2. Remove all GREASE values
  3. Exclude SNI (0x0000) and ALPN (0x0010) extensions
  4. Convert to 4-character lowercase hex strings
  5. Sort in hexadecimal order (as integers, not strings)
  6. Join with commas: 0005,000a,000b,...
  7. Append signature algorithms from the signature_algorithms extension (0x000d) in their original order (not sorted)
  8. Combine: sorted_extensions_signature_algorithms
  9. Compute SHA-256 hash of the combined string
  10. Take the first 12 characters of the hex digest

Note: Signature algorithms are appended in their original order as they appear in the extension, separated by an underscore from the sorted extensions.

Complete Example: JA4 Fingerprint Calculation

Step 1: Analyze the Client Hello Packet

Raw Packet Data:

  • Protocol Type: TLS over TCP → t
  • TLS Version: TLS 1.3 (from supported_versions extension) → 13
  • SNI: Present → d
  • Total Cipher Suites: 15 (no GREASE values in this example)
  • Total Extensions: 14 (excluding GREASE, SNI, and ALPN for the extension hash)
  • ALPN: First value is h2h2

Cipher Suite List (in original order):

1301, 1302, 1303, c02b, c02f, c02c, c030, cca9, cca8, c013, c014, 009c, 009d, 002f, 0035

Extension List (in original order, all extensions including SNI and ALPN):

001b, 0000, 0033, 0010, 4469, 0017, 002d, 000d, 0005, 0023, 0012, 002b, ff01, 000b, 000a, 0015

Extensions for Hash (excluding SNI 0x0000 and ALPN 0x0010):

001b, 0033, 4469, 0017, 002d, 000d, 0005, 0023, 0012, 002b, ff01, 000b, 000a, 0015

Signature Algorithms (from extension 0x000d, in original order):

0403, 0804, 0401, 0503, 0805, 0501, 0806, 0601

Step 2: Construct the First Part

Assemble: t13d1516h2

Breakdown:

  • t = TLS over TCP
  • 13 = TLS 1.3
  • d = SNI present
  • 15 = 15 cipher suites (count after removing GREASE)
  • 16 = 16 extensions total (count after removing GREASE, but before excluding SNI/ALPN)
  • h2 = ALPN abbreviation from h2

Step 3: Compute Cipher Hash

Process:

  1. Filter GREASE values (none in this example)
  2. Sort ciphers in hexadecimal order

Sorted Cipher List:

002f, 0035, 009c, 009d, 1301, 1302, 1303, c013, c014, c02b, c02c, c02f, c030, cca8, cca9

Cipher String for Hashing:

002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9

SHA-256 Hash (first 12 characters): 8daaf6152771

import hashlib
 
def compute_ja4_cipher_hash(cipher_list):
    """
    Compute JA4 cipher hash.
    
    Args:
        cipher_list: List of cipher suite hex codes (as strings)
    
    Returns:
        First 12 characters of SHA-256 hash
    """
    # Filter out GREASE values
    def is_grease(hex_value):
        value = int(hex_value, 16)
        return (value & 0x0F0F) == 0x0A0A
    
    filtered = [c for c in cipher_list if not is_grease(c)]
    
    # Sort by hexadecimal value
    sorted_ciphers = sorted(filtered, key=lambda x: int(x, 16))
    
    # Join with commas
    cipher_string = ",".join(sorted_ciphers)
    
    # Compute SHA-256 hash and return first 12 characters
    return hashlib.sha256(cipher_string.encode()).hexdigest()[:12]
 
# Example
cipher_list = ['1301', '1302', '1303', 'c02b', 'c02f', 'c02c', 'c030', 
               'cca9', 'cca8', 'c013', 'c014', '009c', '009d', '002f', '0035']
cipher_hash = compute_ja4_cipher_hash(cipher_list)
print(f"Cipher Hash: {cipher_hash}")  # Output: 8daaf6152771

Step 4: Compute Extension Hash

Process:

  1. Filter GREASE values (none in this example)
  2. Exclude SNI (0000) and ALPN (0010)
  3. Sort remaining extensions in hexadecimal order
  4. Append signature algorithms in original order

Extensions After Filtering (excluding SNI and ALPN):

001b, 0033, 4469, 0017, 002d, 000d, 0005, 0023, 0012, 002b, ff01, 000b, 000a, 0015

Sorted Extension List:

0005, 000a, 000b, 000d, 0012, 0015, 0017, 001b, 0023, 002b, 002d, 0033, 4469, ff01

Combined String (sorted extensions + underscore + signature algorithms):

0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601

SHA-256 Hash (first 12 characters): e5627efa2ab1

import hashlib
 
def compute_ja4_extension_hash(extension_list, signature_list):
    """
    Compute JA4 extension hash.
    
    Args:
        extension_list: List of extension hex codes (as strings)
        signature_list: List of signature algorithm hex codes (as strings)
    
    Returns:
        First 12 characters of SHA-256 hash
    """
    # Filter out GREASE values
    def is_grease(hex_value):
        value = int(hex_value, 16)
        return (value & 0x0F0F) == 0x0A0A
    
    filtered = [e for e in extension_list if not is_grease(e)]
    
    # Exclude SNI (0000) and ALPN (0010)
    filtered = [e for e in filtered if e not in ['0000', '0010']]
    
    # Sort by hexadecimal value
    sorted_extensions = sorted(filtered, key=lambda x: int(x, 16))
    
    # Join extensions with commas
    extension_string = ",".join(sorted_extensions)
    
    # Append signature algorithms with underscore separator
    if signature_list:
        combined_string = f"{extension_string}_{','.join(signature_list)}"
    else:
        combined_string = extension_string
    
    # Compute SHA-256 hash and return first 12 characters
    return hashlib.sha256(combined_string.encode()).hexdigest()[:12]
 
# Example
extension_list = ['001b', '0000', '0033', '0010', '4469', '0017', '002d', 
                  '000d', '0005', '0023', '0012', '002b', 'ff01', '000b', 
                  '000a', '0015']
signature_list = ['0403', '0804', '0401', '0503', '0805', '0501', '0806', '0601']
extension_hash = compute_ja4_extension_hash(extension_list, signature_list)
print(f"Extension Hash: {extension_hash}")  # Output: e5627efa2ab1

Step 5: Assemble the Complete JA4 Fingerprint

JA4 = t13d1516h2_8daaf6152771_e5627efa2ab1

Final Fingerprint:

t13d1516h2_8daaf6152771_e5627efa2ab1

JA4 Output Formats

JA4 supports multiple output formats for different analysis needs:

1. Standard JA4 Fingerprint (Default)

The standard hashed format for efficient storage and comparison:

t13d1516h2_8daaf6152771_e5627efa2ab1

2. JA4 Raw Sorted (-r or JA4_r)

Shows sorted cipher and extension values for transparency and debugging:

JA4_r = t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601

Format: <header>_<sorted_ciphers>_<sorted_extensions>_<signature_algorithms>

Notes:

  • Ciphers are sorted in hexadecimal order
  • Extensions are sorted in hexadecimal order (SNI and ALPN excluded)
  • Signature algorithms remain in original order
  • GREASE values are filtered out

3. JA4 Raw Original (-ro or JA4_ro)

Shows original ordering as it appears in the Client Hello packet:

JA4_ro = t13d1516h2_1301,1302,1303,c02b,c02f,c02c,c030,cca9,cca8,c013,c014,009c,009d,002f,0035_001b,0033,4469,0017,002d,000d,0005,0023,0012,002b,ff01,000b,000a,0015_0403,0804,0401,0503,0805,0501,0806,0601

Format: <header>_<original_ciphers>_<original_extensions>_<signature_algorithms>

Notes:

  • Ciphers are in the order they appear in the Client Hello
  • Extensions are in the order they appear (SNI and ALPN excluded from this section)
  • Signature algorithms remain in original order
  • GREASE values are filtered out
  • This format is useful for detecting client behaviors that depend on ordering

Format Comparison

FormatUse CaseStorage SizeHuman Readable
StandardDatabase storage, detection rulesSmall (~40 chars)No
Raw SortedDebugging, analysis, comparisonLarge (~200+ chars)Yes
Raw OriginalBehavior analysis, client identificationLarge (~200+ chars)Yes

Complete Python Implementation

Here's a complete implementation of the JA4 fingerprinting algorithm:

import hashlib
from typing import List, Optional
 
def is_grease(hex_value: str) -> bool:
    """
    Check if a hex value is a GREASE value.
    GREASE values match the pattern 0x?A?A.
    
    Args:
        hex_value: 4-character hex string (e.g., '0a0a', '1a1a')
    
    Returns:
        True if value is GREASE, False otherwise
    """
    try:
        value = int(hex_value, 16)
        return (value & 0x0F0F) == 0x0A0A
    except ValueError:
        return False
 
def compute_ja4_fingerprint(
    protocol: str,
    tls_version: str,
    sni_present: bool,
    cipher_suites: List[str],
    extensions: List[str],
    alpn_values: Optional[List[str]],
    signature_algorithms: Optional[List[str]]
) -> str:
    """
    Compute complete JA4 fingerprint.
    
    Args:
        protocol: 't', 'q', or 'd'
        tls_version: '13', '12', '11', '10', etc.
        sni_present: True if SNI extension exists
        cipher_suites: List of cipher hex codes (4-char strings)
        extensions: List of extension hex codes (4-char strings)
        alpn_values: List of ALPN protocol strings (e.g., ['h2', 'http/1.1'])
        signature_algorithms: List of signature algorithm hex codes (4-char strings)
    
    Returns:
        JA4 fingerprint string
    """
    # Filter GREASE values
    filtered_ciphers = [c for c in cipher_suites if not is_grease(c)]
    filtered_extensions = [e for e in extensions if not is_grease(e)]
    
    # Count ciphers and extensions (max 99)
    cipher_count = min(len(filtered_ciphers), 99)
    extension_count = min(len(filtered_extensions), 99)
    
    # SNI flag
    sni_flag = 'd' if sni_present else 'i'
    
    # ALPN abbreviation
    if alpn_values and len(alpn_values) > 0:
        alpn_first = alpn_values[0]
        if len(alpn_first) >= 2:
            alpn_abbr = alpn_first[0] + alpn_first[-1]
        elif len(alpn_first) == 1:
            alpn_abbr = alpn_first[0] + '0'
        else:
            alpn_abbr = '00'
    else:
        alpn_abbr = '00'
    
    # Cipher hash
    sorted_ciphers = sorted(filtered_ciphers, key=lambda x: int(x, 16))
    cipher_string = ','.join(sorted_ciphers)
    cipher_hash = hashlib.sha256(cipher_string.encode()).hexdigest()[:12]
    
    # Extension hash
    # Remove SNI (0000) and ALPN (0010)
    filtered_exts = [e for e in filtered_extensions if e not in ['0000', '0010']]
    sorted_extensions = sorted(filtered_exts, key=lambda x: int(x, 16))
    extension_string = ','.join(sorted_extensions)
    
    # Append signature algorithms
    if signature_algorithms and len(signature_algorithms) > 0:
        sig_string = ','.join(signature_algorithms)
        combined_string = f"{extension_string}_{sig_string}"
    else:
        combined_string = extension_string
    
    extension_hash = hashlib.sha256(combined_string.encode()).hexdigest()[:12]
    
    # Assemble fingerprint
    fingerprint = (
        f"{protocol}{tls_version}{sni_flag}"
        f"{cipher_count:02d}{extension_count:02d}{alpn_abbr}_"
        f"{cipher_hash}_{extension_hash}"
    )
    
    return fingerprint
 
# Example usage
if __name__ == "__main__":
    protocol = 't'
    tls_version = '13'
    sni_present = True
    
    cipher_suites = [
        '1301', '1302', '1303', 'c02b', 'c02f', 'c02c', 'c030',
        'cca9', 'cca8', 'c013', 'c014', '009c', '009d', '002f', '0035'
    ]
    
    extensions = [
        '001b', '0000', '0033', '0010', '4469', '0017', '002d',
        '000d', '0005', '0023', '0012', '002b', 'ff01', '000b',
        '000a', '0015'
    ]
    
    alpn_values = ['h2']
    
    signature_algorithms = [
        '0403', '0804', '0401', '0503', '0805', '0501', '0806', '0601'
    ]
    
    ja4 = compute_ja4_fingerprint(
        protocol, tls_version, sni_present,
        cipher_suites, extensions, alpn_values, signature_algorithms
    )
    
    print(f"JA4 Fingerprint: {ja4}")
    # Output: t13d1516h2_8daaf6152771_e5627efa2ab1

Key Takeaways

  1. GREASE Filtering: Always remove GREASE values before counting or hashing
  2. Consistent Sorting: Ciphers and extensions must be sorted by hexadecimal value (as integers)
  3. SNI/ALPN Exclusion: These extensions are excluded from the extension hash
  4. Signature Algorithm Order: Signature algorithms are appended in their original order
  5. Hash Truncation: Only the first 12 characters of SHA-256 hashes are used
  6. Maximum Counts: Cipher and extension counts are capped at 99

Use Cases

  • Malware Detection: Identify malicious TLS clients by their fingerprints
  • Application Identification: Determine which application or library is making TLS connections
  • Traffic Analysis: Monitor and analyze TLS traffic patterns
  • Threat Hunting: Search for known bad fingerprints in network traffic
  • Anomaly Detection: Detect unusual or suspicious TLS configurations