Empreintes TLS : JA3, JA4 et la trame HTTP/2
Comment un ClientHello devient JA3 ou JA4, pourquoi une valeur change entre exécutions, ce que déclare une trame SETTINGS et comment vérifier son client.
BotBrowser Team
Vous voulez la documentation structurée pour Empreinte ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
Une empreinte de la couche de connexion se calcule à partir d'octets qui existent avant tout document. BotBrowser fournit un profil déclaré qui garde cohérentes entre elles les surfaces qu'une page peut lire, et ne peut pas exposer le ClientHello TLS ni la trame SETTINGS de HTTP/2, car aucune des deux n'est une surface de page. Le parcours part d'un ClientHello capturé et d'une trame SETTINGS décodée, expose la construction de chaque empreinte et montre pourquoi deux exécutions du même navigateur peuvent donner une valeur stable avec une construction et une valeur différente avec l'autre.
Résumé
- JA3 réunit cinq champs du ClientHello dans l'ordre où ils apparaissent et applique un hachage au résultat, donc l'ordre des extensions fait partie de la valeur.
- Un ClientHello Chrome 157 capturé a produit
t13d1517h2comme préfixe JA4 : TLS 1.3, nom de domaine présent, 15 chiffrements, 17 extensions eth2tiré du premier nom de protocole de la liste. - Deux exécutions du même navigateur ont donné des valeurs JA3 différentes et une valeur JA4 identique, parce que Chrome randomise l'ordre des extensions et que JA4 trie avant de hacher.
- L'ordre des extensions a changé du tout au tout entre les exécutions alors que l'ensemble trié restait identique, donc un hachage non trié mesure un ordre que le client ne fixe pas.
- Un client TLS 1.3 annonce
legacy_version0x0303 et place la version réelle dans l'extension des versions prises en charge, donc une version lue dans le seul champ hérité annonce TLS 1.2 pour une poignée de main TLS 1.3. - La trame SETTINGS de HTTP/2 est une suite d'entrées de six octets, avec un identifiant de deux octets et une valeur de quatre octets ; un client et un serveur en envoient des ensembles différents.
- Deux poignées de main qui ne diffèrent que par la liste des protocoles offerts atteignent la même version TLS avec le même chiffrement et négocient des protocoles différents ; une page peut lire le nom négocié mais pas la liste offerte derrière lui.
- Un script de page ne peut lire aucune de ces valeurs, et aucun réglage côté page ne les modifie.
Sommaire
- Le ClientHello, ses champs et le problème de l'ordre en JA3
- JA4, ce qu'indique le préfixe et ce que TLS 1.3 masque
- La trame SETTINGS, le changement entre exécutions et la terminaison par proxy
- Vérifier son propre client et pourquoi une empreinte n'est pas un verdict
- Conclusion pratique
Le ClientHello, ses champs et le problème de l'ordre en JA3
Le premier message d'une connexion TLS est le ClientHello. Il déclare les versions prises en charge par le client, les suites de chiffrement qu'il accepte et une liste d'extensions numérotées. Le capturer depuis un écouteur local plutôt que depuis un objet de bibliothèque est ce qui rend le reste du parcours vérifiable, car les octets sont l'entrée que le pair lit réellement.
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
Deux lignes de cette sortie concentrent presque toute la confusion du sujet. La première est legacy_version=0x0303, qui est la valeur de TLS 1.2 alors qu'il s'agit d'un client TLS 1.3 ; la réponse réelle se trouve dans l'extension des versions prises en charge, à 0x0304. Un outil qui annonce le champ hérité comme version lit correctement le champ et répond à une autre question que celle posée par le lecteur.
La seconde est la valeur grease de la liste des extensions. Un client en insère une pour vérifier qu'un serveur traite correctement les numéros inconnus, et toute construction d'empreinte décrite ici filtre le grease. Un analyseur qui conserve ces valeurs produit une empreinte qui change à chaque connexion et ne correspond à rien.
JA3 prend cinq champs du ClientHello, les écrit dans un ordre fixe, les réunit avec des virgules et des tirets et applique MD5 au résultat. Ces champs sont la version, la liste des chiffrements, la liste des extensions, la liste des courbes elliptiques et la liste des formats de point de courbe elliptique. La construction est assez simple pour être énoncée en entier, et cette exhaustivité explique sa large diffusion.
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
Lisez le deuxième champ de cette chaîne et la raison de la fragilité de la valeur apparaît. Les chiffrements sont listés dans l'ordre où le client les a offerts, et cet ordre fait partie du hachage. Le même ensemble de quinze chiffrements écrit dans une autre séquence produit un MD5 différent sans que change ce que le client prend en charge.
Le champ des extensions porte la même faiblesse plus fortement. Les extensions forment une liste, et un client qui mélange la liste parle toujours les mêmes capacités. JA3 hache le mélange. Ce n'est pas un défaut de la définition, car la définition dit ce qu'elle dit ; c'est une limite de ce que la valeur peut signifier, et un lecteur qui traite un JA3 modifié comme la preuve d'un client différent lit l'ordre comme une identité.
JA4, ce qu'indique le préfixe et ce que TLS 1.3 masque
JA4 conserve l'idée et change deux choses : il trie chaque liste avant de hacher et place devant un préfixe lisible afin qu'une valeur puisse être inspectée sans table de correspondance. Le préfixe est une suite fixe de caractères, et chacun énonce un fait précis.
ja4_prefix=t13d1517h2
ja4=t13d1517h2_8daaf6152771_cb7bf5808d99
Le t marque une connexion TCP. Le 13 est la version TLS négociée, lue dans l'extension des versions prises en charge plutôt que dans le champ hérité. Le d indique qu'un nom de domaine était présent, contre i lorsque le client s'est connecté à une adresse sans en envoyer. Le 15 à deux chiffres qui suit est le nombre de chiffrements, et la seconde paire, 17, est le nombre d'extensions. Le h2 final vient du premier nom de protocole de la liste offerte, en prenant son premier et son dernier caractère alphanumérique, donc h2 donne h2 et une liste menée par http/1.1 donne h1.
La valeur après le premier tiret bas hache les chiffrements, et celle après le second hache les extensions avec les algorithmes de signature. Comme les deux listes sont d'abord triées, les réordonner ne déplace aucun des deux hachages. Ce seul changement sépare une valeur qui décrit un client d'une valeur qui décrit un message particulier.
Le nombre du préfixe et le nombre du hachage ne sont pas la même valeur. Le préfixe compte toutes les extensions non grease, y compris l'extension du nom de domaine et celle de négociation de protocole, ainsi une capture avec dix-sept extensions hors GREASE affiche 17 dans le préfixe et hache les quinze restantes. La partie hachée écarte ces deux-là, parce que le préfixe les énonce déjà, et hache la liste restante avec les algorithmes de signature. Un lecteur qui recalcule les deux à la main doit garder les deux décomptes séparés, et un préfixe inférieur de deux unités vient généralement de l'application au préfixe de l'exclusion prévue pour le hachage.
Une connexion HTTP/2 commence par une trame SETTINGS, et cette trame est une liste d'entrées de six octets : un identifiant de deux octets et une valeur de quatre octets. Six identifiants sont définis, et un client donné n'en envoie qu'une partie.
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
Deux clients qui parlent tous deux HTTP/2 envoient donc des trames visiblement différentes. Le premier déclare une fenêtre de 6291456 et une limite de liste d'en-têtes de 262144 ; le second déclare une fenêtre de 65535 et une taille de trame de 16384. La longueur de la trame diffère aussi, 24 octets contre 30, parce que le premier envoie quatre entrées et le second cinq.
Le sens compte autant que le contenu. Un client envoie un ensemble et lit un ensemble différent depuis le serveur :
server_sees_client_settings -> headerTableSize=4096 enablePush=true initialWindowSize=65535 maxFrameSize=16384 maxConcurrentStreams=100
Cette sortie est ce qu'un serveur a observé de son pair, et non ce que chaque côté voulait déclarer. Un lecteur qui compare une trame capturée à une valeur par défaut documentée compare un message à une valeur par défaut, et les deux divergent dès que le client omet un identifiant, car une entrée omise conserve sa valeur par défaut.
Le passage à TLS 1.3 a changé l'objet du fingerprinting d'une manière facile à énoncer et facile à mal lire. Presque toute la poignée de main après le ClientHello est chiffrée, y compris le certificat envoyé par le serveur. Un observateur passif sur le chemin voit donc le ClientHello en clair puis du texte chiffré. C'est une réduction réelle de ce qu'un observateur apprend, et elle a une conséquence précise : le ClientHello est devenu le message le plus informatif de la connexion, car c'est le seul encore lisible.
La conséquence pour une empreinte est plus étroite qu'il n'y paraît. JA3 ne lit que le ClientHello, donc TLS 1.3 n'a pas supprimé ses entrées ; il en a fait une part plus grande du total. JA4 a été conçu avec cela en tête et lit l'extension des versions prises en charge pour qu'un client TLS 1.3 soit étiqueté 13 plutôt que par le 12 que suggère son champ hérité. Les deux valeurs décrivent le message d'ouverture et aucune ne décrit l'échange chiffré qui suit.
Un second effet, moins évident, existe. En TLS 1.3 le client envoie son secret de partage dans une extension, donc un client qui devine le groupe préféré du serveur peut envoyer des données un aller-retour plus tôt. L'ensemble des groupes qu'un client offre, et l'ordre dans lequel il les offre, relève donc en partie d'une décision de performance et pas seulement de compatibilité, et une compilation qui change son groupe par défaut peut modifier la partie du handshake proche du chiffrement sans changer rien que la page puisse lire. Un lecteur qui compare deux empreintes lors d'une montée de version doit s'attendre à un mouvement dans la liste des groupes et ne pas l'interpréter comme une dérive de configuration.
Une connexion via proxy pose la question du client que décrit une empreinte, et la réponse dépend de l'endroit où TLS se termine. Lorsque le proxy tunnelise des octets bruts, le ClientHello du client lui-même atteint la cible sans modification et l'empreinte appartient au client. Lorsque le proxy termine TLS et ouvre une seconde connexion qui lui est propre, la cible voit le ClientHello du proxy et l'empreinte appartient au 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
Cette distinction explique un rapport qui semble sinon impossible : deux machines avec des navigateurs différents produisent la même empreinte, ou une machine produit deux empreintes différentes sur des requêtes alternées. Ni l'un ni l'autre n'est une erreur de mesure. Le premier cas est un proxy qui termine TLS avec une pile fixe devant les deux navigateurs, et le second est un pool de proxies dont les membres ont été compilés avec des bibliothèques TLS différentes.
Le test pratique est la même capture décrite plus haut, exécutée aux deux extrémités. Capturez chez le client et capturez ce que la cible reçoit ; si les deux diffèrent, quelque chose sur le chemin a reconstruit la poignée de main, et toute conclusion en aval sur le client repose désormais sur le mauvais message. C'est aussi pourquoi une comparaison d'empreintes entre deux réseaux doit indiquer la route et pas seulement le navigateur, puisque la route peut être la variable qui a changé.
La trame SETTINGS, le changement entre exécutions et la terminaison par proxy
La mesure la plus utile de l'ensemble est celle qui surprend. Le même navigateur, la même version, les mêmes réglages de profil, lancés deux fois contre le même écouteur, ont produit deux valeurs JA3 différentes et une valeur JA4 identique.
run1: bytes=1949 ja3=b488a1aa72f7622264c0b7ecca9dcbf0 ja4=t13d1517h2_8daaf6152771_cb7bf5808d99
run2: bytes=1917 ja3=c88062ec9c84b56a124df18d005b1d9b ja4=t13d1517h2_8daaf6152771_cb7bf5808d99
same_ja3=false
same_ja4=true
Une paire d'exécutions ultérieure a produit 1917 octets contre 1949 et les deux mêmes résultats, un JA3 différent à chaque fois et un JA4 commun. Le nombre d'octets bouge aussi, car les valeurs grease de la liste des extensions varient en largeur, donc une longueur égale est une coïncidence d'une paire et non une propriété du client.
Le nombre d'octets diffère entre les deux exécutions, et c'est le premier indice que les valeurs grease ont bougé : Chrome tire un nouveau numéro grease pour la liste des chiffrements, la liste des extensions et la liste des algorithmes de signature à chaque connexion, et ces marqueurs varient en largeur. Une fois le grease filtré, les ensembles sous-jacents correspondent alors que l'ordre brut ne correspond toujours pas, ce qui explique qu'une comparaison triée tienne et qu'une comparaison non triée échoue.
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
Trois lectures pratiques en découlent. Un JA3 modifié entre deux exécutions du même navigateur est normal et ne prouve pas une mauvaise configuration, ce qu'il faut écarter en premier avant d'examiner autre chose. Un JA4 qui reste constant sur ces exécutions décrit le client et non le message. Et un outil qui annonce un JA3 sans dire de quel message il provient a enregistré un fait sur une connexion, pas sur un navigateur.
D'autres changements déplacent bien les deux valeurs, et il vaut la peine de les séparer du réordonnancement. Une mise à jour de bibliothèque TLS peut ajouter ou retirer un chiffrement, une compilation peut ajouter une extension, et un proxy qui termine TLS remplace entièrement la poignée de main, donc la valeur appartient au proxy et non au navigateur derrière lui. Chacun de ces changements modifie l'ensemble trié, donc un JA4 qui bouge est un type d'événement différent d'un JA3 qui bouge.
Vérifier son propre client et pourquoi une empreinte n'est pas un verdict
La manière sûre de savoir ce que son client envoie est de lire une poignée de main que l'on possède. Un écouteur local qui accepte une connexion et imprime le premier enregistrement suffit, et il n'a pas besoin de certificat puisque la poignée de main ne se termine jamais.
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())
Attendez un type d'enregistrement 22, qui est un enregistrement de handshake, et une longueur proche du nombre d'octets capturés. Les quatre octets après l'en-tête d'enregistrement sont le type de handshake et une longueur de trois octets. Une capture qui ne renvoie rien signifie généralement que le client a essayé l'adresse, a échoué et a continué, ce qui arrive lorsqu'un nom résout vers une adresse où l'on n'écoute pas.
Deux règles rendent une capture utilisable. Dirigez le client vers votre propre écouteur et non vers un site que vous n'exploitez pas, car capturer le point de terminaison d'autrui mesure sa connexion et non la vôtre. Et capturez plus d'une fois, car un seul message ne dit pas si une valeur est stable ; la comparaison entre exécutions ci-dessus est la partie qui transforme un nombre en affirmation.
Les valeurs décrites ici sont descriptives, et l'écart entre décrire un client et le juger est l'endroit où se produisent la plupart des mauvaises lectures. Un JA4 qui correspond à un navigateur connu signifie que la poignée de main est cohérente avec la pile de ce navigateur. Cela ne signifie pas que la session est légitime, et une valeur qui ne correspond pas ne signifie pas que la session est automatisée. Une compilation du même code avec une autre bibliothèque TLS, un intermédiaire d'entreprise qui réécrit la poignée de main et un navigateur derrière un proxy qui termine TLS produisent tous une divergence sans aucune automatisation présente.
C'est pourquoi une valeur de la couche de connexion se lit normalement avec d'autres preuves plutôt qu'isolément. Une seule valeur porte un fait : ces paramètres de poignée de main ont été offerts sur cette connexion. Transformer ce fait en affirmation sur une session demande une seconde observation depuis une autre couche, et l'affirmation est aussi solide que l'accord entre les deux. Une page qui annonce un environnement et une poignée de main qui en annonce un autre constituent le cas intéressant, et il est intéressant précisément parce que l'un des deux est mesurablement faux.
Pour qui exploite une flotte de navigateurs, l'habitude utile est d'enregistrer la valeur avec la route et l'horodatage plutôt qu'en isolation. Une empreinte stockée sans route ne peut pas répondre à la question qu'un lecteur ultérieur posera : est-ce le client qui a changé ou le chemin. La paire mesurée dans la section précédente est la version minimale de cet enregistrement : deux exécutions, une route, une valeur qui a bougé et une qui n'a pas bougé. Ajouter le préfixe t13d1517h2, la longueur de la capture et le JA4 complet t13d1517h2_8daaf6152771_cb7bf5808d99 rend cette ligne vérifiable sans refaire la mesure.
Conclusion pratique
Lisez un JA3 comme un fait sur un message et un JA4 comme un fait sur un client, et les rapports confus cessent de l'être. L'ordre des extensions que JA3 hache est mélangé par le client entre les connexions, donc un JA3 modifié avec un JA4 inchangé est le résultat attendu et non une panne. Une poignée de main TLS 1.3 annonce legacy_version 0x0303 à dessein, donc une version lue dans ce seul champ sous-estime le protocole. Et une trame SETTINGS décrit ce qu'un client a déclaré à un pair, les identifiants omis restant à leurs valeurs par défaut, ce qui explique que deux clients HTTP/2 puissent sembler différents tout en étant corrects. Quelles règles de détection ont besoin d'une valeur du câble montre où ces faits entrent dans une décision, et le fingerprinting des informations réseau couvre les faits voisins qu'une page atteint seule.
Rien de tout cela ne rend un profil côté page responsable de la poignée de main. Un profil déclaré peut garder les surfaces lisibles cohérentes entre elles L'agent qui exploite cette construction sans navigateur est traité dans parité TLS de curl-impersonate et curl_cffi., et la poignée de main est produite en dessous par la pile TLS, donc un document cohérent et une poignée de main stable sont deux résultats distincts obtenus à deux endroits distincts. Prendre le premier comme preuve du second est l'erreur qui transforme deux résultats distincts en une conclusion fausse.
Sources
Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?
Articles Connexes
curl-impersonate et curl_cffi : les limites de parité TLS
EmpreinteRègles de couche réseau qu’une page ne peut pas évaluer
EmpreinteClient Hints Fingerprinting : en-têtes HTTP et identité
EmpreinteEmpreinte de fréquence d'images : affichage et vidéo
EmpreinteRapport Canvas de BrowserLeaks : signaux et limites
EmpreinteCollecteur CSS CreepJS : styles calculés
Exécutez la même vérification dans BotBrowser
Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.