// SPDX-License-Identifier: BSD-3-Clause // Based on Go's crypto/tls library: // https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L69 // https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/crypto/tls/handshake_messages.go;l=69 package main import ( "crypto/md5" "encoding/binary" "encoding/hex" "encoding/json" "golang.org/x/crypto/cryptobyte" "log" "strconv" "strings" ) // TLS versions const ( versionSSL30 = 0x0300 // 768 versionTLS10 = 0x0301 // 769 versionTLS11 = 0x0302 // 770 versionTLS12 = 0x0303 // 771 versionTLS13 = 0x0304 // 772 ) // TLS compression types const ( compressionNone uint8 = 0 ) // TLS extension numbers const ( extensionServerName uint16 = 0 extensionStatusRequest uint16 = 5 extensionSupportedGroups uint16 = 10 // elliptic_curves in TLS versions prior to 1.3, see RFC 8446, Section 4.2.7 extensionSupportedPointFormats uint16 = 11 extensionSignatureAlgorithms uint16 = 13 extensionALPN uint16 = 16 extensionSCT uint16 = 18 extensionPadding uint16 = 21 extensionEncryptThenMac uint16 = 22 extensionExtendedMasterSecret uint16 = 23 extensionCompressCertificate uint16 = 27 extensionSessionTicket uint16 = 35 extensionPreSharedKey uint16 = 41 extensionEarlyData uint16 = 42 extensionSupportedVersions uint16 = 43 extensionCookie uint16 = 44 extensionPSKModes uint16 = 45 extensionCertificateAuthorities uint16 = 47 extensionPostHandshakeAuth uint16 = 49 extensionSignatureAlgorithmsCert uint16 = 50 extensionKeyShare uint16 = 51 extensionNextProtoNeg uint16 = 13172 // not IANA assigned extensionApplicationSettings uint16 = 17513 // not IANA assigned extensionRenegotiationInfo uint16 = 0xff01 ) // TLS signaling cipher suite values const ( scsvRenegotiation uint16 = 0x00ff ) // TLS CertificateStatusType (RFC 3546) const ( statusTypeOCSP uint8 = 1 ) // curveID is the type of a TLS identifier for an elliptic curve. See // https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8. // // In TLS 1.3, this type is called NamedGroup, but at this time this library // only supports Elliptic Curve based groups. See RFC 8446, Section 4.2.7. type curveID uint16 const ( curveP256 curveID = 23 curveP384 curveID = 24 curveP521 curveID = 25 x25519 curveID = 29 ) // TLS 1.3 Key Share. See RFC 8446, Section 4.2.8. type keyShare struct { Group curveID `json:"group"` Data byteSlice `json:"data"` } // TLS 1.3 PSK Identity. Can be a Session Ticket, or a reference to a saved // session. See RFC 8446, Section 4.2.11. type pskIdentity struct { Identity byteSlice `json:"identity"` ObfuscatedTicketAge uint32 `json:"obfuscated_ticket_age"` } // signatureScheme identifies a signature algorithm supported by TLS. See // RFC 8446, Section 4.2.3. type signatureScheme uint16 const ( // RSASSA-PKCS1-v1_5 algorithms. PKCS1WithSHA256 signatureScheme = 0x0401 PKCS1WithSHA384 signatureScheme = 0x0501 PKCS1WithSHA512 signatureScheme = 0x0601 // RSASSA-PSS algorithms with public key OID rsaEncryption. PSSWithSHA256 signatureScheme = 0x0804 PSSWithSHA384 signatureScheme = 0x0805 PSSWithSHA512 signatureScheme = 0x0806 // ECDSA algorithms. Only constrained to a specific curve in TLS 1.3. ECDSAWithP256AndSHA256 signatureScheme = 0x0403 ECDSAWithP384AndSHA384 signatureScheme = 0x0503 ECDSAWithP521AndSHA512 signatureScheme = 0x0603 // Legacy signature and hash algorithms for TLS 1.2. PKCS1WithSHA1 signatureScheme = 0x0201 ECDSAWithSHA1 signatureScheme = 0x0203 ) // Check if this is a GREASE value (RFC 8701) func isGREASE(value uint16) bool { // Values for: cipher suites, ALPN, // extensions, named groups, signature algorithms and TLS versions: greaseValues := [16]uint16{ 0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa, } // Values for PSK key exchange modes: //greasePSKModes := [8]uint8{ // 0x0b, 0x2a, 0x49, 0x68, 0x87, 0xa6, 0xc5, 0xe4, //} // Run the check for _, greaseValue := range greaseValues { if value == greaseValue { return true } } return false } // readUint8LengthPrefixed acts like s.ReadUint8LengthPrefixed, but targets a // []byte instead of a cryptobyte.String. func readUint8LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { return s.ReadUint8LengthPrefixed((*cryptobyte.String)(out)) } // readUint16LengthPrefixed acts like s.ReadUint16LengthPrefixed, but targets a // []byte instead of a cryptobyte.String. func readUint16LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { return s.ReadUint16LengthPrefixed((*cryptobyte.String)(out)) } type byteSlice []byte func (bs byteSlice) MarshalText() ([]byte, error) { // Used in json.Marshal() to convert []byte to hex string. return []byte(hex.EncodeToString(bs)), nil } type uint8Slice []uint8 func (numbers uint8Slice) MarshalJSON() ([]byte, error) { // Used in json.Marshal() to convert []uint8 to array. newSlice := make([]uint, len(numbers)) for i, n := range numbers { newSlice[i] = uint(n) } return json.Marshal(newSlice) } type extensionData struct { Raw byteSlice `json:"raw"` ServerName string `json:"server_name,omitempty"` StatusType uint8 `json:"status_type,omitempty"` SupportedGroups []curveID `json:"supported_groups,omitempty"` SupportedPointFormats uint8Slice `json:"supported_point_formats,omitempty"` SupportedSignatureAlgorithms []signatureScheme `json:"supported_signature_algorithms,omitempty"` RenegotiationInfo []byte `json:"renegotiation_info,omitempty"` AlpnProtocols []string `json:"alpn_protocols,omitempty"` SupportedVersions []uint16 `json:"supported_tls_versions,omitempty"` Cookie byteSlice `json:"cookie,omitempty"` KeyShares []keyShare `json:"key_shares,omitempty"` PskModes uint8Slice `json:"psk_modes,omitempty"` PskIdentities []pskIdentity `json:"psk_identities,omitempty"` PskBinders []byteSlice `json:"psk_binders,omitempty"` Length uint16 `json:"length,omitempty"` // padding } type extension struct { Code uint16 `json:"code"` Name string `json:"name"` Data extensionData `json:"data"` } type highlights struct { //SupportedTLSVersions []uint16 GmtUnixTime uint32 `json:"gmt_unix_time"` // first 4 bytes of client random SecureRenegotiationSupport bool `json:"secure_renegotiation_support"` OcspStaplingSupport bool `json:"ocsp_stapling_support"` SctSupport bool `json:"sct_support"` // Go's crypto/tls server does not support early data. EarlyData bool `json:"-"` // don't include in JSON JA3 string `json:"ja3"` JA3MD5 byteSlice `json:"ja3_md5"` } type clientHelloMsg struct { Raw byteSlice `json:"raw"` RecordHeaderTLSVersion uint16 `json:"record_header_tls_version"` // TLSv1.0 (769) TLSVersion uint16 `json:"client_tls_version"` // TLSv1.2 (771) Random byteSlice `json:"random"` SessionID byteSlice `json:"session_id"` CipherSuites []uint16 `json:"cipher_suites"` CompressionMethods uint8Slice `json:"compression_methods"` Extensions []extension `json:"extensions"` Highlights highlights `json:"highlights"` } func (m *clientHelloMsg) unmarshal(data []byte) bool { *m = clientHelloMsg{Raw: data} s := cryptobyte.String(data) var random []byte var sessionID []byte if !s.Skip(1) || !s.ReadUint16(&m.RecordHeaderTLSVersion) || !s.Skip(2) || !s.Skip(4) || // message type and uint24 length field !s.ReadUint16(&m.TLSVersion) || !s.ReadBytes(&random, 32) || !readUint8LengthPrefixed(&s, &sessionID) { return false } m.Random = random m.Highlights.GmtUnixTime = binary.BigEndian.Uint32(random[0:4]) m.SessionID = sessionID var cipherSuites cryptobyte.String if !s.ReadUint16LengthPrefixed(&cipherSuites) { return false } m.CipherSuites = []uint16{} m.Highlights.SecureRenegotiationSupport = false for !cipherSuites.Empty() { var suite uint16 if !cipherSuites.ReadUint16(&suite) { return false } if suite == scsvRenegotiation { m.Highlights.SecureRenegotiationSupport = true } m.CipherSuites = append(m.CipherSuites, suite) } var compressionMethods []uint8 if !readUint8LengthPrefixed(&s, &compressionMethods) { return false } m.CompressionMethods = compressionMethods if s.Empty() { // ClientHello is optionally followed by extension data return true } var extensions cryptobyte.String if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() { return false } for !extensions.Empty() { var extension extension var extData cryptobyte.String if !extensions.ReadUint16(&extension.Code) || !extensions.ReadUint16LengthPrefixed(&extData) { return false } extension.Data.Raw = []byte(extData) switch extension.Code { case extensionServerName: // RFC 6066, Section 3 extension.Name = "server_name" // Server Name Indication var nameList cryptobyte.String if !extData.ReadUint16LengthPrefixed(&nameList) || nameList.Empty() { return false } for !nameList.Empty() { var nameType uint8 var serverName cryptobyte.String if !nameList.ReadUint8(&nameType) || !nameList.ReadUint16LengthPrefixed(&serverName) || serverName.Empty() { return false } if nameType != 0 { continue } if len(extension.Data.ServerName) != 0 { // Multiple names of the same name_type are prohibited. return false } sn := string(serverName) extension.Data.ServerName = sn // An SNI value may not include a trailing dot. if strings.HasSuffix(sn, ".") { return false } } case extensionNextProtoNeg: // draft-agl-tls-nextprotoneg-04 extension.Name = "next_protocol_negotiation" // Next Protocol Negotiation case extensionStatusRequest: // RFC 4366, Section 3.6 extension.Name = "status_request" // Certificate Status Request var ignored cryptobyte.String if !extData.ReadUint8(&extension.Data.StatusType) || !extData.ReadUint16LengthPrefixed(&ignored) || // responder_id_list !extData.ReadUint16LengthPrefixed(&ignored) { // request_extensions return false } m.Highlights.OcspStaplingSupport = extension.Data.StatusType == statusTypeOCSP case extensionSupportedGroups: // RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7 extension.Name = "supported_groups" // Supported Groups //extension.Name = "elliptic_curves" // old name var curves cryptobyte.String if !extData.ReadUint16LengthPrefixed(&curves) || curves.Empty() { return false } extension.Data.SupportedGroups = make([]curveID, 0) for !curves.Empty() { var curve uint16 if !curves.ReadUint16(&curve) { return false } extension.Data.SupportedGroups = append( extension.Data.SupportedGroups, curveID(curve)) } case extensionSupportedPointFormats: // RFC 4492, Section 5.1.2 extension.Name = "ec_point_formats" // Supported Point Formats var supportedPointFormats []uint8 if !readUint8LengthPrefixed(&extData, &supportedPointFormats) || len(supportedPointFormats) == 0 { return false } extension.Data.SupportedPointFormats = supportedPointFormats case extensionSessionTicket: // RFC 5077, Section 3.2 extension.Name = "session_ticket" // Session Ticket case extensionSignatureAlgorithms: // RFC 5246, Section 7.4.1.4.1 extension.Name = "signature_algorithms" // Signature Algorithms var sigAndAlgs cryptobyte.String if !extData.ReadUint16LengthPrefixed(&sigAndAlgs) || sigAndAlgs.Empty() { return false } for !sigAndAlgs.Empty() { var sigAndAlg uint16 if !sigAndAlgs.ReadUint16(&sigAndAlg) { return false } extension.Data.SupportedSignatureAlgorithms = append( extension.Data.SupportedSignatureAlgorithms, signatureScheme(sigAndAlg)) } case extensionSignatureAlgorithmsCert: // RFC 8446, Section 4.2.3 extension.Name = "signature_algorithms_cert" // Signature Algorithms Cert var sigAndAlgs cryptobyte.String if !extData.ReadUint16LengthPrefixed(&sigAndAlgs) || sigAndAlgs.Empty() { return false } for !sigAndAlgs.Empty() { var sigAndAlg uint16 if !sigAndAlgs.ReadUint16(&sigAndAlg) { return false } extension.Data.SupportedSignatureAlgorithms = append( extension.Data.SupportedSignatureAlgorithms, signatureScheme(sigAndAlg)) } case extensionRenegotiationInfo: // RFC 5746, Section 3.2 extension.Name = "renegotiation_info" // Renegotiation Indication if !readUint8LengthPrefixed( &extData, &extension.Data.RenegotiationInfo) { return false } m.Highlights.SecureRenegotiationSupport = true case extensionALPN: // RFC 7301, Section 3.1 extension.Name = "application_layer_protocol_negotiation" // Application-Layer Protocol Negotiation var protoList cryptobyte.String if !extData.ReadUint16LengthPrefixed(&protoList) || protoList.Empty() { return false } for !protoList.Empty() { var proto cryptobyte.String if !protoList.ReadUint8LengthPrefixed(&proto) || proto.Empty() { return false } extension.Data.AlpnProtocols = append(extension.Data.AlpnProtocols, string(proto)) } case extensionSCT: // RFC 6962, Section 3.3.1 extension.Name = "signed_certificate_timestamp" // Signed Certificate Timestamp m.Highlights.SctSupport = true case extensionSupportedVersions: // RFC 8446, Section 4.2.1 extension.Name = "supported_versions" // Supported Versions var versList cryptobyte.String if !extData.ReadUint8LengthPrefixed(&versList) || versList.Empty() { return false } for !versList.Empty() { var vers uint16 if !versList.ReadUint16(&vers) { return false } extension.Data.SupportedVersions = append(extension.Data.SupportedVersions, vers) } case extensionCookie: // RFC 8446, Section 4.2.2 extension.Name = "cookie" // Cookie var cookie []byte if !readUint16LengthPrefixed(&extData, &cookie) || len(extension.Data.Cookie) == 0 { return false } extension.Data.Cookie = cookie case extensionKeyShare: // RFC 8446, Section 4.2.8 extension.Name = "key_share" // Key Share var clientShares cryptobyte.String if !extData.ReadUint16LengthPrefixed(&clientShares) { return false } for !clientShares.Empty() { var ks keyShare var ksData []byte if !clientShares.ReadUint16((*uint16)(&ks.Group)) || !readUint16LengthPrefixed(&clientShares, &ksData) || len(ksData) == 0 { return false } ks.Data = ksData extension.Data.KeyShares = append(extension.Data.KeyShares, ks) } case extensionEarlyData: // RFC 8446, Section 4.2.10 extension.Name = "early_data" // Early Data Indication m.Highlights.EarlyData = true case extensionPSKModes: // RFC 8446, Section 4.2.9 extension.Name = "psk_key_exchange_modes" // Pre-Shared Key Exchange Modes var pskModes []uint8 if !readUint8LengthPrefixed(&extData, &pskModes) { return false } extension.Data.PskModes = pskModes case extensionPreSharedKey: // RFC 8446, Section 4.2.11 extension.Name = "pre_shared_key" // Pre-Shared Key if !extensions.Empty() { return false // pre_shared_key must be the last extension } var identities cryptobyte.String if !extData.ReadUint16LengthPrefixed(&identities) || identities.Empty() { return false } for !identities.Empty() { var psk pskIdentity var identity []byte if !readUint16LengthPrefixed(&identities, &identity) || !identities.ReadUint32(&psk.ObfuscatedTicketAge) || len(identity) == 0 { return false } psk.Identity = identity extension.Data.PskIdentities = append(extension.Data.PskIdentities, psk) } var binders cryptobyte.String if !extData.ReadUint16LengthPrefixed(&binders) || binders.Empty() { return false } for !binders.Empty() { var binder []byte if !readUint8LengthPrefixed(&binders, &binder) || len(binder) == 0 { return false } extension.Data.PskBinders = append(extension.Data.PskBinders, binder) } case extensionPadding: // RFC 7685 extension.Name = "padding" // Padding extension.Data.Length = uint16(len(extData)) case extensionEncryptThenMac: // RFC 7366 extension.Name = "encrypt_then_mac" // Encrypt-then-MAC" case extensionExtendedMasterSecret: // RFC 7627 extension.Name = "extended_master_secret" // Extended Master Secret case extensionPostHandshakeAuth: // RFC 8446, Section 4.2.6 extension.Name = "post_handshake_auth" // Post-Handshake Client Authentication case extensionCompressCertificate: // RFC 8879 extension.Name = "compress_certificate" // Certificate Compression // TODO: parse - https://www.rfc-editor.org/rfc/rfc8879.html case extensionApplicationSettings: // draft-vvv-tls-alps // Application-Layer Protocol Settings extension.Name = " application_settings" // TODO: parse - https://datatracker.ietf.org/doc/html/draft-vvv-tls-alps default: // Check if this is a GREASE extension (RFC 8701) if isGREASE(extension.Code) { extension.Name = "GREASE" } else { log.Println("Unknown extension:", extension.Code) } } m.Extensions = append(m.Extensions, extension) if !extData.Empty() && extension.Name != "GREASE" && extension.Code != extensionPadding { log.Printf("Extension %d data not read.\n", extension.Code) } } // JA3 var ja3 strings.Builder var supportedGroups []curveID var supportedPointFormats uint8Slice ja3.WriteString(strconv.FormatUint(uint64(m.TLSVersion), 10) + ",") for i, cs := range m.CipherSuites { if !isGREASE(cs) { // ignore GREASE values ja3.WriteString(strconv.FormatUint(uint64(cs), 10)) if i+1 != len(m.CipherSuites) { ja3.WriteString("-") } } } ja3.WriteString(",") for i, e := range m.Extensions { if !isGREASE(e.Code) { // ignore GREASE values ja3.WriteString(strconv.FormatUint(uint64(e.Code), 10)) if i+1 != len(m.Extensions) { ja3.WriteString("-") } if e.Code == extensionSupportedGroups { supportedGroups = e.Data.SupportedGroups } else if e.Code == extensionSupportedPointFormats { supportedPointFormats = e.Data.SupportedPointFormats } } } ja3.WriteString(",") for i, g := range supportedGroups { if !isGREASE(uint16(g)) { // ignore GREASE values ja3.WriteString(strconv.FormatUint(uint64(g), 10)) if i+1 != len(supportedGroups) { ja3.WriteString("-") } } } ja3.WriteString(",") for i, pf := range supportedPointFormats { if !isGREASE(uint16(pf)) { // ignore GREASE values ja3.WriteString(strconv.FormatUint(uint64(pf), 10)) if i+1 != len(supportedPointFormats) { ja3.WriteString("-") } } } m.Highlights.JA3 = ja3.String() hash := md5.Sum([]byte(m.Highlights.JA3)) m.Highlights.JA3MD5 = hash[:] return true }