// 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 ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/binary" "net/netip" "time" "golang.org/x/crypto/chacha20poly1305" ) // AEAD and nonce used to compute the Retry Integrity Tag. // https://www.rfc-editor.org/rfc/rfc9001#section-5.8 var ( retrySecret = []byte{0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e} retryNonce = []byte{0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb} retryAEAD = func() cipher.AEAD { c, err := aes.NewCipher(retrySecret) if err != nil { panic(err) } aead, err := cipher.NewGCM(c) if err != nil { panic(err) } return aead }() ) // retryTokenValidityPeriod is how long we accept a Retry packet token after sending it. const retryTokenValidityPeriod = 5 * time.Second // retryState generates and validates an endpoint's retry tokens. type retryState struct { aead cipher.AEAD } func (rs *retryState) init() error { // Retry tokens are authenticated using a per-server key chosen at start time. // TODO: Provide a way for the user to set this key. secret := make([]byte, chacha20poly1305.KeySize) if _, err := rand.Read(secret); err != nil { return err } aead, err := chacha20poly1305.NewX(secret) if err != nil { panic(err) } rs.aead = aead return nil } // Retry tokens are encrypted with an AEAD. // The plaintext contains the time the token was created and // the original destination connection ID. // The additional data contains the sender's source address and original source connection ID. // The token nonce is randomly generated. // We use the nonce as the Source Connection ID of the Retry packet. // Since the 24-byte XChaCha20-Poly1305 nonce is too large to fit in a 20-byte connection ID, // we include the remaining 4 bytes of nonce in the token. // // Token { // Last 4 Bytes of Nonce (32), // Ciphertext (..), // } // // Plaintext { // Timestamp (64), // Original Destination Connection ID, // } // // // Additional Data { // Original Source Connection ID Length (8), // Original Source Connection ID (..), // IP Address (32..128), // Port (16), // } // // TODO: Consider using AES-256-GCM-SIV once crypto/tls supports it. func (rs *retryState) makeToken(now time.Time, srcConnID, origDstConnID []byte, addr netip.AddrPort) (token, newDstConnID []byte, err error) { nonce := make([]byte, rs.aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, nil, err } var plaintext []byte plaintext = binary.BigEndian.AppendUint64(plaintext, uint64(now.Unix())) plaintext = append(plaintext, origDstConnID...) token = append(token, nonce[maxConnIDLen:]...) token = rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr)) return token, nonce[:maxConnIDLen], nil } func (rs *retryState) validateToken(now time.Time, token, srcConnID, dstConnID []byte, addr netip.AddrPort) (origDstConnID []byte, ok bool) { tokenNonceLen := rs.aead.NonceSize() - maxConnIDLen if len(token) < tokenNonceLen { return nil, false } nonce := append([]byte{}, dstConnID...) nonce = append(nonce, token[:tokenNonceLen]...) ciphertext := token[tokenNonceLen:] plaintext, err := rs.aead.Open(nil, nonce, ciphertext, rs.additionalData(srcConnID, addr)) if err != nil { return nil, false } if len(plaintext) < 8 { return nil, false } when := time.Unix(int64(binary.BigEndian.Uint64(plaintext)), 0) origDstConnID = plaintext[8:] // We allow for tokens created in the future (up to the validity period), // which likely indicates that the system clock was adjusted backwards. if d := abs(now.Sub(when)); d > retryTokenValidityPeriod { return nil, false } return origDstConnID, true } func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []byte { var additional []byte additional = appendUint8Bytes(additional, srcConnID) additional = append(additional, addr.Addr().AsSlice()...) additional = binary.BigEndian.AppendUint16(additional, addr.Port()) return additional } func (e *Endpoint) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) { // The retry token is at the start of an Initial packet's data. token, n := consumeUint8Bytes(p.data) if n < 0 { // We've already validated that the packet is at least 1200 bytes long, // so there's no way for even a maximum size token to not fit. // Check anyway. return nil, false } if len(token) == 0 { // The sender has not provided a token. // Send a Retry packet to them with one. e.sendRetry(now, p, addr) return nil, false } origDstConnID, ok = e.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr) if !ok { // This does not seem to be a valid token. // Close the connection with an INVALID_TOKEN error. // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5 e.sendConnectionClose(p, addr, errInvalidToken) return nil, false } return origDstConnID, true } func (e *Endpoint) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) { token, srcConnID, err := e.retry.makeToken(now, p.srcConnID, p.dstConnID, addr) if err != nil { return } b := encodeRetryPacket(p.dstConnID, retryPacket{ dstConnID: p.srcConnID, srcConnID: srcConnID, token: token, }) e.sendDatagram(b, addr) } type retryPacket struct { dstConnID []byte srcConnID []byte token []byte } func encodeRetryPacket(originalDstConnID []byte, p retryPacket) []byte { // Retry packets include an integrity tag, computed by AEAD_AES_128_GCM over // the original destination connection ID followed by the Retry packet // (less the integrity tag itself). // https://www.rfc-editor.org/rfc/rfc9001#section-5.8 // // Create the pseudo-packet (including the original DCID), append the tag, // and return the Retry packet. var b []byte b = appendUint8Bytes(b, originalDstConnID) // Original Destination Connection ID start := len(b) // start of the Retry packet b = append(b, headerFormLong|fixedBit|longPacketTypeRetry) b = binary.BigEndian.AppendUint32(b, quicVersion1) // Version b = appendUint8Bytes(b, p.dstConnID) // Destination Connection ID b = appendUint8Bytes(b, p.srcConnID) // Source Connection ID b = append(b, p.token...) // Token b = retryAEAD.Seal(b, retryNonce, nil, b) // Retry Integrity Tag return b[start:] } func parseRetryPacket(b, origDstConnID []byte) (p retryPacket, ok bool) { const retryIntegrityTagLength = 128 / 8 lp, ok := parseGenericLongHeaderPacket(b) if !ok { return retryPacket{}, false } if len(lp.data) < retryIntegrityTagLength { return retryPacket{}, false } gotTag := lp.data[len(lp.data)-retryIntegrityTagLength:] // Create the pseudo-packet consisting of the original destination connection ID // followed by the Retry packet (less the integrity tag). // Use this to validate the packet integrity tag. pseudo := appendUint8Bytes(nil, origDstConnID) pseudo = append(pseudo, b[:len(b)-retryIntegrityTagLength]...) wantTag := retryAEAD.Seal(nil, retryNonce, nil, pseudo) if !bytes.Equal(gotTag, wantTag) { return retryPacket{}, false } token := lp.data[:len(lp.data)-retryIntegrityTagLength] return retryPacket{ dstConnID: lp.dstConnID, srcConnID: lp.srcConnID, token: token, }, true }