golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/quic/retry.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build go1.21
     6  
     7  package quic
     8  
     9  import (
    10  	"bytes"
    11  	"crypto/aes"
    12  	"crypto/cipher"
    13  	"crypto/rand"
    14  	"encoding/binary"
    15  	"net/netip"
    16  	"time"
    17  
    18  	"golang.org/x/crypto/chacha20poly1305"
    19  )
    20  
    21  // AEAD and nonce used to compute the Retry Integrity Tag.
    22  // https://www.rfc-editor.org/rfc/rfc9001#section-5.8
    23  var (
    24  	retrySecret = []byte{0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e}
    25  	retryNonce  = []byte{0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb}
    26  	retryAEAD   = func() cipher.AEAD {
    27  		c, err := aes.NewCipher(retrySecret)
    28  		if err != nil {
    29  			panic(err)
    30  		}
    31  		aead, err := cipher.NewGCM(c)
    32  		if err != nil {
    33  			panic(err)
    34  		}
    35  		return aead
    36  	}()
    37  )
    38  
    39  // retryTokenValidityPeriod is how long we accept a Retry packet token after sending it.
    40  const retryTokenValidityPeriod = 5 * time.Second
    41  
    42  // retryState generates and validates an endpoint's retry tokens.
    43  type retryState struct {
    44  	aead cipher.AEAD
    45  }
    46  
    47  func (rs *retryState) init() error {
    48  	// Retry tokens are authenticated using a per-server key chosen at start time.
    49  	// TODO: Provide a way for the user to set this key.
    50  	secret := make([]byte, chacha20poly1305.KeySize)
    51  	if _, err := rand.Read(secret); err != nil {
    52  		return err
    53  	}
    54  	aead, err := chacha20poly1305.NewX(secret)
    55  	if err != nil {
    56  		panic(err)
    57  	}
    58  	rs.aead = aead
    59  	return nil
    60  }
    61  
    62  // Retry tokens are encrypted with an AEAD.
    63  // The plaintext contains the time the token was created and
    64  // the original destination connection ID.
    65  // The additional data contains the sender's source address and original source connection ID.
    66  // The token nonce is randomly generated.
    67  // We use the nonce as the Source Connection ID of the Retry packet.
    68  // Since the 24-byte XChaCha20-Poly1305 nonce is too large to fit in a 20-byte connection ID,
    69  // we include the remaining 4 bytes of nonce in the token.
    70  //
    71  // Token {
    72  //   Last 4 Bytes of Nonce (32),
    73  //   Ciphertext (..),
    74  // }
    75  //
    76  // Plaintext {
    77  //   Timestamp (64),
    78  //   Original Destination Connection ID,
    79  // }
    80  //
    81  //
    82  // Additional Data {
    83  //   Original Source Connection ID Length (8),
    84  //   Original Source Connection ID (..),
    85  //   IP Address (32..128),
    86  //   Port (16),
    87  // }
    88  //
    89  // TODO: Consider using AES-256-GCM-SIV once crypto/tls supports it.
    90  
    91  func (rs *retryState) makeToken(now time.Time, srcConnID, origDstConnID []byte, addr netip.AddrPort) (token, newDstConnID []byte, err error) {
    92  	nonce := make([]byte, rs.aead.NonceSize())
    93  	if _, err := rand.Read(nonce); err != nil {
    94  		return nil, nil, err
    95  	}
    96  
    97  	var plaintext []byte
    98  	plaintext = binary.BigEndian.AppendUint64(plaintext, uint64(now.Unix()))
    99  	plaintext = append(plaintext, origDstConnID...)
   100  
   101  	token = append(token, nonce[maxConnIDLen:]...)
   102  	token = rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr))
   103  	return token, nonce[:maxConnIDLen], nil
   104  }
   105  
   106  func (rs *retryState) validateToken(now time.Time, token, srcConnID, dstConnID []byte, addr netip.AddrPort) (origDstConnID []byte, ok bool) {
   107  	tokenNonceLen := rs.aead.NonceSize() - maxConnIDLen
   108  	if len(token) < tokenNonceLen {
   109  		return nil, false
   110  	}
   111  	nonce := append([]byte{}, dstConnID...)
   112  	nonce = append(nonce, token[:tokenNonceLen]...)
   113  	ciphertext := token[tokenNonceLen:]
   114  
   115  	plaintext, err := rs.aead.Open(nil, nonce, ciphertext, rs.additionalData(srcConnID, addr))
   116  	if err != nil {
   117  		return nil, false
   118  	}
   119  	if len(plaintext) < 8 {
   120  		return nil, false
   121  	}
   122  	when := time.Unix(int64(binary.BigEndian.Uint64(plaintext)), 0)
   123  	origDstConnID = plaintext[8:]
   124  
   125  	// We allow for tokens created in the future (up to the validity period),
   126  	// which likely indicates that the system clock was adjusted backwards.
   127  	if d := abs(now.Sub(when)); d > retryTokenValidityPeriod {
   128  		return nil, false
   129  	}
   130  
   131  	return origDstConnID, true
   132  }
   133  
   134  func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []byte {
   135  	var additional []byte
   136  	additional = appendUint8Bytes(additional, srcConnID)
   137  	additional = append(additional, addr.Addr().AsSlice()...)
   138  	additional = binary.BigEndian.AppendUint16(additional, addr.Port())
   139  	return additional
   140  }
   141  
   142  func (e *Endpoint) validateInitialAddress(now time.Time, p genericLongPacket, peerAddr netip.AddrPort) (origDstConnID []byte, ok bool) {
   143  	// The retry token is at the start of an Initial packet's data.
   144  	token, n := consumeUint8Bytes(p.data)
   145  	if n < 0 {
   146  		// We've already validated that the packet is at least 1200 bytes long,
   147  		// so there's no way for even a maximum size token to not fit.
   148  		// Check anyway.
   149  		return nil, false
   150  	}
   151  	if len(token) == 0 {
   152  		// The sender has not provided a token.
   153  		// Send a Retry packet to them with one.
   154  		e.sendRetry(now, p, peerAddr)
   155  		return nil, false
   156  	}
   157  	origDstConnID, ok = e.retry.validateToken(now, token, p.srcConnID, p.dstConnID, peerAddr)
   158  	if !ok {
   159  		// This does not seem to be a valid token.
   160  		// Close the connection with an INVALID_TOKEN error.
   161  		// https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5
   162  		e.sendConnectionClose(p, peerAddr, errInvalidToken)
   163  		return nil, false
   164  	}
   165  	return origDstConnID, true
   166  }
   167  
   168  func (e *Endpoint) sendRetry(now time.Time, p genericLongPacket, peerAddr netip.AddrPort) {
   169  	token, srcConnID, err := e.retry.makeToken(now, p.srcConnID, p.dstConnID, peerAddr)
   170  	if err != nil {
   171  		return
   172  	}
   173  	b := encodeRetryPacket(p.dstConnID, retryPacket{
   174  		dstConnID: p.srcConnID,
   175  		srcConnID: srcConnID,
   176  		token:     token,
   177  	})
   178  	e.sendDatagram(datagram{
   179  		b:        b,
   180  		peerAddr: peerAddr,
   181  	})
   182  }
   183  
   184  type retryPacket struct {
   185  	dstConnID []byte
   186  	srcConnID []byte
   187  	token     []byte
   188  }
   189  
   190  func encodeRetryPacket(originalDstConnID []byte, p retryPacket) []byte {
   191  	// Retry packets include an integrity tag, computed by AEAD_AES_128_GCM over
   192  	// the original destination connection ID followed by the Retry packet
   193  	// (less the integrity tag itself).
   194  	// https://www.rfc-editor.org/rfc/rfc9001#section-5.8
   195  	//
   196  	// Create the pseudo-packet (including the original DCID), append the tag,
   197  	// and return the Retry packet.
   198  	var b []byte
   199  	b = appendUint8Bytes(b, originalDstConnID) // Original Destination Connection ID
   200  	start := len(b)                            // start of the Retry packet
   201  	b = append(b, headerFormLong|fixedBit|longPacketTypeRetry)
   202  	b = binary.BigEndian.AppendUint32(b, quicVersion1) // Version
   203  	b = appendUint8Bytes(b, p.dstConnID)               // Destination Connection ID
   204  	b = appendUint8Bytes(b, p.srcConnID)               // Source Connection ID
   205  	b = append(b, p.token...)                          // Token
   206  	b = retryAEAD.Seal(b, retryNonce, nil, b)          // Retry Integrity Tag
   207  	return b[start:]
   208  }
   209  
   210  func parseRetryPacket(b, origDstConnID []byte) (p retryPacket, ok bool) {
   211  	const retryIntegrityTagLength = 128 / 8
   212  
   213  	lp, ok := parseGenericLongHeaderPacket(b)
   214  	if !ok {
   215  		return retryPacket{}, false
   216  	}
   217  	if len(lp.data) < retryIntegrityTagLength {
   218  		return retryPacket{}, false
   219  	}
   220  	gotTag := lp.data[len(lp.data)-retryIntegrityTagLength:]
   221  
   222  	// Create the pseudo-packet consisting of the original destination connection ID
   223  	// followed by the Retry packet (less the integrity tag).
   224  	// Use this to validate the packet integrity tag.
   225  	pseudo := appendUint8Bytes(nil, origDstConnID)
   226  	pseudo = append(pseudo, b[:len(b)-retryIntegrityTagLength]...)
   227  	wantTag := retryAEAD.Seal(nil, retryNonce, nil, pseudo)
   228  	if !bytes.Equal(gotTag, wantTag) {
   229  		return retryPacket{}, false
   230  	}
   231  
   232  	token := lp.data[:len(lp.data)-retryIntegrityTagLength]
   233  	return retryPacket{
   234  		dstConnID: lp.dstConnID,
   235  		srcConnID: lp.srcConnID,
   236  		token:     token,
   237  	}, true
   238  }