Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/verify/hand.mjs

6.4 KiB, 1 run

created by r2519314175:1023, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify/hand.mjs — seal the machine hand's published source.
2//
3// The served bundle is sealed by `verify/manifest.mjs`, and the claim it carries is strong: the
4// wasm rebuilds byte for byte, so the code a browser runs can be shown to BE the public source.
5// The hand cannot make that claim and this file does not pretend otherwise. It is a native binary
6// built by whoever installs it, on their own toolchain, on their own machine; there is no
7// published binary to compare against, and a Rust release build is not bit-identical across
8// toolchain versions anyway.
9//
10// What can be claimed is narrower and still worth having: **this is exactly the source, and this
11// is exactly how it is built.** So this writes `verify/hand.json` — a SHA-256 for every file of
12// the published hand, one `source` hash over them all, the pinned toolchain, and the one command
13// that turns the first into a binary. A reader who clones the mirror can confirm their copy is
14// the sealed one (`node verify/check.mjs --hand`) before they build and install a program that
15// runs commands on their computer.
16//
17// node verify/hand.mjs # seal ../daimond-oss/hand
18// node verify/hand.mjs --root DIR # some other tree
19// node verify/hand.mjs --no-lock # do not regenerate the lock first
20//
21// **It reads the PUBLIC tree, not this one**, for the same reason the bundle is built there: the
22// two differ where it matters. This tree's `hand/Cargo.toml` depends on fe2o3 by path, into a
23// working copy that exists on no machine but the author's; the mirror's pins it by git revision.
24// Sealing the development copy would seal a manifest nobody outside can build, and every reader's
25// check would fail against an honest tree. So the order at release time is: carve, seal the hand
26// from what the carve produced, carve again to carry `hand.json` across.
27//
28// Regenerating `Cargo.lock` is part of sealing rather than part of carving. The lock is the
29// mirror's own file — it follows from the mirror's git pin, not from this tree's paths — and it
30// records the exact dependency versions this release resolved to, which is the difference between
31// "the same source" and "the same build inputs".
32
33import { readFile, writeFile, stat } from 'node:fs/promises';
34import { join, normalize, resolve } from 'node:path';
35import { fileURLToPath } from 'node:url';
36import { spawnSync } from 'node:child_process';
37import { hashTree, bundleHash } from './lib.mjs';
38
39const HERE = normalize(join(fileURLToPath(import.meta.url), '..'));
40const DEV = normalize(join(HERE, '..'));
41const MIRROR = process.env.MIRROR || normalize(join(DEV, '..', 'daimond-oss'));
42const args = process.argv.slice(2);
43const opt = (name, def = null) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : def; };
44const ROOT = resolve(opt('--root', join(MIRROR, 'hand')));
45const NO_LOCK = args.includes('--no-lock');
46
47/// Everything under the hand except its build output. `target/` is its own cargo workspace's, so
48/// it does not fall under the root `target/` the rest of the repository ignores.
49const EXCLUDE_DIRS = ['target/'];
50
51/// The exact command that turns this source into the binary a browser starts.
52///
53/// `--manifest-path`, never `-p`: the hand is its own cargo workspace, so `-p daimond-hand` from
54/// the repository root fails with "package not found" — which reads like a missing crate rather
55/// than like the workspace boundary it is.
56const BUILD = 'cargo build --release --manifest-path hand/Cargo.toml';
57const BINARY = 'hand/target/release/daimond-hand';
58
59/// The toolchain the repository pins, which is what `rustup` will select for the build above.
60async function toolchain(dir) {
61 try {
62 const text = await readFile(join(dir, 'rust-toolchain.toml'), 'utf8');
63 const m = /^\s*channel\s*=\s*"([^"]+)"/m.exec(text);
64 return m ? m[1] : '';
65 } catch (e) { return ''; }
66}
67
68if (!await stat(ROOT).then(s => s.isDirectory(), () => false)) {
69 console.error(`no hand to seal at ${ROOT}`);
70 console.error(`Carve first — \`node dev/publish.mjs\` — then seal what it produced.`);
71 process.exit(2);
72}
73
74// The same refusal the carve makes, at the second place it matters. A `path` dependency here
75// means the development tree got sealed by mistake, and the resulting hand.json would describe a
76// build that resolves only inside one working copy.
77{
78 const man = await readFile(join(ROOT, 'Cargo.toml'), 'utf8');
79 const paths = man.split('\n').filter(l => /^\s*[A-Za-z0-9_-]+\s*=.*\bpath\s*=/.test(l));
80 if (paths.length) {
81 console.error(`REFUSING TO SEAL ${ROOT}/Cargo.toml — it depends on a path:`);
82 for (const l of paths) { console.error(` ${l.trim()}`); }
83 console.error(`\nThat resolves only inside the tree it was written in, so a reader could not build it.`);
84 console.error(`Seal the PUBLIC tree: \`node dev/publish.mjs\` rewrites those into git pins.`);
85 process.exit(2);
86 }
87}
88
89if (!NO_LOCK) {
90 const r = spawnSync('cargo', ['generate-lockfile', '--manifest-path', join(ROOT, 'Cargo.toml')],
91 { encoding: 'utf8' });
92 if (r.status !== 0) {
93 console.error(`could not resolve the hand's dependencies, so there is nothing honest to seal:`);
94 console.error((r.stderr || '').trim().split('\n').slice(-8).join('\n'));
95 console.error(`\n(--no-lock seals the lock already there, if that is what you meant.)`);
96 process.exit(1);
97 }
98 console.log(`Cargo.lock resolved against ${ROOT}/Cargo.toml`);
99}
100
101const files = await hashTree(ROOT, { exclude: new Set(), excludeDirs: EXCLUDE_DIRS, excludeSuffixes: [] });
102const source = bundleHash(files);
103const sealed = {
104 algo: 'sha-256',
105 source,
106 toolchain: await toolchain(MIRROR),
107 build: BUILD,
108 binary: BINARY,
109 files,
110};
111
112// Indented and key-sorted, unlike `www/manifest.json`, because this file is READ. A change to the
113// hand shows up in a commit diff as the one line that moved, which is the whole point of sealing
114// a component nobody can reproduce bit for bit: the record has to be legible to be useful.
115await writeFile(join(HERE, 'hand.json'), JSON.stringify(sealed, null, '\t') + '\n');
116
117console.log(`hand.json → ${Object.keys(files).length} files, source ${source.slice(0, 16)}…`);
118console.log(` toolchain ${sealed.toolchain || '(none pinned)'}`);
119console.log(` build ${BUILD}`);
120console.log(`\nThis seals the SOURCE and the build recipe. It does not seal a binary, and no`);
121console.log(`reproducibility is claimed for one — see "The machine hand" in verify/README.md.`);
122console.log(`Carry it across with a second \`node dev/publish.mjs\`, then commit both trees.`);