TLS Fingerprints & Client Certificates
Control the TLS ClientHello and present mTLS certificates in replay.
Anti-bot platforms score the TLS ClientHello before any application data arrives: cipher suites, extension order, ALPN, and GREASE placement collapse into JA3/JA4 hashes that gate traffic, so a replay with perfect headers over a stock TLS stack still fails. Traffic Jam’s replay rebuilds the captured client’s ClientHello — or a preset browser/app profile — through uTLS, preserves captured header order, and presents mTLS client certificates on either transport.
Replay transports
Every replay (open a request in the API lab → Replay) picks a transport:
| Transport | ClientHello | Header order |
|---|---|---|
native (default) — Native Go transport | The running Go toolchain’s own hello | Go sorts headers; order diverges from the capture |
utls — Fingerprint-faithful (uTLS) | Rebuilt from a profile (below) | Captured order written byte for byte on HTTP/1.1 |
The native transport’s JA3/JA4 is recorded once per process against a local listener with the same ALPN set. The uTLS transport dials with a single-use HelloCustom spec built fresh per connection.
Select a ClientHello profile
In the replay dialog, set Transport to Fingerprint-faithful (uTLS), then pick a ClientHello profile:
captured— Captured client (session fingerprint), the default on the uTLS transport; rebuilds the hello from the session’s captured TLS fingerprint. If the exchange has no captured fingerprint, the dialog warns and the profile fails at replay time — choose a preset instead.golang— Go (native stack), the native Go hello rebuilt through uTLS; a control for A/B testing TLS fingerprint gating.- Preset profiles — real client hellos from uTLS: Chrome (133, 131, 120, 120-pq, 115-pq, 106, 100, 83, 70), Firefox (120, 105, 102, 99, 65, 55), Safari/iOS (16.0, 14, 13, 12.1, 11.1), and others (
android-11-okhttp,edge-85,360-7.5,qq-11.1).
Each profile’s JA3/JA4 is computed from its spec and served by GET /api/replay/transports.
The Force HTTP/1.1 checkbox rewrites the spec to offer only http/1.1 in ALPN and drops the HTTP/2 application_settings extension, so the captured header order survives byte for byte.
How the backend reproduces the ClientHello
Building a spec from the captured fingerprint preserves what fingerprinting systems observe and drops what a fresh session cannot carry:
- Extension order is preserved — it is part of JA3 and JA4.
- GREASE is regenerated — per-connection values; uTLS emits fresh ones, excluded from fingerprints.
pre_shared_key(0x0029) andearly_data(0x002a) are dropped — a fresh session cannot resume captured PSKs.- Key shares are synthesized from the captured supported groups: hybrid post-quantum clients (
X25519MLKEM768/X25519Kyber768) offer the hybrid share plus X25519; everyone else offers one share for their preferred classical group. - Cipher suites, supported groups, EC point formats, signature algorithms,
supported_versions, and ALPN are rebuilt from the capture;compress_certificatereplays with brotli (the Chrome-family offering); unknown extensions replay presence-only.
JA3 is the MD5 of legacyVersion,ciphers,extensions,groups,ecPointFormats with GREASE filtered; JA4 is the FoxIO a_b_c form.
When ALPN negotiates h2 (and Force HTTP/1.1 is off), the uTLS transport writes the HTTP/2 preface itself instead of Go’s fixed h2 stack: the captured client SETTINGS frame — IDs, values, and order — verbatim, plus the captured pseudo-header order (the Akamai-style HTTP/2 fingerprint).
Preview the fingerprint before sending
The replay dialog fetches a captured-vs-replay report (POST /api/replay/fingerprint-report) that never touches the network. It shows Captured and Replay will send sides — JA3, JA4, ALPN, TLS version, HTTP version, User-Agent, header order, h2 SETTINGS — plus mismatches:
| Dimension | Severity | Flagged when |
|---|---|---|
ja4 | high | JA4 differs — antibot gates (Cloudflare, Akamai, DataDome) compare it first |
ja3 | warning | JA3 differs — still checked by older WAF rulesets |
alpn / httpVersion / userAgent | warning | ALPN lists differ; an h2 capture would replay as HTTP/1.1; User-Agent disagrees with the ClientHello |
headerOrder / h2Settings | warning | Order diverges (native transport sorts headers); native transport cannot reproduce the captured SETTINGS frame |
tlsSession / headerSet / egress | info | PSK dropped, header set differs, proxy egress noted |
Upload mTLS client certificates
Open Settings → mTLS client certificates. Certificates are stored in the local SQLite database; API responses never return the PEM.
- Enter a Name and Pinned hosts (comma-separated, e.g.
api-m.goldapple.ru). - Supply one of two formats:
- Combined PEM — certificate chain followed by the private key (a
PRIVATE KEY,RSA PRIVATE KEY, orEC PRIVATE KEYblock). Uploads without a private key are rejected. - …or a
.p12/.pfxfile with its password — base64-encoded in the browser, unlocked server-side and re-encoded as PEM.
- Combined PEM — certificate chain followed by the private key (a
- Click Store certificate. PEM takes precedence when both are provided. Upload limit is 1 MiB.
Host pinning matches the replay target hostname: an exact match wins over a suffix match, and among suffix matches the longest (most specific) wins.
Attach a certificate in replay
In the replay dialog, set Client certificate (mTLS) to Auto (host-pinned) (the default — the backend attaches the certificate pinned to the target host, if one exists) or to a specific certificate by name. Both transports present the certificate when the server issues a TLS CertificateRequest; the replay response echoes the clientCertId actually used.
[!TIP] Bulk replay is capped at 50 requests per batch.
Rejection classes
Every replay result is classified, giving mutation probes, canary checks, and bulk runs a stable verdict vocabulary:
| Class | Meaning |
|---|---|
| (empty) — accepted | 2xx; not a rejection |
transport-error | Dial or TLS handshake failure — no HTTP status |
client-certificate-required | HTTP 400 whose body matches the nginx mTLS gate (“No required SSL certificate…”) |
unauthorized / forbidden / not-found | 401 / 403 / 404 |
rate-limited | 429 |
antifraud-reject | 451 — the anti-fraud layer refused the request outright |
redirect | 3xx — replay never follows redirects |
client-error / server-error | any other 4xx / 5xx |
client-certificate-required is a body sniff of the first 4096 bytes of a 400, distinguishing the missing-mTLS-cert gate from an ordinary bad request. The diagnosis is in the pair: if attaching a certificate flips client-certificate-required to 2xx or antifraud-reject, the mTLS gate is passed and the next gate is application-level. Canary checks alert on rejection-class drift.
[!NOTE] Replay does not follow redirects, defaults to a 30 s timeout, and excludes captured credentials unless you opt in — see /docs/en/security/. The captured JA3/JA4 come from the TLS analysis tab (/docs/en/analysis/comparing/).