Skip to content

Commit 8add2e1

Browse files
committed
quic: enforce AEAD integrity limit
Immediately close a connection after receiving too many packets which fail authentication. RFC 9001, section 6.6. For golang/go#58547 Change-Id: I646b1e89d93fc013f35a2e7b751c4f7b578f42a9 Reviewed-on: https://go-review.googlesource.com/c/net/+/529596 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 7c40cbd commit 8add2e1

File tree

5 files changed

+114
-26
lines changed

5 files changed

+114
-26
lines changed

internal/quic/conn_recv.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,13 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int {
8989
}
9090

9191
pnumMax := c.acks[appDataSpace].largestSeen()
92-
p, n := parse1RTTPacket(buf, &c.keysAppData, connIDLen, pnumMax)
93-
if n < 0 {
92+
p, err := parse1RTTPacket(buf, &c.keysAppData, connIDLen, pnumMax)
93+
if err != nil {
94+
// A localTransportError terminates the connection.
95+
// Other errors indicate an unparseable packet, but otherwise may be ignored.
96+
if _, ok := err.(localTransportError); ok {
97+
c.abort(now, err)
98+
}
9499
return -1
95100
}
96101
if buf[0]&reserved1RTTBits != 0 {

internal/quic/packet_codec_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ func TestRoundtripEncodeShortPacket(t *testing.T) {
193193
w.b = append(w.b, test.payload...)
194194
w.finish1RTTPacket(test.num, 0, connID, &test.k)
195195
pkt := w.datagram()
196-
p, n := parse1RTTPacket(pkt, &test.k, connIDLen, 0)
197-
if n != len(pkt) {
198-
t.Errorf("parse1RTTPacket: n=%v, want %v", n, len(pkt))
196+
p, err := parse1RTTPacket(pkt, &test.k, connIDLen, 0)
197+
if err != nil {
198+
t.Errorf("parse1RTTPacket: err=%v, want nil", err)
199199
}
200200
if p.num != test.num || !bytes.Equal(p.payload, test.payload) {
201201
t.Errorf("Round-trip encode/decode did not preserve packet.\nsent: num=%v, payload={%x}\ngot: num=%v, payload={%x}", test.num, test.payload, p.num, p.payload)

internal/quic/packet_parser.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,14 @@ func skipLongHeaderPacket(pkt []byte) int {
143143
//
144144
// On input, pkt contains a short header packet, k the decryption keys for the packet,
145145
// and pnumMax the largest packet number seen in the number space of this packet.
146-
func parse1RTTPacket(pkt []byte, k *updatingKeyPair, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, n int) {
146+
func parse1RTTPacket(pkt []byte, k *updatingKeyPair, dstConnIDLen int, pnumMax packetNumber) (p shortPacket, err error) {
147147
pay, pnum, err := k.unprotect(pkt, 1+dstConnIDLen, pnumMax)
148148
if err != nil {
149-
return shortPacket{}, -1
149+
return shortPacket{}, err
150150
}
151151
p.num = pnum
152152
p.payload = pay
153-
return p, len(pkt)
153+
return p, nil
154154
}
155155

156156
// Consume functions return n=-1 on conditions which result in FRAME_ENCODING_ERROR,

internal/quic/packet_protection.go

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,13 @@ func updateSecret(suite uint16, secret []byte) (nextSecret []byte) {
336336
// of reordered packets before discarding the previous phase's keys after
337337
// an update.
338338
type updatingKeyPair struct {
339-
phase uint8 // current key phase (r.pkt[0], w.pkt[0])
340-
updating bool
341-
minSent packetNumber // min packet number sent since entering the updating state
342-
minReceived packetNumber // min packet number received in the next phase
343-
updateAfter packetNumber // packet number after which to initiate key update
344-
r, w updatingKeys
339+
phase uint8 // current key phase (r.pkt[0], w.pkt[0])
340+
updating bool
341+
authFailures int64 // total packet unprotect failures
342+
minSent packetNumber // min packet number sent since entering the updating state
343+
minReceived packetNumber // min packet number received in the next phase
344+
updateAfter packetNumber // packet number after which to initiate key update
345+
r, w updatingKeys
345346
}
346347

347348
func (k *updatingKeyPair) init() {
@@ -424,26 +425,45 @@ func (k *updatingKeyPair) unprotect(pkt []byte, pnumOff int, pnumMax packetNumbe
424425
// Otherwise, use the next phase.
425426
if hdr[0]&keyPhaseBit == k.phase && (!k.updating || pnum < k.minReceived) {
426427
pay, err = k.r.pkt[0].unprotect(hdr, pay, pnum)
427-
if err != nil {
428-
return nil, 0, err
429-
}
430428
} else {
431429
pay, err = k.r.pkt[1].unprotect(hdr, pay, pnum)
432-
if err != nil {
433-
return nil, 0, err
430+
if err == nil {
431+
if !k.updating {
432+
// The peer has initiated a key update.
433+
k.updating = true
434+
k.minSent = maxPacketNumber
435+
k.minReceived = pnum
436+
} else {
437+
k.minReceived = min(pnum, k.minReceived)
438+
}
434439
}
435-
if !k.updating {
436-
// The peer has initiated a key update.
437-
k.updating = true
438-
k.minSent = maxPacketNumber
439-
k.minReceived = pnum
440-
} else {
441-
k.minReceived = min(pnum, k.minReceived)
440+
}
441+
if err != nil {
442+
k.authFailures++
443+
if k.authFailures >= aeadIntegrityLimit(k.r.suite) {
444+
return nil, 0, localTransportError(errAEADLimitReached)
442445
}
446+
return nil, 0, err
443447
}
444448
return pay, pnum, nil
445449
}
446450

451+
// aeadIntegrityLimit returns the integrity limit for an AEAD:
452+
// The maximum number of received packets that may fail authentication
453+
// before closing the connection.
454+
//
455+
// https://www.rfc-editor.org/rfc/rfc9001#section-6.6-4
456+
func aeadIntegrityLimit(suite uint16) int64 {
457+
switch suite {
458+
case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384:
459+
return 1 << 52
460+
case tls.TLS_CHACHA20_POLY1305_SHA256:
461+
return 1 << 36
462+
default:
463+
panic("BUG: unknown cipher suite")
464+
}
465+
}
466+
447467
// https://www.rfc-editor.org/rfc/rfc9001#section-5.2-2
448468
var initialSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
449469

internal/quic/tls_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,66 @@ func TestConnCryptoBufferSizeExceeded(t *testing.T) {
538538
code: errCryptoBufferExceeded,
539539
})
540540
}
541+
542+
func TestConnAEADLimitReached(t *testing.T) {
543+
// "[...] endpoints MUST count the number of received packets that
544+
// fail authentication during the lifetime of a connection.
545+
// If the total number of received packets that fail authentication [...]
546+
// exceeds the integrity limit for the selected AEAD,
547+
// the endpoint MUST immediately close the connection [...]"
548+
// https://www.rfc-editor.org/rfc/rfc9001#section-6.6-6
549+
tc := newTestConn(t, clientSide)
550+
tc.handshake()
551+
552+
var limit int64
553+
switch suite := tc.conn.keysAppData.r.suite; suite {
554+
case tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384:
555+
limit = 1 << 52
556+
case tls.TLS_CHACHA20_POLY1305_SHA256:
557+
limit = 1 << 36
558+
default:
559+
t.Fatalf("conn.keysAppData.r.suite = %v, unknown suite", suite)
560+
}
561+
562+
dstConnID := tc.conn.connIDState.local[0].cid
563+
if tc.conn.connIDState.local[0].seq == -1 {
564+
// Only use the transient connection ID in Initial packets.
565+
dstConnID = tc.conn.connIDState.local[1].cid
566+
}
567+
invalid := tc.encodeTestPacket(&testPacket{
568+
ptype: packetType1RTT,
569+
num: 1000,
570+
frames: []debugFrame{debugFramePing{}},
571+
version: 1,
572+
dstConnID: dstConnID,
573+
srcConnID: tc.peerConnID,
574+
}, 0)
575+
invalid[len(invalid)-1] ^= 1
576+
sendInvalid := func() {
577+
t.Logf("<- conn under test receives invalid datagram")
578+
tc.conn.sendMsg(&datagram{
579+
b: invalid,
580+
})
581+
tc.wait()
582+
}
583+
584+
// Set the conn's auth failure count to just before the AEAD integrity limit.
585+
tc.conn.keysAppData.authFailures = limit - 1
586+
587+
tc.writeFrames(packetType1RTT, debugFramePing{})
588+
tc.advanceToTimer()
589+
tc.wantFrameType("auth failures less than limit: conn ACKs packet",
590+
packetType1RTT, debugFrameAck{})
591+
592+
sendInvalid()
593+
tc.writeFrames(packetType1RTT, debugFramePing{})
594+
tc.advanceToTimer()
595+
tc.wantFrameType("auth failures at limit: conn closes",
596+
packetType1RTT, debugFrameConnectionCloseTransport{
597+
code: errAEADLimitReached,
598+
})
599+
600+
tc.writeFrames(packetType1RTT, debugFramePing{})
601+
tc.advance(1 * time.Second)
602+
tc.wantIdle("auth failures at limit: conn does not process additional packets")
603+
}

0 commit comments

Comments
 (0)