@amritk/nish-x86_64-linux 0.23.1 → 0.25.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/bin/nish +0 -0
- package/docs/AI.md +50 -23
- package/package.json +1 -1
- package/runtime/browser.mjs +4 -3
- package/runtime/core/arrays.d.ts +0 -2
- package/runtime/core/arrays.mjs +0 -2
- package/runtime/core/host.d.ts +8 -0
- package/runtime/core/host.mjs +50 -2
- package/runtime/core/numbers.mjs +2 -3
- package/runtime/nish.d.ts +100 -4
- package/runtime/nish.h +34 -0
- package/runtime/nish.mjs +23 -5
- package/runtime/node.mjs +57 -6
- package/runtime/runtime-net.c +45 -4
- package/runtime/runtime-os.c +115 -12
- package/runtime/runtime-term.c +2 -2
- package/runtime/runtime.c +1 -1
- package/scripts/build.sh +13 -3
- package/std/README.md +44 -7
- package/std/cli-args.ts +1216 -0
- package/std/cli-flags.ts +585 -0
- package/std/cli.ts +308 -19
- package/std/compress/checksum.ts +290 -0
- package/std/compress/deflate.ts +2790 -0
- package/std/compress/gzip.ts +761 -0
- package/std/compress/zlib.ts +168 -0
- package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
- package/std/crypto/p256.ts +63 -1
- package/std/crypto/p384.ts +6044 -0
- package/std/crypto/pki-roots.ts +322 -0
- package/std/crypto/pki.ts +1207 -0
- package/std/crypto/rsa.ts +663 -0
- package/std/crypto/sha256.ts +2 -3
- package/std/crypto/x25519.ts +3 -4
- package/std/crypto/x509.ts +1054 -58
- package/std/net/dns-config.ts +734 -0
- package/std/net/dns-message.ts +517 -0
- package/std/net/dns-resolver.ts +1015 -0
- package/std/net/dns.ts +478 -0
- package/std/net/http-compress.ts +393 -0
- package/std/net/http1-client.ts +1227 -0
- package/std/net/http1-server.ts +101 -1
- package/std/net/http1.ts +9 -9
- package/std/net/tls/client.ts +1094 -0
- package/std/net/tls/codec.ts +585 -3
- package/std/net/tls/record-client.ts +1192 -0
- package/std/net/tls/verify.ts +221 -0
- package/std/net/tls-tcp.ts +642 -11
- package/std/net/tls.ts +2 -2
- package/std/net/url.ts +559 -4
- package/std/yaml-write.ts +415 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/crypto/pki-roots` — the root certificates a program should trust,
|
|
3
|
+
* read from where OpenSSL and Go look for them: the files `SSL_CERT_FILE` and
|
|
4
|
+
* `SSL_CERT_DIR` name, or else the system's bundle.
|
|
5
|
+
*
|
|
6
|
+
* import { PkiRoots, pkiLoadSystemRoots } from "nish/crypto/pki-roots";
|
|
7
|
+
*
|
|
8
|
+
* const store: PkiRoots = pkiLoadSystemRoots();
|
|
9
|
+
* // store.roots: X509Certificate[], parsed, no two with the same DER
|
|
10
|
+
* // store.sources: the files they were read from, in order
|
|
11
|
+
* // store.skipped: the PEM blocks and files that were not a root
|
|
12
|
+
*
|
|
13
|
+
* The order (docs/security/crypto-pki.md, "Where the roots come from"):
|
|
14
|
+
*
|
|
15
|
+
* 1. When `SSL_CERT_FILE` or `SSL_CERT_DIR` is set and not empty, exactly
|
|
16
|
+
* those: the file, and every directory of the `:`-separated list. There is
|
|
17
|
+
* no fallback, so naming a file that is missing trusts nothing.
|
|
18
|
+
* 2. Otherwise the first file of `pkiSystemBundles()` that exists, and
|
|
19
|
+
* the directory `/etc/ssl/certs` beside it.
|
|
20
|
+
*
|
|
21
|
+
* Only a path that leads to a regular file is read. A directory gives every
|
|
22
|
+
* entry named `*.pem`, `*.crt` or OpenSSL's hash form `xxxxxxxx.N`, not its
|
|
23
|
+
* subdirectories. A file is split into PEM blocks here, not by `pemToDer`,
|
|
24
|
+
* which refuses a whole text for one bad block: a `CERTIFICATE`
|
|
25
|
+
* block `x509ParseCertificate` accepts is a root, and every other block — a
|
|
26
|
+
* malformed one, an unterminated one, a `TRUSTED CERTIFICATE` — is counted in
|
|
27
|
+
* `skipped` and passed over. Finding nothing is an empty `roots`, never an
|
|
28
|
+
* exit: what to do with no roots is the caller's decision.
|
|
29
|
+
*
|
|
30
|
+
* Whoever sets the environment or writes these files decides what is trusted.
|
|
31
|
+
* That is the contract, as it is OpenSSL's, and it is not defended against;
|
|
32
|
+
* what is bounded is the work a hostile file can cause (PKI-R1 to PKI-R3).
|
|
33
|
+
*
|
|
34
|
+
* **Native only.** It asks `lstatOwnerModeSync` what each path is before it
|
|
35
|
+
* reads it, which a wasm32 build has no file system to answer, so a program
|
|
36
|
+
* that imports this module does not compile for wasm32 (PKI-R4).
|
|
37
|
+
* `nish/crypto/x509` imports nothing from here.
|
|
38
|
+
*
|
|
39
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
40
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
41
|
+
*/
|
|
42
|
+
import { readdirSync, readFileSyncOrNull, realpathSync } from "nish:fs"
|
|
43
|
+
import { getenv } from "nish:process"
|
|
44
|
+
import { X509Certificate, pemToDer, x509ParseCertificate } from "nish/crypto/x509"
|
|
45
|
+
|
|
46
|
+
/** The largest file read as roots: 16 MiB, some eighty times a full Mozilla bundle. A bigger one counts as one skip. */
|
|
47
|
+
export const PKI_ROOTS_MAX_FILE_BYTES: i32 = 16777216
|
|
48
|
+
|
|
49
|
+
/** The most files one load reads; an entry past it counts as one skip. */
|
|
50
|
+
export const PKI_ROOTS_MAX_FILES: i32 = 4096
|
|
51
|
+
|
|
52
|
+
/** The most roots one load keeps; a certificate past it counts as one skip. */
|
|
53
|
+
export const PKI_ROOTS_MAX_ROOTS: i32 = 4096
|
|
54
|
+
|
|
55
|
+
/** The trust store one load found. */
|
|
56
|
+
export class PkiRoots {
|
|
57
|
+
/** The parsed roots, in the order they were read, no two with the same DER. */
|
|
58
|
+
roots: X509Certificate[]
|
|
59
|
+
/** Every file that was read, in order, whether or not it held a root. */
|
|
60
|
+
sources: string[]
|
|
61
|
+
/** The PEM blocks that were not a root, and the files too large or past the cap to read. */
|
|
62
|
+
skipped: i32 = 0
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
this.roots = []
|
|
66
|
+
this.sources = []
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The system bundles, in the order they are tried; the first one that reads
|
|
72
|
+
* as a file is used. A function, not a constant, because a module constant
|
|
73
|
+
* is a number, a boolean or a string. The paths and their order are those of Go's
|
|
74
|
+
* `crypto/x509/root_linux.go`, cited as facts, not copied code: Debian, Ubuntu,
|
|
75
|
+
* Arch and Gentoo; Fedora and RHEL; openSUSE; OpenELEC; CentOS and RHEL 7;
|
|
76
|
+
* Alpine, macOS and the BSDs.
|
|
77
|
+
*/
|
|
78
|
+
export const pkiSystemBundles = (): string[] => [
|
|
79
|
+
"/etc/ssl/certs/ca-certificates.crt",
|
|
80
|
+
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
81
|
+
"/etc/ssl/ca-bundle.pem",
|
|
82
|
+
"/etc/pki/tls/cacert.pem",
|
|
83
|
+
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
|
84
|
+
"/etc/ssl/cert.pem",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
/** The directory read beside the system bundle. */
|
|
88
|
+
export const PKI_SYSTEM_DIR: string = "/etc/ssl/certs"
|
|
89
|
+
|
|
90
|
+
/** What one load has read so far, and the DER already kept. */
|
|
91
|
+
class PkiRootsLoad {
|
|
92
|
+
out: PkiRoots
|
|
93
|
+
seen: Set<string>
|
|
94
|
+
files: i32 = 0
|
|
95
|
+
|
|
96
|
+
constructor() {
|
|
97
|
+
this.out = new PkiRoots()
|
|
98
|
+
this.seen = new Set<string>()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Keeps `der` as a root when it parses, is new, and the cap has room; counts it skipped otherwise. */
|
|
103
|
+
const pkiRootsKeep = (load: PkiRootsLoad, der: u8[]): void => {
|
|
104
|
+
const cert: X509Certificate | null = x509ParseCertificate(der)
|
|
105
|
+
if (cert === null) {
|
|
106
|
+
load.out.skipped = load.out.skipped + 1
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
const key: string = String.fromBytes(der, 0, der.length)
|
|
110
|
+
if (load.seen.has(key)) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (toI32(load.out.roots.length) >= PKI_ROOTS_MAX_ROOTS) {
|
|
114
|
+
load.out.skipped = load.out.skipped + 1
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
load.seen.add(key)
|
|
118
|
+
load.out.roots.push(cert)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One PEM block, `text` whole from its BEGIN line through its END line. A block
|
|
123
|
+
* labelled other than CERTIFICATE gives `pemToDer` no DER, so it is skipped too.
|
|
124
|
+
*/
|
|
125
|
+
const pkiRootsBlock = (load: PkiRootsLoad, text: string): void => {
|
|
126
|
+
const ders: u8[][] | null = pemToDer(text, "CERTIFICATE")
|
|
127
|
+
if (ders === null || ders.length !== 1) {
|
|
128
|
+
load.out.skipped = load.out.skipped + 1
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
pkiRootsKeep(load, ders[0])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Every PEM block of `text`. A BEGIN line opens a block and the next END line
|
|
136
|
+
* closes it; a BEGIN before that END counts the open block skipped and starts
|
|
137
|
+
* again, and a block still open at the end is skipped. Text outside the blocks,
|
|
138
|
+
* a stray END among it, is passed over, as bundles carry comments there.
|
|
139
|
+
*/
|
|
140
|
+
const pkiRootsSplit = (load: PkiRootsLoad, text: string): void => {
|
|
141
|
+
const n: i32 = toI32(text.length)
|
|
142
|
+
let open: boolean = false
|
|
143
|
+
let blockStart: i32 = 0
|
|
144
|
+
let lineStart: i32 = 0
|
|
145
|
+
while (lineStart < n) {
|
|
146
|
+
let lineEnd: i32 = lineStart
|
|
147
|
+
while (lineEnd >= 0 && lineEnd < toI32(text.length) && toI32(text.charCodeAt(lineEnd)) !== 10) {
|
|
148
|
+
lineEnd = lineEnd + 1
|
|
149
|
+
}
|
|
150
|
+
const next: i32 = lineEnd < n ? lineEnd + 1 : n
|
|
151
|
+
// A carriage return, space or tab before the line feed is no part of the
|
|
152
|
+
// line, so a BEGIN with trailing whitespace still opens a block and is
|
|
153
|
+
// counted when `pemToDer` refuses it, rather than lost unseen.
|
|
154
|
+
let end: i32 = lineEnd
|
|
155
|
+
while (
|
|
156
|
+
end > lineStart &&
|
|
157
|
+
(toI32(text.charCodeAt(end - 1)) === 13 ||
|
|
158
|
+
toI32(text.charCodeAt(end - 1)) === 32 ||
|
|
159
|
+
toI32(text.charCodeAt(end - 1)) === 9)
|
|
160
|
+
) {
|
|
161
|
+
end = end - 1
|
|
162
|
+
}
|
|
163
|
+
const line: string = text.slice(lineStart, end)
|
|
164
|
+
if (line.startsWith("-----BEGIN ") && line.endsWith("-----") && line.length > 16) {
|
|
165
|
+
if (open) {
|
|
166
|
+
load.out.skipped = load.out.skipped + 1
|
|
167
|
+
}
|
|
168
|
+
open = true
|
|
169
|
+
blockStart = lineStart
|
|
170
|
+
} else if (open && line.startsWith("-----END ")) {
|
|
171
|
+
pkiRootsBlock(load, text.slice(blockStart, next))
|
|
172
|
+
open = false
|
|
173
|
+
}
|
|
174
|
+
lineStart = next
|
|
175
|
+
}
|
|
176
|
+
if (open) {
|
|
177
|
+
load.out.skipped = load.out.skipped + 1
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Where `path` leads when that is a regular file, or `null`: a missing path,
|
|
183
|
+
* a directory, and a FIFO or device, whose `open` could wait forever or whose
|
|
184
|
+
* read never ends, are not a file of roots (PKI-R2).
|
|
185
|
+
*/
|
|
186
|
+
const pkiRootsRegular = (path: string): string | null => {
|
|
187
|
+
const real: string | null = realpathSync(path)
|
|
188
|
+
if (real === null) {
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
const om: i64 = lstatOwnerModeSync(real)
|
|
192
|
+
return om >= 0 && (om & 0xf000) === 0x8000 ? real : null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Reads `path` as roots: `false` when it is not a regular file that reads, `true` when it was (or would have been, past a cap). */
|
|
196
|
+
const pkiRootsFile = (load: PkiRootsLoad, path: string): boolean => {
|
|
197
|
+
if (load.files >= PKI_ROOTS_MAX_FILES) {
|
|
198
|
+
load.out.skipped = load.out.skipped + 1
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
const real: string | null = pkiRootsRegular(path)
|
|
202
|
+
const text: string | null = real === null ? null : readFileSyncOrNull(real)
|
|
203
|
+
if (text === null) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
load.files = load.files + 1
|
|
207
|
+
load.out.sources.push(path)
|
|
208
|
+
if (toI32(text.length) > PKI_ROOTS_MAX_FILE_BYTES) {
|
|
209
|
+
load.out.skipped = load.out.skipped + 1
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
pkiRootsSplit(load, text)
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const pkiRootsIsDigit = (c: i32): boolean => c >= 48 && c <= 57
|
|
217
|
+
|
|
218
|
+
const pkiRootsIsHex = (c: i32): boolean => pkiRootsIsDigit(c) || (c >= 97 && c <= 102)
|
|
219
|
+
|
|
220
|
+
/** Whether a directory entry is one a trust directory holds: `*.pem`, `*.crt`, or the hash form `xxxxxxxx.N`. */
|
|
221
|
+
export const pkiRootsDirEntry = (name: string): boolean => {
|
|
222
|
+
if (name.endsWith(".pem") || name.endsWith(".crt")) {
|
|
223
|
+
return name.length > 4
|
|
224
|
+
}
|
|
225
|
+
const n: i32 = toI32(name.length)
|
|
226
|
+
if (n < 10 || toI32(name.charCodeAt(8)) !== 46) {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
for (let i: i32 = 0; i < 8 && i < toI32(name.length); i = i + 1) {
|
|
230
|
+
if (!pkiRootsIsHex(toI32(name.charCodeAt(i)))) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (let i: i32 = 9; i < n; i = i + 1) {
|
|
235
|
+
if (!pkiRootsIsDigit(toI32(name.charCodeAt(i)))) {
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Every trust entry of the directory `dir`, in byte order; a directory that cannot be read gives nothing. */
|
|
243
|
+
const pkiRootsDir = (load: PkiRootsLoad, dir: string): void => {
|
|
244
|
+
const names: string[] | null = readdirSync(dir)
|
|
245
|
+
if (names === null) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
const base: string = dir.endsWith("/") ? dir : `${dir}/`
|
|
249
|
+
for (let i: i32 = 0; i < toI32(names.length); i = i + 1) {
|
|
250
|
+
const name: string = names[i]
|
|
251
|
+
if (pkiRootsDirEntry(name)) {
|
|
252
|
+
pkiRootsFile(load, `${base}${name}`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The roots of an explicit lookup: `file` when it is not `null`, then the
|
|
259
|
+
* first of `bundles` that reads as a file, then every directory of `dirs`, in
|
|
260
|
+
* that order. A path that cannot be read contributes nothing; a duplicate DER
|
|
261
|
+
* is kept once, where it was first read.
|
|
262
|
+
*/
|
|
263
|
+
export const pkiLoadRoots = (file: string | null, dirs: string[], bundles: string[]): PkiRoots => {
|
|
264
|
+
const load: PkiRootsLoad = new PkiRootsLoad()
|
|
265
|
+
if (file !== null) {
|
|
266
|
+
pkiRootsFile(load, file)
|
|
267
|
+
}
|
|
268
|
+
for (let i: i32 = 0; i < toI32(bundles.length); i = i + 1) {
|
|
269
|
+
if (pkiRootsFile(load, bundles[i])) {
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (let i: i32 = 0; i < toI32(dirs.length); i = i + 1) {
|
|
274
|
+
pkiRootsDir(load, dirs[i])
|
|
275
|
+
}
|
|
276
|
+
return load.out
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The non-empty components of a `:`-separated directory list. */
|
|
280
|
+
const pkiRootsSplitDirs = (list: string): string[] => {
|
|
281
|
+
const out: string[] = []
|
|
282
|
+
let start: i32 = 0
|
|
283
|
+
const n: i32 = toI32(list.length)
|
|
284
|
+
for (let i: i32 = 0; i <= n; i = i + 1) {
|
|
285
|
+
if (i >= n || (i >= 0 && i < toI32(list.length) && toI32(list.charCodeAt(i)) === 58)) {
|
|
286
|
+
if (i > start) {
|
|
287
|
+
out.push(list.slice(start, i))
|
|
288
|
+
}
|
|
289
|
+
start = i + 1
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* `pkiLoadSystemRoots` with the two variables' values given, and the defaults
|
|
297
|
+
* it falls back to: what it does with the environment, without reading it, so
|
|
298
|
+
* the order can be tested against fixture trees rather than the host's `/etc`.
|
|
299
|
+
* `certFile` and `certDir` are `SSL_CERT_FILE` and `SSL_CERT_DIR`, `null`
|
|
300
|
+
* when unset; an empty value counts as unset.
|
|
301
|
+
*/
|
|
302
|
+
export const pkiLoadRootsFor = (
|
|
303
|
+
certFile: string | null,
|
|
304
|
+
certDir: string | null,
|
|
305
|
+
dirs: string[],
|
|
306
|
+
bundles: string[]
|
|
307
|
+
): PkiRoots => {
|
|
308
|
+
const file: string | null = certFile !== null && certFile.length > 0 ? certFile : null
|
|
309
|
+
const dir: string | null = certDir !== null && certDir.length > 0 ? certDir : null
|
|
310
|
+
if (file === null && dir === null) {
|
|
311
|
+
return pkiLoadRoots(null, dirs, bundles)
|
|
312
|
+
}
|
|
313
|
+
return pkiLoadRoots(file, dir === null ? [] : pkiRootsSplitDirs(dir), [])
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The roots this program should trust: those `SSL_CERT_FILE` and
|
|
318
|
+
* `SSL_CERT_DIR` name when either is set and not empty, and otherwise the
|
|
319
|
+
* first of `pkiSystemBundles()` that exists and `/etc/ssl/certs`.
|
|
320
|
+
*/
|
|
321
|
+
export const pkiLoadSystemRoots = (): PkiRoots =>
|
|
322
|
+
pkiLoadRootsFor(getenv("SSL_CERT_FILE"), getenv("SSL_CERT_DIR"), [PKI_SYSTEM_DIR], pkiSystemBundles())
|