arquivo 002português

O edge te lê antes do payload

data
tempo de jogo
00:20:00
local
tlsfingerprintja3ja4cloudflare

Como um servidor decide que você é uma ferramenta antes de ler um único byte do seu request: o ClientHello campo por campo, o JA3 que o Chrome quebrou de propósito em 2023, o JA4 que substituiu ele, e as outras camadas que também contam. Com medições reais desta máquina.

A cena se repete. O request funciona no browser, falha no curl. Você confere o header, o token, o body, todos idênticos, e conclui que o alvo “detecta ferramenta”. Aí você melhora o User-Agent, adiciona os headers do Chrome, e continua tomando 403.

O que eu demorei para internalizar é que, nesse ponto, o servidor provavelmente ainda não leu nada do que você mandou. A decisão já foi tomada no handshake, com base em como o seu cliente se apresentou — antes de existir HTTP, antes de existir header, antes de existir payload. Existe uma camada inteira de identidade que eu passei anos tratando como “detalhe de rede” e que, na prática, decide se o meu teste começa ou não.

Este post é sobre essa camada. Vou abrir o ClientHello campo por campo, mostrar por que cada biblioteca TLS produz uma assinatura estável, explicar o JA3 e por que ele morreu para browsers em 2023, decodificar o JA4 inteiro, e fechar com as camadas que não são TLS mas contam igual. Tudo com número medido, não com “dizem que”.

Anatomia de um ClientHello

Antes de qualquer fingerprint, o objeto. A norma vigente é a RFC 9846 (julho de 2026), que obsoleta a RFC 8446. Sim, o TLS 1.3 foi republicado, e quase todo texto que você vai ler ainda cita a 8446. A estrutura do ClientHello, no entanto, é a mesma:

TEXT
struct {
    ProtocolVersion legacy_version = 0x0303;
    Random random;
    opaque legacy_session_id<0..32>;
    CipherSuite cipher_suites<2..2^16-2>;
    opaque legacy_compression_methods<1..2^8-1>;
    Extension extensions<8..2^16-1>;
} ClientHello;

Seis campos. Vou passar por cada um pensando em uma pergunta só: isso é sinal ou é ruído?

CampoBytesO que éVira fingerprint?
legacy_version2Fixado em 0x0303 no TLS 1.3Quase não: todo mundo manda igual
random32Sorteado a cada conexãoNunca. Se aparecer num hash, o hash está errado
legacy_session_id0–32Em modo compatibilidade, 32 bytes aleatóriosSim: presença e tamanho separam clientes
cipher_suitesvariávelLista de suítes, em ordem de preferênciaMuito: conjunto e ordem
legacy_compression_methods1Exatamente 00 no TLS 1.3Hoje é constante
extensionsvariávelConjunto, ordem e conteúdoO campo mais rico de todos

Três coisas aqui merecem atenção, porque são onde o modelo mental costuma quebrar.

O legacy_version é mentira por design. O TLS 1.3 não diz a versão ali; ele diz 0x0303 (TLS 1.2) e informa a versão real numa extensão, supported_versions. O motivo é histórico: middlebox velha rejeitava hello com número de versão alto, então o campo legado foi congelado para atravessar equipamento idiota. Consequência direta para fingerprint: o JA3 lê o campo legado e por isso não distingue 1.2 de 1.3.

O session id de 32 bytes é o “modo compatibilidade” (Apêndice E.4 da RFC 9846). Para o handshake 1.3 parecer uma retomada 1.2, o cliente manda um session id não vazio e um change_cipher_spec falso. Na minha medição, todas as conexões TLS 1.3 dos 10 stacks mandaram 32 bytes, e todos os clientes restritos a TLS 1.2 mandaram vazio, com uma exceção (o Go, que manda 32 bytes nos dois casos). Ou seja: session id vazio num hello 1.3 é um sinal forte de “não sou browser”.

O random não conta. Em duas execuções do Python requests, os bytes brutos do ClientHello são diferentes (0adf02f5c7e64d31… contra 60bebb560e3f784a…) e o JA3/JA4 são idênticos. Isso é a definição de um fingerprint que funciona: os bytes mudam, a estrutura não.

As extensões são a assinatura

Se você só puder decorar uma tabela neste post, é esta. As extensões são o que separa um cliente do outro, e são identificadas por um número de 16 bits. Estes são os codepoints que aparecem de verdade, conferidos no registro da IANA:

hexNomeO que fazOnde entra
0x0000server_nameSNI: o domínio que você querd/i no JA4
0x0005status_requestpede OCSP staplingconjunto
0x000asupported_groupscurvas/grupos aceitosJA3 (campo 4), fora do JA4
0x000bec_point_formatsformato de ponto ECJA3 (campo 5)
0x000dsignature_algorithmsalgoritmos de assinaturaJA4, na ordem
0x0010ALPNh2, http/1.1…h2/h1 no JA4
0x0012signed_certificate_timestampSCT (CT)conjunto
0x0015paddingpadding do helloconjunto
0x0017extended_master_secretherança do 1.2conjunto
0x001bcompress_certificatecompressão de certconjunto
0x0023session_ticketretomadaconjunto
0x002bsupported_versionsa versão real13/12 no JA4
0x002dpsk_key_exchange_modesmodos PSKconjunto
0x0033key_sharematerial público da trocacontagem, conteúdo nunca
0xff01renegotiation_infoanti-renegociaçãoconjunto
0xfe0dencrypted_client_helloECHconjunto
0x44cdapplication_settings (ALPS)settings de aplicaçãoconjunto

Duas linhas dessa tabela merecem parágrafo próprio, porque são as que eu mais vi descritas de forma errada.

0xfe0d no Chrome é ECH GREASE, não ECH de verdade. O Chrome liga SSL_set_enable_ech_grease(ssl, 1) no caminho oportunista, e o BoringSSL documenta que isso “sends a GREASE ECH extension when no supported ECHConfig is available”. Como localhost não tem ECHConfig em DNS, o que sai é um ECH falso: 218 bytes na conexão que eu inspecionei, com payload aleatório, e tamanho que varia. Ou seja: a extensão existe, mas não está escondendo nada, é ruído de extensibilidade.

0x44cd é ALPS de verdade, e não está registrada. O payload que eu capturei é 00 03 02 68 32: um comprimento de lista, um comprimento de protocolo e as letras h2. É literalmente “minhas settings de aplicação são para h2”. Confirmação cruzada: no Chrome rodando com --disable-http2, essa extensão desaparece e o hello cai de 17 para 16 extensões. O BoringSSL chama essa família de “ALPS (draft-vvv-tls-alps)” e mantém dois codepoints (antigo e novo, selecionados por alps_use_new_codepoint), enquanto o draft deixa o valor da IANA como TBD. Por isso o número que você vê no fio é experimental e some do registro.

GREASE: ruído de propósito

A RFC 8701 reserva valores para serem anunciados ao acaso: 0x0A0A até 0xFAFA para extensões, grupos, algoritmos de assinatura e versões; {0x0A,0x0A} até {0xFA,0xFA} para cipher suites e ALPN. A ideia é obrigar a outra ponta a ignorar o que não conhece, para que o ecossistema não “enferruje” e trave quando um valor novo aparecer.

Para fingerprint, GREASE é ruído injetado de propósito, e um anti-fingerprint acidental: o cliente muda o hello sem mudar de software. Na medição, só o Chrome (e o curl_cffi, que copia o Chrome) manda GREASE: uma cipher, duas extensões, um grupo, uma entrada em supported_versions e um algoritmo de assinatura, com valores re-aleatorizados a cada conexão. Todos os outros nove stacks não mandam nenhum.

TLS 1.2 e 1.3 não se parecem no fio

Vale colocar lado a lado, porque é a diferença mais fácil de perder:

TLS 1.2TLS 1.3
versãolegacy_version e prontolegacy_version fixo + supported_versions
troca de chavegrupos e formatos de pontokey_share com material no hello
compressãolista realexatamente 00
session idretomada real32 bytes aleatórios (compat)
resumptionsession id / ticketPSK + psk_key_exchange_modes

O efeito no fingerprint é direto e meio cruel: como o JA3 lê a versão legada, o campo de versão dele é o mesmo (771) em 1.2 e em 1.3. O que separa os dois hellos é o resto da lista, não a versão. O JA4 lê supported_versions e marca t12 contra t13 na primeira parte. E como 1.3 obriga key_share e psk_key_exchange_modes, a contagem de extensões muda junto.

Cada stack escreve o próprio hello

Aqui está o insight estrutural que faz tudo o resto fazer sentido: o ClientHello não é negociado, é escrito. Ele é o resultado de uma tabela compilada no binário.

Na BoringSSL, ssl/extensions.cc tem uma tabela kExtensions[] e o builder percorre essa tabela chamando o add_clienthello de cada entrada. Na prática, a ordem das extensões é uma constante de compilação, exceto quando é embaralhada. E vamos chegar lá. A lista de ciphers também é uma tabela: kCiphers[], em ssl_cipher.cc, com os três suites TLS 1.3 em ordem fixa.

Só que “tabela compilada” não significa “igual entre stacks”. Medindo, o mesmo pedido por ferramentas diferentes:

Cliente medidoCiphers / extensões (sem GREASE)JA4
curl do macOS 8.7.1 (LibreSSL 3.3.6)49 / 7t13d4907h2_0d8feac7bc37_7395dae3b2f3
curl Homebrew 8.21.0 (OpenSSL 3.6.4)30 / 12t13d3012h2_1d37bd780c83_882d495ac381
Python 3.15 (requests e httpx, OpenSSL)17 / 12t13d1712h1_ab0a1bf427ad_0d46a1bf4a7c
Go 1.25 net/http13 / 12t13d1312h2_f57a46bbacb6_ab7e3b40a677
Java 21.0.8 java.net.http37 / 13t13d3713h2_db35923f8641_7c76daad20ec
Node 26.8.2 (undici)52 / 12t13d5212h1_b262b3658495_8e6e362c5eac
Chrome 15615 / 17t13d1517h2_8daaf6152771_cb7bf5808d99

Repare nas duas primeiras linhas. São os dois curl da mesma máquina: um é o do sistema, o outro é o do Homebrew. Mesmo comando, mesmo nome, fingerprint diferente, porque o do macOS usa LibreSSL e o do Homebrew usa OpenSSL. Se você copiou um tutorial do Linux e rodou no Mac, você está anunciando outra stack.

A linguagem também não é o sinal. O Python e o Node usam OpenSSL por baixo; o Python dá 17 ciphers e o Node dá 52. O que conta é a biblioteca e a versão dela, não a linguagem.

JA3: uma linha, um MD5 e um problema

O JA3 (Salesforce, 2017) é a ideia mais simples possível: pegue cinco campos do hello, junte em texto, hasheie.

TEXT
SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat

Valores em decimal, , entre campos, - dentro do campo, e MD5 no fim. O exemplo do README oficial, que eu recalculei:

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

Funcionou em 2017 porque era barato de calcular e suficientemente discriminante. E funciona até hoje para o que ele é: um jeito de dizer “esses bytes apareceram nesta ordem”.

Só que “nesta ordem” é a parte frágil, e a lista de fraquezas é curta e honesta: ordem importa; não há conteúdo de extensão (só os números); 1.2 e 1.3 se confundem; o MD5 é identificador, não autenticador; e a divergência entre implementações que a Fastly mediu. Nenhuma dessas fraquezas é fatal isoladamente. Juntas, elas deixaram o JA3 pronto para ser morto.

E em 2023 alguém o matou de propósito.

2023: o Chrome quebrou o JA3 de propósito

No código do Chromium, net/socket/ssl_client_socket_impl.cc, existe uma linha incondicional no caminho de conexão TLS:

CPP
SSL_set_permute_extensions(ssl_.get(), 1);

Do outro lado, na BoringSSL, ssl_setup_extension_permutation() faz um Fisher-Yates com RAND_bytes sobre os índices de kExtensions — e o laço de emissão percorre a permutação em vez da ordem natural. Resultado: o Chrome embaralha a ordem das extensões do ClientHello a cada conexão.

O motivo é anti-ossificação, e é um motivo bom: se servidores se acostumam com uma ordem fixa, qualquer mudança futura no Chrome quebra a internet. Embaralhar força o ecossistema a tratar a ordem como irrelevante. A consequência colateral é que o JA3, que depende exatamente dessa ordem, virou ruído.

A Fastly mediu o estrago na época: a partir de 20 de janeiro de 2023, o JA3 mais comum de Chrome na rede deles caiu; e como o número de permutações é “quase 15 fatorial (cerca de 10¹²)”, na prática cada conexão gera um JA3 único. A Cloudflare foi direta: aquilo tornou “older TLS fingerprints almost useless for identifying the latest version of Chrome”.

Agora o número que eu medi, porque é aqui que a teoria vira obviedade. Três conexões novas do mesmo binário do Chrome 156:

TEXT
conexão 1  →  JA3 0e250605ce1f2c793bb1a2269dd30e01
conexão 2  →  JA3 47e376a38884e22f8a6bb4661daa71a9
conexão 3  →  JA3 5c34099f1b500a60c0deacb3f8d31187
       todas →  JA4 t13d1517h2_8daaf6152771_cb7bf5808d99

Três JA3 diferentes. Um JA4. Se você construiu uma regra de WAF em cima de JA3 de browser, ela já não vale há três anos.

JA4: como ler o fingerprint inteiro

O JA4 (FoxIO, 2023) resolve isso com duas decisões: ordenar o que é arbitrário, e colocar informação legível na frente do hash. O formato é a_b_c:

TEXT
t13d1516h2_8daaf6152771_e5627efa2ab1

Vou decodificar esse exemplo da especificação por inteiro, porque ele é o melhor material de estudo que existe, e porque eu recalculei cada hash.

ParteValorSignificado
ttTLS sobre TCP (q = QUIC, d = DTLS)
1313TLS 1.3, lido de supported_versions
ddtem SNI (i = sem SNI, foi para um IP)
151515 cipher suites, sem contar GREASE
161616 extensões, sem contar GREASE
h2h2primeiro e último caractere do primeiro ALPN
8daaf6152771hashsha256 das ciphers ordenadas, truncado em 12
e5627efa2ab1hashsha256 das extensões ordenadas (menos SNI e ALPN) + sigalgs na ordem

As 15 ciphers, nomeadas pelo registro da IANA: 002f e 0035 (RSA CBC), 009c e 009d (RSA GCM), 1301/1302/1303 (os três suites TLS 1.3), c013/c014 (ECDHE_RSA CBC), c02b/c02c (ECDHE_ECDSA GCM), c02f/c030 (ECDHE_RSA GCM) e cca8/cca9 (ECDHE ChaCha20). É a mistura clássica de um cliente moderno: os 130x para 1.3 e uma cesta de 1.2 para conversar com servidor velho.

As 16 extensões: 001b compress_certificate, 0000 SNI, 0033 key_share, 0010 ALPN, 4469 ALPS (o codepoint antigo), 0017 extended_master_secret, 002d psk_key_exchange_modes, 000d sigalgs, 0005 status_request, 0023 session_ticket, 0012 SCT, 002b supported_versions, ff01 renegotiation_info, 000b ec_point_formats, 000a supported_groups, 0015 padding.

Recalculando com sha256sum:

TEXT
sha256("002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9")[:12]
  = 8daaf6152771

sha256("0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601")[:12]
  = e5627efa2ab1

Dois detalhes que valem mais que decorar as regras. Primeiro: SNI e ALPN são excluídos do c de propósito, porque já estão no a. Assim, o mesmo cliente dá o mesmo c indo para um domínio ou para um IP. Segundo: os sigalgs entram na ordem de aparição, sem ordenar, e são colados depois de um _. Se você ordenar, o hash muda; eu vi essa implementação errada nascer num laboratório e o FoxIO publica o vetor que denuncia o erro (0403,0804,0401,…, não ordenado).

Agora o pulo do gato, comparando o exemplo da especificação com o meu Chrome 156 medido:

TEXT
spec       t13d1516h2_8daaf6152771_e5627efa2ab1
Chrome 156 t13d1517h2_8daaf6152771_cb7bf5808d99

O b é idêntico. A lista de ciphers do Chrome não mudou entre esses builds. O que mudou foram as extensões: uma a mais (17 contra 16), e o c mudou junto. Isso prova que o algoritmo está fazendo o trabalho certo: a parte estável ficou estável, a parte que mudou mudou.

Ordenar é uma decisão de projeto

O que eu acho mais elegante no JA4 não é o formato, é a escolha de onde ordenar. Eu construí o caso mínimo: o mesmo conjunto de extensões, em duas ordens.

TEXT
ordem A: 10-11-13-16-23-35-43-51  →  JA3 692aefc05b6b8a9afca0d6486e3b15e4
ordem B: 51-43-35-23-16-13-11-10  →  JA3 79e887fad94c5bfc974a532532781ec3
                                     JA4_c 8ccb6ff8ed2a (igual nos dois)
Diagrama da permutação: a mesma lista de oito extensões em duas ordens gera dois JA3 diferentes e um único JA4_c
mesma lista, duas ordens, dois JA3 e um JA4

Dois JA3, um JA4. E a regra por trás não é “ordem não importa”: é que se ordena o que é semanticamente irrelevante e ativamente randomizado. Extensões TLS se encaixam nas duas coisas. Já a ordem de headers HTTP não é randomizada por ninguém e carrega informação — e o JA4H, o fingerprint de cliente HTTP da mesma família, não ordena os headers. O JA4S, que olha a resposta do servidor, preserva até a ordem original de propósito.

Três decisões, uma régua só. É isso que separa um formato que envelhece bem de um que precisa ser substituído em cinco anos.

O que o JA4 escolhe não ver

Nenhum fingerprint vê tudo, e vale ser explícito sobre o que fica de fora, porque é aí que ficam as decisões erradas de diagnóstico.

O que não entra no JA4Por quêConsequência prática
random, session id, bytes do key_sharesão aleatórioso hash é estável; os bytes nunca são
supported_groupsfora do c de propósitodois clientes que só diferem em grupo pós-quântico dão o mesmo JA4
o nome do SNIsó “tem ou não tem”o hostname não está no hash
ordem das extensõesordenadasinvisível ao JA4, visível ao JA3
ordem das ciphersordenadasvocê ganha estabilidade, perde a preferência
ALPN além do primeiro/último charsó 2 caractereshttp/1.1 e http/1.1,h2 ficam idênticos

Esse último é o mais traiçoeiro, e está nos meus registros: httpx em modo HTTP/2 oferece http/1.1, h2, na ordem invertida, e produz exatamente o mesmo JA4 (t13d1712h1_…) que o requests, que só oferece http/1.1. O campo a pega o primeiro e o último caractere do primeiro valor, então http/1.1 vira h1 e o h2 que veio depois desaparece. Um sensor que só olha JA3/JA4 não consegue saber que aquele cliente pediu HTTP/2.

E o pós-quântico merece uma nota: Chrome, Go, Node e OpenSSL 4 oferecem X25519MLKEM768 (0x11ec, definido na RFC 10024); Java 21 e o curl do macOS não oferecem. Como o JA4 não hasheia grupos nem o conteúdo do key_share (o Chrome manda um share de 1216 bytes, diferente a cada conexão), o mesmo cliente antes e depois do PQ mantém o JA4. A instabilidade dessa mudança fica toda no JA3, que hasheia supported_groups.

Não é só TLS

Se você chegou até aqui achando que o problema é TLS, a má notícia é que existem mais três camadas. E uma quarta que não é camada, é histórico.

Diagrama das camadas: TCP/IP, TLS, HTTP/2, HTTP e comportamento, com o que o edge mede em cada uma e se dá para forjar
cada camada tem um dono · nem todas aceitam disfarce

HTTP/2. A gramática veio de uma white paper da Akamai de 2017 e é a que todo mundo ainda implementa: o fingerprint é montado com os frames que o cliente manda antes de qualquer request útil. A notação é S[;]|WU|P[,]#|PS[,]: parâmetros de SETTINGS com seus valores, na ordem em que aparecem; o incremento do WINDOW_UPDATE; as tuplas de prioridade; e a ordem dos pseudo-headers, codificada como m, p, a, s. O paper mediu mais de 10 milhões de conexões e, em 2017, publicou a ordem de cada browser: Chrome m,a,s,p, Firefox m,p,a,s, Safari m,s,p,a.

Correção de premissa, porque eu mesmo já repeti o erro: o formato da Akamai não inclui flags de HEADER frame. Os três fluxos são SETTINGS, WINDOW_UPDATE e PRIORITY, mais a ordem dos pseudo-headers. Se você vir alguém atribuindo flags de HEADER à Akamai, a fonte não existe.

Mas 2017 é 2017. Medi os clientes desta máquina agora, com ALPN negociando h2 em todas as conexões para nada degradar em silêncio. Dois deles, no formato acima:

TEXT
Chrome 156   1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
Java 21      1:16384;2:0;3:0;4:16777216;5:16384;6:393216|33488897|0|a,m,p,s

O PS do Chrome continua m,a,s,p: a linha de 2017 sobreviveu. O Go continua a,m,p,s, batendo com o paper. E o curl mudou: era m,p,s,a na 7.54 e virou m,s,a,p na 8.x. É a razão para colar a versão ao lado de cada fingerprint numa tabela; sem isso, a tabela envelhece sem avisar.

O campo P está morto. Em doze conexões, zero frames PRIORITY: P=0 em todas. O P do paper nasceu das prioridades de stream reservado do Firefox 53; o Chrome de hoje manda o bloco de prioridade (já deprecado) dentro do HEADERS e um header priority do RFC 9218, e a gramática não codifica nenhum dos dois. Então P=0 significa “não houve frame PRIORITY”, não “não há prioridade”. É uma medida honesta do que um formato de 2017 perde em 2026.

E a lição da seção de TLS se repete uma camada acima: os dois curl desta máquina mandam o mesmo WINDOW_UPDATE (1048510465) e a mesma ordem de SETTINGS (3;4;2), mas o Homebrew manda INITIAL_WINDOW_SIZE=65536 e o do macOS manda 10485760. Mesma ferramenta, build diferente, string diferente.

Isso importa porque um proxy que termina a conexão (e o Burp termina) monta os SETTINGS dele, não os do browser. E o curl_cffi mostra o limite de tudo isso: ele empata com o Chrome no TLS (JA4 idêntico) e no h2 (string idêntica), e ainda assim é pego uma camada acima: o Chrome 156 da medição manda HeadlessChrome/156 com 17 headers, o curl_cffi manda Chrome/152 no Windows com 18, incluindo cache-control. Um impersonator precisa acertar em todas as camadas, e as camadas têm ordem.

Headers HTTP. Ordem, capitalização e defaults. O JA4H cobre isso hasheando os nomes na ordem de aparição. A higiene que quase todo mundo esquece é esta: se você forja o User-Agent para “Chrome 131” mas deixa o sec-ch-ua dizendo outra versão, você não ficou mais parecido com um browser. Ficou inconsistente, o que é pior que o original.

TCP/IP. TTL, janela, MSS e a ordem das opções vêm do kernel, não do seu cliente. O p0f faz isso com um SYN desde sempre, e o equivalente moderno é o JA4T, no formato janela_opções_mss_wscale (o exemplo do FoxIO: 64240_2-1-3-1-1-4_1460_8). É a camada que você não corrige com flag de cliente — e é por isso que mimetizar TLS saindo de um VPS continua contando uma história diferente.

Fingerprint é chave, não veredito

O erro conceitual mais comum é achar que o edge decide olhando um hash. Ele não decide assim, e a Cloudflare documenta o contrário: junto do JA4 ela calcula o que chama de JA4 Signals: estatística agregada da última hora, global. Os nomes são explícitos: browser_ratio_1h, h2h3_ratio_1h, reqs_quantile_1h, uas_rank_1h, paths_rank_1h, cache_ratio_1h, ips_rank_1h, entre outros.

O JA4 é a chave. O que pesa é a distribuição dela. Um JA4 raríssimo que se declara Chrome, com browser_ratio_1h baixo e volume alto, é um caso perdido, e nenhum ajuste de cipher conserta isso. Pior: essas signals são de analytics, não de regra (“They cannot be used as filter fields in WAF custom rule expressions”), então você não consegue nem simular a decisão olhando só o hash.

E existe uma confusão que custa horas: bloqueio e desafio não são a mesma coisa. Um desafio volta como HTML, com o header cf-mitigated: challenge, e existe para rodar JavaScript no seu browser. Um cf_clearance obtido ali é amarrado ao dispositivo que resolveu, e resolver de um IP e usar de outro invalida o solve. Se o seu caso é esse, o problema não é TLS nenhum, e nenhum impersonator vai resolver.

Quem calcula isso hoje

Não é nicho de pesquisa. É campo de regra em produção:

OndeO quêDesde
CloudflareJA4 na Bot Management (Enterprise), campo cf.bot_management.ja4JA4+Signals anunciados em ago/2024
AWS WAFJA3set/2023
AWS WAFJA4, inclusive como chave de rate-based rulemar/2025
Google Cloud Armororigin.tls_ja4_fingerprint e origin.tls_ja3_fingerprintdocumentado hoje
AkamaiJA4 (beta, via header injetado)2026
Fastlyvariável VCL tls.client.ja4 e NGWAFfev/2025

Nas 38 conexões, os 10 stacks produziram 24 JA4 distintos, e três clientes diferentes colapsaram no mesmo (requests, httpx em h1 e em h2). Isso resume o estado da arte: o JA4 é bom em dizer “que tipo de software é isso”, e insuficiente para dizer “qual ferramenta exatamente”. Para isso você precisa da próxima camada.

O que eu levo disso

Quatro coisas, e nenhuma delas é “use ferramenta X”.

O bloqueio acontece antes do payload. Se o seu encoding, o seu header e o seu token estão certos e ainda dá 403, a resposta provavelmente foi tomada no handshake. Olhar o body nesse momento é olhar para o lugar errado.

A stack é o sinal, não a linguagem. Dois curl na mesma máquina, fingerprints diferentes. Um upgrade de biblioteca muda a sua identidade na rede, e ninguém te avisa.

JA3 de browser morreu em 2023, e foi de propósito. Não é bug, não é falta de manutenção: é uma decisão de engenharia do Chrome que a Fastly mediu e a Cloudflare comentou. Quem ainda constrói detecção em cima de JA3 de browser está detectando um alvo que se move a cada conexão.

E o fingerprint nunca é a história inteira. Ele é uma chave numa decisão que também olha prevalência, ASN, rate, cookie e comportamento. Confundir “meu TLS está errado” com “meu IP está queimado” é o jeito mais rápido de passar a tarde inteira ajustando cipher sem mudar nada.

No próximo post eu faço o caminho inverso: pego o Burp, mostro em três camadas por que ele anuncia que é uma ferramenta Java, e o que dá para fazer a respeito — incluindo o que não dá, e por quê.