@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.
- package/INSTALL.md +43 -25
- package/bin/nish +0 -0
- package/package.json +2 -2
- package/runtime/nish.d.ts +55 -5
- package/runtime/nish.h +215 -18
- package/runtime/nish.mjs +16 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +7 -6
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +213 -15
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +4 -4
- package/runtime/runtime-wasm.c +5 -3
- package/runtime/runtime.c +69 -13
- package/runtime/shim.mjs +71 -1
- package/scripts/build.sh +223 -22
- package/std/README.md +81 -51
- package/std/bytes.ts +163 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- package/std/crypto/chacha20poly1305.ts +43 -9
- package/std/crypto/ed25519.ts +840 -0
- package/std/crypto/hkdf.ts +304 -12
- package/std/crypto/hmac.ts +245 -53
- package/std/crypto/p256.ts +932 -61
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +9 -9
- package/std/json-tokenizer.ts +1296 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1163 -0
- package/std/json.ts +1120 -34
- package/std/net/buffer-pool.ts +151 -0
- package/std/net/hpack.ts +416 -112
- package/std/net/http-fields.ts +93 -0
- package/std/net/http1-server.ts +417 -55
- package/std/net/http1.ts +31 -6
- package/std/net/http2-tls.ts +112 -24
- package/std/net/http2.ts +333 -75
- package/std/net/http3-server.ts +211 -31
- package/std/net/http3.ts +187 -28
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-listener.ts +88 -7
- package/std/net/quic-packet.ts +243 -82
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +424 -24
- package/std/net/quic.ts +519 -167
- package/std/net/router.ts +676 -0
- package/std/net/tls/codec.ts +426 -27
- package/std/net/tls/record-server.ts +478 -132
- package/std/net/tls/record.ts +133 -42
- package/std/net/tls/schedule.ts +464 -55
- package/std/net/tls/ticket.ts +610 -0
- package/std/net/tls-tcp.ts +196 -53
- package/std/net/tls.ts +1018 -260
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- 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
|
+
}
|