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, ord - TLS Version (2 chars):
13,12,11, etc. - SNI (1 char):
dori - Cipher Count (2 chars):
00to99 - Extension Count (2 chars):
00to99 - 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:
0x0A0Ato0xFAFAwith 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) == 0x0A0ASteps 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:
-
Check for
supported_versionsextension (0x002b):- If present, use the highest non-GREASE version from the extension
- If not present, use the Protocol Version field from the Client Hello
-
Version Mapping:
Hex Value Code Version 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 Unknown 00Unknown
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, use99)
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, use99)
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:
h2→h2http/1.1→h1http/1.0→h0- (empty) →
00
Step 7: Cipher Hash
Create a SHA-256 hash of the sorted cipher suite list:
- Extract all cipher suite codes from the Client Hello
- Remove all GREASE values
- Convert to 4-character lowercase hex strings (e.g.,
002f,c02b) - Sort in hexadecimal order (as integers, not strings)
- Join with commas:
002f,0035,009c,... - Compute SHA-256 hash of the resulting string
- Take the first 12 characters of the hex digest
Step 8: Extension Hash
Create a SHA-256 hash combining sorted extensions and signature algorithms:
- Extract all extension codes from the Client Hello
- Remove all GREASE values
- Exclude SNI (0x0000) and ALPN (0x0010) extensions
- Convert to 4-character lowercase hex strings
- Sort in hexadecimal order (as integers, not strings)
- Join with commas:
0005,000a,000b,... - Append signature algorithms from the
signature_algorithmsextension (0x000d) in their original order (not sorted) - Combine:
sorted_extensions_signature_algorithms - Compute SHA-256 hash of the combined string
- 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
h2→h2
Cipher Suite List (in original order):
1301, 1302, 1303, c02b, c02f, c02c, c030, cca9, cca8, c013, c014, 009c, 009d, 002f, 0035Extension 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, 0015Extensions for Hash (excluding SNI 0x0000 and ALPN 0x0010):
001b, 0033, 4469, 0017, 002d, 000d, 0005, 0023, 0012, 002b, ff01, 000b, 000a, 0015Signature Algorithms (from extension 0x000d, in original order):
0403, 0804, 0401, 0503, 0805, 0501, 0806, 0601Step 2: Construct the First Part
Assemble: t13d1516h2
Breakdown:
t= TLS over TCP13= TLS 1.3d= SNI present15= 15 cipher suites (count after removing GREASE)16= 16 extensions total (count after removing GREASE, but before excluding SNI/ALPN)h2= ALPN abbreviation fromh2
Step 3: Compute Cipher Hash
Process:
- Filter GREASE values (none in this example)
- Sort ciphers in hexadecimal order
Sorted Cipher List:
002f, 0035, 009c, 009d, 1301, 1302, 1303, c013, c014, c02b, c02c, c02f, c030, cca8, cca9Cipher String for Hashing:
002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9SHA-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: 8daaf6152771Step 4: Compute Extension Hash
Process:
- Filter GREASE values (none in this example)
- Exclude SNI (0000) and ALPN (0010)
- Sort remaining extensions in hexadecimal order
- 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, 0015Sorted Extension List:
0005, 000a, 000b, 000d, 0012, 0015, 0017, 001b, 0023, 002b, 002d, 0033, 4469, ff01Combined 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,0601SHA-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: e5627efa2ab1Step 5: Assemble the Complete JA4 Fingerprint
JA4 = t13d1516h2_8daaf6152771_e5627efa2ab1Final Fingerprint:
t13d1516h2_8daaf6152771_e5627efa2ab1JA4 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_e5627efa2ab12. 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,0601Format: <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,0601Format: <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
| Format | Use Case | Storage Size | Human Readable |
|---|---|---|---|
| Standard | Database storage, detection rules | Small (~40 chars) | No |
| Raw Sorted | Debugging, analysis, comparison | Large (~200+ chars) | Yes |
| Raw Original | Behavior analysis, client identification | Large (~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_e5627efa2ab1Key Takeaways
- GREASE Filtering: Always remove GREASE values before counting or hashing
- Consistent Sorting: Ciphers and extensions must be sorted by hexadecimal value (as integers)
- SNI/ALPN Exclusion: These extensions are excluded from the extension hash
- Signature Algorithm Order: Signature algorithms are appended in their original order
- Hash Truncation: Only the first 12 characters of SHA-256 hashes are used
- 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