@amritk/nish-x86_64-linux 0.32.0 → 0.34.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.
@@ -0,0 +1,366 @@
1
+ /* Nish runtime, the ChaCha20 half: the keystream of RFC 8439 §2.3 and §2.4,
2
+ * several blocks at once in vector registers. std/crypto/chacha20poly1305.ts
3
+ * is the cipher and the AEAD; this is a faster body for its private
4
+ * `chacha20XorInto`, which the compiler calls in that function's place when
5
+ * `nish_chacha20_hw` says this machine runs it (src/kernels.ts). Poly1305
6
+ * stays in Nish: WP38 S10 timed it beside this kernel and found the Nish
7
+ * code within the noise of a C copy (docs/wp38-simd.md §3.1).
8
+ *
9
+ * A translation unit of its own for the reason runtime-aes.c is one: its own
10
+ * measured `.text*` ceiling in tests/runtime-budgets.js, and a program that
11
+ * never encrypts with ChaCha20 links none of it (section GC).
12
+ *
13
+ * The paths. x86-64 runs eight blocks at once in AVX2's 256-bit registers,
14
+ * then four in 128-bit ones, then one at a time for what is left, all
15
+ * compiled with `target("avx2")` alone so the binary stays a baseline binary.
16
+ * A block is a lane: word `i` of block `b` is lane `b` of vector `i`, so the
17
+ * rounds are the scalar quarter rounds on vectors, and nothing shuffles
18
+ * until the words are written out. AArch64 has the same four-block loop in
19
+ * NEON, which every AArch64 CPU has; `NISH_CHACHA20_ARM_ON` says whether the
20
+ * resolver takes it (see there). A CPU without AVX2, a process with
21
+ * `NISH_CHACHA20=soft`, and every other target, wasm32 included, run the Nish
22
+ * code: the compiler keeps it and branches on `nish_chacha20_hw` at each call,
23
+ * and here the resolver answers 0 and the kernel is a trap nothing reaches.
24
+ *
25
+ * The kernel takes a buffer of four blocks or more and answers 0 for anything
26
+ * shorter, so a short message (a TLS record header, the Poly1305 key's one
27
+ * block, a QUIC header mask) costs no call and no wipe: S10 measured the
28
+ * kernel losing at 64 bytes until it did. It answers 0 too, touching
29
+ * nothing, when a window is outside its array or the two windows overlap
30
+ * without being the same bytes, so the Nish function runs and fails, or
31
+ * writes in its own order, as it would have.
32
+ *
33
+ * Constant time: ChaCha20 is additions, xors and rotations by constants, with
34
+ * no table and no secret-dependent branch. The kernel's branches are on array
35
+ * headers, offsets and the length, which are public, and its loads and stores
36
+ * are at addresses fixed by them. tests/ct-asm-runtime.js reads the
37
+ * `clang -O2` assembly of this file for both targets and holds the kernel to
38
+ * that. The one branch on anything else is `nish_chacha20_hw`'s, on the CPU's
39
+ * feature bits and `NISH_CHACHA20` (docs/security/ct-verification.md).
40
+ *
41
+ * Wiping: the working words, the per-call input block and the keystream
42
+ * buffers on the stack are zeroed by volatile stores before the kernel
43
+ * returns, the mechanism runtime-aes.c and `nish:secret`'s wipe use. The
44
+ * caller's state is the caller's to wipe, as with the Nish function. What
45
+ * the vector registers hold, and what the register allocator spills, no
46
+ * store from C can reach (docs/security/crypto-chacha20-hw.md).
47
+ *
48
+ * Written from RFC 8439 in this file's own structure; the block-a-lane layout
49
+ * is the one §3 of the RFC points at ("SIMD"), and nothing is ported.
50
+ */
51
+ #include <stddef.h>
52
+ #include <stdint.h>
53
+
54
+ #include "nish.h"
55
+
56
+ #if defined(__x86_64__)
57
+ #include <cpuid.h>
58
+ #define NISH_CHACHA20_X86 1
59
+ #elif defined(__aarch64__) && defined(__ARM_NEON) && defined(__ORDER_LITTLE_ENDIAN__) && \
60
+ __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
61
+ #include <arm_neon.h>
62
+ #define NISH_CHACHA20_ARM 1
63
+ #endif
64
+
65
+ #if defined(NISH_CHACHA20_X86) || defined(NISH_CHACHA20_ARM)
66
+
67
+ /* Named here rather than through <stdlib.h>, for runtime-aes.c's reason: the
68
+ constant-time check compiles this file for targets whose libc headers the
69
+ host does not have. */
70
+ char *getenv(const char *name);
71
+
72
+ #if defined(__has_attribute) && __has_attribute(no_builtin)
73
+ #define NISH_CHACHA20_NO_LIBC no_builtin
74
+ #else
75
+ #define NISH_CHACHA20_NO_LIBC
76
+ #endif
77
+ #if defined(NISH_CHACHA20_X86)
78
+ #define NISH_CHACHA20_TARGET __attribute__((target("avx2"), NISH_CHACHA20_NO_LIBC))
79
+ #else
80
+ #define NISH_CHACHA20_TARGET __attribute__((NISH_CHACHA20_NO_LIBC))
81
+ #endif
82
+ #define NISH_CHACHA20_INLINE static inline __attribute__((always_inline)) NISH_CHACHA20_TARGET
83
+
84
+ /* The buffer a vector loop needs before it runs: four blocks. */
85
+ #define NISH_CHACHA20_MIN 256
86
+
87
+ /* Zeroes `n` bytes, a multiple of 8, by volatile 8-byte stores. */
88
+ NISH_CHACHA20_INLINE void nish_chacha20_wipe(void *p, size_t n) {
89
+ volatile uint64_t *v = (volatile uint64_t *)p;
90
+ for (size_t i = 0; i < n / 8; i++) v[i] = 0;
91
+ }
92
+
93
+ NISH_CHACHA20_INLINE uint32_t nish_chacha20_rotl(uint32_t x, int n) {
94
+ return (x << n) | (x >> (32 - n));
95
+ }
96
+
97
+ #define NISH_CHACHA20_QR(a, b, c, d) \
98
+ a += b; \
99
+ d = nish_chacha20_rotl(d ^ a, 16); \
100
+ c += d; \
101
+ b = nish_chacha20_rotl(b ^ c, 12); \
102
+ a += b; \
103
+ d = nish_chacha20_rotl(d ^ a, 8); \
104
+ c += d; \
105
+ b = nish_chacha20_rotl(b ^ c, 7)
106
+
107
+ /* What is left after the vector loops, a block at a time as the Nish loop
108
+ does: `len` bytes under the keystream from `s`'s counter on, the counter
109
+ moved one a block, the last partial block taken byte by byte. */
110
+ NISH_CHACHA20_INLINE void nish_chacha20_tail(uint32_t s[16], const uint8_t *src, uint8_t *dst, size_t len) {
111
+ uint32_t x[16];
112
+ /* The block's words, which on these little-endian targets are its
113
+ keystream bytes in order: adding into words, not storing byte by byte,
114
+ keeps every store at a fixed offset of the frame. */
115
+ uint32_t ks[16];
116
+ const uint8_t *ks_bytes = (const uint8_t *)ks;
117
+ int ran = len > 0;
118
+ while (len > 0) {
119
+ for (int i = 0; i < 16; i++) x[i] = s[i];
120
+ for (int r = 0; r < 10; r++) {
121
+ NISH_CHACHA20_QR(x[0], x[4], x[8], x[12]);
122
+ NISH_CHACHA20_QR(x[1], x[5], x[9], x[13]);
123
+ NISH_CHACHA20_QR(x[2], x[6], x[10], x[14]);
124
+ NISH_CHACHA20_QR(x[3], x[7], x[11], x[15]);
125
+ NISH_CHACHA20_QR(x[0], x[5], x[10], x[15]);
126
+ NISH_CHACHA20_QR(x[1], x[6], x[11], x[12]);
127
+ NISH_CHACHA20_QR(x[2], x[7], x[8], x[13]);
128
+ NISH_CHACHA20_QR(x[3], x[4], x[9], x[14]);
129
+ }
130
+ for (int i = 0; i < 16; i++) ks[i] = x[i] + s[i];
131
+ s[12] += 1;
132
+ size_t take = len < 64 ? len : 64;
133
+ for (size_t i = 0; i < take; i++) dst[i] = src[i] ^ ks_bytes[i];
134
+ src += take;
135
+ dst += take;
136
+ len -= take;
137
+ }
138
+ if (ran) {
139
+ nish_chacha20_wipe(x, sizeof x);
140
+ nish_chacha20_wipe(ks, sizeof ks);
141
+ }
142
+ }
143
+
144
+ #if defined(NISH_CHACHA20_X86)
145
+
146
+ typedef uint32_t nish_u32x4 __attribute__((vector_size(16)));
147
+ typedef uint32_t nish_u32x8 __attribute__((vector_size(32)));
148
+
149
+ /* The rotate lane by lane; LLVM turns 16 and 8 into byte shuffles. */
150
+ #define NISH_CHACHA20_VROTL(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
151
+
152
+ #define NISH_CHACHA20_VQR(a, b, c, d) \
153
+ a += b; \
154
+ d = NISH_CHACHA20_VROTL(d ^ a, 16); \
155
+ c += d; \
156
+ b = NISH_CHACHA20_VROTL(b ^ c, 12); \
157
+ a += b; \
158
+ d = NISH_CHACHA20_VROTL(d ^ a, 8); \
159
+ c += d; \
160
+ b = NISH_CHACHA20_VROTL(b ^ c, 7)
161
+
162
+ #define NISH_CHACHA20_VROUNDS(x) \
163
+ for (int r = 0; r < 10; r++) { \
164
+ NISH_CHACHA20_VQR(x[0], x[4], x[8], x[12]); \
165
+ NISH_CHACHA20_VQR(x[1], x[5], x[9], x[13]); \
166
+ NISH_CHACHA20_VQR(x[2], x[6], x[10], x[14]); \
167
+ NISH_CHACHA20_VQR(x[3], x[7], x[11], x[15]); \
168
+ NISH_CHACHA20_VQR(x[0], x[5], x[10], x[15]); \
169
+ NISH_CHACHA20_VQR(x[1], x[6], x[11], x[12]); \
170
+ NISH_CHACHA20_VQR(x[2], x[7], x[8], x[13]); \
171
+ NISH_CHACHA20_VQR(x[3], x[4], x[9], x[14]); \
172
+ }
173
+
174
+ /* `lanes` blocks at once, `vec` lanes of `lanes` words: the input block with
175
+ the counter stepped one a lane, the rounds, then the sum written out word
176
+ by word (lane `b` of word `i` to byte 64b + 4i of the keystream) and
177
+ xored into `dst` a vector at a time. Answers the bytes it covered. */
178
+ #define NISH_CHACHA20_LOOP(vec, lanes, s, src, dst, len, ...) \
179
+ { \
180
+ vec x[16]; \
181
+ vec in[16]; \
182
+ uint8_t ks[64 * (lanes)] __attribute__((aligned(32))); \
183
+ const vec step = {__VA_ARGS__}; \
184
+ int ran = (len) >= 64 * (lanes); \
185
+ while ((len) >= 64 * (lanes)) { \
186
+ for (int i = 0; i < 16; i++) in[i] = (vec){0} + (s)[i]; \
187
+ in[12] += step; \
188
+ for (int i = 0; i < 16; i++) x[i] = in[i]; \
189
+ NISH_CHACHA20_VROUNDS(x) \
190
+ for (int i = 0; i < 16; i++) { \
191
+ vec w = x[i] + in[i]; \
192
+ for (int b = 0; b < (lanes); b++) __builtin_memcpy(ks + 64 * b + 4 * i, (const uint8_t *)&w + 4 * b, 4); \
193
+ } \
194
+ for (size_t i = 0; i < 64 * (lanes); i += sizeof(vec)) { \
195
+ vec p, k; \
196
+ __builtin_memcpy(&p, (src) + i, sizeof p); \
197
+ __builtin_memcpy(&k, ks + i, sizeof k); \
198
+ p ^= k; \
199
+ __builtin_memcpy((dst) + i, &p, sizeof p); \
200
+ } \
201
+ (s)[12] += (lanes); \
202
+ (src) += 64 * (lanes); \
203
+ (dst) += 64 * (lanes); \
204
+ (len) -= 64 * (lanes); \
205
+ } \
206
+ if (ran) { \
207
+ nish_chacha20_wipe(x, sizeof x); \
208
+ nish_chacha20_wipe(in, sizeof in); \
209
+ nish_chacha20_wipe(ks, sizeof ks); \
210
+ } \
211
+ }
212
+
213
+ NISH_CHACHA20_INLINE void nish_chacha20_blocks(uint32_t s[16], const uint8_t *src, uint8_t *dst, size_t len) {
214
+ NISH_CHACHA20_LOOP(nish_u32x8, 8, s, src, dst, len, 0, 1, 2, 3, 4, 5, 6, 7)
215
+ NISH_CHACHA20_LOOP(nish_u32x4, 4, s, src, dst, len, 0, 1, 2, 3)
216
+ nish_chacha20_tail(s, src, dst, len);
217
+ }
218
+
219
+ #else
220
+
221
+ #define NISH_CHACHA20_NROTL(x, n) vsriq_n_u32(vshlq_n_u32((x), (n)), (x), 32 - (n))
222
+ #define NISH_CHACHA20_NROT16(x) vreinterpretq_u32_u16(vrev32q_u16(vreinterpretq_u16_u32(x)))
223
+ #define NISH_CHACHA20_NQR(a, b, c, d) \
224
+ a = vaddq_u32(a, b); \
225
+ d = NISH_CHACHA20_NROT16(veorq_u32(d, a)); \
226
+ c = vaddq_u32(c, d); \
227
+ b = NISH_CHACHA20_NROTL(veorq_u32(b, c), 12); \
228
+ a = vaddq_u32(a, b); \
229
+ d = NISH_CHACHA20_NROTL(veorq_u32(d, a), 8); \
230
+ c = vaddq_u32(c, d); \
231
+ b = NISH_CHACHA20_NROTL(veorq_u32(b, c), 7)
232
+
233
+ /* Four blocks at once in NEON, a block a lane; `vst4q` interleaves four
234
+ words so each 16-byte row it stores is one row of one block. */
235
+ NISH_CHACHA20_INLINE void nish_chacha20_blocks(uint32_t s[16], const uint8_t *src, uint8_t *dst, size_t len) {
236
+ uint32x4_t x[16];
237
+ uint32x4_t in[16];
238
+ uint32_t rows[16] __attribute__((aligned(16)));
239
+ uint8_t ks[256] __attribute__((aligned(16)));
240
+ const uint32_t step_words[4] = {0, 1, 2, 3};
241
+ const uint32x4_t step = vld1q_u32(step_words);
242
+ int ran = len >= 256;
243
+ while (len >= 256) {
244
+ for (int i = 0; i < 16; i++) in[i] = vdupq_n_u32(s[i]);
245
+ in[12] = vaddq_u32(in[12], step);
246
+ for (int i = 0; i < 16; i++) x[i] = in[i];
247
+ for (int r = 0; r < 10; r++) {
248
+ NISH_CHACHA20_NQR(x[0], x[4], x[8], x[12]);
249
+ NISH_CHACHA20_NQR(x[1], x[5], x[9], x[13]);
250
+ NISH_CHACHA20_NQR(x[2], x[6], x[10], x[14]);
251
+ NISH_CHACHA20_NQR(x[3], x[7], x[11], x[15]);
252
+ NISH_CHACHA20_NQR(x[0], x[5], x[10], x[15]);
253
+ NISH_CHACHA20_NQR(x[1], x[6], x[11], x[12]);
254
+ NISH_CHACHA20_NQR(x[2], x[7], x[8], x[13]);
255
+ NISH_CHACHA20_NQR(x[3], x[4], x[9], x[14]);
256
+ }
257
+ for (int i = 0; i < 16; i += 4) {
258
+ uint32x4x4_t w;
259
+ w.val[0] = vaddq_u32(x[i], in[i]);
260
+ w.val[1] = vaddq_u32(x[i + 1], in[i + 1]);
261
+ w.val[2] = vaddq_u32(x[i + 2], in[i + 2]);
262
+ w.val[3] = vaddq_u32(x[i + 3], in[i + 3]);
263
+ vst4q_u32(rows, w);
264
+ for (int b = 0; b < 4; b++) __builtin_memcpy(ks + 64 * b + 4 * i, rows + 4 * b, 16);
265
+ }
266
+ for (size_t i = 0; i < 256; i += 16) vst1q_u8(dst + i, veorq_u8(vld1q_u8(src + i), vld1q_u8(ks + i)));
267
+ s[12] += 4;
268
+ src += 256;
269
+ dst += 256;
270
+ len -= 256;
271
+ }
272
+ if (ran) {
273
+ nish_chacha20_wipe(x, sizeof x);
274
+ nish_chacha20_wipe(in, sizeof in);
275
+ nish_chacha20_wipe(rows, sizeof rows);
276
+ nish_chacha20_wipe(ks, sizeof ks);
277
+ }
278
+ nish_chacha20_tail(s, src, dst, len);
279
+ }
280
+
281
+ #endif
282
+
283
+ /* `chacha20XorInto`'s contract, checked rather than trusted: 1 when it ran,
284
+ 0, writing nothing, unless `state` has its sixteen words, `len` is four
285
+ blocks or more, both windows are inside their arrays and end before
286
+ INT32_MAX - 64, and the windows are the same bytes or do not overlap.
287
+ The Nish function reads and writes byte
288
+ by byte in order, so an in-place call (the AEAD's and `chacha20Block`'s)
289
+ agrees with a kernel that loads a vector before it stores it; a shifted
290
+ overlap would not, and is left to the Nish function. */
291
+ NISH_CHACHA20_TARGET int32_t nish_chacha20_xor(nish_array *state, const nish_array *src, int32_t src_at,
292
+ int32_t len, nish_array *dst, int32_t dst_at) {
293
+ if (state->len < 16 || len < NISH_CHACHA20_MIN || src_at < 0 || dst_at < 0) return 0;
294
+ if ((uint64_t)src_at + (uint64_t)len > src->len || (uint64_t)dst_at + (uint64_t)len > dst->len) return 0;
295
+ /* The Nish function's i32 sums reach a window's end and its last block's
296
+ end, so past INT32_MAX - 64 it panics on an overflow; past that the
297
+ kernel refuses, and the Nish function panics as it would have. */
298
+ if ((uint64_t)src_at + (uint64_t)len > INT32_MAX - 64 || (uint64_t)dst_at + (uint64_t)len > INT32_MAX - 64) return 0;
299
+ const uint8_t *from = (const uint8_t *)src->data + src_at;
300
+ uint8_t *to = (uint8_t *)dst->data + dst_at;
301
+ uintptr_t f = (uintptr_t)from, t = (uintptr_t)to;
302
+ if (f != t && f < t + (uintptr_t)len && t < f + (uintptr_t)len) return 0;
303
+ nish_chacha20_blocks((uint32_t *)(void *)state->data, from, to, (size_t)len);
304
+ return 1;
305
+ }
306
+
307
+ #if defined(NISH_CHACHA20_X86)
308
+ /* Whether this CPU and its OS give AVX2: CPUID leaf 1's OSXSAVE and AVX,
309
+ XCR0's SSE and AVX state, and leaf 7's AVX2. Asked through `cpuid.h`, for
310
+ runtime-aes.c's reason. */
311
+ static int32_t nish_chacha20_cpu(void) {
312
+ unsigned a, b, c, d;
313
+ if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
314
+ if ((c & bit_OSXSAVE) == 0 || (c & bit_AVX) == 0) return 0;
315
+ unsigned lo, hi;
316
+ __asm__("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
317
+ (void)hi;
318
+ if ((lo & 6) != 6) return 0;
319
+ if (!__get_cpuid_count(7, 0, &a, &b, &c, &d)) return 0;
320
+ return (b & bit_AVX2) != 0;
321
+ }
322
+ #else
323
+ /* NEON is in every AArch64 CPU, so the question is only whether the kernel
324
+ pays here: `NISH_CHACHA20_ARM_ON` is S10's AArch64 verdict
325
+ (docs/wp38-simd.md §3.1). */
326
+ #ifndef NISH_CHACHA20_ARM_ON
327
+ #define NISH_CHACHA20_ARM_ON 1
328
+ #endif
329
+ static int32_t nish_chacha20_cpu(void) {
330
+ return NISH_CHACHA20_ARM_ON;
331
+ }
332
+ #endif
333
+
334
+ /* The answer: 0 until the first call, then 1 for the kernel or 2 for the Nish
335
+ path, with runtime-aes.c's relaxed atomics for the same reason. */
336
+ static int32_t nish_chacha20_answer;
337
+
338
+ int32_t nish_chacha20_hw(void) {
339
+ int32_t answer = __atomic_load_n(&nish_chacha20_answer, __ATOMIC_RELAXED);
340
+ if (answer == 0) {
341
+ /* `soft` pins the Nish path and never asks the CPU; anything else, or
342
+ nothing, means the best this machine has. */
343
+ const char *pin = getenv("NISH_CHACHA20");
344
+ int soft = pin != 0 && pin[0] == 's' && pin[1] == 'o' && pin[2] == 'f' && pin[3] == 't' && pin[4] == 0;
345
+ answer = !soft && nish_chacha20_cpu() ? 1 : 2;
346
+ __atomic_store_n(&nish_chacha20_answer, answer, __ATOMIC_RELAXED);
347
+ }
348
+ return answer == 1;
349
+ }
350
+
351
+ #else
352
+
353
+ /* A target with no vector path here, wasm32 among them: the Nish path
354
+ always, and a kernel that a program whose every call is behind
355
+ `nish_chacha20_hw` never reaches. */
356
+ int32_t nish_chacha20_hw(void) {
357
+ return 0;
358
+ }
359
+
360
+ int32_t nish_chacha20_xor(nish_array *state, const nish_array *src, int32_t src_at, int32_t len, nish_array *dst,
361
+ int32_t dst_at) {
362
+ (void)state, (void)src, (void)src_at, (void)len, (void)dst, (void)dst_at;
363
+ __builtin_trap();
364
+ }
365
+
366
+ #endif
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 fifteen translation units and is named as one: an input
6
+ # The C runtime is sixteen 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
@@ -14,6 +14,7 @@
14
14
  # the SSE4.1 rounding of `Math.floor` and its kin,
15
15
  # <dir>/runtime-aes.c, AES-GCM on the CPU's AES instructions,
16
16
  # <dir>/runtime-sha256.c, SHA-256 on the CPU's SHA instructions,
17
+ # <dir>/runtime-chacha20.c, ChaCha20's keystream on AVX2 or NEON vectors,
17
18
  # <dir>/runtime-checksum.c, CRC-32 on its carry-less multiply or CRC instructions,
18
19
  # <dir>/runtime-term.c, stdin and the terminal of `nish:term`,
19
20
  # <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`,
@@ -223,14 +224,14 @@ if [ -n "$sqlite_to" ] || [ -n "$sqlite_from" ]; then
223
224
  fi
224
225
  [ -n "$out" ] || [ -n "$objects_to" ] || [ -n "$sqlite_to" ] || { echo "error: -o <out> is required" >&2; exit 2; }
225
226
 
226
- # The runtime is fifteen translation units, and a caller names one: whoever passes
227
+ # The runtime is sixteen translation units, and a caller names one: whoever passes
227
228
  # <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c,
228
229
  # <dir>/runtime-cross.c, <dir>/runtime-host.c, <dir>/runtime-net.c, <dir>/runtime-simd.c,
229
- # <dir>/runtime-aes.c, <dir>/runtime-sha256.c, <dir>/runtime-checksum.c, <dir>/runtime-term.c,
230
+ # <dir>/runtime-aes.c, <dir>/runtime-sha256.c, <dir>/runtime-chacha20.c, <dir>/runtime-checksum.c, <dir>/runtime-term.c,
230
231
  # <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c, <dir>/runtime-shm.c and <dir>/runtime-region.c compiled beside it, on every profile,
231
232
  # wasi included: runtime-simd.c and runtime-bytes.c take their scalar paths there
232
233
  # unless the build adds -msimd128,
233
- # runtime-aes.c, runtime-sha256.c and runtime-checksum.c compile to an answer of 0 and
234
+ # runtime-aes.c, runtime-sha256.c, runtime-chacha20.c and runtime-checksum.c compile to an answer of 0 and
234
235
  # stubs that nothing calls, because the compiler emits no call to their kernels for wasm32, and
235
236
  # runtime-term.c reads stdin and answers "not a terminal", and runtime-sandbox.c
236
237
  # compiles to two functions that refuse to run, which no wasm build calls, and
@@ -244,7 +245,7 @@ fi
244
245
  # here keeps every one of those correct, and keeps "the runtime" one thing to
245
246
  # name from the outside. A caller that names one itself is left alone, because
246
247
  # naming one object twice is a duplicate-symbol error.
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"
248
+ 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-chacha20.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c runtime-region.c"
248
249
  for i in ${inputs[@]+"${inputs[@]}"}; do
249
250
  case "$i" in
250
251
  */runtime.c|runtime.c)
@@ -693,14 +694,16 @@ case "$profile" in
693
694
  fi
694
695
  # IR compiled for the host rather than with `--target wasm32` calls std/crypto/aes.ts's
695
696
  # AES kernels behind `nish_aes_hw`, std/crypto/sha256.ts's block function behind
696
- # `nish_sha256_hw` and std/compress/checksum.ts's CRC-32 kernel behind
697
- # `nish_crc32_hw` (src/kernels.ts). runtime-aes.c's, runtime-sha256.c's and
698
- # runtime-checksum.c's wasm32 builds answer 0 there, so the Nish code runs, and
697
+ # `nish_sha256_hw`, std/crypto/chacha20poly1305.ts's keystream loop behind
698
+ # `nish_chacha20_hw` and std/compress/checksum.ts's CRC-32 kernel behind
699
+ # `nish_crc32_hw` (src/kernels.ts). runtime-aes.c's, runtime-sha256.c's,
700
+ # runtime-chacha20.c's and runtime-checksum.c's wasm32 builds answer 0 there,
701
+ # so the Nish code runs, and
699
702
  # each is added only for such IR: every other module links the bytes it always did.
700
703
  # Each pair is a unit and the kernel its probe names.
701
704
  # A caller that names the unit itself is left alone, as `halves` leaves it:
702
705
  # one object named twice is a duplicate-symbol error.
703
- for pair in aes:aes sha256:sha256 checksum:crc32; do
706
+ for pair in aes:aes sha256:sha256 chacha20:chacha20 checksum:crc32; do
704
707
  unit=${pair%%:*}
705
708
  kernel=${pair#*:}
706
709
  named=0
package/std/README.md CHANGED
@@ -96,11 +96,12 @@ is listed in [`SECURITY.md`](../SECURITY.md#threat-model).
96
96
  | [`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 |
97
97
  | [`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 | — |
98
98
  | [`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$` |
99
+ | [`crypto/bcrypt-batch.ts`](./crypto/bcrypt-batch.ts) | `bcryptBatcher({ width, holdNanos, cores, cost })`, a login batcher: `verify(password, hash, now)` and `hash(password, salt, now)` queue a request and answer its ticket, `waitNanos(now)` says when a run is due (a full batch, or the oldest request held `holdNanos`), and `run(now)` sends up to `width × cores` requests as `bcryptVerifyN` / `bcryptHashN` batches on one `std/threads` task per core and answers a `BcryptAnswer` (`ticket`, `ok`, `hash`, `error`) for each. It keeps and wipes its own copy of each password; a damaged stored hash answers its `Err` on its own row ([the record](../docs/security/crypto-bcrypt.md#the-login-batcher)) | — |
99
100
  | [`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`) |
100
101
  | [`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 |
101
102
  | [`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` |
102
103
  | [`crypto/ed25519.ts`](./crypto/ed25519.ts) | `ed25519PublicKey(seed)`, the 32-byte public key, `ed25519Sign(seed, msg)`, the 64-byte signature `R \|\| S`, each from a `Secret<u8[]>` seed of `ED25519_SEED_SIZE` (32) bytes or `null`, and `ed25519Verify(pub, msg, sig)`. Signing is deterministic and constant time; the seed's hash, the nonce and every intermediate the scalar reaches are wiped. Verify is cofactorless, refuses an S of L or more and a key that does not decode, and is variable time, since its inputs are public. Sign and verify panic on a message longer than 2^31 − 1 bytes. It reuses `crypto/x25519.ts`'s field arithmetic | RFC 8032 §5.1, §7.1 |
103
- | [`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`. What a call derives from the key — the ChaCha20 state, the keystream, the Poly1305 key, `r` and accumulator, and on open the tag it compared — is wiped with `wipe`'s volatile stores before the call returns, the refusal path included | RFC 8439 §2, RFC 9001 §5.4.4, Wycheproof `chacha20_poly1305` (all 325 cases) |
104
+ | [`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`. What a call derives from the key — the ChaCha20 state, the keystream, the Poly1305 key, `r` and accumulator, and on open the tag it compared — is wiped with `wipe`'s volatile stores before the call returns, the refusal path included. Natively, on x86-64 with AVX2 or on AArch64, the compiler sends the keystream of a message of 256 bytes or more to the runtime's kernel instead (`runtime/runtime-chacha20.c`, WP38 S10), and `NISH_CHACHA20=soft` pins the Nish code | RFC 8439 §2, RFC 9001 §5.4.4, Wycheproof `chacha20_poly1305` (all 325 cases) |
104
105
  | [`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; natively, on a CPU with AES-NI and PCLMULQDQ or the ARMv8 AES and PMULL instructions, the compiler sends the key schedule, the block, GCTR and GHASH to the runtime's kernels instead (`runtime/runtime-aes.c`, WP38 S5), and `NISH_AES=soft` pins the bitsliced code: `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. An `AesKey` holds its round keys in `roundKeys` and H in `h`, two `u64` words (high, low); `aesKeyWipe(key)` zeroes both with `wipe`'s volatile stores, the kernels' powers of H after the schedule included, and is how a holder drops a key. The bitsliced schedule's, block's and GCTR's working copies, the tag's E(K, J0) block, the tag open compared and J0 itself are wiped the same way before they are released | 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) |
105
106
  | [`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, and `p256SignSha256Into(scratch, priv, msg, off, len, out, at)`, the same signature written into the caller's window in a `P256SignScratch` made once, keeping nothing in the arena; 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` |
106
107
  | [`crypto/p384.ts`](./crypto/p384.ts) | ECDSA verification over P-384: `p384Verify(pub, digest, sig)` over a digest from any hash (one longer than 48 bytes is truncated to its leftmost 384 bits) and `p384VerifySha384(pub, msg, sig)` over a message; the key is 97-byte uncompressed SEC1 and the signature `r` and `s`, 48 big-endian bytes each, not DER. It only verifies, so it holds no `Secret`. A malformed key, a point off the curve, an `r` or `s` out of range and a sum at the identity answer `false`, never a panic. ECDSA is malleable, and `p384Verify` accepts a high `s` | RFC 6979 A.2.6, Wycheproof `ecdsa_secp384r1_sha384`, `_sha384_p1363`, `_sha256`, `_sha512` and `_sha512_p1363` |