Skip to content

Commit

Permalink
crypto/tls: expose extensions presented by client to GetCertificate
Browse files Browse the repository at this point in the history
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 golang#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]>
  • Loading branch information
bpowers authored and rolandshoemaker committed Aug 9, 2024
1 parent 760b722 commit f053f4f
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 0 deletions.
1 change: 1 addition & 0 deletions api/next/32936.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg crypto/tls, type ClientHelloInfo struct, Extensions []uint16 #32936
1 change: 1 addition & 0 deletions doc/next/6-stdlib/99-minor/crypto/tls/32936.md
Original file line number Diff line number Diff line change
@@ -0,0 +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 -->
4 changes: 4 additions & 0 deletions src/crypto/tls/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ type ClientHelloInfo struct {
// might be rejected if used.
SupportedVersions []uint16

// Extensions lists the IDs of the extensions presented by the client
// in the client hello.
Extensions []uint16

// Conn is the underlying net.Conn for the connection. Do not read
// from, or write to, this connection; that will cause the TLS
// connection to fail.
Expand Down
3 changes: 3 additions & 0 deletions src/crypto/tls/handshake_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ type clientHelloMsg struct {
pskBinders [][]byte
quicTransportParameters []byte
encryptedClientHello []byte
// extensions are only populated on the server-side of a handshake
extensions []uint16
}

func (m *clientHelloMsg) marshalMsg(echInner bool) ([]byte, error) {
Expand Down Expand Up @@ -467,6 +469,7 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool {
return false
}
seenExts[extension] = true
m.extensions = append(m.extensions, extension)

switch extension {
case extensionServerName:
Expand Down
12 changes: 12 additions & 0 deletions src/crypto/tls/handshake_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ func TestMarshalUnmarshal(t *testing.T) {
m.activeCertHandles = nil
}

if ch, ok := m.(*clientHelloMsg); ok {
// extensions is special cased, as it is only populated by the
// server-side of a handshake and is not expected to roundtrip
// through marshal + unmarshal. m ends up with the list of
// extensions necessary to serialize the other fields of
// clientHelloMsg, so check that it is non-empty, then clear it.
if len(ch.extensions) == 0 {
t.Errorf("expected ch.extensions to be populated on unmarshal")
}
ch.extensions = nil
}

// clientHelloMsg and serverHelloMsg, when unmarshalled, store
// their original representation, for later use in the handshake
// transcript. In order to prevent DeepEqual from failing since
Expand Down
1 change: 1 addition & 0 deletions src/crypto/tls/handshake_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,7 @@ func clientHelloInfo(ctx context.Context, c *Conn, clientHello *clientHelloMsg)
SignatureSchemes: clientHello.supportedSignatureAlgorithms,
SupportedProtos: clientHello.alpnProtocols,
SupportedVersions: supportedVersions,
Extensions: clientHello.extensions,
Conn: c.conn,
config: c.config,
ctx: ctx,
Expand Down
60 changes: 60 additions & 0 deletions src/crypto/tls/handshake_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"runtime"
"slices"
"strings"
"sync/atomic"
"testing"
"time"
)
Expand Down Expand Up @@ -1066,6 +1067,65 @@ func TestHandshakeServerSNIGetCertificateNotFound(t *testing.T) {
runServerTestTLS12(t, test)
}

// TestHandshakeServerGetCertificateExtensions tests to make sure that the
// Extensions passed to GetCertificate match what we expect based on the
// clientHelloMsg
func TestHandshakeServerGetCertificateExtensions(t *testing.T) {
const errMsg = "TestHandshakeServerGetCertificateExtensions error"
// ensure the test condition inside our GetCertificate callback
// is actually invoked
var called atomic.Int32

testVersions := []uint16{VersionTLS12, VersionTLS13}
for _, vers := range testVersions {
t.Run(fmt.Sprintf("TLS version %04x", vers), func(t *testing.T) {
pk, _ := ecdh.X25519().GenerateKey(rand.Reader)
clientHello := &clientHelloMsg{
vers: vers,
random: make([]byte, 32),
cipherSuites: []uint16{TLS_CHACHA20_POLY1305_SHA256},
compressionMethods: []uint8{compressionNone},
serverName: "test",
keyShares: []keyShare{{group: X25519, data: pk.PublicKey().Bytes()}},
supportedCurves: []CurveID{X25519},
supportedSignatureAlgorithms: []SignatureScheme{Ed25519},
}

// the clientHelloMsg initialized just above is serialized with
// two extensions: server_name(0) and application_layer_protocol_negotiation(16)
expectedExtensions := []uint16{
extensionServerName,
extensionSupportedCurves,
extensionSignatureAlgorithms,
extensionKeyShare,
}

if vers == VersionTLS13 {
clientHello.supportedVersions = []uint16{VersionTLS13}
expectedExtensions = append(expectedExtensions, extensionSupportedVersions)
}

// Go's TLS client presents extensions in the ClientHello sorted by extension ID
slices.Sort(expectedExtensions)

serverConfig := testConfig.Clone()
serverConfig.GetCertificate = func(clientHello *ClientHelloInfo) (*Certificate, error) {
if !slices.Equal(expectedExtensions, clientHello.Extensions) {
t.Errorf("expected extensions on ClientHelloInfo (%v) to match clientHelloMsg (%v)", expectedExtensions, clientHello.Extensions)
}
called.Add(1)

return nil, errors.New(errMsg)
}
testClientHelloFailure(t, serverConfig, clientHello, errMsg)
})
}

if int(called.Load()) != len(testVersions) {
t.Error("expected our GetCertificate test to be called twice")
}
}

// TestHandshakeServerSNIGetCertificateError tests to make sure that errors in
// GetCertificate result in a tls alert.
func TestHandshakeServerSNIGetCertificateError(t *testing.T) {
Expand Down

0 comments on commit f053f4f

Please sign in to comment.