crypto/tls: clamp effective minimum version to TLS 1.3 when using QUIC

Change-Id: Ieec72362bacf1956a2bd5e0b2eb8dad88e624bd1
Reviewed-on: https://go-review.googlesource.com/c/go/+/745980
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
This commit is contained in:
Marten Seemann 2026-04-12 20:39:26 +08:00 committed by Damien Neil
parent edf006c9a3
commit ad46b4815e
6 changed files with 73 additions and 21 deletions

View file

@ -495,6 +495,9 @@ type ClientHelloInfo struct {
// for use with SupportsCertificate.
config *Config
// isQUIC indicates whether the connection is a QUIC connection.
isQUIC bool
// ctx is the context of the handshake that is in progress.
ctx context.Context
}
@ -1068,7 +1071,6 @@ func (c *Config) initLegacySessionTicketKeyRLocked() {
} else if !bytes.HasPrefix(c.SessionTicketKey[:], deprecatedSessionTicketKey) && len(c.sessionTicketKeys) == 0 {
c.sessionTicketKeys = []ticketKey{c.ticketKeyFromBytes(c.SessionTicketKey)}
}
}
// ticketKeys returns the ticketKeys for this connection.
@ -1218,7 +1220,7 @@ const roleServer = false
// supportedVersions returns the list of supported TLS versions, sorted from
// highest to lowest (and hence also in preference order).
func (c *Config) supportedVersions(isClient bool) []uint16 {
func (c *Config) supportedVersions(isClient, isQUIC bool) []uint16 {
versions := make([]uint16, 0, len(supportedVersions))
for _, v := range supportedVersions {
if fips140tls.Required() && !slices.Contains(allowedSupportedVersionsFIPS, v) {
@ -1236,13 +1238,16 @@ func (c *Config) supportedVersions(isClient bool) []uint16 {
if c != nil && c.MaxVersion != 0 && v > c.MaxVersion {
continue
}
if isQUIC && v < VersionTLS13 {
continue
}
versions = append(versions, v)
}
return versions
}
func (c *Config) maxSupportedVersion(isClient bool) uint16 {
supportedVersions := c.supportedVersions(isClient)
func (c *Config) maxSupportedVersion(isClient, isQUIC bool) uint16 {
supportedVersions := c.supportedVersions(isClient, isQUIC)
if len(supportedVersions) == 0 {
return 0
}
@ -1294,8 +1299,8 @@ func (c *Config) supportsCurve(version uint16, x CurveID) bool {
// mutualVersion returns the protocol version to use given the advertised
// versions of the peer. The highest supported version is preferred.
func (c *Config) mutualVersion(isClient bool, peerVersions []uint16) (uint16, bool) {
supportedVersions := c.supportedVersions(isClient)
func (c *Config) mutualVersion(isClient, isQUIC bool, peerVersions []uint16) (uint16, bool) {
supportedVersions := c.supportedVersions(isClient, isQUIC)
for _, v := range supportedVersions {
if slices.Contains(peerVersions, v) {
return v, true
@ -1381,7 +1386,7 @@ func (chi *ClientHelloInfo) SupportsCertificate(c *Certificate) error {
if config == nil {
config = &Config{}
}
vers, ok := config.mutualVersion(roleServer, chi.SupportedVersions)
vers, ok := config.mutualVersion(roleServer, chi.isQUIC, chi.SupportedVersions)
if !ok {
return errors.New("no mutually supported protocol versions")
}

View file

@ -59,7 +59,7 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli
return nil, nil, nil, errors.New("tls: NextProtos values too large")
}
supportedVersions := config.supportedVersions(roleClient)
supportedVersions := config.supportedVersions(roleClient, c.quic != nil)
if len(supportedVersions) == 0 {
return nil, nil, nil, errors.New("tls: no supported versions satisfy MinVersion and MaxVersion")
}
@ -316,7 +316,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
// If we are negotiating a protocol version that's lower than what we
// support, check for the server downgrade canaries.
// See RFC 8446, Section 4.1.3.
maxVers := c.config.maxSupportedVersion(roleClient)
maxVers := c.config.maxSupportedVersion(roleClient, c.quic != nil)
tls12Downgrade := string(serverHello.random[24:]) == downgradeCanaryTLS12
tls11Downgrade := string(serverHello.random[24:]) == downgradeCanaryTLS11
if maxVers == VersionTLS13 && c.vers <= VersionTLS12 && (tls12Downgrade || tls11Downgrade) ||
@ -508,7 +508,7 @@ func (c *Conn) pickTLSVersion(serverHello *serverHelloMsg) error {
peerVersion = serverHello.supportedVersion
}
vers, ok := c.config.mutualVersion(roleClient, []uint16{peerVersion})
vers, ok := c.config.mutualVersion(roleClient, c.quic != nil, []uint16{peerVersion})
if !ok {
c.sendAlert(alertProtocolVersion)
return fmt.Errorf("tls: server selected unsupported protocol version %x", peerVersion)

View file

@ -188,7 +188,7 @@ func (c *Conn) readClientHello(ctx context.Context) (*clientHelloMsg, *echServer
} else if len(clientVersions) == 0 {
clientVersions = supportedVersionsFromMax(clientHello.vers)
}
c.vers, ok = c.config.mutualVersion(roleServer, clientVersions)
c.vers, ok = c.config.mutualVersion(roleServer, c.quic != nil, clientVersions)
if !ok {
c.sendAlert(alertProtocolVersion)
return nil, nil, fmt.Errorf("tls: client offered only unsupported versions: %x", clientVersions)
@ -235,7 +235,7 @@ func (hs *serverHandshakeState) processClientHello() error {
hs.hello.random = make([]byte, 32)
serverRandom := hs.hello.random
// Downgrade protection canaries. See RFC 8446, Section 4.1.3.
maxVers := c.config.maxSupportedVersion(roleServer)
maxVers := c.config.maxSupportedVersion(roleServer, c.quic != nil)
if maxVers >= VersionTLS12 && c.vers < maxVers || testingOnlyForceDowngradeCanary {
if c.vers == VersionTLS12 {
copy(serverRandom[24:], downgradeCanaryTLS12)
@ -410,7 +410,7 @@ func (hs *serverHandshakeState) pickCipherSuite() error {
for _, id := range hs.clientHello.cipherSuites {
if id == TLS_FALLBACK_SCSV {
// The client is doing a fallback connection. See RFC 7507.
if hs.clientHello.vers < c.config.maxSupportedVersion(roleServer) {
if hs.clientHello.vers < c.config.maxSupportedVersion(roleServer, c.quic != nil) {
c.sendAlert(alertInappropriateFallback)
return errors.New("tls: client using inappropriate protocol fallback")
}
@ -1037,6 +1037,7 @@ func clientHelloInfo(ctx context.Context, c *Conn, clientHello *clientHelloMsg)
Conn: conn,
HelloRetryRequest: c.didHRR,
config: c.config,
isQUIC: c.quic != nil,
ctx: ctx,
}
}

View file

@ -132,7 +132,7 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error {
if id == TLS_FALLBACK_SCSV {
// Use c.vers instead of max(supported_versions) because an attacker
// could defeat this by adding an arbitrary high version otherwise.
if c.vers < c.config.maxSupportedVersion(roleServer) {
if c.vers < c.config.maxSupportedVersion(roleServer, c.quic != nil) {
c.sendAlert(alertInappropriateFallback)
return errors.New("tls: client using inappropriate protocol fallback")
}

View file

@ -185,16 +185,12 @@ type quicState struct {
// QUICClient returns a new TLS client side connection using QUICTransport as the
// underlying transport. The config cannot be nil.
//
// The config's MinVersion must be at least TLS 1.3.
func QUICClient(config *QUICConfig) *QUICConn {
return newQUICConn(Client(nil, config.TLSConfig), config)
}
// QUICServer returns a new TLS server side connection using QUICTransport as the
// underlying transport. The config cannot be nil.
//
// The config's MinVersion must be at least TLS 1.3.
func QUICServer(config *QUICConfig) *QUICConn {
return newQUICConn(Server(nil, config.TLSConfig), config)
}
@ -221,9 +217,6 @@ func (q *QUICConn) Start(ctx context.Context) error {
return quicError(errors.New("tls: Start called more than once"))
}
q.conn.quic.started = true
if q.conn.config.MinVersion < VersionTLS13 {
return quicError(errors.New("tls: Config MinVersion must be at least TLS 1.3"))
}
go q.conn.HandshakeContext(ctx)
if _, ok := <-q.conn.quic.blockedc; !ok {
return q.conn.handshakeErr

View file

@ -215,6 +215,59 @@ func TestQUICConnection(t *testing.T) {
}
}
func TestQUICVersions(t *testing.T) {
for _, tc := range []struct {
name string
clientMin uint16
clientMax uint16
serverMin uint16
serverMax uint16
wantErr bool
}{
{
name: "defaults",
},
{
name: "MinVersion TLS 1.2",
clientMin: VersionTLS12,
serverMin: VersionTLS12,
},
{
name: "MinVersion TLS 1.3",
clientMin: VersionTLS13,
serverMin: VersionTLS13,
},
{
name: "client MaxVersion TLS 1.2",
clientMax: VersionTLS12,
wantErr: true,
},
{
name: "server MaxVersion TLS 1.2",
serverMax: VersionTLS12,
wantErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
client := testConfig.Clone()
client.MinVersion = tc.clientMin
client.MaxVersion = tc.clientMax
server := testConfig.Clone()
server.MinVersion = tc.serverMin
server.MaxVersion = tc.serverMax
cli := newTestQUICClient(t, &QUICConfig{TLSConfig: client})
cli.conn.SetTransportParameters(nil)
srv := newTestQUICServer(t, &QUICConfig{TLSConfig: server})
srv.conn.SetTransportParameters(nil)
err := runTestQUICConnection(context.Background(), cli, srv, nil)
if tc.wantErr == (err == nil) {
t.Errorf("got err=%v, wantErr=%v", err, tc.wantErr)
}
})
}
}
func TestQUICSessionResumption(t *testing.T) {
clientConfig := &QUICConfig{TLSConfig: testConfigClient.Clone()}
clientConfig.TLSConfig.MinVersion = VersionTLS13