-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathja4plus.go
More file actions
207 lines (188 loc) · 5.84 KB
/
ja4plus.go
File metadata and controls
207 lines (188 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package ja4plus
import (
"crypto/sha256"
"crypto/tls"
"encoding/binary"
"encoding/hex"
"slices"
)
// greaseFilter returns true if the provided value is a GREASE entry as defined in
// https://www.rfc-editor.org/rfc/rfc8701.html
func greaseFilter(suite uint16) bool {
return suite&0x000F == 0x000A && // low word is 0x*A
suite>>8 == (suite&0x00FF) // high word is equal to low word
}
// JA4 generates a JA4 fingerprint from the given [tls.ClientHelloInfo].
// It extracts TLS Version, Cipher Suites, Extensions, and ALPN Protocols.
func JA4(hello *tls.ClientHelloInfo) string {
out := make([]byte, 0, 36)
// Determine protocol type based on the network type
if hello.Conn != nil {
switch hello.Conn.LocalAddr().Network() {
case "udp", "sctp":
out = append(out, 'd')
case "quic":
out = append(out, 'q')
default:
out = append(out, 't')
}
} else {
out = append(out, 't')
}
// Extract TLS version
var (
maxVersion uint16
hasVersion bool
)
for _, version := range hello.SupportedVersions {
if greaseFilter(version) {
continue
}
if !hasVersion || version > maxVersion {
maxVersion = version
hasVersion = true
}
}
if !hasVersion {
out = append(out, '0', '0')
} else {
switch maxVersion {
case tls.VersionTLS10:
out = append(out, '1', '0')
case tls.VersionTLS11:
out = append(out, '1', '1')
case tls.VersionTLS12:
out = append(out, '1', '2')
case tls.VersionTLS13:
out = append(out, '1', '3')
case tls.VersionSSL30: // deprecated, but still seen in the wild
out = append(out, 's', '3')
case 0x0002: // unsupported by go; still seen in the wild
out = append(out, 's', '2')
case 0xfeff: // DTLS 1.0
out = append(out, 'd', '1')
case 0xfefd: // DTLS 1.2
out = append(out, 'd', '2')
case 0xfefc: // DTLS 1.3
out = append(out, 'd', '3')
default:
out = append(out, '0', '0')
}
}
// Check for presence of SNI
if hello.ServerName != "" {
out = append(out, 'd')
} else {
out = append(out, 'i')
}
// Count cipher suites; copy to avoid modifying the original
filteredCipherSuites := make([]uint16, 0, len(hello.CipherSuites))
for _, suite := range hello.CipherSuites {
if !greaseFilter(suite) {
filteredCipherSuites = append(filteredCipherSuites, suite)
}
}
cipherCount := min(len(filteredCipherSuites), 99)
out = appendTwoDigits(out, cipherCount)
// Count extensions; copy to avoid modifying the original
filteredExtensions := make([]uint16, 0, len(hello.Extensions))
for _, ext := range hello.Extensions {
if !greaseFilter(ext) {
filteredExtensions = append(filteredExtensions, ext)
}
}
extensionCount := min(len(filteredExtensions), 99)
out = appendTwoDigits(out, extensionCount)
// Extract first ALPN value
var firstALPN string
for _, proto := range hello.SupportedProtos {
// Protocols are tecnically strings, but grease values are 2-byte non-printable, so we convert.
// see: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
if len(proto) >= 2 && !greaseFilter(binary.BigEndian.Uint16([]byte(proto[:2]))) {
firstALPN = proto
break
}
}
if firstALPN != "" {
out = append(out, firstALPN[0], firstALPN[len(firstALPN)-1])
} else {
out = append(out, '0', '0')
}
out = append(out, '_')
ciphersHash := cipherSuiteHash(filteredCipherSuites)
out = hex.AppendEncode(out, ciphersHash[:])
out = append(out, '_')
extensionsHash := extensionHash(filteredExtensions, hello.SignatureSchemes)
out = hex.AppendEncode(out, extensionsHash[:])
return string(out)
}
// cipherSuiteHash computes the truncated SHA256 of sorted cipher suites.
// The input must be filtered for GREASE values.
// The return value is an unencoded byte array of the hash.
func cipherSuiteHash(filteredCipherSuites []uint16) [6]byte {
if len(filteredCipherSuites) == 0 {
return [6]byte{}
}
slices.Sort(filteredCipherSuites)
cipherSuiteList := make([]byte, 0, len(filteredCipherSuites) /* 4 chars + comma */ *5 /* last comma */ -1)
for i, suite := range filteredCipherSuites {
if i > 0 {
cipherSuiteList = append(cipherSuiteList, ',')
}
cipherSuiteList = appendHexUint16(cipherSuiteList, suite)
}
cipherSuiteHash := sha256.Sum256(cipherSuiteList)
var truncated [6]byte
copy(truncated[:], cipherSuiteHash[:6])
return truncated
}
// extensionHash computes the truncated SHA256 of sorted and filtered extensions and unsorted signature algorithms.
// The provided extensions must be filtered for GREASE values.
// It sorts the provided extensions in-place.
// The return value is an unencoded byte array of the hash.
func extensionHash(filteredExtensions []uint16, signatureSchemes []tls.SignatureScheme) [6]byte {
slices.Sort(filteredExtensions)
extensionsList := make([]byte, 0, len(filteredExtensions)*5+len(signatureSchemes)*5+1)
for _, ext := range filteredExtensions {
// SNI and ALPN are counted above, but MUST be ignored for the hash.
if ext == 0x0000 /* SNI */ || ext == 0x0010 /* ALPN */ {
continue
}
if len(extensionsList) > 0 {
extensionsList = append(extensionsList, ',')
}
extensionsList = appendHexUint16(extensionsList, ext)
}
if len(extensionsList) == 0 {
return [6]byte{}
}
hasSignature := false
for _, sig := range signatureSchemes {
if greaseFilter(uint16(sig)) {
continue
}
if !hasSignature {
extensionsList = append(extensionsList, '_')
hasSignature = true
} else {
extensionsList = append(extensionsList, ',')
}
extensionsList = appendHexUint16(extensionsList, uint16(sig))
}
extensionsHash := sha256.Sum256(extensionsList)
var truncated [6]byte
copy(truncated[:], extensionsHash[:6])
return truncated
}
func appendTwoDigits(dst []byte, v int) []byte {
return append(dst, byte('0'+v/10), byte('0'+v%10))
}
func appendHexUint16(dst []byte, v uint16) []byte {
const hex = "0123456789abcdef"
return append(dst,
hex[v>>12],
hex[(v>>8)&0xF],
hex[(v>>4)&0xF],
hex[v&0xF],
)
}