@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
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,8 +84,9 @@
|
|
|
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
92
|
import { P256SignScratch, P256_SIGNATURE_SIZE, p256SignSha256, p256SignSha256Into } from "nish/crypto/p256"
|
|
@@ -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
|
|
@@ -373,41 +413,64 @@ export class TlsP256Signer {
|
|
|
373
413
|
const tlsIsZeroSecret = (shared: u8[]): boolean => timingSafeEqual(shared, new Array<u8>(X25519_SIZE))
|
|
374
414
|
|
|
375
415
|
/**
|
|
376
|
-
* `Derive-Secret(early, "derived", "")` with no PSK, the salt
|
|
416
|
+
* `Derive-Secret(early, "derived", "")` with no PSK, the salt a full handshake's
|
|
377
417
|
* secret is extracted under: a constant of the hash alone (RFC 8448 §3 prints
|
|
378
418
|
* the SHA-256 one; the SHA-384 one was computed with Python's hmac and
|
|
379
419
|
* hashlib). It is written out rather than derived because the functions below
|
|
380
420
|
* run on the ECDHE secret inside `expose`, and Expand-Label may panic with a
|
|
381
|
-
* 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.
|
|
382
424
|
*/
|
|
383
425
|
const TLS_DERIVED_SALT_SHA256: string = "6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba"
|
|
384
426
|
const TLS_DERIVED_SALT_SHA384: string =
|
|
385
427
|
"1591dac5cbbf0330a4a84de9c753330e92d01f0a88214b4464972fd668049e93e52f2b16fad922fdc0584478428f282b"
|
|
386
428
|
|
|
387
|
-
/**
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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)
|
|
393
436
|
|
|
394
|
-
/** The SHA-
|
|
395
|
-
const
|
|
396
|
-
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)
|
|
397
439
|
|
|
398
|
-
/**
|
|
399
|
-
const
|
|
400
|
-
|
|
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
|
+
}
|
|
401
445
|
|
|
402
|
-
/**
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return null
|
|
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]
|
|
407
450
|
}
|
|
408
|
-
return
|
|
451
|
+
return 0
|
|
409
452
|
}
|
|
410
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]
|
|
463
|
+
}
|
|
464
|
+
return copy
|
|
465
|
+
}
|
|
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
|
+
|
|
411
474
|
// ---- The exchange, in a scope of its own --------------------------------------
|
|
412
475
|
|
|
413
476
|
/**
|
|
@@ -427,9 +490,9 @@ const tlsSecretAt = (level: i32, handshake: u8[], application: u8[]): u8[] | nul
|
|
|
427
490
|
* `tests/link/net_tls_memory` would notice.
|
|
428
491
|
*
|
|
429
492
|
* The words hold secrets — the private key and the handshake secret — in
|
|
430
|
-
* fields, where `secureZero` cannot reach, so
|
|
431
|
-
*
|
|
432
|
-
*
|
|
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`).
|
|
433
496
|
*/
|
|
434
497
|
class TlsExchangeWords {
|
|
435
498
|
key0: u64 = 0
|
|
@@ -450,6 +513,17 @@ class TlsExchangeWords {
|
|
|
450
513
|
secret3: u64 = 0
|
|
451
514
|
secret4: u64 = 0
|
|
452
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
|
|
453
527
|
/** 32 or 48: which handshake secret to extract. */
|
|
454
528
|
hashLength: i32 = 0
|
|
455
529
|
}
|
|
@@ -474,10 +548,21 @@ const tlsWordInto = (b: u8[], at: i32, v: u64): void => {
|
|
|
474
548
|
}
|
|
475
549
|
}
|
|
476
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
|
+
|
|
477
562
|
/**
|
|
478
563
|
* The exchange of RFC 8446 §7.4.2 on `w`: the server's public key and the
|
|
479
564
|
* handshake secret from the ephemeral key and the client's share, both
|
|
480
|
-
* written back as words
|
|
565
|
+
* written back as words; the caller wipes the words. Answers 0,
|
|
481
566
|
* `illegal_parameter` for a share whose shared secret is all zeros (§7.4.2,
|
|
482
567
|
* RFC 7748 §6.1), or `internal_error` when the exchange answers nothing. The
|
|
483
568
|
* copy of the key and the ECDHE secret are `Secret`s wiped on every path, and
|
|
@@ -487,15 +572,18 @@ const tlsWordInto = (b: u8[], at: i32, v: u64): void => {
|
|
|
487
572
|
* returns.
|
|
488
573
|
*/
|
|
489
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)
|
|
490
582
|
const raw: u8[] = new Array<u8>(X25519_SIZE)
|
|
491
583
|
tlsWordInto(raw, 0, w.key0)
|
|
492
584
|
tlsWordInto(raw, 8, w.key1)
|
|
493
585
|
tlsWordInto(raw, 16, w.key2)
|
|
494
586
|
tlsWordInto(raw, 24, w.key3)
|
|
495
|
-
w.key0 = toU64(0)
|
|
496
|
-
w.key1 = toU64(0)
|
|
497
|
-
w.key2 = toU64(0)
|
|
498
|
-
w.key3 = toU64(0)
|
|
499
587
|
const share: u8[] = new Array<u8>(X25519_SIZE)
|
|
500
588
|
tlsWordInto(share, 0, w.share0)
|
|
501
589
|
tlsWordInto(share, 8, w.share1)
|
|
@@ -505,33 +593,40 @@ const tlsExchange = (w: TlsExchangeWords): i32 => {
|
|
|
505
593
|
const serverPublic: u8[] | null = x25519Base(key)
|
|
506
594
|
const shared: Secret<u8[]> | null = x25519(key, share)
|
|
507
595
|
wipe(key)
|
|
596
|
+
let alert: i32 = 0
|
|
508
597
|
if (shared === null) {
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
624
|
wipe(shared)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
: expose(shared, tlsHandshakeFromEcdhe256)
|
|
522
|
-
wipe(shared)
|
|
523
|
-
w.public0 = tlsWordAt(serverPublic, 0)
|
|
524
|
-
w.public1 = tlsWordAt(serverPublic, 8)
|
|
525
|
-
w.public2 = tlsWordAt(serverPublic, 16)
|
|
526
|
-
w.public3 = tlsWordAt(serverPublic, 24)
|
|
527
|
-
w.secret0 = tlsWordAt(handshake, 0)
|
|
528
|
-
w.secret1 = tlsWordAt(handshake, 8)
|
|
529
|
-
w.secret2 = tlsWordAt(handshake, 16)
|
|
530
|
-
w.secret3 = tlsWordAt(handshake, 24)
|
|
531
|
-
w.secret4 = tlsWordAt(handshake, 32)
|
|
532
|
-
w.secret5 = tlsWordAt(handshake, 40)
|
|
533
|
-
secureZero(handshake)
|
|
534
|
-
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
|
|
535
630
|
}
|
|
536
631
|
|
|
537
632
|
// ---- Buffers -------------------------------------------------------------------
|
|
@@ -542,20 +637,126 @@ const TLS_INPUT_START: i32 = 2048
|
|
|
542
637
|
const TLS_INITIAL_OUTPUT_SIZE: i32 = 256
|
|
543
638
|
/** The longest DER ECDSA P-256 signature, what the flight is first sized for: two 33-byte integers and their headers. */
|
|
544
639
|
const TLS_ECDSA_SIGNATURE_MAX: i32 = 72
|
|
545
|
-
/**
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
const
|
|
552
|
-
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
|
|
553
652
|
/** The CertificateVerify input sits beside them: 98 bytes and the transcript hash. */
|
|
554
|
-
const TLS_SECRET_TO_BE_SIGNED: i32 = 7
|
|
555
|
-
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
|
|
556
661
|
/** What CertificateVerify signs besides the hash (RFC 8446 §4.4.3): 64 spaces, the 33-byte context and a zero. */
|
|
557
662
|
const TLS_CERTIFICATE_VERIFY_PREFIX: i32 = 98
|
|
558
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
|
+
|
|
559
760
|
/**
|
|
560
761
|
* The most the server's Handshake-level flight under `config` takes with an
|
|
561
762
|
* ECDSA P-256 signature: EncryptedExtensions with every extension it may
|
|
@@ -582,6 +783,73 @@ const tlsFlightCapacity = (config: TlsServerConfig): i32 => {
|
|
|
582
783
|
return extensions + certificate + 8 + TLS_ECDSA_SIGNATURE_MAX + 4 + SHA384_SIZE
|
|
583
784
|
}
|
|
584
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
|
+
|
|
585
853
|
/**
|
|
586
854
|
* One TLS 1.3 server handshake. Make one per connection, or one per slot and
|
|
587
855
|
* `restart` it for each connection the slot takes; feed it what the client
|
|
@@ -591,13 +859,15 @@ const tlsFlightCapacity = (config: TlsServerConfig): i32 => {
|
|
|
591
859
|
*
|
|
592
860
|
* The fields are readable: `state` and `alert`, the negotiated `suite`,
|
|
593
861
|
* `serverName`, `alpn` and the client's `clientTransportParameters`, and the
|
|
594
|
-
*
|
|
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.
|
|
595
865
|
*
|
|
596
866
|
* **What it allocates.** Everything it keeps is allocated by the constructor
|
|
597
867
|
* and reused by `restart`: the input and output buffers, the transcript's
|
|
598
|
-
* hashers, the HKDF scratch,
|
|
599
|
-
*
|
|
600
|
-
*
|
|
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
|
|
601
871
|
* `sign` do their work inside `using a = arena()` blocks, so the compiler has
|
|
602
872
|
* proved that what they allocate on the way dies before they return. Four
|
|
603
873
|
* things grow or are made outside those blocks: the input buffer, doubled up
|
|
@@ -611,7 +881,8 @@ const tlsFlightCapacity = (config: TlsServerConfig): i32 => {
|
|
|
611
881
|
export class TlsServer {
|
|
612
882
|
config: TlsServerConfig
|
|
613
883
|
serverRandom: u8[]
|
|
614
|
-
|
|
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[]>
|
|
615
886
|
/** The client's `host_name`, or empty. */
|
|
616
887
|
serverName: string = ""
|
|
617
888
|
/** The ALPN protocol chosen, one of the configuration's, or empty. */
|
|
@@ -629,22 +900,11 @@ export class TlsServer {
|
|
|
629
900
|
outputInitial: u8[]
|
|
630
901
|
/** What the server has written at the Handshake level and not handed on: `outputHandshake[0 .. outputHandshakeLength)`. */
|
|
631
902
|
outputHandshake: u8[]
|
|
632
|
-
|
|
633
|
-
clientHandshakeSecret: u8[]
|
|
634
|
-
serverHandshakeSecret: u8[]
|
|
635
|
-
clientApplicationSecret: u8[]
|
|
636
|
-
serverApplicationSecret: u8[]
|
|
637
|
-
/** `exporter_master_secret` (RFC 8446 §7.5), for a caller's own exporters. */
|
|
638
|
-
exporterSecret: u8[]
|
|
639
|
-
/** The CertificateVerify input while `state` is `TLS_STATE_WAIT_SIGNATURE`. */
|
|
640
|
-
toBeSigned: u8[]
|
|
641
|
-
/** The client Finished's `verify_data`, known once the server's Finished is written. */
|
|
642
|
-
expectedClientFinished: u8[]
|
|
643
|
-
/** 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. */
|
|
644
904
|
none: u8[]
|
|
645
|
-
/** The
|
|
646
|
-
secrets256:
|
|
647
|
-
secrets384:
|
|
905
|
+
/** The secrets under each hash (`TlsSecretPool`); `secrets()` answers the negotiated hash's. */
|
|
906
|
+
secrets256: TlsSecretPool
|
|
907
|
+
secrets384: TlsSecretPool
|
|
648
908
|
/** The ClientHello last read, as windows into `input`. */
|
|
649
909
|
hello: TlsClientHelloView
|
|
650
910
|
/** Where every HKDF and HMAC of the schedule runs. */
|
|
@@ -655,6 +915,33 @@ export class TlsServer {
|
|
|
655
915
|
serverPublic: u8[]
|
|
656
916
|
/** A transcript hash on its way into the schedule: HashLen bytes of it. */
|
|
657
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
|
|
658
945
|
state: i32 = 0
|
|
659
946
|
/** The alert the server failed with, or 0. */
|
|
660
947
|
alert: i32 = 0
|
|
@@ -664,10 +951,23 @@ export class TlsServer {
|
|
|
664
951
|
inputLength: i32 = 0
|
|
665
952
|
outputInitialLength: i32 = 0
|
|
666
953
|
outputHandshakeLength: i32 = 0
|
|
954
|
+
outputApplicationLength: i32 = 0
|
|
667
955
|
/** Whether a HelloRetryRequest was sent, so a second miss is fatal. */
|
|
668
956
|
retried: boolean = false
|
|
669
957
|
/** Whether `config` passed `tlsConfigFits`, decided once by the constructor. */
|
|
670
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
|
|
671
971
|
|
|
672
972
|
/**
|
|
673
973
|
* A server for one connection under `config`, with the 32-byte server
|
|
@@ -678,7 +978,7 @@ export class TlsServer {
|
|
|
678
978
|
constructor(config: TlsServerConfig, serverRandom: u8[], ephemeralPrivate: u8[]) {
|
|
679
979
|
this.config = config
|
|
680
980
|
this.serverRandom = serverRandom
|
|
681
|
-
this.
|
|
981
|
+
this.ephemeral = secret(new Array<u8>(X25519_SIZE))
|
|
682
982
|
this.none = []
|
|
683
983
|
this.clientTransportParameters = this.none
|
|
684
984
|
this.clientParameters = []
|
|
@@ -687,41 +987,41 @@ export class TlsServer {
|
|
|
687
987
|
this.input = new Array<u8>(TLS_INPUT_START)
|
|
688
988
|
this.outputInitial = new Array<u8>(TLS_INITIAL_OUTPUT_SIZE)
|
|
689
989
|
this.outputHandshake = new Array<u8>(this.configFits ? tlsFlightCapacity(config) : TLS_FROM)
|
|
690
|
-
this.secrets256 =
|
|
691
|
-
this.secrets384 =
|
|
692
|
-
for (let k: i32 = 0; k < TLS_SECRET_COUNT; k++) {
|
|
693
|
-
const extra: i32 = k === TLS_SECRET_TO_BE_SIGNED ? TLS_CERTIFICATE_VERIFY_PREFIX : 0
|
|
694
|
-
this.secrets256.push(new Array<u8>(SHA256_SIZE + extra))
|
|
695
|
-
this.secrets384.push(new Array<u8>(SHA384_SIZE + extra))
|
|
696
|
-
}
|
|
990
|
+
this.secrets256 = new TlsSecretPool(SHA256_SIZE)
|
|
991
|
+
this.secrets384 = new TlsSecretPool(SHA384_SIZE)
|
|
697
992
|
this.hello = new TlsClientHelloView()
|
|
698
993
|
this.kdf = new HkdfScratch()
|
|
699
994
|
this.exchange = new TlsExchangeWords()
|
|
700
995
|
this.serverPublic = new Array<u8>(X25519_SIZE)
|
|
701
996
|
this.hashes = new Array<u8>(SHA384_SIZE)
|
|
702
|
-
this.
|
|
703
|
-
this.
|
|
704
|
-
this.
|
|
705
|
-
this.
|
|
706
|
-
this.
|
|
707
|
-
this.
|
|
708
|
-
this.
|
|
709
|
-
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)
|
|
710
1005
|
this.restart(serverRandom, ephemeralPrivate)
|
|
711
1006
|
}
|
|
712
1007
|
|
|
713
1008
|
/**
|
|
714
1009
|
* Starts the next connection in this server's buffers, with its own
|
|
715
1010
|
* 32-byte random and x25519 private key, as a fresh `TlsServer` would:
|
|
716
|
-
* every secret the last connection left
|
|
717
|
-
*
|
|
718
|
-
*
|
|
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
|
|
719
1015
|
* length, or a configuration `tlsConfigFits` refused, leaves the server
|
|
720
1016
|
* failed with `internal_error`.
|
|
721
1017
|
*/
|
|
722
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)
|
|
723
1024
|
this.serverRandom = serverRandom
|
|
724
|
-
this.ephemeralPrivate = ephemeralPrivate
|
|
725
1025
|
this.state = TLS_STATE_WAIT_CLIENT_HELLO
|
|
726
1026
|
this.alert = 0
|
|
727
1027
|
this.suite = 0
|
|
@@ -733,39 +1033,92 @@ export class TlsServer {
|
|
|
733
1033
|
this.inputLength = 0
|
|
734
1034
|
this.outputInitialLength = 0
|
|
735
1035
|
this.outputHandshakeLength = 0
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
this.
|
|
741
|
-
this.
|
|
742
|
-
this.
|
|
743
|
-
|
|
744
|
-
this.serverApplicationSecret = this.none
|
|
745
|
-
this.exporterSecret = this.none
|
|
746
|
-
this.toBeSigned = this.none
|
|
747
|
-
this.expectedClientFinished = this.none
|
|
748
|
-
if (
|
|
749
|
-
toI32(serverRandom.length) !== TLS_RANDOM_SIZE ||
|
|
750
|
-
toI32(ephemeralPrivate.length) !== X25519_SIZE ||
|
|
751
|
-
!this.configFits
|
|
752
|
-
) {
|
|
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) {
|
|
753
1044
|
this.state = TLS_STATE_FAILED
|
|
754
1045
|
this.alert = TLS_ALERT_INTERNAL_ERROR
|
|
755
1046
|
}
|
|
756
1047
|
}
|
|
757
1048
|
|
|
758
|
-
/**
|
|
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
|
+
*/
|
|
759
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)
|
|
760
1062
|
this.state = TLS_STATE_FAILED
|
|
761
1063
|
this.alert = alert
|
|
762
1064
|
return alert
|
|
763
1065
|
}
|
|
764
1066
|
|
|
765
|
-
/**
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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()
|
|
769
1122
|
}
|
|
770
1123
|
|
|
771
1124
|
/** The level the client's next bytes must arrive at, or -1 when the state expects none. */
|
|
@@ -1014,9 +1367,15 @@ export class TlsServer {
|
|
|
1014
1367
|
if (this.retried && suite !== this.suite) {
|
|
1015
1368
|
return TLS_ALERT_ILLEGAL_PARAMETER
|
|
1016
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
|
|
1017
1374
|
// §9.2: a certificate-authenticated handshake needs all three, and
|
|
1018
|
-
// `supported_groups` and `key_share` come as a pair.
|
|
1019
|
-
|
|
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) {
|
|
1020
1379
|
return TLS_ALERT_MISSING_EXTENSION
|
|
1021
1380
|
}
|
|
1022
1381
|
if (this.config.quic && !hello.hasQuicTransportParameters) {
|
|
@@ -1025,7 +1384,10 @@ export class TlsServer {
|
|
|
1025
1384
|
if (!tlsWindowHasU16(data, hello.groupsAt, hello.groupsLength, TLS_GROUP_X25519)) {
|
|
1026
1385
|
return TLS_ALERT_HANDSHAKE_FAILURE
|
|
1027
1386
|
}
|
|
1028
|
-
if (
|
|
1387
|
+
if (
|
|
1388
|
+
!offersTicket &&
|
|
1389
|
+
!tlsWindowHasU16(data, hello.schemesAt, hello.schemesLength, this.config.signatureScheme)
|
|
1390
|
+
) {
|
|
1029
1391
|
return TLS_ALERT_HANDSHAKE_FAILURE
|
|
1030
1392
|
}
|
|
1031
1393
|
if (!this.retried) {
|
|
@@ -1033,7 +1395,16 @@ export class TlsServer {
|
|
|
1033
1395
|
this.hashLength = tlsSuiteHashLength(suite)
|
|
1034
1396
|
this.transcript.reset(this.hashLength)
|
|
1035
1397
|
}
|
|
1036
|
-
|
|
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
|
+
}
|
|
1037
1408
|
if (!hello.hasX25519Share) {
|
|
1038
1409
|
// §4.1.4: one HelloRetryRequest; a second ClientHello without the share
|
|
1039
1410
|
// the first one asked for is `illegal_parameter`.
|
|
@@ -1077,6 +1448,24 @@ export class TlsServer {
|
|
|
1077
1448
|
return alpnAlert
|
|
1078
1449
|
}
|
|
1079
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
|
|
1080
1469
|
if (this.config.quic) {
|
|
1081
1470
|
// Refilled in place, so a slot's handshakes keep nothing for it (QUIC-3).
|
|
1082
1471
|
const parameters: u8[] = this.clientParameters
|
|
@@ -1092,60 +1481,343 @@ export class TlsServer {
|
|
|
1092
1481
|
}
|
|
1093
1482
|
this.clientTransportParameters = parameters
|
|
1094
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
|
+
}
|
|
1095
1513
|
const exchangeAlert: i32 = this.exchangeKeys()
|
|
1096
1514
|
if (exchangeAlert !== 0) {
|
|
1097
1515
|
return exchangeAlert
|
|
1098
1516
|
}
|
|
1099
|
-
this.
|
|
1517
|
+
if (this.resumed) {
|
|
1518
|
+
this.writeResumedFlight(index)
|
|
1519
|
+
} else {
|
|
1520
|
+
this.writeServerFlight()
|
|
1521
|
+
}
|
|
1100
1522
|
return 0
|
|
1101
1523
|
}
|
|
1102
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
|
+
|
|
1103
1778
|
/**
|
|
1104
1779
|
* Runs the x25519 exchange on the caller's ephemeral key and the client's
|
|
1105
1780
|
* share (`tlsExchange`), outside any arena block since its scope is its
|
|
1106
1781
|
* own, and unpacks the server's public key and the handshake secret from
|
|
1107
|
-
* the words
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
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).
|
|
1110
1788
|
*/
|
|
1111
1789
|
exchangeKeys(): i32 {
|
|
1112
1790
|
const w: TlsExchangeWords = this.exchange
|
|
1113
|
-
const key: u8[] = this.ephemeralPrivate
|
|
1114
1791
|
const share: u8[] = this.input
|
|
1115
1792
|
const at: i32 = this.hello.x25519ShareAt
|
|
1116
|
-
w.key0 =
|
|
1117
|
-
w.key1 =
|
|
1118
|
-
w.key2 =
|
|
1119
|
-
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)
|
|
1120
1797
|
w.share0 = tlsWordAt(share, at)
|
|
1121
1798
|
w.share1 = tlsWordAt(share, at + 8)
|
|
1122
1799
|
w.share2 = tlsWordAt(share, at + 16)
|
|
1123
1800
|
w.share3 = tlsWordAt(share, at + 24)
|
|
1124
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)
|
|
1125
1809
|
const alert: i32 = tlsExchange(w)
|
|
1126
|
-
if (alert
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
tlsWordInto(handshake, 8, w.secret1)
|
|
1138
|
-
tlsWordInto(handshake, 16, w.secret2)
|
|
1139
|
-
tlsWordInto(handshake, 24, w.secret3)
|
|
1140
|
-
tlsWordInto(handshake, 32, w.secret4)
|
|
1141
|
-
tlsWordInto(handshake, 40, w.secret5)
|
|
1142
|
-
w.secret0 = toU64(0)
|
|
1143
|
-
w.secret1 = toU64(0)
|
|
1144
|
-
w.secret2 = toU64(0)
|
|
1145
|
-
w.secret3 = toU64(0)
|
|
1146
|
-
w.secret4 = toU64(0)
|
|
1147
|
-
w.secret5 = toU64(0)
|
|
1148
|
-
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
|
|
1149
1821
|
}
|
|
1150
1822
|
|
|
1151
1823
|
/**
|
|
@@ -1185,10 +1857,8 @@ export class TlsServer {
|
|
|
1185
1857
|
TLS_LEVEL_HANDSHAKE,
|
|
1186
1858
|
tlsWriteCertificate(none, extensionsEnd, this.config.certificateChain)
|
|
1187
1859
|
)
|
|
1188
|
-
// The secrets'
|
|
1189
|
-
|
|
1190
|
-
this.serverHandshakeSecret = this.secretArray(TLS_SECRET_SERVER_HANDSHAKE)
|
|
1191
|
-
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()
|
|
1192
1862
|
using _scope = arena()
|
|
1193
1863
|
const helloAt: i32 = this.outputInitialLength
|
|
1194
1864
|
const helloEnd: i32 = tlsWriteServerHello(
|
|
@@ -1204,24 +1874,11 @@ export class TlsServer {
|
|
|
1204
1874
|
this.sent(TLS_LEVEL_INITIAL, helloAt, helloEnd)
|
|
1205
1875
|
|
|
1206
1876
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
this.hashes,
|
|
1213
|
-
TLS_FROM,
|
|
1214
|
-
this.clientHandshakeSecret
|
|
1215
|
-
)
|
|
1216
|
-
tlsDeriveSecretInto(
|
|
1217
|
-
this.kdf,
|
|
1218
|
-
h,
|
|
1219
|
-
this.handshakeSecret,
|
|
1220
|
-
"s hs traffic",
|
|
1221
|
-
this.hashes,
|
|
1222
|
-
TLS_FROM,
|
|
1223
|
-
this.serverHandshakeSecret
|
|
1224
|
-
)
|
|
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)
|
|
1225
1882
|
|
|
1226
1883
|
const extensionsAt: i32 = this.outputHandshakeLength
|
|
1227
1884
|
const written: i32 = tlsWriteEncryptedExtensions(
|
|
@@ -1241,10 +1898,23 @@ export class TlsServer {
|
|
|
1241
1898
|
this.sent(TLS_LEVEL_HANDSHAKE, written, certificateEnd)
|
|
1242
1899
|
|
|
1243
1900
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
1244
|
-
tlsWriteCertificateVerifyContent(
|
|
1901
|
+
tlsWriteCertificateVerifyContent(pool.toBeSigned, TLS_FROM, this.hashes, TLS_FROM, h)
|
|
1902
|
+
this.held = this.held | (1 << TLS_SECRET_TO_BE_SIGNED)
|
|
1245
1903
|
this.state = TLS_STATE_WAIT_SIGNATURE
|
|
1246
1904
|
}
|
|
1247
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
|
+
|
|
1248
1918
|
/**
|
|
1249
1919
|
* What CertificateVerify signs (RFC 8446 §4.4.3), while the state is
|
|
1250
1920
|
* `TLS_STATE_WAIT_SIGNATURE`; `null` at any other time.
|
|
@@ -1253,7 +1923,7 @@ export class TlsServer {
|
|
|
1253
1923
|
if (this.state !== TLS_STATE_WAIT_SIGNATURE) {
|
|
1254
1924
|
return null
|
|
1255
1925
|
}
|
|
1256
|
-
return this.toBeSigned
|
|
1926
|
+
return this.secrets().toBeSigned
|
|
1257
1927
|
}
|
|
1258
1928
|
|
|
1259
1929
|
/**
|
|
@@ -1274,11 +1944,17 @@ export class TlsServer {
|
|
|
1274
1944
|
}
|
|
1275
1945
|
const h: i32 = this.hashLength
|
|
1276
1946
|
this.reserveOutput(TLS_LEVEL_HANDSHAKE, this.outputHandshakeLength + 8 + signatureLength + 4 + h)
|
|
1277
|
-
// The secrets'
|
|
1278
|
-
|
|
1279
|
-
this.
|
|
1280
|
-
this.
|
|
1281
|
-
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 {
|
|
1282
1958
|
using _scope = arena()
|
|
1283
1959
|
const verifyAt: i32 = this.outputHandshakeLength
|
|
1284
1960
|
const verifyEnd: i32 = tlsWriteCertificateVerify(
|
|
@@ -1288,80 +1964,167 @@ export class TlsServer {
|
|
|
1288
1964
|
signature
|
|
1289
1965
|
)
|
|
1290
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
|
|
1291
1978
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
1292
|
-
|
|
1979
|
+
const serverHandshake: u8[] = expose(pool.serverHandshake, tlsHeldClone)
|
|
1293
1980
|
tlsFinishedVerifyDataInto(
|
|
1294
1981
|
this.kdf,
|
|
1295
1982
|
h,
|
|
1296
|
-
|
|
1983
|
+
serverHandshake,
|
|
1297
1984
|
this.hashes,
|
|
1298
1985
|
TLS_FROM,
|
|
1299
1986
|
this.outputHandshake,
|
|
1300
|
-
|
|
1301
|
-
)
|
|
1302
|
-
const finishedEnd: i32 = tlsWriteFinished(
|
|
1303
|
-
this.outputHandshake,
|
|
1304
|
-
verifyEnd,
|
|
1305
|
-
this.outputHandshake,
|
|
1306
|
-
verifyEnd + 4,
|
|
1307
|
-
h
|
|
1987
|
+
at + 4
|
|
1308
1988
|
)
|
|
1309
|
-
|
|
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
|
+
}
|
|
1310
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()
|
|
1311
2003
|
this.transcript.hashInto(this.hashes, TLS_FROM)
|
|
2004
|
+
const handshake: u8[] = expose(pool.handshake, tlsHeldClone)
|
|
1312
2005
|
const master: u8[] = new Array<u8>(h)
|
|
1313
|
-
tlsMasterSecretInto(this.kdf, h,
|
|
1314
|
-
|
|
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(
|
|
1315
2062
|
this.kdf,
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
this.hashes,
|
|
2063
|
+
this.hashLength,
|
|
2064
|
+
resumption,
|
|
2065
|
+
this.ticketNonce,
|
|
1320
2066
|
TLS_FROM,
|
|
1321
|
-
|
|
2067
|
+
1,
|
|
2068
|
+
this.issuing.psk
|
|
1322
2069
|
)
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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])
|
|
1331
2092
|
)
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
this.
|
|
2093
|
+
this.outputApplicationLength = tlsWriteNewSessionTicket(
|
|
2094
|
+
this.outputApplication,
|
|
2095
|
+
TLS_FROM,
|
|
2096
|
+
toU32(issuing.lifetime),
|
|
2097
|
+
ageAdd,
|
|
2098
|
+
this.ticketNonce,
|
|
1338
2099
|
TLS_FROM,
|
|
1339
|
-
|
|
1340
|
-
|
|
2100
|
+
1,
|
|
2101
|
+
this.ticketScratch,
|
|
2102
|
+
TLS_FROM,
|
|
2103
|
+
length,
|
|
2104
|
+
this.none
|
|
1341
2105
|
)
|
|
1342
|
-
|
|
1343
|
-
this.toBeSigned = this.none
|
|
1344
|
-
this.state = TLS_STATE_WAIT_FINISHED
|
|
1345
|
-
return 0
|
|
2106
|
+
return true
|
|
1346
2107
|
}
|
|
1347
2108
|
|
|
1348
2109
|
/**
|
|
1349
2110
|
* The client's Finished (§4.4.4), `input[0 .. 4 + length)`: its
|
|
1350
2111
|
* `verify_data` is HashLen bytes and must equal the HMAC the server
|
|
1351
2112
|
* computed, compared in constant time. A wrong length is `decode_error`, a
|
|
1352
|
-
* 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.
|
|
1353
2116
|
*/
|
|
1354
2117
|
handleFinished(length: i32): i32 {
|
|
1355
2118
|
if (length !== this.hashLength) {
|
|
1356
2119
|
return TLS_ALERT_DECODE_ERROR
|
|
1357
2120
|
}
|
|
1358
|
-
|
|
2121
|
+
const pool: TlsSecretPool = this.secrets()
|
|
2122
|
+
if (!timingSafeEqualAt(this.input, 4, pool.clientFinished, TLS_FROM, this.hashLength)) {
|
|
1359
2123
|
return TLS_ALERT_DECRYPT_ERROR
|
|
1360
2124
|
}
|
|
1361
|
-
this.transcript.update(this.input, TLS_FROM, 4 + length)
|
|
1362
2125
|
// The handshake secret has done its work: wiped, and no longer held.
|
|
1363
|
-
|
|
1364
|
-
this.
|
|
2126
|
+
pool.zero(TLS_SECRET_HANDSHAKE)
|
|
2127
|
+
this.held = (this.held | (1 << TLS_SECRET_RESUMPTION)) & ~(1 << TLS_SECRET_HANDSHAKE)
|
|
1365
2128
|
this.state = TLS_STATE_CONNECTED
|
|
1366
2129
|
return 0
|
|
1367
2130
|
}
|
|
@@ -1406,8 +2169,15 @@ export class TlsServer {
|
|
|
1406
2169
|
this.outputHandshakeLength = 0
|
|
1407
2170
|
return out
|
|
1408
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
|
+
}
|
|
1409
2180
|
default:
|
|
1410
|
-
// Nothing is written at the Application level before NewSessionTicket, which is out of scope.
|
|
1411
2181
|
return none
|
|
1412
2182
|
}
|
|
1413
2183
|
}
|
|
@@ -1418,20 +2188,8 @@ export class TlsServer {
|
|
|
1418
2188
|
this.outputInitialLength = 0
|
|
1419
2189
|
} else if (level === TLS_LEVEL_HANDSHAKE) {
|
|
1420
2190
|
this.outputHandshakeLength = 0
|
|
2191
|
+
} else if (level === TLS_LEVEL_APPLICATION) {
|
|
2192
|
+
this.outputApplicationLength = 0
|
|
1421
2193
|
}
|
|
1422
2194
|
}
|
|
1423
|
-
|
|
1424
|
-
/**
|
|
1425
|
-
* The secret the server *reads* `level` under — the client's traffic secret
|
|
1426
|
-
* — or `null` while it is not known yet. The Initial level has none: it is
|
|
1427
|
-
* cleartext in TLS, and QUIC derives its own from the connection id.
|
|
1428
|
-
*/
|
|
1429
|
-
readSecret(level: i32): u8[] | null {
|
|
1430
|
-
return tlsSecretAt(level, this.clientHandshakeSecret, this.clientApplicationSecret)
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
/** The secret the server *writes* `level` under — its own traffic secret — or `null` while it is not known yet. */
|
|
1434
|
-
writeSecret(level: i32): u8[] | null {
|
|
1435
|
-
return tlsSecretAt(level, this.serverHandshakeSecret, this.serverApplicationSecret)
|
|
1436
|
-
}
|
|
1437
2195
|
}
|