@amritk/nish-x86_64-linux 0.15.0 → 0.17.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 +58 -40
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +67 -10
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +72 -26
- package/runtime/runtime-net.c +72 -19
- package/runtime/runtime-os.c +127 -45
- package/runtime/runtime-parallel.c +5 -1
- package/runtime/runtime-wasm.c +26 -2
- package/runtime/runtime.c +86 -14
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +1 -3
- package/std/README.md +192 -36
- package/std/crypto/aes.ts +104 -21
- package/std/crypto/base64url.ts +43 -17
- package/std/crypto/chacha20poly1305.ts +52 -31
- package/std/crypto/ct.ts +14 -3
- package/std/crypto/hkdf.ts +385 -8
- package/std/crypto/hmac.ts +325 -9
- package/std/crypto/p256.ts +153 -27
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +15 -3
- package/std/crypto/sha512.ts +16 -2
- package/std/crypto/x25519.ts +90 -37
- package/std/crypto/x509.ts +265 -65
- package/std/net/hpack.ts +1094 -0
- package/std/net/http-fields.ts +604 -0
- package/std/net/http1-server.ts +1389 -0
- package/std/net/http1.ts +1492 -0
- package/std/net/http2-frame.ts +745 -0
- package/std/net/http2-tls.ts +216 -0
- package/std/net/http2.ts +1433 -0
- package/std/net/http3-frame.ts +457 -0
- package/std/net/http3-server.ts +905 -0
- package/std/net/http3.ts +2145 -0
- package/std/net/qpack.ts +1163 -0
- package/std/net/quic-conn-ack.ts +206 -0
- package/std/net/quic-conn-cid.ts +425 -0
- package/std/net/quic-conn-params.ts +392 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +653 -0
- package/std/net/quic-packet.ts +1474 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1603 -0
- package/std/net/quic.ts +3214 -0
- package/std/net/tls/codec.ts +1089 -0
- package/std/net/tls/record-server.ts +1173 -0
- package/std/net/tls/record.ts +621 -0
- package/std/net/tls/schedule.ts +429 -0
- package/std/net/tls-tcp.ts +410 -0
- package/std/net/tls.ts +1368 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
package/runtime/shim.mjs
CHANGED
|
@@ -123,6 +123,52 @@ export function ctEq(a, b) {
|
|
|
123
123
|
return (a ^ b) === 0 ? 0xffffffff : 0;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* `nish:secret` (docs/LANGUAGE.md, "Secrets"). Natively the compiler holds a
|
|
128
|
+
* `Secret` to its rules and `wipe` is a volatile store of zeros; here the rules
|
|
129
|
+
* have already been checked by the compiler or by nobody, so a `Secret` is a
|
|
130
|
+
* plain wrapper and `wipe` zero-fills what it holds. `runtime/nish.mjs`
|
|
131
|
+
* resolves the `nish:secret` specifier to this module, so these are its
|
|
132
|
+
* exports by name. A zero-fill under Node promises nothing about copies the
|
|
133
|
+
* engine made, which is the native wipe's whole point and is not claimed here.
|
|
134
|
+
*/
|
|
135
|
+
export class Secret {
|
|
136
|
+
constructor(value) {
|
|
137
|
+
this.value = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function secret(value) {
|
|
142
|
+
return new Secret(value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function expose(s, f) {
|
|
146
|
+
return f(s.value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function exposeWith(s, arg, f) {
|
|
150
|
+
return f(s.value, arg);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Zero an array of integers, or every integer field of a record: `0` for a number, `0n` for an `i64` or `u64`. */
|
|
154
|
+
function zeroFill(target) {
|
|
155
|
+
if (Array.isArray(target) || ArrayBuffer.isView(target)) {
|
|
156
|
+
for (let i = 0; i < target.length; i++) {
|
|
157
|
+
target[i] = typeof target[i] === "bigint" ? 0n : 0;
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const key of Object.keys(target)) {
|
|
162
|
+
const field = target[key];
|
|
163
|
+
if (typeof field === "bigint") target[key] = 0n;
|
|
164
|
+
else if (typeof field === "number") target[key] = 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function wipe(target) {
|
|
169
|
+
zeroFill(target instanceof Secret ? target.value : target);
|
|
170
|
+
}
|
|
171
|
+
|
|
126
172
|
/** Wrap a BigInt to the i64 range: every i64 `+ - * /` and unary minus goes through here. */
|
|
127
173
|
export function wrapI64(x) {
|
|
128
174
|
return BigInt.asIntN(64, x);
|
|
@@ -497,9 +543,22 @@ export function readFileBytesSync(path) {
|
|
|
497
543
|
}
|
|
498
544
|
}
|
|
499
545
|
|
|
546
|
+
/**
|
|
547
|
+
* The flags `runtime-os.c` opens a file it writes with, `O_NOFOLLOW` included
|
|
548
|
+
* (docs/security/runtime.md, RT-4): a symbolic link as the last component of
|
|
549
|
+
* the path is refused rather than followed, so `writeFileSync`,
|
|
550
|
+
* `appendFileSync` and `spawnSyncTo` fail here where they fail natively. Node's
|
|
551
|
+
* `"w"` and `"a"` follow the link. A link among the directories on the way is
|
|
552
|
+
* still followed, as it is natively. Windows has no `O_NOFOLLOW`, and there the
|
|
553
|
+
* flag is 0.
|
|
554
|
+
*/
|
|
555
|
+
const { O_WRONLY, O_CREAT, O_TRUNC, O_APPEND, O_NOFOLLOW = 0 } = fs.constants;
|
|
556
|
+
const WRITE_FLAGS = O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW;
|
|
557
|
+
const APPEND_FLAGS = O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW;
|
|
558
|
+
|
|
500
559
|
export function writeFileSync(path, data) {
|
|
501
560
|
try {
|
|
502
|
-
fs.writeFileSync(path, data, "utf8");
|
|
561
|
+
fs.writeFileSync(path, data, { encoding: "utf8", flag: WRITE_FLAGS });
|
|
503
562
|
} catch {
|
|
504
563
|
ioFail("write", path);
|
|
505
564
|
}
|
|
@@ -507,7 +566,7 @@ export function writeFileSync(path, data) {
|
|
|
507
566
|
|
|
508
567
|
export function appendFileSync(path, data) {
|
|
509
568
|
try {
|
|
510
|
-
fs.appendFileSync(path, data, "utf8");
|
|
569
|
+
fs.appendFileSync(path, data, { encoding: "utf8", flag: APPEND_FLAGS });
|
|
511
570
|
} catch {
|
|
512
571
|
ioFail("write", path);
|
|
513
572
|
}
|
|
@@ -623,8 +682,9 @@ export function getenv(name) {
|
|
|
623
682
|
*
|
|
624
683
|
* `out` and `err` are paths for the child's stdout and stderr, and an **empty**
|
|
625
684
|
* string leaves that stream inherited. Each file is created or truncated at
|
|
626
|
-
* 0644
|
|
627
|
-
*
|
|
685
|
+
* 0644 with `WRITE_FLAGS`, which is what the runtime's file actions ask for:
|
|
686
|
+
* `O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW`, so a symbolic link at the path
|
|
687
|
+
* is refused and the answer is -1, on both sides. The descriptors opened here are
|
|
628
688
|
* closed again whichever way the child went; natively the child opens them and
|
|
629
689
|
* its exit drops them.
|
|
630
690
|
*/
|
|
@@ -633,7 +693,7 @@ function spawnImpl(argv, out, err) {
|
|
|
633
693
|
const opened = [];
|
|
634
694
|
const stream = (target) => {
|
|
635
695
|
if (target.length === 0) return "inherit";
|
|
636
|
-
const fd = fs.openSync(target,
|
|
696
|
+
const fd = fs.openSync(target, WRITE_FLAGS, 0o644);
|
|
637
697
|
opened.push(fd);
|
|
638
698
|
return fd;
|
|
639
699
|
};
|
|
@@ -696,6 +756,51 @@ export function statMtimeSync(path) {
|
|
|
696
756
|
return st.mtimeMs;
|
|
697
757
|
}
|
|
698
758
|
|
|
759
|
+
/**
|
|
760
|
+
* The RT-9 owner checks (#386): `lstatOwnerModeSync(path)` is the owner's uid
|
|
761
|
+
* in the high 32 bits and `st_mode` in the low 32 of one `lstat`, so a
|
|
762
|
+
* symbolic link answers for itself, or -1 when the path does not resolve or
|
|
763
|
+
* holds a NUL; `geteuid()` is the effective uid; `isExecutableSync(path)` is
|
|
764
|
+
* `access(path, X_OK)`. Each answers what `runtime-host.c` answers, the two
|
|
765
|
+
* `i64`s as BigInts because that is how an `i64` is held on this side. Node's
|
|
766
|
+
* `fs` throws on a NUL in a path where the runtime names nothing, so the
|
|
767
|
+
* `catch` is what makes the two agree there too.
|
|
768
|
+
*/
|
|
769
|
+
export function lstatOwnerModeSync(path) {
|
|
770
|
+
try {
|
|
771
|
+
const st = fs.lstatSync(path);
|
|
772
|
+
return BigInt.asIntN(64, (BigInt(st.uid >>> 0) << 32n) | BigInt(st.mode >>> 0));
|
|
773
|
+
} catch {
|
|
774
|
+
return -1n;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export function geteuid() {
|
|
779
|
+
return BigInt(process.geteuid());
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export function isExecutableSync(path) {
|
|
783
|
+
try {
|
|
784
|
+
fs.accessSync(path, fs.constants.X_OK);
|
|
785
|
+
return true;
|
|
786
|
+
} catch {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* `secureZero(bytes)` (#385): every element set to zero, which is what the
|
|
793
|
+
* native `nish_wipe` leaves. Nothing here can drop a store to an array the
|
|
794
|
+
* caller still holds, so a loop is the whole of it. What it cannot promise is
|
|
795
|
+
* what the native build does: a collector that moved the array may have left a
|
|
796
|
+
* copy of the old bytes behind, and JavaScript has no way to reach it.
|
|
797
|
+
*/
|
|
798
|
+
export function secureZero(bytes) {
|
|
799
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
800
|
+
bytes[i] = 0;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
699
804
|
/**
|
|
700
805
|
* Node's own fill, taken before `runtime/nish.mjs` puts the one below in its
|
|
701
806
|
* place on the same object: `webcrypto` is the global `crypto`.
|
|
@@ -807,3 +912,7 @@ export function arenaUsed() { return 0; }
|
|
|
807
912
|
export function arenaMark() { return 0; }
|
|
808
913
|
export function arenaRelease() {}
|
|
809
914
|
export function arenaReset() {}
|
|
915
|
+
/** `using a = arena()`: a disposable whose disposal does nothing, because there is no arena to release. */
|
|
916
|
+
export function arena() {
|
|
917
|
+
return { [Symbol.dispose]() {} };
|
|
918
|
+
}
|
package/scripts/build.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Build an Nish .ll module (plus optional C sources) into a native binary.
|
|
3
3
|
#
|
|
4
|
-
# scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm]
|
|
4
|
+
# scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm|wasi|napi]
|
|
5
5
|
#
|
|
6
6
|
# The C runtime is five translation units and is named as one: an input
|
|
7
7
|
# <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
|
|
@@ -20,8 +20,6 @@
|
|
|
20
20
|
# load it from Node. Add runtime/runtime-wasm.c to the inputs when a
|
|
21
21
|
# function uses arrays (the arena and the array cold paths, no libc);
|
|
22
22
|
# strings and I/O still need a WASI runtime and are not available.
|
|
23
|
-
# wasm wasm32 freestanding module exporting every non-internal function
|
|
24
|
-
# (for modules that do not use the C runtime); load it from Node.
|
|
25
23
|
# wasi wasm32-wasi command module: runtime.c linked against wasi-libc, so
|
|
26
24
|
# string programs run under any WASI host (`_start` runs `main`;
|
|
27
25
|
# `node examples/wasi-host.mjs app.wasm args...`). Needs a WASI
|
package/std/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# `std/` — the standard library
|
|
2
2
|
|
|
3
3
|
Nish modules written in Nish, for Nish programs to import. There is no magic
|
|
4
|
-
here and — with
|
|
5
|
-
nothing the compiler knows about: a module in this directory is an ordinary Nish source file, compiled as part of
|
|
4
|
+
here and — with four exceptions, `threads.ts`, `collections.ts`, `map.ts` and
|
|
5
|
+
`secret.ts` — nothing the compiler knows about: a module in this directory is an ordinary Nish source file, compiled as part of
|
|
6
6
|
whatever program imports it, and subject to the same rules as `examples/` or
|
|
7
7
|
`src/` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
|
|
8
8
|
|
|
@@ -14,6 +14,7 @@ whatever program imports it, and subject to the same rules as `examples/` or
|
|
|
14
14
|
| [`pair.ts`](./pair.ts) | `Pair<A, B>`: an interface with `first` and `second`, for a function that answers two values from one call. A type and nothing else — the caller writes an object literal at the return — and for returning two values rather than storing them side by side |
|
|
15
15
|
| [`collections.ts`](./collections.ts) | the global `Map<K, V>` and `Set<T>`: insertion-ordered tables whose buckets carry a hash fingerprint beside the entry index and whose entries keep their full hash, so every `get`, `set`, `add`, `has` and `delete` is one probe. `get` is not a method here: its `V | undefined` never crosses a call, so the compiler lowers it to `probe` and, where the key was found, `valueAt`. Nor are `keys()` and `values()`: an iterator is not a value, so a `for...of` over one is lowered to `walkOpen`, `walkNext`, `keyAt` or `valueAt`, and `walkClose`, and a count of live walks defers compaction until no loop is walking the table. A program never imports it: naming `Map` or `Set` loads it, and the compiler emits what a module uses of it into that module ([`docs/wp32-map.md`](../docs/wp32-map.md), [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)). Its `hashKey`, `sameKey` and `storedKey` are lowered by the compiler per key type |
|
|
16
16
|
| [`map.ts`](./map.ts) | `reserve(m, n)` and `getOrInsert(m, k, v)` for the global `Map`. Their bodies are the meaning, and what runs under Node: `reserve` does nothing, and `getOrInsert` is a `get`, and a `set` of `v` when the key was missing. Natively the compiler lowers every call in place — `reserve` to the table's `reserveSlots`, which grows the buckets once so that `n` entries fit without a rebuild, and `getOrInsert` to one `probe` and a `valueAt` or an `insertAt` through its answer — so, like `collections.ts`, it writes no `.ll` of its own ([`docs/wp32-map.md`](../docs/wp32-map.md) §9.2, [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)) |
|
|
17
|
+
| [`secret.ts`](./secret.ts) | The source behind the builtin module `nish:secret`, and imported only by that name: `Secret<T>`, `secret`, `expose`, `exposeWith` and `wipe`. The bodies are the meaning; what makes a `Secret` one is the checker's, keyed on this file ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#secrets-nishsecret)). `wipe`'s body is written by the emitter, one volatile `llvm.memset`, and like `collections.ts` the module writes no `.ll` of its own |
|
|
17
18
|
| [`threads.ts`](./threads.ts) | `parallelMapInto(src, dst, f)` and `parallelReduce(src, f, identity)`: a function over every element of an array, on as many threads as the length is worth. Its bodies are the sequential meaning, which is what runs under Node; the compiler recognises the two templates by module and name, lowers the one loop in each onto `nish_parallel_range`, holds the function to the rules that make that safe, and compiles an importing program with `--threads` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#data-parallelism-nishthreads)). `tests/link/par_*` are its programs |
|
|
18
19
|
|
|
19
20
|
## `nish/crypto` — the primitives under TLS 1.3
|
|
@@ -30,21 +31,38 @@ specification's published vectors in its `tests/link/crypto_*` programs. The
|
|
|
30
31
|
performance gate compiles every module with no diagnostics under both
|
|
31
32
|
`--number-mode i32` and `f64`, and the hashes, HMAC, HKDF, X25519,
|
|
32
33
|
ChaCha20-Poly1305, AES-GCM, P-256 and X.509 also run their vectors in `f64`
|
|
33
|
-
(`crypto_*_f64`).
|
|
34
|
+
(`crypto_*_f64`). SHA-1 is here too, for one non-security use (its row says
|
|
35
|
+
which), and does both.
|
|
36
|
+
|
|
37
|
+
Every module here was security-audited (#363); the records are in
|
|
38
|
+
[`docs/security/`](../docs/security/README.md), and how to report a
|
|
39
|
+
vulnerability is in [`SECURITY.md`](../SECURITY.md). Two limits hold across the
|
|
40
|
+
modules. **Lengths stop at 2^31 − 1.** No array built by `push` or `new Array`,
|
|
41
|
+
no array or string the runtime makes (a file read, a concatenation, a
|
|
42
|
+
template) and no string built by `join` can pass 2^31 − 1 elements or bytes
|
|
43
|
+
(K1-6, CG-3, closed), so `length` is right in both number modes, and the
|
|
44
|
+
one-shot functions below refuse a longer input as each row says. **Private
|
|
45
|
+
keys are `Secret`s.** `crypto/p256.ts`, `crypto/x25519.ts` and
|
|
46
|
+
`crypto/x509.ts` take and give a private key as a `Secret<u8[]>` from
|
|
47
|
+
`nish:secret`, compute on it only inside `expose`, and wipe every intermediate
|
|
48
|
+
the key reaches before they return, with a store the optimiser may not remove
|
|
49
|
+
(ECC-2, X509-7, closed). The other modules
|
|
50
|
+
take their keys as plain arrays and do not wipe them yet.
|
|
34
51
|
|
|
35
52
|
| Module | What it is | Reproduces |
|
|
36
53
|
| --- | --- | --- |
|
|
37
|
-
| [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK` | FIPS 180-4 §6.2 |
|
|
38
|
-
| [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK` | FIPS 180-4 §6.4, §6.5 |
|
|
39
|
-
| [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual` | RFC 2104, RFC 4231 §4 |
|
|
40
|
-
| [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen. TLS 1.3's HKDF-Expand-Label
|
|
41
|
-
| [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public | — |
|
|
42
|
-
| [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null` | RFC 4648 §5, §10 |
|
|
43
|
-
| [`crypto/
|
|
44
|
-
| [`crypto/
|
|
45
|
-
| [`crypto/
|
|
46
|
-
| [`crypto/
|
|
47
|
-
| [`crypto/
|
|
54
|
+
| [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK`. `sha256` panics on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.2 |
|
|
55
|
+
| [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK`. The one-shot `sha512` and `sha384` panic on an array longer than 2^31 − 1 bytes | FIPS 180-4 §6.4, §6.5 |
|
|
56
|
+
| [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual`. The one-shot functions panic on a message longer than 2^31 − 1 bytes. `HmacSha256Scratch` / `HmacSha384Scratch` are the same tag over hashers the caller allocates once — `begin(key, off, len)`, `update`, `finishInto(out, at)`, `wipe` — so a loop of them inside `using a = arena()` leaves the arena where it was; `hmacCopySha256` / `hmacCopySha384` copy a hasher's state into one the caller holds | RFC 2104, RFC 4231 §4 |
|
|
57
|
+
| [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen, for an `info` longer than 2^31 − 1 bytes, and for a PRK shorter than HashLen. `hkdfExpandLabelSha256` / `hkdfExpandLabelSha384` are TLS 1.3's HKDF-Expand-Label, which QUIC uses too; the label is given without its `"tls13 "` prefix, and a length outside 0 to 255, a label outside 1 to 249 bytes, a context past 255 bytes or a secret shorter than HashLen panics, since each is the program's choice rather than the peer's. `hkdfExtractInto`, `hkdfExpandInto` (`false` where the others answer `null`) and `hkdfExpandLabelInto` compute the same bytes in an `HkdfScratch` the caller owns, over the hash a length of 32 or 48 names, into the caller's array, and wipe the scratch before they return | RFC 5869 §2, Appendix A; RFC 8448 §3 and RFC 9001 A.1 for the label |
|
|
58
|
+
| [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public, and so does an array longer than 2^31 − 1 bytes | — |
|
|
59
|
+
| [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null`, as does a text longer than 2^31 − 1 bytes; `base64urlEncode` panics on such an array | RFC 4648 §5, §10 |
|
|
60
|
+
| [`crypto/sha1.ts`](./crypto/sha1.ts) | `sha1(data)`, a fresh 20-byte digest, and `SHA1_SIZE` and `SHA1_BLOCK`. **Not for security**: SHA-1 is broken for collisions, and it is here only because RFC 6455 §4.2.2's `Sec-WebSocket-Accept` is defined on it (`nish/net/websocket` is its importer). One-shot only; it panics on an array longer than 2^31 − 1 bytes. It came after the #363 audit and has no record in `docs/security/` | FIPS 180-4 §6.1 |
|
|
61
|
+
| [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)`, the shared secret as a `Secret<u8[]>`, and `x25519Base(scalar)`, the public key, each from a `Secret<u8[]>` scalar, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy, which is wiped | RFC 7748 §5.2, §6.1, Wycheproof `x25519_test` |
|
|
62
|
+
| [`crypto/chacha20poly1305.ts`](./crypto/chacha20poly1305.ts) | `chacha20Poly1305Seal(key, nonce, aad, plaintext)`, which answers the ciphertext followed by the 16-byte tag, and `chacha20Poly1305Open`, which checks the whole tag before it decrypts anything and answers `null` for a message that does not authenticate. Beneath them `chacha20Block`, `chacha20`, `chacha20QuarterRound`, `poly1305` and `poly1305KeyGen`, and `chacha20HeaderMask`, QUIC's 5-byte header-protection mask. Every input of the wrong length answers `null` | RFC 8439 §2, RFC 9001 §5.4.4, Wycheproof `chacha20_poly1305` (all 325 cases) |
|
|
63
|
+
| [`crypto/aes.ts`](./crypto/aes.ts) | AES-128 and AES-256, bitsliced four blocks at a time with the Boyar–Peralta S-box circuit, so no table is read: `aesKey(key)` expands a 16- or 32-byte key into an `AesKey` (or `aesKeyInto(key, out)` into one the caller holds, allocating nothing that outlives the call), then `aesEncryptBlock`, `aesGcmSeal` / `aesGcmOpen` (tag last, checked in full before anything is decrypted; any non-empty IV) and `aesHeaderMask`. AES-192 is out of scope, and a wrong length answers `null`, as does an `AesKey` not made by `aesKey`; one whose H is zero opens nothing | FIPS 197, SP 800-38D, RFC 9001 §5.4.3, Wycheproof `aes_gcm` (all 316 cases; the 103 AES-192 ones check that the key is refused) |
|
|
64
|
+
| [`crypto/p256.ts`](./crypto/p256.ts) | ECDSA over P-256 with RFC 6979's deterministic nonces: `p256PublicKey(priv)` (65-byte uncompressed SEC1) from a `Secret<u8[]>` private key, `p256Sign` / `p256Verify` over a digest and `p256SignSha256` / `p256VerifySha256` over a message; a signature is `r` and `s`, 32 big-endian bytes each, not DER (`crypto/x509.ts`'s `x509DerSignatureRS` converts one). A malformed key, a point off the curve or an `r` or `s` out of range answers `null` or `false`, never a panic. ECDSA is malleable, and `p256Verify` accepts a high `s`: when `(r, s)` verifies, so does `(r, n − s)` | RFC 6979 A.2.5, Wycheproof `ecdsa_secp256r1_sha256`, `_sha256_p1363`, `_sha512` and `_sha512_p1363` |
|
|
65
|
+
| [`crypto/x509.ts`](./crypto/x509.ts) | DER, PEM and X.509 over P-256: `pemToDer` / `derToPem` (RFC 7468, padded standard base64, strict labels), `x509ParseP256PrivateKey` (SEC1 or PKCS#8, answering a `Secret<u8[]>`), `x509ParseCertificate` / `x509ParseChain` into an `X509Certificate`, `x509VerifySignature` (ecdsa-with-SHA256 under an issuer's key), `x509DerSignatureRS` (a DER ECDSA-Sig-Value as the `r || s` `crypto/p256.ts` takes), `x509MintSelfSigned`, the 1-to-14-day self-signed P-256 certificate WebTransport's `serverCertificateHashes` accepts, and `x509CertificateHash`, its SHA-256. DER is read strictly — non-minimal lengths and integers, indefinite lengths and trailing bytes answer `null`. `x509ParseCertificate` and `x509ParseChain` parse and do not validate: there is no path validation, no extension is read or exposed, and no clock is consulted. `pemToDer` refuses a file in which any block, of any label, is malformed. `x509MintSelfSigned` refuses a common name that is not UTF-8 or holds a NUL, and its key and serial must come from `crypto.getRandomValues`, which the module cannot call itself without refusing wasm32 (X509-6) | X.690, RFC 5280, RFC 7468, RFC 5915, RFC 5208 / 5958; certificates checked against OpenSSL |
|
|
48
66
|
|
|
49
67
|
Three rules hold across the modules:
|
|
50
68
|
|
|
@@ -55,8 +73,10 @@ Three rules hold across the modules:
|
|
|
55
73
|
that is itself spent, so any `update` or `digest` on it panics. Either way,
|
|
56
74
|
copy *before* `digest` when the computation has to go on.
|
|
57
75
|
- **An all-zero X25519 result is returned, not refused.** It is what a
|
|
58
|
-
low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol
|
|
59
|
-
|
|
76
|
+
low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol.
|
|
77
|
+
`nish/net/tls` makes that check (below); every other caller doing a key
|
|
78
|
+
exchange must refuse an all-zero answer itself, with `timingSafeEqual`
|
|
79
|
+
against 32 zero bytes, since the answer is a secret.
|
|
60
80
|
- **Constant time by construction, and verified where the check reaches.** No
|
|
61
81
|
module branches on, or indexes by, a secret: comparisons OR the differences
|
|
62
82
|
into one word and test it once, the ladder swaps with a mask and always runs
|
|
@@ -65,13 +85,22 @@ Three rules hold across the modules:
|
|
|
65
85
|
rather than a table. Every branch is on a length, a loop counter or a bit
|
|
66
86
|
position. WP34 N6 checks that the machine code kept that shape:
|
|
67
87
|
`tests/ct-asm.js` disassembles golden fixtures on x86-64 and aarch64 and
|
|
68
|
-
refuses a branch, a call
|
|
69
|
-
|
|
88
|
+
refuses a branch, a call, a secret-addressed load, or a divide or square
|
|
89
|
+
root of a secret (whose latency follows its operands) in them. It reads what
|
|
90
|
+
`clang -O2` emits for each target's baseline CPU, and only the functions a
|
|
91
|
+
fixture names: the fixtures are copies of each module's straight-line cores,
|
|
92
|
+
so the module functions themselves, and every loop, rest on review.
|
|
93
|
+
[`docs/security/ct-verification.md`](../docs/security/ct-verification.md)
|
|
94
|
+
lists every secret-handling function no fixture reads. What it holds, per
|
|
95
|
+
module, is what that module's header says:
|
|
70
96
|
- `tests/cases/ct_asm_mac` pins the two loop shapes the others are built
|
|
71
|
-
from, a full tag compare and a masked table read
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
from, a full tag compare and a masked table read; `crypto/hmac.ts`'s
|
|
98
|
+
verifiers compare in the first shape.
|
|
99
|
+
- `tests/cases/ct_asm_k1_ct`: copies of `timingSafeEqual`'s loop at 32 and
|
|
100
|
+
48 bytes and `timingSafeEqualAt`'s at a 16-byte window.
|
|
101
|
+
- `tests/cases/ct_asm_k1_base64url`: the two character maps verbatim, and
|
|
102
|
+
one encode group and one decode group. The length-driven loops, the
|
|
103
|
+
decoder's store guard and the encoder's string building remain discipline.
|
|
75
104
|
- `tests/cases/ct_asm_x25519`: X25519's field multiply, square, add,
|
|
76
105
|
subtract and multiply by a24, its conditional swap, and one step of the
|
|
77
106
|
Montgomery ladder, the check following each call into the field functions.
|
|
@@ -96,6 +125,134 @@ Three rules hold across the modules:
|
|
|
96
125
|
whose one secret, a private key, is base64-decoded by range masks and
|
|
97
126
|
otherwise only copied and handed to `crypto/p256.ts`.
|
|
98
127
|
|
|
128
|
+
## `nish/net` — the protocol stack
|
|
129
|
+
|
|
130
|
+
The protocol lanes of [WP34](../docs/wp34-hosting-cs.md) §5, written on
|
|
131
|
+
`nish/crypto` (decision S2). Each module takes bytes and answers bytes:
|
|
132
|
+
randomness is an argument and nothing reads a socket or a clock, so a test
|
|
133
|
+
injects an RFC trace's values and replays it byte for byte. The carriers put
|
|
134
|
+
them on sockets: `net/tls-tcp` is the first, and the one module here that
|
|
135
|
+
reads one, and it still takes its randomness as an argument.
|
|
136
|
+
|
|
137
|
+
| Module | What it is | Reproduces |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| [`net/tls.ts`](./net/tls.ts) | The server side of a TLS 1.3 handshake, ClientHello through the client's Finished. `new TlsServer(config, serverRandom, x25519Private)`, then `receive(level, buf, off, len)` with what the client sent, `takeOutput(level)` for what to send, and `signatureInput()` / `sign(signature)` for the CertificateVerify hand-off (`tlsSignEcdsaP256` signs for a P-256 key, DER-encoded). A `TlsServerConfig` names the certificate chain, the signature scheme, the ALPN protocols, whether the carrier is QUIC, the server's transport parameters, and extensions to pass through in EncryptedExtensions. The suites are `TLS_AES_128_GCM_SHA256`, `TLS_CHACHA20_POLY1305_SHA256` and `TLS_AES_256_GCM_SHA384` (in the client's order), the group x25519 (one HelloRetryRequest when the client offers it without a share), and `server_name`, ALPN and `quic_transport_parameters` are read; PSKs, 0-RTT, NewSessionTicket and client authentication are not. Every refusal is the alert to send — `receive` and `sign` answer 0 or a `TLS_ALERT_*` — never a panic, and a low-order x25519 share is refused. `restart(serverRandom, x25519Private)` hands a server's buffers, made once, to the slot's next connection, so a handshake allocates nothing that outlives it under any suite (TLS-3) | RFC 8446 §4, §7; RFC 8448 §3 byte for byte, and §5's transcript |
|
|
140
|
+
| [`net/tls/codec.ts`](./net/tls/codec.ts) | The handshake messages as bytes: `tlsReadClientHello` into a reusable `TlsClientHelloView` of windows into the message, and `tlsParseClientHello` into a `TlsClientHello` of copies (structure checked, an alert on a malformed or duplicated field); each message's `tlsWrite*` writer into the caller's array at an offset, and `tlsEncodeServerHello`, `tlsEncodeHelloRetryRequest`, `tlsEncodeEncryptedExtensions`, `tlsEncodeCertificate`, `tlsEncodeCertificateVerify`, `tlsEncodeFinished` and `tlsCertificateVerifyContent`. The wire's constants: handshake and extension types, versions, the x25519 group, the two signature schemes, and the `TLS_ALERT_*` descriptions | RFC 8446 §4, RFC 6066 §3, RFC 7301 §3.1, RFC 9001 §8.2 |
|
|
141
|
+
| [`net/tls/schedule.ts`](./net/tls/schedule.ts) | The key schedule over SHA-256 or SHA-384: `tlsEarlySecret`, `tlsHandshakeSecret`, `tlsMasterSecret`, `tlsDeriveSecret`, `tlsExpandLabel`, `tlsFinishedVerifyData`, and `tlsTrafficKey` / `tlsTrafficIv` for a record layer, with in-place forms over an `HkdfScratch` (`tlsDeriveSecretInto`, `tlsMasterSecretInto`, `tlsFinishedVerifyDataInto`, `tlsTrafficKeysInto`). `TlsTranscript` is the running transcript hash, with HelloRetryRequest's `message_hash` rule, `reset` for the next connection and `hashInto` the caller's array. The suite constants and `tlsSuiteHashLength` / `tlsSuiteKeyLength` | RFC 8446 §4.4.1, §7.1, §7.3 |
|
|
142
|
+
| [`net/tls/record.ts`](./net/tls/record.ts) | The record layer (RFC 8446 §5): `TlsRecordReader` cuts a stream into records in a buffer allocated once, refusing a header of unknown type or a body past 2^14 + 256 bytes before the body is waited for; `TlsRecordProtection` is one direction, cleartext until `install(suite, secret)`, then `seal` and `open` a record into the caller's buffer — the nonce from the sequence number, the header as additional data, the inner content type and zero padding found without branching on the padding. `tlsNextTrafficSecret` is KeyUpdate's next secret. Every refusal is the alert, negated | RFC 8448 §3's nine records byte for byte; ChaCha20-Poly1305, AES-256-GCM-SHA384 and padding against Python's `cryptography` |
|
|
143
|
+
| [`net/tls/record-server.ts`](./net/tls/record-server.ts) | `TlsRecordServer`: a TLS 1.3 server over a byte stream, sans-IO, wrapping a `TlsServer` in records. `receive` the client's bytes, send `output[outputStart .. outputEnd)` and `consume` what was sent, `read` and `write` application data, `sign` when asked, `keyUpdate`, `close`; `interest()` answers what it wants next as `TLS_RECORD_*` bits. It adds the compatibility `change_cipher_spec`, alerts both ways, KeyUpdate both ways and on a schedule, a limit of `TLS_RECORD_IDLE_LIMIT` (16) records in a row that carry nothing, a cap of `TLS_RECORD_MAX_KEY_UPDATES` (64) KeyUpdates from the client, and fixed buffers per connection; the handshake is sealed straight out of `TlsServer`'s buffers | RFC 8448 §3 replayed at every cut of the client's stream |
|
|
144
|
+
| [`net/tls-tcp.ts`](./net/tls-tcp.ts) | `TlsTcpServer`: TLS 1.3 over `nish:net` TCP for a program that owns its loop — a pool of `TlsRecordServer` slots sized at start-up, `accept(serverRandom, ephemeralPrivate)` into a free one, `readable` / `writable` when the loop says so, `signP256` with a `Secret` key, and `read`, `write`, `shutdown` and `close` answering as `nish:net`'s calls do. `accept` copies the server random and the ephemeral key into the slot, wipes the caller's key array, and refuses either with -22 when it is not 32 bytes. Every call answers the slot's interest, whose low two bits are `pollModify`'s events. Each slot keeps one `TlsServer`, restarted by `accept`, so a thousand connections through the pool leave the arena where the first did, under any suite (TLS-3). Native only, as `nish:net` is | openssl s_client under all three suites and curl, from `tests/run.js`; RFC 8448 §3 over loopback from a Nish client |
|
|
145
|
+
| [`net/quic-packet.ts`](./net/quic-packet.ts) | QUIC version 1's packets (WP34 Q1). The variable-length integer (`quicVarintPush`, `quicVarintPushSized`, `quicVarintRead`, `quicVarintLength`, `quicVarintSize`); packet-number length choice and recovery (`quicPacketNumberLength`, `quicPacketNumberDecode`); the long and short headers (`quicLongHeader`, `quicShortHeader`, and `quicParseHeader` into a `QuicHeader`, whose `end` is where the next coalesced packet starts); the Initial secrets from the client's first DCID (`quicInitialSecrets`); the packet keys from a traffic secret (`quicKeys` into a `QuicKeys`, for AES-128-GCM, AES-256-GCM and ChaCha20-Poly1305) and a key update, its secret (`quicKeyUpdateSecret`) and the next generation's keys with the header-protection key kept (`quicKeysUpdate`); packet and header protection (`quicSealPacket`, and `quicOpenPacket` or its two halves `quicRemoveHeaderProtection` and `quicDecryptPacket` into a `QuicPacket`); and, for a connection that allocates nothing per packet (WP34 Q4), `quicParseHeaderInto` (windows, no copy), `quicPutLongHeader` / `quicPutShortHeader` / `quicVarintPut` in place, `quicSealInPlace`, and `quicUnprotectHeader` / `quicDecryptPayload`, which store only numbers into the `QuicPacket`; and the Retry integrity tag (`quicRetryIntegrityTag`, `quicRetryPacket`, `quicRetryVerify`). Every parse is bounds-checked against the datagram and answers a `QUIC_ERR_*` code rather than panicking: a truncated header, a clear fixed bit, another version (with its connection IDs read, for Version Negotiation), a connection ID over 20 bytes, a Length past the datagram, a packet too short to sample, a payload that does not authenticate, and reserved bits set in one that does; keys `quicKeys` did not make are `QUIC_ERR_KEYS`, a caller's error rather than a forgery. Long headers always carry a two-byte Length, so a header's size does not move with its payload, and `quicSealPacket` refuses a header whose Length or packet number disagrees with what it seals. The builders answer `null` for an argument out of range. Its Handshake and 1-RTT keys are not wiped yet ([QUIC-1](../docs/security/quic.md)) | RFC 9000 §16, §17 and Appendix A.1–A.3; RFC 9001 §5 and Appendix A.1–A.5 (client and server Initial, Retry, ChaCha20 short header) |
|
|
146
|
+
| [`net/quic-frame.ts`](./net/quic-frame.ts) | QUIC version 1's frames (WP34 Q2). `quicParseFrame` reads any frame of RFC 9000 §19, 0x00 to 0x1e, into a reused `QuicFrame`, leaving a CRYPTO or STREAM frame's data in the packet (`dataStart`, `dataLength`) and answering 0 or the transport error the RFC names: FRAME_ENCODING_ERROR for a truncated field, a length past the packet, an ACK range below zero, an offset past 2^62 − 1, a stream count past 2^60, an empty NEW_TOKEN or a bad NEW_CONNECTION_ID, and PROTOCOL_VIOLATION for a frame type spelled in more bytes than it needs. The writers (`quicPushAck`, `quicPushCrypto`, `quicPushStream`, `quicPushValue`, `quicPushStreamValue`, `quicPushStreamError`, `quicPushNewConnectionId`, `quicPushPathData`, `quicPushConnectionClose`, `quicPushNewToken`, `quicPushTypeOnly`, `quicPushPadding`) answer `false` rather than write a frame that would not parse; `quicCryptoOverhead` and `quicStreamOverhead` size a frame's header before it is written. `quicFrameAllowed` is §12.4's table of which frames each packet type carries, `quicFrameAckEliciting` §13.2's rule, and `QUIC_ERROR_*` the transport error codes of §20.1. RFC 9221's DATAGRAM frames (0x30, 0x31) are read too (WP34 Q4); every writer has a `put` form (`quicPutAck`, `quicPutStream`, …, `quicPutDatagram`) that writes in place into a caller's buffer before an end and answers the offset past it, which the `push` form calls, and NEW_CONNECTION_ID's ID and token are left in the packet as windows | RFC 9000 §12.4, §19, §20; RFC 9221 §4; RFC 9001 A.2's and A.3's frames |
|
|
147
|
+
| [`net/quic-conn-params.ts`](./net/quic-conn-params.ts) | Transport parameters (RFC 9000 §18): a `QuicTransportParameters` that starts at every default, `quicEncodeTransportParameters` (a parameter only when it is not its default) and `quicParseTransportParameters`, which refuses with TRANSPORT_PARAMETER_ERROR, naming the parameter, a parameter that does not frame, a known one sent twice, a varint-valued one that is not exactly one varint, a server-only one from a client, and every value §18.2 bounds. Unknown parameters are skipped. RFC 9221's `max_datagram_frame_size` is read and written too (WP34 Q4) | RFC 9000 §7.4, §18; RFC 9221 §3; RFC 9001 A.2's client parameters |
|
|
148
|
+
| [`net/quic-conn-ack.ts`](./net/quic-conn-ack.ts) | `QuicAckRanges`: the packet numbers one space received, as at most 32 disjoint ranges, for duplicate detection (`record`, `contains`) and for the ACK frame written from them (`pushAck`, or `putAck` in place). Past 32 ranges the lowest is dropped and a floor raised over it, below which every packet number reads as already received | RFC 9000 §12.3, §13.2, §19.3 |
|
|
149
|
+
| [`net/quic-conn-cid.ts`](./net/quic-conn-cid.ts) | `QuicCidTable`: the connection IDs a connection issued (routed by `ownsLocal`, retired by the peer with `retireLocal`) and the ones its peer issued (`addPeer`, with Retire Prior To retiring older ones into `retirePending`, and `currentPeer` the one sent to). A reused sequence number, an ID under two sequence numbers and retiring the ID a packet was sent to are PROTOCOL_VIOLATION; more active IDs than advertised, or more owed retirements than twice that, CONNECTION_ID_LIMIT_ERROR. Its slots are fixed when it is made (WP34 Q4): an ID is copied into one, `addPeerAt` / `ownsLocalAt` / `retireLocal` read it from the packet in place, and `reset` empties the table for the next connection | RFC 9000 §5.1, §19.15, §19.16 |
|
|
150
|
+
| [`net/quic.ts`](./net/quic.ts) | The server side of a QUIC connection (WP34 Q2): `new QuicConnection(config, entropy)`, `receive(datagram, now)`, `signatureInput()` / `sign(signature)`, `takeDatagram(now)` until `null`, `deadline()` / `handleTimer(now)` for its timers, and `readStream()` / `writeStream(id, data, fin)` once connected; `updateKeys()`, `close(code)` and `release()`; `acceptRetry(originalDcid, retryScid)` for a connection a Retry token opened; `quicStatelessResetToken(key, cid)`. TLS 1.3 runs over CRYPTO frames at the Initial, Handshake and 1-RTT levels (`TlsServer` with `quic` set), the keys of each level come from `nish/net/quic-packet` as the secrets appear and are discarded and wiped as RFC 9001 §4.9 says, transport parameters go both ways and the client's are checked, every ack-eliciting packet is acknowledged in its space, the client gets new connection IDs once the handshake is confirmed, PATH_CHALLENGE is answered, and an ack-eliciting Initial is padded to 1200 bytes and held to three times what the unvalidated client sent. A protocol violation closes the connection with the RFC's transport error, a TLS alert with CRYPTO_ERROR; a packet that cannot be used is dropped and counted. The idle timeout is the smaller of both sides' `max_idle_timeout`, at least three probe timeouts, and closes silently; key update runs both ways, the client's followed (at most `QUIC_CONN_MAX_KEY_UPDATES`, QUIC-4) and the server's started by `updateKeys()`, with the next read keys derived in advance and the previous kept a probe timeout for reordered packets; every connection ID carries the stateless reset token the configuration's static key gives it. Loss recovery is `nish/net/quic-recovery`'s (WP34 Q3): what a lost packet carried (CRYPTO, STREAM, HANDSHAKE_DONE and the connection-ID frames) is sent again, a probe timeout sends a probe, and NewReno's window holds back everything but ACKs and probes. Streams (WP34 Q4) are `nish/net/quic-stream`'s, both ways and both kinds, through `nextStreamEvent()`, `openStream(bidirectional)`, `streamRead(id, buf, at, length)`, `streamWrite(id, buf, from, length, fin)`, `streamReset(id, code)` and `streamStopSending(id, code)`; RFC 9221's datagrams through `sendDatagram(buf, from, length)`, `readDatagram(buf, at, cap)` and `maxDatagramPayload()` when `config.maxDatagramFrameSize` offers them. Nothing is allocated per packet (QUIC-3): `receiveWindow(buf, off, len, now)` and `takeDatagramInto(out, at, now)` open and build packets in place inside an arena block, and `reset(entropy)` makes the connection a reusable slot for the next client. Not here yet: the AEAD limits of RFC 9001 §6.6 (QUIC-6) | RFC 9000 §10.1, §10.3.2; RFC 9001 §4, §5.7, §6, §8; RFC 9221; a handshake and stream echo, and an update each way, against aioquic, recorded and replayed byte for byte |
|
|
151
|
+
| [`net/quic-listener.ts`](./net/quic-listener.ts) | What a QUIC server answers to a datagram no connection owns (WP34 Q2, second part): `new QuicListener(config, entropy)` and `handle(datagram, address, now)`, which answers a `QuicListenerAnswer` whose `kind` is `QUIC_LISTEN_DROP`, `_ACCEPT` (make a `QuicConnection`; after a Retry, `acceptRetry` it with the answer's IDs), or a `reply` to send: `_VERSION_NEGOTIATION` for another version in a full-sized datagram, `_RETRY` with an address-validation token when `config.retry` is set, `_INVALID_TOKEN` for a Retry token that is forged, moved, expired or for another ID, and `_STATELESS_RESET` for a short header to an ID the server has no connection for. A token is 128 bits of HMAC-SHA256 under `config.retryTokenKey` over its issue time, the original DCID, the new ID and the client's address and port, good for `config.retryTokenLifetime`. A reset ends in `quicStatelessResetToken` of the packet's ID, is always shorter than the datagram it answers and never under 21 bytes, and is rate limited. Its own unpredictable bytes come from an HMAC generator seeded once by the caller. For the carrier's send loop, `quicListenerTakePaced(conn, now)` takes a connection's next datagram only when its pacer has the credit, and `quicListenerPaceTime(conn, now)` says when it will (WP34 Q3); `quicListenerTakeFlight(conn, now, buf, at, cap, flight)` writes a paced flight back to back for one GSO send, with the `UDP_SEGMENT` size in its `QuicFlight`, and `quicListenerReceiveSegments` hands a connection a GRO receive segment by segment, in place (WP34 Q4) | RFC 9002 §7.7; RFC 9000 §5.2.2, §6, §8.1, §10.3, §17.2.1, §17.2.5; Version Negotiation, Retry, a stateless reset and the idle timeout against aioquic, recorded and replayed byte for byte |
|
|
152
|
+
| [`net/quic-recovery.ts`](./net/quic-recovery.ts) | QUIC loss detection and congestion control (WP34 Q3), sans-IO, for the server side: `QuicRecovery` holds one `QuicSentPackets` ring per packet number space (16 packets for Initial and Handshake, 128 for Application Data, fixed when it is made); `onPacketSent(space, pn, size, now)` records an ack-eliciting packet and answers its slot, `onAck(space, ranges, count, ackDelay, now)` reads an ACK frame's ranges and lists the slots it acknowledged and showed lost (`acked`, `lost`), `deadline(amplificationBlocked)` and `onTimeout(now, amplificationBlocked)` run the time-threshold loss timer and the probe timeout with its backoff, `discardSpace` drops a space whose keys are gone, `evictOldest` frees a slot for a probe. The RTT estimate (`latestRtt`, `smoothedRtt`, `rttVar`, `minRtt`, `probeTimeout()`), loss by packet threshold 3 and time threshold 9/8, NewReno (slow start, one halving per recovery period, congestion avoidance, the 2400-byte floor, persistent congestion) and a pacer at 5/4 of the window per smoothed RTT (`pacerDelay`, `onPaced`), all in integer milliseconds and bytes | RFC 9002 §5–7, Appendices A and B, worked by hand in `net_quic_recovery` |
|
|
153
|
+
| [`net/quic-stream.ts`](./net/quic-stream.ts) | QUIC streams (WP34 Q4), sans-IO, which `nish/net/quic` drives: `QuicStreams` holds one `QuicStream` slot per stream that may be open at once (the client's bidirectional and unidirectional limits and the server's own), each with a fixed receive and send buffer, found by ID through a hash index. The send and receive state machines of §3 (`QUIC_SEND_*`, `QUIC_RECV_*`), reassembly into the receive buffer, flow control per stream and for the connection both ways (MAX_STREAM_DATA and MAX_DATA raised as the application reads, STREAM_DATA_BLOCKED and DATA_BLOCKED once per limit), the stream limits (MAX_STREAMS raised as the client's streams finish, STREAMS_BLOCKED at the client's), STOP_SENDING answered with RESET_STREAM, and the refusals STREAM_STATE_ERROR, STREAM_LIMIT_ERROR, FLOW_CONTROL_ERROR and FINAL_SIZE_ERROR. The application's answers are `QUIC_STREAM_OK`, `QUIC_STREAM_END` and `QUIC_STREAM_ERR_*` | RFC 9000 §2–§4, §19.4–§19.14 |
|
|
154
|
+
| [`net/quic-datagram.ts`](./net/quic-datagram.ts) | `QuicDatagramQueue` (WP34 Q4): a fixed ring of DATAGRAM payloads, one each way per connection, copied in (`push`) and out (`pop`, or `putFrame` as a DATAGRAM frame), refusing past a full ring and counting what it drops | RFC 9221 §5 |
|
|
155
|
+
| [`net/hpack.ts`](./net/hpack.ts) | HPACK, HTTP/2's header compression: `HpackEncoder` and `HpackDecoder`, one per direction of a connection, each with its `HpackTable`; and beneath them the §5.1 integer (`hpackEncodeInteger` / `hpackDecodeInteger`), the §5.2 string (`hpackEncodeString` / `hpackDecodeString`) and Appendix B's Huffman code (`HpackHuffman`, `hpackHuffmanEncode`, `hpackHuffmanDecode`, `hpackHuffmanLength`). Names and values are `u8[]`. The encoder takes the representation and the Huffman choice per field, and sends a never-indexed field as a literal even when a table holds it. The decoder answers a negative `HPACK_ERR_*` code, and stays spent, for a truncated block, an integer past 2^31 - 1, an index of zero or out of range, bad Huffman padding, EOS, a size update above the SETTINGS limit, after a field or missing when a lowered limit requires one; a header list past the caller's limit is decoded to the end, its excess dropped, and answered with `HPACK_LIST_TOO_LARGE`, which is not fatal. A window outside its buffer, a negative size or limit, and an integer prefix outside 1 to 8 bits are the program's mistakes and panic | RFC 7541 Appendix C (C.1–C.6, with the dynamic table after every step) and every code of Appendix B |
|
|
156
|
+
| [`net/qpack.ts`](./net/qpack.ts) | QPACK, HTTP/3's field compression, with a dynamic table of capacity 0: `QpackEncoder` and `QpackDecoder`, one per connection. The encoder writes a field section that starts `00 00` (Required Insert Count 0, Base 0) with `beginSection`, then one field line per `encodeField(out, name, off, len, value, off, len, neverIndexed, huffman)`: an Indexed Field Line when the 99-entry static table holds the field, else a literal with the name by static index when it can, each string Huffman-coded when that is shorter and `huffman` is set, and a never-indexed field always a literal with the N bit. The decoder answers a section's fields as windows into its own `bytes` (`nameStart`, `nameLength`, `valueStart`, `valueLength`, `neverIndexed`, `count`), its arrays reused from section to section. It answers QPACK_DECOMPRESSION_FAILED, and stays spent, for a truncated section, an integer past 2^62 − 1, a non-zero Required Insert Count, a negative Base, any dynamic-table reference, a static index past 98 and bad Huffman; a section past `maxFieldSectionSize` answers `QPACK_SECTION_TOO_LARGE`, which is not fatal. `receiveEncoderStream` accepts Set Dynamic Table Capacity 0 and answers QPACK_ENCODER_STREAM_ERROR for any other capacity, Insert or Duplicate; the encoder's `receiveDecoderStream` accepts Stream Cancellations, split anywhere, and answers QPACK_DECODER_STREAM_ERROR for a Section Acknowledgment or Insert Count Increment; `reason` names the rule. `qpackPushStreamCancellation` is the one instruction it may send. The Huffman code is `nish/net/hpack`'s. A window outside its buffer, a negative limit and a stream ID outside 0 to 2^62 − 1 panic | RFC 9204 Appendix B.1, every Appendix A entry by its index, B.2 to B.5's instructions refused, and a request matched byte for byte with pylsqpack's encoding |
|
|
157
|
+
| [`net/http1.ts`](./net/http1.ts) | `Http1Parser`, an incremental HTTP/1.1 request parser: `feed(buf, off, len)` any slice, then `next()` answers `HTTP1_NEED_MORE`, `HTTP1_HEAD` (`method`, `target`, `minor`, lowercased `names` beside `values`, `header(name)`, `contentLength` or `chunked`, `keepAlive`, `upgrade`), `HTTP1_BODY` (a window onto its buffer, de-chunked), `HTTP1_END`, or one of three final events: `HTTP1_UPGRADE` (`upgradeBytes()` and `declineUpgrade()`), `HTTP1_CLOSED` and `HTTP1_ERROR` with the `status` to answer. It refuses with 400 every request-smuggling shape — `Content-Length` with `Transfer-Encoding`, a repeated or non-digit `Content-Length`, an obs-fold, a bare LF, whitespace before a colon, a malformed chunk size or terminator — and with 413, 414, 431, 501 and 505 what those mean. The head is also kept as spans in a buffer reused from request to request (`head`, `spans`, `methodIs`, `targetIs`, `headerIndex`, `headerIs`, `upgradeIs`), so a parser with `keepText` off allocates nothing per request; `maxChunk` caps each `HTTP1_BODY`, and `restart()` readies it for the next connection. The writer is `http1ResponseHead(status, reason, names, values, bodyLength)`, which adds the framing field itself and refuses (`null`) a CR, LF or other control character anywhere a caller's string lands, and `http1Chunk` and `http1LastChunk` for a chunked body; `http1WriteResponseHead` and `http1WriteChunk` write the same bytes into a caller's buffer at an offset, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED` | RFC 9112 §2–§7 and §9.3, RFC 9110 §5, §6.2, §7.2 and §7.8; a corpus fed at every split point (`tests/link/net_http1`) |
|
|
158
|
+
| [`net/websocket.ts`](./net/websocket.ts) | `WsDecoder(expectMasked, maxMessage)`, the same `feed` and `next()` over WebSocket frames: `WS_MESSAGE` (fragments joined, text checked as UTF-8 fragment by fragment), `WS_PING`, `WS_PONG`, and the final `WS_CLOSE` and `WS_ERROR`, whose `closeCode` is the 1002, 1007 or 1009 to close with. `websocketFrame` encodes a frame with the mask key the caller supplies (`websocketWriteFrame` into a caller's buffer, `websocketFrameSize` its length), `reset()` readies a decoder for the next connection, `websocketClosePayload` a close frame's code and reason, and `websocketIsUtf8` checks a window. The handshake is `websocketRequestKey(parser)` over an `Http1Parser`'s head, `websocketAcceptKey`, `websocketKeyIsValid` and `websocketUpgradeResponse`, the 101 | RFC 6455 §1.3's accept key, the §5.7 example frames byte for byte, §5.5's control-frame rules, §7.4's close codes |
|
|
159
|
+
| [`net/http1-server.ts`](./net/http1-server.ts) | The server side of HTTP/1.1 (WP34 H1): `Http1Connection`, sans-IO, drives an `Http1Parser` — `feed`, then `next()` answers `H1_REQUEST` (the parser's head), `H1_BODY` (at most `chunkSize` bytes, either framing), `H1_END`, `H1_WRITE` (a held-back write may go again), `H1_WS_MESSAGE`, `H1_WS_CLOSE` or the final `H1_ERROR`; the program answers with `respond(status, names, values, bodyLength)`, `write` and `end`, or `acceptWebSocket(protocol)` and then `sendFrame` and `closeWebSocket`. Keep-alive and pipelining one exchange at a time, a response with a length or chunked (unframed to an HTTP/1.0 client), `Connection` and the framing decided by the server, every parser refusal answered with its status and a close, pings and closes answered on a WebSocket. Two slot pools for a program that owns its loop: `Http1Server` on plain TCP and `Http1TlsServer` on `nish/net/tls-tcp` with ALPN `http/1.1` or none (`accept`, `readable` / `writable`, `next(slot)`, `flush(slot)`, `close`, `expire(now)` for the idle timeout). Every cap is an `Http1Config` number fixed at start-up, and a warmed slot allocates nothing per request (`docs/security/http1.md`). Native only, as `nish:net` is | RFC 9112 §6, §7, §9; RFC 9110 §9.3.2, §15; RFC 6455 §4.2, §5.5. Nish clients over loopback against both carriers, requests cut at every byte, 2 MiB bodies each way, pipelining, a WebSocket echo, the arena flat over two hundred requests, and every refusal (`tests/link/net_http1_server`); `curl` against its `serve` mode |
|
|
160
|
+
| [`net/http-fields.ts`](./net/http-fields.ts) | The header model HTTP/2 and HTTP/3 share, version-neutral: `HttpFields` reads a decoded section with `readRequest(names, values, extendedConnect)`, `readResponse` or `readTrailers` into `method`, `scheme`, `authority`, `path`, `protocol`, `status`, the regular fields in order (`get(name)`) and `contentLength`, answering `HTTP_FIELDS_OK` or the first refusal: a name that is empty, not a token or not lowercase; a value with NUL, CR or LF or whitespace at an end; a connection-specific field (`te` but `trailers` included); a pseudo-header unknown, repeated, after a regular field, forbidden by the shape or missing; a pseudo-header value of the wrong syntax; a `host` that disagrees with `:authority`; a bad or disagreeing `content-length`. `:protocol` (RFC 8441) is read only where the caller enabled extended CONNECT. `httpFieldNameValid`, `httpFieldValueValid`, `httpFieldConnectionSpecific` and `httpFieldsCheckOutgoing` check one name, one value or a section a program sends | RFC 9110 §5, RFC 9113 §8.2–§8.5, RFC 9114 §4.2–§4.3, RFC 8441 §4; every refusal in `tests/link/net_http_fields` |
|
|
161
|
+
| [`net/http2-frame.ts`](./net/http2-frame.ts) | HTTP/2's frames, sans-IO and allocation-free: `http2ReadHeader` and `http2ParseFrame` read any frame of RFC 9113 §6 into a reused `Http2Frame` (its content left in the buffer as a window), answering 0 or the connection error — FRAME_SIZE_ERROR past the reader's SETTINGS_MAX_FRAME_SIZE or for a frame of the wrong length, PROTOCOL_ERROR for a frame on the wrong stream, padding as long as its frame, or a WINDOW_UPDATE of zero on the connection — with a stream error (`streamError`) for a PRIORITY of the wrong length, a self-dependency or a WINDOW_UPDATE of zero on a stream. `http2SettingCount`, `http2SettingId`, `http2SettingValue` and `http2SettingError` read and check SETTINGS. The writers, one per type (`http2WriteData` … `http2WriteContinuation`, `http2WriteSettingsAck`), write into the caller's buffer at an offset and answer -1 when a frame does not fit | Every type against frames built by hand from §4.1 and §6, both directions, and every refusal (`tests/link/net_http2_frame`) |
|
|
162
|
+
| [`net/http2.ts`](./net/http2.ts) | `Http2Connection`, the server side of HTTP/2, sans-IO: `feed(buf, off, len)`, then `next()` answers `H2_REQUEST` (`stream`, `fields`), `H2_DATA` (a window onto the input), `H2_TRAILERS`, `H2_RESET`, `H2_WINDOW` (a send window opened), `H2_GOAWAY` or the final `H2_ERROR`; send `output[outputStart .. outputEnd)` and `consume`. `respond`, `writeData` (as far as both windows and the output allow), `writeTrailers`, `reset`, `goaway` and `restart`. The §5.1 stream state machine, connection and stream flow control with WINDOW_UPDATE both ways, SETTINGS and the acknowledgement, PING, header blocks across CONTINUATION decoded by `HpackDecoder`, a 431 past the header-list cap, extended CONNECT behind SETTINGS_ENABLE_CONNECT_PROTOCOL, and every connection and stream error of RFC 9113 as GOAWAY or RST_STREAM. Every cap is an `Http2Config` number fixed at start-up; DATA and every control frame allocate nothing ([H2-1](../docs/security/http2.md)) | RFC 9113 §3–§8, RFC 8441; requests, streamed responses, flow control stalling and resuming and concurrent streams from a scripted client (`tests/link/net_http2`), and a negative for each error class (`tests/link/net_http2_errors`) |
|
|
163
|
+
| [`net/http2-tls.ts`](./net/http2-tls.ts) | `Http2TlsServer`: HTTP/2 over `nish/net/tls-tcp` for a program that owns its loop — a pool of `Http2Connection`s beside the TLS slots, sized at start-up and reused with `restart`; `accept`, `readable` / `writable`, `signP256`, `next(slot)` for the slot's next event, `flush(slot)`, `close`. A slot whose ALPN did not choose `h2` is shut down with `close_notify` (RFC 9113 §3.2). Native only, as `nish:net` is | A Nish TLS client over loopback (`tests/link/net_http2_tls`): ALPN, a GET, an echoed POST, a hundred DATA frames with the arena flat, GOAWAY, and the pool; `curl --http2` against its `serve` mode |
|
|
164
|
+
| [`net/http3-frame.ts`](./net/http3-frame.ts) | HTTP/3's frames and stream types, sans-IO and allocation-free: `h3ReadFrameHeader` reads a type and a length (0 until both are in the window), `h3ReadSettings` a SETTINGS payload into a reused `Http3Settings` (the three identifiers it understands, and the first eight it does not, kept for an extension), `h3ReadIdPayload` a GOAWAY, MAX_PUSH_ID or CANCEL_PUSH; `h3PutFrameHeader`, `h3PutSettings`, `h3PutIdFrame` and `h3PutVarint` write in place. `h3ReservedHttp2Frame` names HTTP/2's reserved types, `h3Greased` the `0x1f * N + 0x21` ones, and the `H3_*` constants are RFC 9114's types, settings and error codes | RFC 9114 §6.2, §7, §8.1, against frames built by hand from §7's layouts (`tests/link/net_http3_frame`) |
|
|
165
|
+
| [`net/http3.ts`](./net/http3.ts) | `Http3Connection`, the server side of HTTP/3 over a `QuicConnection`'s streams: `next()` answers `H3_REQUEST` (`stream`, `fields`), `H3_DATA` (a window onto `data`, at most `bodyChunk`), `H3_TRAILERS`, `H3_END`, `H3_RESET`, `H3_WRITABLE`, `H3_GOAWAY` or the final `H3_ERROR`; `respond`, `writeData` (takes what the send buffer has room for), `writeTrailers`, `reset` and `goaway` write. It opens its control stream with SETTINGS and its QPACK streams (capacity 0, through `nish/net/qpack`), checks every field section with `nish/net/http-fields`, answers a section past `maxFieldSectionSize` with a 431, and closes the connection with RFC 9114's code for each connection error. No server push. A warm connection allocates nothing per request (`docs/security/http3.md`) | RFC 9114 §4–§8 and RFC 9204 §4.2: a Nish client over QUIC, requests with bodies both ways, trailers, GOAWAY, cancellation, every error code (`tests/link/net_http3`, `net_http3_errors`) |
|
|
166
|
+
| [`net/http3-server.ts`](./net/http3-server.ts) | `Http3Server`: HTTP/3 over QUIC on one UDP socket for a program that owns its loop — a pool of `QuicConnection` slots with an `Http3Connection` each, sized at start-up and reused; a `QuicListener` in front; datagrams routed by a hashed index of every slot's connection IDs; `receive(now, key)`, `tick(now)`, `ready()` for the next slot with news, `flush(now)` sending paced GSO flights, `timeout(now)` from a timer wheel. A handshake that did not choose ALPN `h3` is closed with no_application_protocol (RFC 9114 §3.1); a slot done after GOAWAY is closed and freed. Native only, as `nish:net` is | A Nish client across loopback (`tests/link/net_http3_server`): requests, a paced 300 KB response, GOAWAY and the slot freed, ALPN, a full pool, the arena over connections through one slot; aioquic and `curl --http3` against its `serve` mode |
|
|
167
|
+
| [`net/webtransport.ts`](./net/webtransport.ts) | `WebTransport`, the server side of WebTransport over HTTP/3 as draft-ietf-webtrans-http3-02 has it and Chrome and `wtransport` 0.7 negotiate it, a layer over one `Http3Connection` whose `Http3Config.webtransportSessions` turns it on (extended CONNECT, SETTINGS_H3_DATAGRAM and the draft's two WebTransport settings): `next()` answers `WT_SESSION` (the program `accept`s or `refuse`s it), `WT_DATAGRAM`, `WT_STREAM`, `WT_STREAM_DATA`, `WT_STREAM_END`, `WT_STREAM_RESET`, `WT_STREAM_STOPPED`, `WT_WRITABLE`, `WT_DRAIN` and `WT_CLOSED`, and passes a plain request's `H3_*` events through; `sendDatagram` (the quarter stream ID, then the payload), `openStream`, `write`, `resetStream`, `stopSending`, `close` (CLOSE_WEBTRANSPORT_SESSION) and `drain`. Streams that arrive before their session wait, bounded; every cap is fixed at start-up, and a warm session allocates nothing per datagram or stream (`docs/security/webtransport.md`) | RFC 9220, RFC 9297 and draft-02: a Nish client over QUIC — sessions, datagrams at the size limits, streams both ways, close and drain from either side, every refusal — and one exchange recorded from a `wtransport` 0.7 client replayed byte for byte (`tests/link/net_webtransport`) |
|
|
168
|
+
|
|
169
|
+
**How a carrier drives `TlsServer`.** The server speaks in handshake
|
|
170
|
+
*messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
|
|
171
|
+
HelloRetryRequest, ServerHello), `TLS_LEVEL_HANDSHAKE` (EncryptedExtensions
|
|
172
|
+
through both Finished messages) and `TLS_LEVEL_APPLICATION`. A carrier hands
|
|
173
|
+
`receive` the bytes it got at a level, in any split, and sends what
|
|
174
|
+
`takeOutput(level)` answers under that level's keys: TLS over TCP (T2) in
|
|
175
|
+
records, QUIC (Q2) in CRYPTO frames of the matching packet number space.
|
|
176
|
+
`writeSecret(level)` and `readSecret(level)` answer the server's and the
|
|
177
|
+
client's traffic secret for the Handshake and Application levels as soon as
|
|
178
|
+
it is known (`null` before; the Initial level has none), `tlsTrafficKey` and
|
|
179
|
+
`tlsTrafficIv` turn one into a record key, and `exporterSecret` is there for
|
|
180
|
+
exporters. The state says what is due: `TLS_STATE_WAIT_SIGNATURE` after the
|
|
181
|
+
first flight is written, `TLS_STATE_WAIT_FINISHED` once it is signed,
|
|
182
|
+
`TLS_STATE_CONNECTED` when the client's Finished verified, and
|
|
183
|
+
`TLS_STATE_FAILED` with `alert` set after a refusal. Over QUIC, set `quic` in
|
|
184
|
+
the configuration: ALPN becomes mandatory and the transport parameters go both
|
|
185
|
+
ways (`clientTransportParameters` holds the client's).
|
|
186
|
+
|
|
187
|
+
**Secrets.** The ECDHE secret, and the copy of the ephemeral key it is
|
|
188
|
+
computed with, are `Secret`s wiped on every path, and `tlsSignEcdsaP256` takes
|
|
189
|
+
its key as a `Secret`. What `TlsServer` keeps in its fields — the caller's
|
|
190
|
+
ephemeral key bytes and the handshake, traffic and exporter secrets — is not
|
|
191
|
+
wiped until `secureZero` ships in a release (TLS-1 in
|
|
192
|
+
[`docs/security/tls.md`](../docs/security/tls.md), the record that also lists
|
|
193
|
+
what each refusal is and the test that pins it).
|
|
194
|
+
`tests/link/net_tls_*` are the module's programs, each also run under
|
|
195
|
+
`--number-mode f64`.
|
|
196
|
+
|
|
197
|
+
**TLS over TCP.** `nish/net/tls-tcp` is the carrier of the paragraph above
|
|
198
|
+
for TCP, and `nish/net/tls/record-server` is all of it but the socket, for a
|
|
199
|
+
test or another transport. The program is the loop: there are no callbacks,
|
|
200
|
+
so each call answers the slot's interest and the program re-arms the
|
|
201
|
+
descriptor with `pollModify(loop, tls.fd(slot), wants & 3, slot)`, signs on
|
|
202
|
+
`TLS_RECORD_SIGN`, reads on `TLS_RECORD_DATA` and calls `close(slot)` on
|
|
203
|
+
`TLS_RECORD_DONE`. A slot's buffers — about 136 KB of resident memory, measured over a 400-slot pool, with the handshake's own state in them — are
|
|
204
|
+
allocated once, and after its handshake a connection allocates nothing that
|
|
205
|
+
outlives a record. The handshake keeps its state in the slot, which `accept`
|
|
206
|
+
restarts, and a KeyUpdate installs its keys in place, so neither leaves
|
|
207
|
+
anything in the arena (TLS-3); a connection still takes at most
|
|
208
|
+
`TLS_RECORD_MAX_KEY_UPDATES` (64) KeyUpdates from its client. The carrier
|
|
209
|
+
has no clock, so
|
|
210
|
+
closing quiet slots is the program's (TLS-4). On this path the record layer
|
|
211
|
+
wipes `TlsServer`'s traffic and handshake secrets and the ephemeral key once
|
|
212
|
+
it has what it needs from them, which narrows TLS-1.
|
|
213
|
+
`tests/link/net_tls_record_*` are its programs, and the `net_tls_tcp` block
|
|
214
|
+
of `tests/run.js` drives the carrier with openssl and curl.
|
|
215
|
+
|
|
216
|
+
**How a server drives `QuicConnection`.** Keep one `QuicListener` for the
|
|
217
|
+
server, with the same `QuicServerConfig` its connections use: two static keys
|
|
218
|
+
of `QUIC_CONN_STATIC_KEY_SIZE` bytes (the stateless reset key, which has to
|
|
219
|
+
survive a restart to be any use, and the Retry token key; both are the
|
|
220
|
+
caller's to keep, QUIC-5) and whether to ask for Retry. Route each datagram by
|
|
221
|
+
its first packet's DCID: to the connection whose `ownsConnectionId` says it
|
|
222
|
+
is its, and anything else to `listener.handle(datagram, address, now)`, which
|
|
223
|
+
answers what to send back or says to make a connection — and must see nothing
|
|
224
|
+
a live connection owns, or a stateless reset would end that connection.
|
|
225
|
+
Make the connection when it says so, with `QUIC_CONN_ENTROPY_SIZE` bytes from
|
|
226
|
+
`crypto.getRandomValues`: the TLS server random and x25519 key, the server's
|
|
227
|
+
first connection ID and the seed its later IDs are derived from all come out
|
|
228
|
+
of them, so a test injects fixed bytes and a recorded exchange replays byte
|
|
229
|
+
for byte. Neither has a clock: `now` is the caller's monotonic time in
|
|
230
|
+
milliseconds, and `deadline()` says when to call `handleTimer(now)` next.
|
|
231
|
+
Hand the connection every datagram the client sends;
|
|
232
|
+
when `signatureInput()` answers bytes, sign them (`tlsSignEcdsaP256`) and hand
|
|
233
|
+
the signature to `sign`; then send whatever `takeDatagram(now)` answers until it
|
|
234
|
+
answers `null`, to the address the datagram came from. `ownsConnectionId`
|
|
235
|
+
says whether a datagram's DCID is the connection's, which is how a server
|
|
236
|
+
with many routes them. Stream data comes out of `readStream()` in order per
|
|
237
|
+
stream, and `writeStream` queues a reply within the credit the client gave.
|
|
238
|
+
`state` says where it is, and `error` why it closed. The keys of each level
|
|
239
|
+
are wiped when the level is discarded, each key generation when an update
|
|
240
|
+
replaces it, and the rest by `release()` or the idle timeout; what no wipe
|
|
241
|
+
reaches is QUIC-2 in [`docs/security/quic.md`](../docs/security/quic.md).
|
|
242
|
+
`tests/link/net_quic_frame`, `net_quic_conn_parts`, `net_quic_conn`,
|
|
243
|
+
`net_quic_conn_replay`, `net_quic_lifecycle` and `net_quic_lifecycle_replay`
|
|
244
|
+
are its programs, each also run under `--number-mode f64`; the two replays
|
|
245
|
+
send, over loopback from a Nish UDP client, exchanges recorded against
|
|
246
|
+
aioquic by their `record.sh`: a handshake and echo, and Version Negotiation,
|
|
247
|
+
Retry, key update both ways, the idle timeout and a stateless reset.
|
|
248
|
+
|
|
249
|
+
`net/http1.ts`, `net/http1-server.ts` and `net/websocket.ts` run their checks
|
|
250
|
+
again under `--number-mode f64` (`net_http1_f64`, `net_http1_server_f64`,
|
|
251
|
+
`net_websocket_f64`). None handles a secret but through `nish/net/tls-tcp`, so
|
|
252
|
+
none is in the constant-time check; their record is
|
|
253
|
+
[`docs/security/http1.md`](../docs/security/http1.md), written with the code
|
|
254
|
+
rather than after an audit.
|
|
255
|
+
|
|
99
256
|
## How a program imports it
|
|
100
257
|
|
|
101
258
|
By its package specifier:
|
|
@@ -123,7 +280,7 @@ A `std/` module is its own package (`nish`), so its symbols are scoped and a
|
|
|
123
280
|
program may declare a function one of these modules also exports
|
|
124
281
|
(`tests/link/std_package_scope`, `docs/wp21-packages.md` §5a). The package is
|
|
125
282
|
decided by the `nish/` specifier rather than by the directory the file is found
|
|
126
|
-
in — read off the path it would be `nish` from `node_modules/nish/std/` and the
|
|
283
|
+
in — read off the path it would be `nish` from `node_modules/@amritk/nish/std/` and the
|
|
127
284
|
root package from a checkout, and the same program would compile against an
|
|
128
285
|
installed compiler and be refused by a checkout of it.
|
|
129
286
|
|
|
@@ -175,18 +332,17 @@ that are *not* this package.
|
|
|
175
332
|
mode pays nothing for it, because `toI32` on an `i32` is identity.
|
|
176
333
|
`tests/link/std_text_f64` is what keeps this from being prose;
|
|
177
334
|
[`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §4 is the reasoning.
|
|
178
|
-
- **Name the private helpers as though they were exported.** A
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
may declare those names itself.
|
|
335
|
+
- **Name the private helpers as though they were exported.** A `std/` module
|
|
336
|
+
belongs to package `nish`, so its symbols no longer collide with the
|
|
337
|
+
importing program's (`tests/link/std_package_scope`), but every `std/` module
|
|
338
|
+
shares that one package's flat namespace: a function name is unique across
|
|
339
|
+
the package whether or not it is exported, because the whole-program
|
|
340
|
+
attribute analysis is keyed by symbol name. A helper called `isBlank` in one
|
|
341
|
+
module would stop another `std/` module that declares its own `isBlank` from
|
|
342
|
+
compiling, which is why `text.ts` calls it `isTextBlankByte`. Keep the
|
|
343
|
+
helpers few and their names distinctive
|
|
344
|
+
([`docs/wp26-stdlib.md`](../docs/wp26-stdlib.md) §3e). Module constants are
|
|
345
|
+
exempt — `NEWLINE` and `SPACE` fold at their uses.
|
|
190
346
|
- **It is an Nish program**, so the constraints are the language's: a function is
|
|
191
347
|
an arrow bound to a module-level `const`, a function is never a value, there
|
|
192
348
|
is no `try` / `catch`, and there are no optional or default parameters. Those
|
|
@@ -200,7 +356,7 @@ that are *not* this package.
|
|
|
200
356
|
multi-module program is exercised end to end, and it is also what puts the
|
|
201
357
|
module into the corpus the stage1 oracles read
|
|
202
358
|
([`.claude/selfhost.md`](../.claude/selfhost.md)): a `std/` module with no
|
|
203
|
-
importer in `tests/link/` is compiled
|
|
359
|
+
importer in `tests/link/` is compiled on no run.
|
|
204
360
|
`testing.ts` has two, one per outcome — `tests/link/std_testing` (exit 0) and
|
|
205
361
|
`tests/link/std_testing_fail` (exit 1, and the wording of every failure
|
|
206
362
|
message) — and `text.ts` and `json.ts` have `tests/link/std_text` and
|