]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/tls: expose extensions presented by client to GetCertificate
authorBobby Powers <bobbypowers@gmail.com>
Sun, 26 Feb 2023 00:24:54 +0000 (16:24 -0800)
committerRoland Shoemaker <roland@golang.org>
Fri, 9 Aug 2024 18:45:11 +0000 (18:45 +0000)
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 <dmitshur@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

api/next/32936.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/tls/32936.md [new file with mode: 0644]
src/crypto/tls/common.go
src/crypto/tls/handshake_messages.go
src/crypto/tls/handshake_messages_test.go
src/crypto/tls/handshake_server.go
src/crypto/tls/handshake_server_test.go

diff --git a/api/next/32936.txt b/api/next/32936.txt
new file mode 100644 (file)
index 0000000..920bfe3
--- /dev/null
@@ -0,0 +1 @@
+pkg crypto/tls, type ClientHelloInfo struct, Extensions []uint16 #32936
diff --git a/doc/next/6-stdlib/99-minor/crypto/tls/32936.md b/doc/next/6-stdlib/99-minor/crypto/tls/32936.md
new file mode 100644 (file)
index 0000000..60c0602
--- /dev/null
@@ -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 -->
\ No newline at end of file
index 5fd92d3c639b53fd41a405347fa6fe57b0b0d032..dba96509362658340c4306c9451065cf603d985e 100644 (file)
@@ -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.
index 8620b66a47490334146c09f7332444dc5bc31f09..823caff603ef1e7e78431b2041643a7096df25b7 100644 (file)
@@ -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) {
@@ -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:
index 197a1c55eeb5a8f1e3991582b413169a3e4e02a7..2c360e6a50dfc560f32211f9baaff8bb95a55f4a 100644 (file)
@@ -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
index ac3d915d1746d754f2e110a605faca5e6aedb6cb..bc4e51ba364cf1923d19b2aff69717a502ee5931 100644 (file)
@@ -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,
index 94d3d0f6dc87bc6f48ce9e592ce1e273cd30182c..788a26af7559b6fafeccd70499a86c590cab8f6f 100644 (file)
@@ -23,6 +23,7 @@ import (
        "runtime"
        "slices"
        "strings"
+       "sync/atomic"
        "testing"
        "time"
 )
@@ -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) {