Skip to content

Commit f053f4f

Browse files
bpowersrolandshoemaker
authored andcommitted
crypto/tls: expose extensions presented by client to GetCertificate
This enables JA3 and JA4 TLS fingerprinting to be implemented from the GetCertificate callback, similar to what BoringSSL provides with its SSL_CTX_set_dos_protection_cb hook. fixes #32936 Change-Id: Idb54ebcb43075582fcef0ac6438727f494543424 Reviewed-on: https://go-review.googlesource.com/c/go/+/471396 Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 760b722 commit f053f4f

File tree

7 files changed

+82
-0
lines changed

7 files changed

+82
-0
lines changed

api/next/32936.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg crypto/tls, type ClientHelloInfo struct, Extensions []uint16 #32936
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The [ClientHelloInfo] struct passed to [Config.GetCertificate] now includes an `Extensions` field, which can be useful for fingerprinting TLS clients.<!-- go.dev/issue/32936 -->

src/crypto/tls/common.go

+4
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,10 @@ type ClientHelloInfo struct {
447447
// might be rejected if used.
448448
SupportedVersions []uint16
449449

450+
// Extensions lists the IDs of the extensions presented by the client
451+
// in the client hello.
452+
Extensions []uint16
453+
450454
// Conn is the underlying net.Conn for the connection. Do not read
451455
// from, or write to, this connection; that will cause the TLS
452456
// connection to fail.

src/crypto/tls/handshake_messages.go

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ type clientHelloMsg struct {
9797
pskBinders [][]byte
9898
quicTransportParameters []byte
9999
encryptedClientHello []byte
100+
// extensions are only populated on the server-side of a handshake
101+
extensions []uint16
100102
}
101103

102104
func (m *clientHelloMsg) marshalMsg(echInner bool) ([]byte, error) {
@@ -467,6 +469,7 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool {
467469
return false
468470
}
469471
seenExts[extension] = true
472+
m.extensions = append(m.extensions, extension)
470473

471474
switch extension {
472475
case extensionServerName:

src/crypto/tls/handshake_messages_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ func TestMarshalUnmarshal(t *testing.T) {
7676
m.activeCertHandles = nil
7777
}
7878

79+
if ch, ok := m.(*clientHelloMsg); ok {
80+
// extensions is special cased, as it is only populated by the
81+
// server-side of a handshake and is not expected to roundtrip
82+
// through marshal + unmarshal. m ends up with the list of
83+
// extensions necessary to serialize the other fields of
84+
// clientHelloMsg, so check that it is non-empty, then clear it.
85+
if len(ch.extensions) == 0 {
86+
t.Errorf("expected ch.extensions to be populated on unmarshal")
87+
}
88+
ch.extensions = nil
89+
}
90+
7991
// clientHelloMsg and serverHelloMsg, when unmarshalled, store
8092
// their original representation, for later use in the handshake
8193
// transcript. In order to prevent DeepEqual from failing since

src/crypto/tls/handshake_server.go

+1
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,7 @@ func clientHelloInfo(ctx context.Context, c *Conn, clientHello *clientHelloMsg)
963963
SignatureSchemes: clientHello.supportedSignatureAlgorithms,
964964
SupportedProtos: clientHello.alpnProtocols,
965965
SupportedVersions: supportedVersions,
966+
Extensions: clientHello.extensions,
966967
Conn: c.conn,
967968
config: c.config,
968969
ctx: ctx,

src/crypto/tls/handshake_server_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"runtime"
2424
"slices"
2525
"strings"
26+
"sync/atomic"
2627
"testing"
2728
"time"
2829
)
@@ -1066,6 +1067,65 @@ func TestHandshakeServerSNIGetCertificateNotFound(t *testing.T) {
10661067
runServerTestTLS12(t, test)
10671068
}
10681069

1070+
// TestHandshakeServerGetCertificateExtensions tests to make sure that the
1071+
// Extensions passed to GetCertificate match what we expect based on the
1072+
// clientHelloMsg
1073+
func TestHandshakeServerGetCertificateExtensions(t *testing.T) {
1074+
const errMsg = "TestHandshakeServerGetCertificateExtensions error"
1075+
// ensure the test condition inside our GetCertificate callback
1076+
// is actually invoked
1077+
var called atomic.Int32
1078+
1079+
testVersions := []uint16{VersionTLS12, VersionTLS13}
1080+
for _, vers := range testVersions {
1081+
t.Run(fmt.Sprintf("TLS version %04x", vers), func(t *testing.T) {
1082+
pk, _ := ecdh.X25519().GenerateKey(rand.Reader)
1083+
clientHello := &clientHelloMsg{
1084+
vers: vers,
1085+
random: make([]byte, 32),
1086+
cipherSuites: []uint16{TLS_CHACHA20_POLY1305_SHA256},
1087+
compressionMethods: []uint8{compressionNone},
1088+
serverName: "test",
1089+
keyShares: []keyShare{{group: X25519, data: pk.PublicKey().Bytes()}},
1090+
supportedCurves: []CurveID{X25519},
1091+
supportedSignatureAlgorithms: []SignatureScheme{Ed25519},
1092+
}
1093+
1094+
// the clientHelloMsg initialized just above is serialized with
1095+
// two extensions: server_name(0) and application_layer_protocol_negotiation(16)
1096+
expectedExtensions := []uint16{
1097+
extensionServerName,
1098+
extensionSupportedCurves,
1099+
extensionSignatureAlgorithms,
1100+
extensionKeyShare,
1101+
}
1102+
1103+
if vers == VersionTLS13 {
1104+
clientHello.supportedVersions = []uint16{VersionTLS13}
1105+
expectedExtensions = append(expectedExtensions, extensionSupportedVersions)
1106+
}
1107+
1108+
// Go's TLS client presents extensions in the ClientHello sorted by extension ID
1109+
slices.Sort(expectedExtensions)
1110+
1111+
serverConfig := testConfig.Clone()
1112+
serverConfig.GetCertificate = func(clientHello *ClientHelloInfo) (*Certificate, error) {
1113+
if !slices.Equal(expectedExtensions, clientHello.Extensions) {
1114+
t.Errorf("expected extensions on ClientHelloInfo (%v) to match clientHelloMsg (%v)", expectedExtensions, clientHello.Extensions)
1115+
}
1116+
called.Add(1)
1117+
1118+
return nil, errors.New(errMsg)
1119+
}
1120+
testClientHelloFailure(t, serverConfig, clientHello, errMsg)
1121+
})
1122+
}
1123+
1124+
if int(called.Load()) != len(testVersions) {
1125+
t.Error("expected our GetCertificate test to be called twice")
1126+
}
1127+
}
1128+
10691129
// TestHandshakeServerSNIGetCertificateError tests to make sure that errors in
10701130
// GetCertificate result in a tls alert.
10711131
func TestHandshakeServerSNIGetCertificateError(t *testing.T) {

0 commit comments

Comments
 (0)