// Package parser fournit les parseurs TLS ClientHello et HTTP/2 // pour l'extraction des empreintes de fingerprinting réseau. package parser import ( "crypto/md5" "crypto/sha256" "encoding/binary" "encoding/hex" "fmt" "sort" "strings" ) // ClientHello représente les champs extraits d'un message TLS ClientHello. type ClientHello struct { RecordVersion uint16 // version du record TLS (ex: 0x0303) HandshakeVersion uint16 // version dans le handshake CipherSuites []uint16 // suites de chiffrement proposées CompressionMethods []uint8 // méthodes de compression Extensions []Extension // liste des extensions TLS SNI string // Server Name Indication (si présent) ALPN []string // protocoles ALPN annoncés SupportedGroups []uint16 // groupes Diffie-Hellman supportés ECPointFormats []uint8 // formats de points elliptiques SupportedVersions []uint16 // versions TLS annoncées (extension 0x002b) } // Extension représente une extension TLS avec son type et son contenu brut. type Extension struct { Type uint16 // identifiant de l'extension Data []byte // données brutes de l'extension } // ParseClientHello extrait les champs du ClientHello TLS depuis le payload brut. // Le payload doit commencer au record layer TLS (premier octet = 0x16). // Retourne une erreur si le payload est tronqué ou structurellement invalide. func ParseClientHello(payload []byte) (*ClientHello, error) { if len(payload) < 5 { return nil, fmt.Errorf("payload trop court pour le record TLS: %d octets", len(payload)) } // Vérifier le type de contenu : 0x16 = Handshake if payload[0] != 0x16 { return nil, fmt.Errorf("type de contenu TLS inattendu: 0x%02x (attendu 0x16)", payload[0]) } recordVersion := binary.BigEndian.Uint16(payload[1:3]) recordLength := int(binary.BigEndian.Uint16(payload[3:5])) // Le programme TC capture au maximum MAX_TLS_PAYLOAD (2048) octets. // Si la taille du record TLS dépasse les données disponibles, on travaille // avec ce qu'on a (le ClientHello est toujours en début de record). available := len(payload) - 5 if recordLength > available { recordLength = available } // Parsing du message Handshake hs := payload[5 : 5+recordLength] if len(hs) < 4 { return nil, fmt.Errorf("message Handshake trop court") } // Vérifier le type de message Handshake : 0x01 = ClientHello if hs[0] != 0x01 { return nil, fmt.Errorf("type de message Handshake inattendu: 0x%02x (attendu 0x01)", hs[0]) } // Longueur du ClientHello (3 octets big-endian) chLen := int(uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])) // Tolérance à la troncature TC : on travaille avec ce qu'on a if chLen > len(hs)-4 { chLen = len(hs) - 4 } ch := &ClientHello{RecordVersion: recordVersion} data := hs[4 : 4+chLen] // Version du handshake (2 octets) if len(data) < 2 { return nil, fmt.Errorf("ClientHello: version manquante") } ch.HandshakeVersion = binary.BigEndian.Uint16(data[0:2]) offset := 2 // Random (32 octets) if len(data) < offset+32 { return nil, fmt.Errorf("ClientHello: random manquant") } offset += 32 // Session ID (longueur 1 octet + données) if len(data) < offset+1 { return nil, fmt.Errorf("ClientHello: session ID manquant") } sessionIDLen := int(data[offset]) offset += 1 + sessionIDLen // Cipher Suites (longueur 2 octets + données) if len(data) < offset+2 { return nil, fmt.Errorf("ClientHello: longueur cipher suites manquante") } csLen := int(binary.BigEndian.Uint16(data[offset : offset+2])) offset += 2 if len(data) < offset+csLen { csLen = len(data) - offset // troncature tolérée } for i := 0; i+2 <= csLen; i += 2 { cs := binary.BigEndian.Uint16(data[offset+i : offset+i+2]) ch.CipherSuites = append(ch.CipherSuites, cs) } offset += csLen // Compression Methods (longueur 1 octet + données) if len(data) < offset+1 { return ch, nil // troncature : retourner ce qu'on a } compLen := int(data[offset]) offset++ if len(data) < offset+compLen { compLen = len(data) - offset } ch.CompressionMethods = data[offset : offset+compLen] offset += compLen // Extensions (optionnelles) if len(data) < offset+2 { return ch, nil // pas d'extensions } extTotalLen := int(binary.BigEndian.Uint16(data[offset : offset+2])) offset += 2 if len(data) < offset+extTotalLen { extTotalLen = len(data) - offset // troncature tolérée } // Parsing des extensions extData := data[offset : offset+extTotalLen] extOffset := 0 for extOffset+4 <= len(extData) { extType := binary.BigEndian.Uint16(extData[extOffset : extOffset+2]) extLen := int(binary.BigEndian.Uint16(extData[extOffset+2 : extOffset+4])) extOffset += 4 if extOffset+extLen > len(extData) { break } extPayload := extData[extOffset : extOffset+extLen] ch.Extensions = append(ch.Extensions, Extension{Type: extType, Data: extPayload}) // Décoder les extensions importantes switch extType { case 0x0000: // SNI ch.SNI = parseSNI(extPayload) case 0x0010: // ALPN ch.ALPN = parseALPN(extPayload) case 0x000a: // Supported Groups (elliptic_curves) ch.SupportedGroups = parseSupportedGroups(extPayload) case 0x000b: // EC Point Formats ch.ECPointFormats = parseECPointFormats(extPayload) case 0x002b: // Supported Versions ch.SupportedVersions = parseSupportedVersions(extPayload) } extOffset += extLen } return ch, nil } // parseSNI extrait le nom d'hôte depuis l'extension SNI (type 0x0000). func parseSNI(data []byte) string { // Structure : list_len(2) + type(1) + name_len(2) + name if len(data) < 5 { return "" } // Ignorer list_len et name_type, lire directement name_len nameLen := int(binary.BigEndian.Uint16(data[3:5])) if len(data) < 5+nameLen { return "" } return string(data[5 : 5+nameLen]) } // parseALPN extrait la liste des protocoles ALPN (extension 0x0010). func parseALPN(data []byte) []string { if len(data) < 2 { return nil } listLen := int(binary.BigEndian.Uint16(data[0:2])) offset := 2 var protocols []string for offset < 2+listLen && offset < len(data) { if offset+1 > len(data) { break } protoLen := int(data[offset]) offset++ if offset+protoLen > len(data) { break } protocols = append(protocols, string(data[offset:offset+protoLen])) offset += protoLen } return protocols } // parseSupportedGroups extrait les groupes Diffie-Hellman (extension 0x000a). func parseSupportedGroups(data []byte) []uint16 { if len(data) < 2 { return nil } listLen := int(binary.BigEndian.Uint16(data[0:2])) offset := 2 var groups []uint16 for i := 0; i < listLen/2 && offset+2 <= len(data); i++ { groups = append(groups, binary.BigEndian.Uint16(data[offset:offset+2])) offset += 2 } return groups } // parseECPointFormats extrait les formats de points elliptiques (extension 0x000b). func parseECPointFormats(data []byte) []uint8 { if len(data) < 1 { return nil } listLen := int(data[0]) if len(data) < 1+listLen { return nil } return data[1 : 1+listLen] } // parseSupportedVersions extrait les versions TLS supportées (extension 0x002b). func parseSupportedVersions(data []byte) []uint16 { if len(data) < 1 { return nil } listLen := int(data[0]) offset := 1 var versions []uint16 for i := 0; i < listLen/2 && offset+2 <= len(data); i++ { versions = append(versions, binary.BigEndian.Uint16(data[offset:offset+2])) offset += 2 } return versions } // IsGREASE vérifie si une valeur est une valeur GREASE (RFC 8701). // Les valeurs GREASE suivent le motif 0x?A?A (ex: 0x0A0A, 0x1A1A, ...). func IsGREASE(v uint16) bool { return v&0x0F0F == 0x0A0A && v>>8 == v&0xFF } // tlsVersionString convertit un code de version TLS en chaîne à 2 caractères JA4. func tlsVersionString(v uint16) string { switch v { case 0x0304: return "13" case 0x0303: return "12" case 0x0302: return "11" case 0x0301: return "10" default: return "00" } } // ComputeJA4 calcule l'empreinte JA4 selon la spécification FoxIO. // // Format: t{tls_ver}{sni}{cipher_count}{ext_count}_{sorted_ciphers_sha256[:12]}_{sorted_exts_alpn_sha256[:12]} func ComputeJA4(ch *ClientHello) string { // --- Protocole : toujours "t" (TCP) --- proto := "t" // --- Version TLS : version la plus haute annoncée --- var tlsVer uint16 for _, v := range ch.SupportedVersions { if !IsGREASE(v) && v > tlsVer { tlsVer = v } } if tlsVer == 0 { // Fallback : version du handshake tlsVer = ch.HandshakeVersion } verStr := tlsVersionString(tlsVer) // --- SNI : "d" si présent, "i" si absent --- sniFlag := "i" if ch.SNI != "" { sniFlag = "d" } // --- Comptage des cipher suites (sans GREASE) --- var ciphers []uint16 for _, cs := range ch.CipherSuites { if !IsGREASE(cs) { ciphers = append(ciphers, cs) } } cipherCount := fmt.Sprintf("%02d", len(ciphers)) // --- Comptage des extensions (sans GREASE, sans SNI 0x0000) --- var extensions []uint16 for _, ext := range ch.Extensions { if IsGREASE(ext.Type) { continue } if ext.Type == 0x0000 { // SNI exclue du comptage continue } extensions = append(extensions, ext.Type) } extCount := fmt.Sprintf("%02d", len(extensions)) // --- Partie 1 : identifiant de base --- part1 := proto + verStr + sniFlag + cipherCount + extCount // --- Partie 2 : SHA-256 des cipher suites triées (12 premiers hex chars) --- sortedCiphers := make([]uint16, len(ciphers)) copy(sortedCiphers, ciphers) sort.Slice(sortedCiphers, func(i, j int) bool { return sortedCiphers[i] < sortedCiphers[j] }) cipherStrings := make([]string, len(sortedCiphers)) for i, cs := range sortedCiphers { cipherStrings[i] = fmt.Sprintf("%04x", cs) } cipherRaw := strings.Join(cipherStrings, ",") cipherHash := sha256.Sum256([]byte(cipherRaw)) part2 := hex.EncodeToString(cipherHash[:])[:12] // --- Partie 3 : SHA-256 des extensions triées + ALPN (12 premiers hex chars) --- sortedExts := make([]uint16, len(extensions)) copy(sortedExts, extensions) sort.Slice(sortedExts, func(i, j int) bool { return sortedExts[i] < sortedExts[j] }) extStrings := make([]string, len(sortedExts)) for i, e := range sortedExts { extStrings[i] = fmt.Sprintf("%04x", e) } extRaw := strings.Join(extStrings, ",") // Premier protocole ALPN (ou "00" si absent) alpnFirst := "00" if len(ch.ALPN) > 0 { alpnFirst = ch.ALPN[0] } extAlpnRaw := extRaw + "_" + alpnFirst extHash := sha256.Sum256([]byte(extAlpnRaw)) part3 := hex.EncodeToString(extHash[:])[:12] return part1 + "_" + part2 + "_" + part3 } // ComputeJA3 calcule l'empreinte JA3 selon la spécification Salesforce. // // Format : TLSVersion,CipherSuites,Extensions,EllipticCurves,EllipticPointFormats // Chaque segment est une liste de valeurs décimales séparées par des tirets. // Le JA3 hash est le MD5 hex de cette chaîne. func ComputeJA3(ch *ClientHello) (ja3Raw string, ja3Hash string) { // --- Version TLS --- var tlsVer uint16 for _, v := range ch.SupportedVersions { if !IsGREASE(v) && v > tlsVer { tlsVer = v } } if tlsVer == 0 { tlsVer = ch.HandshakeVersion } // --- Cipher suites (sans GREASE) --- var ciphers []string for _, cs := range ch.CipherSuites { if !IsGREASE(cs) { ciphers = append(ciphers, fmt.Sprintf("%d", cs)) } } // --- Extensions (sans GREASE, sans SNI 0x0000) --- var exts []string for _, e := range ch.Extensions { if IsGREASE(e.Type) { continue } // SNI (0x0000) est inclus dans JA3 exts = append(exts, fmt.Sprintf("%d", e.Type)) } // --- Groupes elliptiques (extension supported_groups 0x000a) --- var groups []string for _, e := range ch.Extensions { if e.Type == 0x000a && len(e.Data) >= 4 { // Format : longueur (2 octets) + liste de groupes (2 octets chacun) groupLen := int(binary.BigEndian.Uint16(e.Data[:2])) for i := 2; i+1 < len(e.Data) && i < groupLen+2; i += 2 { g := binary.BigEndian.Uint16(e.Data[i : i+2]) if !IsGREASE(g) { groups = append(groups, fmt.Sprintf("%d", g)) } } } } // --- Formats de points elliptiques (extension ec_point_formats 0x000b) --- var ecPointFormats []string for _, e := range ch.Extensions { if e.Type == 0x000b && len(e.Data) >= 2 { fmtLen := int(e.Data[0]) for i := 1; i < len(e.Data) && i <= fmtLen; i++ { ecPointFormats = append(ecPointFormats, fmt.Sprintf("%d", e.Data[i])) } } } // --- Assemblage JA3 raw --- parts := []string{ fmt.Sprintf("%d", tlsVer), strings.Join(ciphers, "-"), strings.Join(exts, "-"), strings.Join(groups, "-"), strings.Join(ecPointFormats, "-"), } ja3Raw = strings.Join(parts, ",") // --- JA3 hash = MD5 du raw --- md5Hash := md5.Sum([]byte(ja3Raw)) ja3Hash = hex.EncodeToString(md5Hash[:]) return ja3Raw, ja3Hash }