1
0
mirror of https://github.com/golang/go synced 2024-09-23 07:23:18 -06:00

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 <dmitshur@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Bobby Powers 2023-02-25 16:24:54 -08:00 committed by Roland Shoemaker
parent 760b722c34
commit f053f4f921
7 changed files with 82 additions and 0 deletions

1
api/next/32936.txt Normal file
View File

@ -0,0 +1 @@
pkg crypto/tls, type ClientHelloInfo struct, Extensions []uint16 #32936

View File

@ -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 -->

View 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.

View 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:

View 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

View 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,

View 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) {