@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.
- package/INSTALL.md +10 -8
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/core.d.ts +83 -6
- package/runtime/core.mjs +90 -0
- package/runtime/nish.d.ts +83 -6
- package/runtime/nish.h +27 -4
- package/runtime/runtime-chacha20.c +366 -0
- package/scripts/build.sh +12 -9
- package/std/README.md +2 -1
- package/std/crypto/bcrypt-batch.ts +702 -0
- package/std/net/http1-codec.ts +81 -41
- package/std/net/http1-server.ts +62 -47
|
@@ -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
|
|
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
|
|
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
|
|
697
|
-
# `
|
|
698
|
-
# runtime-
|
|
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` |
|