JA3 TLS Fingerprinting#

InfraGuard has a built-in JA3 implementation that fingerprints TLS clients by parsing the raw ClientHello message before Python’s ssl module processes the handshake. Automated scanners, bots, and known-bad TLS stacks are detected without any external dependencies.

How It Works#

ClientHello Interception#

The JA3InjectingProtocol wraps uvicorn’s SSL protocol layer. When a TCP connection arrives, InfraGuard reads the raw bytes of the TLS ClientHello before passing them to OpenSSL:

Client ──TLS ClientHello──▶ JA3InjectingProtocol ──▶ ssl module
                                    │
                             parse ClientHello
                             compute JA3 hash
                             store in registry

The JA3 hash is stored in a global registry keyed by (client_ip, client_port) and is available to the filter pipeline when the HTTP request arrives.

JA3 Hash Computation#

The JA3 string is computed from five fields extracted from the ClientHello:

  1. TLS version - the version advertised in the handshake
  2. Cipher suites - the ordered list of cipher suite IDs
  3. Extensions - the ordered list of extension type IDs
  4. Elliptic curves - supported curve IDs (from the supported_groups extension)
  5. EC point formats - supported point format IDs

These fields are joined with commas, and dashes separate values within each field:

769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0

The MD5 hash of this string is the JA3 fingerprint.

GREASE Filtering#

Per RFC 8701, TLS clients may insert GREASE (Generate Random Extensions And Sustain Extensibility) values to test server robustness. These values follow a pattern (0x0a0a, 0x1a1a, etc.) and are filtered out before computing the JA3 hash, since they are randomized and would make every fingerprint unique.

Registry#

JA3 hashes are stored in an in-memory registry with a cap of 10,000 entries. Entries are keyed by (ip, port) tuple and are looked up when the HTTP request arrives on the same connection.

Pipeline Filter#

The tls_filter in the pipeline checks the JA3 hash against configured rules:

pipeline:
  tls_filter:
    enabled: true
    block_unknown: false       # block connections with no JA3 match (aggressive)
    allowed_ja3: []            # allowlist of known-good JA3 hashes
    blocked_ja3: []            # additional hashes to block beyond the seed list

Seed Blocklist#

The TLS filter ships with a built-in blocklist of JA3 hashes for common scanning tools:

ToolDescription
MasscanHigh-speed port scanner
Python requestsDefault Python HTTP client (no browser TLS stack)
curlCommand-line HTTP client
ZGrab2Internet-wide application-layer scanner
NmapNetwork scanner
ShodanSearch engine for internet-connected devices

Filter Modes#

ModeBehavior
Blocklist onlyBlocks connections matching known scanner JA3 hashes. Default.
Blocklist + allowlistBlocks scanners AND requires JA3 to match an allowed hash.
block_unknown: trueBlocks any connection whose JA3 is not in the allowlist. Most restrictive.

Scoring#

JA3 filter results contribute to the pipeline’s cumulative score:

  • Blocked JA3 match: score 1.0 (terminal block)
  • Unknown JA3 with block_unknown: true: score 1.0 (terminal block)
  • Allowed JA3 match: score 0.0 (pass)