Files
Ronni Skansing 07c8adaf76 update vendor deps
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2025-11-06 23:31:08 +01:00

352 lines
13 KiB
Go

package clienthellod
import (
"crypto/sha1" // skipcq: GSC-G505
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"sort"
"github.com/gaukas/clienthellod/internal/utils"
"github.com/gaukas/godicttls"
tls "github.com/refraction-networking/utls"
"golang.org/x/crypto/cryptobyte"
)
type ClientHello struct {
raw []byte
TLSRecordVersion uint16 `json:"tls_record_version"` // TLS record version (major, minor)
TLSHandshakeVersion uint16 `json:"tls_handshake_version"` // TLS handshake version (major, minor)
CipherSuites []uint16 `json:"cipher_suites"`
CompressionMethods utils.Uint8Arr `json:"compression_methods"`
Extensions []uint16 `json:"extensions"` // extension IDs in original order
ExtensionsNormalized []uint16 `json:"extensions_normalized"` // sorted extension IDs
ServerName string `json:"server_name"` // server_name(0)
NamedGroupList []uint16 `json:"supported_groups"` // supported_groups(10) a.k.a elliptic_curves
ECPointFormatList utils.Uint8Arr `json:"ec_point_formats"` // ec_point_formats(11)
SignatureSchemeList []uint16 `json:"signature_algorithms"` // signature_algorithms(13)
ALPN []string `json:"alpn"` // alpn(16)
CertCompressAlgo []uint16 `json:"compress_certificate"` // compress_certificate(27)
RecordSizeLimit utils.Uint8Arr `json:"record_size_limit"` // record_size_limit(28)
SupportedVersions []uint16 `json:"supported_versions"` // supported_versions(43)
PSKKeyExchangeModes utils.Uint8Arr `json:"psk_key_exchange_modes"` // psk_key_exchange_modes(45)
KeyShare []uint16 `json:"key_share"` // key_share(51)
ApplicationSettings []string `json:"application_settings"` // application_settings(17513) a.k.a ALPS
UserAgent string `json:"user_agent,omitempty"` // User-Agent header, set by the caller
NID int64 `json:"nid,omitempty"` // NID of the fingerprint
NormNID int64 `json:"norm_nid,omitempty"` // Normalized NID of the fingerprint
ID string `json:"id,omitempty"` // ID of the fingerprint (hex string)
NormID string `json:"norm_id,omitempty"` // Normalized ID of the fingerprint (hex string)
// below are ONLY used for calculating the fingerprint (hash)
lengthPrefixedSupportedGroups []uint16
lengthPrefixedEcPointFormats []uint8
lengthPrefixedSignatureAlgos []uint16
alpnWithLengths []uint8
lengthPrefixedCertCompressAlgos []uint8
keyshareGroupsWithLengths []uint16
// _nid int64
// norm_nid int64
// QUIC-only
qtp *QUICTransportParameters
}
// ReadClientHello reads a ClientHello from a connection (io.Reader)
// and returns a ClientHello struct.
//
// It will return an error if the reader does not give a stream of bytes
// representing a valid ClientHello. But all bytes read from the reader
// will be stored in the ClientHello struct to be rewinded by the caller.
func ReadClientHello(r io.Reader) (ch *ClientHello, err error) {
ch = new(ClientHello)
// Read a TLS record
// Read exactly 5 bytes from the reader
ch.raw = make([]byte, 5)
if _, err = io.ReadFull(r, ch.raw); err != nil {
return
}
// Check if the first byte is 0x16 (TLS Handshake)
if ch.raw[0] != 0x16 {
err = errors.New("not a TLS handshake record")
return
}
// Read exactly length bytes from the reader
ch.raw = append(ch.raw, make([]byte, binary.BigEndian.Uint16(ch.raw[3:5]))...)
_, err = io.ReadFull(r, ch.raw[5:])
return
}
func (ch *ClientHello) Raw() []byte {
return ch.raw
}
// ParseClientHello parses the raw bytes of a ClientHello into a ClientHello struct.
func (ch *ClientHello) ParseClientHello() error {
// Call uTLS to parse the raw bytes into ClientHelloSpec
fingerprinter := tls.Fingerprinter{
AllowBluntMimicry: true, // we will need all the extensions even when not recognized
}
chs, err := fingerprinter.RawClientHello(ch.raw)
if err != nil {
return fmt.Errorf("failed to parse ClientHello, (*tls.Fingerprinter).RawClientHello(): %w", err)
}
// ch.TLSRecordVersion = chs.TLSVersMin // won't work for TLS 1.3
// ch.TLSHandshakeVersion = chs.TLSVersMax // won't work for TLS 1.3
ch.CipherSuites = chs.CipherSuites
ch.CompressionMethods = chs.CompressionMethods
// parse extensions
ch.parseExtensions(chs)
// Call uTLS to parse the raw bytes into ClientHelloMsg
chm := tls.UnmarshalClientHello(ch.raw[5:])
if chm == nil {
return errors.New("failed to parse ClientHello, (*tls.ClientHelloInfo).Unmarshal(): nil")
}
ch.ServerName = chm.ServerName
// In the end parse extra information from raw
return ch.parseExtra()
}
func (ch *ClientHello) parseExtensions(chs *tls.ClientHelloSpec) {
for _, ext := range chs.Extensions {
switch ext := ext.(type) {
case *tls.SupportedCurvesExtension:
for _, curve := range ext.Curves {
ch.NamedGroupList = append(ch.NamedGroupList, uint16(curve))
}
ch.lengthPrefixedSupportedGroups = append(ch.lengthPrefixedSupportedGroups, 2*uint16(len(ch.NamedGroupList)))
ch.lengthPrefixedSupportedGroups = append(ch.lengthPrefixedSupportedGroups, ch.NamedGroupList...)
case *tls.SupportedPointsExtension:
ch.ECPointFormatList = ext.SupportedPoints
ch.lengthPrefixedEcPointFormats = append(ch.lengthPrefixedEcPointFormats, uint8(len(ext.SupportedPoints)))
ch.lengthPrefixedEcPointFormats = append(ch.lengthPrefixedEcPointFormats, ext.SupportedPoints...)
case *tls.SignatureAlgorithmsExtension:
for _, sig := range ext.SupportedSignatureAlgorithms {
ch.SignatureSchemeList = append(ch.SignatureSchemeList, uint16(sig))
}
ch.lengthPrefixedSignatureAlgos = append(ch.lengthPrefixedSignatureAlgos, 2*uint16(len(ch.SignatureSchemeList)))
ch.lengthPrefixedSignatureAlgos = append(ch.lengthPrefixedSignatureAlgos, ch.SignatureSchemeList...)
case *tls.ALPNExtension:
ch.ALPN = ext.AlpnProtocols
// we will get alpnWithLengths from raw
case *tls.UtlsCompressCertExtension:
for _, algo := range ext.Algorithms {
ch.CertCompressAlgo = append(ch.CertCompressAlgo, uint16(algo))
}
ch.lengthPrefixedCertCompressAlgos = append(ch.lengthPrefixedCertCompressAlgos, 2*uint8(len(ch.CertCompressAlgo)))
ch.lengthPrefixedCertCompressAlgos = append(
ch.lengthPrefixedCertCompressAlgos,
utils.Uint16ToUint8(ch.CertCompressAlgo)...,
)
case *tls.FakeRecordSizeLimitExtension:
ch.RecordSizeLimit = append(ch.RecordSizeLimit, uint8(ext.Limit>>8), uint8(ext.Limit))
case *tls.SupportedVersionsExtension:
for _, ver := range ext.Versions {
ch.SupportedVersions = append(ch.SupportedVersions, uint16(ver))
}
case *tls.PSKKeyExchangeModesExtension:
ch.PSKKeyExchangeModes = ext.Modes
case *tls.KeyShareExtension:
for _, ks := range ext.KeyShares {
ch.KeyShare = append(ch.KeyShare, uint16(ks.Group))
// get below from raw instead
// ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, uint16(ks.Group))
// ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, uint16(len(ks.Data)))
}
case *tls.ApplicationSettingsExtension:
ch.ApplicationSettings = ext.SupportedProtocols
case *tls.GenericExtension:
if ext.Id == godicttls.ExtType_quic_transport_parameters {
ch.qtp = ParseQUICTransportParameters(ext.Data)
}
}
}
}
// parseExtra parses extra information from raw bytes which couldn't be parsed by uTLS.
func (ch *ClientHello) parseExtra() error {
// parse alpnWithLengths and Extensions
s := cryptobyte.String(ch.raw)
var recordVersion uint16
if !s.Skip(1) || !s.ReadUint16(&recordVersion) || !s.Skip(2) { // skip TLS record header
return errors.New("failed to parse TLS header, cryptobyte.String().Skip(): false")
}
ch.TLSRecordVersion = recordVersion
var handshakeVersion uint16
if !s.Skip(1) || // skip Handshake type
!s.Skip(3) || // skip Handshake length
!s.ReadUint16(&handshakeVersion) || // parse ClientHello version
!s.Skip(32) { // skip ClientHello random
return errors.New("failed to parse ClientHello, cryptobyte.String().Skip(): false")
}
ch.TLSHandshakeVersion = handshakeVersion
var ignoredSessionID cryptobyte.String
if !s.ReadUint8LengthPrefixed(&ignoredSessionID) {
return errors.New("unable to read session id")
}
var ignoredCipherSuites cryptobyte.String
if !s.ReadUint16LengthPrefixed(&ignoredCipherSuites) {
return errors.New("unable to read ciphersuites")
}
var ignoredCompressionMethods cryptobyte.String
if !s.ReadUint8LengthPrefixed(&ignoredCompressionMethods) {
return errors.New("unable to read compression methods")
}
if s.Empty() {
return nil // no extensions
}
var extensions cryptobyte.String
if !s.ReadUint16LengthPrefixed(&extensions) {
return errors.New("unable to read extensions data")
}
err := ch.parseExtensionsExtra(extensions)
if err != nil {
return fmt.Errorf("failed to parse extensions, parseExtensionsExtra(): %w", err)
}
// sort ch.Extensions and put result to ch.ExtensionsNormalized
ch.ExtensionsNormalized = make([]uint16, len(ch.Extensions))
copy(ch.ExtensionsNormalized, ch.Extensions)
sort.Slice(ch.ExtensionsNormalized, func(i, j int) bool {
return ch.ExtensionsNormalized[i] < ch.ExtensionsNormalized[j]
})
return nil
}
func (ch *ClientHello) parseExtensionsExtra(extensions cryptobyte.String) error {
var extensionIDs []uint16
for !extensions.Empty() {
var extensionID uint16
var extensionData cryptobyte.String
if !extensions.ReadUint16(&extensionID) {
return errors.New("unable to read extension ID")
}
if !extensions.ReadUint16LengthPrefixed(&extensionData) {
return errors.New("unable to read extension data")
}
extensionID, err := ch.parseExtensionExtra(extensionID, extensionData)
if err != nil {
return fmt.Errorf("failed to parse extension, parseExtensionExtra(): %w", err)
}
extensionIDs = append(extensionIDs, extensionID) // extension ID might need to be overridden by parseExtensionExtra() in case of GREASE
}
ch.Extensions = extensionIDs
return nil
}
func (ch *ClientHello) parseExtensionExtra(extensionID uint16, extensionData cryptobyte.String) (uint16, error) {
switch extensionID {
case 16: // ALPN
ch.alpnWithLengths = extensionData
case 51: // keyshare
if !extensionData.Skip(2) {
return 0, errors.New("unable to skip keyshare total length")
}
for !extensionData.Empty() {
var group uint16
var length uint16
if !extensionData.ReadUint16(&group) || !extensionData.ReadUint16(&length) {
return 0, errors.New("unable to read keyshare group")
}
if utils.IsGREASEUint16(group) {
group = tls.GREASE_PLACEHOLDER
}
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, group)
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, length)
if !extensionData.Skip(int(length)) {
return 0, errors.New("unable to skip keyshare data")
}
}
default:
if utils.IsGREASEUint16(extensionID) {
return tls.GREASE_PLACEHOLDER, nil
}
}
return extensionID, nil
}
// FingerprintNID calculates fingerprint Numerical ID of ClientHello.
// Fingerprint is defined by
func (ch *ClientHello) FingerprintNID(normalized bool) int64 {
if normalized && ch.NormNID != 0 {
return ch.NormNID
}
if !normalized && ch.NID != 0 {
return ch.NID
}
h := sha1.New() // skipcq: GO-S1025, GSC-G401,
binary.Write(h, binary.BigEndian, uint16(ch.TLSRecordVersion))
binary.Write(h, binary.BigEndian, uint16(ch.TLSHandshakeVersion))
updateArr(h, utils.Uint16ToUint8(ch.CipherSuites))
updateArr(h, ch.CompressionMethods)
if normalized {
updateArr(h, utils.Uint16ToUint8(ch.ExtensionsNormalized))
} else {
updateArr(h, utils.Uint16ToUint8(ch.Extensions))
}
updateArr(h, utils.Uint16ToUint8(ch.lengthPrefixedSupportedGroups))
updateArr(h, ch.lengthPrefixedEcPointFormats)
updateArr(h, utils.Uint16ToUint8(ch.lengthPrefixedSignatureAlgos))
updateArr(h, ch.alpnWithLengths)
updateArr(h, utils.Uint16ToUint8(ch.keyshareGroupsWithLengths))
updateArr(h, ch.PSKKeyExchangeModes)
updateArr(h, utils.Uint16ToUint8(ch.SupportedVersions))
updateArr(h, ch.lengthPrefixedCertCompressAlgos)
updateArr(h, ch.RecordSizeLimit)
out := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8]))
if normalized {
// ch.norm_nid = out
ch.NormNID = out
} else {
// ch._nid = out
ch.NID = out
}
return out
}
// FingerprintID calculates fingerprint ID of ClientHello and
// represents it as hexadecimal string.
func (ch *ClientHello) FingerprintID(normalized bool) string {
nid := ch.FingerprintNID(normalized)
hid := make([]byte, 8)
binary.BigEndian.PutUint64(hid, uint64(nid))
id := hex.EncodeToString(hid)
if normalized {
ch.NormID = id
} else {
ch.ID = id
}
return id
}