release: version 1.1.15 - Fix ALPN detection for malformed TLS extensions
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled
- FIX: ALPN (tls_alpn) not appearing in logs for packets with truncated extensions - Add sanitizeTLSRecord fallback in extractTLSExtensions (tlsparse/parser.go) - Mirrors sanitization already present in fingerprint/engine.go - ALPN now correctly extracted even when ParseClientHello fails on raw payload - Bump version to 1.1.15 in main.go and packaging/rpm/ja4sentinel.spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -5,6 +5,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"ja4sentinel/api"
|
||||
|
||||
tlsfingerprint "github.com/psanford/tlsfingerprint"
|
||||
)
|
||||
|
||||
func TestFromClientHello(t *testing.T) {
|
||||
@ -253,3 +255,132 @@ func TestFromClientHello_EmptyJA4Hash(t *testing.T) {
|
||||
// JA4Hash may be empty if the JA4 format doesn't include underscores
|
||||
// This is acceptable behavior
|
||||
}
|
||||
|
||||
// buildClientHelloWithTruncatedExtension creates a ClientHello where the last
|
||||
// extension declares more data than actually present.
|
||||
func buildClientHelloWithTruncatedExtension() []byte {
|
||||
// Build a valid SNI extension first
|
||||
sniHostname := []byte("example.com")
|
||||
sniExt := []byte{
|
||||
0x00, 0x00, // Extension type: server_name
|
||||
}
|
||||
sniData := []byte{0x00}
|
||||
sniListLen := 1 + 2 + len(sniHostname) // type(1) + len(2) + hostname
|
||||
sniData = append(sniData, byte(sniListLen>>8), byte(sniListLen))
|
||||
sniData = append(sniData, 0x00) // hostname type
|
||||
sniData = append(sniData, byte(len(sniHostname)>>8), byte(len(sniHostname)))
|
||||
sniData = append(sniData, sniHostname...)
|
||||
sniExt = append(sniExt, byte(len(sniData)>>8), byte(len(sniData)))
|
||||
sniExt = append(sniExt, sniData...)
|
||||
|
||||
// Build a truncated extension: declares 100 bytes but only has 5
|
||||
truncatedExt := []byte{
|
||||
0x00, 0x15, // Extension type: padding
|
||||
0x00, 0x64, // Extension data length: 100 (but we only provide 5)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, // Only 5 bytes of padding
|
||||
}
|
||||
|
||||
// Extensions = valid SNI + truncated padding
|
||||
extensions := append(sniExt, truncatedExt...)
|
||||
// But the extensions length field claims the full size (including the bad extension)
|
||||
extLen := len(extensions)
|
||||
|
||||
// Cipher suites
|
||||
cipherSuites := []byte{0x00, 0x04, 0x13, 0x01, 0x13, 0x02, 0xc0, 0x2f}
|
||||
compressionMethods := []byte{0x01, 0x00}
|
||||
|
||||
handshakeBody := []byte{0x03, 0x03}
|
||||
for i := 0; i < 32; i++ {
|
||||
handshakeBody = append(handshakeBody, 0x01)
|
||||
}
|
||||
handshakeBody = append(handshakeBody, 0x00) // session ID length: 0
|
||||
handshakeBody = append(handshakeBody, byte(len(cipherSuites)>>8), byte(len(cipherSuites)))
|
||||
handshakeBody = append(handshakeBody, cipherSuites...)
|
||||
handshakeBody = append(handshakeBody, compressionMethods...)
|
||||
handshakeBody = append(handshakeBody, byte(extLen>>8), byte(extLen))
|
||||
handshakeBody = append(handshakeBody, extensions...)
|
||||
|
||||
handshakeLen := len(handshakeBody)
|
||||
handshake := append([]byte{
|
||||
0x01,
|
||||
byte(handshakeLen >> 16), byte(handshakeLen >> 8), byte(handshakeLen),
|
||||
}, handshakeBody...)
|
||||
|
||||
recordLen := len(handshake)
|
||||
record := make([]byte, 5+recordLen)
|
||||
record[0] = 0x16
|
||||
record[1] = 0x03
|
||||
record[2] = 0x03
|
||||
record[3] = byte(recordLen >> 8)
|
||||
record[4] = byte(recordLen)
|
||||
copy(record[5:], handshake)
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
func TestFromClientHello_TruncatedExtension_StillGeneratesFingerprint(t *testing.T) {
|
||||
payload := buildClientHelloWithTruncatedExtension()
|
||||
|
||||
ch := api.TLSClientHello{
|
||||
SrcIP: "4.251.36.192",
|
||||
SrcPort: 19346,
|
||||
DstIP: "212.95.72.88",
|
||||
DstPort: 443,
|
||||
Payload: payload,
|
||||
ConnID: "4.251.36.192:19346->212.95.72.88:443",
|
||||
}
|
||||
|
||||
engine := NewEngine()
|
||||
fp, err := engine.FromClientHello(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("FromClientHello() should succeed after sanitization, got error: %v", err)
|
||||
}
|
||||
if fp == nil {
|
||||
t.Fatal("FromClientHello() returned nil fingerprint")
|
||||
}
|
||||
if fp.JA4 == "" {
|
||||
t.Error("JA4 should be populated even with truncated extension")
|
||||
}
|
||||
if fp.JA3 == "" {
|
||||
t.Error("JA3 should be populated even with truncated extension")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeClientHelloExtensions(t *testing.T) {
|
||||
t.Run("valid payload returns nil", func(t *testing.T) {
|
||||
valid := buildMinimalClientHelloForTest()
|
||||
result := sanitizeClientHelloExtensions(valid)
|
||||
if result != nil {
|
||||
t.Error("should return nil for valid payload (no fix needed)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("truncated extension is fixed", func(t *testing.T) {
|
||||
truncated := buildClientHelloWithTruncatedExtension()
|
||||
result := sanitizeClientHelloExtensions(truncated)
|
||||
if result == nil {
|
||||
t.Fatal("should return sanitized payload")
|
||||
}
|
||||
// The sanitized payload should be parseable by the library
|
||||
fp, err := tlsfingerprint.ParseClientHello(result)
|
||||
if err != nil {
|
||||
t.Fatalf("sanitized payload should parse without error, got: %v", err)
|
||||
}
|
||||
if fp == nil {
|
||||
t.Fatal("sanitized payload should produce a fingerprint")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("too short returns nil", func(t *testing.T) {
|
||||
if sanitizeClientHelloExtensions([]byte{0x16}) != nil {
|
||||
t.Error("should return nil for short payload")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-TLS returns nil", func(t *testing.T) {
|
||||
if sanitizeClientHelloExtensions([]byte{0x15, 0x03, 0x03, 0x00, 0x01, 0x00}) != nil {
|
||||
t.Error("should return nil for non-TLS payload")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user