@amritk/nish-x86_64-linux 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/std/net/quic.ts CHANGED
@@ -58,10 +58,15 @@
58
58
  * its current value. When nothing is acknowledged for a probe timeout, with
59
59
  * its backoff, everything in flight is queued again and a probe goes out in
60
60
  * each space that has packets in flight, a PING if there is nothing to
61
- * resend (§6.2.4). NewReno's window holds back everything ack-eliciting but
62
- * a probe; an ACK always goes. PATH_RESPONSE and DATAGRAM are never sent
63
- * again (RFC 9000 §13.3, RFC 9221 §5.2). Pacing, and GSO batching, are the
64
- * carrier's: `nish/net/quic-listener`.
61
+ * resend (§6.2.4) — two datagrams of them during the handshake, each with
62
+ * the oldest CRYPTO data not acknowledged. A client that shows it lacks the
63
+ * server's handshake data (an Initial repeating CRYPTO data, an
64
+ * ack-eliciting Handshake packet) gets it again at once, a datagram each
65
+ * time, up to `QUIC_CONN_EARLY_RESENDS` times (§6.2.3). NewReno's window
66
+ * holds back everything ack-eliciting but a probe; an ACK always goes.
67
+ * PATH_RESPONSE and DATAGRAM are never sent again (RFC 9000 §13.3, RFC 9221
68
+ * §5.2). Pacing, and GSO batching, are the carrier's:
69
+ * `nish/net/quic-listener`.
65
70
  *
66
71
  * **Nothing allocated per packet** (QUIC-3). A connection is a slot: every
67
72
  * buffer it uses — the packet record, the stream buffers, the CRYPTO
@@ -70,11 +75,15 @@
70
75
  * the next peer. A packet is opened (`receiveWindow`) and a datagram built
71
76
  * and sealed (`takeDatagramInto`) inside a `using a = arena()` block, so the
72
77
  * AEAD's and the header protection's temporaries go when the block ends, and
73
- * the compiler refuses the block if anything in it could store one. What
74
- * still allocates is the handshake's, once a connection: `TlsServer` and its
75
- * key schedule (TLS-3), the packet keys of each level, the stateless reset
76
- * tokens and connection IDs (HMAC), and a key update (QUIC-4) — recorded in
77
- * `docs/security/quic.md`.
78
+ * the compiler refuses the block if anything in it could store one. The
79
+ * handshake keeps its state in the slot too: one `TlsServer`, made by the
80
+ * slot's first handshake and `restart`ed by each after it; every level's
81
+ * packet keys, and a key update's, derived in place into `QuicKeysSlot`s
82
+ * (`nish/net/quic-packet`) over the slot's HKDF scratch; the Initial
83
+ * secrets, the stateless reset tokens and the connection IDs computed in
84
+ * arena blocks; the transport parameters encoded and parsed into the
85
+ * slot's own objects. After the first connection in a slot, a connection
86
+ * keeps nothing at all (QUIC-3 in `docs/security/quic.md`).
78
87
  *
79
88
  * **Time.** The connection has no clock: `receive`, `takeDatagram` and
80
89
  * `handleTimer` take the caller's monotonic time in milliseconds, and
@@ -94,11 +103,11 @@
94
103
  * derivation and shows no timing difference (§6.3); if it opens, the client
95
104
  * has updated, and the server's write keys follow before anything is
96
105
  * acknowledged (§6.2). `updateKeys()` starts an update from the server once
97
- * the client has acknowledged a packet of the current phase (§6.1). Each
98
- * update the client starts costs two key derivations, which stay in the arena
99
- * (QUIC-4 in `docs/security/quic.md`), so a connection takes at most
100
- * `QUIC_CONN_MAX_KEY_UPDATES` of them and closes on the next with
101
- * KEY_UPDATE_ERROR.
106
+ * the client has acknowledged a packet of the current phase (§6.1). The
107
+ * generations take turns in two key slots and two secrets a direction, so an
108
+ * update keeps nothing in the arena; each still costs two key derivations of
109
+ * work, so a connection takes at most `QUIC_CONN_MAX_KEY_UPDATES` updates the
110
+ * client starts and closes on the next with KEY_UPDATE_ERROR (QUIC-4).
102
111
  *
103
112
  * **Sans-IO and deterministic.** No socket, no clock and no random device:
104
113
  * every random choice (the TLS server random and ephemeral key, the server's
@@ -122,25 +131,27 @@
122
131
  * `maxData`, the connection-ID table by the limit advertised, the received
123
132
  * packet numbers by `QUIC_ACK_MAX_RANGES`, and datagrams by their ring.
124
133
  *
125
- * **Secrets.** The packet keys of each level live in this connection's
126
- * fields as long as the level does, and in `TlsServer`'s (TLS-1); so do the
134
+ * **Secrets.** The packet keys of each level live in this connection's key
135
+ * slots as long as the level does, and in `TlsServer`'s (TLS-1); so do the
127
136
  * 1-RTT secrets the next key generation is derived from, and the next
128
137
  * generation's read keys. A `Secret` may not be a field (NL2430), so they are
129
138
  * plain bytes; `secureZero` wipes each level's key, IV and header-protection
130
- * key when the level is discarded, each generation's key, IV and secret when
131
- * a key update replaces it, and `release()` (and `reset`) the rest — the
132
- * 1-RTT keys, the traffic secrets `TlsServer` holds, its ephemeral key and
133
- * the connection-ID seed. What no wipe reaches (the expanded AES key
134
- * schedules, the HKDF and HMAC intermediates in arena memory) is recorded as
135
- * QUIC-2 in `docs/security/quic.md`, with #430, the follow-up that moves
136
- * these structs onto `nish:secret`.
139
+ * key, and its AES schedules with ordinary stores, when the level is
140
+ * discarded, each generation's key, IV and secret when a key update
141
+ * replaces it, and `release()` (and `reset`) the rest — the 1-RTT keys, the
142
+ * key-update secrets, the traffic secrets `TlsServer` holds, its ephemeral
143
+ * key and the connection-ID seed. What no wipe reaches (`aesKeyInto`'s
144
+ * working copy of a schedule) is recorded as QUIC-2 in
145
+ * `docs/security/quic.md`, with #430, the follow-up that moves these structs
146
+ * onto `nish:secret`.
137
147
  *
138
148
  * Written from RFC 9000, RFC 9001 and RFC 9221, in this module's own
139
149
  * structure; nothing here is ported from another implementation. Private
140
150
  * names carry the `quicConn` prefix (`docs/wp26-stdlib.md` §3e).
141
151
  */
142
152
  import { timingSafeEqualAt } from "nish/crypto/ct"
143
- import { hmacSha256 } from "nish/crypto/hmac"
153
+ import { HkdfScratch } from "nish/crypto/hkdf"
154
+ import { HmacSha256Scratch, hmacSha256 } from "nish/crypto/hmac"
144
155
  import {
145
156
  QUIC_AEAD_AES_128_GCM,
146
157
  QUIC_AEAD_AES_256_GCM,
@@ -154,14 +165,14 @@ import {
154
165
  QUIC_PACKET_OK,
155
166
  QUIC_PACKET_SHORT,
156
167
  QuicHeader,
157
- QuicInitialSecrets,
158
168
  QuicKeys,
169
+ QuicKeysSlot,
159
170
  QuicPacket,
160
171
  quicDecryptPayload,
161
- quicInitialSecrets,
162
- quicKeyUpdateSecret,
163
- quicKeys,
164
- quicKeysUpdate,
172
+ quicInitialSecretsInto,
173
+ quicKeyUpdateSecretInto,
174
+ quicKeysInto,
175
+ quicKeysUpdateInto,
165
176
  quicPacketNumberLength,
166
177
  quicParseHeaderInto,
167
178
  quicPutLongHeader,
@@ -218,8 +229,8 @@ import {
218
229
  } from "nish/net/quic-frame"
219
230
  import {
220
231
  QuicTransportParameters,
221
- quicEncodeTransportParameters,
222
- quicParseTransportParameters,
232
+ quicEncodeTransportParametersInto,
233
+ quicParseTransportParametersInto,
223
234
  } from "nish/net/quic-conn-params"
224
235
  import { QuicAckRanges } from "nish/net/quic-conn-ack"
225
236
  import {
@@ -320,12 +331,31 @@ export const QUIC_CONN_PACKET_STREAMS: i32 = 4
320
331
  export const QUIC_CONN_PACKET_CONTROL: i32 = 4
321
332
  /**
322
333
  * How many key updates a connection takes from its client (RFC 9001 §6).
323
- * Each costs two key derivations whose temporaries stay in the arena, so the
324
- * cap bounds what a client can make the server derive (QUIC-4); the next one
334
+ * Each costs two key derivations of work, though none of their memory, so
335
+ * the cap bounds what a client can make the server derive (QUIC-4); the next one
325
336
  * closes the connection with KEY_UPDATE_ERROR. A client also has to wait
326
337
  * a probe timeout between two of them.
327
338
  */
328
339
  export const QUIC_CONN_MAX_KEY_UPDATES: i32 = 64
340
+ /**
341
+ * How many ack-eliciting datagrams a probe timeout sends in the Initial and
342
+ * Handshake spaces (RFC 9002 §6.2.4 allows up to two). The server's first
343
+ * flight is usually one datagram, so with one probe a lossy path loses every
344
+ * copy of it about as often as it loses one datagram per probe timeout; two
345
+ * square that, and stay inside the amplification limit, which a one-datagram
346
+ * flight and two probes fill exactly. The Application Data space sends one.
347
+ */
348
+ export const QUIC_CONN_HANDSHAKE_PROBES: i32 = 2
349
+ /**
350
+ * How many times a connection resends its handshake data before the probe
351
+ * timeout because the client showed it lacks it (RFC 9002 §6.2.3, "for a
352
+ * limited number of times per connection"): a client Initial carrying
353
+ * CRYPTO data the server already read, or an ack-eliciting Handshake packet,
354
+ * while the server's own CRYPTO data at that level is in flight. Each
355
+ * resend is one datagram, so a client cannot make the server answer more
356
+ * than this many packets that way, whatever it sends.
357
+ */
358
+ export const QUIC_CONN_EARLY_RESENDS: i32 = 2
329
359
  /**
330
360
  * How many connection IDs a connection issues in all, its first included.
331
361
  * The client retiring one asks for a replacement (§5.1.1, a SHOULD), and
@@ -381,6 +411,10 @@ export const QUIC_DATAGRAM_ERR_FULL: i32 = -5
381
411
 
382
412
  /** A typed zero for the offsets below: a bare literal is an `f64` under `--number-mode f64`. */
383
413
  const QUIC_CONN_FROM: i32 = 0
414
+ /** A space's key slots (`QuicConnSpace.keySlot`): the read keys', the write keys', and where Application Data's write keys start. */
415
+ const QUIC_CONN_READ_SLOT: i32 = 0
416
+ const QUIC_CONN_WRITE_SLOT: i32 = 1
417
+ const QUIC_CONN_APP_WRITE_SLOTS: i32 = 2
384
418
  /** The same for the `i64` arguments of the methods below, where a literal is not given its parameter's type. */
385
419
  const QUIC_CONN_NONE: i64 = 0
386
420
  /** TLS's unexpected_message alert, which CRYPTO data at a level TLS is not reading is (RFC 8446 §6). */
@@ -577,6 +611,12 @@ class QuicConnSpace {
577
611
  */
578
612
  cryptoOut: u8[]
579
613
  cryptoOutLength: i32 = 0
614
+ /**
615
+ * How many ack-eliciting probe packets this space still owes, which the
616
+ * window does not hold back: what a probe timeout asked for (RFC 9002
617
+ * §6.2.4), or an early resend (§6.2.3).
618
+ */
619
+ probes: i32 = 0
580
620
  cryptoOutOffset: i64 = 0
581
621
  /** How many CRYPTO bytes the client sent here that were handed to TLS, for a duplicate arriving after the next level started. */
582
622
  cryptoInDelivered: i64 = 0
@@ -597,6 +637,12 @@ class QuicConnSpace {
597
637
  sentControlCount: i32[]
598
638
  sentControlKind: u8[]
599
639
  sentControlValue: i64[]
640
+ /**
641
+ * Where this space's packet keys live, made once for the slot (QUIC-3):
642
+ * the read keys' and the write keys', and for the Application Data space a
643
+ * second of each, which a key update derives the next generation into.
644
+ */
645
+ keySlots: QuicKeysSlot[]
600
646
  /** `TLS_LEVEL_INITIAL`, `_HANDSHAKE` or `_APPLICATION`, which is also the space's index. */
601
647
  level: i32 = 0
602
648
  cryptoOutHead: i32 = 0
@@ -609,13 +655,16 @@ class QuicConnSpace {
609
655
  discarded: boolean = false
610
656
  /** Whether the packet being built elicits an acknowledgement, and so is recorded. */
611
657
  stagedEliciting: boolean = false
612
- /** Whether a probe timeout asked this space for an ack-eliciting packet, which the window does not hold back (RFC 9002 §6.2.4). */
613
- probe: boolean = false
614
658
 
615
659
  constructor(level: i32) {
616
660
  this.level = level
617
661
  this.received = new QuicAckRanges()
618
662
  this.cryptoOut = []
663
+ this.keySlots = []
664
+ const slots: i32 = level === TLS_LEVEL_APPLICATION ? 4 : 2
665
+ for (let k: i32 = 0; k < slots; k += 1) {
666
+ this.keySlots.push(new QuicKeysSlot())
667
+ }
619
668
  const capacity: i32 =
620
669
  level === TLS_LEVEL_APPLICATION ? QUIC_RECOVERY_APPLICATION_CAPACITY : QUIC_RECOVERY_HANDSHAKE_CAPACITY
621
670
  const rows: i32 = capacity + 1
@@ -649,23 +698,44 @@ class QuicConnSpace {
649
698
  this.cryptoResendLow = -1
650
699
  this.cryptoResendHigh = -1
651
700
  this.discarded = false
652
- this.probe = false
701
+ this.probes = 0
653
702
  this.clearStaged()
654
703
  }
655
704
 
656
- /** Appends `bytes` to the CRYPTO stream kept, writing into the array's old room before growing it. */
657
- appendCrypto(bytes: u8[]): void {
658
- for (const b of bytes) {
705
+ /**
706
+ * Appends `bytes[0 .. length)` to the CRYPTO stream kept, writing into the
707
+ * array's old room before growing it: TLS's output buffer is read in
708
+ * place, so its flight is not copied on the way.
709
+ */
710
+ appendCrypto(bytes: u8[], length: i32): void {
711
+ for (let k: i32 = 0; k < length && k < toI32(bytes.length); k += 1) {
659
712
  const at: i32 = this.cryptoOutLength
660
713
  if (at >= 0 && at < toI32(this.cryptoOut.length)) {
661
- this.cryptoOut[at] = b
714
+ this.cryptoOut[at] = bytes[k]
662
715
  } else {
663
- this.cryptoOut.push(b)
716
+ this.cryptoOut.push(bytes[k])
664
717
  }
665
718
  this.cryptoOutLength = at + 1
666
719
  }
667
720
  }
668
721
 
722
+ /**
723
+ * Key slot `k` of this space: in the Initial and Handshake spaces 0 holds
724
+ * the read keys and 1 the write keys; in Application Data 0 and 1 hold
725
+ * the read keys' generations and 2 and 3 the write keys'.
726
+ */
727
+ keySlot(k: i32): QuicKeysSlot {
728
+ const at: i32 = k >= 0 && k < toI32(this.keySlots.length) ? k : 0
729
+ return this.keySlots[at]
730
+ }
731
+
732
+ /** Zeroes every key this space's slots hold, the AES schedules included. */
733
+ wipeKeySlots(): void {
734
+ for (const slot of this.keySlots) {
735
+ slot.wipe()
736
+ }
737
+ }
738
+
669
739
  /** The number of STREAM chunks row `row` holds, or 0. */
670
740
  streamCount(row: i32): i32 {
671
741
  return row >= 0 && row < toI32(this.sentStreamCount.length) ? this.sentStreamCount[row] : 0
@@ -888,6 +958,68 @@ export const quicStatelessResetToken = (key: u8[], cid: u8[]): u8[] => {
888
958
  return token
889
959
  }
890
960
 
961
+ /**
962
+ * `quicStatelessResetToken` of the ID `cid[at .. at + length)` into
963
+ * `out[outAt ..]`, computed in a connection's own HMAC scratch inside an
964
+ * arena block, so issuing an ID keeps nothing (QUIC-3). The scratch and the
965
+ * full MAC are wiped before it returns.
966
+ */
967
+ export const quicConnResetTokenInto = (
968
+ mac: HmacSha256Scratch,
969
+ key: u8[],
970
+ cid: u8[],
971
+ at: i32,
972
+ length: i32,
973
+ out: u8[],
974
+ outAt: i32
975
+ ): void => {
976
+ using _scope = arena()
977
+ const tag: u8[] = new Array<u8>(32)
978
+ mac.begin(key, QUIC_CONN_FROM, toI32(key.length))
979
+ mac.update(cid, at, length)
980
+ mac.finishInto(tag, QUIC_CONN_FROM)
981
+ for (
982
+ let k: i32 = 0;
983
+ k < QUIC_RESET_TOKEN_SIZE && k < toI32(tag.length) && outAt + k < toI32(out.length);
984
+ k += 1
985
+ ) {
986
+ out[outAt + k] = tag[k]
987
+ }
988
+ secureZero(tag)
989
+ mac.wipe()
990
+ }
991
+
992
+ /** Puts `from[at .. at + length)` in `to`, emptied first, in the room `to` already has. */
993
+ const quicConnRefill = (to: u8[], from: u8[], at: i32, length: i32): void => {
994
+ while (to.length > 0) {
995
+ to.pop()
996
+ }
997
+ for (let k: i32 = 0; k < length && at + k >= 0 && at + k < toI32(from.length); k += 1) {
998
+ to.push(from[at + k])
999
+ }
1000
+ }
1001
+
1002
+ /**
1003
+ * Secret `index` (0 or 1) of a direction's pool, of the length `aead`'s hash
1004
+ * gives: what a key update derives the next traffic secret into.
1005
+ */
1006
+ const quicConnSecretAt = (pool: u8[][], aead: i32, index: i32): u8[] => {
1007
+ let wanted: i32 = index === 1 ? 1 : 0
1008
+ if (aead === QUIC_AEAD_AES_256_GCM) {
1009
+ wanted = wanted + 2
1010
+ }
1011
+ const at: i32 = wanted < toI32(pool.length) ? wanted : 0
1012
+ return pool[at]
1013
+ }
1014
+
1015
+ /** Two secrets of SHA-256's length and two of SHA-384's, for one direction's key updates. */
1016
+ const quicConnSecretPool = (): u8[][] => [
1017
+ new Array<u8>(32),
1018
+ new Array<u8>(32),
1019
+ new Array<u8>(48),
1020
+ new Array<u8>(48),
1021
+ ]
1022
+
891
1023
  /** Whether the configuration's limits are ones this module can honour. */
892
1024
  const quicConnConfigFits = (config: QuicServerConfig): boolean =>
893
1025
  config.maxStreamData >= 1 &&
@@ -940,6 +1072,30 @@ export class QuicConnection {
940
1072
  /** The server's first connection ID, the SCID of every long-header packet it sends. */
941
1073
  localScid: u8[]
942
1074
  tls: TlsServer | null = null
1075
+ /** The slot's `TlsServer`, made by its first handshake and `restart`ed by every one after (QUIC-3). */
1076
+ tlsSlot: TlsServer | null = null
1077
+ /** Its configuration, made once: the parameters it carries are `localEncoded`, rewritten for each connection. */
1078
+ tlsConfig: TlsServerConfig
1079
+ /** The server's transport parameters, set for each connection, and their encoding. */
1080
+ localParameters: QuicTransportParameters
1081
+ localEncoded: u8[]
1082
+ /** The client's transport parameters, parsed into the slot's own object. */
1083
+ parsedParameters: QuicTransportParameters
1084
+ /** Where every key derivation and HMAC of the connection runs. */
1085
+ kdf: HkdfScratch
1086
+ /** The Initial secrets of RFC 9001 §5.2, the client's and the server's. */
1087
+ initialClient: u8[]
1088
+ initialServer: u8[]
1089
+ /**
1090
+ * The 1-RTT secrets a key update derives, two of each hash's length a
1091
+ * direction (SHA-256's first, then SHA-384's), used in turn: the next
1092
+ * generation's goes in the one the current generation's is not in.
1093
+ */
1094
+ writeSecrets: u8[][]
1095
+ readSecrets: u8[][]
1096
+ /** A connection ID being issued and its stateless reset token, before the table copies them. */
1097
+ cidScratch: u8[]
1098
+ tokenScratch: u8[]
943
1099
  initial: QuicConnSpace
944
1100
  handshake: QuicConnSpace
945
1101
  application: QuicConnSpace
@@ -1009,6 +1165,16 @@ export class QuicConnection {
1009
1165
  dropped: i32 = 0
1010
1166
  /** Key updates the client started, against `QUIC_CONN_MAX_KEY_UPDATES`. */
1011
1167
  keyUpdates: i32 = 0
1168
+ /** Which of the Application Data space's two read slots, and two write slots, hold the current keys. */
1169
+ appReadSlot: i32 = 0
1170
+ appWriteSlot: i32 = 0
1171
+ /** Which of `readSecrets` and `writeSecrets` (0 or 1) holds the current secret, or -1 for `TlsServer`'s own. */
1172
+ readSecretCurrent: i32 = -1
1173
+ writeSecretCurrent: i32 = -1
1174
+ /** Handshake data sent again early, against `QUIC_CONN_EARLY_RESENDS` (RFC 9002 §6.2.3). */
1175
+ earlyResends: i32 = 0
1176
+ /** The level at which the packet just read showed the client lacks the server's handshake data, or -1. */
1177
+ resendLevel: i32 = -1
1012
1178
  /** Whether `error` is an application code (CONNECTION_CLOSE 0x1d) rather than a transport error. */
1013
1179
  errorIsApplication: boolean = false
1014
1180
  /** Whether the one CONNECTION_CLOSE this side owes has gone out. */
@@ -1080,8 +1246,27 @@ export class QuicConnection {
1080
1246
  this.header = new QuicHeader()
1081
1247
  this.packet = new QuicPacket()
1082
1248
  this.none = []
1083
- this.retryOriginalDcid = this.none
1084
- this.retryScid = this.none
1249
+ this.retryOriginalDcid = []
1250
+ this.retryScid = []
1251
+ this.localParameters = new QuicTransportParameters()
1252
+ this.localEncoded = []
1253
+ this.parsedParameters = new QuicTransportParameters()
1254
+ const extra: u8[] = []
1255
+ this.tlsConfig = {
1256
+ certificateChain: config.certificateChain,
1257
+ alpn: config.alpn,
1258
+ quicTransportParameters: this.localEncoded,
1259
+ extraExtensions: extra,
1260
+ signatureScheme: config.signatureScheme,
1261
+ quic: true,
1262
+ }
1263
+ this.kdf = new HkdfScratch()
1264
+ this.initialClient = new Array<u8>(32)
1265
+ this.initialServer = new Array<u8>(32)
1266
+ this.writeSecrets = quicConnSecretPool()
1267
+ this.readSecrets = quicConnSecretPool()
1268
+ this.cidScratch = new Array<u8>(QUIC_CONN_CID_LENGTH)
1269
+ this.tokenScratch = new Array<u8>(QUIC_RESET_TOKEN_SIZE)
1085
1270
  this.appReadSecret = this.none
1086
1271
  this.appWriteSecret = this.none
1087
1272
  this.nextReadSecret = this.none
@@ -1094,8 +1279,8 @@ export class QuicConnection {
1094
1279
  * array): every secret of the last connection is wiped first, as
1095
1280
  * `release()` does, and every buffer is kept and emptied. The limits are
1096
1281
  * the constructor's configuration's. A slot reset this way allocates
1097
- * nothing for itself; the next handshake allocates what any handshake does
1098
- * (TLS-3 in `docs/security/tls.md`).
1282
+ * nothing, and neither does the next handshake (QUIC-3 in
1283
+ * `docs/security/quic.md`).
1099
1284
  */
1100
1285
  reset(entropy: u8[]): void {
1101
1286
  this.wipeAll()
@@ -1109,6 +1294,7 @@ export class QuicConnection {
1109
1294
  this.datagramsIn.reset()
1110
1295
  this.cryptoIn.reset(TLS_LEVEL_INITIAL)
1111
1296
  this.peerParameters = this.defaultParameters
1297
+ // The slot keeps its `TlsServer`, wiped by `wipeAll`; the next handshake restarts it.
1112
1298
  this.tls = null
1113
1299
  this.error = 0
1114
1300
  this.errorFrameType = 0
@@ -1116,12 +1302,18 @@ export class QuicConnection {
1116
1302
  this.bytesSent = 0
1117
1303
  this.alpn = ""
1118
1304
  this.serverName = ""
1119
- this.retryOriginalDcid = this.none
1120
- this.retryScid = this.none
1305
+ quicConnRefill(this.retryOriginalDcid, this.none, QUIC_CONN_FROM, QUIC_CONN_FROM)
1306
+ quicConnRefill(this.retryScid, this.none, QUIC_CONN_FROM, QUIC_CONN_FROM)
1121
1307
  this.appReadSecret = this.none
1122
1308
  this.appWriteSecret = this.none
1123
1309
  this.nextReadSecret = this.none
1124
1310
  this.otherReadKeys = null
1311
+ this.appReadSlot = 0
1312
+ this.appWriteSlot = 0
1313
+ this.readSecretCurrent = -1
1314
+ this.writeSecretCurrent = -1
1315
+ this.earlyResends = 0
1316
+ this.resendLevel = -1
1125
1317
  this.now = 0
1126
1318
  this.idleSince = -1
1127
1319
  this.readPhaseLowest = -1
@@ -1201,8 +1393,8 @@ export class QuicConnection {
1201
1393
  ) {
1202
1394
  return false
1203
1395
  }
1204
- this.retryOriginalDcid = quicConnSlice(originalDcid, 0, toI32(originalDcid.length))
1205
- this.retryScid = quicConnSlice(retryScid, 0, toI32(retryScid.length))
1396
+ quicConnRefill(this.retryOriginalDcid, originalDcid, QUIC_CONN_FROM, toI32(originalDcid.length))
1397
+ quicConnRefill(this.retryScid, retryScid, QUIC_CONN_FROM, toI32(retryScid.length))
1206
1398
  this.retried = true
1207
1399
  return true
1208
1400
  }
@@ -1352,13 +1544,29 @@ export class QuicConnection {
1352
1544
  ) {
1353
1545
  return false
1354
1546
  }
1355
- const dcid: u8[] = quicConnSlice(buf, header.dcidStart, header.dcidLength)
1356
- const secrets: QuicInitialSecrets | null = quicInitialSecrets(dcid)
1357
- if (secrets === null) {
1547
+ const secrets: boolean = quicInitialSecretsInto(
1548
+ this.kdf,
1549
+ buf,
1550
+ header.dcidStart,
1551
+ header.dcidLength,
1552
+ this.initialClient,
1553
+ this.initialServer
1554
+ )
1555
+ if (!secrets) {
1358
1556
  return false
1359
1557
  }
1360
- this.initial.readKeys = quicKeys(QUIC_AEAD_AES_128_GCM, secrets.client)
1361
- this.initial.writeKeys = quicKeys(QUIC_AEAD_AES_128_GCM, secrets.server)
1558
+ this.initial.readKeys = quicKeysInto(
1559
+ this.kdf,
1560
+ this.initial.keySlot(QUIC_CONN_READ_SLOT),
1561
+ QUIC_AEAD_AES_128_GCM,
1562
+ this.initialClient
1563
+ )
1564
+ this.initial.writeKeys = quicKeysInto(
1565
+ this.kdf,
1566
+ this.initial.keySlot(QUIC_CONN_WRITE_SLOT),
1567
+ QUIC_AEAD_AES_128_GCM,
1568
+ this.initialServer
1569
+ )
1362
1570
  quicCidCopy(this.originalDcid, buf, header.dcidStart, header.dcidLength)
1363
1571
  this.originalDcidLength = header.dcidLength
1364
1572
  quicCidCopy(this.peerScid, buf, header.scidStart, header.scidLength)
@@ -1369,14 +1577,24 @@ export class QuicConnection {
1369
1577
 
1370
1578
  /**
1371
1579
  * The rest of `start`, once the first Initial has opened: the first local
1372
- * connection ID, and a `TlsServer` whose transport parameters name both
1373
- * IDs (RFC 9000 §7.3), the Retry's when `acceptRetry` set one up, and the
1374
- * first ID's stateless reset token (§18.2).
1580
+ * connection ID, and the slot's `TlsServer` started with transport
1581
+ * parameters that name both IDs (RFC 9000 §7.3), the Retry's when
1582
+ * `acceptRetry` set one up, and the first ID's stateless reset token
1583
+ * (§18.2).
1375
1584
  */
1376
1585
  establish(): void {
1377
1586
  const none: u8[] = this.none
1378
- const resetToken: u8[] = quicStatelessResetToken(this.config.statelessResetKey, this.localScid)
1379
- this.cids.addLocal(this.localScid, resetToken)
1587
+ const token: u8[] = this.tokenScratch
1588
+ quicConnResetTokenInto(
1589
+ this.kdf.sha256,
1590
+ this.config.statelessResetKey,
1591
+ this.localScid,
1592
+ QUIC_CONN_FROM,
1593
+ QUIC_CONN_CID_LENGTH,
1594
+ token,
1595
+ QUIC_CONN_FROM
1596
+ )
1597
+ this.cids.addLocal(this.localScid, token)
1380
1598
  this.cids.addPeerAt(
1381
1599
  QUIC_CONN_NONE,
1382
1600
  QUIC_CONN_NONE,
@@ -1390,17 +1608,27 @@ export class QuicConnection {
1390
1608
  // §8.1.2: a Retry token the listener checked has validated the address.
1391
1609
  this.addressValidated = this.retried
1392
1610
 
1393
- const params: QuicTransportParameters = new QuicTransportParameters()
1394
- params.originalDcid = this.retried
1395
- ? this.retryOriginalDcid
1396
- : quicConnSlice(this.originalDcid, 0, this.originalDcidLength)
1611
+ // The parameters, and their encoding, are rewritten in the slot's own
1612
+ // arrays, which grow only when a connection needs more than any before.
1613
+ const params: QuicTransportParameters = this.localParameters
1614
+ if (this.retried) {
1615
+ quicConnRefill(
1616
+ params.originalDcid,
1617
+ this.retryOriginalDcid,
1618
+ QUIC_CONN_FROM,
1619
+ toI32(this.retryOriginalDcid.length)
1620
+ )
1621
+ } else {
1622
+ quicConnRefill(params.originalDcid, this.originalDcid, QUIC_CONN_FROM, this.originalDcidLength)
1623
+ }
1397
1624
  params.hasOriginalDcid = true
1398
1625
  params.initialScid = this.localScid
1399
1626
  params.hasInitialScid = true
1400
- params.retryScid = this.retryScid
1627
+ quicConnRefill(params.retryScid, this.retryScid, QUIC_CONN_FROM, toI32(this.retryScid.length))
1401
1628
  params.hasRetryScid = this.retried
1402
- params.statelessResetToken = resetToken
1629
+ quicConnRefill(params.statelessResetToken, token, QUIC_CONN_FROM, QUIC_RESET_TOKEN_SIZE)
1403
1630
  params.hasStatelessResetToken = true
1631
+ secureZero(token)
1404
1632
  params.maxIdleTimeout = this.config.maxIdleTimeout
1405
1633
  params.initialMaxData = this.config.maxData
1406
1634
  params.initialMaxStreamDataBidiRemote = this.config.maxStreamData
@@ -1415,16 +1643,18 @@ export class QuicConnection {
1415
1643
  // The server never migrates and asks the client not to (§9): path
1416
1644
  // validation beyond answering PATH_CHALLENGE is not here.
1417
1645
  params.disableActiveMigration = true
1418
- const extra: u8[] = []
1419
- const tlsConfig: TlsServerConfig = {
1420
- certificateChain: this.config.certificateChain,
1421
- alpn: this.config.alpn,
1422
- quicTransportParameters: quicEncodeTransportParameters(params),
1423
- extraExtensions: extra,
1424
- signatureScheme: this.config.signatureScheme,
1425
- quic: true,
1646
+ quicEncodeTransportParametersInto(params, this.localEncoded)
1647
+ // One `TlsServer` a slot, made by its first handshake (so its flight is
1648
+ // sized for these parameters) and restarted, wiped, by each after it.
1649
+ const tls: TlsServer | null = this.tlsSlot
1650
+ if (tls !== null) {
1651
+ tls.restart(this.serverRandom, this.ephemeralPrivate)
1652
+ this.tls = tls
1653
+ } else {
1654
+ const made: TlsServer = new TlsServer(this.tlsConfig, this.serverRandom, this.ephemeralPrivate)
1655
+ this.tlsSlot = made
1656
+ this.tls = made
1426
1657
  }
1427
- this.tls = new TlsServer(tlsConfig, this.serverRandom, this.ephemeralPrivate)
1428
1658
  }
1429
1659
 
1430
1660
  /**
@@ -1460,10 +1690,10 @@ export class QuicConnection {
1460
1690
  /**
1461
1691
  * Opens one packet of a connection already started, and acts on its frames.
1462
1692
  * The opening and the frames run inside an arena block (`openScoped`), so
1463
- * their temporaries go when it ends; what has to keep memory — a first
1464
- * Initial's `TlsServer`, a key update's new keys, TLS reading the CRYPTO
1465
- * bytes and what it answers — runs after it. Answers whether the packet
1466
- * was used.
1693
+ * their temporaries go when it ends; what may grow a buffer of the slot —
1694
+ * a first Initial's `TlsServer` and parameters, a key update, TLS reading
1695
+ * the CRYPTO bytes and what it answers — runs after it. Answers whether
1696
+ * the packet was used.
1467
1697
  */
1468
1698
  openPacket(buf: u8[], header: QuicHeader, first: boolean): boolean {
1469
1699
  const type: i32 = header.type
@@ -1481,6 +1711,7 @@ export class QuicConnection {
1481
1711
  this.cryptoArrived = false
1482
1712
  this.topUpOwed = false
1483
1713
  this.phasePending = -1
1714
+ this.resendLevel = -1
1484
1715
  let used: boolean = false
1485
1716
  {
1486
1717
  using _scope = arena()
@@ -1511,6 +1742,9 @@ export class QuicConnection {
1511
1742
  if (this.topUpOwed) {
1512
1743
  this.topUpConnectionIds()
1513
1744
  }
1745
+ if (this.resendLevel >= 0 && !this.closed()) {
1746
+ this.resendEarly(this.resendLevel)
1747
+ }
1514
1748
  return true
1515
1749
  }
1516
1750
 
@@ -1555,6 +1789,11 @@ export class QuicConnection {
1555
1789
  if (this.closed()) {
1556
1790
  return true
1557
1791
  }
1792
+ // RFC 9002 §6.2.3: a client that sends an ack-eliciting Handshake packet
1793
+ // while the server's Handshake data is in flight may not have it all.
1794
+ if (eliciting && type === QUIC_PACKET_HANDSHAKE) {
1795
+ this.resendLevel = TLS_LEVEL_HANDSHAKE
1796
+ }
1558
1797
  space.received.record(pn, eliciting)
1559
1798
  // §10.1: a packet received and processed restarts the idle timer.
1560
1799
  this.idleSince = this.now
@@ -1619,8 +1858,10 @@ export class QuicConnection {
1619
1858
  this.application.readKeys = this.otherReadKeys
1620
1859
  this.otherReadKeys = previous
1621
1860
  this.otherIsNext = false
1861
+ this.appReadSlot = 1 - this.appReadSlot
1622
1862
  secureZero(this.appReadSecret)
1623
1863
  this.appReadSecret = this.nextReadSecret
1864
+ this.readSecretCurrent = this.readSecretCurrent === 0 ? 1 : 0
1624
1865
  this.nextReadSecret = this.none
1625
1866
  this.readPhase = !this.readPhase
1626
1867
  this.readPhaseLowest = pn
@@ -1639,11 +1880,13 @@ export class QuicConnection {
1639
1880
  if (keys === null) {
1640
1881
  return false
1641
1882
  }
1642
- const secret: u8[] | null = quicKeyUpdateSecret(keys.aead, this.appWriteSecret)
1643
- if (secret === null) {
1883
+ const target: i32 = this.writeSecretCurrent === 0 ? 1 : 0
1884
+ const secret: u8[] = quicConnSecretAt(this.writeSecrets, keys.aead, target)
1885
+ if (!quicKeyUpdateSecretInto(this.kdf, keys.aead, this.appWriteSecret, secret)) {
1644
1886
  return false
1645
1887
  }
1646
- const next: QuicKeys | null = quicKeysUpdate(keys, secret)
1888
+ const slot: QuicKeysSlot = this.application.keySlot(QUIC_CONN_APP_WRITE_SLOTS + 1 - this.appWriteSlot)
1889
+ const next: QuicKeys | null = quicKeysUpdateInto(this.kdf, slot, keys, secret)
1647
1890
  if (next === null) {
1648
1891
  secureZero(secret)
1649
1892
  return false
@@ -1652,6 +1895,8 @@ export class QuicConnection {
1652
1895
  secureZero(this.appWriteSecret)
1653
1896
  this.application.writeKeys = next
1654
1897
  this.appWriteSecret = secret
1898
+ this.writeSecretCurrent = target
1899
+ this.appWriteSlot = 1 - this.appWriteSlot
1655
1900
  this.writePhase = !this.writePhase
1656
1901
  this.writePhaseFirst = this.application.nextPn
1657
1902
  return true
@@ -1673,12 +1918,17 @@ export class QuicConnection {
1673
1918
  if (keys === null) {
1674
1919
  return
1675
1920
  }
1676
- const secret: u8[] | null = quicKeyUpdateSecret(keys.aead, this.appReadSecret)
1677
- if (secret === null) {
1921
+ const secret: u8[] = quicConnSecretAt(this.readSecrets, keys.aead, this.readSecretCurrent === 0 ? 1 : 0)
1922
+ if (!quicKeyUpdateSecretInto(this.kdf, keys.aead, this.appReadSecret, secret)) {
1678
1923
  return
1679
1924
  }
1680
1925
  this.nextReadSecret = secret
1681
- this.otherReadKeys = quicKeysUpdate(keys, secret)
1926
+ this.otherReadKeys = quicKeysUpdateInto(
1927
+ this.kdf,
1928
+ this.application.keySlot(1 - this.appReadSlot),
1929
+ keys,
1930
+ secret
1931
+ )
1682
1932
  this.otherIsNext = this.otherReadKeys !== null
1683
1933
  }
1684
1934
 
@@ -1984,10 +2234,12 @@ export class QuicConnection {
1984
2234
 
1985
2235
  /**
1986
2236
  * A probe timeout fired (RFC 9002 §6.2.4): in every space with packets in
1987
- * flight, everything they carry is queued again and the next packet is a
1988
- * probe, sent whatever the window says, with a PING if nothing is left to
2237
+ * flight, everything they carry is queued again and the next packets are
2238
+ * probes, sent whatever the window says, with a PING if nothing is left to
1989
2239
  * carry. During the handshake that resends the Initial and the Handshake
1990
- * flight together, which is what a client that lost both needs.
2240
+ * flight together, which is what a client that lost both needs, and does
2241
+ * it twice (`QUIC_CONN_HANDSHAKE_PROBES`), so that one more lost datagram
2242
+ * does not cost another, doubled, probe timeout.
1991
2243
  */
1992
2244
  probe(): void {
1993
2245
  for (let level: i32 = 0; level < 3; level += 1) {
@@ -2002,7 +2254,56 @@ export class QuicConnection {
2002
2254
  this.requeue(space, slot)
2003
2255
  }
2004
2256
  }
2005
- space.probe = true
2257
+ space.probes = QUIC_CONN_HANDSHAKE_PROBES
2258
+ if (level === TLS_LEVEL_APPLICATION) {
2259
+ space.probes = 1
2260
+ }
2261
+ }
2262
+ }
2263
+
2264
+ /**
2265
+ * Speeds up the handshake (RFC 9002 §6.2.3): the client showed at `level`
2266
+ * that it lacks the server's handshake data there — an Initial with CRYPTO
2267
+ * data the server already read, or an ack-eliciting Handshake packet —
2268
+ * so, while the server's own packets at that level are in flight, the
2269
+ * oldest one's CRYPTO data at that level and at Handshake goes again now,
2270
+ * as one probe datagram, rather than at the probe timeout. It does so at
2271
+ * most `QUIC_CONN_EARLY_RESENDS` times a connection, and not while an
2272
+ * earlier probe is still owed, so each packet the client sends asks for
2273
+ * one datagram at most. The anti-amplification limit holds it like any
2274
+ * datagram (`takeDatagramInto`).
2275
+ */
2276
+ resendEarly(level: i32): void {
2277
+ if (
2278
+ this.earlyResends >= QUIC_CONN_EARLY_RESENDS ||
2279
+ this.initial.probes > 0 ||
2280
+ this.handshake.probes > 0
2281
+ ) {
2282
+ return
2283
+ }
2284
+ const asked: QuicSentPackets | null = this.recovery.space(level)
2285
+ if (asked === null || asked.inFlight === 0) {
2286
+ return
2287
+ }
2288
+ let queued: boolean = false
2289
+ for (let at: i32 = level; at <= TLS_LEVEL_HANDSHAKE; at += 1) {
2290
+ const space: QuicConnSpace = this.spaceAt(at)
2291
+ const sent: QuicSentPackets | null = this.recovery.space(at)
2292
+ if (sent === null || space.discarded || sent.inFlight === 0) {
2293
+ continue
2294
+ }
2295
+ // Compacted, the ring's oldest slot is the oldest packet in flight.
2296
+ sent.compact()
2297
+ const oldest: i32 = sent.slot(QUIC_CONN_FROM)
2298
+ const length: i32 = space.cryptoLength(oldest)
2299
+ if (length > 0) {
2300
+ space.resendCrypto(space.cryptoOffset(oldest), length)
2301
+ space.probes = 1
2302
+ queued = true
2303
+ }
2304
+ }
2305
+ if (queued) {
2306
+ this.earlyResends += 1
2006
2307
  }
2007
2308
  }
2008
2309
 
@@ -2026,6 +2327,11 @@ export class QuicConnection {
2026
2327
  this.spaceAt(reassembly.level).cryptoInDelivered = reassembly.delivered
2027
2328
  reassembly.reset(space.level)
2028
2329
  }
2330
+ // RFC 9002 §6.2.3: an Initial with CRYPTO data the server already read
2331
+ // says the client did not get the server's Initial flight.
2332
+ if (space.level === TLS_LEVEL_INITIAL && frame.offset < reassembly.delivered) {
2333
+ this.resendLevel = TLS_LEVEL_INITIAL
2334
+ }
2029
2335
  if (!reassembly.insert(frame.offset, payload, frame.dataStart, frame.dataLength)) {
2030
2336
  this.fail(QUIC_ERROR_CRYPTO_BUFFER_EXCEEDED, toI64(QUIC_FRAME_CRYPTO))
2031
2337
  return
@@ -2116,10 +2422,12 @@ export class QuicConnection {
2116
2422
  return
2117
2423
  }
2118
2424
  if (!this.initial.discarded) {
2119
- this.initial.appendCrypto(tls.takeOutput(TLS_LEVEL_INITIAL))
2425
+ this.initial.appendCrypto(tls.outputInitial, tls.outputInitialLength)
2426
+ tls.clearOutput(TLS_LEVEL_INITIAL)
2120
2427
  }
2121
2428
  if (!this.handshake.discarded) {
2122
- this.handshake.appendCrypto(tls.takeOutput(TLS_LEVEL_HANDSHAKE))
2429
+ this.handshake.appendCrypto(tls.outputHandshake, tls.outputHandshakeLength)
2430
+ tls.clearOutput(TLS_LEVEL_HANDSHAKE)
2123
2431
  }
2124
2432
  const aead: i32 = quicConnAead(tls.suite)
2125
2433
  if (
@@ -2132,8 +2440,16 @@ export class QuicConnection {
2132
2440
  }
2133
2441
  this.alpn = tls.alpn
2134
2442
  this.serverName = tls.serverName
2135
- this.handshake.readKeys = this.keysFor(aead, tls.readSecret(TLS_LEVEL_HANDSHAKE))
2136
- this.handshake.writeKeys = this.keysFor(aead, tls.writeSecret(TLS_LEVEL_HANDSHAKE))
2443
+ this.handshake.readKeys = this.keysFor(
2444
+ this.handshake.keySlot(QUIC_CONN_READ_SLOT),
2445
+ aead,
2446
+ tls.readSecret(TLS_LEVEL_HANDSHAKE)
2447
+ )
2448
+ this.handshake.writeKeys = this.keysFor(
2449
+ this.handshake.keySlot(QUIC_CONN_WRITE_SLOT),
2450
+ aead,
2451
+ tls.writeSecret(TLS_LEVEL_HANDSHAKE)
2452
+ )
2137
2453
  }
2138
2454
  if (
2139
2455
  !this.application.discarded &&
@@ -2143,8 +2459,12 @@ export class QuicConnection {
2143
2459
  const write: u8[] | null = tls.writeSecret(TLS_LEVEL_APPLICATION)
2144
2460
  const read: u8[] | null = tls.readSecret(TLS_LEVEL_APPLICATION)
2145
2461
  if (write !== null && read !== null) {
2146
- this.application.writeKeys = this.keysFor(aead, write)
2147
- this.application.readKeys = this.keysFor(aead, read)
2462
+ this.application.writeKeys = this.keysFor(
2463
+ this.application.keySlot(QUIC_CONN_APP_WRITE_SLOTS + this.appWriteSlot),
2464
+ aead,
2465
+ write
2466
+ )
2467
+ this.application.readKeys = this.keysFor(this.application.keySlot(this.appReadSlot), aead, read)
2148
2468
  // The key update secrets start from `TlsServer`'s own arrays, so the
2149
2469
  // first update wipes those too, rather than leaving a copy behind.
2150
2470
  this.appWriteSecret = write
@@ -2162,12 +2482,12 @@ export class QuicConnection {
2162
2482
  }
2163
2483
  }
2164
2484
 
2165
- /** `quicKeys` of a secret TLS has, or `null` when it has none yet. */
2166
- keysFor(aead: i32, secret: u8[] | null): QuicKeys | null {
2485
+ /** `quicKeysInto` `slot` of a secret TLS has, or `null` when it has none yet. */
2486
+ keysFor(slot: QuicKeysSlot, aead: i32, secret: u8[] | null): QuicKeys | null {
2167
2487
  if (secret === null) {
2168
2488
  return null
2169
2489
  }
2170
- return quicKeys(aead, secret)
2490
+ return quicKeysInto(this.kdf, slot, aead, secret)
2171
2491
  }
2172
2492
 
2173
2493
  /**
@@ -2179,7 +2499,11 @@ export class QuicConnection {
2179
2499
  * the datagrams its `max_datagram_frame_size`.
2180
2500
  */
2181
2501
  checkPeerParameters(tls: TlsServer): boolean {
2182
- const p: QuicTransportParameters = quicParseTransportParameters(tls.clientTransportParameters, false)
2502
+ const p: QuicTransportParameters = quicParseTransportParametersInto(
2503
+ this.parsedParameters,
2504
+ tls.clientTransportParameters,
2505
+ false
2506
+ )
2183
2507
  if (p.error !== QUIC_ERROR_NO_ERROR || !p.hasInitialScid) {
2184
2508
  this.fail(QUIC_ERROR_TRANSPORT_PARAMETER, toI64(QUIC_FRAME_CRYPTO))
2185
2509
  return false
@@ -2215,9 +2539,10 @@ export class QuicConnection {
2215
2539
  this.recovery.discardSpace(space.level)
2216
2540
  space.cryptoResendLow = -1
2217
2541
  space.cryptoResendHigh = -1
2218
- space.probe = false
2542
+ space.probes = 0
2219
2543
  quicConnWipeKeys(space.readKeys)
2220
2544
  quicConnWipeKeys(space.writeKeys)
2545
+ space.wipeKeySlots()
2221
2546
  space.readKeys = null
2222
2547
  space.writeKeys = null
2223
2548
  space.cryptoOutLength = 0
@@ -2227,15 +2552,30 @@ export class QuicConnection {
2227
2552
  /**
2228
2553
  * The HMAC-SHA256 under the connection's seed of the sequence number
2229
2554
  * `sequence`, whose first 8 bytes are the local connection ID of that
2230
- * number: so the IDs are unlinkable to anyone without the seed and need no
2231
- * more entropy.
2555
+ * number, into `cidScratch`: so the IDs are unlinkable to anyone without
2556
+ * the seed and need no more entropy. It runs in the connection's HMAC
2557
+ * scratch inside an arena block, and wipes the MAC and the scratch.
2232
2558
  */
2233
- deriveConnectionId(sequence: i64): u8[] {
2559
+ deriveConnectionId(sequence: i64): void {
2560
+ const mac: HmacSha256Scratch = this.kdf.sha256
2561
+ using _scope = arena()
2234
2562
  const counter: u8[] = new Array<u8>(8)
2235
2563
  for (let k: i32 = 0; k < 8 && k < toI32(counter.length); k += 1) {
2236
2564
  counter[k] = toU8(toI32((sequence >> (toI64(7 - k) * 8)) & 255))
2237
2565
  }
2238
- return hmacSha256(this.cidSeed, counter)
2566
+ const tag: u8[] = new Array<u8>(32)
2567
+ mac.begin(this.cidSeed, QUIC_CONN_FROM, toI32(this.cidSeed.length))
2568
+ mac.update(counter, QUIC_CONN_FROM, toI32(counter.length))
2569
+ mac.finishInto(tag, QUIC_CONN_FROM)
2570
+ for (
2571
+ let k: i32 = 0;
2572
+ k < QUIC_CONN_CID_LENGTH && k < toI32(tag.length) && k < toI32(this.cidScratch.length);
2573
+ k += 1
2574
+ ) {
2575
+ this.cidScratch[k] = tag[k]
2576
+ }
2577
+ secureZero(tag)
2578
+ mac.wipe()
2239
2579
  }
2240
2580
 
2241
2581
  /**
@@ -2243,27 +2583,34 @@ export class QuicConnection {
2243
2583
  * would take (its `active_connection_id_limit`), up to
2244
2584
  * `QUIC_CONN_LOCAL_CIDS` at once and `QUIC_CONN_MAX_ISSUED_CIDS` in all;
2245
2585
  * each goes out in a NEW_CONNECTION_ID frame with the stateless reset
2246
- * token the configuration's static key gives it. One ID at a time, each
2247
- * call issuing one and then topping up the rest, since each leaves its
2248
- * HMAC's temporaries in the arena (QUIC-3) and a loop would hold them
2249
- * across its passes for no reason.
2586
+ * token the configuration's static key gives it. Each ID and token is made
2587
+ * in the slot's scratch and copied into the table, so issuing keeps
2588
+ * nothing (QUIC-3).
2250
2589
  */
2251
2590
  topUpConnectionIds(): void {
2252
- if (!this.handshakeComplete || this.cids.nextLocal >= QUIC_CONN_MAX_ISSUED_CIDS) {
2591
+ if (!this.handshakeComplete) {
2253
2592
  return
2254
2593
  }
2255
2594
  let want: i64 = this.peerParameters.activeConnectionIdLimit
2256
2595
  if (want > toI64(QUIC_CONN_LOCAL_CIDS)) {
2257
2596
  want = toI64(QUIC_CONN_LOCAL_CIDS)
2258
2597
  }
2259
- if (toI64(this.cids.activeLocal()) >= want) {
2260
- return
2261
- }
2262
- const mac: u8[] = this.deriveConnectionId(this.cids.nextLocal)
2263
- const cid: u8[] = quicConnSlice(mac, 0, QUIC_CONN_CID_LENGTH)
2264
- secureZero(mac)
2265
- if (this.cids.addLocal(cid, quicStatelessResetToken(this.config.statelessResetKey, cid)) >= 0) {
2266
- this.topUpConnectionIds()
2598
+ while (this.cids.nextLocal < QUIC_CONN_MAX_ISSUED_CIDS && toI64(this.cids.activeLocal()) < want) {
2599
+ this.deriveConnectionId(this.cids.nextLocal)
2600
+ quicConnResetTokenInto(
2601
+ this.kdf.sha256,
2602
+ this.config.statelessResetKey,
2603
+ this.cidScratch,
2604
+ QUIC_CONN_FROM,
2605
+ QUIC_CONN_CID_LENGTH,
2606
+ this.tokenScratch,
2607
+ QUIC_CONN_FROM
2608
+ )
2609
+ const added: i64 = this.cids.addLocal(this.cidScratch, this.tokenScratch)
2610
+ secureZero(this.tokenScratch)
2611
+ if (added < 0) {
2612
+ return
2613
+ }
2267
2614
  }
2268
2615
  }
2269
2616
 
@@ -2722,7 +3069,7 @@ export class QuicConnection {
2722
3069
  if (sent === null || overhead === 0 || remaining <= overhead + 8) {
2723
3070
  continue
2724
3071
  }
2725
- if (space.probe && sent.full()) {
3072
+ if (space.probes > 0 && sent.full()) {
2726
3073
  // A probe has to be recorded: the oldest packet gives up its slot,
2727
3074
  // and what it carried, already queued again by the probe, stays queued.
2728
3075
  const slot: i32 = this.recovery.evictOldest(level)
@@ -2730,7 +3077,7 @@ export class QuicConnection {
2730
3077
  this.requeue(space, slot)
2731
3078
  }
2732
3079
  }
2733
- const elicit: boolean = space.probe || (open && !sent.full())
3080
+ const elicit: boolean = space.probes > 0 || (open && !sent.full())
2734
3081
  const headerLength: i32 = overhead - QUIC_AEAD_TAG_SIZE
2735
3082
  const payloadStart: i32 = position + headerLength
2736
3083
  let payloadEnd: i32 = this.buildPayloadInto(
@@ -2842,7 +3189,7 @@ export class QuicConnection {
2842
3189
  if (space.level === TLS_LEVEL_APPLICATION) {
2843
3190
  at = this.buildApplication(space, out, at, end)
2844
3191
  }
2845
- if (space.probe && !space.stagedEliciting && at < end) {
3192
+ if (space.probes > 0 && !space.stagedEliciting && at < end) {
2846
3193
  at = quicPutTypeOnly(out, at, end, QUIC_FRAME_PING)
2847
3194
  space.stagedEliciting = true
2848
3195
  }
@@ -3059,7 +3406,15 @@ export class QuicConnection {
3059
3406
  const slot: i32 = this.recovery.onPacketSent(space.level, pn, size, this.now)
3060
3407
  if (slot >= 0) {
3061
3408
  space.commitStaged(slot)
3062
- space.probe = false
3409
+ if (space.probes > 0) {
3410
+ space.probes -= 1
3411
+ // §6.2.4: a second probe carries unacknowledged data too. When the
3412
+ // first took everything queued, the oldest — what it just carried —
3413
+ // goes again.
3414
+ if (space.probes > 0 && space.cryptoResendLow < 0) {
3415
+ this.requeue(space, slot)
3416
+ }
3417
+ }
3063
3418
  } else {
3064
3419
  this.requeue(space, space.staging)
3065
3420
  }
@@ -3187,6 +3542,14 @@ export class QuicConnection {
3187
3542
  secureZero(this.appReadSecret)
3188
3543
  secureZero(this.appWriteSecret)
3189
3544
  secureZero(this.nextReadSecret)
3545
+ for (const secret of this.writeSecrets) {
3546
+ secureZero(secret)
3547
+ }
3548
+ for (const secret of this.readSecrets) {
3549
+ secureZero(secret)
3550
+ }
3551
+ secureZero(this.initialClient)
3552
+ secureZero(this.initialServer)
3190
3553
  secureZero(this.cidSeed)
3191
3554
  secureZero(this.ephemeralPrivate)
3192
3555
  // The stateless reset tokens of both sides' IDs: either one ends the
@@ -3198,6 +3561,7 @@ export class QuicConnection {
3198
3561
  secureZero(entry.resetToken)
3199
3562
  }
3200
3563
  secureZero(this.peerParameters.statelessResetToken)
3564
+ secureZero(this.localParameters.statelessResetToken)
3201
3565
  const tls: TlsServer | null = this.tls
3202
3566
  if (tls !== null) {
3203
3567
  // The server's encoded transport parameters carry its first ID's token.