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 }