package internal import ( "reflect" "testing" "git.difuse.io/Difuse/Mellaris/analyzer/utils" ) func buildClientHelloMsg(t *testing.T) *utils.ByteBuffer { t.Helper() // Bytes taken from the standard TLS test vector, starting after the record // header (5 bytes) and handshake header (4 bytes). body := []byte{ 0x03, 0x03, // version TLS 1.2 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // random 0x00, // session ID length = 0 0x00, 0x20, // cipher suites length = 32 bytes = 16 suites 0xcc, 0xa8, 0xcc, 0xa9, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13, 0xc0, 0x09, 0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a, // ciphers 0x01, // compression methods length = 1 0x00, // compression method = null 0x00, 0x58, // extensions length = 88 // extension: server_name 0x00, 0x00, // type = server_name 0x00, 0x18, // length = 24 0x00, 0x16, // server name list length = 22 0x00, // name type = hostname 0x00, 0x13, // name length = 19 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'u', 'l', 'f', 'h', 'e', 'i', 'm', '.', 'n', 'e', 't', // extension: status_request 0x00, 0x05, // type = status_request 0x00, 0x05, // length = 5 0x01, 0x00, 0x00, 0x00, 0x00, // extension: supported_groups 0x00, 0x0a, // type = supported_groups 0x00, 0x0a, // length = 10 0x00, 0x08, // list length = 8 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, // extension: ec_point_formats 0x00, 0x0b, // type = ec_point_formats 0x00, 0x02, // length = 2 0x01, 0x00, // extension: signature_algorithms 0x00, 0x0d, // type = signature_algorithms 0x00, 0x12, // length = 18 0x00, 0x10, // list length = 16 0x04, 0x01, 0x04, 0x03, 0x05, 0x01, 0x05, 0x03, 0x06, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02, 0x03, // extension: renegotiation_info 0xff, 0x01, // type = renegotiation_info 0x00, 0x01, // length = 1 0x00, // extension: extended_master_secret (empty) 0x00, 0x17, // type = extended_master_secret 0x00, 0x00, // length = 0 // extension: session_ticket (empty) 0x00, 0x23, // type = session_ticket 0x00, 0x00, // length = 0 } return &utils.ByteBuffer{Buf: body} } func TestParseTLSClientHelloMsgData(t *testing.T) { chBuf := buildClientHelloMsg(t) m := ParseTLSClientHelloMsgData(chBuf) if m == nil { t.Fatal("ParseTLSClientHelloMsgData returned nil") } wantVersion := uint16(0x0303) if v, ok := m["version"].(uint16); !ok || v != wantVersion { t.Errorf("version = %v, want %v", m["version"], wantVersion) } wantCiphers := []uint16{52392, 52393, 49199, 49200, 49195, 49196, 49171, 49161, 49172, 49162, 156, 157, 47, 53, 49170, 10} if c, ok := m["ciphers"].([]uint16); ok { if !reflect.DeepEqual(c, wantCiphers) { t.Errorf("ciphers = %v, want %v", c, wantCiphers) } } else { t.Errorf("ciphers missing or wrong type: %T", m["ciphers"]) } if sni, ok := m["sni"].(string); !ok || sni != "example.ulfheim.net" { t.Errorf("sni = %q, want %q", m["sni"], "example.ulfheim.net") } if _, ok := m["compression"]; !ok { t.Error("compression key missing") } if _, ok := m["session"]; !ok { t.Error("session key missing") } if _, ok := m["random"]; !ok { t.Error("random key missing") } } func TestParseTLSServerHelloMsgData(t *testing.T) { // ServerHello message body (after record header + handshake header) body := []byte{ 0x03, 0x03, // version TLS 1.2 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, // random 0x00, // session ID length = 0 0xc0, 0x13, // cipher suite 0x00, // compression method 0x00, 0x05, // extensions length = 5 // extension: renegotiation_info 0xff, 0x01, // type 0x00, 0x01, // length = 1 0x00, } m := ParseTLSServerHelloMsgData(&utils.ByteBuffer{Buf: body}) if m == nil { t.Fatal("ParseTLSServerHelloMsgData returned nil") } wantCipher := uint16(0xc013) if c, ok := m["cipher"].(uint16); !ok || c != wantCipher { t.Errorf("cipher = %v, want %v", m["cipher"], wantCipher) } wantCompression := uint8(0) if c, ok := m["compression"].(uint8); !ok || c != wantCompression { t.Errorf("compression = %v, want %v", m["compression"], wantCompression) } } func TestParseTLSServerHelloMsgData_NoExtensions(t *testing.T) { // ServerHello message without extensions body := []byte{ 0x03, 0x03, // version 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // random 0x00, // session ID length 0x00, 0xff, // cipher suite 0x00, // compression method } m := ParseTLSServerHelloMsgData(&utils.ByteBuffer{Buf: body}) if m == nil { t.Fatal("ParseTLSServerHelloMsgData returned nil for no-extensions case") } if _, ok := m["cipher"]; !ok { t.Error("cipher key missing") } } func TestParseTLSClientHelloMsgData_Truncated(t *testing.T) { tests := []struct { name string buf []byte }{ {"too short for session id", []byte{0x03, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f}}, {"odd cipher suites length", []byte{ 0x03, 0x03, // version 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // random 0x00, // session ID length 0x00, 0x03, // odd cipher suites length }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := ParseTLSClientHelloMsgData(&utils.ByteBuffer{Buf: tt.buf}) if m != nil { t.Error("expected nil for truncated input") } }) } } func TestParseTLSClientHelloMsgData_ECH(t *testing.T) { // ClientHello with ECH extension body := []byte{ 0x03, 0x03, // version 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // random 0x00, // session ID length 0x00, 0x02, // cipher suites length = 2 0x13, 0x01, // TLS_AES_128_GCM_SHA256 0x01, // compression methods length = 1 0x00, // null compression 0x00, 0x07, // extensions length = 7 // ECH extension 0xfe, 0x0d, // type = encrypted_client_hello 0x00, 0x03, // length = 3 0x01, 0x02, 0x03, // some ECH data } m := ParseTLSClientHelloMsgData(&utils.ByteBuffer{Buf: body}) if m == nil { t.Fatal("ParseTLSClientHelloMsgData returned nil") } ech, ok := m["ech"].(bool) if !ok || !ech { t.Errorf("ech = %v, want true", m["ech"]) } } func TestParseTLSClientHelloMsgData_ALPN(t *testing.T) { // ClientHello with ALPN extension body := []byte{ 0x03, 0x03, // version 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // random 0x00, // session ID length 0x00, 0x02, // cipher suites length = 2 0x13, 0x01, // cipher suite 0x01, // compression methods length = 1 0x00, // compression method 0x00, 0x12, // extensions length = 18 // ALPN extension 0x00, 0x10, // type = ALPN 0x00, 0x0e, // length = 14 0x00, 0x0c, // list length = 12 0x08, 'h', 't', 't', 'p', '/', '1', '.', '1', 0x02, 'h', '2', } m := ParseTLSClientHelloMsgData(&utils.ByteBuffer{Buf: body}) if m == nil { t.Fatal("ParseTLSClientHelloMsgData returned nil") } alpn, ok := m["alpn"].([]string) if !ok { t.Fatalf("alpn missing or wrong type: %T", m["alpn"]) } if len(alpn) != 2 || alpn[0] != "http/1.1" || alpn[1] != "h2" { t.Errorf("alpn = %v, want [http/1.1 h2]", alpn) } } func TestParseTLSClientHelloMsgData_SupportedVersionsClient(t *testing.T) { // ClientHello with supported_versions extension (client format - list) body := []byte{ 0x03, 0x03, // version 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // random 0x00, // session ID length 0x00, 0x02, // cipher suites length = 2 0x13, 0x01, // cipher suite 0x01, // compression methods length = 1 0x00, // compression method 0x00, 0x0b, // extensions length = 11 // supported_versions (client list format) 0x00, 0x2b, // type = supported_versions 0x00, 0x07, // length = 7 0x06, // list length = 6 0x03, 0x04, // TLS 1.3 0x03, 0x03, // TLS 1.2 0x03, 0x01, // TLS 1.0 } m := ParseTLSClientHelloMsgData(&utils.ByteBuffer{Buf: body}) if m == nil { t.Fatal("ParseTLSClientHelloMsgData returned nil") } versions, ok := m["supported_versions"].([]uint16) if !ok { t.Fatalf("supported_versions missing or wrong type: %T", m["supported_versions"]) } want := []uint16{0x0304, 0x0303, 0x0301} if !reflect.DeepEqual(versions, want) { t.Errorf("supported_versions = %v, want %v", versions, want) } } func TestParseTLSServerHelloMsgData_SupportedVersionsServer(t *testing.T) { // ServerHello with supported_versions extension (server format - single value) body := []byte{ 0x03, 0x03, // version 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, // random 0x00, // session ID length 0x13, 0x01, // cipher suite 0x00, // compression method 0x00, 0x06, // extensions length = 6 // supported_versions (server format - single value) 0x00, 0x2b, // type 0x00, 0x02, // length = 2 0x03, 0x04, // TLS 1.3 } m := ParseTLSServerHelloMsgData(&utils.ByteBuffer{Buf: body}) if m == nil { t.Fatal("ParseTLSServerHelloMsgData returned nil") } v, ok := m["supported_versions"].(uint16) if !ok { t.Fatalf("supported_versions missing or wrong type: %T", m["supported_versions"]) } if v != 0x0304 { t.Errorf("supported_versions = 0x%04x, want 0x0304", v) } }