package parser_test import ( "encoding/hex" "testing" "github.com/antitbone/ja4/ja4ebpf/internal/parser" ) func TestParseClientHelloMinimal(t *testing.T) { // Construit un ClientHello minimal valide manuellement // pour tester le parsing sans dépendre d'un vrai paquet capturé. raw := buildMinimalClientHello() ch, err := parser.ParseClientHello(raw) if err != nil { t.Fatalf("ParseClientHello a échoué: %v", err) } if ch.HandshakeVersion != 0x0303 { t.Errorf("version attendue 0x0303, obtenue 0x%04x", ch.HandshakeVersion) } if len(ch.CipherSuites) == 0 { t.Error("aucune cipher suite extraite") } } func TestIsGREASEFiltering(t *testing.T) { // Les valeurs GREASE doivent être filtrées dans ComputeJA4 raw := buildClientHelloWithGREASE() ch, err := parser.ParseClientHello(raw) if err != nil { t.Fatalf("ParseClientHello: %v", err) } ja4 := parser.ComputeJA4(ch) if ja4 == "" { t.Error("ComputeJA4 a retourné une chaîne vide") } t.Logf("JA4 = %s", ja4) } func TestComputeJA4Format(t *testing.T) { raw := buildMinimalClientHello() ch, err := parser.ParseClientHello(raw) if err != nil { t.Fatalf("ParseClientHello: %v", err) } ja4 := parser.ComputeJA4(ch) // Format attendu : t{ver}{sni}{cc}{ec}_{12hex}_{12hex} // 5 chars + "_" + 12 chars + "_" + 12 chars = 31 chars minimum if len(ja4) < 31 { t.Errorf("longueur JA4 inattendue: %d chars — valeur: %q", len(ja4), ja4) } // Le premier caractère doit être 't' (TCP) if ja4[0] != 't' { t.Errorf("JA4 doit commencer par 't', obtenu %q", string(ja4[0])) } } func TestSNIExtraction(t *testing.T) { raw := buildClientHelloWithSNI("example.com") ch, err := parser.ParseClientHello(raw) if err != nil { t.Fatalf("ParseClientHello: %v", err) } if ch.SNI != "example.com" { t.Errorf("SNI attendu %q, obtenu %q", "example.com", ch.SNI) } ja4 := parser.ComputeJA4(ch) // SNI présent → flag 'd' en position 3 if len(ja4) >= 3 && ja4[3] != 'd' { t.Errorf("flag SNI attendu 'd', obtenu %q dans JA4=%q", string(ja4[3]), ja4) } } func TestParseClientHelloTooShort(t *testing.T) { _, err := parser.ParseClientHello([]byte{0x16, 0x03, 0x01}) if err == nil { t.Error("attendu une erreur pour un payload trop court") } } func TestParseClientHelloWrongType(t *testing.T) { // Type 0x17 = Application Data, pas un Handshake raw := make([]byte, 20) raw[0] = 0x17 _, err := parser.ParseClientHello(raw) if err == nil { t.Error("attendu une erreur pour un Record Type incorrect") } } // ── Helpers de construction de paquets TLS de test ──────────────────────── // buildMinimalClientHello construit un ClientHello TLS 1.2 minimal valide. func buildMinimalClientHello() []byte { // Contenu du Handshake (sans l'en-tête Record Layer) var hs []byte // Version ClientHello : TLS 1.2 hs = append(hs, 0x03, 0x03) // Random : 32 octets random := make([]byte, 32) hs = append(hs, random...) // Session ID : longueur 0 hs = append(hs, 0x00) // Cipher Suites : 2 suites (TLS_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA) hs = append(hs, 0x00, 0x04) // longueur = 4 octets hs = append(hs, 0x13, 0x01) // TLS_AES_128_GCM_SHA256 hs = append(hs, 0x00, 0x2f) // TLS_RSA_WITH_AES_128_CBC_SHA // Compression Methods : 1 méthode (null) hs = append(hs, 0x01, 0x00) // Pas d'extensions hs = append(hs, 0x00, 0x00) return buildTLSRecord(hs) } // buildClientHelloWithGREASE ajoute des valeurs GREASE aux cipher suites. func buildClientHelloWithGREASE() []byte { var hs []byte hs = append(hs, 0x03, 0x03) // version hs = append(hs, make([]byte, 32)...) // random hs = append(hs, 0x00) // session id len // Cipher suites avec GREASE (0x0a0a) hs = append(hs, 0x00, 0x06) // longueur = 6 hs = append(hs, 0x0a, 0x0a) // GREASE (doit être filtré) hs = append(hs, 0x13, 0x01) // TLS_AES_128_GCM_SHA256 hs = append(hs, 0x00, 0x2f) // TLS_RSA_WITH_AES_128_CBC_SHA hs = append(hs, 0x01, 0x00) // compression hs = append(hs, 0x00, 0x00) // no extensions return buildTLSRecord(hs) } // buildClientHelloWithSNI construit un ClientHello avec l'extension SNI. func buildClientHelloWithSNI(hostname string) []byte { var hs []byte hs = append(hs, 0x03, 0x03) hs = append(hs, make([]byte, 32)...) hs = append(hs, 0x00) hs = append(hs, 0x00, 0x04) hs = append(hs, 0x13, 0x01, 0x00, 0x2f) hs = append(hs, 0x01, 0x00) // Extension SNI nameBytes := []byte(hostname) sniExt := buildSNIExtension(nameBytes) // Bloc d'extensions extBlock := sniExt hs = appendUint16(hs, uint16(len(extBlock))) hs = append(hs, extBlock...) return buildTLSRecord(hs) } func buildSNIExtension(hostname []byte) []byte { // Type : 0x0000 (SNI) // Longueur extension = 2 (liste len) + 1 (type) + 2 (name len) + len(hostname) nameLen := len(hostname) listLen := 1 + 2 + nameLen var ext []byte ext = append(ext, 0x00, 0x00) // type SNI ext = appendUint16(ext, uint16(2+listLen)) // longueur extension ext = appendUint16(ext, uint16(listLen)) // longueur liste SNI ext = append(ext, 0x00) // type : host_name ext = appendUint16(ext, uint16(nameLen)) // longueur hostname ext = append(ext, hostname...) return ext } // buildTLSRecord encapsule un message Handshake dans un Record Layer TLS. func buildTLSRecord(handshakeBody []byte) []byte { hsLen := len(handshakeBody) var rec []byte // Record Layer rec = append(rec, 0x16) // ContentType : Handshake rec = append(rec, 0x03, 0x01) // Version : TLS 1.0 rec = appendUint16(rec, uint16(4+hsLen)) // longueur = type(1) + len(3) + body // En-tête Handshake rec = append(rec, 0x01) // HandshakeType : ClientHello // Longueur sur 3 octets rec = append(rec, byte(hsLen>>16), byte(hsLen>>8), byte(hsLen)) rec = append(rec, handshakeBody...) return rec } func appendUint16(b []byte, v uint16) []byte { return append(b, byte(v>>8), byte(v)) } // TestParseClientHelloHex teste le parsing d'un hex dump (smoke test). func TestParseClientHelloHexDump(t *testing.T) { // Hex dump minimal connu valide hexStr := "160301002f" + // Record : type=22, ver=0x0301, len=47 "01000002b" + // Handshake : type=1, len=43 (ajusté) "0303" + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + "00" + // session id len = 0 "0004" + "13010035" + // cipher suites : 2 suites "0100" + // compression "0000" // no extensions raw, err := hex.DecodeString( "1603010032" + // Record Layer : type=22, len=50 "01" + "00002e" + // HandshakeType=1, len=46 "0303" + // version "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + // random "00" + // session id len "0004" + "13010035" + // 2 cipher suites "01" + "00" + // compression "0000", // extensions ) if err != nil { t.Skipf("hex decode: %v", err) } ch, err := parser.ParseClientHello(raw) if err != nil { // Acceptable : le hex dump est peut-être mal formé (longueurs) t.Logf("ParseClientHello (hex dump): %v", err) return } t.Logf("ClientHello parsé : version=0x%04x, ciphers=%d", ch.HandshakeVersion, len(ch.CipherSuites)) _ = hexStr }