|
|
@@ -0,0 +1,360 @@
|
|
|
+// Copyright 2017 Google Inc. All rights reserved.
|
|
|
+// Use of this source code is governed by a BSD-style
|
|
|
+// license that can be found in the LICENSE file.
|
|
|
+
|
|
|
+package tls
|
|
|
+
|
|
|
+import (
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ "golang.org/x/crypto/cryptobyte"
|
|
|
+)
|
|
|
+
|
|
|
+// Fingerprinter is a struct largely for holding options for the FingerprintClientHello func
|
|
|
+type Fingerprinter struct {
|
|
|
+ // KeepPSK will ensure that the PreSharedKey extension is passed along into the resulting ClientHelloSpec as-is
|
|
|
+ KeepPSK bool
|
|
|
+ // AllowBluntMimicry will ensure that unknown extensions are
|
|
|
+ // passed along into the resulting ClientHelloSpec as-is
|
|
|
+ // It will not ensure that the PSK is passed along, if you require that, use KeepPSK
|
|
|
+ // WARNING: there could be numerous subtle issues with ClientHelloSpecs
|
|
|
+ // that are generated with this flag which could compromise security and/or mimicry
|
|
|
+ AllowBluntMimicry bool
|
|
|
+ // AlwaysAddPadding will always add a UtlsPaddingExtension with BoringPaddingStyle
|
|
|
+ // at the end of the extensions list if it isn't found in the fingerprinted hello.
|
|
|
+ // This could be useful in scenarios where the hello you are fingerprinting does not
|
|
|
+ // have any padding, but you suspect that other changes you make to the final hello
|
|
|
+ // (including things like different SNI lengths) would cause padding to be necessary
|
|
|
+ AlwaysAddPadding bool
|
|
|
+}
|
|
|
+
|
|
|
+// FingerprintClientHello returns a ClientHelloSpec which is based on the
|
|
|
+// ClientHello that is passed in as the data argument
|
|
|
+//
|
|
|
+// If the ClientHello passed in has extensions that are not recognized or cannot be handled
|
|
|
+// it will return a non-nil error and a nil *ClientHelloSpec value
|
|
|
+//
|
|
|
+// The data should be the full tls record, including the record type/version/length header
|
|
|
+// as well as the handshake type/length/version header
|
|
|
+// https://tools.ietf.org/html/rfc5246#section-6.2
|
|
|
+// https://tools.ietf.org/html/rfc5246#section-7.4
|
|
|
+func (f *Fingerprinter) FingerprintClientHello(data []byte) (*ClientHelloSpec, error) {
|
|
|
+ clientHelloSpec := &ClientHelloSpec{}
|
|
|
+ s := cryptobyte.String(data)
|
|
|
+
|
|
|
+ var contentType uint8
|
|
|
+ var recordVersion uint16
|
|
|
+ if !s.ReadUint8(&contentType) || // record type
|
|
|
+ !s.ReadUint16(&recordVersion) || !s.Skip(2) { // record version and length
|
|
|
+ return nil, errors.New("unable to read record type, version, and length")
|
|
|
+ }
|
|
|
+
|
|
|
+ if recordType(contentType) != recordTypeHandshake {
|
|
|
+ return nil, errors.New("record is not a handshake")
|
|
|
+ }
|
|
|
+
|
|
|
+ var handshakeVersion uint16
|
|
|
+ var handshakeType uint8
|
|
|
+
|
|
|
+ if !s.ReadUint8(&handshakeType) || !s.Skip(3) || // message type and 3 byte length
|
|
|
+ !s.ReadUint16(&handshakeVersion) || !s.Skip(32) { // 32 byte random
|
|
|
+ return nil, errors.New("unable to read handshake message type, length, and random")
|
|
|
+ }
|
|
|
+
|
|
|
+ if handshakeType != typeClientHello {
|
|
|
+ return nil, errors.New("handshake message is not a ClientHello")
|
|
|
+ }
|
|
|
+
|
|
|
+ clientHelloSpec.TLSVersMin = recordVersion
|
|
|
+ clientHelloSpec.TLSVersMax = handshakeVersion
|
|
|
+
|
|
|
+ var ignoredSessionID cryptobyte.String
|
|
|
+ if !s.ReadUint8LengthPrefixed(&ignoredSessionID) {
|
|
|
+ return nil, errors.New("unable to read session id")
|
|
|
+ }
|
|
|
+
|
|
|
+ var cipherSuitesBytes cryptobyte.String
|
|
|
+ if !s.ReadUint16LengthPrefixed(&cipherSuitesBytes) {
|
|
|
+ return nil, errors.New("unable to read ciphersuites")
|
|
|
+ }
|
|
|
+ cipherSuites := []uint16{}
|
|
|
+ for !cipherSuitesBytes.Empty() {
|
|
|
+ var suite uint16
|
|
|
+ if !cipherSuitesBytes.ReadUint16(&suite) {
|
|
|
+ return nil, errors.New("unable to read ciphersuite")
|
|
|
+ }
|
|
|
+ cipherSuites = append(cipherSuites, unGREASEUint16(suite))
|
|
|
+ }
|
|
|
+ clientHelloSpec.CipherSuites = cipherSuites
|
|
|
+
|
|
|
+ if !readUint8LengthPrefixed(&s, &clientHelloSpec.CompressionMethods) {
|
|
|
+ return nil, errors.New("unable to read compression methods")
|
|
|
+ }
|
|
|
+
|
|
|
+ if s.Empty() {
|
|
|
+ // ClientHello is optionally followed by extension data
|
|
|
+ return clientHelloSpec, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ var extensions cryptobyte.String
|
|
|
+ if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() {
|
|
|
+ return nil, errors.New("unable to read extensions data")
|
|
|
+ }
|
|
|
+
|
|
|
+ for !extensions.Empty() {
|
|
|
+ var extension uint16
|
|
|
+ var extData cryptobyte.String
|
|
|
+ if !extensions.ReadUint16(&extension) ||
|
|
|
+ !extensions.ReadUint16LengthPrefixed(&extData) {
|
|
|
+ return nil, errors.New("unable to read extension data")
|
|
|
+ }
|
|
|
+
|
|
|
+ switch extension {
|
|
|
+ case extensionServerName:
|
|
|
+ // RFC 6066, Section 3
|
|
|
+ var nameList cryptobyte.String
|
|
|
+ if !extData.ReadUint16LengthPrefixed(&nameList) || nameList.Empty() {
|
|
|
+ return nil, errors.New("unable to read server name extension data")
|
|
|
+ }
|
|
|
+ var serverName string
|
|
|
+ for !nameList.Empty() {
|
|
|
+ var nameType uint8
|
|
|
+ var serverNameBytes cryptobyte.String
|
|
|
+ if !nameList.ReadUint8(&nameType) ||
|
|
|
+ !nameList.ReadUint16LengthPrefixed(&serverNameBytes) ||
|
|
|
+ serverNameBytes.Empty() {
|
|
|
+ return nil, errors.New("unable to read server name extension data")
|
|
|
+ }
|
|
|
+ if nameType != 0 {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if len(serverName) != 0 {
|
|
|
+ return nil, errors.New("multiple names of the same name_type in server name extension are prohibited")
|
|
|
+ }
|
|
|
+ serverName = string(serverNameBytes)
|
|
|
+ if strings.HasSuffix(serverName, ".") {
|
|
|
+ return nil, errors.New("SNI value may not include a trailing dot")
|
|
|
+ }
|
|
|
+
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SNIExtension{})
|
|
|
+
|
|
|
+ }
|
|
|
+ case extensionNextProtoNeg:
|
|
|
+ // draft-agl-tls-nextprotoneg-04
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &NPNExtension{})
|
|
|
+
|
|
|
+ case extensionStatusRequest:
|
|
|
+ // RFC 4366, Section 3.6
|
|
|
+ var statusType uint8
|
|
|
+ var ignored cryptobyte.String
|
|
|
+ if !extData.ReadUint8(&statusType) ||
|
|
|
+ !extData.ReadUint16LengthPrefixed(&ignored) ||
|
|
|
+ !extData.ReadUint16LengthPrefixed(&ignored) {
|
|
|
+ return nil, errors.New("unable to read status request extension data")
|
|
|
+ }
|
|
|
+
|
|
|
+ if statusType == statusTypeOCSP {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &StatusRequestExtension{})
|
|
|
+ } else {
|
|
|
+ return nil, errors.New("status request extension statusType is not statusTypeOCSP")
|
|
|
+ }
|
|
|
+
|
|
|
+ case extensionSupportedCurves:
|
|
|
+ // RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7
|
|
|
+ var curvesBytes cryptobyte.String
|
|
|
+ if !extData.ReadUint16LengthPrefixed(&curvesBytes) || curvesBytes.Empty() {
|
|
|
+ return nil, errors.New("unable to read supported curves extension data")
|
|
|
+ }
|
|
|
+ curves := []CurveID{}
|
|
|
+ for !curvesBytes.Empty() {
|
|
|
+ var curve uint16
|
|
|
+ if !curvesBytes.ReadUint16(&curve) {
|
|
|
+ return nil, errors.New("unable to read supported curves extension data")
|
|
|
+ }
|
|
|
+ curves = append(curves, CurveID(unGREASEUint16(curve)))
|
|
|
+ }
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SupportedCurvesExtension{curves})
|
|
|
+
|
|
|
+ case extensionSupportedPoints:
|
|
|
+ // RFC 4492, Section 5.1.2
|
|
|
+ supportedPoints := []uint8{}
|
|
|
+ if !readUint8LengthPrefixed(&extData, &supportedPoints) ||
|
|
|
+ len(supportedPoints) == 0 {
|
|
|
+ return nil, errors.New("unable to read supported points extension data")
|
|
|
+ }
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SupportedPointsExtension{supportedPoints})
|
|
|
+
|
|
|
+ case extensionSessionTicket:
|
|
|
+ // RFC 5077, Section 3.2
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SessionTicketExtension{})
|
|
|
+
|
|
|
+ case extensionSignatureAlgorithms:
|
|
|
+ // RFC 5246, Section 7.4.1.4.1
|
|
|
+ var sigAndAlgs cryptobyte.String
|
|
|
+ if !extData.ReadUint16LengthPrefixed(&sigAndAlgs) || sigAndAlgs.Empty() {
|
|
|
+ return nil, errors.New("unable to read signature algorithms extension data")
|
|
|
+ }
|
|
|
+ supportedSignatureAlgorithms := []SignatureScheme{}
|
|
|
+ for !sigAndAlgs.Empty() {
|
|
|
+ var sigAndAlg uint16
|
|
|
+ if !sigAndAlgs.ReadUint16(&sigAndAlg) {
|
|
|
+ return nil, errors.New("unable to read signature algorithms extension data")
|
|
|
+ }
|
|
|
+ supportedSignatureAlgorithms = append(
|
|
|
+ supportedSignatureAlgorithms, SignatureScheme(sigAndAlg))
|
|
|
+ }
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SignatureAlgorithmsExtension{supportedSignatureAlgorithms})
|
|
|
+
|
|
|
+ case extensionSignatureAlgorithmsCert:
|
|
|
+ // RFC 8446, Section 4.2.3
|
|
|
+ if f.AllowBluntMimicry {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
|
|
|
+ } else {
|
|
|
+ return nil, errors.New("unsupported extension SignatureAlgorithmsCert")
|
|
|
+ }
|
|
|
+
|
|
|
+ case extensionRenegotiationInfo:
|
|
|
+ // RFC 5746, Section 3.2
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &RenegotiationInfoExtension{RenegotiateOnceAsClient})
|
|
|
+
|
|
|
+ case extensionALPN:
|
|
|
+ // RFC 7301, Section 3.1
|
|
|
+ var protoList cryptobyte.String
|
|
|
+ if !extData.ReadUint16LengthPrefixed(&protoList) || protoList.Empty() {
|
|
|
+ return nil, errors.New("unable to read ALPN extension data")
|
|
|
+ }
|
|
|
+ alpnProtocols := []string{}
|
|
|
+ for !protoList.Empty() {
|
|
|
+ var proto cryptobyte.String
|
|
|
+ if !protoList.ReadUint8LengthPrefixed(&proto) || proto.Empty() {
|
|
|
+ return nil, errors.New("unable to read ALPN extension data")
|
|
|
+ }
|
|
|
+ alpnProtocols = append(alpnProtocols, string(proto))
|
|
|
+
|
|
|
+ }
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &ALPNExtension{alpnProtocols})
|
|
|
+
|
|
|
+ case extensionSCT:
|
|
|
+ // RFC 6962, Section 3.3.1
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SCTExtension{})
|
|
|
+
|
|
|
+ case extensionSupportedVersions:
|
|
|
+ // RFC 8446, Section 4.2.1
|
|
|
+ var versList cryptobyte.String
|
|
|
+ if !extData.ReadUint8LengthPrefixed(&versList) || versList.Empty() {
|
|
|
+ return nil, errors.New("unable to read supported versions extension data")
|
|
|
+ }
|
|
|
+ supportedVersions := []uint16{}
|
|
|
+ for !versList.Empty() {
|
|
|
+ var vers uint16
|
|
|
+ if !versList.ReadUint16(&vers) {
|
|
|
+ return nil, errors.New("unable to read supported versions extension data")
|
|
|
+ }
|
|
|
+ supportedVersions = append(supportedVersions, unGREASEUint16(vers))
|
|
|
+ }
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SupportedVersionsExtension{supportedVersions})
|
|
|
+ // If SupportedVersionsExtension is present, use that instead of record+handshake versions
|
|
|
+ clientHelloSpec.TLSVersMin = 0
|
|
|
+ clientHelloSpec.TLSVersMax = 0
|
|
|
+
|
|
|
+ case extensionKeyShare:
|
|
|
+ // RFC 8446, Section 4.2.8
|
|
|
+ var clientShares cryptobyte.String
|
|
|
+ if !extData.ReadUint16LengthPrefixed(&clientShares) {
|
|
|
+ return nil, errors.New("unable to read key share extension data")
|
|
|
+ }
|
|
|
+ keyShares := []KeyShare{}
|
|
|
+ for !clientShares.Empty() {
|
|
|
+ var ks KeyShare
|
|
|
+ var group uint16
|
|
|
+ if !clientShares.ReadUint16(&group) ||
|
|
|
+ !readUint16LengthPrefixed(&clientShares, &ks.Data) ||
|
|
|
+ len(ks.Data) == 0 {
|
|
|
+ return nil, errors.New("unable to read key share extension data")
|
|
|
+ }
|
|
|
+ ks.Group = CurveID(unGREASEUint16(group))
|
|
|
+ // if not GREASE, key share data will be discarded as it should
|
|
|
+ // be generated per connection
|
|
|
+ if ks.Group != GREASE_PLACEHOLDER {
|
|
|
+ ks.Data = nil
|
|
|
+ }
|
|
|
+ keyShares = append(keyShares, ks)
|
|
|
+ }
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &KeyShareExtension{keyShares})
|
|
|
+
|
|
|
+ case extensionPSKModes:
|
|
|
+ // RFC 8446, Section 4.2.9
|
|
|
+ // TODO: PSK Modes have their own form of GREASE-ing which is not currently implemented
|
|
|
+ // the current functionality will NOT re-GREASE/re-randomize these values when using a fingerprinted spec
|
|
|
+ // https://github.com/refraction-networking/utls/pull/58#discussion_r522354105
|
|
|
+ // https://tools.ietf.org/html/draft-ietf-tls-grease-01#section-2
|
|
|
+ pskModes := []uint8{}
|
|
|
+ if !readUint8LengthPrefixed(&extData, &pskModes) {
|
|
|
+ return nil, errors.New("unable to read PSK extension data")
|
|
|
+ }
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &PSKKeyExchangeModesExtension{pskModes})
|
|
|
+
|
|
|
+ case utlsExtensionExtendedMasterSecret:
|
|
|
+ // https://tools.ietf.org/html/rfc7627
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsExtendedMasterSecretExtension{})
|
|
|
+
|
|
|
+ case utlsExtensionPadding:
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle})
|
|
|
+
|
|
|
+ case fakeExtensionChannelID, fakeCertCompressionAlgs, fakeRecordSizeLimit:
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
|
|
|
+
|
|
|
+ case extensionPreSharedKey:
|
|
|
+ // RFC 8446, Section 4.2.11
|
|
|
+ if f.KeepPSK {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
|
|
|
+ } else {
|
|
|
+ return nil, errors.New("unsupported extension PreSharedKey")
|
|
|
+ }
|
|
|
+
|
|
|
+ case extensionCookie:
|
|
|
+ // RFC 8446, Section 4.2.2
|
|
|
+ if f.AllowBluntMimicry {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
|
|
|
+ } else {
|
|
|
+ return nil, errors.New("unsupported extension Cookie")
|
|
|
+ }
|
|
|
+
|
|
|
+ case extensionEarlyData:
|
|
|
+ // RFC 8446, Section 4.2.10
|
|
|
+ if f.AllowBluntMimicry {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
|
|
|
+ } else {
|
|
|
+ return nil, errors.New("unsupported extension EarlyData")
|
|
|
+ }
|
|
|
+
|
|
|
+ default:
|
|
|
+ if isGREASEUint16(extension) {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsGREASEExtension{unGREASEUint16(extension), extData})
|
|
|
+ } else if f.AllowBluntMimicry {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
|
|
|
+ } else {
|
|
|
+ return nil, fmt.Errorf("unsupported extension %#x", extension)
|
|
|
+ }
|
|
|
+
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if f.AlwaysAddPadding {
|
|
|
+ alreadyHasPadding := false
|
|
|
+ for _, ext := range clientHelloSpec.Extensions {
|
|
|
+ if _, ok := ext.(*UtlsPaddingExtension); ok {
|
|
|
+ alreadyHasPadding = true
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if !alreadyHasPadding {
|
|
|
+ clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle})
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return clientHelloSpec, nil
|
|
|
+}
|