129 lines
3.6 KiB
Go
129 lines
3.6 KiB
Go
// SPDX-FileCopyrightText: 2023 nervuri <https://nervuri.net/contact>
|
|
//
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package clienthello
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/sha256"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func toString[N uint16 | uint8](val N) string {
|
|
return strconv.FormatUint(uint64(val), 10)
|
|
}
|
|
|
|
func deGREASE[N uint16 | uint8](val N, replace bool) string {
|
|
if isGREASE(val) {
|
|
if replace {
|
|
switch any(val).(type) {
|
|
case uint16:
|
|
return "2570-" // generic 16-bit GREASE code 0x0A0A (2570)
|
|
default: // uint8
|
|
return "11-" // generic 8-bit GREASE code 0x0B (11)
|
|
}
|
|
} else {
|
|
return ""
|
|
}
|
|
} else {
|
|
return toString(val) + "-"
|
|
}
|
|
}
|
|
|
|
func (m *ClientHelloMsg) ja3() {
|
|
var codeGroups [5]string
|
|
codeGroups[0] = toString(m.TLSVersion.(uint16))
|
|
for _, cs := range m.CipherSuites {
|
|
codeGroups[1] += deGREASE(cs.(uint16), false)
|
|
}
|
|
for _, e := range m.Extensions {
|
|
codeGroups[2] += deGREASE(e.Code, false)
|
|
switch e.Code {
|
|
case extensionSupportedGroups:
|
|
for _, g := range e.Data.SupportedGroups {
|
|
codeGroups[3] += deGREASE(g.(uint16), false)
|
|
}
|
|
case extensionSupportedPointFormats:
|
|
for _, pf := range e.Data.SupportedPointFormats {
|
|
codeGroups[4] += toString(pf.(uint8)) + "-"
|
|
}
|
|
}
|
|
}
|
|
for i, cg := range codeGroups {
|
|
codeGroups[i] = strings.TrimSuffix(cg, "-")
|
|
}
|
|
m.Highlights.JA3 = strings.Join(codeGroups[:], ",")
|
|
hash := md5.Sum([]byte(m.Highlights.JA3))
|
|
m.Highlights.JA3MD5 = hash[:]
|
|
}
|
|
|
|
func (m *ClientHelloMsg) nja3() {
|
|
const genericGREASECode16 = uint16(0x0a0a) // 2570
|
|
var codeGroups [10]string
|
|
var extCodes []uint16
|
|
codeGroups[0] = toString(m.RecordHeaderTLSVersion.(uint16))
|
|
codeGroups[1] = toString(m.TLSVersion.(uint16))
|
|
for _, cs := range m.CipherSuites {
|
|
codeGroups[2] += deGREASE(cs.(uint16), true)
|
|
}
|
|
for _, e := range m.Extensions {
|
|
// Ignore conditional extensions.
|
|
if e.Code == extensionServerName ||
|
|
e.Code == extensionPadding ||
|
|
e.Code == extensionPreSharedKey ||
|
|
e.Code == extensionSessionTicket || // Firefox
|
|
e.Code == extensionALPN || // iTunes
|
|
e.Code == extensionNextProtoNeg || // iTunes
|
|
e.Code == extensionTokenBinding || // Edge
|
|
e.Code == extensionChannelID || // Chrome
|
|
e.Code == extensionChannelIDOld {
|
|
continue
|
|
}
|
|
if isGREASE(e.Code) {
|
|
e.Code = genericGREASECode16
|
|
}
|
|
extCodes = append(extCodes, e.Code)
|
|
switch e.Code {
|
|
case extensionSupportedGroups:
|
|
for _, g := range e.Data.SupportedGroups {
|
|
codeGroups[4] += deGREASE(g.(uint16), true)
|
|
}
|
|
case extensionSupportedPointFormats:
|
|
for _, pf := range e.Data.SupportedPointFormats {
|
|
codeGroups[5] += toString(pf.(uint8)) + "-"
|
|
}
|
|
case extensionSupportedVersions:
|
|
for _, v := range e.Data.SupportedVersions {
|
|
codeGroups[6] += deGREASE(v.(uint16), true)
|
|
}
|
|
case extensionSignatureAlgorithms:
|
|
for _, sa := range e.Data.SupportedSignatureAlgorithms {
|
|
codeGroups[7] += deGREASE(sa.(uint16), true)
|
|
}
|
|
case extensionPSKModes:
|
|
for _, mode := range e.Data.PSKModes {
|
|
codeGroups[8] += deGREASE(mode.(uint8), true)
|
|
}
|
|
case extensionCompressCertificate:
|
|
for _, algo := range e.Data.CertificateCompressionAlgos {
|
|
codeGroups[9] += toString(algo.(uint16)) + "-"
|
|
}
|
|
}
|
|
}
|
|
// Sort extension codes.
|
|
sort.Slice(extCodes, func(i, j int) bool { return extCodes[i] < extCodes[j] })
|
|
// Add sorted extension codes to NJA3 string.
|
|
for _, code := range extCodes {
|
|
codeGroups[3] += deGREASE(code, true)
|
|
}
|
|
for i, cg := range codeGroups {
|
|
codeGroups[i] = strings.TrimSuffix(cg, "-")
|
|
}
|
|
m.Highlights.NJA3v1 = strings.Join(codeGroups[:], ",")
|
|
hash := sha256.Sum256([]byte(m.Highlights.NJA3v1))
|
|
m.Highlights.NJA3v1Hash = hash[:16]
|
|
}
|