fallow 3.36.0 → 3.38.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.
@@ -1,809 +0,0 @@
1
- const test = require("node:test");
2
- const assert = require("node:assert/strict");
3
- const crypto = require("node:crypto");
4
- const fs = require("node:fs");
5
- const os = require("node:os");
6
- const path = require("node:path");
7
-
8
- const {
9
- _verifyWithKey,
10
- isPreSigningVersion,
11
- describeSigMissing,
12
- verifyBinaryAt,
13
- verifyDigestAt,
14
- verifyInstalled,
15
- verifyInstalledSync,
16
- sha256Hex,
17
- normalizeDigest,
18
- readEmbeddedDigest,
19
- EMBEDDED_PUBLIC_KEY,
20
- ED25519_SPKI_HEADER,
21
- SKIP_ENV,
22
- } = require("./verify-binary");
23
- const { getPlatformPackage } = require("./platform-package");
24
-
25
- function makeDigestProvider(_dir) {
26
- return ({ binaryPath }) => {
27
- const data = fs.readFileSync(binaryPath);
28
- return Promise.resolve("sha256:" + crypto.createHash("sha256").update(data).digest("hex"));
29
- };
30
- }
31
-
32
- function makeMismatchedDigestProvider() {
33
- return () => Promise.resolve("sha256:" + "a".repeat(64));
34
- }
35
-
36
- function makeKeypair() {
37
- const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
38
- const spki = publicKey.export({ format: "der", type: "spki" });
39
- const rawPub = spki.subarray(spki.length - 32);
40
- return { privateKey, rawPub };
41
- }
42
-
43
- function makeFixture(binaryBytes, signFn) {
44
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
45
- const binaryPath = path.join(dir, "fallow");
46
- fs.writeFileSync(binaryPath, binaryBytes);
47
- if (signFn) {
48
- fs.writeFileSync(`${binaryPath}.sig`, signFn(binaryBytes));
49
- }
50
- return { dir, binaryPath };
51
- }
52
-
53
- function cleanup(dir) {
54
- fs.rmSync(dir, { recursive: true, force: true });
55
- }
56
-
57
- test("embedded public key is 32 bytes and SPKI header is 12 bytes", () => {
58
- assert.equal(EMBEDDED_PUBLIC_KEY.length, 32);
59
- assert.equal(ED25519_SPKI_HEADER.length, 12);
60
- });
61
-
62
- test("embedded public key reconstructs a valid Ed25519 SPKI key", () => {
63
- const spki = Buffer.concat([ED25519_SPKI_HEADER, EMBEDDED_PUBLIC_KEY]);
64
- const key = crypto.createPublicKey({ key: spki, format: "der", type: "spki" });
65
- assert.equal(key.asymmetricKeyType, "ed25519");
66
- });
67
-
68
- test("_verifyWithKey returns ok for a valid signature", () => {
69
- const { privateKey, rawPub } = makeKeypair();
70
- const content = Buffer.from("hello world");
71
- const { dir, binaryPath } = makeFixture(content, (data) => crypto.sign(null, data, privateKey));
72
- try {
73
- const result = _verifyWithKey(binaryPath, rawPub);
74
- assert.deepEqual(result, { ok: true });
75
- } finally {
76
- cleanup(dir);
77
- }
78
- });
79
-
80
- test("_verifyWithKey returns sig-invalid when the signature is corrupted", () => {
81
- const { privateKey, rawPub } = makeKeypair();
82
- const content = Buffer.from("hello world");
83
- const { dir, binaryPath } = makeFixture(content, (data) => crypto.sign(null, data, privateKey));
84
- try {
85
- const sig = fs.readFileSync(`${binaryPath}.sig`);
86
- sig[0] ^= 0xff;
87
- fs.writeFileSync(`${binaryPath}.sig`, sig);
88
- const result = _verifyWithKey(binaryPath, rawPub);
89
- assert.equal(result.ok, false);
90
- assert.equal(result.code, "sig-invalid");
91
- } finally {
92
- cleanup(dir);
93
- }
94
- });
95
-
96
- test("_verifyWithKey returns sig-invalid for the wrong key", () => {
97
- const { rawPub } = makeKeypair();
98
- const wrongKey = makeKeypair();
99
- const content = Buffer.from("hello world");
100
- const { dir, binaryPath } = makeFixture(content, (data) =>
101
- crypto.sign(null, data, wrongKey.privateKey),
102
- );
103
- try {
104
- const result = _verifyWithKey(binaryPath, rawPub);
105
- assert.equal(result.ok, false);
106
- assert.equal(result.code, "sig-invalid");
107
- } finally {
108
- cleanup(dir);
109
- }
110
- });
111
-
112
- test("_verifyWithKey returns sig-invalid when the binary bytes are tampered", () => {
113
- const { privateKey, rawPub } = makeKeypair();
114
- const original = Buffer.from("hello world");
115
- const { dir, binaryPath } = makeFixture(original, (data) => crypto.sign(null, data, privateKey));
116
- try {
117
- fs.writeFileSync(binaryPath, Buffer.from("tampered"));
118
- const result = _verifyWithKey(binaryPath, rawPub);
119
- assert.equal(result.ok, false);
120
- assert.equal(result.code, "sig-invalid");
121
- } finally {
122
- cleanup(dir);
123
- }
124
- });
125
-
126
- test("_verifyWithKey returns sig-missing when the signature file does not exist", () => {
127
- const { rawPub } = makeKeypair();
128
- const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
129
- try {
130
- const result = _verifyWithKey(binaryPath, rawPub);
131
- assert.equal(result.ok, false);
132
- assert.equal(result.code, "sig-missing");
133
- } finally {
134
- cleanup(dir);
135
- }
136
- });
137
-
138
- // The version-aware remediation lives in describeSigMissing (attached by
139
- // verifyOneBinary{,Sync}), not in the low-level _verifyWithKey result. Refs #944.
140
- test("isPreSigningVersion is true below 2.77.0 and false at/above it", () => {
141
- assert.equal(isPreSigningVersion("2.76.0"), true);
142
- assert.equal(isPreSigningVersion("2.73.0"), true);
143
- assert.equal(isPreSigningVersion("1.9.0"), true);
144
- assert.equal(isPreSigningVersion("2.77.0"), false);
145
- assert.equal(isPreSigningVersion("2.83.0"), false);
146
- assert.equal(isPreSigningVersion("2.88.2"), false);
147
- // Unknown / unparsable versions default to false so the caller uses the
148
- // cautious possible-tampering message rather than telling the user to bump.
149
- assert.equal(isPreSigningVersion("unknown"), false);
150
- assert.equal(isPreSigningVersion(undefined), false);
151
- });
152
-
153
- test("describeSigMissing gives upgrade guidance for a pre-signing version", () => {
154
- const base = { ok: false, code: "sig-missing", message: "signature not found at /x/fallow.sig" };
155
- const result = describeSigMissing(base, "2.76.0");
156
- // The security-critical invariant: enriching the message must never flip the
157
- // verdict. A sig-missing result stays a hard failure.
158
- assert.equal(result.ok, false);
159
- assert.equal(result.code, "sig-missing");
160
- assert.match(result.message, /predates signed binaries/);
161
- assert.match(result.message, /2\.77\.0/);
162
- // Names WHERE the pin lives so the user can act.
163
- assert.match(result.message, /package\.json/);
164
- // The bypass escape hatch is owned by the caller's trailer + SECURITY.md, not
165
- // surfaced inline, so it is not normalized in CI logs.
166
- assert.doesNotMatch(result.message, new RegExp(SKIP_ENV));
167
- // The original low-level detail is preserved.
168
- assert.match(result.message, /signature not found/);
169
- });
170
-
171
- test("describeSigMissing flags possible tampering for a signed-era version", () => {
172
- const base = { ok: false, code: "sig-missing", message: "signature not found at /x/fallow.sig" };
173
- const result = describeSigMissing(base, "2.83.0");
174
- assert.equal(result.ok, false);
175
- assert.equal(result.code, "sig-missing");
176
- assert.match(result.message, /tampered with or incomplete/);
177
- assert.match(result.message, /Reinstall/);
178
- // Must NOT nudge a possible-tampering victim toward bypassing verification.
179
- assert.doesNotMatch(result.message, new RegExp(SKIP_ENV));
180
- });
181
-
182
- test("describeSigMissing passes non-sig-missing results through untouched", () => {
183
- const base = { ok: false, code: "digest-mismatch", message: "digest mismatch" };
184
- const result = describeSigMissing(base, "2.76.0");
185
- assert.equal(result.message, "digest mismatch");
186
- });
187
-
188
- test("_verifyWithKey returns binary-missing when the binary does not exist", () => {
189
- const { rawPub } = makeKeypair();
190
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
191
- try {
192
- const result = _verifyWithKey(path.join(dir, "nonexistent"), rawPub);
193
- assert.equal(result.ok, false);
194
- assert.equal(result.code, "binary-missing");
195
- } finally {
196
- cleanup(dir);
197
- }
198
- });
199
-
200
- test("_verifyWithKey returns sig-invalid when the signature length is wrong", () => {
201
- const { rawPub } = makeKeypair();
202
- const { dir, binaryPath } = makeFixture(Buffer.from("hello"));
203
- try {
204
- fs.writeFileSync(`${binaryPath}.sig`, Buffer.from("short"));
205
- const result = _verifyWithKey(binaryPath, rawPub);
206
- assert.equal(result.ok, false);
207
- assert.equal(result.code, "sig-invalid");
208
- } finally {
209
- cleanup(dir);
210
- }
211
- });
212
-
213
- test("_verifyWithKey throws when given a non-32-byte raw public key", () => {
214
- const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
215
- try {
216
- const result = _verifyWithKey(binaryPath, Buffer.from("too short"));
217
- assert.equal(result.ok, false);
218
- assert.equal(result.code, "sig-missing");
219
- } finally {
220
- cleanup(dir);
221
- }
222
- });
223
-
224
- test("verifyBinaryAt uses the embedded production public key", () => {
225
- // The embedded key cannot sign our test data because we do not have the
226
- // private key, so we only assert that verifyBinaryAt returns sig-invalid
227
- // for a random signature against the production key, not the underlying
228
- // crypto throwing. This locks in that the public API uses the embedded
229
- // key path.
230
- const { privateKey } = makeKeypair();
231
- const content = Buffer.from("hello world");
232
- const { dir, binaryPath } = makeFixture(content, (data) => crypto.sign(null, data, privateKey));
233
- try {
234
- const result = verifyBinaryAt(binaryPath);
235
- assert.equal(result.ok, false);
236
- assert.equal(result.code, "sig-invalid");
237
- } finally {
238
- cleanup(dir);
239
- }
240
- });
241
-
242
- // Mirror binaryTargetsForPlatform: the suffix comes from the platformId under
243
- // test, never from the live process.platform. A `dirOverride` run without an
244
- // explicit platformId is labelled "test-platform" by
245
- // resolvePlatformPackageForVerify, so the binary it looks for is plain `fallow`
246
- // on every host -- a fixture keyed off process.platform writes `fallow.exe` on a
247
- // Windows host and the verify then finds nothing.
248
- function extForPlatformId(platformId) {
249
- return typeof platformId === "string" && platformId.startsWith("win32") ? ".exe" : "";
250
- }
251
-
252
- function makePlatformDir(privateKey, options) {
253
- const opts = options || {};
254
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
255
- const ext = extForPlatformId(opts.platformId);
256
- for (const base of ["fallow", "fallow-similar-code"]) {
257
- const binaryPath = path.join(dir, `${base}${ext}`);
258
- const content = Buffer.from(`mock ${base} contents`);
259
- fs.writeFileSync(binaryPath, content);
260
- if (opts.skipSigFor === base) {
261
- continue;
262
- }
263
- const data = opts.corruptBinaryFor === base ? Buffer.from("tampered") : content;
264
- const sig = crypto.sign(null, data, privateKey);
265
- if (opts.corruptSigFor === base) {
266
- sig[0] ^= 0xff;
267
- }
268
- fs.writeFileSync(`${binaryPath}.sig`, sig);
269
- }
270
- return dir;
271
- }
272
-
273
- function currentPlatformPackage() {
274
- if (process.platform !== "linux") {
275
- return getPlatformPackage(process.platform, process.arch);
276
- }
277
- let libcFamily;
278
- try {
279
- libcFamily = require("detect-libc").familySync();
280
- } catch {
281
- libcFamily = undefined;
282
- }
283
- return getPlatformPackage(process.platform, process.arch, libcFamily);
284
- }
285
-
286
- test("normalizeDigest accepts sha256: prefix and bare hex", () => {
287
- const sample = "a".repeat(64);
288
- assert.equal(normalizeDigest("sha256:" + sample), sample);
289
- assert.equal(normalizeDigest(sample), sample);
290
- assert.equal(normalizeDigest("SHA256:" + sample.toUpperCase()), sample);
291
- });
292
-
293
- test("normalizeDigest rejects malformed digests", () => {
294
- assert.equal(normalizeDigest(null), null);
295
- assert.equal(normalizeDigest(""), null);
296
- assert.equal(normalizeDigest("not-hex"), null);
297
- assert.equal(normalizeDigest("a".repeat(63)), null);
298
- });
299
-
300
- test("sha256Hex returns 64-char hex over file bytes", () => {
301
- const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
302
- try {
303
- const result = sha256Hex(binaryPath);
304
- assert.equal(result.ok, true);
305
- assert.equal(
306
- result.digest,
307
- crypto.createHash("sha256").update(Buffer.from("hello world")).digest("hex"),
308
- );
309
- } finally {
310
- cleanup(dir);
311
- }
312
- });
313
-
314
- test("verifyDigestAt accepts matching digest and rejects mismatched", () => {
315
- const { dir, binaryPath } = makeFixture(Buffer.from("hello world"));
316
- try {
317
- const correct = crypto.createHash("sha256").update(Buffer.from("hello world")).digest("hex");
318
- assert.deepEqual(verifyDigestAt(binaryPath, "sha256:" + correct), { ok: true });
319
- const wrong = "b".repeat(64);
320
- const bad = verifyDigestAt(binaryPath, wrong);
321
- assert.equal(bad.ok, false);
322
- assert.equal(bad.code, "digest-mismatch");
323
- } finally {
324
- cleanup(dir);
325
- }
326
- });
327
-
328
- test("verifyInstalled with dirOverride returns ok when every binary verifies", async (t) => {
329
- const { privateKey, rawPub } = makeKeypair();
330
- const dir = makePlatformDir(privateKey);
331
- t.after(() => cleanup(dir));
332
- const result = await verifyInstalled({
333
- dirOverride: dir,
334
- verifyFn: (p) => _verifyWithKey(p, rawPub),
335
- digestProvider: makeDigestProvider(dir),
336
- });
337
- assert.equal(result.ok, true);
338
- assert.equal(result.package, "<override>");
339
- });
340
-
341
- // binaryTargetsForPlatform reads windows-ness off the platformId so a Windows
342
- // verify can be synthesized anywhere. Nothing exercised that, which left the
343
- // `.exe` target unverified on Linux CI and unverified on Windows too.
344
- test("verifyInstalled verifies the .exe target for a win32 platformId on any host", async (t) => {
345
- const { privateKey, rawPub } = makeKeypair();
346
- const platformId = "win32-x64-msvc";
347
- const dir = makePlatformDir(privateKey, { platformId });
348
- t.after(() => cleanup(dir));
349
- assert.ok(fs.existsSync(path.join(dir, "fallow.exe")), "fixture must write the .exe target");
350
-
351
- const result = await verifyInstalled({
352
- dirOverride: dir,
353
- platformId,
354
- verifyFn: (p) => _verifyWithKey(p, rawPub),
355
- digestProvider: makeDigestProvider(dir),
356
- });
357
- assert.equal(result.ok, true);
358
- });
359
-
360
- test("verifyInstalled reports the .exe name when a win32 signature is absent", async (t) => {
361
- const { privateKey, rawPub } = makeKeypair();
362
- const platformId = "win32-arm64-msvc";
363
- const dir = makePlatformDir(privateKey, { platformId, skipSigFor: "fallow" });
364
- t.after(() => cleanup(dir));
365
-
366
- const result = await verifyInstalled({
367
- dirOverride: dir,
368
- platformId,
369
- verifyFn: (p) => _verifyWithKey(p, rawPub),
370
- digestProvider: makeDigestProvider(dir),
371
- });
372
- assert.equal(result.ok, false);
373
- assert.equal(result.code, "sig-missing");
374
- assert.match(result.message, /fallow\.exe/);
375
- });
376
-
377
- test("verifyInstalled resolves a global npm install from the fallow package directory", async (t) => {
378
- const pkg = currentPlatformPackage();
379
- if (!pkg) {
380
- t.skip("unsupported platform");
381
- return;
382
- }
383
-
384
- const { privateKey, rawPub } = makeKeypair();
385
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-global-"));
386
- t.after(() => cleanup(root));
387
-
388
- const resolveFrom = path.join(root, "node_modules", "fallow");
389
- const platformDir = path.join(root, "node_modules", ...pkg.split("/"));
390
- fs.mkdirSync(resolveFrom, { recursive: true });
391
- fs.mkdirSync(platformDir, { recursive: true });
392
- fs.writeFileSync(
393
- path.join(platformDir, "package.json"),
394
- JSON.stringify({ name: pkg, version: "9.9.9" }),
395
- );
396
-
397
- const ext = process.platform === "win32" ? ".exe" : "";
398
- for (const base of ["fallow", "fallow-similar-code"]) {
399
- const binaryPath = path.join(platformDir, `${base}${ext}`);
400
- const content = Buffer.from(`global install ${base}`);
401
- fs.writeFileSync(binaryPath, content);
402
- fs.writeFileSync(`${binaryPath}.sig`, crypto.sign(null, content, privateKey));
403
- }
404
-
405
- const result = await verifyInstalled({
406
- resolveFrom,
407
- verifyFn: (p) => _verifyWithKey(p, rawPub),
408
- digestProvider: ({ binaryPath }) =>
409
- crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
410
- });
411
- assert.equal(result.ok, true);
412
- assert.equal(result.package, pkg);
413
- assert.equal(result.version, "9.9.9");
414
- });
415
-
416
- test("verifyInstalled with dirOverride fails on a bad signature", async (t) => {
417
- const { privateKey, rawPub } = makeKeypair();
418
- const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow" });
419
- t.after(() => cleanup(dir));
420
- const result = await verifyInstalled({
421
- dirOverride: dir,
422
- verifyFn: (p) => _verifyWithKey(p, rawPub),
423
- digestProvider: makeDigestProvider(dir),
424
- });
425
- assert.equal(result.ok, false);
426
- assert.equal(result.code, "sig-invalid");
427
- assert.match(result.binary, /fallow/);
428
- });
429
-
430
- test("verifyInstalled with dirOverride reports sig-missing when a .sig is absent", async (t) => {
431
- const { privateKey, rawPub } = makeKeypair();
432
- const dir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
433
- t.after(() => cleanup(dir));
434
- const result = await verifyInstalled({
435
- dirOverride: dir,
436
- verifyFn: (p) => _verifyWithKey(p, rawPub),
437
- digestProvider: makeDigestProvider(dir),
438
- });
439
- assert.equal(result.ok, false);
440
- assert.equal(result.code, "sig-missing");
441
- assert.match(result.binary, /fallow/);
442
- });
443
-
444
- test("verifyInstalled threads the resolved version into the sig-missing message", async (t) => {
445
- const { privateKey, rawPub } = makeKeypair();
446
- const preDir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
447
- const eraDir = makePlatformDir(privateKey, { skipSigFor: "fallow" });
448
- t.after(() => {
449
- cleanup(preDir);
450
- cleanup(eraDir);
451
- });
452
- const pre = await verifyInstalled({
453
- dirOverride: preDir,
454
- version: "2.76.0",
455
- verifyFn: (p) => _verifyWithKey(p, rawPub),
456
- digestProvider: makeDigestProvider(preDir),
457
- });
458
- assert.equal(pre.code, "sig-missing");
459
- assert.match(pre.message, /predates signed binaries/);
460
- const era = await verifyInstalled({
461
- dirOverride: eraDir,
462
- version: "2.83.0",
463
- verifyFn: (p) => _verifyWithKey(p, rawPub),
464
- digestProvider: makeDigestProvider(eraDir),
465
- });
466
- assert.equal(era.code, "sig-missing");
467
- assert.match(era.message, /tampered with or incomplete/);
468
- });
469
-
470
- test("verifyInstalled reports digest-mismatch when SHA-256 disagrees with the provider", async (t) => {
471
- const { privateKey, rawPub } = makeKeypair();
472
- const dir = makePlatformDir(privateKey);
473
- t.after(() => cleanup(dir));
474
- const result = await verifyInstalled({
475
- dirOverride: dir,
476
- verifyFn: (p) => _verifyWithKey(p, rawPub),
477
- digestProvider: makeMismatchedDigestProvider(),
478
- });
479
- assert.equal(result.ok, false);
480
- assert.equal(result.code, "digest-mismatch");
481
- });
482
-
483
- test("verifyInstalled reports digest-unavailable when the provider rejects", async (t) => {
484
- const { privateKey, rawPub } = makeKeypair();
485
- const dir = makePlatformDir(privateKey);
486
- t.after(() => cleanup(dir));
487
- const result = await verifyInstalled({
488
- dirOverride: dir,
489
- verifyFn: (p) => _verifyWithKey(p, rawPub),
490
- digestProvider: () => Promise.reject(new Error("network down")),
491
- });
492
- assert.equal(result.ok, false);
493
- assert.equal(result.code, "digest-unavailable");
494
- assert.match(result.message, /network down/);
495
- });
496
-
497
- test("verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY", async (t) => {
498
- const previous = process.env[SKIP_ENV];
499
- process.env[SKIP_ENV] = "1";
500
- t.after(() => {
501
- if (previous === undefined) delete process.env[SKIP_ENV];
502
- else process.env[SKIP_ENV] = previous;
503
- });
504
- const result = await verifyInstalled({ dirOverride: "/does/not/exist" });
505
- assert.equal(result.ok, true);
506
- assert.equal(result.skipped, true);
507
- });
508
-
509
- function computeDigestsForDir(dir, platformId) {
510
- const ext = extForPlatformId(platformId);
511
- const out = {};
512
- for (const base of ["fallow", "fallow-similar-code"]) {
513
- const fileName = `${base}${ext}`;
514
- const full = path.join(dir, fileName);
515
- out[fileName] =
516
- "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex");
517
- }
518
- return out;
519
- }
520
-
521
- function writeManifest(dir, body) {
522
- fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(body));
523
- }
524
-
525
- test("readEmbeddedDigest returns null when manifest is missing", () => {
526
- assert.equal(readEmbeddedDigest("/does/not/exist/package.json", "fallow"), null);
527
- });
528
-
529
- test("readEmbeddedDigest returns null when fallowDigests field is absent", (t) => {
530
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-emb-"));
531
- t.after(() => cleanup(dir));
532
- writeManifest(dir, { name: "@fallow-cli/x", version: "1.0.0" });
533
- assert.equal(readEmbeddedDigest(path.join(dir, "package.json"), "fallow"), null);
534
- });
535
-
536
- test("readEmbeddedDigest returns null when the per-binary entry is malformed", (t) => {
537
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-emb-"));
538
- t.after(() => cleanup(dir));
539
- writeManifest(dir, {
540
- name: "@fallow-cli/x",
541
- version: "1.0.0",
542
- fallowDigests: { fallow: "not-a-real-digest" },
543
- });
544
- assert.equal(readEmbeddedDigest(path.join(dir, "package.json"), "fallow"), null);
545
- });
546
-
547
- test("readEmbeddedDigest returns normalized hex for a valid sha256: prefixed entry", (t) => {
548
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-emb-"));
549
- t.after(() => cleanup(dir));
550
- const hex = "a".repeat(64);
551
- writeManifest(dir, {
552
- name: "@fallow-cli/x",
553
- version: "1.0.0",
554
- fallowDigests: { fallow: "sha256:" + hex },
555
- });
556
- assert.equal(readEmbeddedDigest(path.join(dir, "package.json"), "fallow"), hex);
557
- });
558
-
559
- test("verifyInstalled uses the embedded digest without calling the provider", async (t) => {
560
- const { privateKey, rawPub } = makeKeypair();
561
- const dir = makePlatformDir(privateKey);
562
- t.after(() => cleanup(dir));
563
- writeManifest(dir, {
564
- name: "@fallow-cli/x",
565
- version: "1.0.0",
566
- fallowDigests: computeDigestsForDir(dir),
567
- });
568
- let providerCalls = 0;
569
- const result = await verifyInstalled({
570
- dirOverride: dir,
571
- verifyFn: (p) => _verifyWithKey(p, rawPub),
572
- digestProvider: () => {
573
- providerCalls += 1;
574
- return Promise.reject(new Error("provider should not be called"));
575
- },
576
- });
577
- assert.equal(result.ok, true, JSON.stringify(result));
578
- assert.equal(providerCalls, 0);
579
- });
580
-
581
- test("verifyInstalled falls back to the provider when the embedded digest is missing", async (t) => {
582
- const { privateKey, rawPub } = makeKeypair();
583
- const dir = makePlatformDir(privateKey);
584
- t.after(() => cleanup(dir));
585
- // No package.json at all; legacy platform package shape.
586
- let providerCalls = 0;
587
- const result = await verifyInstalled({
588
- dirOverride: dir,
589
- verifyFn: (p) => _verifyWithKey(p, rawPub),
590
- digestProvider: ({ binaryPath }) => {
591
- providerCalls += 1;
592
- return Promise.resolve(
593
- "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
594
- );
595
- },
596
- });
597
- assert.equal(result.ok, true);
598
- assert.equal(providerCalls, 2);
599
- });
600
-
601
- test("verifyInstalled falls back to the provider when fallowDigests is partial / malformed", async (t) => {
602
- const { privateKey, rawPub } = makeKeypair();
603
- const dir = makePlatformDir(privateKey);
604
- t.after(() => cleanup(dir));
605
- writeManifest(dir, {
606
- name: "@fallow-cli/x",
607
- version: "1.0.0",
608
- fallowDigests: { fallow: "not-a-real-digest" },
609
- });
610
- let providerCalls = 0;
611
- const result = await verifyInstalled({
612
- dirOverride: dir,
613
- verifyFn: (p) => _verifyWithKey(p, rawPub),
614
- digestProvider: ({ binaryPath }) => {
615
- providerCalls += 1;
616
- return Promise.resolve(
617
- "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex"),
618
- );
619
- },
620
- });
621
- assert.equal(result.ok, true);
622
- // Both shipped binaries fall back because their embedded entries are absent or malformed.
623
- assert.equal(providerCalls, 2);
624
- });
625
-
626
- test("verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary", async (t) => {
627
- const { privateKey, rawPub } = makeKeypair();
628
- const dir = makePlatformDir(privateKey);
629
- t.after(() => cleanup(dir));
630
- const ext = extForPlatformId();
631
- writeManifest(dir, {
632
- name: "@fallow-cli/x",
633
- version: "1.0.0",
634
- fallowDigests: {
635
- [`fallow${ext}`]: "sha256:" + "a".repeat(64),
636
- },
637
- });
638
- const result = await verifyInstalled({
639
- dirOverride: dir,
640
- verifyFn: (p) => _verifyWithKey(p, rawPub),
641
- digestProvider: () => Promise.reject(new Error("should not be reached")),
642
- });
643
- assert.equal(result.ok, false);
644
- assert.equal(result.code, "digest-mismatch");
645
- });
646
-
647
- test("verifyInstalled ignores skip env when allowSkipEnv is false", async (t) => {
648
- const previous = process.env[SKIP_ENV];
649
- process.env[SKIP_ENV] = "1";
650
- t.after(() => {
651
- if (previous === undefined) delete process.env[SKIP_ENV];
652
- else process.env[SKIP_ENV] = previous;
653
- });
654
- const { privateKey, rawPub } = makeKeypair();
655
- const dir = makePlatformDir(privateKey);
656
- t.after(() => cleanup(dir));
657
- const result = await verifyInstalled({
658
- dirOverride: dir,
659
- verifyFn: (p) => _verifyWithKey(p, rawPub),
660
- digestProvider: makeDigestProvider(dir),
661
- allowSkipEnv: false,
662
- });
663
- assert.equal(result.ok, true);
664
- assert.notEqual(result.skipped, true);
665
- });
666
-
667
- // ---- verifyInstalledSync (lazy first-run path) ----------------------------
668
-
669
- function makeSyncDigestProvider(_dir) {
670
- return ({ binaryPath }) =>
671
- "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex");
672
- }
673
-
674
- test("verifyInstalledSync with embedded digests returns ok end-to-end", (t) => {
675
- const { privateKey, rawPub } = makeKeypair();
676
- const dir = makePlatformDir(privateKey);
677
- t.after(() => cleanup(dir));
678
- writeManifest(dir, {
679
- name: "@fallow-cli/x",
680
- version: "9.9.9",
681
- fallowDigests: computeDigestsForDir(dir),
682
- });
683
- const result = verifyInstalledSync({
684
- dirOverride: dir,
685
- verifyFn: (p) => _verifyWithKey(p, rawPub),
686
- });
687
- assert.equal(result.ok, true);
688
- assert.equal(result.package, "<override>");
689
- });
690
-
691
- test("verifyInstalledSync fails on a bad signature", (t) => {
692
- const { privateKey, rawPub } = makeKeypair();
693
- const dir = makePlatformDir(privateKey, { corruptSigFor: "fallow" });
694
- t.after(() => cleanup(dir));
695
- writeManifest(dir, {
696
- name: "@fallow-cli/x",
697
- version: "9.9.9",
698
- fallowDigests: computeDigestsForDir(dir),
699
- });
700
- const result = verifyInstalledSync({
701
- dirOverride: dir,
702
- verifyFn: (p) => _verifyWithKey(p, rawPub),
703
- });
704
- assert.equal(result.ok, false);
705
- assert.equal(result.code, "sig-invalid");
706
- assert.match(result.binary, /fallow/);
707
- });
708
-
709
- test("verifyInstalledSync reports digest-mismatch when bytes diverge from embedded digest", (t) => {
710
- const { privateKey, rawPub } = makeKeypair();
711
- const dir = makePlatformDir(privateKey);
712
- t.after(() => cleanup(dir));
713
- writeManifest(dir, {
714
- name: "@fallow-cli/x",
715
- version: "9.9.9",
716
- fallowDigests: {
717
- fallow: "sha256:" + "a".repeat(64),
718
- "fallow.exe": "sha256:" + "a".repeat(64),
719
- },
720
- });
721
- const result = verifyInstalledSync({
722
- dirOverride: dir,
723
- verifyFn: (p) => _verifyWithKey(p, rawPub),
724
- });
725
- assert.equal(result.ok, false);
726
- assert.equal(result.code, "digest-mismatch");
727
- });
728
-
729
- test("verifyInstalledSync reports digest-unavailable when no embedded digest and no provider", (t) => {
730
- const { privateKey, rawPub } = makeKeypair();
731
- const dir = makePlatformDir(privateKey);
732
- t.after(() => cleanup(dir));
733
- writeManifest(dir, { name: "@fallow-cli/x", version: "9.9.9" });
734
- const result = verifyInstalledSync({
735
- dirOverride: dir,
736
- verifyFn: (p) => _verifyWithKey(p, rawPub),
737
- });
738
- assert.equal(result.ok, false);
739
- assert.equal(result.code, "digest-unavailable");
740
- assert.match(result.message, /predates fallow 2\.78\.1/);
741
- assert.match(result.message, new RegExp(SKIP_ENV));
742
- });
743
-
744
- test("verifyInstalledSync accepts a sync digestProvider for test isolation", (t) => {
745
- const { privateKey, rawPub } = makeKeypair();
746
- const dir = makePlatformDir(privateKey);
747
- t.after(() => cleanup(dir));
748
- // No fallowDigests on manifest; the sync provider supplies them.
749
- writeManifest(dir, { name: "@fallow-cli/x", version: "9.9.9" });
750
- const result = verifyInstalledSync({
751
- dirOverride: dir,
752
- verifyFn: (p) => _verifyWithKey(p, rawPub),
753
- digestProvider: makeSyncDigestProvider(dir),
754
- });
755
- assert.equal(result.ok, true);
756
- });
757
-
758
- test("verifyInstalledSync surfaces provider errors as digest-unavailable", (t) => {
759
- const { privateKey, rawPub } = makeKeypair();
760
- const dir = makePlatformDir(privateKey);
761
- t.after(() => cleanup(dir));
762
- writeManifest(dir, { name: "@fallow-cli/x", version: "9.9.9" });
763
- const result = verifyInstalledSync({
764
- dirOverride: dir,
765
- verifyFn: (p) => _verifyWithKey(p, rawPub),
766
- digestProvider: () => {
767
- throw new Error("disk on fire");
768
- },
769
- });
770
- assert.equal(result.ok, false);
771
- assert.equal(result.code, "digest-unavailable");
772
- assert.match(result.message, /disk on fire/);
773
- });
774
-
775
- test("verifyInstalledSync honors FALLOW_SKIP_BINARY_VERIFY", (t) => {
776
- const previous = process.env[SKIP_ENV];
777
- process.env[SKIP_ENV] = "1";
778
- t.after(() => {
779
- if (previous === undefined) delete process.env[SKIP_ENV];
780
- else process.env[SKIP_ENV] = previous;
781
- });
782
- const result = verifyInstalledSync({ dirOverride: "/does/not/exist" });
783
- assert.equal(result.ok, true);
784
- assert.equal(result.skipped, true);
785
- });
786
-
787
- test("verifyInstalledSync ignores skip env when allowSkipEnv is false", (t) => {
788
- const previous = process.env[SKIP_ENV];
789
- process.env[SKIP_ENV] = "1";
790
- t.after(() => {
791
- if (previous === undefined) delete process.env[SKIP_ENV];
792
- else process.env[SKIP_ENV] = previous;
793
- });
794
- const { privateKey, rawPub } = makeKeypair();
795
- const dir = makePlatformDir(privateKey);
796
- t.after(() => cleanup(dir));
797
- writeManifest(dir, {
798
- name: "@fallow-cli/x",
799
- version: "9.9.9",
800
- fallowDigests: computeDigestsForDir(dir),
801
- });
802
- const result = verifyInstalledSync({
803
- dirOverride: dir,
804
- verifyFn: (p) => _verifyWithKey(p, rawPub),
805
- allowSkipEnv: false,
806
- });
807
- assert.equal(result.ok, true);
808
- assert.notEqual(result.skipped, true);
809
- });