#!/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}")