@amritk/nish-x86_64-linux 0.28.0 → 0.30.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 +8 -5
- package/bin/nish +0 -0
- package/docs/AI.md +114 -24
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -1
- package/runtime/core/host.mjs +4 -0
- package/runtime/nish.d.ts +127 -0
- package/runtime/nish.h +94 -5
- package/runtime/nish.mjs +18 -4
- package/runtime/runtime-net.c +386 -5
- package/runtime/runtime-os.c +59 -8
- package/runtime/runtime-parallel.c +60 -5
- package/runtime/runtime-region.c +231 -0
- package/runtime/runtime-sandbox.c +43 -34
- package/runtime/runtime-shm.c +541 -0
- package/runtime/runtime.c +838 -792
- package/scripts/build.sh +55 -12
- package/std/README.md +12 -3
- package/std/collections.ts +5 -5
- package/std/crypto/hmac.ts +305 -2
- package/std/crypto/jwt.ts +1661 -0
- package/std/json-typed.ts +34 -0
- package/std/json.ts +35 -6
- package/std/net/handler.ts +1348 -0
- package/std/net/http-client.ts +97 -16
- package/std/net/http-compress.ts +54 -12
- package/std/net/http1-client.ts +270 -41
- package/std/net/http1-server.ts +145 -32
- package/std/net/http1.ts +274 -51
- package/std/net/outbound.ts +409 -0
- package/std/net/postgres-carrier.ts +985 -0
- package/std/net/postgres.ts +297 -4
- package/std/net/prefork.ts +344 -20
- package/std/net/upstream.ts +759 -0
- package/std/secret.ts +1 -1
- package/std/sync/ring.ts +587 -0
- package/std/sync/shm.ts +284 -0
- package/std/term-rate.ts +13 -1
- package/std/threads.ts +35 -3
- /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
package/scripts/build.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm|wasi|napi]
|
|
5
5
|
#
|
|
6
|
-
# The C runtime is
|
|
6
|
+
# The C runtime is fifteen 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
|
|
8
8
|
# system calls (files, directories, subprocesses, the environment, the clock),
|
|
9
9
|
# <dir>/runtime-parallel.c, the half that divides a range of work across
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
# <dir>/runtime-sha256.c, SHA-256 on the CPU's SHA instructions,
|
|
17
17
|
# <dir>/runtime-checksum.c, CRC-32 on its carry-less multiply or CRC instructions,
|
|
18
18
|
# <dir>/runtime-term.c, stdin and the terminal of `nish:term`,
|
|
19
|
-
# <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`,
|
|
20
|
-
# <dir>/runtime-sandbox.c, the confinement `--sandbox` applies
|
|
19
|
+
# <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`,
|
|
20
|
+
# <dir>/runtime-sandbox.c, the confinement `--sandbox` applies,
|
|
21
|
+
# <dir>/runtime-shm.c, the shared-memory regions of `nish/sync/shm`, and
|
|
22
|
+
# <dir>/runtime-region.c, the arenas a program makes and frees one at a time. Each of those
|
|
21
23
|
# files says why they are compiled and measured apart.
|
|
22
24
|
#
|
|
23
25
|
# Profiles:
|
|
@@ -112,6 +114,12 @@
|
|
|
112
114
|
# --sqlite-from <dir> link <dir>/<unit>.o in place of compiling
|
|
113
115
|
# runtime/sqlite/, when the IR calls SQLite.
|
|
114
116
|
# All four refuse the wasm, wasi and napi profiles and --pgo-*, with exit 2.
|
|
117
|
+
# --module-objects <dir> debug with --runtime-from only: the object of each
|
|
118
|
+
# input X.ll is <dir>/X.o, linked as it stands when
|
|
119
|
+
# the file is there and compiled into it when it is
|
|
120
|
+
# not, so the caller keeps every object it compiled
|
|
121
|
+
# (`nish run`'s module cache, src/build-cache.ts).
|
|
122
|
+
# The inputs' file names must differ.
|
|
115
123
|
#
|
|
116
124
|
# Works on Linux (clang + lld preferred, GNU ld tolerated) and macOS (Apple ld64
|
|
117
125
|
# or Homebrew llvm). Set CC to pick a compiler (default: clang on PATH).
|
|
@@ -128,6 +136,7 @@ objects_from="" # --runtime-from <dir>: link against the
|
|
|
128
136
|
cpu="" # --cpu x86-64-v3: the one CPU level above the baseline (WP38 S2)
|
|
129
137
|
sqlite_to="" # --sqlite-objects <dir>: compile runtime/sqlite/ alone, into <dir>
|
|
130
138
|
sqlite_from="" # --sqlite-from <dir>: link runtime/sqlite/ compiled there
|
|
139
|
+
modules_at="" # --module-objects <dir>: each X.ll's object is <dir>/X.o
|
|
131
140
|
while [ $# -gt 0 ]; do
|
|
132
141
|
case "$1" in
|
|
133
142
|
-o) out="$2"; shift 2 ;;
|
|
@@ -141,6 +150,7 @@ while [ $# -gt 0 ]; do
|
|
|
141
150
|
--runtime-from) objects_from="$2"; shift 2 ;;
|
|
142
151
|
--sqlite-objects) sqlite_to="$2"; shift 2 ;;
|
|
143
152
|
--sqlite-from) sqlite_from="$2"; shift 2 ;;
|
|
153
|
+
--module-objects) modules_at="$2"; shift 2 ;;
|
|
144
154
|
--pgo-generate) pgo=(-fprofile-generate); shift ;;
|
|
145
155
|
--pgo-use)
|
|
146
156
|
[ -f "$2" ] || { echo "error: --pgo-use: profile '$2' not found (run the instrumented binary, then llvm-profdata merge)" >&2; exit 2; }
|
|
@@ -195,6 +205,12 @@ if [ -n "$objects_to" ] || [ -n "$objects_from" ]; then
|
|
|
195
205
|
[ ${#pgo[@]} -eq 0 ] || { echo "error: --runtime-objects and --runtime-from do not take --pgo-*" >&2; exit 2; }
|
|
196
206
|
[ -z "$objects_to" ] || [ -z "$objects_from" ] || { echo "error: --runtime-objects and --runtime-from are two different builds" >&2; exit 2; }
|
|
197
207
|
fi
|
|
208
|
+
if [ -n "$modules_at" ]; then
|
|
209
|
+
[ -n "$objects_from" ] || { echo "error: --module-objects takes --runtime-from" >&2; exit 2; }
|
|
210
|
+
[ -d "$modules_at" ] || { echo "error: --module-objects: $modules_at is not a directory" >&2; exit 2; }
|
|
211
|
+
# Speed and size compile a module to LTO bitcode, not the native object kept there.
|
|
212
|
+
[ "$profile" = debug ] || { echo "error: --module-objects takes the debug profile, not '$profile'" >&2; exit 2; }
|
|
213
|
+
fi
|
|
198
214
|
if [ -n "$sqlite_to" ] || [ -n "$sqlite_from" ]; then
|
|
199
215
|
# The cached SQLite objects sit beside the cached runtime and take what it takes.
|
|
200
216
|
case "$profile" in
|
|
@@ -207,17 +223,18 @@ if [ -n "$sqlite_to" ] || [ -n "$sqlite_from" ]; then
|
|
|
207
223
|
fi
|
|
208
224
|
[ -n "$out" ] || [ -n "$objects_to" ] || [ -n "$sqlite_to" ] || { echo "error: -o <out> is required" >&2; exit 2; }
|
|
209
225
|
|
|
210
|
-
# The runtime is
|
|
226
|
+
# The runtime is fifteen translation units, and a caller names one: whoever passes
|
|
211
227
|
# <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c,
|
|
212
228
|
# <dir>/runtime-cross.c, <dir>/runtime-host.c, <dir>/runtime-net.c, <dir>/runtime-simd.c,
|
|
213
229
|
# <dir>/runtime-aes.c, <dir>/runtime-sha256.c, <dir>/runtime-checksum.c, <dir>/runtime-term.c,
|
|
214
|
-
# <dir>/runtime-bytes.c and <dir>/runtime-
|
|
230
|
+
# <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c, <dir>/runtime-shm.c and <dir>/runtime-region.c compiled beside it, on every profile,
|
|
215
231
|
# wasi included: runtime-simd.c and runtime-bytes.c take their scalar paths there
|
|
216
232
|
# unless the build adds -msimd128,
|
|
217
233
|
# runtime-aes.c, runtime-sha256.c and runtime-checksum.c compile to an answer of 0 and
|
|
218
234
|
# stubs that nothing calls, because the compiler emits no call to their kernels for wasm32, and
|
|
219
235
|
# runtime-term.c reads stdin and answers "not a terminal", and runtime-sandbox.c
|
|
220
|
-
# compiles to two functions that refuse to run, which no wasm build calls
|
|
236
|
+
# compiles to two functions that refuse to run, which no wasm build calls, and
|
|
237
|
+
# runtime-shm.c answers -95 for every region. They were one file until the
|
|
221
238
|
# operating-system half was split out for its own size budget, and the parallel
|
|
222
239
|
# half followed for the same reason (each file's header comment says why), and
|
|
223
240
|
# a link line is where those splits
|
|
@@ -227,7 +244,7 @@ fi
|
|
|
227
244
|
# here keeps every one of those correct, and keeps "the runtime" one thing to
|
|
228
245
|
# name from the outside. A caller that names one itself is left alone, because
|
|
229
246
|
# naming one object twice is a duplicate-symbol error.
|
|
230
|
-
halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c"
|
|
247
|
+
halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c runtime-region.c"
|
|
231
248
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
232
249
|
case "$i" in
|
|
233
250
|
*/runtime.c|runtime.c)
|
|
@@ -262,11 +279,15 @@ if [ -n "$sqlite_to" ]; then
|
|
|
262
279
|
fi
|
|
263
280
|
sqlite_wanted=1
|
|
264
281
|
fi
|
|
282
|
+
# One grep reads every module, not one grep each: a program of forty modules
|
|
283
|
+
# spent a third of a second here starting greps.
|
|
284
|
+
lls=()
|
|
265
285
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
266
|
-
case "$i" in
|
|
267
|
-
*.ll) if grep -Eq '^declare [^"]*@(sqlite3|nishsqlite)_' "$i"; then sqlite_wanted=1; fi ;;
|
|
268
|
-
esac
|
|
286
|
+
case "$i" in *.ll) lls+=("$i") ;; esac
|
|
269
287
|
done
|
|
288
|
+
if [ ${#lls[@]} -gt 0 ] && grep -Eq '^declare [^"]*@(sqlite3|nishsqlite)_' "${lls[@]}"; then
|
|
289
|
+
sqlite_wanted=1
|
|
290
|
+
fi
|
|
270
291
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
271
292
|
case "$i" in
|
|
272
293
|
*.c) if [ -n "$sqlite_dir" ] && [ "$(cd "$(dirname -- "$i")" && pwd -P)" = "$sqlite_dir" ]; then sqlite_wanted=0; break; fi ;;
|
|
@@ -611,9 +632,19 @@ if [ -n "$objects_from" ]; then
|
|
|
611
632
|
for i in "${inputs[@]}"; do
|
|
612
633
|
case "$i" in */runtime.c|runtime.c) echo "error: --runtime-from links the runtime in $objects_from, so $i is not an input" >&2; exit 2 ;; esac
|
|
613
634
|
n=$((n + 1))
|
|
614
|
-
"$
|
|
635
|
+
object="$work/$n.o"
|
|
636
|
+
if [ -n "$modules_at" ]; then
|
|
637
|
+
case "$i" in
|
|
638
|
+
*.ll)
|
|
639
|
+
name=${i##*/}
|
|
640
|
+
object="$modules_at/${name%.ll}.o"
|
|
641
|
+
# An object the caller left there is this module's, compiled before.
|
|
642
|
+
if [ -f "$object" ]; then objects+=("$object"); continue; fi ;;
|
|
643
|
+
esac
|
|
644
|
+
fi
|
|
645
|
+
"$CC" "${module_compile[@]}" -c "$i" -o "$object" &
|
|
615
646
|
pids+=("$!")
|
|
616
|
-
objects+=("$
|
|
647
|
+
objects+=("$object")
|
|
617
648
|
done
|
|
618
649
|
join_jobs || { echo "error: a module did not compile" >&2; exit 1; }
|
|
619
650
|
# In the order the one-command link names them: the inputs, then runtime.c
|
|
@@ -641,6 +672,18 @@ case "$profile" in
|
|
|
641
672
|
"$CC" "${common[@]}" "${optimise[@]}" ${lto_parts[@]+"${lto_parts[@]}"} \
|
|
642
673
|
"${gc[@]}" ${strip_flag[@]+"${strip_flag[@]}"} "${inputs[@]}" -lm -o "$out" ;;
|
|
643
674
|
wasm)
|
|
675
|
+
# A region's chunks come from malloc, and the freestanding wasm runtime
|
|
676
|
+
# (runtime-wasm.c) has none, so IR that makes or enters one is refused here
|
|
677
|
+
# rather than failing at the link on an undefined `nish_region_*`. The wasi
|
|
678
|
+
# profile links runtime-region.c with the rest of the runtime.
|
|
679
|
+
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
680
|
+
case "$i" in
|
|
681
|
+
*.ll) if grep -q "@nish_region_" "$i"; then
|
|
682
|
+
echo "error: $i uses regions, which the wasm profile cannot link (it has no malloc); build it with --profile wasi" >&2
|
|
683
|
+
exit 2
|
|
684
|
+
fi ;;
|
|
685
|
+
esac
|
|
686
|
+
done
|
|
644
687
|
# clang resolves wasm-ld next to its own binary first, then on PATH; ask it
|
|
645
688
|
# rather than probing PATH so a Homebrew llvm without a PATH entry still works.
|
|
646
689
|
wasm_ld=$("$CC" -print-prog-name=wasm-ld)
|
package/std/README.md
CHANGED
|
@@ -42,7 +42,9 @@ whatever program imports it, and subject to the same rules as `examples/` or
|
|
|
42
42
|
| [`term-pixels.ts`](./term-pixels.ts) | a `Framebuffer` of `0xRRGGBB` pixels and the encoders that draw it with the backend the terminal has: `kittyFrame` (kitty graphics, one image id retransmitted each frame), `sixelFrame` (a fixed palette, optionally dithered), `drawHalfBlocks` onto a `Canvas` and `drawAscii` onto a `Screen`, each into the `PixelRect` `fitFramebuffer` letterboxes in the cells; `scaleFramebuffer` shrinks a frame by the controller's divisor. `PixelBytes` keeps the encoders' output and working arrays, so frame after frame allocates nothing. Pure (`tests/link/std_term_pixels`, `std_term_pixels_f64`; [guide](../docs/guide/using-nish/standard-library.md#nishterm-pixels)) |
|
|
43
43
|
| [`term-io.ts`](./term-io.ts) | the term module that does the I/O. `runTerminal(options, state, update, render)` is a whole program loop: it opens the terminal, steps `update` at a fixed rate, calls `render` with a `Frame` (the `Screen`, the `Framebuffer`, the step `alpha`, `TermCaps` and `TermStats`) whenever the controller lets a frame go, and closes when `update` answers false or the user presses Ctrl-C. `openTerminal(options)` answers a `Terminal` for a program's own loop (`readEvents`, `present`, `stats`, `close`). Opening refuses a pipe, enters raw mode and the alternate screen, probes the terminal and turns on the kitty keyboard, mouse, paste and focus reports the options ask for, with the runtime's restore string set before each so a panic or signal leaves the terminal as it was; `present` sends the frame in the best backend the controller allows as one write and acknowledges it with DSR 5. Reaches `tty` (`tests/term-io/relay.mjs` drives it in a pseudo-terminal from `tests/run.js`'s `term-io` section; `tests/link/std_term_io_deny`; [guide](../docs/guide/using-nish/terminal.md)) |
|
|
44
44
|
| [`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 |
|
|
45
|
-
| [`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. `scope()`, bound by `using s = scope()`, runs unlike tasks, each on a thread of its own, and joins them at every exit of the block; `s.spawn(entry, arg, dst, at)` stores a task's answer, and `s.spawn(entry, arg)` and `s.spawn(() => ...)`, whose arrow reads the function's `const` locals, run one that answers `void` (`spawnWith` and `spawnThunk` are the methods the compiler reads them as, and under Node `spawn` itself tells them apart) ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#scoped-tasks-using-s--scope)). `Mutex<T>` is a lock that owns its data: the value is reachable only through the guard `using g = m.lock()` binds, which releases it at every exit of the block, so a scope's tasks can share and update one value without a data race or a deadlock ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#a-lock-that-owns-its-data-mutext); `tests/link/thread_mutex_*`, and `examples/histogram.ts` measured). `Channel<T>` carries scalars to one receiver, a task of the scope or the parent after the join, from the scope's sender tasks and from the parent before the block that spawns them, and closes when every sending task has returned ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#channels-channelt); [`docs/wp29-thread-surface.md`](../docs/wp29-thread-surface.md#44-stage-p3--channels-of-scalars) §4.4). Every body in the module is its meaning under Node, where a task runs at its `spawn` |
|
|
45
|
+
| [`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. `scope()`, bound by `using s = scope()`, runs unlike tasks, each on a thread of its own, and joins them at every exit of the block; `s.spawn(entry, arg, dst, at)` stores a task's answer, and `s.spawn(entry, arg)` and `s.spawn(() => ...)`, whose arrow reads the function's `const` locals, run one that answers `void` (`spawnWith` and `spawnThunk` are the methods the compiler reads them as, and under Node `spawn` itself tells them apart) ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#scoped-tasks-using-s--scope)). `scopeWithStack(bytes)` is a scope whose tasks each run on a new thread with at least `bytes` of stack, for a task that recurses deeper than a thread's default allows; under Node and without threads the size is checked and otherwise ignored (`tests/link/thread_stack_deep`). `Mutex<T>` is a lock that owns its data: the value is reachable only through the guard `using g = m.lock()` binds, which releases it at every exit of the block, so a scope's tasks can share and update one value without a data race or a deadlock ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#a-lock-that-owns-its-data-mutext); `tests/link/thread_mutex_*`, and `examples/histogram.ts` measured). `Channel<T>` carries scalars to one receiver, a task of the scope or the parent after the join, from the scope's sender tasks and from the parent before the block that spawns them, and closes when every sending task has returned ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#channels-channelt); [`docs/wp29-thread-surface.md`](../docs/wp29-thread-surface.md#44-stage-p3--channels-of-scalars) §4.4). Every body in the module is its meaning under Node, where a task runs at its `spawn` |
|
|
46
|
+
| [`sync/shm.ts`](./sync/shm.ts) | `createRegion(name, bytes)`, `openRegion(name)` and `unlinkRegion(name)`: a named region of memory that several processes of one user map at once, through `runtime/runtime-shm.c`. A `SharedRegion` is read and written through checked slots, `loadI32` to `compareExchangeI64` at an ordering the call names (`RELAXED` to `SEQ_CST`), and byte windows (`read`, `write`); an offset outside the region or not a multiple of the slot's width stops the program. `waitI32` and `wake` sleep and wake on a slot across processes (Linux futex; `SHM_UNSUPPORTED` elsewhere). Native only, and it reaches `ffi` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#shared-memory-between-processes-nishsyncshm-and-nishsyncring); [`docs/wp47-shared-memory.md`](../docs/wp47-shared-memory.md); [`docs/security/shm.md`](../docs/security/shm.md)) |
|
|
47
|
+
| [`sync/ring.ts`](./sync/ring.ts) | Lock-free rings of byte records in a `SharedRegion`: `SpscRing` (one producer, one consumer, records of any length) and `MpscRing` (many producers, one consumer, fixed slots), made with `initSpscRing` / `initMpscRing` and found from another process with `attachSpscRing` / `attachMpscRing`. `tryPush`, `tryPop` and `popBatch` never block; a ring made with a doorbell adds `popWait`, which sleeps on a futex that a push rings only when the consumer is asleep. A length a peer wrote that the ring cannot hold stops the program as corrupt. `tests/link/sync_ring`, `examples/prefork-jobs.ts` and `bench/ring-latency/` are its programs |
|
|
46
48
|
|
|
47
49
|
## `nish/crypto` — the primitives under TLS 1.3
|
|
48
50
|
|
|
@@ -87,8 +89,9 @@ is listed in [`SECURITY.md`](../SECURITY.md#threat-model).
|
|
|
87
89
|
| --- | --- | --- |
|
|
88
90
|
| [`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. Natively, on a CPU with SHA-NI (and SSE4.1 and SSSE3) or the ARMv8 SHA-256 instructions, the compiler sends each block's compression to the runtime's kernel instead (`runtime/runtime-sha256.c`, WP38), and `NISH_SHA256=soft` pins the portable code | FIPS 180-4 §6.2 |
|
|
89
91
|
| [`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 |
|
|
90
|
-
| [`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. `pbkdf2HmacSha256(password, salt, iterations, dkLen)` is PBKDF2 with HMAC-SHA-256, for SCRAM-SHA-256: the password goes in and the key comes out as a `Secret<u8[]>`, every intermediate is wiped, and fewer than one iteration or a `dkLen` below one answers `null` | RFC 2104, RFC 4231 §4, RFC 8018 §5.2 |
|
|
92
|
+
| [`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. `pbkdf2HmacSha256(password, salt, iterations, dkLen)` is PBKDF2 with HMAC-SHA-256, for SCRAM-SHA-256: the password goes in and the key comes out as a `Secret<u8[]>`, every intermediate is wiped, and fewer than one iteration or a `dkLen` below one answers `null`. `HmacKey(hash, key)` keys the inner and outer pads of HMAC-SHA-256, -384 or -512 once, from a `Secret<u8[]>`, and holds them as a `Secret`; `hmacKeyedInto(key, data, off, len, out, at)` copies that keyed state per message and writes the tag, and `hmacKeyedVerify` compares a received tag window in constant time, so a loop of them allocates nothing past its arena block; `HmacKey.wipe()` zeroes the pads | RFC 2104, RFC 4231 §4, RFC 8018 §5.2 |
|
|
91
93
|
| [`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. A key held as a `Secret<u8[]>` (`nish:secret`) goes through the `*Secret` twins, which read it only inside `exposeWith` and answer a `Secret`: `hkdfSha256Secret` / `hkdfSha384Secret`, each step's, and `hkdfExpandLabelSha256Secret(prk, label, context, length)` / `hkdfExpandLabelSha384Secret`, which panic where HKDF-Expand-Label does. A destination already held as a `Secret` is derived into in place by `hkdfExpandLabelSha256Held(dst, prk, label, context)` / `hkdfExpandLabelSha384Held`, as many bytes as `dst` holds, the plain output copied in inside `exposeWith` and wiped; `dst` may be `prk`. `hkdfExpandLabelHeldInto(hashLength, prk, label, context, out, at, length)` writes a plain window from a held PRK, for output that is plain by design, a record key or IV | RFC 5869 §2, Appendix A; RFC 8448 §3 and RFC 9001 A.1 for the label |
|
|
94
|
+
| [`crypto/jwt.ts`](./crypto/jwt.ts) | JSON Web Tokens in the JWS compact form. `JwtVerifier(maxToken)` holds its buffers, sized once, and `verify` / `verifyString(key, token, now)` answer `JWT_OK` or a `JWT_ERR_*` code: the key decides the algorithm (`alg` must name it exactly, so `none` and an HS header in front of an RSA key are refused), the header holds `alg` once and no `crit` or `b64`, base64url is strict, the signature is checked before the payload is read, and then `exp`, `nbf` and `iat` with a leeway, and `iss`, `aud` and `sub`, are checked. Keys: `jwtHmacKey` (HS256/384/512, keyed once through `HmacKey`), `jwtRsaKey` / `jwtRsaKeyParts` (RS256/384/512, 2048 bits or more), `jwtEcKey` (ES256, ES384), `jwtEd25519Key` (EdDSA), and the signing `jwtEs256SigningKey` / `jwtEd25519SigningKey`, each from a `Secret<u8[]>` it copies; `JwtKey.wipe()` zeroes the copy. `jwtSign` / `jwtSignString` / `jwtSignKid` mint a token under an HS, ES256 or EdDSA key. `peekKid` reads the unauthenticated `kid`, to choose a key | RFC 7515 Appendix A, RFC 7519, RFC 7518, RFC 8037 A.4, RFC 8725 |
|
|
92
95
|
| [`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 | — |
|
|
93
96
|
| [`crypto/bcrypt.ts`](./crypto/bcrypt.ts) | `bcryptHash(password, salt, cost)`, the 60-character `$2b$` hash of a `Secret<u8[]>` password under a 16-byte salt and a cost of 4 to 31, and `bcryptVerify(password, hash)`, which reads `$2a$`, `$2b$` and `$2y$` hashes, refuses `$2x$`, and compares in constant time. The first 72 bytes of a password count, up to its first NUL, as under libxcrypt's `crypt_r` (where other implementations differ: [the interoperability table](../docs/security/crypto-bcrypt.md#interoperability)); the Blowfish state is wiped before either returns. `bcryptHash4(p0, p1, p2, p3, salts, cost)` and `bcryptVerify4(p0, p1, p2, p3, hashes)` do up to four at once, interleaved (four in the time of about 1.2 single hashes on one core), each lane the single path's answer; verify groups lanes by cost, and a malformed lane makes the whole call an `Err` naming it. `BCRYPT_MAX_PASSWORD`, `BCRYPT_HASH_LENGTH`, `BCRYPT_SALT_SIZE`, `BCRYPT_MIN_COST`, `BCRYPT_MAX_COST`. A `std/threads` task may call all four ([BCRYPT-9](../docs/security/crypto-bcrypt.md#findings)) | OpenBSD `bcrypt(3)`; crypt_blowfish 1.3 for `$2a$` and `$2y$` |
|
|
94
97
|
| [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded; `base64StdPadEncode(data)` and `base64StdPadDecode(text)`, the padded standard alphabet that SCRAM and PEM use, constant-time in both directions. These are not `nish/base64`, which encodes only, is not constant-time and is for pixels; the names differ because std modules share the program's one namespace. Decoding is strict, so every byte string has one spelling. `base64urlDecode` answers `null` for any `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits; `base64StdPadDecode` requires the padding, answering `null` unless the text is whole four-character groups with `=` only as the last one or two characters, every other character in the alphabet and the unused bits zero. Both refuse a text longer than 2^31 − 1 bytes, and `base64urlEncode` panics on such an array | RFC 4648 §4, §5, §10 (`tests/link/std_base64_padded`) |
|
|
@@ -231,6 +234,7 @@ reads one, and it still takes its randomness as an argument.
|
|
|
231
234
|
| [`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`, and the first takes a date's bytes to send as a `Date` field on a final response, unless the caller's fields hold one | 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`) |
|
|
232
235
|
| [`net/http1-client.ts`](./net/http1-client.ts) | The client side of HTTP/1.1, sans-IO. The writer is `http1RequestHead(method, target, names, values, bodyLength)`, `Host` first and the framing field added for `bodyLength` (`Content-Length`, `HTTP1_CHUNKED` or `HTTP1_NO_BODY`), and `http1WriteRequestHead` into a caller's buffer, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED` with nothing written; it refuses a method that is not a token, a target that is not a form the method may send, a CR, LF or control byte in a value, not exactly one `Host`, the caller's own framing fields and a `Connection: upgrade`; `http1UpgradeRequestHead(method, target, names, values, upgrade)` writes `Upgrade` and `Connection: Upgrade` itself, with no body. `Http1ResponseParser(maxStatusLine, maxHeaderBytes, maxHeaders, maxBody, maxTrailerBytes)` is told what was asked (`expect(head, connect)`, then `expectUpgrade()` for a request that asked to switch protocols), fed any slice, and `next()` answers `HTTP1_INFORMATIONAL`, `HTTP1_HEAD` (`status`, `reason`, `minor`, lowercased `names` beside `values`, `header(name)`, `contentLength`), `HTTP1_BODY` (a window, de-chunked), `HTTP1_END` with `keepAlive()`, or the final `HTTP1_UPGRADE` (a 2xx to CONNECT or a 101 asked for, `tunnelBytes()`), `HTTP1_CLOSED` and `HTTP1_ERROR`; `finish()` reports the close that ends a body read until close, and anywhere else is `HTTP1_E_TRUNCATED`. Every smuggling shape is refused with its own `HTTP1_E_*` code, as is a 101 not asked for, more than `HTTP1_MAX_INFORMATIONAL` interim responses and a chunk-size line over `HTTP1_MAX_CHUNK_LINE`. Record: [`docs/security/http1-client.md`](../docs/security/http1-client.md) | RFC 9112 §4–§7 and §9.3, RFC 9110 §9.3.6 and §15.2; a corpus fed whole, cut at every byte and a byte at a time, every refusal and limit at its edge, and the server parser reading what the writer writes (`tests/link/net_http1_client`, `_f64`, `_feed_window`) |
|
|
233
236
|
| [`net/http-client.ts`](./net/http-client.ts) | A blocking HTTP/1.1 client for command-line programs: `httpFetch(request, options)` looks the host up, connects, sends an `HttpRequest` (`method`, `url`, `header(name, value)`, `body`) and answers an `HttpResponse` (`status`, `reason`, `url`, `header(name)`) once its head is in, its body read through `body.read` or `body.readAll(limit)`; null on a failure. `HttpClient(options)` does the same and keeps one connection alive between fetches, sending an idempotent request once more when a kept connection turns out to be gone, and says why a fetch failed in `error` (an `HTTP_E_*` code, `httpErrorName`) and `errorText`. `HttpOptions` bounds every wait (`connectTimeoutMs`, `readTimeoutMs`, `totalTimeoutMs`) and every body (`maxBodyBytes`), follows redirects only when asked (`followRedirects`, `maxRedirects`, `crossOriginRedirects`, never https to http, credentials dropped across origins), and reads `http_proxy`, `https_proxy` and `no_proxy` when `useEnvProxy` is set (`HTTP_PROXY` ignored under CGI; https through `CONNECT`). Every byte goes through `nish/net/http1-client`. Native only. Record: [`docs/security/http-client.md`](../docs/security/http-client.md) | RFC 9110 §9.2.2 and §15.4, RFC 9112; a loopback server and proxy in the test itself: keep-alive, retries, redirects, proxies, timeouts and body caps (`tests/link/net_http_fetch`) |
|
|
237
|
+
| [`net/outbound.ts`](./net/outbound.ts) | The policy for fetching URLs your program did not choose, set as `HttpOptions.outbound`: `OutboundPolicy` reaches only public addresses (no loopback, private, link-local, CGNAT, metadata or other special-purpose range, IPv4-mapped, NAT64 and 6to4 forms judged as the IPv4 address they carry), checked on the resolved address the client then connects to, so DNS rebinding cannot move it; every redirect hop is checked again, the proxy is not used, and redirects, total time and body size are capped. `permit(cidr)` lets a trusted prefix through, `allowHost(name)` limits the names a URL or hop may carry (`HTTP_E_BLOCKED` before the lookup). Native only. Record: [`docs/security/outbound.md`](../docs/security/outbound.md) | RFC 6890, RFC 4291, RFC 6052, RFC 3056 and the IANA special-purpose address registries; every range at its edges and a loopback server fetched with and without a policy (`tests/link/net_outbound`) |
|
|
234
238
|
| [`net/tls/ticket.ts`](./net/tls/ticket.ts) | Session tickets a server seals for itself (RFC 8446 §4.6.1): `tlsTicketSeal` and `tlsTicketOpen` seal a `TlsTicketState` (suite, issue time, lifetime, chain start, server name, resumption PSK) under ChaCha20-Poly1305 with a borrowed `Secret<u8[]>` key, as key id ‖ nonce ‖ ciphertext ‖ tag, and wipe the plaintext on every path. `TlsTicketKeyInfo`, `tlsTicketKeyDraw` and `tlsTicketKeyDue` are a key's public record, its draw and when to replace it: a key seals for 12 h of awake time and 2^30 tickets, and opens for 14 h. `TlsTicketStrikes` is the bounded strike register (§8.1) that makes a ticket single-use, failing closed when full | `tests/link/net_tls_ticket`: every bound at its instant and 1 ns past it, a flipped bit in each region, three key rotations, replay and a full register |
|
|
235
239
|
| [`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`, `websocketOfferedProtocols` and `websocketUpgradeResponse`, the 101. The client's side is `websocketClientKey(nonce)` (a 16-byte nonce the caller draws; the module draws no randomness), `websocketUpgradeRequest(host, target, key, protocols)` and `websocketCheckResponse(parser, key, offered)` over an `Http1ResponseParser` told `expectUpgrade()`, answering a `WsHandshake` (`error`, a `WS_HS_*` code, and `protocol`) | 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 |
|
|
236
240
|
| [`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 and the deadline for a whole head, `headTimeout`, against a client that trickles its head). Both build a slot's connection on the first accept into it rather than at start-up. `Http1Server` reads a socket until a short read rather than until it would block, takes a free slot off a stack of released slots or the next never-used one rather than scanning for one, and answers `rearm(slot, wants)`, whether the interest changed and needs a `pollModify` at all: a keep-alive request is one `recv` and one `send`. The slots borrow their output from one `Http1OutputPool` while they write and give it back once sent, so a pool's memory follows the connections the socket is holding back, not its size: an idle slot holds no output buffer. Every final response carries a `Date` from `HttpDate`, which reads `Date.now()` and formats at most once a second, shared by a pool's slots; `pin(unixMs)` fixes it for a test, and a program's own `date` field replaces it. 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, the date formatted once a second, and every refusal (`tests/link/net_http1_server`); `curl` against its `serve` mode |
|
|
@@ -245,7 +249,11 @@ reads one, and it still takes its randomness as an argument.
|
|
|
245
249
|
| [`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`) |
|
|
246
250
|
| [`net/http-compress.ts`](./net/http-compress.ts) | Content coding for HTTP responses: `httpAcceptEncoding(buf, start, end, offered)` reads an `Accept-Encoding` value (q-values, `*`, `identity`, `x-gzip`) into `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`, `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, allocating nothing; `HttpCompressor(level, minLength, maxLength)` gzips a body into its own buffer when it is worth it (-1 to send it as it is) and builds the response fields (`addFields`: `Content-Encoding: gzip` and, while `vary`, `Vary: Accept-Encoding`). It sends gzip only when it pays: `minLength`, `minSaving` bytes or `minSavingPercent`, and `segmentMode` (fewer TCP segments of `mss`, head included, than as it is); `decision` says why (`HTTP_COMPRESS_SENT`, `_NOT_ACCEPTED`, `_TOO_SMALL`, `_TOO_LARGE`, `_TOO_LITTLE_SAVED`, `_NO_FEWER_SEGMENTS`). Bodies up to `fixedLimit` use the fixed-Huffman encoder (`encoder`: `HTTP_ENCODER_AUTO`, `_FIXED`, `_LEVEL`). On HTTP/1.1, `Http1Connection.acceptEncoding()` and `respondEncoded(status, names, values, body, off, len, enc, coding)` answer a request in one call with the compressed `Content-Length`, or nothing at all (`H1_AGAIN`, `H1_INVALID`); `identity;q=0` is honoured. Opt-in; BREACH is the program's to avoid (`docs/security/compress.md`) | RFC 9110 §8.4 and §12.5.3: the negotiation table, and sans-IO responses gunzipped back (`tests/link/std_http_compress`) |
|
|
247
251
|
| [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
|
|
252
|
+
| [`net/handler.ts`](./net/handler.ts) | `HttpHandler`, complete requests and their answers over the HTTP/1.1 carrier: it owns the readiness loop, routes each request through a `Router` at its head, gathers its body up to `HandlerLimits.http.maxBody` (1 MiB; past it the carrier answers 413), and hands it over whole. The program pulls — `wait(ms)`, `ready()`, `take()` — and switches on `req.route`; `HttpRequest` reads parameters, form-decoded query values and fields as ranges into its own buffers. Each request is answered once, now or in a later turn, by `send`, `sendFields`, `empty`, `json()` + `sendJson`, `problem` (RFC 9457), `refuse` (404, or 405 with `Allow`) or `start`/`write`/`end`; an answer whose connection closed first is `HANDLER_DROPPED` and reaches no later peer. `HEAD` takes a `GET` route. `watch`/`fired` add the program's own descriptors (`signalFd()`), and `drain` stops gracefully. Nothing is allocated per request once warm (`docs/security/handler.md`) | Routing, parameters and form-decoded queries, `HEAD`, 404 and 405 problems, chunked bodies in and out, the body cap at and one past it, pipelining order, late answers dropped across a reused slot, the arena and pool flat over two thousand requests, and the drain (`tests/link/net_handler`, and `net_http_app` over `examples/http-app`) |
|
|
253
|
+
| [`net/upstream.ts`](./net/upstream.ts) | `Upstream`, outbound HTTP/1.1 calls from a route handler that never block it: a pool of keep-alive connections to one `http:` origin on the `HttpHandler`'s own loop (watch ids `UpstreamLimits.firstId` and up). `get(req, path)` returns at once, or `UPSTREAM_E_FULL` with nothing held; each call comes back once through `ready()`/`take()` tagged with `r.req`, `ok` with its status and `body[0 .. length)`, or failed with the codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code (timeout, closed, connect, origin, lookup, blocked, request), and goes back with `release`. The origin is looked up once at start-up and pinned; `UpstreamLimits.outbound` checks it against an `OutboundPolicy`. A failed call's connection is closed. Each connection keeps one codec in spans mode, so a warm pool allocates nothing per call | A slow upstream while other clients and a second upstream are answered, the timeout, keep-alive and tagging, five codec refusals as failures, the policy refusing loopback, origin and request-target refusals, three thousand warm calls on one connection with `Arena.used()` flat (`tests/link/net_upstream`), and a second `release` panicking (`net_upstream_release`) |
|
|
248
254
|
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`, and a one-dimensional `int2[]`, `int4[]` or `int8[]` by `PgBinaryArray`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first (read with `binaryInt` when `PgBatchConfig.binary` is set, so the batch's rows come in binary too), to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. `PgBatchClock` says when to take, from times the program passes in: once max(`minCount`, the moving average of batch sizes × `factor`) wait, the oldest has waited `maxWaitUs`, or, with the idle bypass, nothing is in flight and the load is light (its defaults are the fixed window); `PgBatcher.takeDue` takes only then, and `PgBatchTurns` lets one free connection collect while the others execute. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`); the clock's reasons, backlog and turns (`tests/link/std_postgres_batch_clock`) |
|
|
255
|
+
| [`net/postgres-carrier.ts`](./net/postgres-carrier.ts) | `PgCarrier`, a `PgPool`'s queries on an `HttpHandler`'s loop: its sockets sit in the handler's epoll set, a route handler hands a request to `run` (or `runInt` / `runText` for one of up to 8 attached `PgBatcher`s) and gets its number at once, and `next()` answers `PG_CARRIER_BUILD` to bind a plain query on `connection()`, `PG_CARRIER_ROW`, `PG_CARRIER_COMPLETE`, `PG_CARRIER_ERROR` and one final `PG_CARRIER_DONE`, `PG_CARRIER_MISSING` or `PG_CARRIER_FAILED` (`failure`, `socketError`) with `request()` the request to answer and `tag` the caller's. It connects the pool's slots itself (TCP or a Unix socket), times out a connect after `connectTimeoutMs`, sheds batched keys when no slot can connect, and sizes every table from `PgCarrierLimits` at construction: `run` answers `PG_CARRIER_FULL` rather than grow one, and once connections are up it allocates nothing per query. Kept apart from `net/postgres.ts` so that module reaches no capability (`docs/security/postgres.md`) | A loopback backend in the test itself: routing, a slow query not stalling another client, lost and refused connections, a connect timeout, and `Arena.used()` flat over thousands of queries (`tests/link/std_postgres_carrier`); a live server by `tests/postgres/live/run.mjs` |
|
|
256
|
+
| [`net/prefork.ts`](./net/prefork.ts) | A server on every core: `preforkWorkers(process.argv)` or `preforkWith(process.argv, config)` answers a worker's index in each worker it starts again from the same program, and in the supervisor runs them until SIGTERM or SIGINT, restarting one that exits after a backoff. `PreforkConfig.supervisorChannels` gives each worker a `unixSocketPair` to the supervisor, which `preforkSupervise(args, config, state, onChannel)` hands to a callback; `serviceWorker` gives every other worker a pair to that one, and makes the workers one group that restarts together on fresh pairs. The ends arrive from descriptor 3 through `spawnWithFds`, found with `preforkSupervisorChannel`, `preforkServiceChannel` and `preforkServiceChannelOf`. Native only | `tests/link/net_prefork` (the spread, a dead worker, SIGTERM, the backoff); `tests/link/net_prefork_channels` (each worker holds exactly its own ends, a restart, the supervisor's descriptors flat) |
|
|
249
257
|
|
|
250
258
|
**How a carrier drives `TlsServer`.** The server speaks in handshake
|
|
251
259
|
*messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
|
|
@@ -439,7 +447,8 @@ that are *not* this package.
|
|
|
439
447
|
exempt — `NEWLINE` and `SPACE` fold at their uses.
|
|
440
448
|
- **It is an Nish program**, so the constraints are the language's: a function is
|
|
441
449
|
an arrow bound to a module-level `const`, a function is never a value, there
|
|
442
|
-
is no `try` / `catch`, and there are no optional
|
|
450
|
+
is no `try` / `catch`, and there are no optional parameters (a default is a
|
|
451
|
+
constant or an earlier parameter only). Those
|
|
443
452
|
three are what shape an API here more than any style preference — see the
|
|
444
453
|
header of `testing.ts` for what they did to that one, which was written before
|
|
445
454
|
WP18 added generic functions and classes
|
package/std/collections.ts
CHANGED
|
@@ -54,11 +54,11 @@ const INITIAL_SLOTS: i32 = 8
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* The key's hash, never 0: SipHash-1-3 of a string's bytes under the process's
|
|
57
|
-
* random key, folded to 32 bits (docs/security/json.md, JSON-13),
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* CC0; MurmurHash3
|
|
57
|
+
* random key, folded to 32 bits (docs/security/json.md, JSON-13); under a 2nd,
|
|
58
|
+
* multiply-add-shift for an int of 32 bits or fewer, a boolean or an enum, a
|
|
59
|
+
* folded 128-bit multiply for a 64-bit int; `fmix64` folded to 32 bits for a
|
|
60
|
+
* float (normalised first, so -0 and +0, and every NaN, hash alike) and a class
|
|
61
|
+
* address (§5.2). SipHash's reference is CC0; MurmurHash3 is public domain.
|
|
62
62
|
*
|
|
63
63
|
* The body is never emitted. Every call is lowered in place, per key type, by
|
|
64
64
|
* `src/emit-map.ts`; the constant is what the checker and the whole-program
|
package/std/crypto/hmac.ts
CHANGED
|
@@ -46,6 +46,13 @@
|
|
|
46
46
|
* stores nothing of its own and an arena scope around it releases all it
|
|
47
47
|
* allocated (see "Caller-owned scratch" below).
|
|
48
48
|
*
|
|
49
|
+
* `HmacKey` keys a `Secret<u8[]>` once, for a key MACed with many times (a
|
|
50
|
+
* JWT verifier's HS key): it keeps the inner and outer midstates (RFC 2104
|
|
51
|
+
* §4) as a `Secret<u64[]>`, and `hmacKeyedVerify` / `hmacKeyedInto` copy them
|
|
52
|
+
* into the key's own hashers per message instead of absorbing the key again.
|
|
53
|
+
* Each call wipes the copy, the hashers and the inner digest before it
|
|
54
|
+
* answers; `HmacKey.wipe()` zeroes the pads (see "A key held keyed" below).
|
|
55
|
+
*
|
|
49
56
|
* `pbkdf2HmacSha256` is PBKDF2 with HMAC-SHA-256 (RFC 8018 §5.2), the key
|
|
50
57
|
* derivation SCRAM-SHA-256 runs over a password: the password goes in as a
|
|
51
58
|
* `Secret<u8[]>` and the derived key comes out as one, and every U_j and T_i
|
|
@@ -55,8 +62,8 @@
|
|
|
55
62
|
*/
|
|
56
63
|
import { SHA256_BLOCK, SHA256_SIZE, Sha256 } from "nish/crypto/sha256"
|
|
57
64
|
import { SHA384_SIZE, SHA512_BLOCK, SHA512_SIZE, Sha384, Sha512 } from "nish/crypto/sha512"
|
|
58
|
-
import { timingSafeEqual } from "nish/crypto/ct"
|
|
59
|
-
import { Secret, exposeWith, secret, wipe } from "nish:secret"
|
|
65
|
+
import { timingSafeEqual, timingSafeEqualAt } from "nish/crypto/ct"
|
|
66
|
+
import { Secret, expose, exposeWith, secret, wipe } from "nish:secret"
|
|
60
67
|
|
|
61
68
|
/** RFC 2104 §2's `ipad`, the byte XORed into the key for the inner hash. */
|
|
62
69
|
const HMAC_IPAD: i32 = 0x36
|
|
@@ -797,3 +804,299 @@ export const pbkdf2HmacSha256 = (
|
|
|
797
804
|
}
|
|
798
805
|
return secret(dk)
|
|
799
806
|
}
|
|
807
|
+
|
|
808
|
+
// ---- A key held keyed: the pads absorbed once, as a Secret --------------------
|
|
809
|
+
//
|
|
810
|
+
// A verifier that checks many tags under one key — a JWT's HS256 signature on
|
|
811
|
+
// every request — pays for the key's two pad blocks once, not once per tag.
|
|
812
|
+
// RFC 2104 §4 notes the two keyed chaining values can be kept and the
|
|
813
|
+
// computation started from them; `HmacKey` keeps exactly those, and keeps them
|
|
814
|
+
// as a `Secret`, because they are key material: either one, with the hash's
|
|
815
|
+
// length padding, MACs any message under the key without the key itself.
|
|
816
|
+
//
|
|
817
|
+
// A function `exposeWith` runs may not write through the argument it is
|
|
818
|
+
// handed (NL2450), and a hasher made per tag stores its arrays in its fields,
|
|
819
|
+
// which no arena block may release around (NL2424). So each tag copies the
|
|
820
|
+
// midstates out with `expose`, resumes the two hashers the key owns from the
|
|
821
|
+
// copy, wipes the copy, feeds and finishes them and wipes them; what it
|
|
822
|
+
// allocates — the copy and the two digests — dies inside the arena block of
|
|
823
|
+
// `hmacKeyedInto` and `hmacKeyedVerify`. That is the shape
|
|
824
|
+
// `p256SignSha256Into` has (`std/crypto/p256.ts`): a plain copy of a secret
|
|
825
|
+
// step's answer, used in caller-owned scratch and wiped before return.
|
|
826
|
+
|
|
827
|
+
/** `HmacKey`'s hash: HMAC-SHA-256. */
|
|
828
|
+
export const HMAC_SHA256: i32 = 1
|
|
829
|
+
/** `HmacKey`'s hash: HMAC-SHA-384. */
|
|
830
|
+
export const HMAC_SHA384: i32 = 2
|
|
831
|
+
/** `HmacKey`'s hash: HMAC-SHA-512. */
|
|
832
|
+
export const HMAC_SHA512: i32 = 3
|
|
833
|
+
|
|
834
|
+
/** Words of one chaining value: eight, for SHA-256's `u32` and SHA-512's `u64` alike. */
|
|
835
|
+
const HMAC_WORDS: i32 = 8
|
|
836
|
+
|
|
837
|
+
/** The tag length of `hash` in bytes, 0 for no hash `HmacKey` knows. */
|
|
838
|
+
const hmacTagSize = (hash: i32): i32 => {
|
|
839
|
+
if (hash === HMAC_SHA256) {
|
|
840
|
+
return SHA256_SIZE
|
|
841
|
+
}
|
|
842
|
+
if (hash === HMAC_SHA384) {
|
|
843
|
+
return SHA384_SIZE
|
|
844
|
+
}
|
|
845
|
+
if (hash === HMAC_SHA512) {
|
|
846
|
+
return SHA512_SIZE
|
|
847
|
+
}
|
|
848
|
+
return 0
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* The inner and outer chaining values after the key's pad blocks, for
|
|
853
|
+
* `exposeWith` to run on the plain key: words 0 to 7 the inner, 8 to 15 the
|
|
854
|
+
* outer, SHA-256's `u32` words widened to `u64`. The two keying hashers are
|
|
855
|
+
* wiped before it answers.
|
|
856
|
+
*/
|
|
857
|
+
const hmacMidstates = (key: u8[], hash: i32): u64[] => {
|
|
858
|
+
const out: u64[] = new Array<u64>(2 * HMAC_WORDS)
|
|
859
|
+
if (hash === HMAC_SHA256) {
|
|
860
|
+
const inner = new Sha256()
|
|
861
|
+
const outer = new Sha256()
|
|
862
|
+
hmacKeySha256(inner, outer, key)
|
|
863
|
+
for (
|
|
864
|
+
let i: i32 = 0;
|
|
865
|
+
i < HMAC_WORDS && i < toI32(inner.state.length) && i < toI32(outer.state.length);
|
|
866
|
+
i += 1
|
|
867
|
+
) {
|
|
868
|
+
const j: i32 = i + HMAC_WORDS
|
|
869
|
+
if (i < toI32(out.length) && j < toI32(out.length)) {
|
|
870
|
+
out[i] = toU64(inner.state[i])
|
|
871
|
+
out[j] = toU64(outer.state[i])
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
hmacWipeSha256(inner)
|
|
875
|
+
hmacWipeSha256(outer)
|
|
876
|
+
} else if (hash === HMAC_SHA384) {
|
|
877
|
+
const inner = new Sha384()
|
|
878
|
+
const outer = new Sha384()
|
|
879
|
+
hmacKeySha384(inner, outer, key)
|
|
880
|
+
hmacMidstates512(out, inner.engine.state, outer.engine.state)
|
|
881
|
+
hmacWipeSha384(inner)
|
|
882
|
+
hmacWipeSha384(outer)
|
|
883
|
+
} else {
|
|
884
|
+
const inner = new Sha512()
|
|
885
|
+
const outer = new Sha512()
|
|
886
|
+
hmacKeySha512(inner, outer, key)
|
|
887
|
+
hmacMidstates512(out, inner.engine.state, outer.engine.state)
|
|
888
|
+
hmacWipeSha512(inner)
|
|
889
|
+
hmacWipeSha512(outer)
|
|
890
|
+
}
|
|
891
|
+
return out
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** `out[0 .. 8) = inner`, `out[8 .. 16) = outer`, for the SHA-512 family. */
|
|
895
|
+
const hmacMidstates512 = (out: u64[], inner: u64[], outer: u64[]): void => {
|
|
896
|
+
for (let i: i32 = 0; i < HMAC_WORDS && i < toI32(inner.length) && i < toI32(outer.length); i += 1) {
|
|
897
|
+
const j: i32 = i + HMAC_WORDS
|
|
898
|
+
if (i < toI32(out.length) && j < toI32(out.length)) {
|
|
899
|
+
out[i] = inner[i]
|
|
900
|
+
out[j] = outer[i]
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* An HMAC key absorbed once: the inner and outer chaining values after the
|
|
907
|
+
* key's pad blocks (RFC 2104 §4), held as a `Secret<u64[]>`. Every tag it
|
|
908
|
+
* makes or checks starts from them, so a tag costs the message's blocks and
|
|
909
|
+
* two finishing blocks, and never the key's two again.
|
|
910
|
+
*
|
|
911
|
+
* const key = new HmacKey(HMAC_SHA256, secretKey); // once
|
|
912
|
+
* hmacKeyedInto(key, data, 0, data.length, tag, 0); // per message
|
|
913
|
+
* const ok: boolean = hmacKeyedVerify(key, data, 0, data.length, tag, 0);
|
|
914
|
+
* key.wipe(); // when done
|
|
915
|
+
*
|
|
916
|
+
* The tags are `hmacSha256`'s, `hmacSha384`'s and `hmacSha512`'s for the same
|
|
917
|
+
* key. The key itself is read only inside `exposeWith`, and the hashers that
|
|
918
|
+
* keyed it are wiped; the caller still owns and wipes the `Secret` it passed.
|
|
919
|
+
* An unknown `hash` panics.
|
|
920
|
+
*
|
|
921
|
+
* The key also owns two hashers of its family, made once, that each tag is
|
|
922
|
+
* computed in and that are wiped before the tag call returns. One key is
|
|
923
|
+
* therefore used by one computation at a time, as a `Sha256` is.
|
|
924
|
+
*/
|
|
925
|
+
export class HmacKey {
|
|
926
|
+
/** Words 0 to 7: the keyed inner chaining value; 8 to 15: the keyed outer one. */
|
|
927
|
+
readonly pads: Secret<u64[]>
|
|
928
|
+
/** SHA-256's work hashers; idle and wiped between tags. */
|
|
929
|
+
readonly inner256: Sha256
|
|
930
|
+
readonly outer256: Sha256
|
|
931
|
+
/**
|
|
932
|
+
* SHA-384's and SHA-512's work hashers: SHA-384 is SHA-512's compression
|
|
933
|
+
* from another start, cut to 48 bytes, so resumed from a chaining value the
|
|
934
|
+
* two are one hasher and differ only in the digest length taken.
|
|
935
|
+
*/
|
|
936
|
+
readonly inner512: Sha512
|
|
937
|
+
readonly outer512: Sha512
|
|
938
|
+
/** `HMAC_SHA256`, `HMAC_SHA384` or `HMAC_SHA512`. */
|
|
939
|
+
readonly hash: i32
|
|
940
|
+
/**
|
|
941
|
+
* Whether the key still MACs: `wipe()` clears it. A wiped key's chaining
|
|
942
|
+
* values are zeros anyone can resume from, so a tag under them proves
|
|
943
|
+
* nothing and none is computed.
|
|
944
|
+
*/
|
|
945
|
+
live: boolean
|
|
946
|
+
|
|
947
|
+
constructor(hash: i32, key: Secret<u8[]>) {
|
|
948
|
+
if (hmacTagSize(hash) === 0) {
|
|
949
|
+
panic("HmacKey: an unknown hash")
|
|
950
|
+
}
|
|
951
|
+
this.hash = hash
|
|
952
|
+
this.pads = secret(exposeWith(key, hash, hmacMidstates))
|
|
953
|
+
this.inner256 = new Sha256()
|
|
954
|
+
this.outer256 = new Sha256()
|
|
955
|
+
this.inner512 = new Sha512()
|
|
956
|
+
this.outer512 = new Sha512()
|
|
957
|
+
hmacWipeSha256(this.inner256)
|
|
958
|
+
hmacWipeSha256(this.outer256)
|
|
959
|
+
hmacWipeSha512(this.inner512)
|
|
960
|
+
hmacWipeSha512(this.outer512)
|
|
961
|
+
this.live = true
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** The tag length in bytes: 32, 48 or 64. */
|
|
965
|
+
size(): i32 {
|
|
966
|
+
return hmacTagSize(this.hash)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** Zero both keyed chaining values and the work hashers; the key MACs nothing after. */
|
|
970
|
+
wipe(): void {
|
|
971
|
+
this.live = false
|
|
972
|
+
wipe(this.pads)
|
|
973
|
+
hmacWipeSha256(this.inner256)
|
|
974
|
+
hmacWipeSha256(this.outer256)
|
|
975
|
+
hmacWipeSha512(this.inner512)
|
|
976
|
+
hmacWipeSha512(this.outer512)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** A plain copy of the 16 midstate words, for `expose` to run; the caller wipes it. */
|
|
981
|
+
const hmacCopyWords = (pads: u64[]): u64[] => {
|
|
982
|
+
const out: u64[] = new Array<u64>(2 * HMAC_WORDS)
|
|
983
|
+
for (let i: i32 = 0; i < toI32(out.length) && i < toI32(pads.length); i += 1) {
|
|
984
|
+
out[i] = pads[i]
|
|
985
|
+
}
|
|
986
|
+
return out
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** `h` started again at the point after one keyed block, chaining value `mid[at .. at + 8)`. */
|
|
990
|
+
const hmacResume256 = (h: Sha256, mid: u64[], at: i32): void => {
|
|
991
|
+
hmacWipeSha256(h)
|
|
992
|
+
const state: u32[] = h.state
|
|
993
|
+
for (let i: i32 = 0; i < HMAC_WORDS && i < toI32(state.length) && at + i < toI32(mid.length); i += 1) {
|
|
994
|
+
state[i] = toU32(mid[at + i])
|
|
995
|
+
}
|
|
996
|
+
h.total = toI64(SHA256_BLOCK)
|
|
997
|
+
h.finished = false
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** `hmacResume256` for the SHA-512 family's hasher. */
|
|
1001
|
+
const hmacResume512 = (h: Sha512, mid: u64[], at: i32): void => {
|
|
1002
|
+
hmacWipeSha512(h)
|
|
1003
|
+
const state: u64[] = h.engine.state
|
|
1004
|
+
for (let i: i32 = 0; i < HMAC_WORDS && i < toI32(state.length) && at + i < toI32(mid.length); i += 1) {
|
|
1005
|
+
state[i] = mid[at + i]
|
|
1006
|
+
}
|
|
1007
|
+
h.engine.count = toU64(SHA512_BLOCK)
|
|
1008
|
+
h.engine.finished = false
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* The tag of `data[off .. off + len)` under `key`, as a fresh array the
|
|
1013
|
+
* caller wipes: the midstates copied out of their `Secret`, the key's two
|
|
1014
|
+
* hashers resumed from them, fed and finished, and the copy, the inner digest
|
|
1015
|
+
* and both hashers wiped before it answers.
|
|
1016
|
+
*/
|
|
1017
|
+
const hmacKeyedTag = (key: HmacKey, data: u8[], off: i32, len: i32): u8[] => {
|
|
1018
|
+
const mid: u64[] = expose(key.pads, hmacCopyWords)
|
|
1019
|
+
if (key.hash === HMAC_SHA256) {
|
|
1020
|
+
const inner: Sha256 = key.inner256
|
|
1021
|
+
const outer: Sha256 = key.outer256
|
|
1022
|
+
hmacResume256(inner, mid, 0)
|
|
1023
|
+
hmacResume256(outer, mid, HMAC_WORDS)
|
|
1024
|
+
wipe(mid)
|
|
1025
|
+
inner.update(data, off, len)
|
|
1026
|
+
const digest: u8[] = inner.digest()
|
|
1027
|
+
outer.update(digest, HMAC_FROM, SHA256_SIZE)
|
|
1028
|
+
const tag: u8[] = outer.digest()
|
|
1029
|
+
wipe(digest)
|
|
1030
|
+
hmacWipeSha256(inner)
|
|
1031
|
+
hmacWipeSha256(outer)
|
|
1032
|
+
return tag
|
|
1033
|
+
}
|
|
1034
|
+
const size: i32 = key.hash === HMAC_SHA384 ? SHA384_SIZE : SHA512_SIZE
|
|
1035
|
+
const inner: Sha512 = key.inner512
|
|
1036
|
+
const outer: Sha512 = key.outer512
|
|
1037
|
+
hmacResume512(inner, mid, 0)
|
|
1038
|
+
hmacResume512(outer, mid, HMAC_WORDS)
|
|
1039
|
+
wipe(mid)
|
|
1040
|
+
inner.update(data, off, len)
|
|
1041
|
+
const digest: u8[] = inner.digest()
|
|
1042
|
+
outer.update(digest, HMAC_FROM, size)
|
|
1043
|
+
const tag: u8[] = outer.digest()
|
|
1044
|
+
wipe(digest)
|
|
1045
|
+
hmacWipeSha512(inner)
|
|
1046
|
+
hmacWipeSha512(outer)
|
|
1047
|
+
return tag
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* The tag of `data[off .. off + len)` under `key`, written at
|
|
1052
|
+
* `out[at .. at + key.size())`. A window outside its array panics before
|
|
1053
|
+
* anything is read. It runs in an arena block of its own and stores nothing,
|
|
1054
|
+
* so a tag leaves the arena where it was.
|
|
1055
|
+
*/
|
|
1056
|
+
export const hmacKeyedInto = (key: HmacKey, data: u8[], off: i32, len: i32, out: u8[], at: i32): void => {
|
|
1057
|
+
const size: i32 = key.size()
|
|
1058
|
+
if (!key.live) {
|
|
1059
|
+
panic("hmacKeyedInto: the key is wiped")
|
|
1060
|
+
}
|
|
1061
|
+
if (!hmacWindowFits(toI32(data.length), off, len)) {
|
|
1062
|
+
panic("hmacKeyedInto: the message window is outside its array")
|
|
1063
|
+
}
|
|
1064
|
+
if (!hmacWindowFits(toI32(out.length), at, size)) {
|
|
1065
|
+
panic("hmacKeyedInto: the tag window is outside its array")
|
|
1066
|
+
}
|
|
1067
|
+
using _scope = arena()
|
|
1068
|
+
const tag: u8[] = hmacKeyedTag(key, data, off, len)
|
|
1069
|
+
for (let i: i32 = 0; i < size && i < toI32(tag.length) && at + i < toI32(out.length); i += 1) {
|
|
1070
|
+
out[at + i] = tag[i]
|
|
1071
|
+
}
|
|
1072
|
+
wipe(tag)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Whether `tag[tagAt .. tagAt + key.size())` is the tag of
|
|
1077
|
+
* `data[off .. off + len)` under `key`, compared in constant time and the
|
|
1078
|
+
* computed tag wiped, since it is the one tag a forger needs. A tag window
|
|
1079
|
+
* that does not fit answers `false`; a message window outside its array
|
|
1080
|
+
* panics. It runs in an arena block of its own and stores nothing.
|
|
1081
|
+
*/
|
|
1082
|
+
export const hmacKeyedVerify = (
|
|
1083
|
+
key: HmacKey,
|
|
1084
|
+
data: u8[],
|
|
1085
|
+
off: i32,
|
|
1086
|
+
len: i32,
|
|
1087
|
+
tag: u8[],
|
|
1088
|
+
tagAt: i32
|
|
1089
|
+
): boolean => {
|
|
1090
|
+
const size: i32 = key.size()
|
|
1091
|
+
if (!hmacWindowFits(toI32(data.length), off, len)) {
|
|
1092
|
+
panic("hmacKeyedVerify: the message window is outside its array")
|
|
1093
|
+
}
|
|
1094
|
+
if (!key.live || !hmacWindowFits(toI32(tag.length), tagAt, size)) {
|
|
1095
|
+
return false
|
|
1096
|
+
}
|
|
1097
|
+
using _scope = arena()
|
|
1098
|
+
const mine: u8[] = hmacKeyedTag(key, data, off, len)
|
|
1099
|
+
const ok: boolean = timingSafeEqualAt(mine, HMAC_FROM, tag, tagAt, size)
|
|
1100
|
+
wipe(mine)
|
|
1101
|
+
return ok
|
|
1102
|
+
}
|