client-hello-mirror/clienthello/fingerprint.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]
}