blob: 14f74a00aa83d255372e606803ea17a0cdb31c10 [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.21
package quic
import (
"crypto/tls"
"crypto/x509"
"errors"
"reflect"
"testing"
"time"
)
// handshake executes the handshake.
func (tc *testConn) handshake() {
tc.t.Helper()
if *testVV {
*testVV = false
defer func() {
tc.t.Helper()
*testVV = true
tc.t.Logf("performed connection handshake")
}()
}
defer func(saved map[byte]bool) {
tc.ignoreFrames = saved
}(tc.ignoreFrames)
tc.ignoreFrames = nil
t := tc.t
dgrams := handshakeDatagrams(tc)
i := 0
for {
if i == len(dgrams)-1 {
if tc.conn.side == clientSide {
want := tc.endpoint.now.Add(maxAckDelay - timerGranularity)
if !tc.timer.Equal(want) {
t.Fatalf("want timer = %v (max_ack_delay), got %v", want, tc.timer)
}
if got := tc.readDatagram(); got != nil {
t.Fatalf("client unexpectedly sent: %v", got)
}
}
tc.advance(maxAckDelay)
}
// Check that we're sending exactly the data we expect.
// Any variation from the norm here should be intentional.
got := tc.readDatagram()
var want *testDatagram
if !(tc.conn.side == serverSide && i == 0) && i < len(dgrams) {
want = dgrams[i]
fillCryptoFrames(want, tc.cryptoDataOut)
i++
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("dgram %v:\ngot %v\n\nwant %v", i, got, want)
}
if i >= len(dgrams) {
break
}
fillCryptoFrames(dgrams[i], tc.cryptoDataIn)
tc.write(dgrams[i])
i++
}
}
func handshakeDatagrams(tc *testConn) (dgrams []*testDatagram) {
var (
clientConnIDs [][]byte
serverConnIDs [][]byte
clientResetToken statelessResetToken
serverResetToken statelessResetToken
transientConnID []byte
)
localConnIDs := [][]byte{
testLocalConnID(0),
testLocalConnID(1),
}
peerConnIDs := [][]byte{
testPeerConnID(0),
testPeerConnID(1),
}
localResetToken := tc.endpoint.e.resetGen.tokenForConnID(localConnIDs[1])
peerResetToken := testPeerStatelessResetToken(1)
if tc.conn.side == clientSide {
clientConnIDs = localConnIDs
serverConnIDs = peerConnIDs
clientResetToken = localResetToken
serverResetToken = peerResetToken
transientConnID = testLocalConnID(-1)
} else {
clientConnIDs = peerConnIDs
serverConnIDs = localConnIDs
clientResetToken = peerResetToken
serverResetToken = localResetToken
transientConnID = testPeerConnID(-1)
}
return []*testDatagram{{
// Client Initial
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 0,
version: quicVersion1,
srcConnID: clientConnIDs[0],
dstConnID: transientConnID,
frames: []debugFrame{
debugFrameCrypto{},
},
}},
paddedSize: 1200,
}, {
// Server Initial + Handshake + 1-RTT
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 0,
version: quicVersion1,
srcConnID: serverConnIDs[0],
dstConnID: clientConnIDs[0],
frames: []debugFrame{
debugFrameAck{
ranges: []i64range[packetNumber]{{0, 1}},
},
debugFrameCrypto{},
},
}, {
ptype: packetTypeHandshake,
num: 0,
version: quicVersion1,
srcConnID: serverConnIDs[0],
dstConnID: clientConnIDs[0],
frames: []debugFrame{
debugFrameCrypto{},
},
}, {
ptype: packetType1RTT,
num: 0,
dstConnID: clientConnIDs[0],
frames: []debugFrame{
debugFrameNewConnectionID{
seq: 1,
connID: serverConnIDs[1],
token: serverResetToken,
},
},
}},
paddedSize: 1200,
}, {
// Client Initial + Handshake + 1-RTT
packets: []*testPacket{{
ptype: packetTypeInitial,
num: 1,
version: quicVersion1,
srcConnID: clientConnIDs[0],
dstConnID: serverConnIDs[0],
frames: []debugFrame{
debugFrameAck{
ranges: []i64range[packetNumber]{{0, 1}},
},
},
}, {
ptype: packetTypeHandshake,
num: 0,
version: quicVersion1,
srcConnID: clientConnIDs[0],
dstConnID: serverConnIDs[0],
frames: []debugFrame{
debugFrameAck{
ranges: []i64range[packetNumber]{{0, 1}},
},
debugFrameCrypto{},
},
}, {
ptype: packetType1RTT,
num: 0,
dstConnID: serverConnIDs[0],
frames: []debugFrame{
debugFrameAck{
ranges: []i64range[packetNumber]{{0, 1}},
},
debugFrameNewConnectionID{
seq: 1,
connID: clientConnIDs[1],
token: clientResetToken,
},
},
}},
paddedSize: 1200,
}, {
// Server HANDSHAKE_DONE
packets: []*testPacket{{
ptype: packetType1RTT,
num: 1,
dstConnID: clientConnIDs[0],
frames: []debugFrame{
debugFrameAck{
ranges: []i64range[packetNumber]{{0, 1}},
},
debugFrameHandshakeDone{},
},
}},
}, {
// Client ack (after max_ack_delay)
packets: []*testPacket{{
ptype: packetType1RTT,
num: 1,
dstConnID: serverConnIDs[0],
frames: []debugFrame{
debugFrameAck{
ackDelay: unscaledAckDelayFromDuration(
maxAckDelay, ackDelayExponent),
ranges: []i64range[packetNumber]{{0, 2}},
},
},
}},
}}
}
func fillCryptoFrames(d *testDatagram, data map[tls.QUICEncryptionLevel][]byte) {
for _, p := range d.packets {
var level tls.QUICEncryptionLevel
switch p.ptype {
case packetTypeInitial:
level = tls.QUICEncryptionLevelInitial
case packetTypeHandshake:
level = tls.QUICEncryptionLevelHandshake
case packetType1RTT:
level = tls.QUICEncryptionLevelApplication
default:
continue
}
for i := range p.frames {
c, ok := p.frames[i].(debugFrameCrypto)
if !ok {
continue
}
c.data = data[level]
data[level] = nil
p.frames[i] = c
}
}
}
// uncheckedHandshake executes the handshake.
//
// Unlike testConn.handshake, it sends nothing unnecessary
// (in particular, no NEW_CONNECTION_ID frames),
// and does not validate the conn's responses.
//
// Useful for testing scenarios where configuration has
// changed the handshake responses in some way.
func (tc *testConn) uncheckedHandshake() {
tc.t.Helper()
defer func(saved map[byte]bool) {
tc.ignoreFrames = saved
}(tc.ignoreFrames)
tc.ignoreFrames = map[byte]bool{
frameTypeAck: true,
frameTypeCrypto: true,
frameTypeNewConnectionID: true,
}
if tc.conn.side == serverSide {
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("send HANDSHAKE_DONE after handshake completes",
packetType1RTT, debugFrameHandshakeDone{})
tc.writeFrames(packetType1RTT,
debugFrameAck{
ackDelay: unscaledAckDelayFromDuration(
maxAckDelay, ackDelayExponent),
ranges: []i64range[packetNumber]{{0, tc.lastPacket.num + 1}},
})
} else {
tc.wantIdle("initial frames are ignored")
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantIdle("don't expect any frames we aren't ignoring")
// Send the next two frames in separate packets, so the client sends an
// ack immediately without delay. We want to consume that ack here, rather
// than returning with a delayed ack waiting to be sent.
tc.ignoreFrames = nil
tc.writeFrames(packetType1RTT,
debugFrameHandshakeDone{})
tc.writeFrames(packetType1RTT,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelApplication],
})
tc.wantFrame("client ACKs server's first 1-RTT packet",
packetType1RTT, debugFrameAck{
ranges: []i64range[packetNumber]{{0, 2}},
})
}
tc.wantIdle("handshake is done")
}
func TestConnClientHandshake(t *testing.T) {
tc := newTestConn(t, clientSide)
tc.handshake()
tc.advance(1 * time.Second)
tc.wantIdle("no packets should be sent by an idle conn after the handshake")
}
func TestConnServerHandshake(t *testing.T) {
tc := newTestConn(t, serverSide)
tc.handshake()
tc.advance(1 * time.Second)
tc.wantIdle("no packets should be sent by an idle conn after the handshake")
}
func TestConnKeysDiscardedClient(t *testing.T) {
tc := newTestConn(t, clientSide)
tc.ignoreFrame(frameTypeAck)
tc.wantFrame("client sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("client sends Handshake CRYPTO frame",
packetTypeHandshake, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("client provides an additional connection ID",
packetType1RTT, debugFrameNewConnectionID{
seq: 1,
connID: testLocalConnID(1),
token: testLocalStatelessResetToken(1),
})
// The client discards Initial keys after sending a Handshake packet.
tc.writeFrames(packetTypeInitial,
debugFrameConnectionCloseTransport{code: errInternal})
tc.wantIdle("client has discarded Initial keys, cannot read CONNECTION_CLOSE")
// The client discards Handshake keys after receiving a HANDSHAKE_DONE frame.
tc.writeFrames(packetType1RTT,
debugFrameHandshakeDone{})
tc.writeFrames(packetTypeHandshake,
debugFrameConnectionCloseTransport{code: errInternal})
tc.wantIdle("client has discarded Handshake keys, cannot read CONNECTION_CLOSE")
tc.writeFrames(packetType1RTT,
debugFrameConnectionCloseTransport{code: errInternal})
tc.conn.Abort(nil)
tc.wantFrame("client closes connection after 1-RTT CONNECTION_CLOSE",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errNo,
})
}
func TestConnKeysDiscardedServer(t *testing.T) {
tc := newTestConn(t, serverSide)
tc.ignoreFrame(frameTypeAck)
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.wantFrame("server sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.wantFrame("server sends Handshake CRYPTO frame",
packetTypeHandshake, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake],
})
// The server discards Initial keys after receiving a Handshake packet.
// The Handshake packet contains only the start of the client's CRYPTO flight here,
// to avoids completing the handshake yet.
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][:1],
})
tc.writeFrames(packetTypeInitial,
debugFrameConnectionCloseTransport{code: errInternal})
tc.wantFrame("server provides an additional connection ID",
packetType1RTT, debugFrameNewConnectionID{
seq: 1,
connID: testLocalConnID(1),
token: testLocalStatelessResetToken(1),
})
tc.wantIdle("server has discarded Initial keys, cannot read CONNECTION_CLOSE")
// The server discards Handshake keys after sending a HANDSHAKE_DONE frame.
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
off: 1,
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][1:],
})
tc.wantFrame("server sends HANDSHAKE_DONE after handshake completes",
packetType1RTT, debugFrameHandshakeDone{})
tc.writeFrames(packetTypeHandshake,
debugFrameConnectionCloseTransport{code: errInternal})
tc.wantIdle("server has discarded Handshake keys, cannot read CONNECTION_CLOSE")
tc.writeFrames(packetType1RTT,
debugFrameConnectionCloseTransport{code: errInternal})
tc.conn.Abort(nil)
tc.wantFrame("server closes connection after 1-RTT CONNECTION_CLOSE",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errNo,
})
}
func TestConnInvalidCryptoData(t *testing.T) {
tc := newTestConn(t, clientSide)
tc.ignoreFrame(frameTypeAck)
tc.wantFrame("client sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
// Render the server's response invalid.
//
// The client closes the connection with CRYPTO_ERROR.
//
// Changing the first byte will change the TLS message type,
// so we can reasonably assume that this is an unexpected_message alert (10).
tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][0] ^= 0x1
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("client closes connection due to TLS handshake error",
packetTypeInitial, debugFrameConnectionCloseTransport{
code: errTLSBase + 10,
})
}
func TestConnInvalidPeerCertificate(t *testing.T) {
tc := newTestConn(t, clientSide, func(c *tls.Config) {
c.VerifyPeerCertificate = func([][]byte, [][]*x509.Certificate) error {
return errors.New("I will not buy this certificate. It is scratched.")
}
})
tc.ignoreFrame(frameTypeAck)
tc.wantFrame("client sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
})
tc.wantFrame("client closes connection due to rejecting server certificate",
packetTypeInitial, debugFrameConnectionCloseTransport{
code: errTLSBase + 42, // 42: bad_certificate
})
}
func TestConnHandshakeDoneSentToServer(t *testing.T) {
tc := newTestConn(t, serverSide)
tc.handshake()
tc.writeFrames(packetType1RTT,
debugFrameHandshakeDone{})
tc.wantFrame("server closes connection when client sends a HANDSHAKE_DONE frame",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errProtocolViolation,
})
}
func TestConnCryptoDataOutOfOrder(t *testing.T) {
tc := newTestConn(t, clientSide)
tc.ignoreFrame(frameTypeAck)
tc.wantFrame("client sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
})
tc.wantIdle("client is idle, server Handshake flight has not arrived")
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
off: 15,
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][15:],
})
tc.wantIdle("client is idle, server Handshake flight is not complete")
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
off: 1,
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][1:20],
})
tc.wantIdle("client is idle, server Handshake flight is still not complete")
tc.writeFrames(packetTypeHandshake,
debugFrameCrypto{
data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][0:1],
})
tc.wantFrame("client sends Handshake CRYPTO frame",
packetTypeHandshake, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake],
})
}
func TestConnCryptoBufferSizeExceeded(t *testing.T) {
tc := newTestConn(t, clientSide)
tc.ignoreFrame(frameTypeAck)
tc.wantFrame("client sends Initial CRYPTO frame",
packetTypeInitial, debugFrameCrypto{
data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
})
tc.writeFrames(packetTypeInitial,
debugFrameCrypto{
off: cryptoBufferSize,
data: []byte{0},
})
tc.wantFrame("client closes connection after server exceeds CRYPTO buffer",
packetTypeInitial, debugFrameConnectionCloseTransport{
code: errCryptoBufferExceeded,
})
}
func TestConnAEADLimitReached(t *testing.T) {
// "[...] endpoints MUST count the number of received packets that
// fail authentication during the lifetime of a connection.
// If the total number of received packets that fail authentication [...]
// exceeds the integrity limit for the selected AEAD,
// the endpoint MUST immediately close the connection [...]"
// https://www.rfc-editor.org/rfc/rfc9001#section-6.6-6
tc := newTestConn(t, clientSide, func(c *Config) {
clear(c.StatelessResetKey[:])
})
tc.handshake()
var limit int64
switch suite := tc.conn.keysAppData.r.suite; suite {
case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384:
limit = 1 << 52
case tls.TLS_CHACHA20_POLY1305_SHA256:
limit = 1 << 36
default:
t.Fatalf("conn.keysAppData.r.suite = %v, unknown suite", suite)
}
dstConnID := tc.conn.connIDState.local[0].cid
if tc.conn.connIDState.local[0].seq == -1 {
// Only use the transient connection ID in Initial packets.
dstConnID = tc.conn.connIDState.local[1].cid
}
invalid := encodeTestPacket(t, tc, &testPacket{
ptype: packetType1RTT,
num: 1000,
frames: []debugFrame{debugFramePing{}},
version: quicVersion1,
dstConnID: dstConnID,
srcConnID: tc.peerConnID,
}, 0)
invalid[len(invalid)-1] ^= 1
sendInvalid := func() {
t.Logf("<- conn under test receives invalid datagram")
tc.conn.sendMsg(&datagram{
b: invalid,
})
tc.wait()
}
// Set the conn's auth failure count to just before the AEAD integrity limit.
tc.conn.keysAppData.authFailures = limit - 1
tc.writeFrames(packetType1RTT, debugFramePing{})
tc.advanceToTimer()
tc.wantFrameType("auth failures less than limit: conn ACKs packet",
packetType1RTT, debugFrameAck{})
sendInvalid()
tc.writeFrames(packetType1RTT, debugFramePing{})
tc.advanceToTimer()
tc.wantFrameType("auth failures at limit: conn closes",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errAEADLimitReached,
})
tc.writeFrames(packetType1RTT, debugFramePing{})
tc.advance(1 * time.Second)
tc.wantIdle("auth failures at limit: conn does not process additional packets")
}