crypto/tls: add GetEncryptedClientHelloKeys

This allows servers to rotate their ECH keys without needing to restart
the server.

Fixes #71920

Change-Id: I55591ab3303d5fde639038541c50edcf1fafc9aa
Reviewed-on: https://go-review.googlesource.com/c/go/+/670655
TryBot-Bypass: Roland Shoemaker <roland@golang.org>
Reviewed-by: David Chase <drchase@google.com>
Auto-Submit: Roland Shoemaker <roland@golang.org>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
This commit is contained in:
Roland Shoemaker 2025-05-07 11:37:52 -07:00 committed by Gopher Robot
parent a731955f0f
commit c5a1fc1f97
7 changed files with 88 additions and 27 deletions

1
api/next/71920.txt Normal file
View file

@ -0,0 +1 @@
pkg crypto/tls, type Config struct, GetEncryptedClientHelloKeys func(*ClientHelloInfo) ([]EncryptedClientHelloKey, error) #71920

View file

@ -0,0 +1,3 @@
The new [Config.GetEncryptedClientHelloKeys] callback can be used to set the
[EncryptedClientHelloKey]s for a server to use when a client sends an Encrypted
Client Hello extension.

View file

@ -837,6 +837,20 @@ type Config struct {
// when ECH is rejected, even if set, and InsecureSkipVerify is ignored. // when ECH is rejected, even if set, and InsecureSkipVerify is ignored.
EncryptedClientHelloRejectionVerify func(ConnectionState) error EncryptedClientHelloRejectionVerify func(ConnectionState) error
// GetEncryptedClientHelloKeys, if not nil, is called when by a server when
// a client attempts ECH.
//
// If GetEncryptedClientHelloKeys is not nil, [EncryptedClientHelloKeys] is
// ignored.
//
// If GetEncryptedClientHelloKeys returns an error, the handshake will be
// aborted and the error will be returned. Otherwise,
// GetEncryptedClientHelloKeys must return a non-nil slice of
// [EncryptedClientHelloKey] that represents the acceptable ECH keys.
//
// For further details, see [EncryptedClientHelloKeys].
GetEncryptedClientHelloKeys func(*ClientHelloInfo) ([]EncryptedClientHelloKey, error)
// EncryptedClientHelloKeys are the ECH keys to use when a client // EncryptedClientHelloKeys are the ECH keys to use when a client
// attempts ECH. // attempts ECH.
// //
@ -847,6 +861,9 @@ type Config struct {
// will send a list of configs to retry based on the set of // will send a list of configs to retry based on the set of
// EncryptedClientHelloKeys which have the SendAsRetry field set. // EncryptedClientHelloKeys which have the SendAsRetry field set.
// //
// If GetEncryptedClientHelloKeys is non-nil, EncryptedClientHelloKeys is
// ignored.
//
// On the client side, this field is ignored. In order to configure ECH for // On the client side, this field is ignored. In order to configure ECH for
// clients, see the EncryptedClientHelloConfigList field. // clients, see the EncryptedClientHelloConfigList field.
EncryptedClientHelloKeys []EncryptedClientHelloKey EncryptedClientHelloKeys []EncryptedClientHelloKey
@ -935,6 +952,7 @@ func (c *Config) Clone() *Config {
GetCertificate: c.GetCertificate, GetCertificate: c.GetCertificate,
GetClientCertificate: c.GetClientCertificate, GetClientCertificate: c.GetClientCertificate,
GetConfigForClient: c.GetConfigForClient, GetConfigForClient: c.GetConfigForClient,
GetEncryptedClientHelloKeys: c.GetEncryptedClientHelloKeys,
VerifyPeerCertificate: c.VerifyPeerCertificate, VerifyPeerCertificate: c.VerifyPeerCertificate,
VerifyConnection: c.VerifyConnection, VerifyConnection: c.VerifyConnection,
RootCAs: c.RootCAs, RootCAs: c.RootCAs,

View file

@ -578,7 +578,7 @@ func marshalEncryptedClientHelloConfigList(configs []EncryptedClientHelloKey) ([
return builder.Bytes() return builder.Bytes()
} }
func (c *Conn) processECHClientHello(outer *clientHelloMsg) (*clientHelloMsg, *echServerContext, error) { func (c *Conn) processECHClientHello(outer *clientHelloMsg, echKeys []EncryptedClientHelloKey) (*clientHelloMsg, *echServerContext, error) {
echType, echCiphersuite, configID, encap, payload, err := parseECHExt(outer.encryptedClientHello) echType, echCiphersuite, configID, encap, payload, err := parseECHExt(outer.encryptedClientHello)
if err != nil { if err != nil {
if errors.Is(err, errInvalidECHExt) { if errors.Is(err, errInvalidECHExt) {
@ -594,11 +594,11 @@ func (c *Conn) processECHClientHello(outer *clientHelloMsg) (*clientHelloMsg, *e
return outer, &echServerContext{inner: true}, nil return outer, &echServerContext{inner: true}, nil
} }
if len(c.config.EncryptedClientHelloKeys) == 0 { if len(echKeys) == 0 {
return outer, nil, nil return outer, nil, nil
} }
for _, echKey := range c.config.EncryptedClientHelloKeys { for _, echKey := range echKeys {
skip, config, err := parseECHConfig(echKey.Config) skip, config, err := parseECHConfig(echKey.Config)
if err != nil || skip { if err != nil || skip {
c.sendAlert(alertInternalError) c.sendAlert(alertInternalError)

View file

@ -149,7 +149,15 @@ func (c *Conn) readClientHello(ctx context.Context) (*clientHelloMsg, *echServer
// the contents of the client hello, since we may swap it out completely. // the contents of the client hello, since we may swap it out completely.
var ech *echServerContext var ech *echServerContext
if len(clientHello.encryptedClientHello) != 0 { if len(clientHello.encryptedClientHello) != 0 {
clientHello, ech, err = c.processECHClientHello(clientHello) echKeys := c.config.EncryptedClientHelloKeys
if c.config.GetEncryptedClientHelloKeys != nil {
echKeys, err = c.config.GetEncryptedClientHelloKeys(clientHelloInfo(ctx, c, clientHello))
if err != nil {
c.sendAlert(alertInternalError)
return nil, nil, err
}
}
clientHello, ech, err = c.processECHClientHello(clientHello, echKeys)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View file

@ -804,8 +804,16 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error {
// If client sent ECH extension, but we didn't accept it, // If client sent ECH extension, but we didn't accept it,
// send retry configs, if available. // send retry configs, if available.
if len(hs.c.config.EncryptedClientHelloKeys) > 0 && len(hs.clientHello.encryptedClientHello) > 0 && hs.echContext == nil { echKeys := hs.c.config.EncryptedClientHelloKeys
encryptedExtensions.echRetryConfigs, err = buildRetryConfigList(hs.c.config.EncryptedClientHelloKeys) if hs.c.config.GetEncryptedClientHelloKeys != nil {
echKeys, err = hs.c.config.GetEncryptedClientHelloKeys(clientHelloInfo(hs.ctx, c, hs.clientHello))
if err != nil {
c.sendAlert(alertInternalError)
return err
}
}
if len(echKeys) > 0 && len(hs.clientHello.encryptedClientHello) > 0 && hs.echContext == nil {
encryptedExtensions.echRetryConfigs, err = buildRetryConfigList(echKeys)
if err != nil { if err != nil {
c.sendAlert(alertInternalError) c.sendAlert(alertInternalError)
return err return err

View file

@ -811,7 +811,7 @@ func TestWarningAlertFlood(t *testing.T) {
} }
func TestCloneFuncFields(t *testing.T) { func TestCloneFuncFields(t *testing.T) {
const expectedCount = 9 const expectedCount = 10
called := 0 called := 0
c1 := Config{ c1 := Config{
@ -851,6 +851,10 @@ func TestCloneFuncFields(t *testing.T) {
called |= 1 << 8 called |= 1 << 8
return nil return nil
}, },
GetEncryptedClientHelloKeys: func(*ClientHelloInfo) ([]EncryptedClientHelloKey, error) {
called |= 1 << 9
return nil, nil
},
} }
c2 := c1.Clone() c2 := c1.Clone()
@ -864,6 +868,7 @@ func TestCloneFuncFields(t *testing.T) {
c2.UnwrapSession(nil, ConnectionState{}) c2.UnwrapSession(nil, ConnectionState{})
c2.WrapSession(ConnectionState{}, nil) c2.WrapSession(ConnectionState{}, nil)
c2.EncryptedClientHelloRejectionVerify(ConnectionState{}) c2.EncryptedClientHelloRejectionVerify(ConnectionState{})
c2.GetEncryptedClientHelloKeys(nil)
if called != (1<<expectedCount)-1 { if called != (1<<expectedCount)-1 {
t.Fatalf("expected %d calls but saw calls %b", expectedCount, called) t.Fatalf("expected %d calls but saw calls %b", expectedCount, called)
@ -882,7 +887,7 @@ func TestCloneNonFuncFields(t *testing.T) {
switch fn := typ.Field(i).Name; fn { switch fn := typ.Field(i).Name; fn {
case "Rand": case "Rand":
f.Set(reflect.ValueOf(io.Reader(os.Stdin))) f.Set(reflect.ValueOf(io.Reader(os.Stdin)))
case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "WrapSession", "UnwrapSession", "EncryptedClientHelloRejectionVerify": case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "WrapSession", "UnwrapSession", "EncryptedClientHelloRejectionVerify", "GetEncryptedClientHelloKeys":
// DeepEqual can't compare functions. If you add a // DeepEqual can't compare functions. If you add a
// function field to this list, you must also change // function field to this list, you must also change
// TestCloneFuncFields to ensure that the func field is // TestCloneFuncFields to ensure that the func field is
@ -2301,26 +2306,44 @@ func TestECH(t *testing.T) {
{Config: echConfig, PrivateKey: echKey.Bytes(), SendAsRetry: true}, {Config: echConfig, PrivateKey: echKey.Bytes(), SendAsRetry: true},
} }
ss, cs, err := testHandshake(t, clientConfig, serverConfig) check := func() {
ss, cs, err := testHandshake(t, clientConfig, serverConfig)
if err != nil {
t.Fatalf("unexpected failure: %s", err)
}
if !ss.ECHAccepted {
t.Fatal("server ConnectionState shows ECH not accepted")
}
if !cs.ECHAccepted {
t.Fatal("client ConnectionState shows ECH not accepted")
}
if cs.ServerName != "secret.example" || ss.ServerName != "secret.example" {
t.Fatalf("unexpected ConnectionState.ServerName, want %q, got server:%q, client: %q", "secret.example", ss.ServerName, cs.ServerName)
}
if len(cs.VerifiedChains) != 1 {
t.Fatal("unexpect number of certificate chains")
}
if len(cs.VerifiedChains[0]) != 1 {
t.Fatal("unexpect number of certificates")
}
if !cs.VerifiedChains[0][0].Equal(secretCert) {
t.Fatal("unexpected certificate")
}
}
check()
serverConfig.GetEncryptedClientHelloKeys = func(_ *ClientHelloInfo) ([]EncryptedClientHelloKey, error) {
return []EncryptedClientHelloKey{{Config: echConfig, PrivateKey: echKey.Bytes(), SendAsRetry: true}}, nil
}
randKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil { if err != nil {
t.Fatalf("unexpected failure: %s", err) t.Fatal(err)
} }
if !ss.ECHAccepted { randConfig := marshalECHConfig(32, randKey.PublicKey().Bytes(), "random.example", 32)
t.Fatal("server ConnectionState shows ECH not accepted") serverConfig.EncryptedClientHelloKeys = []EncryptedClientHelloKey{
} {Config: randConfig, PrivateKey: randKey.Bytes(), SendAsRetry: true},
if !cs.ECHAccepted {
t.Fatal("client ConnectionState shows ECH not accepted")
}
if cs.ServerName != "secret.example" || ss.ServerName != "secret.example" {
t.Fatalf("unexpected ConnectionState.ServerName, want %q, got server:%q, client: %q", "secret.example", ss.ServerName, cs.ServerName)
}
if len(cs.VerifiedChains) != 1 {
t.Fatal("unexpect number of certificate chains")
}
if len(cs.VerifiedChains[0]) != 1 {
t.Fatal("unexpect number of certificates")
}
if !cs.VerifiedChains[0][0].Equal(secretCert) {
t.Fatal("unexpected certificate")
} }
check()
} }