@amritk/nish-x86_64-linux 0.17.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 +231 -20
- 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 +203 -0
- package/runtime/runtime-wasm.c +16 -3
- package/runtime/runtime.c +89 -23
- package/runtime/shim.mjs +71 -1
- package/scripts/build.sh +225 -20
- package/std/README.md +86 -55
- 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 +1376 -42
- 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 +1412 -70
- 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 +231 -45
- package/std/net/http3.ts +187 -28
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-conn-params.ts +86 -16
- package/std/net/quic-listener.ts +576 -219
- package/std/net/quic-packet.ts +443 -58
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +434 -28
- package/std/net/quic.ts +922 -206
- 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 +204 -57
- package/std/net/tls.ts +1132 -305
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/text.ts +77 -0
- package/std/threads.ts +438 -14
package/std/net/tls.ts
CHANGED
|
@@ -39,21 +39,38 @@
|
|
|
39
39
|
* the share is refused. `server_name` is read and exposed as `serverName`.
|
|
40
40
|
* ALPN picks the first of the configuration's protocols the client offers.
|
|
41
41
|
* `quic_transport_parameters` is read and written only when the configuration
|
|
42
|
-
* says the carrier is QUIC.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* says the carrier is QUIC. 0-RTT and client authentication are not here:
|
|
43
|
+
* an early-data offer is ignored.
|
|
44
|
+
*
|
|
45
|
+
* **Resumption.** A server the carrier calls `enableTickets` on, over TCP
|
|
46
|
+
* only, resumes with a session ticket in `psk_dhe_ke` mode (RFC 8446 §2.2,
|
|
47
|
+
* §4.2.11) and issues one after each handshake. The ticket keys and the
|
|
48
|
+
* strike register are the program's, lent to each call and never kept: a
|
|
49
|
+
* ClientHello that offers a ticket stops at `TLS_STATE_WAIT_TICKET`, and
|
|
50
|
+
* `resume` reads the identities in the client's order, verifies the binder of
|
|
51
|
+
* the one it selects, records its id, and answers a resumed flight — or a full
|
|
52
|
+
* handshake when no identity can be used (`docs/security/tls.md`,
|
|
53
|
+
* "Resumption"). Once connected, `issue` writes one NewSessionTicket at the
|
|
54
|
+
* Application level. Sealing draws the ticket's nonce and `issue` the
|
|
55
|
+
* `ticket_age_add` from the random device; nothing else here does. Over QUIC
|
|
56
|
+
* the hand-off is never called, so a PSK offer gets a full handshake.
|
|
45
57
|
*
|
|
46
58
|
* **Refusals are alerts.** `receive` and `sign` answer 0 or a TLS alert
|
|
47
59
|
* description (`TLS_ALERT_*` in `nish/net/tls/codec`); after one the server
|
|
48
60
|
* is in `TLS_STATE_FAILED` and answers the same alert to every call. Nothing
|
|
49
61
|
* the peer sends can make it panic.
|
|
50
62
|
*
|
|
51
|
-
* **Secrets.**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
63
|
+
* **Secrets.** Every secret the server holds is a `Secret` (`nish:secret`):
|
|
64
|
+
* the ECDHE secret and the words it is computed in, wiped on every path; the
|
|
65
|
+
* ephemeral key, copied into the `ephemeral` field by `restart` with the
|
|
66
|
+
* caller's array zeroed; and the handshake, traffic, exporter and resumption
|
|
67
|
+
* secrets, and a resumed handshake's PSK, early secret and binder key, fields
|
|
68
|
+
* of the two `TlsSecretPool`s, read only inside `expose`.
|
|
69
|
+
* `wipe` zeroes them all, and `restart` calls it before the next connection;
|
|
70
|
+
* the carriers call it when a connection ends, and `wipeSecret` and
|
|
71
|
+
* `wipeEphemeral` as each secret's work is done. `fail` wipes the resumption
|
|
72
|
+
* master secret and the PSK's secrets, and `issue` the resumption master
|
|
73
|
+
* secret once the ticket is sealed (CLAUDE.md §Security; TLS-1 in
|
|
57
74
|
* `docs/security/tls.md`).
|
|
58
75
|
*
|
|
59
76
|
* **Memory.** A `TlsServer` keeps its state in buffers it allocates once —
|
|
@@ -67,11 +84,12 @@
|
|
|
67
84
|
* 8446, RFC 7301, RFC 6066 and RFC 9001, not ported from another
|
|
68
85
|
* implementation.
|
|
69
86
|
*/
|
|
70
|
-
import { Secret, expose, secret, wipe } from "nish:secret"
|
|
71
|
-
import { HkdfScratch
|
|
87
|
+
import { Secret, expose, exposeWith, secret, wipe } from "nish:secret"
|
|
88
|
+
import { HkdfScratch } from "nish/crypto/hkdf"
|
|
89
|
+
import { hmacSha256, hmacSha384 } from "nish/crypto/hmac"
|
|
72
90
|
import { SHA256_SIZE } from "nish/crypto/sha256"
|
|
73
91
|
import { SHA384_SIZE } from "nish/crypto/sha512"
|
|
74
|
-
import { p256SignSha256 } from "nish/crypto/p256"
|
|
92
|
+
import { P256SignScratch, P256_SIGNATURE_SIZE, p256SignSha256, p256SignSha256Into } from "nish/crypto/p256"
|
|
75
93
|
import { timingSafeEqual, timingSafeEqualAt } from "nish/crypto/ct"
|
|
76
94
|
import { X25519_SIZE, x25519, x25519Base } from "nish/crypto/x25519"
|
|
77
95
|
import {
|
|
@@ -100,16 +118,36 @@ import {
|
|
|
100
118
|
tlsWriteCertificateVerifyContent,
|
|
101
119
|
tlsWriteEncryptedExtensions,
|
|
102
120
|
tlsWriteFinished,
|
|
121
|
+
tlsSelectPskIdentity,
|
|
103
122
|
tlsWriteHelloRetryRequest,
|
|
123
|
+
tlsWriteNewSessionTicket,
|
|
104
124
|
tlsWriteServerHello,
|
|
125
|
+
tlsWriteServerHelloPsk,
|
|
105
126
|
} from "nish/net/tls/codec"
|
|
106
127
|
import {
|
|
128
|
+
TLS_AES_256_GCM_SHA384,
|
|
107
129
|
TlsTranscript,
|
|
130
|
+
tlsBinderKeyInto,
|
|
131
|
+
tlsBinderVerify,
|
|
108
132
|
tlsDeriveSecretInto,
|
|
109
133
|
tlsFinishedVerifyDataInto,
|
|
134
|
+
tlsHandshakeSaltInto,
|
|
110
135
|
tlsMasterSecretInto,
|
|
136
|
+
tlsPskEarlySecretInto,
|
|
137
|
+
tlsResumptionPskInto,
|
|
111
138
|
tlsSuiteHashLength,
|
|
112
139
|
} from "nish/net/tls/schedule"
|
|
140
|
+
import {
|
|
141
|
+
TLS_TICKET_LIFETIME_SECONDS,
|
|
142
|
+
TLS_TICKET_PSK_MAX,
|
|
143
|
+
TLS_TICKET_SERVER_NAME_MAX,
|
|
144
|
+
TlsTicketKeyInfo,
|
|
145
|
+
TlsTicketState,
|
|
146
|
+
TlsTicketStrikes,
|
|
147
|
+
tlsTicketOpen,
|
|
148
|
+
tlsTicketSeal,
|
|
149
|
+
tlsTicketSize,
|
|
150
|
+
} from "nish/net/tls/ticket"
|
|
113
151
|
|
|
114
152
|
// ---- Levels ------------------------------------------------------------------
|
|
115
153
|
|
|
@@ -117,7 +155,7 @@ import {
|
|
|
117
155
|
export const TLS_LEVEL_INITIAL: i32 = 0
|
|
118
156
|
/** Under the handshake traffic secrets: EncryptedExtensions through both Finished messages. */
|
|
119
157
|
export const TLS_LEVEL_HANDSHAKE: i32 = 1
|
|
120
|
-
/** Under the application traffic secrets
|
|
158
|
+
/** Under the application traffic secrets: the one NewSessionTicket `issue` writes. */
|
|
121
159
|
export const TLS_LEVEL_APPLICATION: i32 = 2
|
|
122
160
|
|
|
123
161
|
// ---- States --------------------------------------------------------------------
|
|
@@ -132,6 +170,8 @@ export const TLS_STATE_WAIT_FINISHED: i32 = 2
|
|
|
132
170
|
export const TLS_STATE_CONNECTED: i32 = 3
|
|
133
171
|
/** An alert was answered; `alert` holds it, and every later call answers it again. */
|
|
134
172
|
export const TLS_STATE_FAILED: i32 = 4
|
|
173
|
+
/** A ClientHello offered a ticket to a server with tickets on; `resume` is due, with the keys and the strike register. */
|
|
174
|
+
export const TLS_STATE_WAIT_TICKET: i32 = 5
|
|
135
175
|
|
|
136
176
|
/**
|
|
137
177
|
* The longest handshake message accepted, in bytes. A ClientHello carrying a
|
|
@@ -238,29 +278,57 @@ const tlsBytesSpell = (data: u8[], at: i32, length: i32, text: string): boolean
|
|
|
238
278
|
return true
|
|
239
279
|
}
|
|
240
280
|
|
|
241
|
-
/** The
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
/** The shortest and longest DER `ECDSA-Sig-Value` of a P-256 signature: a SEQUENCE of two INTEGERs of 1 to 33 bytes. */
|
|
282
|
+
const TLS_P256_DER_MIN: i32 = 8
|
|
283
|
+
const TLS_P256_DER_MAX: i32 = 72
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The body length of the minimal DER INTEGER for the unsigned big-endian
|
|
287
|
+
* magnitude `rs[at .. at + 32)`: leading zeros dropped, one added when the top
|
|
288
|
+
* bit is set, and zero one zero octet rather than an empty integer (X.690
|
|
289
|
+
* §8.3.1).
|
|
290
|
+
*/
|
|
291
|
+
const tlsDerIntegerLength = (rs: u8[], at: i32): i32 => {
|
|
292
|
+
let first: i32 = 0
|
|
293
|
+
while (first < 31 && at + first >= 0 && at + first < toI32(rs.length) && toI32(rs[at + first]) === 0) {
|
|
294
|
+
first++
|
|
254
295
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
296
|
+
const top: boolean = at + first >= 0 && at + first < toI32(rs.length) && toI32(rs[at + first]) >= 0x80
|
|
297
|
+
return 32 - first + (top ? 1 : 0)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** That INTEGER, of body length `length`, written at `out[to ..)`; answers where it ends. */
|
|
301
|
+
const tlsDerIntegerInto = (out: u8[], to: i32, rs: u8[], at: i32, length: i32): i32 => {
|
|
302
|
+
const end: i32 = to + 2 + length
|
|
303
|
+
if (to < 0 || end > toI32(out.length)) {
|
|
304
|
+
return end
|
|
258
305
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
306
|
+
out[to] = toU8(0x02)
|
|
307
|
+
out[to + 1] = toU8(length)
|
|
308
|
+
for (let k: i32 = 0; k < length; k++) {
|
|
309
|
+
// The body's last `length` bytes are the magnitude's, a zero first when it is 33.
|
|
310
|
+
const from: i32 = at + 32 - length + k
|
|
311
|
+
const j: i32 = to + 2 + k
|
|
312
|
+
if (j >= 0 && j < toI32(out.length)) {
|
|
313
|
+
out[j] = from >= at && from >= 0 && from < toI32(rs.length) ? rs[from] : toU8(0)
|
|
314
|
+
}
|
|
262
315
|
}
|
|
263
|
-
return
|
|
316
|
+
return end
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The length of `rs`'s DER `ECDSA-Sig-Value`: the SEQUENCE's two bytes and two INTEGERs. */
|
|
320
|
+
const tlsEcdsaDerLength = (rs: u8[]): i32 =>
|
|
321
|
+
6 + tlsDerIntegerLength(rs, TLS_FROM) + tlsDerIntegerLength(rs, 32)
|
|
322
|
+
|
|
323
|
+
/** `rs`'s DER `ECDSA-Sig-Value` into all of `der`, which is `tlsEcdsaDerLength(rs)` bytes. At most 72, so the length is one short-form byte. */
|
|
324
|
+
const tlsEcdsaDerInto = (der: u8[], rs: u8[]): void => {
|
|
325
|
+
if (toI32(der.length) < 2) {
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
der[0] = toU8(0x30)
|
|
329
|
+
der[1] = toU8(toI32(der.length) - 2)
|
|
330
|
+
const r: i32 = tlsDerIntegerLength(rs, TLS_FROM)
|
|
331
|
+
tlsDerIntegerInto(der, tlsDerIntegerInto(der, 2, rs, TLS_FROM, r), rs, 32, tlsDerIntegerLength(rs, 32))
|
|
264
332
|
}
|
|
265
333
|
|
|
266
334
|
/**
|
|
@@ -272,25 +340,9 @@ export const tlsEcdsaDerSignature = (rs: u8[]): u8[] | null => {
|
|
|
272
340
|
if (toI32(rs.length) !== 64) {
|
|
273
341
|
return null
|
|
274
342
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (k < 32) {
|
|
279
|
-
r.push(rs[k])
|
|
280
|
-
} else {
|
|
281
|
-
s.push(rs[k])
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const body: u8[] = tlsDerInteger(r)
|
|
285
|
-
for (const b of tlsDerInteger(s)) {
|
|
286
|
-
body.push(b)
|
|
287
|
-
}
|
|
288
|
-
// At most 2 * (2 + 33) = 70 bytes, so the length is one short-form byte.
|
|
289
|
-
const out: u8[] = [toU8(0x30), toU8(toI32(body.length))]
|
|
290
|
-
for (const b of body) {
|
|
291
|
-
out.push(b)
|
|
292
|
-
}
|
|
293
|
-
return out
|
|
343
|
+
const der: u8[] = new Array<u8>(tlsEcdsaDerLength(rs))
|
|
344
|
+
tlsEcdsaDerInto(der, rs)
|
|
345
|
+
return der
|
|
294
346
|
}
|
|
295
347
|
|
|
296
348
|
/**
|
|
@@ -308,45 +360,117 @@ export const tlsSignEcdsaP256 = (priv: Secret<u8[]>, content: u8[]): u8[] | null
|
|
|
308
360
|
return tlsEcdsaDerSignature(rs)
|
|
309
361
|
}
|
|
310
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Signs CertificateVerify inputs for `ecdsa_secp256r1_sha256` with nothing
|
|
365
|
+
* kept per signature: `tlsSignEcdsaP256`'s answer, made in a
|
|
366
|
+
* `P256SignScratch` and handed back in one of this signer's own arrays, one
|
|
367
|
+
* per DER length (8 to 72 bytes), since `TlsServer.sign` takes the
|
|
368
|
+
* signature's length from its array and stores what it computes, so a
|
|
369
|
+
* signature it is handed cannot be a temporary. A carrier keeps one signer,
|
|
370
|
+
* not one per slot: signing is synchronous, and the array it answers is read
|
|
371
|
+
* by `sign` before the next signature replaces it.
|
|
372
|
+
*/
|
|
373
|
+
export class TlsP256Signer {
|
|
374
|
+
/** The P-256 signer's scratch. */
|
|
375
|
+
scratch: P256SignScratch
|
|
376
|
+
/** `r || s` as `p256SignSha256Into` writes it. */
|
|
377
|
+
rs: u8[]
|
|
378
|
+
/** The DER signatures, index `length - TLS_P256_DER_MIN`. */
|
|
379
|
+
ders: u8[][]
|
|
380
|
+
|
|
381
|
+
constructor() {
|
|
382
|
+
this.scratch = new P256SignScratch()
|
|
383
|
+
this.rs = new Array<u8>(P256_SIGNATURE_SIZE)
|
|
384
|
+
this.ders = []
|
|
385
|
+
for (let length: i32 = TLS_P256_DER_MIN; length <= TLS_P256_DER_MAX; length++) {
|
|
386
|
+
this.ders.push(new Array<u8>(length))
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* `tlsSignEcdsaP256(priv, content)`, byte for byte, in the array of this
|
|
392
|
+
* signer's that has its length; `null` for a malformed key. It keeps
|
|
393
|
+
* nothing: the P-256 signature releases what it allocated, and the DER is
|
|
394
|
+
* written in place. The key is borrowed.
|
|
395
|
+
*/
|
|
396
|
+
sign(priv: Secret<u8[]>, content: u8[]): u8[] | null {
|
|
397
|
+
if (
|
|
398
|
+
!p256SignSha256Into(this.scratch, priv, content, TLS_FROM, toI32(content.length), this.rs, TLS_FROM)
|
|
399
|
+
) {
|
|
400
|
+
return null
|
|
401
|
+
}
|
|
402
|
+
const at: i32 = tlsEcdsaDerLength(this.rs) - TLS_P256_DER_MIN
|
|
403
|
+
if (at < 0 || at >= toI32(this.ders.length)) {
|
|
404
|
+
return null
|
|
405
|
+
}
|
|
406
|
+
const der: u8[] = this.ders[at]
|
|
407
|
+
tlsEcdsaDerInto(der, this.rs)
|
|
408
|
+
return der
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
311
412
|
/** Whether an x25519 shared secret is all zeros, reading every byte (run inside `expose`). */
|
|
312
413
|
const tlsIsZeroSecret = (shared: u8[]): boolean => timingSafeEqual(shared, new Array<u8>(X25519_SIZE))
|
|
313
414
|
|
|
314
415
|
/**
|
|
315
|
-
* `Derive-Secret(early, "derived", "")` with no PSK, the salt
|
|
416
|
+
* `Derive-Secret(early, "derived", "")` with no PSK, the salt a full handshake's
|
|
316
417
|
* secret is extracted under: a constant of the hash alone (RFC 8448 §3 prints
|
|
317
418
|
* the SHA-256 one; the SHA-384 one was computed with Python's hmac and
|
|
318
419
|
* hashlib). It is written out rather than derived because the functions below
|
|
319
420
|
* run on the ECDHE secret inside `expose`, and Expand-Label may panic with a
|
|
320
|
-
* computed message, which such a function may not reach.
|
|
421
|
+
* computed message, which such a function may not reach. A resumed
|
|
422
|
+
* handshake's salt is derived from the PSK's early secret by `resume`, before
|
|
423
|
+
* the exchange, and reaches it the same way: as words.
|
|
321
424
|
*/
|
|
322
425
|
const TLS_DERIVED_SALT_SHA256: string = "6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba"
|
|
323
426
|
const TLS_DERIVED_SALT_SHA384: string =
|
|
324
427
|
"1591dac5cbbf0330a4a84de9c753330e92d01f0a88214b4464972fd668049e93e52f2b16fad922fdc0584478428f282b"
|
|
325
428
|
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
429
|
+
/**
|
|
430
|
+
* The SHA-256 handshake secret: HKDF-Extract of the ECDHE secret under `salt`
|
|
431
|
+
* (run inside `exposeWith`). The salt is always HashLen bytes, so this is the
|
|
432
|
+
* HMAC itself: `hkdfExtractSha256` would pass it through `hkdfSalt`, which the
|
|
433
|
+
* checker reads as a write through the argument `exposeWith` hands it.
|
|
434
|
+
*/
|
|
435
|
+
const tlsHandshakeFromEcdhe256 = (ecdhe: u8[], salt: u8[]): u8[] => hmacSha256(salt, ecdhe)
|
|
332
436
|
|
|
333
|
-
/** The SHA-
|
|
334
|
-
const
|
|
335
|
-
hkdfExtractSha256(tlsHexBytes(TLS_DERIVED_SALT_SHA256), ecdhe)
|
|
437
|
+
/** The SHA-384 handshake secret: HKDF-Extract of the ECDHE secret under `salt`, as the SHA-256 one is. */
|
|
438
|
+
const tlsHandshakeFromEcdhe384 = (ecdhe: u8[], salt: u8[]): u8[] => hmacSha384(salt, ecdhe)
|
|
336
439
|
|
|
337
|
-
/**
|
|
338
|
-
const
|
|
339
|
-
|
|
440
|
+
/** Wipes the bytes a `Secret` holds, for `exposeWith` to run on one secret of a holder outside its `wipe()`; answers `answer`. */
|
|
441
|
+
const tlsZeroHeld = (held: u8[], answer: i32): i32 => {
|
|
442
|
+
wipe(held)
|
|
443
|
+
return answer
|
|
444
|
+
}
|
|
340
445
|
|
|
341
|
-
/**
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
446
|
+
/** Overwrites the bytes a `Secret` holds with `from`'s, for `exposeWith` to run on the destination; answers 0. */
|
|
447
|
+
export const tlsHeldCopyIn = (held: u8[], from: u8[]): i32 => {
|
|
448
|
+
for (let k: i32 = 0; k < toI32(held.length) && k < toI32(from.length); k++) {
|
|
449
|
+
held[k] = from[k]
|
|
450
|
+
}
|
|
451
|
+
return 0
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* A plain copy of a held secret, for a derivation that runs on its bytes
|
|
456
|
+
* inside a `using arena()` block: the copy dies with the block, and its
|
|
457
|
+
* caller `secureZero`s it before the block ends.
|
|
458
|
+
*/
|
|
459
|
+
export const tlsHeldClone = (held: u8[]): u8[] => {
|
|
460
|
+
const copy: u8[] = new Array<u8>(held.length)
|
|
461
|
+
for (let k: i32 = 0; k < toI32(copy.length) && k < toI32(held.length); k++) {
|
|
462
|
+
copy[k] = held[k]
|
|
346
463
|
}
|
|
347
|
-
return
|
|
464
|
+
return copy
|
|
348
465
|
}
|
|
349
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Copies the bytes one `Secret` holds over another's, for `exposeWith` to
|
|
469
|
+
* run on the source with the destination as its argument: no plain copy is
|
|
470
|
+
* made on the way. Answers 0.
|
|
471
|
+
*/
|
|
472
|
+
export const tlsHeldCopyOver = (from: u8[], to: Secret<u8[]>): i32 => exposeWith(to, from, tlsHeldCopyIn)
|
|
473
|
+
|
|
350
474
|
// ---- The exchange, in a scope of its own --------------------------------------
|
|
351
475
|
|
|
352
476
|
/**
|
|
@@ -366,9 +490,9 @@ const tlsSecretAt = (level: i32, handshake: u8[], application: u8[]): u8[] | nul
|
|
|
366
490
|
* `tests/link/net_tls_memory` would notice.
|
|
367
491
|
*
|
|
368
492
|
* The words hold secrets — the private key and the handshake secret — in
|
|
369
|
-
* fields, where `secureZero` cannot reach, so
|
|
370
|
-
*
|
|
371
|
-
*
|
|
493
|
+
* fields, where `secureZero` cannot reach, so `exchangeKeys` wipes the whole
|
|
494
|
+
* record with `wipe`, a volatile store, once it has unpacked the answers, on
|
|
495
|
+
* the failure path too (TLS-1 in `docs/security/tls.md`).
|
|
372
496
|
*/
|
|
373
497
|
class TlsExchangeWords {
|
|
374
498
|
key0: u64 = 0
|
|
@@ -389,6 +513,17 @@ class TlsExchangeWords {
|
|
|
389
513
|
secret3: u64 = 0
|
|
390
514
|
secret4: u64 = 0
|
|
391
515
|
secret5: u64 = 0
|
|
516
|
+
/**
|
|
517
|
+
* The salt the handshake secret is extracted under, `hashLength` bytes of
|
|
518
|
+
* it: the `TLS_DERIVED_SALT_*` constant on a full handshake, the salt
|
|
519
|
+
* derived from the PSK early secret on a resumed one.
|
|
520
|
+
*/
|
|
521
|
+
salt0: u64 = 0
|
|
522
|
+
salt1: u64 = 0
|
|
523
|
+
salt2: u64 = 0
|
|
524
|
+
salt3: u64 = 0
|
|
525
|
+
salt4: u64 = 0
|
|
526
|
+
salt5: u64 = 0
|
|
392
527
|
/** 32 or 48: which handshake secret to extract. */
|
|
393
528
|
hashLength: i32 = 0
|
|
394
529
|
}
|
|
@@ -413,10 +548,21 @@ const tlsWordInto = (b: u8[], at: i32, v: u64): void => {
|
|
|
413
548
|
}
|
|
414
549
|
}
|
|
415
550
|
|
|
551
|
+
/** Unpacks the handshake secret from the exchange's words into the bytes a `Secret` holds, for `exposeWith`. */
|
|
552
|
+
const tlsHandshakeFromWords = (held: u8[], w: TlsExchangeWords): i32 => {
|
|
553
|
+
tlsWordInto(held, 0, w.secret0)
|
|
554
|
+
tlsWordInto(held, 8, w.secret1)
|
|
555
|
+
tlsWordInto(held, 16, w.secret2)
|
|
556
|
+
tlsWordInto(held, 24, w.secret3)
|
|
557
|
+
tlsWordInto(held, 32, w.secret4)
|
|
558
|
+
tlsWordInto(held, 40, w.secret5)
|
|
559
|
+
return 0
|
|
560
|
+
}
|
|
561
|
+
|
|
416
562
|
/**
|
|
417
563
|
* The exchange of RFC 8446 §7.4.2 on `w`: the server's public key and the
|
|
418
564
|
* handshake secret from the ephemeral key and the client's share, both
|
|
419
|
-
* written back as words
|
|
565
|
+
* written back as words; the caller wipes the words. Answers 0,
|
|
420
566
|
* `illegal_parameter` for a share whose shared secret is all zeros (§7.4.2,
|
|
421
567
|
* RFC 7748 §6.1), or `internal_error` when the exchange answers nothing. The
|
|
422
568
|
* copy of the key and the ECDHE secret are `Secret`s wiped on every path, and
|
|
@@ -426,15 +572,18 @@ const tlsWordInto = (b: u8[], at: i32, v: u64): void => {
|
|
|
426
572
|
* returns.
|
|
427
573
|
*/
|
|
428
574
|
const tlsExchange = (w: TlsExchangeWords): i32 => {
|
|
575
|
+
const salt: u8[] = new Array<u8>(w.hashLength === SHA384_SIZE ? SHA384_SIZE : SHA256_SIZE)
|
|
576
|
+
tlsWordInto(salt, 0, w.salt0)
|
|
577
|
+
tlsWordInto(salt, 8, w.salt1)
|
|
578
|
+
tlsWordInto(salt, 16, w.salt2)
|
|
579
|
+
tlsWordInto(salt, 24, w.salt3)
|
|
580
|
+
tlsWordInto(salt, 32, w.salt4)
|
|
581
|
+
tlsWordInto(salt, 40, w.salt5)
|
|
429
582
|
const raw: u8[] = new Array<u8>(X25519_SIZE)
|
|
430
583
|
tlsWordInto(raw, 0, w.key0)
|
|
431
584
|
tlsWordInto(raw, 8, w.key1)
|
|
432
585
|
tlsWordInto(raw, 16, w.key2)
|
|
433
586
|
tlsWordInto(raw, 24, w.key3)
|
|
434
|
-
w.key0 = toU64(0)
|
|
435
|
-
w.key1 = toU64(0)
|
|
436
|
-
w.key2 = toU64(0)
|
|
437
|
-
w.key3 = toU64(0)
|
|
438
587
|
const share: u8[] = new Array<u8>(X25519_SIZE)
|
|
439
588
|
tlsWordInto(share, 0, w.share0)
|
|
440
589
|
tlsWordInto(share, 8, w.share1)
|
|
@@ -444,33 +593,40 @@ const tlsExchange = (w: TlsExchangeWords): i32 => {
|
|
|
444
593
|
const serverPublic: u8[] | null = x25519Base(key)
|
|
445
594
|
const shared: Secret<u8[]> | null = x25519(key, share)
|
|
446
595
|
wipe(key)
|
|
596
|
+
let alert: i32 = 0
|
|
447
597
|
if (shared === null) {
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
598
|
+
alert = TLS_ALERT_INTERNAL_ERROR
|
|
599
|
+
} else {
|
|
600
|
+
// RFC 7748 §6.1 leaves the all-zero check to the protocol, and RFC 8446
|
|
601
|
+
// §7.4.2 makes it: a low-order client share gives the zero secret, which
|
|
602
|
+
// anybody can compute. The secret is secret, so the compare reads it all.
|
|
603
|
+
if (serverPublic === null) {
|
|
604
|
+
alert = TLS_ALERT_INTERNAL_ERROR
|
|
605
|
+
} else if (expose(shared, tlsIsZeroSecret)) {
|
|
606
|
+
alert = TLS_ALERT_ILLEGAL_PARAMETER
|
|
607
|
+
} else {
|
|
608
|
+
const handshake: u8[] =
|
|
609
|
+
w.hashLength === SHA384_SIZE
|
|
610
|
+
? exposeWith(shared, salt, tlsHandshakeFromEcdhe384)
|
|
611
|
+
: exposeWith(shared, salt, tlsHandshakeFromEcdhe256)
|
|
612
|
+
w.public0 = tlsWordAt(serverPublic, 0)
|
|
613
|
+
w.public1 = tlsWordAt(serverPublic, 8)
|
|
614
|
+
w.public2 = tlsWordAt(serverPublic, 16)
|
|
615
|
+
w.public3 = tlsWordAt(serverPublic, 24)
|
|
616
|
+
w.secret0 = tlsWordAt(handshake, 0)
|
|
617
|
+
w.secret1 = tlsWordAt(handshake, 8)
|
|
618
|
+
w.secret2 = tlsWordAt(handshake, 16)
|
|
619
|
+
w.secret3 = tlsWordAt(handshake, 24)
|
|
620
|
+
w.secret4 = tlsWordAt(handshake, 32)
|
|
621
|
+
w.secret5 = tlsWordAt(handshake, 40)
|
|
622
|
+
secureZero(handshake)
|
|
623
|
+
}
|
|
454
624
|
wipe(shared)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
: expose(shared, tlsHandshakeFromEcdhe256)
|
|
461
|
-
wipe(shared)
|
|
462
|
-
w.public0 = tlsWordAt(serverPublic, 0)
|
|
463
|
-
w.public1 = tlsWordAt(serverPublic, 8)
|
|
464
|
-
w.public2 = tlsWordAt(serverPublic, 16)
|
|
465
|
-
w.public3 = tlsWordAt(serverPublic, 24)
|
|
466
|
-
w.secret0 = tlsWordAt(handshake, 0)
|
|
467
|
-
w.secret1 = tlsWordAt(handshake, 8)
|
|
468
|
-
w.secret2 = tlsWordAt(handshake, 16)
|
|
469
|
-
w.secret3 = tlsWordAt(handshake, 24)
|
|
470
|
-
w.secret4 = tlsWordAt(handshake, 32)
|
|
471
|
-
w.secret5 = tlsWordAt(handshake, 40)
|
|
472
|
-
secureZero(handshake)
|
|
473
|
-
return 0
|
|
625
|
+
}
|
|
626
|
+
// One wipe every path reaches, before the one return: a wipe on each path
|
|
627
|
+
// is one `opt -O2` may merge (docs/security/tls.md, "What stays in memory").
|
|
628
|
+
secureZero(salt)
|
|
629
|
+
return alert
|
|
474
630
|
}
|
|
475
631
|
|
|
476
632
|
// ---- Buffers -------------------------------------------------------------------
|
|
@@ -481,20 +637,126 @@ const TLS_INPUT_START: i32 = 2048
|
|
|
481
637
|
const TLS_INITIAL_OUTPUT_SIZE: i32 = 256
|
|
482
638
|
/** The longest DER ECDSA P-256 signature, what the flight is first sized for: two 33-byte integers and their headers. */
|
|
483
639
|
const TLS_ECDSA_SIGNATURE_MAX: i32 = 72
|
|
484
|
-
/**
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
const
|
|
640
|
+
/**
|
|
641
|
+
* Which secret of a `TlsSecretPool` an index names: what `TlsServer.known`,
|
|
642
|
+
* `wipeSecret` and the pool's `zero` take, and the bit of `TlsServer.held`
|
|
643
|
+
* that says the secret is derived (`1 << index`).
|
|
644
|
+
*/
|
|
645
|
+
export const TLS_SECRET_HANDSHAKE: i32 = 0
|
|
646
|
+
export const TLS_SECRET_CLIENT_HANDSHAKE: i32 = 1
|
|
647
|
+
export const TLS_SECRET_SERVER_HANDSHAKE: i32 = 2
|
|
648
|
+
export const TLS_SECRET_CLIENT_APPLICATION: i32 = 3
|
|
649
|
+
export const TLS_SECRET_SERVER_APPLICATION: i32 = 4
|
|
650
|
+
export const TLS_SECRET_EXPORTER: i32 = 5
|
|
651
|
+
export const TLS_SECRET_CLIENT_FINISHED: i32 = 6
|
|
492
652
|
/** The CertificateVerify input sits beside them: 98 bytes and the transcript hash. */
|
|
493
|
-
const TLS_SECRET_TO_BE_SIGNED: i32 = 7
|
|
494
|
-
const
|
|
653
|
+
export const TLS_SECRET_TO_BE_SIGNED: i32 = 7
|
|
654
|
+
export const TLS_SECRET_RESUMPTION: i32 = 8
|
|
655
|
+
/** The PSK a ticket opened to, held from the binder's check to the early secret's extract. */
|
|
656
|
+
export const TLS_SECRET_PSK: i32 = 9
|
|
657
|
+
/** The PSK early secret (RFC 8446 §7.1), held until the handshake secret's salt is derived from it. */
|
|
658
|
+
export const TLS_SECRET_EARLY: i32 = 10
|
|
659
|
+
/** The binder key (§4.2.11.2), held until the binder is compared. */
|
|
660
|
+
export const TLS_SECRET_BINDER_KEY: i32 = 11
|
|
495
661
|
/** What CertificateVerify signs besides the hash (RFC 8446 §4.4.3): 64 spaces, the 33-byte context and a zero. */
|
|
496
662
|
const TLS_CERTIFICATE_VERIFY_PREFIX: i32 = 98
|
|
497
663
|
|
|
664
|
+
/**
|
|
665
|
+
* The secrets of one connection under one hash, as `TlsServer` holds them
|
|
666
|
+
* in `secrets256` and `secrets384`: an array of exactly `hashLength` bytes
|
|
667
|
+
* for each, allocated once and reused by every connection. The ten the key
|
|
668
|
+
* schedule derives are `Secret`s (`nish:secret`), read only inside `expose`
|
|
669
|
+
* and wiped by `wipe`, so the checker proves no copy of them leaves the
|
|
670
|
+
* pool and that `wipe` reaches every one. The expected client Finished and
|
|
671
|
+
* the CertificateVerify input are what the wire carries, and stay plain;
|
|
672
|
+
* `wipe` zeroes them too (TLS-1).
|
|
673
|
+
*/
|
|
674
|
+
export class TlsSecretPool {
|
|
675
|
+
readonly handshake: Secret<u8[]>
|
|
676
|
+
readonly clientHandshake: Secret<u8[]>
|
|
677
|
+
readonly serverHandshake: Secret<u8[]>
|
|
678
|
+
readonly clientApplication: Secret<u8[]>
|
|
679
|
+
readonly serverApplication: Secret<u8[]>
|
|
680
|
+
/** `exporter_master_secret` (RFC 8446 §7.5), for a caller's own exporters. */
|
|
681
|
+
readonly exporter: Secret<u8[]>
|
|
682
|
+
/**
|
|
683
|
+
* `resumption_master_secret` (RFC 8446 §7.1), derived by `sign` and the
|
|
684
|
+
* connection's once the client's Finished is accepted: what a
|
|
685
|
+
* NewSessionTicket's PSK would be derived from.
|
|
686
|
+
*/
|
|
687
|
+
readonly resumption: Secret<u8[]>
|
|
688
|
+
/** The PSK the selected ticket opened to, on a resumed handshake only. */
|
|
689
|
+
readonly psk: Secret<u8[]>
|
|
690
|
+
/** The PSK early secret, on a resumed handshake only. */
|
|
691
|
+
readonly early: Secret<u8[]>
|
|
692
|
+
/** The binder key, from the selected ticket's early secret. */
|
|
693
|
+
readonly binderKey: Secret<u8[]>
|
|
694
|
+
/** The client Finished's `verify_data`, known once the server's Finished is written. */
|
|
695
|
+
clientFinished: u8[]
|
|
696
|
+
/** The CertificateVerify input, 98 bytes longer than the rest. */
|
|
697
|
+
toBeSigned: u8[]
|
|
698
|
+
|
|
699
|
+
constructor(hashLength: i32) {
|
|
700
|
+
this.handshake = secret(new Array<u8>(hashLength))
|
|
701
|
+
this.clientHandshake = secret(new Array<u8>(hashLength))
|
|
702
|
+
this.serverHandshake = secret(new Array<u8>(hashLength))
|
|
703
|
+
this.clientApplication = secret(new Array<u8>(hashLength))
|
|
704
|
+
this.serverApplication = secret(new Array<u8>(hashLength))
|
|
705
|
+
this.exporter = secret(new Array<u8>(hashLength))
|
|
706
|
+
this.resumption = secret(new Array<u8>(hashLength))
|
|
707
|
+
this.psk = secret(new Array<u8>(hashLength))
|
|
708
|
+
this.early = secret(new Array<u8>(hashLength))
|
|
709
|
+
this.binderKey = secret(new Array<u8>(hashLength))
|
|
710
|
+
this.clientFinished = new Array<u8>(hashLength)
|
|
711
|
+
this.toBeSigned = new Array<u8>(hashLength + TLS_CERTIFICATE_VERIFY_PREFIX)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Wipes the one secret `index` names (`TLS_SECRET_*`), leaving the rest; any other index wipes nothing. */
|
|
715
|
+
zero(index: i32): void {
|
|
716
|
+
if (index === TLS_SECRET_HANDSHAKE) {
|
|
717
|
+
exposeWith(this.handshake, TLS_FROM, tlsZeroHeld)
|
|
718
|
+
} else if (index === TLS_SECRET_CLIENT_HANDSHAKE) {
|
|
719
|
+
exposeWith(this.clientHandshake, TLS_FROM, tlsZeroHeld)
|
|
720
|
+
} else if (index === TLS_SECRET_SERVER_HANDSHAKE) {
|
|
721
|
+
exposeWith(this.serverHandshake, TLS_FROM, tlsZeroHeld)
|
|
722
|
+
} else if (index === TLS_SECRET_CLIENT_APPLICATION) {
|
|
723
|
+
exposeWith(this.clientApplication, TLS_FROM, tlsZeroHeld)
|
|
724
|
+
} else if (index === TLS_SECRET_SERVER_APPLICATION) {
|
|
725
|
+
exposeWith(this.serverApplication, TLS_FROM, tlsZeroHeld)
|
|
726
|
+
} else if (index === TLS_SECRET_EXPORTER) {
|
|
727
|
+
exposeWith(this.exporter, TLS_FROM, tlsZeroHeld)
|
|
728
|
+
} else if (index === TLS_SECRET_RESUMPTION) {
|
|
729
|
+
exposeWith(this.resumption, TLS_FROM, tlsZeroHeld)
|
|
730
|
+
} else if (index === TLS_SECRET_PSK) {
|
|
731
|
+
exposeWith(this.psk, TLS_FROM, tlsZeroHeld)
|
|
732
|
+
} else if (index === TLS_SECRET_EARLY) {
|
|
733
|
+
exposeWith(this.early, TLS_FROM, tlsZeroHeld)
|
|
734
|
+
} else if (index === TLS_SECRET_BINDER_KEY) {
|
|
735
|
+
exposeWith(this.binderKey, TLS_FROM, tlsZeroHeld)
|
|
736
|
+
} else if (index === TLS_SECRET_CLIENT_FINISHED) {
|
|
737
|
+
secureZero(this.clientFinished)
|
|
738
|
+
} else if (index === TLS_SECRET_TO_BE_SIGNED) {
|
|
739
|
+
secureZero(this.toBeSigned)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Wipes every secret in the pool. */
|
|
744
|
+
wipe(): void {
|
|
745
|
+
wipe(this.handshake)
|
|
746
|
+
wipe(this.clientHandshake)
|
|
747
|
+
wipe(this.serverHandshake)
|
|
748
|
+
wipe(this.clientApplication)
|
|
749
|
+
wipe(this.serverApplication)
|
|
750
|
+
wipe(this.exporter)
|
|
751
|
+
wipe(this.resumption)
|
|
752
|
+
wipe(this.psk)
|
|
753
|
+
wipe(this.early)
|
|
754
|
+
wipe(this.binderKey)
|
|
755
|
+
secureZero(this.clientFinished)
|
|
756
|
+
secureZero(this.toBeSigned)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
498
760
|
/**
|
|
499
761
|
* The most the server's Handshake-level flight under `config` takes with an
|
|
500
762
|
* ECDSA P-256 signature: EncryptedExtensions with every extension it may
|
|
@@ -521,6 +783,73 @@ const tlsFlightCapacity = (config: TlsServerConfig): i32 => {
|
|
|
521
783
|
return extensions + certificate + 8 + TLS_ECDSA_SIGNATURE_MAX + 4 + SHA384_SIZE
|
|
522
784
|
}
|
|
523
785
|
|
|
786
|
+
// ---- Resumption ------------------------------------------------------------------
|
|
787
|
+
|
|
788
|
+
/** What `tlsServerStrikeProbe` finds for an id: a free entry it could be recorded in. */
|
|
789
|
+
const TLS_SERVER_STRIKE_FREE: i32 = 0
|
|
790
|
+
/** The id is held: recorded and live (row 6 of the record's table). */
|
|
791
|
+
const TLS_SERVER_STRIKE_HELD: i32 = 1
|
|
792
|
+
/** No entry is free (row 7). */
|
|
793
|
+
const TLS_SERVER_STRIKE_FULL: i32 = 2
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* What `strikes.take(id, …, now)` would find, without recording anything:
|
|
797
|
+
* the same scan as `TlsTicketStrikes.take`, bounded by `longest` the same
|
|
798
|
+
* way. `resume` asks before the binder is checked, so that an identity
|
|
799
|
+
* already recorded is passed over rather than costing a binder, and records
|
|
800
|
+
* with `take` only once the binder verifies, so that a binder that fails
|
|
801
|
+
* records nothing (row 9).
|
|
802
|
+
*/
|
|
803
|
+
const tlsServerStrikeProbe = (strikes: TlsTicketStrikes, id: u8[], now: i64): i32 => {
|
|
804
|
+
const capacity: i32 = strikes.capacity
|
|
805
|
+
const home: i32 = strikes.home(id)
|
|
806
|
+
let free: i32 = -1
|
|
807
|
+
for (let n: i32 = 0; n < capacity; n++) {
|
|
808
|
+
const slot: i32 = (home + n) % capacity
|
|
809
|
+
if (strikes.used[slot] === 0) {
|
|
810
|
+
if (free < 0) {
|
|
811
|
+
free = slot
|
|
812
|
+
}
|
|
813
|
+
break
|
|
814
|
+
}
|
|
815
|
+
if (strikes.expires[slot] < now) {
|
|
816
|
+
if (free < 0) {
|
|
817
|
+
free = slot
|
|
818
|
+
}
|
|
819
|
+
} else if (n <= strikes.longest && strikes.holds(slot, id)) {
|
|
820
|
+
return TLS_SERVER_STRIKE_HELD
|
|
821
|
+
}
|
|
822
|
+
if (free >= 0 && n >= strikes.longest) {
|
|
823
|
+
break
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return free < 0 ? TLS_SERVER_STRIKE_FULL : TLS_SERVER_STRIKE_FREE
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** `answerHello`'s index when no ticket was chosen: a full handshake. */
|
|
830
|
+
const TLS_NO_IDENTITY: i32 = -1
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* What a NewSessionTicket adds around its ticket: the handshake header (4),
|
|
834
|
+
* `ticket_lifetime` (4), `ticket_age_add` (4), the nonce's length and its one
|
|
835
|
+
* byte (2), the ticket's length (2) and an empty extensions list (2).
|
|
836
|
+
*/
|
|
837
|
+
const TLS_SERVER_TICKET_OVERHEAD: i32 = 18
|
|
838
|
+
|
|
839
|
+
/** The bytes the largest ticket takes: a SHA-384 PSK and a 255-byte server name. */
|
|
840
|
+
const tlsServerTicketCapacity = (state: TlsTicketState): i32 => {
|
|
841
|
+
state.suite = TLS_AES_256_GCM_SHA384
|
|
842
|
+
state.pskLength = TLS_TICKET_PSK_MAX
|
|
843
|
+
state.serverNameLength = TLS_TICKET_SERVER_NAME_MAX
|
|
844
|
+
state.lifetime = TLS_TICKET_LIFETIME_SECONDS
|
|
845
|
+
const size: i32 = tlsTicketSize(state)
|
|
846
|
+
state.suite = 0
|
|
847
|
+
state.pskLength = 0
|
|
848
|
+
state.serverNameLength = 0
|
|
849
|
+
state.lifetime = 0
|
|
850
|
+
return size
|
|
851
|
+
}
|
|
852
|
+
|
|
524
853
|
/**
|
|
525
854
|
* One TLS 1.3 server handshake. Make one per connection, or one per slot and
|
|
526
855
|
* `restart` it for each connection the slot takes; feed it what the client
|
|
@@ -530,26 +859,30 @@ const tlsFlightCapacity = (config: TlsServerConfig): i32 => {
|
|
|
530
859
|
*
|
|
531
860
|
* The fields are readable: `state` and `alert`, the negotiated `suite`,
|
|
532
861
|
* `serverName`, `alpn` and the client's `clientTransportParameters`, and the
|
|
533
|
-
*
|
|
862
|
+
* `held` bits of the secrets derived so far. The secrets themselves are
|
|
863
|
+
* `Secret` fields of `secrets()`, read through `expose`; `readKnown` and
|
|
864
|
+
* `writeKnown` say whether the traffic secrets of a level are known yet.
|
|
534
865
|
*
|
|
535
866
|
* **What it allocates.** Everything it keeps is allocated by the constructor
|
|
536
867
|
* and reused by `restart`: the input and output buffers, the transcript's
|
|
537
|
-
* hashers, the HKDF scratch,
|
|
538
|
-
*
|
|
539
|
-
*
|
|
868
|
+
* hashers, the HKDF scratch, the ephemeral key's 32 bytes, and HashLen bytes for every secret
|
|
869
|
+
* under each hash, which each derivation writes over in place and `wipe`
|
|
870
|
+
* zeroes before the next connection starts. `receive` and
|
|
540
871
|
* `sign` do their work inside `using a = arena()` blocks, so the compiler has
|
|
541
872
|
* proved that what they allocate on the way dies before they return. Four
|
|
542
873
|
* things grow or are made outside those blocks: the input buffer, doubled up
|
|
543
874
|
* to the largest message when a ClientHello does not fit (once per slot);
|
|
544
875
|
* the Handshake-level output, when a signature is longer than an ECDSA one;
|
|
545
876
|
* the `serverName` string, made when the client's host name differs from the
|
|
546
|
-
* last one this server held; and, over QUIC only,
|
|
547
|
-
* transport parameters
|
|
877
|
+
* last one this server held; and, over QUIC only, the array the client's
|
|
878
|
+
* transport parameters are copied into, which grows only when a client's
|
|
879
|
+
* are longer than any before (TLS-3).
|
|
548
880
|
*/
|
|
549
881
|
export class TlsServer {
|
|
550
882
|
config: TlsServerConfig
|
|
551
883
|
serverRandom: u8[]
|
|
552
|
-
|
|
884
|
+
/** The connection's x25519 private key, copied in by `restart` and wiped by the record layer once the ServerHello is written. */
|
|
885
|
+
readonly ephemeral: Secret<u8[]>
|
|
553
886
|
/** The client's `host_name`, or empty. */
|
|
554
887
|
serverName: string = ""
|
|
555
888
|
/** The ALPN protocol chosen, one of the configuration's, or empty. */
|
|
@@ -558,6 +891,8 @@ export class TlsServer {
|
|
|
558
891
|
lastServerName: string = ""
|
|
559
892
|
/** The client's `quic_transport_parameters`, opaque; empty unless the carrier is QUIC. */
|
|
560
893
|
clientTransportParameters: u8[]
|
|
894
|
+
/** The array `clientTransportParameters` points at once read, kept across `restart` and refilled in the room it has. */
|
|
895
|
+
clientParameters: u8[]
|
|
561
896
|
transcript: TlsTranscript
|
|
562
897
|
/** The bytes received at the current level and not yet a whole message: `input[0 .. inputLength)`. */
|
|
563
898
|
input: u8[]
|
|
@@ -565,22 +900,11 @@ export class TlsServer {
|
|
|
565
900
|
outputInitial: u8[]
|
|
566
901
|
/** What the server has written at the Handshake level and not handed on: `outputHandshake[0 .. outputHandshakeLength)`. */
|
|
567
902
|
outputHandshake: u8[]
|
|
568
|
-
|
|
569
|
-
clientHandshakeSecret: u8[]
|
|
570
|
-
serverHandshakeSecret: u8[]
|
|
571
|
-
clientApplicationSecret: u8[]
|
|
572
|
-
serverApplicationSecret: u8[]
|
|
573
|
-
/** `exporter_master_secret` (RFC 8446 §7.5), for a caller's own exporters. */
|
|
574
|
-
exporterSecret: u8[]
|
|
575
|
-
/** The CertificateVerify input while `state` is `TLS_STATE_WAIT_SIGNATURE`. */
|
|
576
|
-
toBeSigned: u8[]
|
|
577
|
-
/** The client Finished's `verify_data`, known once the server's Finished is written. */
|
|
578
|
-
expectedClientFinished: u8[]
|
|
579
|
-
/** The empty array every secret field points at until its secret is derived. */
|
|
903
|
+
/** An empty array: what `clientTransportParameters` holds until a client's are read. */
|
|
580
904
|
none: u8[]
|
|
581
|
-
/** The
|
|
582
|
-
secrets256:
|
|
583
|
-
secrets384:
|
|
905
|
+
/** The secrets under each hash (`TlsSecretPool`); `secrets()` answers the negotiated hash's. */
|
|
906
|
+
secrets256: TlsSecretPool
|
|
907
|
+
secrets384: TlsSecretPool
|
|
584
908
|
/** The ClientHello last read, as windows into `input`. */
|
|
585
909
|
hello: TlsClientHelloView
|
|
586
910
|
/** Where every HKDF and HMAC of the schedule runs. */
|
|
@@ -591,6 +915,33 @@ export class TlsServer {
|
|
|
591
915
|
serverPublic: u8[]
|
|
592
916
|
/** A transcript hash on its way into the schedule: HashLen bytes of it. */
|
|
593
917
|
hashes: u8[]
|
|
918
|
+
/**
|
|
919
|
+
* The salt the handshake secret is extracted under, HashLen bytes of it:
|
|
920
|
+
* the `TLS_DERIVED_SALT_*` constant on a full handshake, derived from the
|
|
921
|
+
* PSK early secret on a resumed one. `exchangeKeys` reads it into the
|
|
922
|
+
* exchange's words and zeroes it.
|
|
923
|
+
*/
|
|
924
|
+
salt: u8[]
|
|
925
|
+
/** The transcript hash through the ClientHello's binders prefix, which a binder covers (§4.2.11.2). */
|
|
926
|
+
binderHash: u8[]
|
|
927
|
+
/** What the ticket `resume` is reading opened to; its PSK is zeroed once read. */
|
|
928
|
+
opened: TlsTicketState
|
|
929
|
+
/** What `issue` seals; its PSK is zeroed once sealed. */
|
|
930
|
+
issuing: TlsTicketState
|
|
931
|
+
/** The sealed ticket, sized once for the largest. */
|
|
932
|
+
ticketScratch: u8[]
|
|
933
|
+
/** What the server has written at the Application level and not handed on: one NewSessionTicket. */
|
|
934
|
+
outputApplication: u8[]
|
|
935
|
+
/** The ticket nonce: one ticket a connection, so one byte, the same each time, keeps each PSK distinct (§4.6.1). */
|
|
936
|
+
ticketNonce: u8[]
|
|
937
|
+
/** The ticket's `ticket_age_add`, drawn fresh for each ticket (§4.6.1). */
|
|
938
|
+
ageAddBytes: u8[]
|
|
939
|
+
/**
|
|
940
|
+
* Which of the negotiated pool's secrets the handshake has derived: bit
|
|
941
|
+
* `1 << TLS_SECRET_*` for each. A secret wiped by `wipeSecret` keeps its
|
|
942
|
+
* bit, since its place is still the one it was derived into.
|
|
943
|
+
*/
|
|
944
|
+
held: i32 = 0
|
|
594
945
|
state: i32 = 0
|
|
595
946
|
/** The alert the server failed with, or 0. */
|
|
596
947
|
alert: i32 = 0
|
|
@@ -600,10 +951,23 @@ export class TlsServer {
|
|
|
600
951
|
inputLength: i32 = 0
|
|
601
952
|
outputInitialLength: i32 = 0
|
|
602
953
|
outputHandshakeLength: i32 = 0
|
|
954
|
+
outputApplicationLength: i32 = 0
|
|
603
955
|
/** Whether a HelloRetryRequest was sent, so a second miss is fatal. */
|
|
604
956
|
retried: boolean = false
|
|
605
957
|
/** Whether `config` passed `tlsConfigFits`, decided once by the constructor. */
|
|
606
958
|
configFits: boolean = false
|
|
959
|
+
/** Whether the carrier turned resumption on with `enableTickets`; kept across `restart`. */
|
|
960
|
+
tickets: boolean = false
|
|
961
|
+
/**
|
|
962
|
+
* Whether this connection may be issued a ticket: tickets are on, the
|
|
963
|
+
* client listed `psk_dhe_ke`, the only mode this server resumes in, and its
|
|
964
|
+
* server name, if any, fits a ticket.
|
|
965
|
+
*/
|
|
966
|
+
ticketWanted: boolean = false
|
|
967
|
+
/** Whether `issue` has run for this connection. */
|
|
968
|
+
issued: boolean = false
|
|
969
|
+
/** Whether this connection resumed a ticket (row 1 of the record's table). */
|
|
970
|
+
resumed: boolean = false
|
|
607
971
|
|
|
608
972
|
/**
|
|
609
973
|
* A server for one connection under `config`, with the 32-byte server
|
|
@@ -614,49 +978,50 @@ export class TlsServer {
|
|
|
614
978
|
constructor(config: TlsServerConfig, serverRandom: u8[], ephemeralPrivate: u8[]) {
|
|
615
979
|
this.config = config
|
|
616
980
|
this.serverRandom = serverRandom
|
|
617
|
-
this.
|
|
981
|
+
this.ephemeral = secret(new Array<u8>(X25519_SIZE))
|
|
618
982
|
this.none = []
|
|
619
983
|
this.clientTransportParameters = this.none
|
|
984
|
+
this.clientParameters = []
|
|
620
985
|
this.transcript = new TlsTranscript(0)
|
|
621
986
|
this.configFits = tlsConfigFits(config)
|
|
622
987
|
this.input = new Array<u8>(TLS_INPUT_START)
|
|
623
988
|
this.outputInitial = new Array<u8>(TLS_INITIAL_OUTPUT_SIZE)
|
|
624
989
|
this.outputHandshake = new Array<u8>(this.configFits ? tlsFlightCapacity(config) : TLS_FROM)
|
|
625
|
-
this.secrets256 =
|
|
626
|
-
this.secrets384 =
|
|
627
|
-
for (let k: i32 = 0; k < TLS_SECRET_COUNT; k++) {
|
|
628
|
-
const extra: i32 = k === TLS_SECRET_TO_BE_SIGNED ? TLS_CERTIFICATE_VERIFY_PREFIX : 0
|
|
629
|
-
this.secrets256.push(new Array<u8>(SHA256_SIZE + extra))
|
|
630
|
-
this.secrets384.push(new Array<u8>(SHA384_SIZE + extra))
|
|
631
|
-
}
|
|
990
|
+
this.secrets256 = new TlsSecretPool(SHA256_SIZE)
|
|
991
|
+
this.secrets384 = new TlsSecretPool(SHA384_SIZE)
|
|
632
992
|
this.hello = new TlsClientHelloView()
|
|
633
993
|
this.kdf = new HkdfScratch()
|
|
634
994
|
this.exchange = new TlsExchangeWords()
|
|
635
995
|
this.serverPublic = new Array<u8>(X25519_SIZE)
|
|
636
996
|
this.hashes = new Array<u8>(SHA384_SIZE)
|
|
637
|
-
this.
|
|
638
|
-
this.
|
|
639
|
-
this.
|
|
640
|
-
this.
|
|
641
|
-
this.
|
|
642
|
-
this.
|
|
643
|
-
this.
|
|
644
|
-
this.
|
|
997
|
+
this.salt = new Array<u8>(SHA384_SIZE)
|
|
998
|
+
this.binderHash = new Array<u8>(SHA384_SIZE)
|
|
999
|
+
this.opened = new TlsTicketState()
|
|
1000
|
+
this.issuing = new TlsTicketState()
|
|
1001
|
+
this.ticketScratch = new Array<u8>(tlsServerTicketCapacity(this.issuing))
|
|
1002
|
+
this.outputApplication = new Array<u8>(toI32(this.ticketScratch.length) + TLS_SERVER_TICKET_OVERHEAD)
|
|
1003
|
+
this.ticketNonce = new Array<u8>(1)
|
|
1004
|
+
this.ageAddBytes = new Array<u8>(4)
|
|
645
1005
|
this.restart(serverRandom, ephemeralPrivate)
|
|
646
1006
|
}
|
|
647
1007
|
|
|
648
1008
|
/**
|
|
649
1009
|
* Starts the next connection in this server's buffers, with its own
|
|
650
1010
|
* 32-byte random and x25519 private key, as a fresh `TlsServer` would:
|
|
651
|
-
* every secret the last connection left
|
|
652
|
-
*
|
|
653
|
-
*
|
|
1011
|
+
* every secret the last connection left is wiped (`wipe`) and none is
|
|
1012
|
+
* held. The random is kept, as the constructor keeps it; the key is copied
|
|
1013
|
+
* into `ephemeral` and the caller's array zeroed with `secureZero`, so the
|
|
1014
|
+
* only copy left is the one the server wipes. A random or key of another
|
|
654
1015
|
* length, or a configuration `tlsConfigFits` refused, leaves the server
|
|
655
1016
|
* failed with `internal_error`.
|
|
656
1017
|
*/
|
|
657
1018
|
restart(serverRandom: u8[], ephemeralPrivate: u8[]): void {
|
|
1019
|
+
const keyLength: i32 = toI32(ephemeralPrivate.length)
|
|
1020
|
+
this.wipe()
|
|
1021
|
+
this.held = 0
|
|
1022
|
+
exposeWith(this.ephemeral, ephemeralPrivate, tlsHeldCopyIn)
|
|
1023
|
+
secureZero(ephemeralPrivate)
|
|
658
1024
|
this.serverRandom = serverRandom
|
|
659
|
-
this.ephemeralPrivate = ephemeralPrivate
|
|
660
1025
|
this.state = TLS_STATE_WAIT_CLIENT_HELLO
|
|
661
1026
|
this.alert = 0
|
|
662
1027
|
this.suite = 0
|
|
@@ -668,39 +1033,92 @@ export class TlsServer {
|
|
|
668
1033
|
this.inputLength = 0
|
|
669
1034
|
this.outputInitialLength = 0
|
|
670
1035
|
this.outputHandshakeLength = 0
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
this.
|
|
676
|
-
this.
|
|
677
|
-
this.
|
|
678
|
-
|
|
679
|
-
this.serverApplicationSecret = this.none
|
|
680
|
-
this.exporterSecret = this.none
|
|
681
|
-
this.toBeSigned = this.none
|
|
682
|
-
this.expectedClientFinished = this.none
|
|
683
|
-
if (
|
|
684
|
-
toI32(serverRandom.length) !== TLS_RANDOM_SIZE ||
|
|
685
|
-
toI32(ephemeralPrivate.length) !== X25519_SIZE ||
|
|
686
|
-
!this.configFits
|
|
687
|
-
) {
|
|
1036
|
+
this.outputApplicationLength = 0
|
|
1037
|
+
this.ticketWanted = false
|
|
1038
|
+
this.issued = false
|
|
1039
|
+
this.resumed = false
|
|
1040
|
+
secureZero(this.salt)
|
|
1041
|
+
secureZero(this.opened.psk)
|
|
1042
|
+
secureZero(this.issuing.psk)
|
|
1043
|
+
if (toI32(serverRandom.length) !== TLS_RANDOM_SIZE || keyLength !== X25519_SIZE || !this.configFits) {
|
|
688
1044
|
this.state = TLS_STATE_FAILED
|
|
689
1045
|
this.alert = TLS_ALERT_INTERNAL_ERROR
|
|
690
1046
|
}
|
|
691
1047
|
}
|
|
692
1048
|
|
|
693
|
-
/**
|
|
1049
|
+
/**
|
|
1050
|
+
* Fails the handshake with `alert` and answers it, wiping the resumption
|
|
1051
|
+
* master secret `sign` derived ahead and what a ticket being read left: its
|
|
1052
|
+
* PSK, early secret and binder key, and the salt.
|
|
1053
|
+
*/
|
|
694
1054
|
fail(alert: i32): i32 {
|
|
1055
|
+
this.wipeResumption()
|
|
1056
|
+
const pool: TlsSecretPool = this.secrets()
|
|
1057
|
+
pool.zero(TLS_SECRET_PSK)
|
|
1058
|
+
pool.zero(TLS_SECRET_EARLY)
|
|
1059
|
+
pool.zero(TLS_SECRET_BINDER_KEY)
|
|
1060
|
+
secureZero(this.salt)
|
|
1061
|
+
secureZero(this.opened.psk)
|
|
695
1062
|
this.state = TLS_STATE_FAILED
|
|
696
1063
|
this.alert = alert
|
|
697
1064
|
return alert
|
|
698
1065
|
}
|
|
699
1066
|
|
|
700
|
-
/**
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1067
|
+
/**
|
|
1068
|
+
* Wipes the resumption master secret, which `sign` derives ahead of the
|
|
1069
|
+
* client's Finished. `issue` calls it once the ticket is sealed, and the
|
|
1070
|
+
* carrier when a connection ends, closed or failed, without one (TLS-1).
|
|
1071
|
+
*/
|
|
1072
|
+
wipeResumption(): void {
|
|
1073
|
+
this.secrets().zero(TLS_SECRET_RESUMPTION)
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** The pool of the negotiated hash: SHA-384's for a SHA-384 suite, SHA-256's otherwise. */
|
|
1077
|
+
secrets(): TlsSecretPool {
|
|
1078
|
+
return this.hashLength === SHA384_SIZE ? this.secrets384 : this.secrets256
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** Whether the handshake has derived secret `index` (`TLS_SECRET_*`) for this connection. */
|
|
1082
|
+
known(index: i32): boolean {
|
|
1083
|
+
return index >= 0 && index < 31 && (this.held & (1 << index)) !== 0
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Whether the secret the server *reads* `level` under — the client's
|
|
1088
|
+
* traffic secret, `clientHandshake` or `clientApplication` of `secrets()` —
|
|
1089
|
+
* is known yet. The Initial level has none: it is cleartext in TLS, and
|
|
1090
|
+
* QUIC derives its own from the connection id.
|
|
1091
|
+
*/
|
|
1092
|
+
readKnown(level: i32): boolean {
|
|
1093
|
+
if (level === TLS_LEVEL_HANDSHAKE) {
|
|
1094
|
+
return this.known(TLS_SECRET_CLIENT_HANDSHAKE)
|
|
1095
|
+
}
|
|
1096
|
+
return level === TLS_LEVEL_APPLICATION && this.known(TLS_SECRET_CLIENT_APPLICATION)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** Whether the secret the server *writes* `level` under — `serverHandshake` or `serverApplication` — is known yet. */
|
|
1100
|
+
writeKnown(level: i32): boolean {
|
|
1101
|
+
if (level === TLS_LEVEL_HANDSHAKE) {
|
|
1102
|
+
return this.known(TLS_SECRET_SERVER_HANDSHAKE)
|
|
1103
|
+
}
|
|
1104
|
+
return level === TLS_LEVEL_APPLICATION && this.known(TLS_SECRET_SERVER_APPLICATION)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** Zeroes secret `index` of `secrets()` once a carrier has taken what it needs from it; it stays known. */
|
|
1108
|
+
wipeSecret(index: i32): void {
|
|
1109
|
+
this.secrets().zero(index)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** Zeroes the ephemeral key, which the record layer does once the ServerHello is written: not while a hello that offers a ticket waits for `resume`, whose exchange still needs it. */
|
|
1113
|
+
wipeEphemeral(): void {
|
|
1114
|
+
exposeWith(this.ephemeral, TLS_FROM, tlsZeroHeld)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** Wipes every secret the server holds: the ephemeral key and both pools. `restart` calls it, and the carriers when a connection ends. */
|
|
1118
|
+
wipe(): void {
|
|
1119
|
+
wipe(this.ephemeral)
|
|
1120
|
+
this.secrets256.wipe()
|
|
1121
|
+
this.secrets384.wipe()
|
|
704
1122
|
}
|
|
705
1123
|
|
|
706
1124
|
/** The level the client's next bytes must arrive at, or -1 when the state expects none. */
|
|
@@ -949,9 +1367,15 @@ export class TlsServer {
|
|
|
949
1367
|
if (this.retried && suite !== this.suite) {
|
|
950
1368
|
return TLS_ALERT_ILLEGAL_PARAMETER
|
|
951
1369
|
}
|
|
1370
|
+
// A ticket offered in `psk_dhe_ke` that this server reads: the
|
|
1371
|
+
// identities are read by `resume`, and a hello that resumes needs no
|
|
1372
|
+
// signature (row 14 of "Resumption" in `docs/security/tls.md`).
|
|
1373
|
+
const offersTicket: boolean = this.tickets && hello.hasPreSharedKey && hello.pskDheKe
|
|
952
1374
|
// §9.2: a certificate-authenticated handshake needs all three, and
|
|
953
|
-
// `supported_groups` and `key_share` come as a pair.
|
|
954
|
-
|
|
1375
|
+
// `supported_groups` and `key_share` come as a pair. A hello offering a
|
|
1376
|
+
// ticket has `signature_algorithms` checked by `answerHello`, once it is
|
|
1377
|
+
// known not to resume.
|
|
1378
|
+
if ((!offersTicket && !hello.hasSignatureAlgorithms) || !hello.hasSupportedGroups || !hello.hasKeyShare) {
|
|
955
1379
|
return TLS_ALERT_MISSING_EXTENSION
|
|
956
1380
|
}
|
|
957
1381
|
if (this.config.quic && !hello.hasQuicTransportParameters) {
|
|
@@ -960,7 +1384,10 @@ export class TlsServer {
|
|
|
960
1384
|
if (!tlsWindowHasU16(data, hello.groupsAt, hello.groupsLength, TLS_GROUP_X25519)) {
|
|
961
1385
|
return TLS_ALERT_HANDSHAKE_FAILURE
|
|
962
1386
|
}
|
|
963
|
-
if (
|
|
1387
|
+
if (
|
|
1388
|
+
!offersTicket &&
|
|
1389
|
+
!tlsWindowHasU16(data, hello.schemesAt, hello.schemesLength, this.config.signatureScheme)
|
|
1390
|
+
) {
|
|
964
1391
|
return TLS_ALERT_HANDSHAKE_FAILURE
|
|
965
1392
|
}
|
|
966
1393
|
if (!this.retried) {
|
|
@@ -968,7 +1395,16 @@ export class TlsServer {
|
|
|
968
1395
|
this.hashLength = tlsSuiteHashLength(suite)
|
|
969
1396
|
this.transcript.reset(this.hashLength)
|
|
970
1397
|
}
|
|
971
|
-
|
|
1398
|
+
if (offersTicket) {
|
|
1399
|
+
// The binders cover the transcript up to the binders list (§4.2.11.2),
|
|
1400
|
+
// so its hash is taken on the way through.
|
|
1401
|
+
const prefix: i32 = hello.pskBindersPrefixAt
|
|
1402
|
+
this.transcript.update(data, TLS_FROM, prefix)
|
|
1403
|
+
this.transcript.hashInto(this.binderHash, TLS_FROM)
|
|
1404
|
+
this.transcript.update(data, prefix, 4 + length - prefix)
|
|
1405
|
+
} else {
|
|
1406
|
+
this.transcript.update(data, TLS_FROM, 4 + length)
|
|
1407
|
+
}
|
|
972
1408
|
if (!hello.hasX25519Share) {
|
|
973
1409
|
// §4.1.4: one HelloRetryRequest; a second ClientHello without the share
|
|
974
1410
|
// the first one asked for is `illegal_parameter`.
|
|
@@ -1012,71 +1448,376 @@ export class TlsServer {
|
|
|
1012
1448
|
return alpnAlert
|
|
1013
1449
|
}
|
|
1014
1450
|
this.takeServerName(hello)
|
|
1451
|
+
this.ticketWanted =
|
|
1452
|
+
this.tickets &&
|
|
1453
|
+
hello.hasPskModes &&
|
|
1454
|
+
hello.pskDheKe &&
|
|
1455
|
+
(!hello.hasServerName || hello.serverNameLength <= TLS_TICKET_SERVER_NAME_MAX)
|
|
1456
|
+
const name: u8[] = this.issuing.serverName
|
|
1457
|
+
const nameLength: i32 = this.ticketWanted && hello.hasServerName ? hello.serverNameLength : 0
|
|
1458
|
+
for (
|
|
1459
|
+
let k: i32 = 0;
|
|
1460
|
+
k < nameLength &&
|
|
1461
|
+
k < toI32(name.length) &&
|
|
1462
|
+
hello.serverNameAt + k >= 0 &&
|
|
1463
|
+
hello.serverNameAt + k < toI32(data.length);
|
|
1464
|
+
k++
|
|
1465
|
+
) {
|
|
1466
|
+
name[k] = data[hello.serverNameAt + k]
|
|
1467
|
+
}
|
|
1468
|
+
this.issuing.serverNameLength = nameLength
|
|
1015
1469
|
if (this.config.quic) {
|
|
1016
|
-
|
|
1470
|
+
// Refilled in place, so a slot's handshakes keep nothing for it (QUIC-3).
|
|
1471
|
+
const parameters: u8[] = this.clientParameters
|
|
1472
|
+
while (parameters.length > 0) {
|
|
1473
|
+
parameters.pop()
|
|
1474
|
+
}
|
|
1017
1475
|
for (
|
|
1018
1476
|
let k: i32 = 0;
|
|
1019
|
-
k <
|
|
1477
|
+
k < hello.quicLength && hello.quicAt + k >= 0 && hello.quicAt + k < toI32(data.length);
|
|
1020
1478
|
k++
|
|
1021
1479
|
) {
|
|
1022
|
-
parameters
|
|
1480
|
+
parameters.push(data[hello.quicAt + k])
|
|
1023
1481
|
}
|
|
1024
1482
|
this.clientTransportParameters = parameters
|
|
1025
1483
|
}
|
|
1484
|
+
if (offersTicket) {
|
|
1485
|
+
// The carrier lends the ticket keys and the strike register to `resume`.
|
|
1486
|
+
this.state = TLS_STATE_WAIT_TICKET
|
|
1487
|
+
return 0
|
|
1488
|
+
}
|
|
1489
|
+
return this.answerHello(TLS_NO_IDENTITY)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Answers the ClientHello once it is known whether it resumes: the
|
|
1494
|
+
* ServerHello naming identity `index` and the resumed flight, or, when it
|
|
1495
|
+
* does not resume, the checks a certificate needs and the full flight.
|
|
1496
|
+
* Answers 0 or the alert.
|
|
1497
|
+
*/
|
|
1498
|
+
answerHello(index: i32): i32 {
|
|
1499
|
+
const hello: TlsClientHelloView = this.hello
|
|
1500
|
+
if (!this.resumed) {
|
|
1501
|
+
if (!hello.hasSignatureAlgorithms) {
|
|
1502
|
+
return TLS_ALERT_MISSING_EXTENSION
|
|
1503
|
+
}
|
|
1504
|
+
if (!tlsWindowHasU16(this.input, hello.schemesAt, hello.schemesLength, this.config.signatureScheme)) {
|
|
1505
|
+
return TLS_ALERT_HANDSHAKE_FAILURE
|
|
1506
|
+
}
|
|
1507
|
+
tlsHexInto(
|
|
1508
|
+
this.hashLength === SHA384_SIZE ? TLS_DERIVED_SALT_SHA384 : TLS_DERIVED_SALT_SHA256,
|
|
1509
|
+
this.salt,
|
|
1510
|
+
TLS_FROM
|
|
1511
|
+
)
|
|
1512
|
+
}
|
|
1026
1513
|
const exchangeAlert: i32 = this.exchangeKeys()
|
|
1027
1514
|
if (exchangeAlert !== 0) {
|
|
1028
1515
|
return exchangeAlert
|
|
1029
1516
|
}
|
|
1030
|
-
this.
|
|
1517
|
+
if (this.resumed) {
|
|
1518
|
+
this.writeResumedFlight(index)
|
|
1519
|
+
} else {
|
|
1520
|
+
this.writeServerFlight()
|
|
1521
|
+
}
|
|
1031
1522
|
return 0
|
|
1032
1523
|
}
|
|
1033
1524
|
|
|
1525
|
+
/**
|
|
1526
|
+
* Turns resumption on: from the next ClientHello, a hello offering a
|
|
1527
|
+
* ticket in `psk_dhe_ke` waits in `TLS_STATE_WAIT_TICKET` for `resume`,
|
|
1528
|
+
* and a connection whose client lists `psk_dhe_ke` may be issued a ticket
|
|
1529
|
+
* by `issue`. Kept across `restart`. A QUIC server never resumes, so on one
|
|
1530
|
+
* this does nothing.
|
|
1531
|
+
*/
|
|
1532
|
+
enableTickets(): void {
|
|
1533
|
+
this.tickets = !this.config.quic
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Whether the ticket in `opened` may resume this hello: its suite's hash is
|
|
1538
|
+
* the negotiated one, its PSK is that long, and it names the server name
|
|
1539
|
+
* this hello names, or none when the hello names none (rows 4 and 5).
|
|
1540
|
+
*/
|
|
1541
|
+
ticketFits(): boolean {
|
|
1542
|
+
const hello: TlsClientHelloView = this.hello
|
|
1543
|
+
const opened: TlsTicketState = this.opened
|
|
1544
|
+
const h: i32 = this.hashLength
|
|
1545
|
+
if (tlsSuiteHashLength(opened.suite) !== h || opened.pskLength !== h) {
|
|
1546
|
+
return false
|
|
1547
|
+
}
|
|
1548
|
+
if (!hello.hasServerName) {
|
|
1549
|
+
return opened.serverNameLength === 0
|
|
1550
|
+
}
|
|
1551
|
+
return (
|
|
1552
|
+
opened.serverNameLength === hello.serverNameLength &&
|
|
1553
|
+
timingSafeEqualAt(hello.data, hello.serverNameAt, opened.serverName, TLS_FROM, hello.serverNameLength)
|
|
1554
|
+
)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* The first identity whose ticket opens under `current` or `previous`,
|
|
1559
|
+
* fits (`ticketFits`) and is not yet recorded in `strikes`, left selected
|
|
1560
|
+
* in `hello` with its ticket in `opened`; or -1, with every PSK an
|
|
1561
|
+
* identity opened to zeroed. A full register ends the search and is
|
|
1562
|
+
* counted with `take`, so that a full register fails closed into a full
|
|
1563
|
+
* handshake (row 7).
|
|
1564
|
+
*/
|
|
1565
|
+
selectTicket(
|
|
1566
|
+
current: Secret<u8[]>,
|
|
1567
|
+
currentInfo: TlsTicketKeyInfo,
|
|
1568
|
+
previous: Secret<u8[]>,
|
|
1569
|
+
previousInfo: TlsTicketKeyInfo,
|
|
1570
|
+
strikes: TlsTicketStrikes,
|
|
1571
|
+
now: i64
|
|
1572
|
+
): i32 {
|
|
1573
|
+
const hello: TlsClientHelloView = this.hello
|
|
1574
|
+
const opened: TlsTicketState = this.opened
|
|
1575
|
+
const data: u8[] = this.input
|
|
1576
|
+
let chosen: i32 = -1
|
|
1577
|
+
using _scope = arena()
|
|
1578
|
+
for (let k: i32 = 0; k < hello.pskIdentityCount; k++) {
|
|
1579
|
+
if (!tlsSelectPskIdentity(hello, k)) {
|
|
1580
|
+
break
|
|
1581
|
+
}
|
|
1582
|
+
const ticket: u8[] = new Array<u8>(hello.pskIdentityLength)
|
|
1583
|
+
for (
|
|
1584
|
+
let n: i32 = 0;
|
|
1585
|
+
n < toI32(ticket.length) &&
|
|
1586
|
+
hello.pskIdentityAt + n >= 0 &&
|
|
1587
|
+
hello.pskIdentityAt + n < toI32(data.length);
|
|
1588
|
+
n++
|
|
1589
|
+
) {
|
|
1590
|
+
ticket[n] = data[hello.pskIdentityAt + n]
|
|
1591
|
+
}
|
|
1592
|
+
const opens: boolean =
|
|
1593
|
+
tlsTicketOpen(current, currentInfo, ticket, now, opened) ||
|
|
1594
|
+
tlsTicketOpen(previous, previousInfo, ticket, now, opened)
|
|
1595
|
+
if (opens && this.ticketFits()) {
|
|
1596
|
+
const probe: i32 = tlsServerStrikeProbe(strikes, opened.id, now)
|
|
1597
|
+
if (probe === TLS_SERVER_STRIKE_FREE) {
|
|
1598
|
+
chosen = k
|
|
1599
|
+
break
|
|
1600
|
+
}
|
|
1601
|
+
if (probe === TLS_SERVER_STRIKE_FULL) {
|
|
1602
|
+
strikes.take(opened.id, opened.expiresAt(), now)
|
|
1603
|
+
secureZero(opened.psk)
|
|
1604
|
+
break
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
secureZero(opened.psk)
|
|
1608
|
+
}
|
|
1609
|
+
if (chosen >= 0) {
|
|
1610
|
+
tlsSelectPskIdentity(hello, chosen)
|
|
1611
|
+
}
|
|
1612
|
+
return chosen
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* The selected ticket's PSK, early secret and binder key, and whether the
|
|
1617
|
+
* hello's binder for it verifies (§4.2.11.2). The PSK and binder key are
|
|
1618
|
+
* zeroed before it returns; the early secret stays for `saltFromEarly`.
|
|
1619
|
+
*/
|
|
1620
|
+
bindTicket(): boolean {
|
|
1621
|
+
const hello: TlsClientHelloView = this.hello
|
|
1622
|
+
const pool: TlsSecretPool = this.secrets()
|
|
1623
|
+
const h: i32 = this.hashLength
|
|
1624
|
+
exposeWith(pool.psk, this.opened.psk, tlsHeldCopyIn)
|
|
1625
|
+
secureZero(this.opened.psk)
|
|
1626
|
+
using _scope = arena()
|
|
1627
|
+
const psk: u8[] = expose(pool.psk, tlsHeldClone)
|
|
1628
|
+
const early: u8[] = new Array<u8>(h)
|
|
1629
|
+
tlsPskEarlySecretInto(this.kdf, h, psk, early)
|
|
1630
|
+
secureZero(psk)
|
|
1631
|
+
exposeWith(pool.early, early, tlsHeldCopyIn)
|
|
1632
|
+
const binderKey: u8[] = new Array<u8>(h)
|
|
1633
|
+
tlsBinderKeyInto(this.kdf, h, early, binderKey)
|
|
1634
|
+
secureZero(early)
|
|
1635
|
+
exposeWith(pool.binderKey, binderKey, tlsHeldCopyIn)
|
|
1636
|
+
const verified: boolean = tlsBinderVerify(
|
|
1637
|
+
this.kdf,
|
|
1638
|
+
h,
|
|
1639
|
+
binderKey,
|
|
1640
|
+
this.binderHash,
|
|
1641
|
+
TLS_FROM,
|
|
1642
|
+
this.input,
|
|
1643
|
+
hello.pskBinderAt,
|
|
1644
|
+
hello.pskBinderLength
|
|
1645
|
+
)
|
|
1646
|
+
secureZero(binderKey)
|
|
1647
|
+
pool.zero(TLS_SECRET_PSK)
|
|
1648
|
+
pool.zero(TLS_SECRET_BINDER_KEY)
|
|
1649
|
+
return verified
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/** The salt a resumed handshake secret is extracted under, `Derive-Secret(early, "derived", "")`, into `salt`. */
|
|
1653
|
+
saltFromEarly(): void {
|
|
1654
|
+
const pool: TlsSecretPool = this.secrets()
|
|
1655
|
+
using _scope = arena()
|
|
1656
|
+
const early: u8[] = expose(pool.early, tlsHeldClone)
|
|
1657
|
+
tlsHandshakeSaltInto(this.kdf, this.hashLength, early, this.salt)
|
|
1658
|
+
secureZero(early)
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* In `TLS_STATE_WAIT_TICKET`: reads the hello's tickets under the keys the
|
|
1663
|
+
* carrier lends, `current` and `previous` with what each `info` says of
|
|
1664
|
+
* them (a program with one key passes it as both), records the one it
|
|
1665
|
+
* resumes in `strikes`, and answers the hello, resumed or in full, as the
|
|
1666
|
+
* table of "Resumption" in `docs/security/tls.md` says. A selected ticket
|
|
1667
|
+
* whose binder fails is `decrypt_error`, with nothing recorded (row 9).
|
|
1668
|
+
* Answers 0, or the alert the server fails with.
|
|
1669
|
+
*/
|
|
1670
|
+
resume(
|
|
1671
|
+
current: Secret<u8[]>,
|
|
1672
|
+
currentInfo: TlsTicketKeyInfo,
|
|
1673
|
+
previous: Secret<u8[]>,
|
|
1674
|
+
previousInfo: TlsTicketKeyInfo,
|
|
1675
|
+
strikes: TlsTicketStrikes,
|
|
1676
|
+
now: i64
|
|
1677
|
+
): i32 {
|
|
1678
|
+
if (this.state === TLS_STATE_FAILED) {
|
|
1679
|
+
return this.alert
|
|
1680
|
+
}
|
|
1681
|
+
if (this.state !== TLS_STATE_WAIT_TICKET) {
|
|
1682
|
+
return this.fail(TLS_ALERT_INTERNAL_ERROR)
|
|
1683
|
+
}
|
|
1684
|
+
const index: i32 = this.selectTicket(current, currentInfo, previous, previousInfo, strikes, now)
|
|
1685
|
+
let bound: boolean = true
|
|
1686
|
+
let resumed: boolean = false
|
|
1687
|
+
if (index >= 0) {
|
|
1688
|
+
bound = this.bindTicket()
|
|
1689
|
+
resumed = bound && strikes.take(this.opened.id, this.opened.expiresAt(), now)
|
|
1690
|
+
}
|
|
1691
|
+
if (resumed) {
|
|
1692
|
+
this.saltFromEarly()
|
|
1693
|
+
this.issuing.chainStart = this.opened.chainStart
|
|
1694
|
+
}
|
|
1695
|
+
secureZero(this.opened.psk)
|
|
1696
|
+
this.secrets().zero(TLS_SECRET_EARLY)
|
|
1697
|
+
if (!bound) {
|
|
1698
|
+
return this.fail(TLS_ALERT_DECRYPT_ERROR)
|
|
1699
|
+
}
|
|
1700
|
+
this.resumed = resumed
|
|
1701
|
+
const alert: i32 = this.answerHello(resumed ? index : TLS_NO_IDENTITY)
|
|
1702
|
+
return alert !== 0 ? this.fail(alert) : 0
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* A resumed flight: the ServerHello selecting identity `index`, the
|
|
1707
|
+
* handshake traffic secrets, EncryptedExtensions and the server's
|
|
1708
|
+
* Finished, after which the server waits for the client's.
|
|
1709
|
+
*/
|
|
1710
|
+
writeResumedFlight(index: i32): void {
|
|
1711
|
+
const hello: TlsClientHelloView = this.hello
|
|
1712
|
+
const none: u8[] = []
|
|
1713
|
+
this.reserveOutput(
|
|
1714
|
+
TLS_LEVEL_INITIAL,
|
|
1715
|
+
tlsWriteServerHelloPsk(
|
|
1716
|
+
none,
|
|
1717
|
+
this.outputInitialLength,
|
|
1718
|
+
this.serverRandom,
|
|
1719
|
+
hello.data,
|
|
1720
|
+
hello.sessionIdAt,
|
|
1721
|
+
hello.sessionIdLength,
|
|
1722
|
+
this.suite,
|
|
1723
|
+
this.serverPublic,
|
|
1724
|
+
index
|
|
1725
|
+
)
|
|
1726
|
+
)
|
|
1727
|
+
const extensionsEnd: i32 = tlsWriteEncryptedExtensions(
|
|
1728
|
+
none,
|
|
1729
|
+
this.outputHandshakeLength,
|
|
1730
|
+
this.config.extraExtensions,
|
|
1731
|
+
this.alpn,
|
|
1732
|
+
hello.hasServerName,
|
|
1733
|
+
this.config.quic ? this.config.quicTransportParameters : null
|
|
1734
|
+
)
|
|
1735
|
+
this.reserveOutput(TLS_LEVEL_HANDSHAKE, extensionsEnd + 4 + this.hashLength)
|
|
1736
|
+
// The secrets' pool is the slot's, chosen before the blocks open.
|
|
1737
|
+
const pool: TlsSecretPool = this.secrets()
|
|
1738
|
+
this.writeResumedHello(pool, index)
|
|
1739
|
+
this.writeServerFinished(pool)
|
|
1740
|
+
this.masterSecretStep(pool)
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/** The resumed ServerHello, the handshake traffic secrets and EncryptedExtensions, in the room `writeResumedFlight` reserved. */
|
|
1744
|
+
writeResumedHello(pool: TlsSecretPool, index: i32): void {
|
|
1745
|
+
const hello: TlsClientHelloView = this.hello
|
|
1746
|
+
using _scope = arena()
|
|
1747
|
+
const helloAt: i32 = this.outputInitialLength
|
|
1748
|
+
const helloEnd: i32 = tlsWriteServerHelloPsk(
|
|
1749
|
+
this.outputInitial,
|
|
1750
|
+
helloAt,
|
|
1751
|
+
this.serverRandom,
|
|
1752
|
+
hello.data,
|
|
1753
|
+
hello.sessionIdAt,
|
|
1754
|
+
hello.sessionIdLength,
|
|
1755
|
+
this.suite,
|
|
1756
|
+
this.serverPublic,
|
|
1757
|
+
index
|
|
1758
|
+
)
|
|
1759
|
+
this.sent(TLS_LEVEL_INITIAL, helloAt, helloEnd)
|
|
1760
|
+
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
1761
|
+
const handshake: u8[] = expose(pool.handshake, tlsHeldClone)
|
|
1762
|
+
this.deriveInto(handshake, "c hs traffic", pool.clientHandshake)
|
|
1763
|
+
this.deriveInto(handshake, "s hs traffic", pool.serverHandshake)
|
|
1764
|
+
secureZero(handshake)
|
|
1765
|
+
this.held = this.held | (1 << TLS_SECRET_CLIENT_HANDSHAKE) | (1 << TLS_SECRET_SERVER_HANDSHAKE)
|
|
1766
|
+
const extensionsAt: i32 = this.outputHandshakeLength
|
|
1767
|
+
const written: i32 = tlsWriteEncryptedExtensions(
|
|
1768
|
+
this.outputHandshake,
|
|
1769
|
+
extensionsAt,
|
|
1770
|
+
this.config.extraExtensions,
|
|
1771
|
+
this.alpn,
|
|
1772
|
+
hello.hasServerName,
|
|
1773
|
+
this.config.quic ? this.config.quicTransportParameters : null
|
|
1774
|
+
)
|
|
1775
|
+
this.sent(TLS_LEVEL_HANDSHAKE, extensionsAt, written)
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1034
1778
|
/**
|
|
1035
1779
|
* Runs the x25519 exchange on the caller's ephemeral key and the client's
|
|
1036
1780
|
* share (`tlsExchange`), outside any arena block since its scope is its
|
|
1037
1781
|
* own, and unpacks the server's public key and the handshake secret from
|
|
1038
|
-
* the words
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1782
|
+
* the words. The words, the ephemeral key's and the handshake secret's
|
|
1783
|
+
* among them, are wiped with one `wipe` after the branch on the alert, so
|
|
1784
|
+
* that a refused exchange leaves them zero too and `opt` has one wipe to
|
|
1785
|
+
* keep rather than two to merge. The ephemeral key is read out of its
|
|
1786
|
+
* `Secret` field a word at a time; the record layer wipes it once the
|
|
1787
|
+
* ServerHello is written (TLS-1).
|
|
1041
1788
|
*/
|
|
1042
1789
|
exchangeKeys(): i32 {
|
|
1043
1790
|
const w: TlsExchangeWords = this.exchange
|
|
1044
|
-
const key: u8[] = this.ephemeralPrivate
|
|
1045
1791
|
const share: u8[] = this.input
|
|
1046
1792
|
const at: i32 = this.hello.x25519ShareAt
|
|
1047
|
-
w.key0 =
|
|
1048
|
-
w.key1 =
|
|
1049
|
-
w.key2 =
|
|
1050
|
-
w.key3 =
|
|
1793
|
+
w.key0 = exposeWith(this.ephemeral, TLS_FROM, tlsWordAt)
|
|
1794
|
+
w.key1 = exposeWith(this.ephemeral, TLS_FROM + 8, tlsWordAt)
|
|
1795
|
+
w.key2 = exposeWith(this.ephemeral, TLS_FROM + 16, tlsWordAt)
|
|
1796
|
+
w.key3 = exposeWith(this.ephemeral, TLS_FROM + 24, tlsWordAt)
|
|
1051
1797
|
w.share0 = tlsWordAt(share, at)
|
|
1052
1798
|
w.share1 = tlsWordAt(share, at + 8)
|
|
1053
1799
|
w.share2 = tlsWordAt(share, at + 16)
|
|
1054
1800
|
w.share3 = tlsWordAt(share, at + 24)
|
|
1055
1801
|
w.hashLength = this.hashLength
|
|
1802
|
+
w.salt0 = tlsWordAt(this.salt, 0)
|
|
1803
|
+
w.salt1 = tlsWordAt(this.salt, 8)
|
|
1804
|
+
w.salt2 = tlsWordAt(this.salt, 16)
|
|
1805
|
+
w.salt3 = tlsWordAt(this.salt, 24)
|
|
1806
|
+
w.salt4 = tlsWordAt(this.salt, 32)
|
|
1807
|
+
w.salt5 = tlsWordAt(this.salt, 40)
|
|
1808
|
+
secureZero(this.salt)
|
|
1056
1809
|
const alert: i32 = tlsExchange(w)
|
|
1057
|
-
if (alert
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
tlsWordInto(handshake, 8, w.secret1)
|
|
1069
|
-
tlsWordInto(handshake, 16, w.secret2)
|
|
1070
|
-
tlsWordInto(handshake, 24, w.secret3)
|
|
1071
|
-
tlsWordInto(handshake, 32, w.secret4)
|
|
1072
|
-
tlsWordInto(handshake, 40, w.secret5)
|
|
1073
|
-
w.secret0 = toU64(0)
|
|
1074
|
-
w.secret1 = toU64(0)
|
|
1075
|
-
w.secret2 = toU64(0)
|
|
1076
|
-
w.secret3 = toU64(0)
|
|
1077
|
-
w.secret4 = toU64(0)
|
|
1078
|
-
w.secret5 = toU64(0)
|
|
1079
|
-
return 0
|
|
1810
|
+
if (alert === 0) {
|
|
1811
|
+
const pub: u8[] = this.serverPublic
|
|
1812
|
+
tlsWordInto(pub, 0, w.public0)
|
|
1813
|
+
tlsWordInto(pub, 8, w.public1)
|
|
1814
|
+
tlsWordInto(pub, 16, w.public2)
|
|
1815
|
+
tlsWordInto(pub, 24, w.public3)
|
|
1816
|
+
exposeWith(this.secrets().handshake, w, tlsHandshakeFromWords)
|
|
1817
|
+
this.held = this.held | (1 << TLS_SECRET_HANDSHAKE)
|
|
1818
|
+
}
|
|
1819
|
+
wipe(w)
|
|
1820
|
+
return alert
|
|
1080
1821
|
}
|
|
1081
1822
|
|
|
1082
1823
|
/**
|
|
@@ -1116,10 +1857,8 @@ export class TlsServer {
|
|
|
1116
1857
|
TLS_LEVEL_HANDSHAKE,
|
|
1117
1858
|
tlsWriteCertificate(none, extensionsEnd, this.config.certificateChain)
|
|
1118
1859
|
)
|
|
1119
|
-
// The secrets'
|
|
1120
|
-
|
|
1121
|
-
this.serverHandshakeSecret = this.secretArray(TLS_SECRET_SERVER_HANDSHAKE)
|
|
1122
|
-
this.toBeSigned = this.secretArray(TLS_SECRET_TO_BE_SIGNED)
|
|
1860
|
+
// The secrets' pool is the slot's, chosen before the block opens.
|
|
1861
|
+
const pool: TlsSecretPool = this.secrets()
|
|
1123
1862
|
using _scope = arena()
|
|
1124
1863
|
const helloAt: i32 = this.outputInitialLength
|
|
1125
1864
|
const helloEnd: i32 = tlsWriteServerHello(
|
|
@@ -1135,24 +1874,11 @@ export class TlsServer {
|
|
|
1135
1874
|
this.sent(TLS_LEVEL_INITIAL, helloAt, helloEnd)
|
|
1136
1875
|
|
|
1137
1876
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
this.hashes,
|
|
1144
|
-
TLS_FROM,
|
|
1145
|
-
this.clientHandshakeSecret
|
|
1146
|
-
)
|
|
1147
|
-
tlsDeriveSecretInto(
|
|
1148
|
-
this.kdf,
|
|
1149
|
-
h,
|
|
1150
|
-
this.handshakeSecret,
|
|
1151
|
-
"s hs traffic",
|
|
1152
|
-
this.hashes,
|
|
1153
|
-
TLS_FROM,
|
|
1154
|
-
this.serverHandshakeSecret
|
|
1155
|
-
)
|
|
1877
|
+
const handshake: u8[] = expose(pool.handshake, tlsHeldClone)
|
|
1878
|
+
this.deriveInto(handshake, "c hs traffic", pool.clientHandshake)
|
|
1879
|
+
this.deriveInto(handshake, "s hs traffic", pool.serverHandshake)
|
|
1880
|
+
secureZero(handshake)
|
|
1881
|
+
this.held = this.held | (1 << TLS_SECRET_CLIENT_HANDSHAKE) | (1 << TLS_SECRET_SERVER_HANDSHAKE)
|
|
1156
1882
|
|
|
1157
1883
|
const extensionsAt: i32 = this.outputHandshakeLength
|
|
1158
1884
|
const written: i32 = tlsWriteEncryptedExtensions(
|
|
@@ -1172,10 +1898,23 @@ export class TlsServer {
|
|
|
1172
1898
|
this.sent(TLS_LEVEL_HANDSHAKE, written, certificateEnd)
|
|
1173
1899
|
|
|
1174
1900
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
1175
|
-
tlsWriteCertificateVerifyContent(
|
|
1901
|
+
tlsWriteCertificateVerifyContent(pool.toBeSigned, TLS_FROM, this.hashes, TLS_FROM, h)
|
|
1902
|
+
this.held = this.held | (1 << TLS_SECRET_TO_BE_SIGNED)
|
|
1176
1903
|
this.state = TLS_STATE_WAIT_SIGNATURE
|
|
1177
1904
|
}
|
|
1178
1905
|
|
|
1906
|
+
/**
|
|
1907
|
+
* Derive-Secret(`prk`, `label`, the transcript hash in `hashes`) into the
|
|
1908
|
+
* held secret `dst` (RFC 8446 §7.1): derived into a plain array that dies
|
|
1909
|
+
* with the caller's arena block, copied in, and zeroed.
|
|
1910
|
+
*/
|
|
1911
|
+
deriveInto(prk: u8[], label: string, dst: Secret<u8[]>): void {
|
|
1912
|
+
const out: u8[] = new Array<u8>(this.hashLength)
|
|
1913
|
+
tlsDeriveSecretInto(this.kdf, this.hashLength, prk, label, this.hashes, TLS_FROM, out)
|
|
1914
|
+
exposeWith(dst, out, tlsHeldCopyIn)
|
|
1915
|
+
secureZero(out)
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1179
1918
|
/**
|
|
1180
1919
|
* What CertificateVerify signs (RFC 8446 §4.4.3), while the state is
|
|
1181
1920
|
* `TLS_STATE_WAIT_SIGNATURE`; `null` at any other time.
|
|
@@ -1184,7 +1923,7 @@ export class TlsServer {
|
|
|
1184
1923
|
if (this.state !== TLS_STATE_WAIT_SIGNATURE) {
|
|
1185
1924
|
return null
|
|
1186
1925
|
}
|
|
1187
|
-
return this.toBeSigned
|
|
1926
|
+
return this.secrets().toBeSigned
|
|
1188
1927
|
}
|
|
1189
1928
|
|
|
1190
1929
|
/**
|
|
@@ -1205,11 +1944,17 @@ export class TlsServer {
|
|
|
1205
1944
|
}
|
|
1206
1945
|
const h: i32 = this.hashLength
|
|
1207
1946
|
this.reserveOutput(TLS_LEVEL_HANDSHAKE, this.outputHandshakeLength + 8 + signatureLength + 4 + h)
|
|
1208
|
-
// The secrets'
|
|
1209
|
-
|
|
1210
|
-
this.
|
|
1211
|
-
this.
|
|
1212
|
-
this.
|
|
1947
|
+
// The secrets' pool is the slot's, chosen before the blocks open.
|
|
1948
|
+
const pool: TlsSecretPool = this.secrets()
|
|
1949
|
+
this.writeCertificateVerify(signature)
|
|
1950
|
+
this.writeServerFinished(pool)
|
|
1951
|
+
this.masterSecretStep(pool)
|
|
1952
|
+
this.held = this.held & ~(1 << TLS_SECRET_TO_BE_SIGNED)
|
|
1953
|
+
return 0
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** CertificateVerify, carrying `signature`, at the Handshake level, in the room `sign` reserved. */
|
|
1957
|
+
writeCertificateVerify(signature: u8[]): void {
|
|
1213
1958
|
using _scope = arena()
|
|
1214
1959
|
const verifyAt: i32 = this.outputHandshakeLength
|
|
1215
1960
|
const verifyEnd: i32 = tlsWriteCertificateVerify(
|
|
@@ -1219,80 +1964,167 @@ export class TlsServer {
|
|
|
1219
1964
|
signature
|
|
1220
1965
|
)
|
|
1221
1966
|
this.sent(TLS_LEVEL_HANDSHAKE, verifyAt, verifyEnd)
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* The server's Finished at the Handshake level, in the room reserved for
|
|
1971
|
+
* it: its `verify_data`, under the server's handshake traffic secret, goes
|
|
1972
|
+
* straight into its place in the flight.
|
|
1973
|
+
*/
|
|
1974
|
+
writeServerFinished(pool: TlsSecretPool): void {
|
|
1975
|
+
const h: i32 = this.hashLength
|
|
1976
|
+
using _scope = arena()
|
|
1977
|
+
const at: i32 = this.outputHandshakeLength
|
|
1222
1978
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
1223
|
-
|
|
1979
|
+
const serverHandshake: u8[] = expose(pool.serverHandshake, tlsHeldClone)
|
|
1224
1980
|
tlsFinishedVerifyDataInto(
|
|
1225
1981
|
this.kdf,
|
|
1226
1982
|
h,
|
|
1227
|
-
|
|
1983
|
+
serverHandshake,
|
|
1228
1984
|
this.hashes,
|
|
1229
1985
|
TLS_FROM,
|
|
1230
1986
|
this.outputHandshake,
|
|
1231
|
-
|
|
1232
|
-
)
|
|
1233
|
-
const finishedEnd: i32 = tlsWriteFinished(
|
|
1234
|
-
this.outputHandshake,
|
|
1235
|
-
verifyEnd,
|
|
1236
|
-
this.outputHandshake,
|
|
1237
|
-
verifyEnd + 4,
|
|
1238
|
-
h
|
|
1987
|
+
at + 4
|
|
1239
1988
|
)
|
|
1240
|
-
|
|
1989
|
+
secureZero(serverHandshake)
|
|
1990
|
+
const finishedEnd: i32 = tlsWriteFinished(this.outputHandshake, at, this.outputHandshake, at + 4, h)
|
|
1991
|
+
this.sent(TLS_LEVEL_HANDSHAKE, at, finishedEnd)
|
|
1992
|
+
}
|
|
1241
1993
|
|
|
1994
|
+
/**
|
|
1995
|
+
* Once the server's Finished is written, full handshake or resumed: the
|
|
1996
|
+
* master secret, the application and exporter secrets, the client's
|
|
1997
|
+
* Finished the server expects, and the resumption master secret, after
|
|
1998
|
+
* which the server waits for that Finished.
|
|
1999
|
+
*/
|
|
2000
|
+
masterSecretStep(pool: TlsSecretPool): void {
|
|
2001
|
+
const h: i32 = this.hashLength
|
|
2002
|
+
using _scope = arena()
|
|
1242
2003
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
2004
|
+
const handshake: u8[] = expose(pool.handshake, tlsHeldClone)
|
|
1243
2005
|
const master: u8[] = new Array<u8>(h)
|
|
1244
|
-
tlsMasterSecretInto(this.kdf, h,
|
|
1245
|
-
|
|
2006
|
+
tlsMasterSecretInto(this.kdf, h, handshake, master)
|
|
2007
|
+
secureZero(handshake)
|
|
2008
|
+
this.deriveInto(master, "c ap traffic", pool.clientApplication)
|
|
2009
|
+
this.deriveInto(master, "s ap traffic", pool.serverApplication)
|
|
2010
|
+
this.deriveInto(master, "exp master", pool.exporter)
|
|
2011
|
+
const clientHandshake: u8[] = expose(pool.clientHandshake, tlsHeldClone)
|
|
2012
|
+
const finished: u8[] = pool.clientFinished
|
|
2013
|
+
tlsFinishedVerifyDataInto(this.kdf, h, clientHandshake, this.hashes, TLS_FROM, finished, TLS_FROM)
|
|
2014
|
+
secureZero(clientHandshake)
|
|
2015
|
+
// The only client Finished `handleFinished` accepts is the one just
|
|
2016
|
+
// computed, so the transcript through it is known now (RFC 8446 §4.6.1),
|
|
2017
|
+
// and the resumption master secret is derived while the master secret is.
|
|
2018
|
+
const clientFinished: u8[] = new Array<u8>(4 + h)
|
|
2019
|
+
tlsWriteFinished(clientFinished, TLS_FROM, finished, TLS_FROM, h)
|
|
2020
|
+
this.transcript.update(clientFinished, TLS_FROM, toI32(clientFinished.length))
|
|
2021
|
+
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
2022
|
+
this.deriveInto(master, "res master", pool.resumption)
|
|
2023
|
+
secureZero(master)
|
|
2024
|
+
this.held =
|
|
2025
|
+
this.held |
|
|
2026
|
+
(1 << TLS_SECRET_CLIENT_APPLICATION) |
|
|
2027
|
+
(1 << TLS_SECRET_SERVER_APPLICATION) |
|
|
2028
|
+
(1 << TLS_SECRET_EXPORTER) |
|
|
2029
|
+
(1 << TLS_SECRET_CLIENT_FINISHED)
|
|
2030
|
+
this.state = TLS_STATE_WAIT_FINISHED
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/**
|
|
2034
|
+
* Once the handshake is done, at most once a connection: seals a ticket
|
|
2035
|
+
* for it under `key`, which `info` describes, and writes the
|
|
2036
|
+
* NewSessionTicket carrying it at the Application level, for the carrier
|
|
2037
|
+
* to send under the server's application keys. Answers whether one was
|
|
2038
|
+
* written: none is when tickets are off, the client did not list
|
|
2039
|
+
* `psk_dhe_ke`, or the key does not seal. On a connected connection's
|
|
2040
|
+
* first call the resumption master secret is wiped before it returns,
|
|
2041
|
+
* written or not, so a carrier that issues no ticket still calls this, or
|
|
2042
|
+
* `wipeResumption`. Before the handshake is done, or on a second call, it
|
|
2043
|
+
* answers false and touches nothing: a secret wiped then would leave no
|
|
2044
|
+
* ticket to issue, and one wiped already has nothing left to wipe.
|
|
2045
|
+
*/
|
|
2046
|
+
issue(key: Secret<u8[]>, info: TlsTicketKeyInfo, now: i64): boolean {
|
|
2047
|
+
if (this.state !== TLS_STATE_CONNECTED || this.issued) {
|
|
2048
|
+
return false
|
|
2049
|
+
}
|
|
2050
|
+
this.issued = true
|
|
2051
|
+
const written: boolean = this.ticketWanted && this.writeTicket(key, info, now)
|
|
2052
|
+
this.wipeResumption()
|
|
2053
|
+
return written
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/** The resumption PSK for nonce `ticketNonce`, into `issuing.psk` (§4.6.1). */
|
|
2057
|
+
resumptionPsk(): void {
|
|
2058
|
+
const pool: TlsSecretPool = this.secrets()
|
|
2059
|
+
using _scope = arena()
|
|
2060
|
+
const resumption: u8[] = expose(pool.resumption, tlsHeldClone)
|
|
2061
|
+
tlsResumptionPskInto(
|
|
1246
2062
|
this.kdf,
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
this.hashes,
|
|
2063
|
+
this.hashLength,
|
|
2064
|
+
resumption,
|
|
2065
|
+
this.ticketNonce,
|
|
1251
2066
|
TLS_FROM,
|
|
1252
|
-
|
|
2067
|
+
1,
|
|
2068
|
+
this.issuing.psk
|
|
1253
2069
|
)
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2070
|
+
secureZero(resumption)
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/** `issue`'s ticket: sealed into `ticketScratch`, its PSK zeroed, and the NewSessionTicket written. */
|
|
2074
|
+
writeTicket(key: Secret<u8[]>, info: TlsTicketKeyInfo, now: i64): boolean {
|
|
2075
|
+
const issuing: TlsTicketState = this.issuing
|
|
2076
|
+
issuing.suite = this.suite
|
|
2077
|
+
issuing.lifetime = TLS_TICKET_LIFETIME_SECONDS
|
|
2078
|
+
issuing.pskLength = this.hashLength
|
|
2079
|
+
if (!this.resumed) {
|
|
2080
|
+
issuing.chainStart = now
|
|
2081
|
+
}
|
|
2082
|
+
this.resumptionPsk()
|
|
2083
|
+
const length: i32 = tlsTicketSeal(key, info, issuing, now, this.ticketScratch)
|
|
2084
|
+
secureZero(issuing.psk)
|
|
2085
|
+
if (length === 0) {
|
|
2086
|
+
return false
|
|
2087
|
+
}
|
|
2088
|
+
const b: u8[] = this.ageAddBytes
|
|
2089
|
+
crypto.getRandomValues(b)
|
|
2090
|
+
const ageAdd: u32 = toU32(
|
|
2091
|
+
(toI64(b[0]) << toI64(24)) | (toI64(b[1]) << toI64(16)) | (toI64(b[2]) << toI64(8)) | toI64(b[3])
|
|
1262
2092
|
)
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
this.
|
|
2093
|
+
this.outputApplicationLength = tlsWriteNewSessionTicket(
|
|
2094
|
+
this.outputApplication,
|
|
2095
|
+
TLS_FROM,
|
|
2096
|
+
toU32(issuing.lifetime),
|
|
2097
|
+
ageAdd,
|
|
2098
|
+
this.ticketNonce,
|
|
2099
|
+
TLS_FROM,
|
|
2100
|
+
1,
|
|
2101
|
+
this.ticketScratch,
|
|
1269
2102
|
TLS_FROM,
|
|
1270
|
-
|
|
1271
|
-
|
|
2103
|
+
length,
|
|
2104
|
+
this.none
|
|
1272
2105
|
)
|
|
1273
|
-
|
|
1274
|
-
this.toBeSigned = this.none
|
|
1275
|
-
this.state = TLS_STATE_WAIT_FINISHED
|
|
1276
|
-
return 0
|
|
2106
|
+
return true
|
|
1277
2107
|
}
|
|
1278
2108
|
|
|
1279
2109
|
/**
|
|
1280
2110
|
* The client's Finished (§4.4.4), `input[0 .. 4 + length)`: its
|
|
1281
2111
|
* `verify_data` is HashLen bytes and must equal the HMAC the server
|
|
1282
2112
|
* computed, compared in constant time. A wrong length is `decode_error`, a
|
|
1283
|
-
* wrong value `decrypt_error`.
|
|
2113
|
+
* wrong value `decrypt_error`. Once it verifies, the resumption master
|
|
2114
|
+
* secret `sign` derived is the connection's, and known. The transcript already holds this Finished: `sign` absorbed the
|
|
2115
|
+
* one it expected, and these bytes are equal to it.
|
|
1284
2116
|
*/
|
|
1285
2117
|
handleFinished(length: i32): i32 {
|
|
1286
2118
|
if (length !== this.hashLength) {
|
|
1287
2119
|
return TLS_ALERT_DECODE_ERROR
|
|
1288
2120
|
}
|
|
1289
|
-
|
|
2121
|
+
const pool: TlsSecretPool = this.secrets()
|
|
2122
|
+
if (!timingSafeEqualAt(this.input, 4, pool.clientFinished, TLS_FROM, this.hashLength)) {
|
|
1290
2123
|
return TLS_ALERT_DECRYPT_ERROR
|
|
1291
2124
|
}
|
|
1292
|
-
this.transcript.update(this.input, TLS_FROM, 4 + length)
|
|
1293
2125
|
// The handshake secret has done its work: wiped, and no longer held.
|
|
1294
|
-
|
|
1295
|
-
this.
|
|
2126
|
+
pool.zero(TLS_SECRET_HANDSHAKE)
|
|
2127
|
+
this.held = (this.held | (1 << TLS_SECRET_RESUMPTION)) & ~(1 << TLS_SECRET_HANDSHAKE)
|
|
1296
2128
|
this.state = TLS_STATE_CONNECTED
|
|
1297
2129
|
return 0
|
|
1298
2130
|
}
|
|
@@ -1337,8 +2169,15 @@ export class TlsServer {
|
|
|
1337
2169
|
this.outputHandshakeLength = 0
|
|
1338
2170
|
return out
|
|
1339
2171
|
}
|
|
2172
|
+
case TLS_LEVEL_APPLICATION: {
|
|
2173
|
+
const out: u8[] = new Array<u8>(this.outputApplicationLength)
|
|
2174
|
+
for (let k: i32 = 0; k < toI32(out.length) && k < toI32(this.outputApplication.length); k++) {
|
|
2175
|
+
out[k] = this.outputApplication[k]
|
|
2176
|
+
}
|
|
2177
|
+
this.outputApplicationLength = 0
|
|
2178
|
+
return out
|
|
2179
|
+
}
|
|
1340
2180
|
default:
|
|
1341
|
-
// Nothing is written at the Application level before NewSessionTicket, which is out of scope.
|
|
1342
2181
|
return none
|
|
1343
2182
|
}
|
|
1344
2183
|
}
|
|
@@ -1349,20 +2188,8 @@ export class TlsServer {
|
|
|
1349
2188
|
this.outputInitialLength = 0
|
|
1350
2189
|
} else if (level === TLS_LEVEL_HANDSHAKE) {
|
|
1351
2190
|
this.outputHandshakeLength = 0
|
|
2191
|
+
} else if (level === TLS_LEVEL_APPLICATION) {
|
|
2192
|
+
this.outputApplicationLength = 0
|
|
1352
2193
|
}
|
|
1353
2194
|
}
|
|
1354
|
-
|
|
1355
|
-
/**
|
|
1356
|
-
* The secret the server *reads* `level` under — the client's traffic secret
|
|
1357
|
-
* — or `null` while it is not known yet. The Initial level has none: it is
|
|
1358
|
-
* cleartext in TLS, and QUIC derives its own from the connection id.
|
|
1359
|
-
*/
|
|
1360
|
-
readSecret(level: i32): u8[] | null {
|
|
1361
|
-
return tlsSecretAt(level, this.clientHandshakeSecret, this.clientApplicationSecret)
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
/** The secret the server *writes* `level` under — its own traffic secret — or `null` while it is not known yet. */
|
|
1365
|
-
writeSecret(level: i32): u8[] | null {
|
|
1366
|
-
return tlsSecretAt(level, this.serverHandshakeSecret, this.serverApplicationSecret)
|
|
1367
|
-
}
|
|
1368
2195
|
}
|