Volver al Centro de conocimiento
Huella digital

Huellas TLS: JA3, JA4 y la trama HTTP/2

Cómo un ClientHello se convierte en JA3 o JA4, por qué uno cambia entre ejecuciones, qué declara una trama SETTINGS y cómo revisar el propio cliente.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Huella digital?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

Una huella en la capa de conexión se calcula a partir de bytes que existen antes que cualquier documento. BotBrowser proporciona un perfil declarado que mantiene coherentes entre sí las superficies que una página puede leer, y no puede exponer el ClientHello de TLS ni la trama de ajustes de HTTP/2, porque ninguna de las dos es una superficie de página. El recorrido parte de un ClientHello capturado y de una trama de ajustes decodificada, expone cómo se arma cada huella y muestra por qué dos ejecuciones del mismo navegador pueden dar un valor estable con una construcción y otro distinto con otra.

Resumen

  • JA3 une cinco campos del ClientHello en el orden en que aparecen y aplica un hash al resultado, así que el orden de las extensiones forma parte del valor.
  • Un ClientHello capturado de Chrome 157 produjo t13d1517h2 como prefijo JA4: TLS 1.3, nombre de dominio presente, 15 cifrados, 17 extensiones y h2 tomado del primer nombre de protocolo de la lista.
  • Dos ejecuciones del mismo navegador dieron valores JA3 distintos y un valor JA4 idéntico, porque Chrome aleatoriza el orden de las extensiones y JA4 ordena antes de aplicar el hash.
  • El orden de las extensiones cambió por completo entre ejecuciones mientras el conjunto ordenado siguió igual, así que un hash sin ordenar mide un orden que el cliente no fija.
  • Un cliente TLS 1.3 informa legacy_version 0x0303 y coloca la versión real en la extensión de versiones admitidas, así que leer solo el campo heredado indica TLS 1.2 para una negociación TLS 1.3.
  • La trama SETTINGS de HTTP/2 son entradas de seis bytes con un identificador de dos bytes y un valor de cuatro bytes; un cliente y un servidor envían conjuntos distintos.
  • Dos negociaciones que solo difieren en la lista de protocolos ofrecidos llegan a la misma versión TLS con el mismo cifrado y acuerdan protocolos distintos; una página puede leer el nombre negociado pero no la lista ofrecida detrás de él.
  • Un script de página no puede leer ninguno de estos valores, y ningún ajuste del lado de la página los cambia.
Un registro ClientHello dividido en sus cinco campos JA3 con la lista de extensiones en dos órdenes, después el prefijo JA4 descompuesto en versión, presencia de nombre, número de cifrados, número de extensiones e iniciales de protocolo, y una trama SETTINGS representada como entradas de seis bytes.

Contenido

El ClientHello, sus campos y el problema del orden de JA3

El primer mensaje de una conexión TLS es el ClientHello. Declara las versiones que admite el cliente, los conjuntos de cifrado que acepta y una lista de extensiones numeradas. Capturarlo desde un listener local en lugar de desde un objeto de biblioteca es lo que hace verificable el resto del recorrido, porque los bytes son la entrada que el par realmente lee.

record_bytes=1949 handshake_bytes=1944
legacy_version=0x0303
offered_versions=0x0304,0x0303
sni=localhost
alpn=h2,http/1.1
cipher_count=15 extension_count=17
groups=11ec,001d,0017,0018
point_formats=0

Dos líneas de esa salida concentran casi toda la confusión del tema. La primera es legacy_version=0x0303, que es el valor de TLS 1.2 aunque se trate de un cliente TLS 1.3; la respuesta real está en la extensión de versiones admitidas, en 0x0304. Una herramienta que informa el campo heredado como versión lee el campo correctamente y responde a una pregunta distinta de la que el lector planteó.

La segunda es el valor grease de la lista de extensiones. Un cliente inserta uno para comprobar que un servidor trata bien los números desconocidos, y toda construcción de huella descrita aquí filtra el grease. Un analizador que conserva esos valores produce una huella que cambia en cada conexión y no coincide con nada.

JA3 toma cinco campos del ClientHello, los escribe en un orden fijo, los une con comas y guiones y aplica MD5 al resultado. Los campos son la versión, la lista de cifrados, la lista de extensiones, la lista de curvas elípticas y la lista de formatos de punto de curva elíptica. La construcción es lo bastante simple para enunciarla por completo, y esa completitud explica su amplia difusión.

ja3_string=771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,17613-35-51-13-11-43-65281-5-0-65037-10-45-16-23-51764-27-18,4588-29-23-24,0
ja3=33bb5fa9c4be545f39008976c0f67d67

Lee el segundo campo de esa cadena y aparece la razón de la fragilidad del valor. Los cifrados se enumeran en el orden en que el cliente los ofreció, y ese orden forma parte del hash. El mismo conjunto de quince cifrados escrito en otra secuencia produce un MD5 distinto sin que cambie nada de lo que el cliente admite.

El campo de extensiones arrastra la misma debilidad con más fuerza. Las extensiones son una lista, y un cliente que baraja la lista sigue hablando las mismas capacidades. JA3 aplica el hash al barajado. No es un defecto de la definición, porque la definición dice lo que dice; es un límite de lo que el valor puede significar, y quien interpreta un JA3 cambiado como prueba de un cliente distinto está leyendo el orden como identidad.

JA4, lo que indica el prefijo y lo que oculta TLS 1.3

JA4 conserva la idea y cambia dos cosas: ordena cada lista antes de aplicar el hash y antepone un prefijo legible para que el valor pueda inspeccionarse sin una tabla de consulta. El prefijo es una secuencia fija de caracteres, y cada uno declara un hecho concreto.

ja4_prefix=t13d1517h2
ja4=t13d1517h2_8daaf6152771_cb7bf5808d99

La t marca una conexión TCP. El 13 es la versión TLS negociada, leída de la extensión de versiones admitidas y no del campo heredado. La d indica que había un nombre de dominio, frente a i cuando el cliente se conectó a una dirección y no envió ninguno. El 15 de dos dígitos que sigue es el número de cifrados, y el segundo par, 17, es el número de extensiones. El h2 final procede del primer nombre de protocolo de la lista ofrecida, tomando su primer y su último carácter alfanumérico, así que h2 da h2 y una lista encabezada por http/1.1 da h1.

El valor posterior al primer guion bajo aplica un hash a los cifrados, y el posterior al segundo lo aplica a las extensiones junto con los algoritmos de firma. Como ambas listas se ordenan primero, reordenarlas no mueve ninguno de los dos hashes. Ese único cambio separa un valor que describe un cliente de un valor que describe un mensaje concreto.

El número del prefijo y el número del hash no son la misma cifra. El prefijo cuenta todas las extensiones que no son grease, incluidas la extensión de nombre de dominio y la de negociación de protocolo, así que una captura con diecisiete extensiones sin GREASE indica 17 en el prefijo y aplica el hash a las quince restantes. La parte hasheada descarta esas dos, porque el prefijo ya las declara, y aplica el hash a la lista restante junto con los algoritmos de firma. Quien recalcula ambos a mano tiene que mantener los dos recuentos separados, y un prefijo que sale dos unidades por debajo suele ser el error de aplicar al prefijo la exclusión del hash.

Una conexión HTTP/2 comienza con una trama SETTINGS, y esa trama es una lista de entradas de seis bytes: un identificador de dos bytes y un valor de cuatro bytes. Hay seis identificadores definidos, y un cliente concreto envía solo algunos.

settings:chrome_like bytes=24 HEADER_TABLE_SIZE=65536 ENABLE_PUSH=0 INITIAL_WINDOW_SIZE=6291456 MAX_HEADER_LIST_SIZE=262144
settings:node_like bytes=30 HEADER_TABLE_SIZE=4096 ENABLE_PUSH=0 INITIAL_WINDOW_SIZE=65535 MAX_FRAME_SIZE=16384 MAX_CONCURRENT_STREAMS=4294967295
settings_distinct=true

Dos clientes que hablan HTTP/2 envían por tanto tramas visiblemente distintas. El primero declara una ventana de 6291456 y un límite de lista de cabeceras de 262144; el segundo declara una ventana de 65535 y un tamaño de trama de 16384. La longitud de la trama también difiere, 24 bytes frente a 30, porque el primero envía cuatro entradas y el segundo cinco.

La dirección importa tanto como el contenido. Un cliente envía un conjunto y lee otro distinto del servidor:

server_sees_client_settings -> headerTableSize=4096 enablePush=true initialWindowSize=65535 maxFrameSize=16384 maxConcurrentStreams=100

Esa salida es lo que un servidor observó de su par, no lo que cada lado pretendía declarar. Quien compara una trama capturada con un valor predeterminado documentado compara un mensaje con un valor predeterminado, y ambos discrepan siempre que el cliente omite un identificador, porque una entrada omitida conserva su valor predeterminado.

El paso a TLS 1.3 cambió el objeto del fingerprinting de una forma fácil de enunciar y fácil de malinterpretar. Casi toda la negociación posterior al ClientHello va cifrada, incluido el certificado que envía el servidor. Un observador pasivo en la ruta ve por tanto el ClientHello en claro y después ve texto cifrado. Es una reducción real de lo que aprende un observador, y tiene una consecuencia concreta: el ClientHello pasó a ser el mensaje más informativo de la conexión, porque es el único que sigue siendo legible.

La consecuencia para una huella es más estrecha de lo que parece. JA3 lee solo el ClientHello, así que TLS 1.3 no eliminó sus entradas; las convirtió en una proporción mayor del total. JA4 se diseñó con eso en mente y lee la extensión de versiones admitidas para que un cliente TLS 1.3 se etiquete 13 y no con el 12 que sugiere el campo heredado. Ambos valores describen el mensaje de apertura y ninguno describe el intercambio cifrado que sigue.

Hay un segundo efecto menos evidente. En TLS 1.3 el cliente envía su clave compartida en una extensión, así que un cliente que acierta el grupo preferido del servidor puede enviar datos un viaje de ida y vuelta antes. El conjunto de grupos que ofrece un cliente, y el orden en que los ofrece, es por tanto en parte una decisión de rendimiento y no solo de compatibilidad, y una compilación que cambia su grupo predeterminado puede alterar la parte del saludo cercana al cifrado sin cambiar nada que una página pueda leer. Quien compara dos huellas entre una actualización de versión debe esperar movimiento en la lista de grupos y no interpretarlo como una deriva de configuración.

Una conexión con proxy plantea qué cliente describe una huella, y la respuesta depende de dónde termina TLS. Cuando el proxy tuneliza bytes sin procesar, el ClientHello del propio cliente llega intacto al destino y la huella pertenece al cliente. Cuando el proxy termina TLS y abre una segunda conexión propia, el destino ve el ClientHello del proxy y la huella pertenece al proxy.

tunnel_mode    -> target reads the client's ClientHello; value describes the client
terminate_mode -> target reads the proxy's ClientHello; value describes the proxy

Esa distinción explica un informe que de otro modo parece imposible: dos máquinas con navegadores distintos producen la misma huella, o una máquina produce dos huellas distintas en peticiones alternas. Ninguno de los dos casos es un error de medición. El primero es un proxy que termina TLS con una pila fija delante de ambos navegadores, y el segundo es un grupo de proxies cuyos miembros se compilaron con bibliotecas TLS distintas.

La prueba práctica es la misma captura descrita antes, ejecutada en ambos extremos. Captura en el cliente y captura lo que recibe el destino; si las dos difieren, algo en la ruta reconstruyó la negociación, y toda conclusión posterior sobre el cliente se apoya ahora en el mensaje equivocado. Por eso una comparación de huellas entre dos redes debe indicar la ruta y no solo el navegador, ya que la ruta puede ser la variable que cambió.

La trama SETTINGS, el cambio entre ejecuciones y la terminación por proxy

La medición más útil del conjunto es la que sorprende a la gente. El mismo navegador, la misma versión, los mismos ajustes de perfil, lanzados dos veces contra el mismo listener, produjeron dos valores JA3 distintos y un valor JA4 idéntico.

run1: bytes=1949 ja3=b488a1aa72f7622264c0b7ecca9dcbf0 ja4=t13d1517h2_8daaf6152771_cb7bf5808d99
run2: bytes=1917 ja3=c88062ec9c84b56a124df18d005b1d9b ja4=t13d1517h2_8daaf6152771_cb7bf5808d99
same_ja3=false
same_ja4=true

Un par de ejecuciones posterior produjo 1917 bytes frente a 1949 y los mismos dos resultados, un JA3 distinto cada vez y un JA4 compartido. El recuento de bytes también se mueve, porque los valores grease de la lista de extensiones varían de ancho, así que una longitud igual es una coincidencia de un par y no una propiedad del cliente.

El recuento de bytes difiere entre las dos ejecuciones, y esa es la primera pista de que los valores grease se movieron: Chrome sortea un número grease nuevo para la lista de cifrados, la lista de extensiones y la lista de algoritmos de firma en cada conexión, y esos marcadores varían de ancho. Si se filtra el grease, los conjuntos subyacentes coinciden mientras el orden bruto sigue sin coincidir, y por eso una comparación ordenada se sostiene y una sin ordenar no.

run1 ext order: 65037-11-35-0-23-13-5-65281-10-18-16-43-45-51764-17613-27-51
run2 ext order: 10-27-45-5-43-18-51-0-23-35-65037-16-11-17613-51764-65281-13
same_raw_ext_order=false
same_sorted_set=true
same_ja4=true

De ahí salen tres lecturas prácticas. Un JA3 cambiado entre dos ejecuciones del mismo navegador es normal y no prueba una mala configuración, que es lo primero que hay que descartar antes de investigar cualquier otra cosa. Un JA4 que se mantiene constante entre esas ejecuciones describe al cliente y no al mensaje. Y una herramienta que informa un JA3 sin decir de qué mensaje provino ha registrado un hecho sobre una conexión, no sobre un navegador.

Otros cambios sí mueven ambos valores, y conviene separarlos del reordenamiento. Una actualización de la biblioteca TLS puede añadir o quitar un cifrado, una compilación puede añadir una extensión, y un proxy que termina TLS sustituye la negociación entera, así que el valor pertenece al proxy y no al navegador que está detrás. Cada uno de esos cambios altera el conjunto ordenado, así que un JA4 que se mueve es un tipo de evento distinto de un JA3 que se mueve.

Comprobar el propio cliente y por qué una huella no es un veredicto

La forma segura de saber qué envía el propio cliente es leer una negociación propia. Un listener local que acepta una conexión e imprime el primer registro basta, y no necesita certificado porque la negociación nunca se completa.

import socket
import struct

server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 8443))
server.listen(1)

# Point the client you want to measure at https://localhost:8443 and accept its hello.
connection, _ = server.accept()
record = connection.recv(8192)
connection.close()
server.close()

length = struct.unpack(">H", record[3:5])[0]
print("record_type", record[0], "record_length", length, "captured", len(record))
print("first_bytes", record[5:9].hex())

Cabe esperar un tipo de registro 22, que es un registro de handshake, y una longitud cercana al número de bytes capturados. Los cuatro bytes posteriores a la cabecera del registro son el tipo de handshake y una longitud de tres bytes. Una captura que no devuelve nada suele significar que el cliente probó la dirección, falló y siguió adelante, lo que ocurre cuando un nombre resuelve a una dirección donde no se está escuchando.

Dos reglas hacen utilizable una captura. Apunta el cliente a tu propio listener y no a un sitio que no operas, porque capturar el extremo de otra persona mide su conexión y no tu cliente. Y captura más de una vez, porque un solo mensaje no dice si un valor es estable; la comparación entre ejecuciones anterior es la parte que convierte un número en una afirmación.

Los valores descritos aquí son descriptivos, y la distancia entre describir un cliente y juzgarlo es donde ocurren casi todas las malas interpretaciones. Un JA4 que coincide con un navegador conocido significa que la negociación es coherente con la pila de ese navegador. No significa que la sesión sea legítima, y un valor que no coincide no significa que la sesión sea automatizada. Una compilación del mismo código con otra biblioteca TLS, un intermediario corporativo que reescribe la negociación y un navegador detrás de un proxy que termina TLS producen una discrepancia sin que haya automatización alguna.

Por eso un valor de la capa de conexión se lee normalmente junto con otras pruebas y no de forma aislada. Un solo valor contiene un hecho: estos parámetros de negociación se ofrecieron en esta conexión. Convertir ese hecho en una afirmación sobre una sesión exige una segunda observación desde otra capa, y la afirmación es tan fuerte como el acuerdo entre ambas. Una página que informa un entorno y una negociación que informa otro es el caso interesante, y lo es precisamente porque uno de los dos está equivocado de forma medible.

Para quien opera una flota de navegadores, el hábito útil es registrar el valor junto con la ruta y la marca de tiempo, y no en aislamiento. Una huella guardada sin ruta no puede responder la pregunta que hará un lector posterior: si cambió el cliente o cambió el camino. El par medido en la sección anterior es la versión mínima de ese registro: dos ejecuciones, una ruta, un valor que se movió y otro que no. Guardar además el prefijo t13d1517h2 y la longitud de la captura convierte ese par en una fila que otro lector puede contrastar sin repetir la medición.

Conclusión práctica

Lee un JA3 como un hecho sobre un mensaje y un JA4 como un hecho sobre un cliente, y los informes confusos dejan de serlo. El orden de extensiones que un JA3 incluye en el hash lo baraja el cliente entre conexiones, así que un JA3 cambiado con un JA4 sin cambios es el resultado esperado y no una falla. Una negociación TLS 1.3 informa legacy_version 0x0303 a propósito, así que una versión leída solo de ese campo subestima el protocolo. Y una trama SETTINGS describe lo que un cliente declaró a un par, con los identificadores omitidos en sus valores predeterminados, y por eso dos clientes HTTP/2 pueden verse distintos y ambos ser correctos. Qué reglas de detección necesitan un valor del cable muestra dónde entran estos hechos en una decisión, y fingerprinting de información de red cubre los hechos vecinos que una página alcanza por sí sola.

Nada de esto hace que un perfil del lado de la página sea responsable de la negociación. Un perfil declarado puede mantener coherentes entre sí las superficies legibles El cliente que usa esta construcción sin navegador se trata en paridad TLS de curl-impersonate y curl_cffi., y la negociación se produce por debajo de ellas en la pila TLS, así que un documento coherente y una negociación estable son dos resultados separados obtenidos en dos lugares separados. Tomar el primero como prueba del segundo es el error que convierte dos resultados separados en una conclusión falsa.

Fuentes

#TLS#JA3#JA4#HTTP2#fingerprinting

Este artículo forma parte de la guía: ¿Qué es la huella digital del navegador?

Ejecuta la misma comprobación en BotBrowser

Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.