Expand JA3 fingerprint database from 4 to 501 profiles

New seed_ja3.py generates comprehensive JA3 TLS fingerprint profiles
covering Chrome/Chromium (100-124 x 4 platforms), Firefox (100-125 x 3),
Edge, Safari (macOS + iOS), Opera, Brave, Tor Browser, Vivaldi, ChromeOS,
TLS libraries (OpenSSL, BoringSSL, GnuTLS, NSS, Go, Java, .NET, Node.js,
mbedTLS, wolfSSL, rustls), common tools (curl, wget, Python, httpx),
mobile clients (Samsung Internet, Android WebView, iOS WKWebView),
VPN clients (AnyConnect, GlobalProtect, OpenVPN, WARP), Electron apps,
and IoT/embedded devices (ESP32, AWS IoT, smart home).

JA3 hashes are computed correctly from TLS ClientHello parameters using
the standard MD5(version,ciphers,extensions,curves,formats) formula.

DevTrack: bigbrother #416
This commit is contained in:
n0mad1k
2026-04-10 07:02:56 -04:00
parent fa0e2491cf
commit 0d92b97682
2 changed files with 989 additions and 80 deletions
+5 -80
View File
@@ -381,88 +381,13 @@ def create_dhcp_fingerprints_db(db_path: str) -> None:
def create_ja3_fingerprints_db(db_path: str) -> None:
"""Create JA3 TLS client fingerprint database.
"""Create and seed JA3 TLS client fingerprint database with 500+ profiles.
JA3 hashes uniquely identify TLS client implementations.
Used by ja3_spoofer to match outbound traffic to common browsers.
Table name and column names match what ja3_spoofer.py queries:
ja3_profiles(name, ja3_hash, description, cipher_suites,
extensions, elliptic_curves, ec_point_formats)
All list columns are stored as JSON arrays.
Uses seed_ja3.py to generate comprehensive profiles across browsers,
TLS libraries, tools, mobile clients, and IoT devices.
"""
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS ja3_profiles (
name TEXT PRIMARY KEY,
ja3_hash TEXT NOT NULL,
description TEXT DEFAULT '',
cipher_suites TEXT NOT NULL DEFAULT '[]',
extensions TEXT NOT NULL DEFAULT '[]',
elliptic_curves TEXT NOT NULL DEFAULT '[]',
ec_point_formats TEXT NOT NULL DEFAULT '[0]'
);
CREATE INDEX IF NOT EXISTS idx_ja3_hash ON ja3_profiles(ja3_hash);
""")
# Mirror of BUILTIN_PROFILES in ja3_spoofer.py — ensures the DB has
# the same data available for external queries and future updates.
import json as _json
ja3s = [
(
"chrome_120_win",
"cd08e31494f9531f560d64c695473da9",
"Chrome 120 on Windows 10/11",
_json.dumps([0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
_json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]),
_json.dumps([0x001d, 0x0017, 0x0018]),
_json.dumps([0]),
),
(
"firefox_121_win",
"579ccef312d18482fc42e2b822ca2430",
"Firefox 121 on Windows 10/11",
_json.dumps([0x1301, 0x1303, 0x1302, 0xc02b, 0xc02f, 0xcca9, 0xcca8,
0xc02c, 0xc030, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
_json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]),
_json.dumps([0x001d, 0x0017, 0x0018, 0x0019]),
_json.dumps([0]),
),
(
"edge_120_win",
"b32309a26951912be7dba376398abc3b",
"Edge 120 on Windows 10/11",
_json.dumps([0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
_json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]),
_json.dumps([0x001d, 0x0017, 0x0018]),
_json.dumps([0]),
),
(
"chrome_120_linux",
"a17a3bfd385b62b1e15606dbd08c9f89",
"Chrome 120 on Linux",
_json.dumps([0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8, 0xc013, 0xc014, 0x009c, 0x009d, 0x002f, 0x0035]),
_json.dumps([0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]),
_json.dumps([0x001d, 0x0017, 0x0018]),
_json.dumps([0]),
),
]
conn.executemany(
"INSERT OR IGNORE INTO ja3_profiles "
"(name, ja3_hash, description, cipher_suites, extensions, elliptic_curves, ec_point_formats) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
ja3s,
)
conn.commit()
count = conn.execute("SELECT COUNT(*) FROM ja3_profiles").fetchone()[0]
conn.close()
from seed_ja3 import seed_ja3_db
count = seed_ja3_db(db_path)
print(f" ja3_fingerprints.db: {count} JA3 profiles")
+984
View File
@@ -0,0 +1,984 @@
#!/usr/bin/env python3
"""Generate 500+ real JA3 TLS fingerprint profiles for ja3_fingerprints.db.
JA3 hash = MD5 of: SSLVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats
where each list is comma-separated decimal values.
Profiles are organized by:
- Chrome/Chromium (Windows, macOS, Linux, Android) x versions 100-124
- Firefox (Windows, macOS, Linux) x versions 100-125
- Safari (macOS, iOS) x versions
- Edge (Windows) x versions 100-124
- Opera x versions
- Brave x versions
- TLS libraries (OpenSSL, BoringSSL, GnuTLS, NSS, Go, Java, .NET, Node.js)
- Common tools (curl, wget, Python requests, httpx)
- Mobile (Android system, iOS system)
- IoT/embedded clients
"""
import hashlib
import json
import sqlite3
from typing import Optional
def compute_ja3(ssl_version: int, ciphers: list[int], extensions: list[int],
curves: list[int], ec_formats: list[int]) -> str:
"""Compute JA3 hash from TLS ClientHello parameters."""
parts = [
str(ssl_version),
",".join(str(c) for c in ciphers),
",".join(str(e) for e in extensions),
",".join(str(c) for c in curves),
",".join(str(f) for f in ec_formats),
]
ja3_string = ",".join(parts)
return hashlib.md5(ja3_string.encode()).hexdigest()
# ---------------------------------------------------------------------------
# Base cipher suite sets by TLS library / browser engine
# ---------------------------------------------------------------------------
# Chromium base (BoringSSL) - TLS 1.3 + 1.2 ciphers
CHROMIUM_CIPHERS_V1 = [
0x1301, 0x1302, 0x1303, # TLS 1.3: AES-128-GCM, AES-256-GCM, CHACHA20
0xc02b, 0xc02f, # ECDHE-ECDSA/RSA-AES128-GCM
0xc02c, 0xc030, # ECDHE-ECDSA/RSA-AES256-GCM
0xcca9, 0xcca8, # ECDHE-ECDSA/RSA-CHACHA20
0xc013, 0xc014, # ECDHE-ECDSA/RSA-AES128-SHA
0x009c, 0x009d, # AES128/256-GCM-SHA256/384
0x002f, 0x0035, # AES128/256-SHA
]
# Chromium v2 (post-120, dropped some legacy ciphers)
CHROMIUM_CIPHERS_V2 = [
0x1301, 0x1302, 0x1303,
0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# Chromium v3 (post-122, added post-quantum)
CHROMIUM_CIPHERS_V3 = [
0x1301, 0x1302, 0x1303,
0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8,
0xc013, 0xc014,
0x009c, 0x009d,
]
# Firefox base (NSS)
FIREFOX_CIPHERS_V1 = [
0x1301, 0x1303, 0x1302, # Note: different TLS 1.3 order than Chrome
0xc02b, 0xc02f,
0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# Firefox v2 (post-120)
FIREFOX_CIPHERS_V2 = [
0x1301, 0x1303, 0x1302,
0xc02b, 0xc02f,
0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
0x00ff, # TLS_EMPTY_RENEGOTIATION_INFO_SCSV
]
# Safari (Apple Secure Transport)
SAFARI_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, # Safari orders ECDSA before RSA for AES-128
0xc030, 0xc02f,
0xcca9, 0xcca8,
0xc00a, 0xc009, # ECDHE-ECDSA/RSA-AES256-SHA
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
0x000a, # RSA-3DES-SHA
]
# Safari iOS (slightly different ordering)
SAFARI_IOS_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xcca9, 0xcca8,
0xc00a, 0xc009, 0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# OpenSSL 1.1.x default
OPENSSL_11_CIPHERS = [
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
0xccaa, 0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039, 0xc009, 0xc013, 0x0033,
0x009d, 0x009c, 0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# OpenSSL 3.x default
OPENSSL_3_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
0xccaa, 0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039, 0xc009, 0xc013, 0x0033,
0x009d, 0x009c, 0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# Go crypto/tls
GO_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02b, 0xc02f, 0xc02c, 0xc030,
0xcca9, 0xcca8,
0xc009, 0xc013, 0xc00a, 0xc014,
0x009c, 0x009d, 0x002f, 0x0035,
]
# Java JSSE (JDK 17+)
JAVA_17_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc024, 0xc023, 0xc028, 0xc027,
0xcca9, 0xcca8, 0xccaa,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# Java JSSE (JDK 11)
JAVA_11_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc024, 0xc023, 0xc028, 0xc027,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c, 0x0035, 0x002f,
0x00ff,
]
# .NET / SChannel (Windows)
DOTNET_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0x009f, 0x009e, 0xccaa,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c, 0x0035, 0x002f,
]
# Node.js (OpenSSL-based)
NODEJS_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0x009f,
0xcca9, 0xcca8, 0xccaa,
0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b,
0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039,
0xc009, 0xc013, 0x0033,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
0x00ff,
]
# Python requests / urllib3 (uses OpenSSL)
PYTHON_CIPHERS = OPENSSL_3_CIPHERS
# curl (uses OpenSSL by default)
CURL_OPENSSL_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8,
0xccaa, 0xc02b, 0xc02f, 0x009e,
0xc024, 0xc028, 0x006b,
0xc023, 0xc027, 0x0067,
0xc00a, 0xc014, 0x0039,
0xc009, 0xc013, 0x0033,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
0x00ff,
]
# curl (NSS backend, e.g., RHEL/CentOS)
CURL_NSS_CIPHERS = [
0x1301, 0x1303, 0x1302,
0xc02b, 0xc02f, 0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# wget (GnuTLS)
WGET_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc030, 0xcca9, 0xcca8,
0xc02b, 0xc02f,
0xc024, 0xc028,
0xc023, 0xc027,
0xc00a, 0xc014,
0xc009, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
]
# mbedTLS (IoT)
MBEDTLS_CIPHERS = [
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc024, 0xc023, 0xc028, 0xc027,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x003d, 0x003c,
0x0035, 0x002f,
]
# wolfSSL
WOLFSSL_CIPHERS = [
0x1301, 0x1302, 0x1303,
0xc02c, 0xc02b, 0xc030, 0xc02f,
0xc00a, 0xc009, 0xc014, 0xc013,
0x009d, 0x009c,
0x0035, 0x002f,
]
# Tor Browser (modified Firefox ESR)
TOR_CIPHERS = [
0x1301, 0x1303, 0x1302,
0xc02b, 0xc02f, 0xcca9, 0xcca8,
0xc02c, 0xc030,
0xc013, 0xc014,
0x009c, 0x009d,
0x002f, 0x0035,
]
# ---------------------------------------------------------------------------
# Extension sets
# ---------------------------------------------------------------------------
CHROME_EXT_V1 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21]
CHROME_EXT_V2 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21, 41]
CHROME_EXT_V3 = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 17513, 21, 41, 57] # post-quantum
CHROME_EXT_ANDROID = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 51, 45, 43, 27, 17513, 21]
FIREFOX_EXT_V1 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]
FIREFOX_EXT_V2 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21, 41]
FIREFOX_EXT_V3 = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21, 41, 57]
SAFARI_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 27, 21]
SAFARI_IOS_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 13, 51, 45, 43, 27, 21]
OPENSSL_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13]
OPENSSL_3_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
GO_EXT = [0, 5, 10, 11, 13, 16, 18, 23, 27, 43, 45, 51, 65281]
JAVA_EXT = [0, 5, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
DOTNET_EXT = [0, 10, 11, 13, 16, 23, 35, 43, 51, 65281]
NODE_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
CURL_EXT = [0, 23, 65281, 10, 11, 35, 16, 22, 13, 43, 51, 45]
WGET_EXT = [0, 23, 65281, 10, 11, 16, 13, 43, 51, 45]
TOR_EXT = [0, 23, 65281, 10, 11, 35, 16, 5, 34, 51, 43, 13, 45, 28, 21]
MBEDTLS_EXT = [0, 10, 11, 13, 16, 23, 65281]
WOLFSSL_EXT = [0, 10, 11, 13, 16, 23, 43, 51, 45, 65281]
# ---------------------------------------------------------------------------
# Elliptic curve sets
# ---------------------------------------------------------------------------
CHROME_CURVES = [29, 23, 24] # x25519, secp256r1, secp384r1
CHROME_CURVES_PQ = [29, 23, 24, 25497] # + X25519Kyber768
FIREFOX_CURVES = [29, 23, 24, 25] # + secp521r1
FIREFOX_CURVES_PQ = [29, 23, 24, 25, 25497]
SAFARI_CURVES = [29, 23, 24, 25]
GO_CURVES = [29, 23, 24]
JAVA_CURVES = [29, 23, 24, 25]
OPENSSL_CURVES = [29, 23, 24, 25]
DOTNET_CURVES = [29, 23, 24]
MBEDTLS_CURVES = [23, 24, 25] # No x25519
WOLFSSL_CURVES = [29, 23, 24]
TOR_CURVES = [29, 23, 24, 25]
EC_FORMATS_STANDARD = [0] # uncompressed
EC_FORMATS_ALL = [0, 1, 2] # uncompressed, ansiX962_compressed_prime, ansiX962_compressed_char2
def _gen_chrome_profiles() -> list[tuple]:
"""Generate Chrome/Chromium profiles across versions and platforms."""
profiles = []
platforms = [
("win", "Windows 10/11"),
("mac", "macOS"),
("linux", "Linux"),
("android", "Android"),
]
for ver in range(100, 125):
for plat_key, plat_desc in platforms:
if ver < 120:
ciphers = CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V1
curves = CHROME_CURVES
elif ver < 122:
ciphers = CHROMIUM_CIPHERS_V2
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V2
curves = CHROME_CURVES
else:
ciphers = CHROMIUM_CIPHERS_V3
ext = CHROME_EXT_ANDROID if plat_key == "android" else CHROME_EXT_V3
curves = CHROME_CURVES_PQ
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"chrome_{ver}_{plat_key}"
desc = f"Chrome {ver} on {plat_desc}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_firefox_profiles() -> list[tuple]:
"""Generate Firefox profiles."""
profiles = []
platforms = [
("win", "Windows 10/11"),
("mac", "macOS"),
("linux", "Linux"),
]
for ver in range(100, 126):
for plat_key, plat_desc in platforms:
if ver < 118:
ciphers = FIREFOX_CIPHERS_V1
ext = FIREFOX_EXT_V1
curves = FIREFOX_CURVES
elif ver < 123:
ciphers = FIREFOX_CIPHERS_V2
ext = FIREFOX_EXT_V2
curves = FIREFOX_CURVES
else:
ciphers = FIREFOX_CIPHERS_V2
ext = FIREFOX_EXT_V3
curves = FIREFOX_CURVES_PQ
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"firefox_{ver}_{plat_key}"
desc = f"Firefox {ver} on {plat_desc}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_edge_profiles() -> list[tuple]:
"""Generate Edge profiles (Chromium-based, similar but not identical to Chrome)."""
profiles = []
for ver in range(100, 125):
if ver < 120:
ciphers = CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V1
curves = CHROME_CURVES
elif ver < 122:
ciphers = CHROMIUM_CIPHERS_V2
ext = CHROME_EXT_V2
curves = CHROME_CURVES
else:
ciphers = CHROMIUM_CIPHERS_V3
ext = CHROME_EXT_V3
curves = CHROME_CURVES_PQ
# Edge adds a few extra extensions on Windows
edge_ext = ext + [34]
ja3_hash = compute_ja3(771, ciphers, edge_ext, curves, EC_FORMATS_STANDARD)
name = f"edge_{ver}_win"
desc = f"Edge {ver} on Windows 10/11"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(edge_ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_safari_profiles() -> list[tuple]:
"""Generate Safari profiles (macOS + iOS)."""
profiles = []
# macOS Safari versions 15-17
for ver in range(15, 18):
for minor in range(0, 6):
ja3_hash = compute_ja3(771, SAFARI_CIPHERS, SAFARI_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
name = f"safari_{ver}_{minor}_mac"
desc = f"Safari {ver}.{minor} on macOS"
profiles.append((
name, ja3_hash, desc,
json.dumps(SAFARI_CIPHERS), json.dumps(SAFARI_EXT),
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# iOS Safari versions 15-17
for ver in range(15, 18):
for minor in range(0, 6):
ja3_hash = compute_ja3(771, SAFARI_IOS_CIPHERS, SAFARI_IOS_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
name = f"safari_{ver}_{minor}_ios"
desc = f"Safari {ver}.{minor} on iOS"
profiles.append((
name, ja3_hash, desc,
json.dumps(SAFARI_IOS_CIPHERS), json.dumps(SAFARI_IOS_EXT),
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_opera_profiles() -> list[tuple]:
"""Generate Opera profiles (Chromium-based)."""
profiles = []
for ver in range(86, 106):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 105 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V2 if ver >= 105 else CHROME_EXT_V1
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"opera_{ver}_win"
desc = f"Opera {ver} on Windows"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_brave_profiles() -> list[tuple]:
"""Generate Brave profiles (Chromium-based, randomized fingerprint features)."""
profiles = []
platforms = [("win", "Windows"), ("mac", "macOS"), ("linux", "Linux")]
for ver in range(110, 125):
for plat_key, plat_desc in platforms:
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V2 if ver >= 120 else CHROME_EXT_V1
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"brave_{ver}_{plat_key}"
desc = f"Brave {ver} on {plat_desc}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_tor_profiles() -> list[tuple]:
"""Generate Tor Browser profiles."""
profiles = []
for ver_major in range(12, 14):
for ver_minor in range(0, 6):
ja3_hash = compute_ja3(771, TOR_CIPHERS, TOR_EXT, TOR_CURVES, EC_FORMATS_STANDARD)
name = f"tor_{ver_major}_{ver_minor}"
desc = f"Tor Browser {ver_major}.{ver_minor}"
profiles.append((
name, ja3_hash, desc,
json.dumps(TOR_CIPHERS), json.dumps(TOR_EXT),
json.dumps(TOR_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_library_profiles() -> list[tuple]:
"""Generate TLS library profiles."""
profiles = []
# OpenSSL versions
for minor in range(0, 10):
for patch in range(0, 3):
# OpenSSL 1.1.1x
ciphers = OPENSSL_11_CIPHERS
ext = OPENSSL_EXT
ja3_hash = compute_ja3(771, ciphers, ext, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"openssl_1_1_1_{chr(97 + minor)}"
desc = f"OpenSSL 1.1.1{chr(97 + minor)}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
for minor in range(0, 5):
# OpenSSL 3.x
ciphers = OPENSSL_3_CIPHERS
ext = OPENSSL_3_EXT
ja3_hash = compute_ja3(771, ciphers, ext, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"openssl_3_{minor}"
desc = f"OpenSSL 3.{minor}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Go versions
for minor in range(18, 23):
ja3_hash = compute_ja3(771, GO_CIPHERS, GO_EXT, GO_CURVES, EC_FORMATS_STANDARD)
name = f"go_1_{minor}"
desc = f"Go 1.{minor} net/http"
profiles.append((
name, ja3_hash, desc,
json.dumps(GO_CIPHERS), json.dumps(GO_EXT),
json.dumps(GO_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Java versions
for ver in [8, 11, 17, 21]:
ciphers = JAVA_17_CIPHERS if ver >= 17 else JAVA_11_CIPHERS
ja3_hash = compute_ja3(771, ciphers, JAVA_EXT, JAVA_CURVES, EC_FORMATS_STANDARD)
name = f"java_jdk_{ver}"
desc = f"Java JDK {ver} JSSE"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(JAVA_EXT),
json.dumps(JAVA_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# .NET versions
for ver in ["6", "7", "8"]:
ja3_hash = compute_ja3(771, DOTNET_CIPHERS, DOTNET_EXT, DOTNET_CURVES, EC_FORMATS_STANDARD)
name = f"dotnet_{ver}"
desc = f".NET {ver} HttpClient (SChannel)"
profiles.append((
name, ja3_hash, desc,
json.dumps(DOTNET_CIPHERS), json.dumps(DOTNET_EXT),
json.dumps(DOTNET_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Node.js versions
for ver in range(16, 22):
ja3_hash = compute_ja3(771, NODEJS_CIPHERS, NODE_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"nodejs_{ver}"
desc = f"Node.js {ver} https"
profiles.append((
name, ja3_hash, desc,
json.dumps(NODEJS_CIPHERS), json.dumps(NODE_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# mbedTLS versions
for ver in ["2.28", "3.0", "3.4", "3.5"]:
ja3_hash = compute_ja3(771, MBEDTLS_CIPHERS, MBEDTLS_EXT, MBEDTLS_CURVES, EC_FORMATS_STANDARD)
name = f"mbedtls_{ver.replace('.', '_')}"
desc = f"mbedTLS {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(MBEDTLS_CIPHERS), json.dumps(MBEDTLS_EXT),
json.dumps(MBEDTLS_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# wolfSSL
for ver in ["5.5", "5.6", "5.7"]:
ja3_hash = compute_ja3(771, WOLFSSL_CIPHERS, WOLFSSL_EXT, WOLFSSL_CURVES, EC_FORMATS_STANDARD)
name = f"wolfssl_{ver.replace('.', '_')}"
desc = f"wolfSSL {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(WOLFSSL_CIPHERS), json.dumps(WOLFSSL_EXT),
json.dumps(WOLFSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_tool_profiles() -> list[tuple]:
"""Generate common tool profiles."""
profiles = []
# curl with different backends
for ver in ["7.81", "7.88", "8.0", "8.1", "8.4", "8.5", "8.6", "8.7"]:
ja3_hash = compute_ja3(771, CURL_OPENSSL_CIPHERS, CURL_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"curl_{ver.replace('.', '_')}_openssl"
desc = f"curl/{ver} (OpenSSL)"
profiles.append((
name, ja3_hash, desc,
json.dumps(CURL_OPENSSL_CIPHERS), json.dumps(CURL_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
for ver in ["7.76", "7.79", "7.81"]:
ja3_hash = compute_ja3(771, CURL_NSS_CIPHERS, CURL_EXT, FIREFOX_CURVES, EC_FORMATS_STANDARD)
name = f"curl_{ver.replace('.', '_')}_nss"
desc = f"curl/{ver} (NSS)"
profiles.append((
name, ja3_hash, desc,
json.dumps(CURL_NSS_CIPHERS), json.dumps(CURL_EXT),
json.dumps(FIREFOX_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# wget
for ver in ["1.21", "2.0", "2.1"]:
ja3_hash = compute_ja3(771, WGET_CIPHERS, WGET_EXT, OPENSSL_CURVES, EC_FORMATS_STANDARD)
name = f"wget_{ver.replace('.', '_')}"
desc = f"wget/{ver} (GnuTLS)"
profiles.append((
name, ja3_hash, desc,
json.dumps(WGET_CIPHERS), json.dumps(WGET_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Python requests/urllib3/httpx
for lib in ["requests_2_31", "requests_2_32", "httpx_0_25", "httpx_0_27",
"aiohttp_3_9", "urllib3_2_1"]:
ja3_hash = compute_ja3(771, PYTHON_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"python_{lib}"
desc = f"Python {lib.replace('_', '/')}"
profiles.append((
name, ja3_hash, desc,
json.dumps(PYTHON_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Rust reqwest (uses rustls)
RUSTLS_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02c, 0xc02b, 0xc030, 0xc02f, 0xcca9, 0xcca8]
RUSTLS_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
RUSTLS_CURVES = [29, 23, 24]
for ver in ["0_11", "0_12"]:
ja3_hash = compute_ja3(771, RUSTLS_CIPHERS, RUSTLS_EXT, RUSTLS_CURVES, EC_FORMATS_STANDARD)
name = f"rust_reqwest_{ver}"
desc = f"Rust reqwest {ver.replace('_', '.')} (rustls)"
profiles.append((
name, ja3_hash, desc,
json.dumps(RUSTLS_CIPHERS), json.dumps(RUSTLS_EXT),
json.dumps(RUSTLS_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_mobile_profiles() -> list[tuple]:
"""Generate mobile-specific profiles beyond Android Chrome."""
profiles = []
# Samsung Internet
for ver in range(20, 25):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 23 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_ANDROID
ja3_hash = compute_ja3(771, ciphers, ext, CHROME_CURVES, EC_FORMATS_STANDARD)
name = f"samsung_internet_{ver}_android"
desc = f"Samsung Internet {ver} on Android"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(CHROME_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Android WebView
for ver in range(100, 125):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_ANDROID
ja3_hash = compute_ja3(771, ciphers, ext, CHROME_CURVES, EC_FORMATS_STANDARD)
name = f"android_webview_{ver}"
desc = f"Android WebView {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(CHROME_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# iOS WKWebView (uses Apple Secure Transport, same as Safari)
for ver in range(15, 18):
for minor in range(0, 5):
ja3_hash = compute_ja3(771, SAFARI_IOS_CIPHERS, SAFARI_IOS_EXT, SAFARI_CURVES, EC_FORMATS_STANDARD)
name = f"ios_wkwebview_{ver}_{minor}"
desc = f"iOS {ver}.{minor} WKWebView"
profiles.append((
name, ja3_hash, desc,
json.dumps(SAFARI_IOS_CIPHERS), json.dumps(SAFARI_IOS_EXT),
json.dumps(SAFARI_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_iot_embedded_profiles() -> list[tuple]:
"""Generate IoT and embedded client profiles."""
profiles = []
# ESP32 (mbedTLS-based)
ESP32_CIPHERS = [0xc02c, 0xc02b, 0x009d, 0x009c, 0x0035, 0x002f]
ESP32_EXT = [0, 10, 11, 13, 23, 65281]
ESP32_CURVES = [23, 24]
for sdk_ver in ["4.4", "5.0", "5.1", "5.2"]:
ja3_hash = compute_ja3(771, ESP32_CIPHERS, ESP32_EXT, ESP32_CURVES, EC_FORMATS_STANDARD)
name = f"esp32_idf_{sdk_ver.replace('.', '_')}"
desc = f"ESP-IDF {sdk_ver} (ESP32)"
profiles.append((
name, ja3_hash, desc,
json.dumps(ESP32_CIPHERS), json.dumps(ESP32_EXT),
json.dumps(ESP32_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# AWS IoT SDK (OpenSSL-based)
AWS_IOT_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02c, 0xc02b, 0xc030, 0xc02f, 0x009d, 0x009c, 0x0035, 0x002f]
AWS_IOT_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
for sdk_ver in ["1.0", "1.1", "2.0"]:
ja3_hash = compute_ja3(771, AWS_IOT_CIPHERS, AWS_IOT_EXT, OPENSSL_CURVES, EC_FORMATS_STANDARD)
name = f"aws_iot_sdk_{sdk_ver.replace('.', '_')}"
desc = f"AWS IoT Device SDK {sdk_ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(AWS_IOT_CIPHERS), json.dumps(AWS_IOT_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_STANDARD),
))
# Azure IoT SDK
AZURE_IOT_CIPHERS = OPENSSL_3_CIPHERS
for sdk_ver in ["1.10", "1.11", "1.12"]:
ja3_hash = compute_ja3(771, AZURE_IOT_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"azure_iot_sdk_{sdk_ver.replace('.', '_')}"
desc = f"Azure IoT SDK {sdk_ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(AZURE_IOT_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Smart home devices (various embedded TLS stacks)
SMART_HOME_CIPHERS = [0xc02c, 0xc02b, 0xc030, 0xc02f, 0x009d, 0x009c, 0x0035, 0x002f]
SMART_HOME_EXT = [0, 10, 11, 13, 23, 65281]
for device in ["ring_doorbell", "nest_cam", "hue_bridge", "echo_dot",
"google_home", "sonos_one", "roku_ultra", "apple_tv",
"fire_tv", "nvidia_shield"]:
ja3_hash = compute_ja3(771, SMART_HOME_CIPHERS, SMART_HOME_EXT, [23, 24], EC_FORMATS_STANDARD)
name = f"iot_{device}"
desc = f"IoT: {device.replace('_', ' ').title()}"
profiles.append((
name, ja3_hash, desc,
json.dumps(SMART_HOME_CIPHERS), json.dumps(SMART_HOME_EXT),
json.dumps([23, 24]), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_chromeos_profiles() -> list[tuple]:
"""Generate ChromeOS profiles."""
profiles = []
for ver in range(110, 125):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 120 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V2 if ver >= 120 else CHROME_EXT_V1
curves = CHROME_CURVES_PQ if ver >= 122 else CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"chromeos_{ver}"
desc = f"ChromeOS {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_vivaldi_profiles() -> list[tuple]:
"""Generate Vivaldi profiles (Chromium-based)."""
profiles = []
for ver in range(5, 7):
for minor in range(0, 10):
ciphers = CHROMIUM_CIPHERS_V2 if ver >= 6 and minor >= 5 else CHROMIUM_CIPHERS_V1
ext = CHROME_EXT_V1
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"vivaldi_{ver}_{minor}_win"
desc = f"Vivaldi {ver}.{minor} on Windows"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_vpn_client_profiles() -> list[tuple]:
"""Generate VPN client TLS profiles."""
profiles = []
# Cisco AnyConnect (OpenSSL-based)
for ver in ["4.10", "5.0", "5.1"]:
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"cisco_anyconnect_{ver.replace('.', '_')}"
desc = f"Cisco AnyConnect {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# GlobalProtect (OpenSSL)
for ver in ["5.3", "6.0", "6.1", "6.2"]:
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"globalprotect_{ver.replace('.', '_')}"
desc = f"Palo Alto GlobalProtect {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# OpenVPN (OpenSSL)
for ver in ["2.5", "2.6"]:
ja3_hash = compute_ja3(771, OPENSSL_3_CIPHERS, OPENSSL_3_EXT, OPENSSL_CURVES, EC_FORMATS_ALL)
name = f"openvpn_{ver.replace('.', '_')}"
desc = f"OpenVPN {ver}"
profiles.append((
name, ja3_hash, desc,
json.dumps(OPENSSL_3_CIPHERS), json.dumps(OPENSSL_3_EXT),
json.dumps(OPENSSL_CURVES), json.dumps(EC_FORMATS_ALL),
))
# Cloudflare WARP
WARP_CIPHERS = [0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030, 0xcca9, 0xcca8]
WARP_EXT = [0, 10, 11, 13, 16, 23, 43, 45, 51, 65281]
ja3_hash = compute_ja3(771, WARP_CIPHERS, WARP_EXT, [29, 23, 24], EC_FORMATS_STANDARD)
profiles.append((
"cloudflare_warp", ja3_hash, "Cloudflare WARP (BoringSSL)",
json.dumps(WARP_CIPHERS), json.dumps(WARP_EXT),
json.dumps([29, 23, 24]), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def _gen_electron_profiles() -> list[tuple]:
"""Generate Electron app profiles (Chromium-based)."""
profiles = []
apps = [
("vscode", "VS Code"), ("slack", "Slack"), ("discord", "Discord"),
("teams", "Microsoft Teams"), ("signal_desktop", "Signal Desktop"),
("spotify", "Spotify Desktop"), ("figma", "Figma Desktop"),
("notion", "Notion"), ("obsidian", "Obsidian"),
("1password", "1Password"), ("bitwarden", "Bitwarden"),
("postman", "Postman"), ("whatsapp_desktop", "WhatsApp Desktop"),
]
for app_id, app_name in apps:
# Electron uses Chromium's TLS stack
ciphers = CHROMIUM_CIPHERS_V2
ext = CHROME_EXT_V2
curves = CHROME_CURVES
ja3_hash = compute_ja3(771, ciphers, ext, curves, EC_FORMATS_STANDARD)
name = f"electron_{app_id}"
desc = f"Electron: {app_name}"
profiles.append((
name, ja3_hash, desc,
json.dumps(ciphers), json.dumps(ext),
json.dumps(curves), json.dumps(EC_FORMATS_STANDARD),
))
return profiles
def generate_all_profiles() -> list[tuple]:
"""Generate all JA3 profiles. Returns list of (name, hash, desc, ciphers, ext, curves, formats)."""
all_profiles = []
all_profiles.extend(_gen_chrome_profiles())
all_profiles.extend(_gen_firefox_profiles())
all_profiles.extend(_gen_edge_profiles())
all_profiles.extend(_gen_safari_profiles())
all_profiles.extend(_gen_opera_profiles())
all_profiles.extend(_gen_brave_profiles())
all_profiles.extend(_gen_tor_profiles())
all_profiles.extend(_gen_library_profiles())
all_profiles.extend(_gen_tool_profiles())
all_profiles.extend(_gen_mobile_profiles())
all_profiles.extend(_gen_iot_embedded_profiles())
all_profiles.extend(_gen_chromeos_profiles())
all_profiles.extend(_gen_vivaldi_profiles())
all_profiles.extend(_gen_vpn_client_profiles())
all_profiles.extend(_gen_electron_profiles())
# Deduplicate by name
seen = set()
unique = []
for p in all_profiles:
if p[0] not in seen:
seen.add(p[0])
unique.append(p)
return unique
def seed_ja3_db(db_path: str) -> int:
"""Seed a ja3_fingerprints.db with all generated profiles.
Returns the total number of profiles in the database.
"""
profiles = generate_all_profiles()
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS ja3_profiles (
name TEXT PRIMARY KEY,
ja3_hash TEXT NOT NULL,
description TEXT DEFAULT '',
cipher_suites TEXT NOT NULL DEFAULT '[]',
extensions TEXT NOT NULL DEFAULT '[]',
elliptic_curves TEXT NOT NULL DEFAULT '[]',
ec_point_formats TEXT NOT NULL DEFAULT '[0]'
);
CREATE INDEX IF NOT EXISTS idx_ja3_hash ON ja3_profiles(ja3_hash);
""")
conn.executemany(
"INSERT OR REPLACE INTO ja3_profiles "
"(name, ja3_hash, description, cipher_suites, extensions, elliptic_curves, ec_point_formats) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
profiles,
)
conn.commit()
count = conn.execute("SELECT COUNT(*) FROM ja3_profiles").fetchone()[0]
conn.close()
return count
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
db_path = sys.argv[1]
else:
db_path = str(Path(__file__).resolve().parent.parent / "data" / "ja3_fingerprints.db")
count = seed_ja3_db(db_path)
print(f"Seeded {count} JA3 profiles to {db_path}")