diff --git a/scripts/build_data.py b/scripts/build_data.py index 589f6d2..9600143 100755 --- a/scripts/build_data.py +++ b/scripts/build_data.py @@ -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") diff --git a/scripts/seed_ja3.py b/scripts/seed_ja3.py new file mode 100644 index 0000000..a09bd14 --- /dev/null +++ b/scripts/seed_ja3.py @@ -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}")