diff --git a/README.md b/README.md index f5c1e8e..86539a3 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,11 @@ # sensor-d4-tls-fingerprinting Extracts TLS certificates from pcap files or network interfaces (tcpreassembly is done thanks to gopacket), fingerprints TLS client/server interactions with ja3/ja3s and print output in JSON form. # Use -This project is currently in its very early stage and relies on a customized version of ![gopacket](http://github.com/google/gopacket "gopacket link"). -Check the list of issues. +This project is currently in its very early stage and should not be used in production. + Check the list of issues. ## Install dependencies & go get ``` shell -$go get github.com/gallypette/gopacket $go get github.com/google/gopacket -$cd $GOPATH/src/github.com/google/gopacket -$git remote add fork github.com/gallypette/gopacket $go get github.com/D4-project/sensor-d4-tls-fingerprinting ``` make allows to compile for amd64 and arm ATM. diff --git a/d4-tlsf.go b/d4-tlsf.go index 9a659f5..7d2c9f2 100644 --- a/d4-tlsf.go +++ b/d4-tlsf.go @@ -24,14 +24,13 @@ import ( "github.com/google/gopacket" "github.com/google/gopacket/examples/util" "github.com/google/gopacket/ip4defrag" - - // ATM add a fork to gopacket like this to pull my TLS code: - // $go get github.com/gallypette/gopacket - // $cd $GOPATH/src/github.com/google/gopacket - // $git remote add fork github.com/gallypette/gopacket "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" "github.com/google/gopacket/reassembly" + + // My Extended TLS layer + + "github.com/D4-project/sensor-d4-tls-fingerprinting/etls" ) var nodefrag = flag.Bool("nodefrag", false, "If true, do not do IPv4 defrag") @@ -47,6 +46,9 @@ var quiet = flag.Bool("quiet", false, "Be quiet regarding errors") var iface = flag.String("i", "eth0", "Interface to read packets from") var fname = flag.String("r", "", "Filename to read from, overrides -i") +// decoding +//var LayerTypeETLS gopacket.LayerType + // writing var outCerts = flag.String("w", "", "Folder to write certificates into") var outJSON = flag.String("j", "", "Folder to write certificates into, stdin if not set") @@ -68,7 +70,7 @@ type SessionRecord struct { type TLSSession struct { record SessionRecord - tlsHdskR layers.TLSHandshakeRecord + tlsHdskR etls.ETLSHandshakeRecord } var grease = map[uint16]bool{ @@ -231,25 +233,25 @@ func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass if length > 0 { // We can't rely on TLS length field has there can be several successive Record Layers // We attempt to decode, and if it fails, we keep the slice for later. - // Now we attempts TLS decoding - tls := &layers.TLS{} + // Now we attempts Extended TLS decoding + tls := &etls.ETLS{} var decoded []gopacket.LayerType + p := gopacket.NewDecodingLayerParser(etls.LayerTypeETLS, tls) + p.DecodingLayerParserOptions.IgnoreUnsupported = true // First we check if the packet is fragmented - p := gopacket.NewDecodingLayerParser(layers.LayerTypeTLS, tls) err := p.DecodeLayers(data, &decoded) if err != nil { - // Error("TLS-parser", "Failed to decode TLS: %v\n", err) - // Debug("RAW %s\n", hex.Dump(data)) + // If it's malformed as it we keep for next round sg.KeepFrom(0) } else { //Debug("TLS: %s\n", gopacket.LayerDump(tls)) // Debug("TLS: %s\n", gopacket.LayerGoString(tls)) if tls.Handshake != nil { for _, tlsrecord := range tls.Handshake { - switch tlsrecord.TLSHandshakeMsgType { + switch tlsrecord.ETLSHandshakeMsgType { // Client Hello case 1: - t.tlsSession.tlsHdskR.TLSHandshakeClientHello = tlsrecord.TLSHandshakeClientHello + t.tlsSession.tlsHdskR.ETLSHandshakeClientHello = tlsrecord.ETLSHandshakeClientHello t.tlsSession.record.ClientIP, t.tlsSession.record.ServerIP, t.tlsSession.record.ClientPort, t.tlsSession.record.ServerPort = getIPPorts(t) // Set up first seen info := sg.CaptureInfo(0) @@ -257,12 +259,12 @@ func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass t.tlsSession.gatherJa3() // Server Hello case 2: - t.tlsSession.tlsHdskR.TLSHandshakeServerHello = tlsrecord.TLSHandshakeServerHello + t.tlsSession.tlsHdskR.ETLSHandshakeServerHello = tlsrecord.ETLSHandshakeServerHello t.tlsSession.gatherJa3s() // Server Certificate case 11: //certs := make([]*x509.Certificate, len(tlsrecord.TLSHandshakeCertificate.Certificates)) - for _, asn1Data := range tlsrecord.TLSHandshakeCertificate.Certificates { + for _, asn1Data := range tlsrecord.ETLSHandshakeCertificate.Certificates { cert, err := x509.ParseCertificate(asn1Data) if err != nil { panic("tls: failed to parse certificate from server: " + err.Error()) @@ -294,21 +296,21 @@ func getIPPorts(t *tcpStream) (string, string, string, string) { func (ts *TLSSession) gatherJa3s() bool { var buf []byte - buf = strconv.AppendInt(buf, int64(ts.tlsHdskR.TLSHandshakeServerHello.Vers), 10) + buf = strconv.AppendInt(buf, int64(ts.tlsHdskR.ETLSHandshakeServerHello.Vers), 10) // byte (44) is "," buf = append(buf, byte(44)) // If there are Cipher Suites - buf = strconv.AppendInt(buf, int64(ts.tlsHdskR.TLSHandshakeServerHello.CipherSuite), 10) + buf = strconv.AppendInt(buf, int64(ts.tlsHdskR.ETLSHandshakeServerHello.CipherSuite), 10) buf = append(buf, byte(44)) // If there are extensions - if len(ts.tlsHdskR.TLSHandshakeServerHello.AllExtensions) > 0 { - for i, e := range ts.tlsHdskR.TLSHandshakeServerHello.AllExtensions { + if len(ts.tlsHdskR.ETLSHandshakeServerHello.AllExtensions) > 0 { + for i, e := range ts.tlsHdskR.ETLSHandshakeServerHello.AllExtensions { // TODO check this grease thingy if grease[uint16(e)] == false { buf = strconv.AppendInt(buf, int64(e), 10) - if (i + 1) < len(ts.tlsHdskR.TLSHandshakeServerHello.AllExtensions) { + if (i + 1) < len(ts.tlsHdskR.ETLSHandshakeServerHello.AllExtensions) { // byte(45) is "-" buf = append(buf, byte(45)) } @@ -325,16 +327,16 @@ func (ts *TLSSession) gatherJa3s() bool { func (ts *TLSSession) gatherJa3() bool { var buf []byte - buf = strconv.AppendInt(buf, int64(ts.tlsHdskR.TLSHandshakeClientHello.Vers), 10) + buf = strconv.AppendInt(buf, int64(ts.tlsHdskR.ETLSHandshakeClientHello.Vers), 10) // byte (44) is "," buf = append(buf, byte(44)) // If there are Cipher Suites - if len(ts.tlsHdskR.TLSHandshakeClientHello.CipherSuites) > 0 { - for i, cs := range ts.tlsHdskR.TLSHandshakeClientHello.CipherSuites { + if len(ts.tlsHdskR.ETLSHandshakeClientHello.CipherSuites) > 0 { + for i, cs := range ts.tlsHdskR.ETLSHandshakeClientHello.CipherSuites { buf = strconv.AppendInt(buf, int64(cs), 10) // byte(45) is "-" - if (i + 1) < len(ts.tlsHdskR.TLSHandshakeClientHello.CipherSuites) { + if (i + 1) < len(ts.tlsHdskR.ETLSHandshakeClientHello.CipherSuites) { buf = append(buf, byte(45)) } } @@ -342,12 +344,12 @@ func (ts *TLSSession) gatherJa3() bool { buf = append(buf, byte(44)) // If there are extensions - if len(ts.tlsHdskR.TLSHandshakeClientHello.AllExtensions) > 0 { - for i, e := range ts.tlsHdskR.TLSHandshakeClientHello.AllExtensions { + if len(ts.tlsHdskR.ETLSHandshakeClientHello.AllExtensions) > 0 { + for i, e := range ts.tlsHdskR.ETLSHandshakeClientHello.AllExtensions { // TODO check this grease thingy if grease[uint16(e)] == false { buf = strconv.AppendInt(buf, int64(e), 10) - if (i + 1) < len(ts.tlsHdskR.TLSHandshakeClientHello.AllExtensions) { + if (i + 1) < len(ts.tlsHdskR.ETLSHandshakeClientHello.AllExtensions) { buf = append(buf, byte(45)) } } @@ -356,10 +358,10 @@ func (ts *TLSSession) gatherJa3() bool { buf = append(buf, byte(44)) // If there are Supported Curves - if len(ts.tlsHdskR.TLSHandshakeClientHello.SupportedCurves) > 0 { - for i, cs := range ts.tlsHdskR.TLSHandshakeClientHello.SupportedCurves { + if len(ts.tlsHdskR.ETLSHandshakeClientHello.SupportedCurves) > 0 { + for i, cs := range ts.tlsHdskR.ETLSHandshakeClientHello.SupportedCurves { buf = strconv.AppendInt(buf, int64(cs), 10) - if (i + 1) < len(ts.tlsHdskR.TLSHandshakeClientHello.SupportedCurves) { + if (i + 1) < len(ts.tlsHdskR.ETLSHandshakeClientHello.SupportedCurves) { buf = append(buf, byte(45)) } } @@ -367,10 +369,10 @@ func (ts *TLSSession) gatherJa3() bool { buf = append(buf, byte(44)) // If there are Supported Points - if len(ts.tlsHdskR.TLSHandshakeClientHello.SupportedPoints) > 0 { - for i, cs := range ts.tlsHdskR.TLSHandshakeClientHello.SupportedPoints { + if len(ts.tlsHdskR.ETLSHandshakeClientHello.SupportedPoints) > 0 { + for i, cs := range ts.tlsHdskR.ETLSHandshakeClientHello.SupportedPoints { buf = strconv.AppendInt(buf, int64(cs), 10) - if (i + 1) < len(ts.tlsHdskR.TLSHandshakeClientHello.SupportedPoints) { + if (i + 1) < len(ts.tlsHdskR.ETLSHandshakeClientHello.SupportedPoints) { buf = append(buf, byte(45)) } } diff --git a/etls/etls.go b/etls/etls.go new file mode 100644 index 0000000..d389f0b --- /dev/null +++ b/etls/etls.go @@ -0,0 +1,215 @@ +// Copyright 2018 The GoPacket Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package etls + +import ( + "encoding/binary" + "errors" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" +) + +var LayerTypeETLS gopacket.LayerType + +// ETLSType defines the type of data after the ETLS Record +type ETLSType uint8 + +// ETLSType known values. +const ( + ETLSChangeCipherSpec ETLSType = 20 + ETLSAlert ETLSType = 21 + ETLSHandshake ETLSType = 22 + ETLSApplicationData ETLSType = 23 + ETLSUnknown ETLSType = 255 +) + +// String shows the register type nicely formatted +func (tt ETLSType) String() string { + switch tt { + default: + return "Unknown" + case ETLSChangeCipherSpec: + return "Change Cipher Spec" + case ETLSAlert: + return "Alert" + case ETLSHandshake: + return "Handshake" + case ETLSApplicationData: + return "Application Data" + } +} + +// ETLSVersion represents the ETLS version in numeric format +type ETLSVersion uint16 + +// Strings shows the ETLS version nicely formatted +func (tv ETLSVersion) String() string { + switch tv { + default: + return "Unknown" + case 0x0200: + return "SSL 2.0" + case 0x0300: + return "SSL 3.0" + case 0x0301: + return "TLS 1.0" + case 0x0302: + return "TLS 1.1" + case 0x0303: + return "TLS 1.2" + case 0x0304: + return "TLS 1.3" + } +} + +// ETLS is specified in RFC 5246 +// +// ETLS Record Protocol +// 0 1 2 3 4 5 6 7 8 +// +--+--+--+--+--+--+--+--+ +// | Content Type | +// +--+--+--+--+--+--+--+--+ +// | Version (major) | +// +--+--+--+--+--+--+--+--+ +// | Version (minor) | +// +--+--+--+--+--+--+--+--+ +// | Length | +// +--+--+--+--+--+--+--+--+ +// | Length | +// +--+--+--+--+--+--+--+--+ + +// ETLS is actually a slide of ETLSrecord structures +type ETLS struct { + layers.BaseLayer + + // ETLS Records + ChangeCipherSpec []ETLSChangeCipherSpecRecord + Handshake []ETLSHandshakeRecord + AppData []ETLSAppDataRecord + Alert []ETLSAlertRecord +} + +// ETLSRecordHeader contains all the information that each ETLS Record types should have +type ETLSRecordHeader struct { + ContentType ETLSType + Version ETLSVersion + Length uint16 +} + +// LayerType returns gopacket.LayerTypeETLS. +func (t *ETLS) LayerType() gopacket.LayerType { return LayerTypeETLS } + +// decodeETLS decodes the byte slice into a ETLS type. It also +// setups the application Layer in PacketBuilder. +func decodeETLS(data []byte, p gopacket.PacketBuilder) error { + t := &ETLS{} + err := t.DecodeFromBytes(data, p) + if err != nil { + return err + } + p.AddLayer(t) + p.SetApplicationLayer(t) + return nil +} + +// DecodeFromBytes decodes the slice into the ETLS struct. +func (t *ETLS) DecodeFromBytes(data []byte, df gopacket.DecodeFeedback) error { + t.BaseLayer.Contents = data + t.BaseLayer.Payload = nil + + t.ChangeCipherSpec = t.ChangeCipherSpec[:0] + t.Handshake = t.Handshake[:0] + t.AppData = t.AppData[:0] + t.Alert = t.Alert[:0] + + return t.decodeETLSRecords(data, df) +} + +func (t *ETLS) decodeETLSRecords(data []byte, df gopacket.DecodeFeedback) error { + if len(data) < 5 { + df.SetTruncated() + return errors.New("ETLS record too short") + } + + // since there are no further layers, the baselayer's content is + // pointing to this layer + t.BaseLayer = layers.BaseLayer{Contents: data[:len(data)]} + + var h ETLSRecordHeader + h.ContentType = ETLSType(data[0]) + h.Version = ETLSVersion(binary.BigEndian.Uint16(data[1:3])) + h.Length = binary.BigEndian.Uint16(data[3:5]) + + if h.ContentType.String() == "Unknown" { + return errors.New("Unknown ETLS record type") + } + + hl := 5 // header length + tl := hl + int(h.Length) + if len(data) < tl { + df.SetTruncated() + return errors.New("ETLS packet length mismatch") + } + + switch h.ContentType { + default: + return errors.New("Unknown ETLS record type") + case ETLSChangeCipherSpec: + var r ETLSChangeCipherSpecRecord + e := r.decodeFromBytes(h, data[hl:tl], df) + if e != nil { + return e + } + t.ChangeCipherSpec = append(t.ChangeCipherSpec, r) + case ETLSAlert: + var r ETLSAlertRecord + e := r.decodeFromBytes(h, data[hl:tl], df) + if e != nil { + return e + } + t.Alert = append(t.Alert, r) + case ETLSHandshake: + var r ETLSHandshakeRecord + e := r.decodeFromBytes(h, data[hl:tl], df) + if e != nil { + return e + } + t.Handshake = append(t.Handshake, r) + case ETLSApplicationData: + var r ETLSAppDataRecord + e := r.decodeFromBytes(h, data[hl:tl], df) + if e != nil { + return e + } + t.AppData = append(t.AppData, r) + } + + if len(data) == tl { + return nil + } + return t.decodeETLSRecords(data[tl:len(data)], df) +} + +// CanDecode implements gopacket.DecodingLayer. +func (t *ETLS) CanDecode() gopacket.LayerClass { + return LayerTypeETLS +} + +// NextLayerType implements gopacket.DecodingLayer. +func (t *ETLS) NextLayerType() gopacket.LayerType { + return gopacket.LayerTypeZero +} + +// Payload returns nil, since ETLS encrypted payload is inside ETLSAppDataRecord +func (t *ETLS) Payload() []byte { + return nil +} + +func init() { + _ = gopacket.RegisterLayerType(1337, gopacket.LayerTypeMetadata{Name: "ETLS", Decoder: gopacket.DecodeFunc(decodeETLS)}) +} diff --git a/etls/etls_alert.go b/etls/etls_alert.go new file mode 100644 index 0000000..1402d15 --- /dev/null +++ b/etls/etls_alert.go @@ -0,0 +1,165 @@ +// Copyright 2018 The GoPacket Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package etls + +import ( + "errors" + "fmt" + + "github.com/google/gopacket" +) + +// ETLSAlertLevel defines the alert level data type +type ETLSAlertLevel uint8 + +// ETLSAlertDescr defines the alert descrption data type +type ETLSAlertDescr uint8 + +const ( + ETLSAlertWarning ETLSAlertLevel = 1 + ETLSAlertFatal ETLSAlertLevel = 2 + ETLSAlertUnknownLevel ETLSAlertLevel = 255 + + ETLSAlertCloseNotify ETLSAlertDescr = 0 + ETLSAlertUnexpectedMessage ETLSAlertDescr = 10 + ETLSAlertBadRecordMac ETLSAlertDescr = 20 + ETLSAlertDecryptionFailedRESERVED ETLSAlertDescr = 21 + ETLSAlertRecordOverflow ETLSAlertDescr = 22 + ETLSAlertDecompressionFailure ETLSAlertDescr = 30 + ETLSAlertHandshakeFailure ETLSAlertDescr = 40 + ETLSAlertNoCertificateRESERVED ETLSAlertDescr = 41 + ETLSAlertBadCertificate ETLSAlertDescr = 42 + ETLSAlertUnsupportedCertificate ETLSAlertDescr = 43 + ETLSAlertCertificateRevoked ETLSAlertDescr = 44 + ETLSAlertCertificateExpired ETLSAlertDescr = 45 + ETLSAlertCertificateUnknown ETLSAlertDescr = 46 + ETLSAlertIllegalParameter ETLSAlertDescr = 47 + ETLSAlertUnknownCa ETLSAlertDescr = 48 + ETLSAlertAccessDenied ETLSAlertDescr = 49 + ETLSAlertDecodeError ETLSAlertDescr = 50 + ETLSAlertDecryptError ETLSAlertDescr = 51 + ETLSAlertExportRestrictionRESERVED ETLSAlertDescr = 60 + ETLSAlertProtocolVersion ETLSAlertDescr = 70 + ETLSAlertInsufficientSecurity ETLSAlertDescr = 71 + ETLSAlertInternalError ETLSAlertDescr = 80 + ETLSAlertUserCanceled ETLSAlertDescr = 90 + ETLSAlertNoRenegotiation ETLSAlertDescr = 100 + ETLSAlertUnsupportedExtension ETLSAlertDescr = 110 + ETLSAlertUnknownDescription ETLSAlertDescr = 255 +) + +// ETLS Alert +// 0 1 2 3 4 5 6 7 8 +// +--+--+--+--+--+--+--+--+ +// | Level | +// +--+--+--+--+--+--+--+--+ +// | Description | +// +--+--+--+--+--+--+--+--+ + +// ETLSAlertRecord contains all the information that each Alert Record type should have +type ETLSAlertRecord struct { + ETLSRecordHeader + + Level ETLSAlertLevel + Description ETLSAlertDescr + + EncryptedMsg []byte +} + +// DecodeFromBytes decodes the slice into the ETLS struct. +func (t *ETLSAlertRecord) decodeFromBytes(h ETLSRecordHeader, data []byte, df gopacket.DecodeFeedback) error { + // ETLS Record Header + t.ContentType = h.ContentType + t.Version = h.Version + t.Length = h.Length + + if len(data) < 2 { + df.SetTruncated() + return errors.New("ETLS Alert packet too short") + } + + if t.Length == 2 { + t.Level = ETLSAlertLevel(data[0]) + t.Description = ETLSAlertDescr(data[1]) + } else { + t.Level = ETLSAlertUnknownLevel + t.Description = ETLSAlertUnknownDescription + t.EncryptedMsg = data + } + + return nil +} + +// Strings shows the ETLS alert level nicely formatted +func (al ETLSAlertLevel) String() string { + switch al { + default: + return fmt.Sprintf("Unknown(%d)", al) + case ETLSAlertWarning: + return "Warning" + case ETLSAlertFatal: + return "Fatal" + } +} + +// Strings shows the ETLS alert description nicely formatted +func (ad ETLSAlertDescr) String() string { + switch ad { + default: + return "Unknown" + case ETLSAlertCloseNotify: + return "close_notify" + case ETLSAlertUnexpectedMessage: + return "unexpected_message" + case ETLSAlertBadRecordMac: + return "bad_record_mac" + case ETLSAlertDecryptionFailedRESERVED: + return "decryption_failed_RESERVED" + case ETLSAlertRecordOverflow: + return "record_overflow" + case ETLSAlertDecompressionFailure: + return "decompression_failure" + case ETLSAlertHandshakeFailure: + return "handshake_failure" + case ETLSAlertNoCertificateRESERVED: + return "no_certificate_RESERVED" + case ETLSAlertBadCertificate: + return "bad_certificate" + case ETLSAlertUnsupportedCertificate: + return "unsupported_certificate" + case ETLSAlertCertificateRevoked: + return "certificate_revoked" + case ETLSAlertCertificateExpired: + return "certificate_expired" + case ETLSAlertCertificateUnknown: + return "certificate_unknown" + case ETLSAlertIllegalParameter: + return "illegal_parameter" + case ETLSAlertUnknownCa: + return "unknown_ca" + case ETLSAlertAccessDenied: + return "access_denied" + case ETLSAlertDecodeError: + return "decode_error" + case ETLSAlertDecryptError: + return "decrypt_error" + case ETLSAlertExportRestrictionRESERVED: + return "export_restriction_RESERVED" + case ETLSAlertProtocolVersion: + return "protocol_version" + case ETLSAlertInsufficientSecurity: + return "insufficient_security" + case ETLSAlertInternalError: + return "internal_error" + case ETLSAlertUserCanceled: + return "user_canceled" + case ETLSAlertNoRenegotiation: + return "no_renegotiation" + case ETLSAlertUnsupportedExtension: + return "unsupported_extension" + } +} diff --git a/etls/etls_appdata.go b/etls/etls_appdata.go new file mode 100644 index 0000000..14f2a2f --- /dev/null +++ b/etls/etls_appdata.go @@ -0,0 +1,34 @@ +// Copyright 2018 The GoPacket Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package etls + +import ( + "errors" + + "github.com/google/gopacket" +) + +// ETLSAppDataRecord contains all the information that each AppData Record types should have +type ETLSAppDataRecord struct { + ETLSRecordHeader + Payload []byte +} + +// DecodeFromBytes decodes the slice into the ETLS struct. +func (t *ETLSAppDataRecord) decodeFromBytes(h ETLSRecordHeader, data []byte, df gopacket.DecodeFeedback) error { + // ETLS Record Header + t.ContentType = h.ContentType + t.Version = h.Version + t.Length = h.Length + + if len(data) != int(t.Length) { + return errors.New("ETLS Application Data length mismatch") + } + + t.Payload = data + return nil +} diff --git a/etls/etls_cipherspec.go b/etls/etls_cipherspec.go new file mode 100644 index 0000000..d87a865 --- /dev/null +++ b/etls/etls_cipherspec.go @@ -0,0 +1,64 @@ +// Copyright 2018 The GoPacket Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package etls + +import ( + "errors" + + "github.com/google/gopacket" +) + +// ETLSchangeCipherSpec defines the message value inside ChangeCipherSpec Record +type ETLSchangeCipherSpec uint8 + +const ( + ETLSChangecipherspecMessage ETLSchangeCipherSpec = 1 + ETLSChangecipherspecUnknown ETLSchangeCipherSpec = 255 +) + +// ETLS Change Cipher Spec +// 0 1 2 3 4 5 6 7 8 +// +--+--+--+--+--+--+--+--+ +// | Message | +// +--+--+--+--+--+--+--+--+ + +// ETLSChangeCipherSpecRecord defines the type of data inside ChangeCipherSpec Record +type ETLSChangeCipherSpecRecord struct { + ETLSRecordHeader + + Message ETLSchangeCipherSpec +} + +// DecodeFromBytes decodes the slice into the ETLS struct. +func (t *ETLSChangeCipherSpecRecord) decodeFromBytes(h ETLSRecordHeader, data []byte, df gopacket.DecodeFeedback) error { + // ETLS Record Header + t.ContentType = h.ContentType + t.Version = h.Version + t.Length = h.Length + + if len(data) != 1 { + df.SetTruncated() + return errors.New("ETLS Change Cipher Spec record incorrect length") + } + + t.Message = ETLSchangeCipherSpec(data[0]) + if t.Message != ETLSChangecipherspecMessage { + t.Message = ETLSChangecipherspecUnknown + } + + return nil +} + +// String shows the message value nicely formatted +func (ccs ETLSchangeCipherSpec) String() string { + switch ccs { + default: + return "Unknown" + case ETLSChangecipherspecMessage: + return "Change Cipher Spec Message" + } +} diff --git a/etls/etls_common.go b/etls/etls_common.go new file mode 100644 index 0000000..75baa92 --- /dev/null +++ b/etls/etls_common.go @@ -0,0 +1,75 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package etls + +// ETLS handshake message types. +const ( + typeHelloRequest uint8 = 0 + typeClientHello uint8 = 1 + typeServerHello uint8 = 2 + typeNewSessionTicket uint8 = 4 + typeCertificate uint8 = 11 + typeServerKeyExchange uint8 = 12 + typeCertificateRequest uint8 = 13 + typeServerHelloDone uint8 = 14 + typeCertificateVerify uint8 = 15 + typeClientKeyExchange uint8 = 16 + typeFinished uint8 = 20 + typeCertificateStatus uint8 = 22 + typeNextProtocol uint8 = 67 // Not IANA assigned +) + +// ETLS compression types. +const ( + compressionNone uint8 = 0 +) + +// ETLS extension numbers +const ( + extensionServerName uint16 = 0 + extensionStatusRequest uint16 = 5 + extensionSupportedCurves uint16 = 10 + extensionSupportedPoints uint16 = 11 + extensionSignatureAlgorithms uint16 = 13 + extensionALPN uint16 = 16 + extensionSCT uint16 = 18 // https://tools.ietf.org/html/rfc6962#section-6 + extensionSessionTicket uint16 = 35 + extensionNextProtoNeg uint16 = 13172 // not IANA assigned + extensionRenegotiationInfo uint16 = 0xff01 +) + +// CurveID is the type of a ETLS identifier for an elliptic curve. See +// https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8 +type CurveID uint16 + +const ( + CurveP256 CurveID = 23 + CurveP384 CurveID = 24 + CurveP521 CurveID = 25 + X25519 CurveID = 29 +) + +// Certificate types (for certificateRequestMsg) +const ( + certTypeRSASign = 1 // A certificate containing an RSA key + certTypeDSSSign = 2 // A certificate containing a DSA key + certTypeRSAFixedDH = 3 // A certificate containing a static DH key + certTypeDSSFixedDH = 4 // A certificate containing a static DH key + + // See RFC 4492 sections 3 and 5.5. + certTypeECDSASign = 64 // A certificate containing an ECDSA-capable public key, signed with ECDSA. + certTypeRSAFixedECDH = 65 // A certificate containing an ECDH-capable public key, signed with RSA. + certTypeECDSAFixedECDH = 66 // A certificate containing an ECDH-capable public key, signed with ECDSA. +) + +// SignatureScheme identifies a signature algorithm supported by ETLS. See +// https://tools.ietf.org/html/draft-ietf-tls-tls13-18#section-4.2.3. +type SignatureScheme uint16 +type Extension uint16 + +// ETLS signaling cipher suite values +const ( + scsvRenegotiation uint16 = 0x00ff +) diff --git a/etls/etls_handshake.go b/etls/etls_handshake.go new file mode 100644 index 0000000..8615f77 --- /dev/null +++ b/etls/etls_handshake.go @@ -0,0 +1,48 @@ +// Copyright 2018 The GoPacket Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +package etls + +import ( + "github.com/google/gopacket" +) + +// ETLSHandshakeRecord defines the structure of a Handskake Record +type ETLSHandshakeRecord struct { + ETLSRecordHeader + ETLSHandshakeMsgType uint8 + ETLSHandshakeServerHello *serverHelloMsg + ETLSHandshakeClientHello *clientHelloMsg + ETLSHandshakeCertificate *certificateMsg +} + +// DecodeFromBytes decodes the slice into the ETLS struct. +func (t *ETLSHandshakeRecord) decodeFromBytes(h ETLSRecordHeader, data []byte, df gopacket.DecodeFeedback) error { + // ETLS Record Header + t.ContentType = h.ContentType + t.Version = h.Version + t.Length = h.Length + + // Switch on Handshake message type + switch uint8(data[0]) { + case typeClientHello: + t.ETLSHandshakeMsgType = typeClientHello + t.ETLSHandshakeClientHello = new(clientHelloMsg) + t.ETLSHandshakeClientHello.unmarshal(data) + case typeServerHello: + t.ETLSHandshakeMsgType = typeServerHello + t.ETLSHandshakeServerHello = new(serverHelloMsg) + t.ETLSHandshakeServerHello.unmarshal(data) + case typeCertificate: + t.ETLSHandshakeMsgType = typeCertificate + t.ETLSHandshakeCertificate = new(certificateMsg) + t.ETLSHandshakeCertificate.unmarshal(data) + } + // Please see the following url if you are interested into implementing the rest: + // https://golang.org/src/crypto/tls/conn.go?h=readHandshake#L950 + + return nil +} diff --git a/etls/etls_handshake_messages.go b/etls/etls_handshake_messages.go new file mode 100644 index 0000000..60a5072 --- /dev/null +++ b/etls/etls_handshake_messages.go @@ -0,0 +1,369 @@ +// Copyright 2018 The GoPacket Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. + +// Most of this content was extracted from go tls package + +package etls + +type handshakeMessage interface { + unmarshal([]byte) bool +} + +type clientHelloMsg struct { + raw []byte + extensions map[Extension]uint16 + AllExtensions []uint16 + Vers uint16 + random []byte + sessionId []byte + CipherSuites []uint16 + compressionMethods []uint8 + nextProtoNeg bool + serverName string + ocspStapling bool + scts bool + SupportedCurves []CurveID + SupportedPoints []uint8 + ticketSupported bool + sessionTicket []uint8 + supportedSignatureAlgorithms []SignatureScheme + secureRenegotiation []byte + secureRenegotiationSupported bool + alpnProtocols []string +} + +func (m *clientHelloMsg) unmarshal(data []byte) bool { + if len(data) < 42 { + return false + } + + m.raw = data + m.Vers = uint16(data[4])<<8 | uint16(data[5]) + m.random = data[6:38] + sessionIdLen := int(data[38]) + if sessionIdLen > 32 || len(data) < 39+sessionIdLen { + return false + } + + m.sessionId = data[39 : 39+sessionIdLen] + data = data[39+sessionIdLen:] + if len(data) < 2 { + return false + } + + // cipherSuiteLen is the number of bytes of cipher suite numbers. Since + // they are uint16s, the number must be even. + cipherSuiteLen := int(data[0])<<8 | int(data[1]) + if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { + return false + } + + numCipherSuites := cipherSuiteLen / 2 + m.CipherSuites = make([]uint16, numCipherSuites) + for i := 0; i < numCipherSuites; i++ { + m.CipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i]) + if m.CipherSuites[i] == scsvRenegotiation { + m.secureRenegotiationSupported = true + } + } + data = data[2+cipherSuiteLen:] + if len(data) < 1 { + return false + } + + compressionMethodsLen := int(data[0]) + if len(data) < 1+compressionMethodsLen { + return false + } + + m.compressionMethods = data[1 : 1+compressionMethodsLen] + data = data[1+compressionMethodsLen:] + m.nextProtoNeg = false + m.serverName = "" + m.ocspStapling = false + m.ticketSupported = false + m.sessionTicket = nil + m.supportedSignatureAlgorithms = nil + m.alpnProtocols = nil + m.scts = false + + if len(data) == 0 { + // ClientHello is optionally followed by extension data + return true + } + + if len(data) < 2 { + return false + + } + + // We parse extensions as their needed for ja3 + extensionsLength := int(data[0])<<8 | int(data[1]) + data = data[2:] + m.extensions = make(map[Extension]uint16) + + if extensionsLength != len(data) { + return false + } + + for len(data) != 0 { + if len(data) < 4 { + return false + } + extension := uint16(data[0])<<8 | uint16(data[1]) + length := int(data[2])<<8 | int(data[3]) + data = data[4:] + + m.AllExtensions = append(m.AllExtensions, uint16(extension)) + + if len(data) < length { + return false + } + + switch extension { + case extensionSupportedCurves: + // https://tools.ietf.org/html/rfc4492#section-5.5.1 + if length < 2 { + return false + } + l := int(data[0])<<8 | int(data[1]) + if l%2 == 1 || length != l+2 { + return false + } + numCurves := l / 2 + m.SupportedCurves = make([]CurveID, numCurves) + d := data[2:] + for i := 0; i < numCurves; i++ { + m.SupportedCurves[i] = CurveID(d[0])<<8 | CurveID(d[1]) + d = d[2:] + } + case extensionSupportedPoints: + // https://tools.ietf.org/html/rfc4492#section-5.5.2 + if length < 1 { + return false + } + l := int(data[0]) + if length != l+1 { + return false + } + m.SupportedPoints = make([]uint8, l) + copy(m.SupportedPoints, data[1:]) + } + + data = data[length:] + } + return true +} + +type serverHelloMsg struct { + raw []byte + extensions map[Extension]uint16 + AllExtensions []uint16 + Vers uint16 + random []byte + sessionId []byte + CipherSuite uint16 + compressionMethod uint8 + nextProtoNeg bool + nextProtos []string + ocspStapling bool + scts [][]byte + ticketSupported bool + secureRenegotiation []byte + secureRenegotiationSupported bool + alpnProtocol string +} + +func (m *serverHelloMsg) unmarshal(data []byte) bool { + if len(data) < 42 { + return false + } + + m.raw = data + m.Vers = uint16(data[4])<<8 | uint16(data[5]) + m.random = data[6:38] + sessionIdLen := int(data[38]) + if sessionIdLen > 32 || len(data) < 39+sessionIdLen { + return false + } + m.sessionId = data[39 : 39+sessionIdLen] + data = data[39+sessionIdLen:] + if len(data) < 3 { + return false + } + m.CipherSuite = uint16(data[0])<<8 | uint16(data[1]) + m.compressionMethod = data[2] + data = data[3:] + + m.nextProtoNeg = false + m.nextProtos = nil + m.ocspStapling = false + m.scts = nil + m.ticketSupported = false + m.alpnProtocol = "" + if len(data) == 0 { + // ServerHello is optionally followed by extension data + return true + } + + if len(data) < 2 { + return false + } + + // Import Extension code needed for ja3s + extensionsLength := int(data[0])<<8 | int(data[1]) + data = data[2:] + m.extensions = make(map[Extension]uint16) + + if len(data) != extensionsLength { + return false + } + + for len(data) != 0 { + if len(data) < 4 { + return false + } + extension := uint16(data[0])<<8 | uint16(data[1]) + length := int(data[2])<<8 | int(data[3]) + data = data[4:] + + m.AllExtensions = append(m.AllExtensions, uint16(extension)) + + if len(data) < length { + return false + } + + switch extension { + + case extensionNextProtoNeg: + m.nextProtoNeg = true + d := data[:length] + for len(d) > 0 { + l := int(d[0]) + d = d[1:] + if l == 0 || l > len(d) { + return false + } + m.nextProtos = append(m.nextProtos, string(d[:l])) + d = d[l:] + } + + case extensionStatusRequest: + if length > 0 { + return false + } + m.ocspStapling = true + + case extensionSessionTicket: + if length > 0 { + return false + } + m.ticketSupported = true + + case extensionRenegotiationInfo: + if length == 0 { + return false + } + d := data[:length] + l := int(d[0]) + d = d[1:] + if l != len(d) { + return false + } + + m.secureRenegotiation = d + m.secureRenegotiationSupported = true + + case extensionALPN: + d := data[:length] + if len(d) < 3 { + return false + } + l := int(d[0])<<8 | int(d[1]) + if l != len(d)-2 { + return false + } + d = d[2:] + l = int(d[0]) + if l != len(d)-1 { + return false + } + d = d[1:] + if len(d) == 0 { + // ALPN protocols must not be empty. + return false + } + m.alpnProtocol = string(d) + + case extensionSCT: + d := data[:length] + if len(d) < 2 { + return false + } + l := int(d[0])<<8 | int(d[1]) + d = d[2:] + if len(d) != l || l == 0 { + return false + } + m.scts = make([][]byte, 0, 3) + for len(d) != 0 { + if len(d) < 2 { + return false + } + sctLen := int(d[0])<<8 | int(d[1]) + d = d[2:] + if sctLen == 0 || len(d) < sctLen { + return false + } + m.scts = append(m.scts, d[:sctLen]) + d = d[sctLen:] + } + } + data = data[length:] + } + return true +} + +type certificateMsg struct { + raw []byte + Certificates [][]byte +} + +func (m *certificateMsg) unmarshal(data []byte) bool { + if len(data) < 7 { + return false + } + m.raw = data + certsLen := uint32(data[4])<<16 | uint32(data[5])<<8 | uint32(data[6]) + if uint32(len(data)) != certsLen+7 { + return false + } + numCerts := 0 + d := data[7:] + for certsLen > 0 { + if len(d) < 4 { + return false + } + certLen := uint32(d[0])<<16 | uint32(d[1])<<8 | uint32(d[2]) + if uint32(len(d)) < 3+certLen { + return false + } + d = d[3+certLen:] + certsLen -= 3 + certLen + numCerts++ + } + + m.Certificates = make([][]byte, numCerts) + d = data[7:] + + for i := 0; i < numCerts; i++ { + certLen := uint32(d[0])<<16 | uint32(d[1])<<8 | uint32(d[2]) + m.Certificates[i] = d[3 : 3+certLen] + d = d[3+certLen:] + } + + return true +} diff --git a/etls/etls_test.go b/etls/etls_test.go new file mode 100644 index 0000000..2ae4222 --- /dev/null +++ b/etls/etls_test.go @@ -0,0 +1,339 @@ +package etls + +import ( + "reflect" + "testing" + + "github.com/google/gopacket" +) + +// https://github.com/tintinweb/scapy-ssl_tls/blob/master/tests/files/RSA_WITH_AES_128_CBC_SHA.pcap +// WARNING! Tests are specific for each packet. If you change a packet, please review their tests. + +// Packet 4 - Client Hello (full packet, from Ethernet to ETLS layers) +var testClientHello = []byte{ + 0x00, 0x0c, 0x29, 0x1f, 0xab, 0x17, 0x00, 0x50, 0x56, 0xc0, 0x00, 0x08, 0x08, 0x00, 0x45, 0x00, + 0x00, 0xfe, 0x71, 0x42, 0x40, 0x00, 0x80, 0x06, 0x4e, 0xe1, 0xc0, 0xa8, 0xdc, 0x01, 0xc0, 0xa8, + 0xdc, 0x83, 0x2f, 0x0e, 0x01, 0xbb, 0x25, 0x6c, 0xbd, 0x3d, 0xcc, 0xce, 0xe1, 0xf7, 0x50, 0x18, + 0xff, 0xff, 0x7c, 0xaf, 0x00, 0x00, 0x16, 0x03, 0x01, 0x00, 0xd1, 0x01, 0x00, 0x00, 0xcd, 0x03, + 0x01, 0xff, 0xa2, 0x88, 0x97, 0x7c, 0x41, 0xa1, 0x08, 0x34, 0x2c, 0x98, 0xc2, 0x70, 0x04, 0xa0, + 0x5d, 0x5f, 0x39, 0xef, 0xe0, 0x70, 0xd5, 0x12, 0xf1, 0x35, 0x17, 0xb6, 0x0d, 0xc4, 0xd3, 0x09, + 0x85, 0x00, 0x00, 0x5a, 0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x39, 0x00, 0x38, 0x00, 0x88, 0x00, 0x87, + 0xc0, 0x0f, 0xc0, 0x05, 0x00, 0x35, 0x00, 0x84, 0xc0, 0x13, 0xc0, 0x09, 0x00, 0x33, 0x00, 0x32, + 0x00, 0x9a, 0x00, 0x99, 0x00, 0x45, 0x00, 0x44, 0xc0, 0x0e, 0xc0, 0x04, 0x00, 0x2f, 0x00, 0x96, + 0x00, 0x41, 0xc0, 0x11, 0xc0, 0x07, 0xc0, 0x0c, 0xc0, 0x02, 0x00, 0x05, 0x00, 0x04, 0xc0, 0x12, + 0xc0, 0x08, 0x00, 0x16, 0x00, 0x13, 0xc0, 0x0d, 0xc0, 0x03, 0x00, 0x0a, 0x00, 0x15, 0x00, 0x12, + 0x00, 0x09, 0x00, 0x14, 0x00, 0x11, 0x00, 0x08, 0x00, 0x06, 0x00, 0x03, 0x00, 0xff, 0x02, 0x01, + 0x00, 0x00, 0x49, 0x00, 0x0b, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x34, 0x00, + 0x32, 0x00, 0x0e, 0x00, 0x0d, 0x00, 0x19, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x09, 0x00, + 0x0a, 0x00, 0x16, 0x00, 0x17, 0x00, 0x08, 0x00, 0x06, 0x00, 0x07, 0x00, 0x14, 0x00, 0x15, 0x00, + 0x04, 0x00, 0x05, 0x00, 0x12, 0x00, 0x13, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x0f, 0x00, + 0x10, 0x00, 0x11, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, 0x01, +} +var testClientHelloDecoded = &ETLS{ + BaseLayer: BaseLayer{ + Contents: testClientHello[54:], + Payload: nil, + }, + ChangeCipherSpec: nil, + Handshake: []ETLSHandshakeRecord{ + { + ETLSRecordHeader{ + ContentType: 22, + Version: 0x0301, + Length: 209, + }, + }, + }, + AppData: nil, + Alert: nil, +} + +// Packet 6 - Server Hello, Certificate, Server Hello Done +var testServerHello = []byte{ + 0x16, 0x03, 0x01, 0x00, 0x3a, 0x02, 0x00, 0x00, 0x36, 0x03, 0x01, 0x55, 0x5c, 0xd6, 0x97, 0xa3, + 0x97, 0xe9, 0xf4, 0x0c, 0xf4, 0x56, 0x14, 0x9f, 0xe4, 0x24, 0xf9, 0xeb, 0x49, 0xd4, 0xd1, 0x5f, + 0xfc, 0x12, 0xb4, 0xfd, 0x45, 0x4e, 0x3d, 0xeb, 0x6a, 0xad, 0xcf, 0x00, 0x00, 0x2f, 0x01, 0x00, + 0x0e, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01, 0x01, 0x16, + 0x03, 0x01, 0x01, 0x90, 0x0b, 0x00, 0x01, 0x8c, 0x00, 0x01, 0x89, 0x00, 0x01, 0x86, 0x30, 0x82, + 0x01, 0x82, 0x30, 0x82, 0x01, 0x2c, 0x02, 0x01, 0x04, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, + 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x04, 0x05, 0x00, 0x30, 0x38, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, + 0x55, 0x04, 0x06, 0x13, 0x02, 0x41, 0x55, 0x31, 0x0c, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x04, 0x08, + 0x13, 0x03, 0x51, 0x4c, 0x44, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x12, + 0x53, 0x53, 0x4c, 0x65, 0x61, 0x79, 0x2f, 0x72, 0x73, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, + 0x43, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x39, 0x35, 0x31, 0x30, 0x30, 0x39, 0x32, 0x33, 0x33, 0x32, + 0x30, 0x35, 0x5a, 0x17, 0x0d, 0x39, 0x38, 0x30, 0x37, 0x30, 0x35, 0x32, 0x33, 0x33, 0x32, 0x30, + 0x35, 0x5a, 0x30, 0x60, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x41, + 0x55, 0x31, 0x0c, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x03, 0x51, 0x4c, 0x44, 0x31, + 0x19, 0x30, 0x17, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x10, 0x4d, 0x69, 0x6e, 0x63, 0x6f, 0x6d, + 0x20, 0x50, 0x74, 0x79, 0x2e, 0x20, 0x4c, 0x74, 0x64, 0x2e, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, + 0x55, 0x04, 0x0b, 0x13, 0x02, 0x43, 0x53, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x03, + 0x13, 0x12, 0x53, 0x53, 0x4c, 0x65, 0x61, 0x79, 0x20, 0x64, 0x65, 0x6d, 0x6f, 0x20, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x30, 0x5c, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x4b, 0x00, 0x30, 0x48, 0x02, 0x41, 0x00, 0xb7, 0x2c, 0x25, + 0xdc, 0x49, 0xc5, 0xae, 0x6b, 0x43, 0xc5, 0x2e, 0x41, 0xc1, 0x2e, 0x6d, 0x95, 0x7a, 0x3a, 0xa9, + 0x03, 0x51, 0x78, 0x45, 0x0f, 0x2a, 0xd1, 0x58, 0xd1, 0x88, 0xf6, 0x9f, 0x8f, 0x1f, 0xd9, 0xfd, + 0xa5, 0x87, 0xde, 0x2a, 0x5d, 0x31, 0x5b, 0xee, 0x24, 0x66, 0xbf, 0xc0, 0x55, 0xdb, 0xfe, 0x70, + 0xc5, 0x2c, 0x39, 0x5f, 0x5a, 0x9f, 0xa8, 0x08, 0xfc, 0x21, 0x06, 0xd5, 0x4f, 0x02, 0x03, 0x01, + 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x04, 0x05, + 0x00, 0x03, 0x41, 0x00, 0x2b, 0x34, 0x5b, 0x22, 0x85, 0x62, 0x23, 0x07, 0x36, 0xf4, 0x0c, 0x2b, + 0x14, 0xd0, 0x1b, 0xcb, 0xd9, 0xbb, 0xd2, 0xc0, 0x9a, 0xcf, 0x12, 0xa1, 0x65, 0x90, 0x3a, 0xb7, + 0x17, 0x83, 0x3a, 0x10, 0x6b, 0xad, 0x2f, 0xd6, 0xb1, 0x11, 0xc0, 0x0d, 0x5a, 0x06, 0xdb, 0x11, + 0xd0, 0x2f, 0x34, 0x90, 0xf5, 0x76, 0x61, 0x26, 0xa1, 0x69, 0xf2, 0xdb, 0xb3, 0xe7, 0x20, 0xcb, + 0x3a, 0x64, 0xe6, 0x41, 0x16, 0x03, 0x01, 0x00, 0x04, 0x0e, 0x00, 0x00, 0x00, +} + +// Packet 7 - Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message +var testClientKeyExchange = []byte{ + 0x16, 0x03, 0x01, 0x00, 0x46, 0x10, 0x00, 0x00, 0x42, 0x00, 0x40, 0x9e, 0x73, 0xdf, 0xe0, 0xf2, + 0xd0, 0x40, 0x32, 0x44, 0x9a, 0x34, 0x7f, 0x57, 0x86, 0x10, 0xea, 0x3d, 0xc5, 0xe2, 0xf9, 0xa5, + 0x69, 0x43, 0xc9, 0x0b, 0x00, 0x7e, 0x91, 0x31, 0x57, 0xfc, 0xc5, 0x65, 0x18, 0x0d, 0x44, 0xfd, + 0x51, 0xf8, 0xda, 0x8a, 0x7a, 0xab, 0x16, 0x03, 0xeb, 0xac, 0x23, 0x6e, 0x8d, 0xdd, 0xbb, 0xf4, + 0x75, 0xe7, 0xb7, 0xa3, 0xce, 0xdb, 0x67, 0x6b, 0x7d, 0x30, 0x2a, 0x14, 0x03, 0x01, 0x00, 0x01, + 0x01, 0x16, 0x03, 0x01, 0x00, 0x30, 0x15, 0xcb, 0x7a, 0x5b, 0x2d, 0xc0, 0x27, 0x09, 0x28, 0x62, + 0x95, 0x44, 0x9f, 0xa1, 0x1e, 0x4e, 0x6a, 0xfb, 0x49, 0x9d, 0x6a, 0x24, 0x44, 0xc6, 0x8e, 0x26, + 0xbc, 0xc1, 0x28, 0x8c, 0x27, 0xcc, 0xa2, 0xba, 0xec, 0x38, 0x63, 0x6e, 0x64, 0xd8, 0x52, 0x94, + 0x17, 0x96, 0x61, 0xfd, 0x9c, 0x54, +} +var testClientKeyExchangeDecoded = &ETLS{ + BaseLayer: BaseLayer{ + Contents: testClientKeyExchange[81:], + Payload: nil, + }, + ChangeCipherSpec: []ETLSChangeCipherSpecRecord{ + { + ETLSRecordHeader{ + ContentType: 20, + Version: 0x0301, + Length: 1, + }, + 1, + }, + }, + Handshake: []ETLSHandshakeRecord{ + { + ETLSRecordHeader{ + ContentType: 22, + Version: 0x0301, + Length: 70, + }, + }, + { + ETLSRecordHeader{ + ContentType: 22, + Version: 0x0301, + Length: 48, + }, + }, + }, + AppData: nil, + Alert: nil, +} + +// Packet 9 - New Session Ticket, Change Cipher Spec, Encryption Handshake Message +var testNewSessionTicket = []byte{ + 0x16, 0x03, 0x01, 0x00, 0xaa, 0x04, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x1c, 0x20, 0x00, 0xa0, 0xd4, + 0xee, 0xb0, 0x9b, 0xb5, 0xa2, 0xd3, 0x00, 0x57, 0x84, 0x59, 0xec, 0x0d, 0xbf, 0x05, 0x0c, 0xd5, + 0xb9, 0xe2, 0xf8, 0x32, 0xb5, 0xec, 0xce, 0xe2, 0x9c, 0x25, 0x25, 0xd9, 0x3e, 0x4a, 0x94, 0x5b, + 0xca, 0x18, 0x2b, 0x0f, 0x5f, 0xf6, 0x73, 0x38, 0x62, 0xcd, 0xcc, 0xf1, 0x32, 0x39, 0xe4, 0x5e, + 0x30, 0xf3, 0x94, 0xf5, 0xc5, 0x94, 0x3a, 0x8c, 0x8e, 0xe5, 0x12, 0x4a, 0x1e, 0xd8, 0x31, 0xb5, + 0x17, 0x09, 0xa6, 0x4c, 0x69, 0xca, 0xae, 0xfb, 0x04, 0x17, 0x64, 0x54, 0x9e, 0xc2, 0xfa, 0xf3, + 0x6d, 0xe9, 0xa5, 0xed, 0xa6, 0x65, 0xfe, 0x2f, 0xf3, 0xc6, 0xce, 0x78, 0x40, 0xf7, 0x65, 0xe0, + 0x13, 0xd3, 0x77, 0xc7, 0xc5, 0x79, 0x16, 0x56, 0x4c, 0x30, 0x94, 0xcf, 0xb0, 0x3c, 0x00, 0x91, + 0xbd, 0x86, 0x08, 0x9f, 0x2f, 0x05, 0x67, 0x03, 0x6f, 0xa7, 0x3b, 0xb9, 0x36, 0xf2, 0x80, 0x4f, + 0x60, 0x5d, 0x4c, 0xc4, 0x42, 0x5d, 0x02, 0x44, 0xba, 0x31, 0x8f, 0x39, 0x8e, 0x0c, 0x1e, 0xa8, + 0x26, 0x4f, 0x3e, 0x01, 0x96, 0xb3, 0x6f, 0xc6, 0x25, 0xe4, 0x30, 0x03, 0xd6, 0x3a, 0x7d, 0x14, + 0x03, 0x01, 0x00, 0x01, 0x01, 0x16, 0x03, 0x01, 0x00, 0x30, 0x25, 0xb8, 0x58, 0xc1, 0xa6, 0x3f, + 0xf8, 0xbd, 0xe6, 0xae, 0xbd, 0x98, 0xd4, 0x75, 0xa5, 0x45, 0x1b, 0xd8, 0x6a, 0x70, 0x79, 0x86, + 0x29, 0x4e, 0x4f, 0x64, 0xba, 0xe7, 0x1f, 0xca, 0x4b, 0x96, 0x9b, 0xf7, 0x0b, 0x50, 0xf5, 0x4f, + 0xfd, 0xda, 0xda, 0xcd, 0xcd, 0x4b, 0x12, 0x2e, 0xdf, 0xd5, +} + +// Packet 13 - Two Application Data Records +var testDoubleAppData = []byte{ + 0x17, 0x03, 0x01, 0x00, 0x20, 0x77, 0x3a, 0x94, 0x7d, 0xb4, 0x47, 0x4a, 0x1d, 0xd4, 0x6c, 0x5a, + 0x69, 0x74, 0x03, 0x93, 0x32, 0xca, 0x54, 0x5e, 0xa5, 0x81, 0x99, 0x6a, 0x73, 0x66, 0xbf, 0x06, + 0xa0, 0xdc, 0x6a, 0x9c, 0xb1, 0x17, 0x03, 0x01, 0x00, 0x20, 0x44, 0x64, 0xc8, 0xc2, 0x5a, 0xfc, + 0x4a, 0x82, 0xdd, 0x53, 0x6d, 0x30, 0x82, 0x4d, 0x35, 0x22, 0xf1, 0x5f, 0x3b, 0x96, 0x66, 0x79, + 0x61, 0x9f, 0x51, 0x93, 0x1b, 0xbf, 0x53, 0x3b, 0xf8, 0x26, +} +var testDoubleAppDataDecoded = &ETLS{ + BaseLayer: BaseLayer{ + Contents: testDoubleAppData[37:], + Payload: nil, + }, + ChangeCipherSpec: nil, + Handshake: nil, + AppData: []ETLSAppDataRecord{ + { + ETLSRecordHeader{ + ContentType: 23, + Version: 0x0301, + Length: 32, + }, + testDoubleAppData[5 : 5+32], + }, + { + ETLSRecordHeader{ + ContentType: 23, + Version: 0x0301, + Length: 32, + }, + testDoubleAppData[42 : 42+32], + }, + }, + Alert: nil, +} + +var testAlertEncrypted = []byte{ + 0x15, 0x03, 0x03, 0x00, 0x20, 0x44, 0xb9, 0x9c, 0x2c, 0x6e, 0xab, 0xa3, 0xdf, 0xb1, 0x77, 0x04, + 0xa2, 0xa4, 0x3a, 0x9a, 0x08, 0x1d, 0xe6, 0x51, 0xac, 0xa0, 0x5f, 0xab, 0x74, 0xa7, 0x96, 0x24, + 0xfe, 0x62, 0xfe, 0xe8, 0x5e, +} +var testAlertEncryptedDecoded = &ETLS{ + BaseLayer: BaseLayer{ + Contents: testAlertEncrypted, + Payload: nil, + }, + ChangeCipherSpec: nil, + Handshake: nil, + AppData: nil, + Alert: []ETLSAlertRecord{ + { + ETLSRecordHeader{ + ContentType: 21, + Version: 0x0303, + Length: 32, + }, + 0xFF, + 0xFF, + testAlertEncrypted[5:], + }, + }, +} + +// Malformed ETLS records +var testMalformed = []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, +} + +var testETLSDecodeOptions = gopacket.DecodeOptions{ + SkipDecodeRecovery: true, + DecodeStreamsAsDatagrams: true, +} + +func TestParseETLSClientHello(t *testing.T) { + p := gopacket.NewPacket(testClientHello, LinkTypeEthernet, testETLSDecodeOptions) + if p.ErrorLayer() != nil { + t.Error("Failed to decode packet:", p.ErrorLayer().Error()) + } + checkLayers(p, []gopacket.LayerType{LayerTypeEthernet, LayerTypeIPv4, LayerTypeTCP, LayerTypeETLS}, t) + + if got, ok := p.Layer(LayerTypeETLS).(*ETLS); ok { + want := testClientHelloDecoded + if !reflect.DeepEqual(got, want) { + t.Errorf("ETLS ClientHello packet processing failed:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } + } else { + t.Error("No ETLS layer type found in packet") + } +} + +func testETLSClientHelloDecodeFromBytes(t *testing.T) { + var got ETLS + want := testClientKeyExchangeDecoded + + if err := got.DecodeFromBytes(testClientKeyExchange, gopacket.NilDecodeFeedback); err != nil { + t.Errorf("ETLS DecodeFromBytes first decode failed:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("ETLS DecodeFromBytes first decode doesn't match:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } + + if err := got.DecodeFromBytes(testClientKeyExchange, gopacket.NilDecodeFeedback); err != nil { + t.Errorf("ETLS DecodeFromBytes second decode failed:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("ETLS DecodeFromBytes second decode doesn't match:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } +} + +func TestParseETLSChangeCipherSpec(t *testing.T) { + p := gopacket.NewPacket(testClientKeyExchange, LayerTypeETLS, testETLSDecodeOptions) + if p.ErrorLayer() != nil { + t.Error("Failed to decode packet:", p.ErrorLayer().Error()) + } + checkLayers(p, []gopacket.LayerType{LayerTypeETLS}, t) + + if got, ok := p.Layer(LayerTypeETLS).(*ETLS); ok { + want := testClientKeyExchangeDecoded + if !reflect.DeepEqual(got, want) { + t.Errorf("ETLS ChangeCipherSpec packet processing failed:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } + } else { + t.Error("No ETLS layer type found in packet") + } +} + +func TestParseETLSAppData(t *testing.T) { + p := gopacket.NewPacket(testDoubleAppData, LayerTypeETLS, testETLSDecodeOptions) + if p.ErrorLayer() != nil { + t.Error("Failed to decode packet:", p.ErrorLayer().Error()) + } + checkLayers(p, []gopacket.LayerType{LayerTypeETLS}, t) + + if got, ok := p.Layer(LayerTypeETLS).(*ETLS); ok { + want := testDoubleAppDataDecoded + if !reflect.DeepEqual(got, want) { + t.Errorf("ETLS ETLSAppData packet processing failed:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } + } else { + t.Error("No ETLS layer type found in packet") + } +} + +func TestParseETLSMalformed(t *testing.T) { + p := gopacket.NewPacket(testMalformed, LayerTypeETLS, testETLSDecodeOptions) + if p.ErrorLayer() == nil { + t.Error("No Decoding Error when parsing a malformed data") + } +} + +func TestParseETLSTooShort(t *testing.T) { + p := gopacket.NewPacket(testMalformed[0:2], LayerTypeETLS, testETLSDecodeOptions) + if p.ErrorLayer() == nil { + t.Error("No Decoding Error when parsing a malformed data") + } +} + +func TestParseETLSLengthMismatch(t *testing.T) { + var testLengthMismatch = make([]byte, len(testDoubleAppData)) + copy(testLengthMismatch, testDoubleAppData) + testLengthMismatch[3] = 0xFF + testLengthMismatch[4] = 0xFF + p := gopacket.NewPacket(testLengthMismatch, LayerTypeETLS, testETLSDecodeOptions) + if p.ErrorLayer() == nil { + t.Error("No Decoding Error when parsing a malformed data") + } +} + +func TestParseETLSAlertEncrypted(t *testing.T) { + p := gopacket.NewPacket(testAlertEncrypted, LayerTypeETLS, testETLSDecodeOptions) + if p.ErrorLayer() != nil { + t.Error("Failed to decode packet:", p.ErrorLayer().Error()) + } + checkLayers(p, []gopacket.LayerType{LayerTypeETLS}, t) + + if got, ok := p.Layer(LayerTypeETLS).(*ETLS); ok { + want := testAlertEncryptedDecoded + if !reflect.DeepEqual(got, want) { + t.Errorf("ETLS ETLSAlert packet processing failed:\ngot:\n%#v\n\nwant:\n%#v\n\n", got, want) + } + } else { + t.Error("No ETLS layer type found in packet") + } +}