@amritk/nish-x86_64-linux 0.18.0 → 0.20.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.
Files changed (59) hide show
  1. package/INSTALL.md +43 -25
  2. package/bin/nish +0 -0
  3. package/package.json +2 -2
  4. package/runtime/nish.d.ts +55 -5
  5. package/runtime/nish.h +215 -18
  6. package/runtime/nish.mjs +16 -0
  7. package/runtime/runtime-aes.c +490 -0
  8. package/runtime/runtime-cross.c +366 -0
  9. package/runtime/runtime-host.c +7 -6
  10. package/runtime/runtime-net.c +93 -34
  11. package/runtime/runtime-os.c +213 -15
  12. package/runtime/runtime-parallel.c +590 -107
  13. package/runtime/runtime-simd.c +4 -4
  14. package/runtime/runtime-wasm.c +5 -3
  15. package/runtime/runtime.c +69 -13
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +223 -22
  18. package/std/README.md +81 -51
  19. package/std/bytes.ts +163 -0
  20. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  21. package/std/crypto/aes.ts +238 -76
  22. package/std/crypto/chacha20poly1305.ts +43 -9
  23. package/std/crypto/ed25519.ts +840 -0
  24. package/std/crypto/hkdf.ts +304 -12
  25. package/std/crypto/hmac.ts +245 -53
  26. package/std/crypto/p256.ts +932 -61
  27. package/std/crypto/x25519.ts +55 -37
  28. package/std/crypto/x509-random.ts +71 -0
  29. package/std/crypto/x509.ts +9 -9
  30. package/std/json-tokenizer.ts +1296 -0
  31. package/std/json-typed.ts +1014 -0
  32. package/std/json-value.ts +1163 -0
  33. package/std/json.ts +1120 -34
  34. package/std/net/buffer-pool.ts +151 -0
  35. package/std/net/hpack.ts +416 -112
  36. package/std/net/http-fields.ts +93 -0
  37. package/std/net/http1-server.ts +417 -55
  38. package/std/net/http1.ts +31 -6
  39. package/std/net/http2-tls.ts +112 -24
  40. package/std/net/http2.ts +333 -75
  41. package/std/net/http3-server.ts +211 -31
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-listener.ts +88 -7
  45. package/std/net/quic-packet.ts +243 -82
  46. package/std/net/quic-recovery.ts +361 -30
  47. package/std/net/quic-stream.ts +424 -24
  48. package/std/net/quic.ts +519 -167
  49. package/std/net/router.ts +676 -0
  50. package/std/net/tls/codec.ts +426 -27
  51. package/std/net/tls/record-server.ts +478 -132
  52. package/std/net/tls/record.ts +133 -42
  53. package/std/net/tls/schedule.ts +464 -55
  54. package/std/net/tls/ticket.ts +610 -0
  55. package/std/net/tls-tcp.ts +196 -53
  56. package/std/net/tls.ts +1018 -260
  57. package/std/net/url.ts +571 -0
  58. package/std/random.ts +94 -0
  59. package/std/threads.ts +438 -14
@@ -0,0 +1,610 @@
1
+ /**
2
+ * `nish/net/tls/ticket` — TLS 1.3 session tickets a server seals for itself
3
+ * (RFC 8446 §4.6.1), and the strike register that makes each one single-use
4
+ * (§8.1). `docs/security/tls.md`, "Resumption", is the design this follows.
5
+ *
6
+ * A ticket is the key id (16 bytes, public, the additional data) ‖ a fresh
7
+ * random nonce (12) ‖ the ChaCha20-Poly1305 ciphertext of the state ‖ its tag
8
+ * (16). The state is what a resumed handshake reads: the suite, the time the
9
+ * ticket was issued, its lifetime, the time of the full handshake its chain
10
+ * started from, the server name, and the resumption PSK. Its plaintext is the
11
+ * suite (2 bytes), the issue time (8), the lifetime in seconds (4), the chain
12
+ * start (8), the server name with a one-byte length, and the PSK with a
13
+ * one-byte length, every integer big-endian.
14
+ *
15
+ * Every time is `monotonicNanos()`, read by the caller outside `expose` and
16
+ * handed in as `now`: keys never leave the process, so neither does a ticket,
17
+ * and a clock that cannot step keeps the strike register honest.
18
+ *
19
+ * - **Keys.** A ticket key is a `Secret<u8[]>` of 32 random bytes, which no
20
+ * class may hold (NL2430): the program keeps its current and previous keys
21
+ * in locals and lends them to `tlsTicketSeal` and `tlsTicketOpen`, which
22
+ * read them only inside `exposeWith`. Beside each key it keeps a
23
+ * `TlsTicketKeyInfo` — the id, the time it was drawn, how many tickets it has
24
+ * sealed, and its seal cap. `tlsTicketKeyDraw` draws a key into a record and
25
+ * `tlsTicketKeyDue` says when to replace it. The rotation turn is the
26
+ * program's, because a function hands back one `Secret` and may not reassign
27
+ * a borrowed one: `wipe(previous)`, `previous = current`,
28
+ * `previousInfo.copyFrom(currentInfo)`, `current = tlsTicketKeyDraw(currentInfo, now)`.
29
+ * - **What the module enforces from the record.** A key seals for 12 hours of
30
+ * awake time after its draw and until its seal cap (2^30 tickets), and opens
31
+ * for 14 hours, inclusive; a ticket opens while `now` is no earlier than its
32
+ * issue time, no later than issue time plus lifetime, and no later than 24
33
+ * hours after its chain started, and only when its sealed lifetime is at
34
+ * most 7200 s and its chain start is no later than its issue time.
35
+ * - **Wiping.** The plaintext of a seal or an open is a plain `u8[]` made in
36
+ * a `using a = arena()` block, wiped with `secureZero` once, at the point
37
+ * every path reaches; a failed open also wipes the state's PSK array. The
38
+ * seal's exposed function wipes the copy of the plaintext it hands the
39
+ * AEAD, and what the AEAD makes from the key is wiped by the AEAD
40
+ * (`nish/crypto/chacha20poly1305`).
41
+ * - **`TlsTicketStrikes`** records each ticket's id (its nonce) until the
42
+ * ticket expires or 7200 s after the recording, whichever is sooner, in a
43
+ * table made once at its capacity. It never grows and fails closed when
44
+ * full.
45
+ *
46
+ * Written from the RFC and the design record, not ported from another
47
+ * implementation. Private names carry the `tlsTicket` prefix because a `std/`
48
+ * module's private functions share the importing program's flat symbol
49
+ * namespace (`docs/wp26-stdlib.md` §3e).
50
+ */
51
+
52
+ import {
53
+ CHACHA20POLY1305_NONCE_SIZE,
54
+ CHACHA20_KEY_SIZE,
55
+ POLY1305_TAG_SIZE,
56
+ chacha20Poly1305Open,
57
+ chacha20Poly1305Seal,
58
+ } from "nish/crypto/chacha20poly1305"
59
+ import { tlsSuiteHashLength } from "nish/net/tls/schedule"
60
+ import { Secret, exposeWith, secret, wipe } from "nish:secret"
61
+
62
+ /** A ticket key's id, sent in the clear at the head of each ticket it seals. */
63
+ export const TLS_TICKET_KEY_ID_SIZE: i32 = 16
64
+
65
+ /** A ticket key: 32 bytes, ChaCha20-Poly1305's key size. */
66
+ export const TLS_TICKET_KEY_SIZE: i32 = CHACHA20_KEY_SIZE
67
+
68
+ /** A ticket's nonce, which is also its id in the strike register. */
69
+ export const TLS_TICKET_NONCE_SIZE: i32 = CHACHA20POLY1305_NONCE_SIZE
70
+
71
+ /** The `ticket_lifetime` this server sends, and the longest a ticket it opens may claim, in seconds. */
72
+ export const TLS_TICKET_LIFETIME_SECONDS: i32 = 7200
73
+
74
+ /** The longest server name a ticket carries: a session with a longer one is issued none. */
75
+ export const TLS_TICKET_SERVER_NAME_MAX: i32 = 255
76
+
77
+ /** The longest resumption PSK: SHA-384's length. */
78
+ export const TLS_TICKET_PSK_MAX: i32 = 48
79
+
80
+ /** How many tickets a key seals before `tlsTicketSeal` refuses it: 2^30. */
81
+ export const TLS_TICKET_SEAL_CAP: i64 = 1073741824
82
+
83
+ /** The strike register's capacity when the program has no other figure: 65,536 ids, about 1.3 MB. */
84
+ export const TLS_TICKET_STRIKES_DEFAULT: i32 = 65536
85
+
86
+ /** Nanoseconds in a second. */
87
+ const TLS_TICKET_NANOS: i64 = 1000000000
88
+
89
+ /** A key's issue window: 12 hours of awake time after its draw, in nanoseconds. */
90
+ export const TLS_TICKET_SEAL_WINDOW: i64 = TLS_TICKET_NANOS * 43200
91
+
92
+ /** How long after its draw a key opens tickets: its issue window and one lifetime more, 14 hours. */
93
+ export const TLS_TICKET_OPEN_WINDOW: i64 = TLS_TICKET_NANOS * 50400
94
+
95
+ /** The longest a chain of resumptions runs after the full handshake it started from: 24 hours. */
96
+ export const TLS_TICKET_CHAIN_LIMIT: i64 = TLS_TICKET_NANOS * 86400
97
+
98
+ /** One server ticket lifetime, in nanoseconds: the longest a strike entry is held. */
99
+ const TLS_TICKET_LIFETIME_NANOS: i64 = TLS_TICKET_NANOS * 7200
100
+
101
+ /** The plaintext's fixed part: suite 2, issue time 8, lifetime 4, chain start 8, and the two length bytes. */
102
+ const TLS_TICKET_PLAIN_FIXED: i32 = 24
103
+
104
+ /** Where the server name's length byte sits in the plaintext. */
105
+ const TLS_TICKET_NAME_AT: i32 = 22
106
+
107
+ /**
108
+ * A ticket key's public record. A class, so the program may keep it in a
109
+ * field or anywhere else; the key it describes is a `Secret` the program
110
+ * keeps in a local.
111
+ */
112
+ export class TlsTicketKeyInfo {
113
+ /** The key id, the ticket's additional data. */
114
+ id: u8[]
115
+ /** When the key was drawn, in `monotonicNanos()`. */
116
+ createdAt: i64
117
+ /** How many tickets the key has sealed. */
118
+ sealed: i64
119
+ /** How many it may seal: `TLS_TICKET_SEAL_CAP` unless the program lowers it. */
120
+ sealCap: i64
121
+
122
+ constructor() {
123
+ this.id = new Array<u8>(TLS_TICKET_KEY_ID_SIZE)
124
+ this.createdAt = 0
125
+ this.sealed = 0
126
+ this.sealCap = TLS_TICKET_SEAL_CAP
127
+ }
128
+
129
+ /** Copies `other` into this record, as the rotation turn does from the current key's to the previous one's. */
130
+ copyFrom(other: TlsTicketKeyInfo): void {
131
+ for (let k: i32 = 0; k < TLS_TICKET_KEY_ID_SIZE; k++) {
132
+ this.id[k] = other.id[k]
133
+ }
134
+ this.createdAt = other.createdAt
135
+ this.sealed = other.sealed
136
+ this.sealCap = other.sealCap
137
+ }
138
+ }
139
+
140
+ /**
141
+ * What a ticket carries: filled by the server before `tlsTicketSeal`, and by
142
+ * `tlsTicketOpen` from a ticket that opens. Made once and reused; the server
143
+ * name and PSK are windows of fixed arrays, `serverNameLength` and
144
+ * `pskLength` bytes long.
145
+ */
146
+ export class TlsTicketState {
147
+ /** When the ticket was issued, in `monotonicNanos()`: `tlsTicketSeal` writes its `now` here. */
148
+ issuedAt: i64
149
+ /** When the full handshake this ticket's chain started from ran. */
150
+ chainStart: i64
151
+ /** The server name the session agreed, `serverNameLength` bytes of it; none when the client sent none. */
152
+ serverName: u8[]
153
+ /** The resumption PSK, `pskLength` bytes of it: the suite's hash length. */
154
+ psk: u8[]
155
+ /** The ticket's nonce, written by `tlsTicketOpen`: its id in the strike register. */
156
+ id: u8[]
157
+ /** The cipher suite the session agreed. */
158
+ suite: i32
159
+ /** The ticket's lifetime in seconds, at most `TLS_TICKET_LIFETIME_SECONDS`. */
160
+ lifetime: i32
161
+ serverNameLength: i32
162
+ pskLength: i32
163
+
164
+ constructor() {
165
+ this.suite = 0
166
+ this.issuedAt = 0
167
+ this.lifetime = 0
168
+ this.chainStart = 0
169
+ this.serverName = new Array<u8>(TLS_TICKET_SERVER_NAME_MAX)
170
+ this.serverNameLength = 0
171
+ this.psk = new Array<u8>(TLS_TICKET_PSK_MAX)
172
+ this.pskLength = 0
173
+ this.id = new Array<u8>(TLS_TICKET_NONCE_SIZE)
174
+ }
175
+
176
+ /** When the ticket stops opening: its issue time plus its lifetime. It opens at that instant, and not after. */
177
+ expiresAt(): i64 {
178
+ return this.issuedAt + toI64(this.lifetime) * TLS_TICKET_NANOS
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Draws a fresh key: 32 random bytes moved into the `Secret` it answers, a
184
+ * fresh random id in `info`, `now` as its creation time and a count of 0.
185
+ * The seal cap is left as the program set it.
186
+ */
187
+ export const tlsTicketKeyDraw = (info: TlsTicketKeyInfo, now: i64): Secret<u8[]> => {
188
+ const key: u8[] = new Array<u8>(TLS_TICKET_KEY_SIZE)
189
+ crypto.getRandomValues(key)
190
+ crypto.getRandomValues(info.id)
191
+ info.createdAt = now
192
+ info.sealed = 0
193
+ return secret(key)
194
+ }
195
+
196
+ /** True when the key `info` describes should be replaced: at the end of its issue window, or at its seal cap. */
197
+ export const tlsTicketKeyDue = (info: TlsTicketKeyInfo, now: i64): boolean =>
198
+ now - info.createdAt >= TLS_TICKET_SEAL_WINDOW || info.sealed >= info.sealCap
199
+
200
+ /** True when the key `info` describes may seal at `now`: within its issue window, inclusive, and under its cap. */
201
+ const tlsTicketKeySeals = (info: TlsTicketKeyInfo, now: i64): boolean =>
202
+ now >= info.createdAt && now - info.createdAt <= TLS_TICKET_SEAL_WINDOW && info.sealed < info.sealCap
203
+
204
+ /** True when the key `info` describes may open at `now`: within 14 hours of its draw, inclusive. */
205
+ const tlsTicketKeyOpens = (info: TlsTicketKeyInfo, now: i64): boolean =>
206
+ now >= info.createdAt && now - info.createdAt <= TLS_TICKET_OPEN_WINDOW
207
+
208
+ /** The plaintext's length for `state`, or -1 when the state cannot be sealed. */
209
+ const tlsTicketPlainLength = (state: TlsTicketState): i32 => {
210
+ const hashLength: i32 = tlsSuiteHashLength(state.suite)
211
+ if (
212
+ hashLength === 0 ||
213
+ state.pskLength !== hashLength ||
214
+ state.pskLength > toI32(state.psk.length) ||
215
+ state.serverNameLength < 0 ||
216
+ state.serverNameLength > TLS_TICKET_SERVER_NAME_MAX ||
217
+ state.serverNameLength > toI32(state.serverName.length) ||
218
+ state.lifetime < 0
219
+ ) {
220
+ return -1
221
+ }
222
+ return TLS_TICKET_PLAIN_FIXED + state.serverNameLength + state.pskLength
223
+ }
224
+
225
+ /**
226
+ * The bytes `tlsTicketSeal` makes of `state`, 0 when it cannot seal it: a
227
+ * suite this server does not know, a PSK of another length than its hash, or
228
+ * a server name longer than 255 bytes, which is issued no ticket.
229
+ */
230
+ export const tlsTicketSize = (state: TlsTicketState): i32 => {
231
+ const plain: i32 = tlsTicketPlainLength(state)
232
+ return plain < 0 ? 0 : TLS_TICKET_KEY_ID_SIZE + TLS_TICKET_NONCE_SIZE + plain + POLY1305_TAG_SIZE
233
+ }
234
+
235
+ /** Writes `value`'s low `size` bytes big-endian at `at`. */
236
+ const tlsTicketPut = (buf: u8[], at: i32, value: i64, size: i32): void => {
237
+ for (let k: i32 = 0; k < size; k++) {
238
+ buf[at + k] = toU8(toI32((value >> (toI64(size - 1 - k) * 8)) & 255))
239
+ }
240
+ }
241
+
242
+ /** Reads `size` bytes big-endian at `at`. */
243
+ const tlsTicketGet = (buf: u8[], at: i32, size: i32): i64 => {
244
+ let value: i64 = 0
245
+ for (let k: i32 = 0; k < size; k++) {
246
+ value = (value << 8) | toI64(buf[at + k])
247
+ }
248
+ return value
249
+ }
250
+
251
+ /** A copy of `buf[from .. to)`. */
252
+ const tlsTicketRange = (buf: u8[], from: i32, to: i32): u8[] => {
253
+ const out: u8[] = new Array<u8>(to - from)
254
+ for (let k: i32 = 0; k < toI32(out.length); k++) {
255
+ const at: i32 = from + k
256
+ if (at >= 0 && at < toI32(buf.length)) {
257
+ out[k] = buf[at]
258
+ }
259
+ }
260
+ return out
261
+ }
262
+
263
+ /**
264
+ * Seals the plaintext of `input`, laid out as the key id ‖ the nonce ‖ the
265
+ * plaintext, under the exposed key: the `exposeWith` function of
266
+ * `tlsTicketSeal`, which hands it one array because an arrow captures
267
+ * nothing. The AEAD takes the plaintext as an array of its own, so the copy
268
+ * made for it is wiped here with `wipe`, after the seal, since `secureZero`
269
+ * is C and the function `exposeWith` runs may reach none. Empty when the AEAD refuses.
270
+ */
271
+ const tlsTicketSealExposed = (key: u8[], input: u8[]): u8[] => {
272
+ const head: i32 = TLS_TICKET_KEY_ID_SIZE + TLS_TICKET_NONCE_SIZE
273
+ const plain: u8[] = tlsTicketRange(input, head, toI32(input.length))
274
+ const sealed: u8[] | null = chacha20Poly1305Seal(
275
+ key,
276
+ tlsTicketRange(input, TLS_TICKET_KEY_ID_SIZE, head),
277
+ tlsTicketRange(input, 0, TLS_TICKET_KEY_ID_SIZE),
278
+ plain
279
+ )
280
+ wipe(plain)
281
+ if (sealed === null) {
282
+ const none: u8[] = []
283
+ return none
284
+ }
285
+ return sealed
286
+ }
287
+
288
+ /**
289
+ * Seals `state` under `key`, the key `info` describes, into `out` from index
290
+ * 0, and answers the ticket's length; 0 when it seals nothing: a key past its
291
+ * issue window or at its seal cap, a state `tlsTicketSize` refuses, or an
292
+ * `out` too short. The issue time sealed is `now`, which is also written to
293
+ * `state.issuedAt`; the nonce is drawn fresh, and `info` counts the ticket.
294
+ * The plaintext is built after the key id and nonce in one array, the one
295
+ * `exposeWith` hands its function, and wiped once, after the seal and its
296
+ * refusals.
297
+ */
298
+ export const tlsTicketSeal = (
299
+ key: Secret<u8[]>,
300
+ info: TlsTicketKeyInfo,
301
+ state: TlsTicketState,
302
+ now: i64,
303
+ out: u8[]
304
+ ): i32 => {
305
+ const plainLength: i32 = tlsTicketPlainLength(state)
306
+ const size: i32 = tlsTicketSize(state)
307
+ if (!tlsTicketKeySeals(info, now) || plainLength < 0 || size > toI32(out.length)) {
308
+ return 0
309
+ }
310
+ using _scope = arena()
311
+ state.issuedAt = now
312
+ const head: i32 = TLS_TICKET_KEY_ID_SIZE + TLS_TICKET_NONCE_SIZE
313
+ const input: u8[] = new Array<u8>(head + plainLength)
314
+ const nonce: u8[] = new Array<u8>(TLS_TICKET_NONCE_SIZE)
315
+ crypto.getRandomValues(nonce)
316
+ input.set(info.id, 0)
317
+ input.set(nonce, TLS_TICKET_KEY_ID_SIZE)
318
+ tlsTicketPut(input, head, toI64(state.suite), 2)
319
+ tlsTicketPut(input, head + 2, now, 8)
320
+ tlsTicketPut(input, head + 10, toI64(state.lifetime), 4)
321
+ tlsTicketPut(input, head + 14, state.chainStart, 8)
322
+ const nameAt: i32 = head + TLS_TICKET_NAME_AT
323
+ input[nameAt] = toU8(state.serverNameLength)
324
+ for (let k: i32 = 0; k < state.serverNameLength; k++) {
325
+ input[nameAt + 1 + k] = state.serverName[k]
326
+ }
327
+ const pskAt: i32 = nameAt + 1 + state.serverNameLength
328
+ input[pskAt] = toU8(state.pskLength)
329
+ for (let k: i32 = 0; k < state.pskLength; k++) {
330
+ input[pskAt + 1 + k] = state.psk[k]
331
+ }
332
+ const sealed: u8[] = exposeWith(key, input, tlsTicketSealExposed)
333
+ let written: i32 = 0
334
+ if (toI32(sealed.length) === plainLength + POLY1305_TAG_SIZE) {
335
+ out.set(info.id, 0)
336
+ out.set(nonce, TLS_TICKET_KEY_ID_SIZE)
337
+ out.set(sealed, head)
338
+ info.sealed = info.sealed + 1
339
+ written = size
340
+ }
341
+ secureZero(input)
342
+ return written
343
+ }
344
+
345
+ /**
346
+ * Opens `ticket`'s ciphertext under the exposed key: the `exposeWith` function
347
+ * of `tlsTicketOpen`. The plaintext in a fresh array, empty when the tag does
348
+ * not verify.
349
+ */
350
+ const tlsTicketOpenExposed = (key: u8[], ticket: u8[]): u8[] => {
351
+ const head: i32 = TLS_TICKET_KEY_ID_SIZE + TLS_TICKET_NONCE_SIZE
352
+ const plain: u8[] | null = chacha20Poly1305Open(
353
+ key,
354
+ tlsTicketRange(ticket, TLS_TICKET_KEY_ID_SIZE, head),
355
+ tlsTicketRange(ticket, 0, TLS_TICKET_KEY_ID_SIZE),
356
+ tlsTicketRange(ticket, head, toI32(ticket.length))
357
+ )
358
+ if (plain === null) {
359
+ const none: u8[] = []
360
+ return none
361
+ }
362
+ return plain
363
+ }
364
+
365
+ /**
366
+ * Reads `plain` into `out` and answers whether it is a ticket that opens at
367
+ * `now`: a well-formed plaintext of a known suite, a sealed lifetime of at most
368
+ * 7200 s, a chain start no later than its issue time, and `now` within its
369
+ * bounds, each inclusive.
370
+ */
371
+ const tlsTicketParse = (plain: u8[], now: i64, out: TlsTicketState): boolean => {
372
+ const length: i32 = toI32(plain.length)
373
+ if (length < TLS_TICKET_PLAIN_FIXED) {
374
+ return false
375
+ }
376
+ const suite: i32 = toI32(tlsTicketGet(plain, 0, 2))
377
+ const issuedAt: i64 = tlsTicketGet(plain, 2, 8)
378
+ const lifetime: i64 = tlsTicketGet(plain, 10, 4)
379
+ const chainStart: i64 = tlsTicketGet(plain, 14, 8)
380
+ const nameLength: i32 = toI32(plain[TLS_TICKET_NAME_AT])
381
+ const pskAt: i32 = TLS_TICKET_NAME_AT + 1 + nameLength
382
+ if (pskAt >= length) {
383
+ return false
384
+ }
385
+ const pskLength: i32 = toI32(plain[pskAt])
386
+ const hashLength: i32 = tlsSuiteHashLength(suite)
387
+ if (
388
+ pskAt + 1 + pskLength !== length ||
389
+ hashLength === 0 ||
390
+ pskLength !== hashLength ||
391
+ pskLength > toI32(out.psk.length) ||
392
+ nameLength > toI32(out.serverName.length)
393
+ ) {
394
+ return false
395
+ }
396
+ out.suite = suite
397
+ out.issuedAt = issuedAt
398
+ out.lifetime = toI32(lifetime)
399
+ out.chainStart = chainStart
400
+ out.serverNameLength = nameLength
401
+ for (let k: i32 = 0; k < nameLength; k++) {
402
+ out.serverName[k] = plain[TLS_TICKET_NAME_AT + 1 + k]
403
+ }
404
+ out.pskLength = pskLength
405
+ for (let k: i32 = 0; k < pskLength; k++) {
406
+ out.psk[k] = plain[pskAt + 1 + k]
407
+ }
408
+ return (
409
+ lifetime <= toI64(TLS_TICKET_LIFETIME_SECONDS) &&
410
+ chainStart <= issuedAt &&
411
+ issuedAt <= now &&
412
+ now <= out.expiresAt() &&
413
+ now - chainStart <= TLS_TICKET_CHAIN_LIMIT
414
+ )
415
+ }
416
+
417
+ /** True when `ticket` is long enough to hold a state and names `info`'s key id. */
418
+ const tlsTicketNamesKey = (info: TlsTicketKeyInfo, ticket: u8[]): boolean => {
419
+ if (
420
+ toI32(ticket.length) <
421
+ TLS_TICKET_KEY_ID_SIZE + TLS_TICKET_NONCE_SIZE + TLS_TICKET_PLAIN_FIXED + POLY1305_TAG_SIZE
422
+ ) {
423
+ return false
424
+ }
425
+ for (let k: i32 = 0; k < TLS_TICKET_KEY_ID_SIZE && k < toI32(ticket.length); k++) {
426
+ if (ticket[k] !== info.id[k]) {
427
+ return false
428
+ }
429
+ }
430
+ return true
431
+ }
432
+
433
+ /**
434
+ * Opens `ticket` under `key`, the key `info` describes, into `out`, and
435
+ * answers whether it opened at `now`. False — with nothing of the key read —
436
+ * when the ticket names another key id or the key is past 14 hours of awake
437
+ * time; false when the tag does not verify, the plaintext is malformed, or the
438
+ * ticket is outside its bounds (`tlsTicketParse`). On true, `out` holds the
439
+ * state and the ticket's nonce in `id`, its strike-register id. On false after
440
+ * the AEAD ran, the state's PSK array is wiped. The plaintext is wiped once,
441
+ * after the branch on the tag and on the parse, so the one wipe covers every
442
+ * path.
443
+ */
444
+ export const tlsTicketOpen = (
445
+ key: Secret<u8[]>,
446
+ info: TlsTicketKeyInfo,
447
+ ticket: u8[],
448
+ now: i64,
449
+ out: TlsTicketState
450
+ ): boolean => {
451
+ if (!tlsTicketKeyOpens(info, now) || !tlsTicketNamesKey(info, ticket)) {
452
+ return false
453
+ }
454
+ using _scope = arena()
455
+ const plain: u8[] = exposeWith(key, ticket, tlsTicketOpenExposed)
456
+ const opened: boolean = tlsTicketParse(plain, now, out)
457
+ if (opened) {
458
+ for (let k: i32 = 0; k < TLS_TICKET_NONCE_SIZE; k++) {
459
+ out.id[k] = ticket[TLS_TICKET_KEY_ID_SIZE + k]
460
+ }
461
+ } else {
462
+ secureZero(out.psk)
463
+ }
464
+ secureZero(plain)
465
+ return opened
466
+ }
467
+
468
+ /**
469
+ * The strike register (§8.1): the ids of the tickets this process has
470
+ * accepted, each held until its ticket expires or one server lifetime after
471
+ * it was recorded, whichever is sooner, so that a ticket resumes at most once.
472
+ * An open-addressed table with linear probing, made once at the capacity the
473
+ * program gives (`TLS_TICKET_STRIKES_DEFAULT` unless it has a better figure)
474
+ * and never grown: an expired entry is reused, and when no entry is free
475
+ * `take` refuses and counts the refusal, failing closed. A slot never used
476
+ * ends a probe chain; a slot once used never is unused again, so every live
477
+ * entry of an id sits on its chain before the first unused slot.
478
+ */
479
+ export class TlsTicketStrikes {
480
+ /** The ids, `TLS_TICKET_NONCE_SIZE` bytes a slot. */
481
+ ids: u8[]
482
+ /** Each slot's expiry: the entry is held through it, inclusive, and free strictly after. */
483
+ expires: i64[]
484
+ /** 1 for a slot that has ever held an id. */
485
+ used: u8[]
486
+ /** How many `take`s were refused because no entry was free. */
487
+ refusedFull: i64
488
+ /** How many ids the register holds. */
489
+ capacity: i32
490
+ /** How many ids are held or were held and expired: slots ever used. */
491
+ filled: i32
492
+ /** The farthest from its home slot any id has been recorded: no entry of an id lies further along its chain. */
493
+ longest: i32
494
+
495
+ constructor(capacity: i32) {
496
+ if (capacity < 1) {
497
+ panic("TlsTicketStrikes: a capacity below 1")
498
+ }
499
+ this.capacity = capacity
500
+ this.ids = new Array<u8>(capacity * TLS_TICKET_NONCE_SIZE)
501
+ this.expires = new Array<i64>(capacity)
502
+ this.used = new Array<u8>(capacity)
503
+ this.filled = 0
504
+ this.longest = 0
505
+ this.refusedFull = 0
506
+ }
507
+
508
+ /** The slot an id's probe chain starts at: FNV-1a over its bytes. */
509
+ home(id: u8[]): i32 {
510
+ let hash: i64 = 2166136261
511
+ for (let k: i32 = 0; k < TLS_TICKET_NONCE_SIZE && k < toI32(id.length); k++) {
512
+ hash = ((hash ^ toI64(id[k])) * 16777619) & 4294967295
513
+ }
514
+ return toI32(hash % toI64(this.capacity))
515
+ }
516
+
517
+ /** True when slot `slot` holds `id`. */
518
+ holds(slot: i32, id: u8[]): boolean {
519
+ const at: i32 = slot * TLS_TICKET_NONCE_SIZE
520
+ for (let k: i32 = 0; k < TLS_TICKET_NONCE_SIZE && k < toI32(id.length); k++) {
521
+ if (this.ids[at + k] !== id[k]) {
522
+ return false
523
+ }
524
+ }
525
+ return true
526
+ }
527
+
528
+ /** How many entries hold `id` at `now`, live or expired: what the tests read to see one id recorded once. */
529
+ count(id: u8[]): i32 {
530
+ let found: i32 = 0
531
+ for (let slot: i32 = 0; slot < this.capacity; slot++) {
532
+ if (this.used[slot] !== 0 && this.holds(slot, id)) {
533
+ found++
534
+ }
535
+ }
536
+ return found
537
+ }
538
+
539
+ /** How many entries are held at `now`. */
540
+ live(now: i64): i32 {
541
+ let held: i32 = 0
542
+ for (let slot: i32 = 0; slot < this.capacity; slot++) {
543
+ if (this.used[slot] !== 0 && this.expires[slot] >= now) {
544
+ held++
545
+ }
546
+ }
547
+ return held
548
+ }
549
+
550
+ /**
551
+ * Records `id` until `min(expiresAt, now + 7200 s)` and answers true; false
552
+ * when `id` is still held, or when no entry is free, which is counted in
553
+ * `refusedFull`. The probe chain is scanned for a live entry of `id` as far
554
+ * as `longest`, the farthest any id has been recorded from its home, before
555
+ * an expired slot on it is reused, so an expired entry ahead of the id's
556
+ * live one cannot let it be recorded twice. Past that bound the scan only
557
+ * looks for a free slot and stops at the first, so a `take` costs the
558
+ * longest probe so far, not the capacity. An id of another length than a
559
+ * nonce is refused, uncounted.
560
+ */
561
+ take(id: u8[], expiresAt: i64, now: i64): boolean {
562
+ if (toI32(id.length) !== TLS_TICKET_NONCE_SIZE) {
563
+ return false
564
+ }
565
+ const bound: i64 = now + TLS_TICKET_LIFETIME_NANOS
566
+ const until: i64 = expiresAt < bound ? expiresAt : bound
567
+ const home: i32 = this.home(id)
568
+ let free: i32 = -1
569
+ for (let n: i32 = 0; n < this.capacity; n++) {
570
+ const slot: i32 = (home + n) % this.capacity
571
+ if (this.used[slot] === 0) {
572
+ if (free < 0) {
573
+ free = slot
574
+ }
575
+ break
576
+ }
577
+ if (this.expires[slot] < now) {
578
+ if (free < 0) {
579
+ free = slot
580
+ }
581
+ } else if (n <= this.longest && this.holds(slot, id)) {
582
+ return false
583
+ }
584
+ if (free >= 0 && n >= this.longest) {
585
+ break
586
+ }
587
+ }
588
+ if (free < 0) {
589
+ this.refusedFull = this.refusedFull + 1
590
+ return false
591
+ }
592
+ if (this.used[free] === 0) {
593
+ this.used[free] = 1
594
+ this.filled = this.filled + 1
595
+ }
596
+ const at: i32 = free * TLS_TICKET_NONCE_SIZE
597
+ for (let k: i32 = 0; k < TLS_TICKET_NONCE_SIZE && k < toI32(id.length); k++) {
598
+ this.ids[at + k] = id[k]
599
+ }
600
+ this.expires[free] = until
601
+ let distance: i32 = free - home
602
+ if (distance < 0) {
603
+ distance = distance + this.capacity
604
+ }
605
+ if (distance > this.longest) {
606
+ this.longest = distance
607
+ }
608
+ return true
609
+ }
610
+ }