Decouples bundled-add-on updates from Theseus releases. An add-on
whose addon.json declares an updateURL can be republished at any time
without shipping a new Theseus installer; existing installs pick it up
on the next boot's +30 s background check.
Client flow (main-process only, no UI touchpoints in this commit):
initAddons()
├── promoteStagedUpdates() # promote signed stage if newer
├── seedBundledAddons() # bundle wins over on-disk if newer
└── AddonHost.discoverAndActivate()
30 s later:
└── checkAndStageUpdates() # fetch, verify, download, stage
Signature: Ed25519 over
"silentmode.addon-update-v1|<id>|<version>|<tarball-sha256>",
verified against a hardcoded set of operator pubkeys living in
addon-update-pubkeys.js. Domain-separated so the operator key can't
be tricked into signing a message with a different purpose. Empty
pubkey array is the shipping default — checkAndStageUpdates() then
short-circuits and no outbound requests are made, which is the safe
posture until the operator ceremonies a key in.
Payload: gzipped tar, extracted with the system tar (present on
Win10 1803+, macOS, Linux). Path traversal defended by tar's default
refusal of `..` entries; the extracted manifest's id + version are
re-checked against the signed values before staging.
Staged updates go to <userData>/addons-updates-staged/<id>-<version>/.
Promotion into <userData>/addons/<id>/ reuses seedBundledAddons's
backup dance: existing folder moves to
<userData>/addons-backups/<id>-<oldver>-<timestamp>/ so any local
edits survive.
New files:
- addon-updater.js — client
- addon-update-pubkeys.js — hardcoded pubkeys (empty; edit + rebuild to rotate)
- scripts/generate-update-keypair.mjs — one-time keygen
- scripts/sign-addon-update.mjs — operator packager+signer
- docs/ADDON-UPDATES.md — operator brief + threat model
Wired into main.js at boot; screenshot add-on's addon.json advertises
the reference updateURL for when the endpoint goes live.
41 lines
1.4 KiB
JavaScript
41 lines
1.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Generate an Ed25519 keypair for signing add-on updates.
|
|
//
|
|
// node scripts/generate-update-keypair.mjs <out-dir>
|
|
//
|
|
// Writes:
|
|
// <out-dir>/addon-update-key.pem — PKCS#8 PEM private key (keep secret)
|
|
// Prints:
|
|
// <64-char hex pubkey> — copy into addon-update-pubkeys.js
|
|
//
|
|
// The out-dir MUST NOT be inside the repo. Recommended: a machine-local
|
|
// ops folder outside git, e.g. `%USERPROFILE%\.silentmode\ops`.
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import crypto from "node:crypto";
|
|
import process from "node:process";
|
|
|
|
const outDir = process.argv[2];
|
|
if (!outDir) {
|
|
console.error("usage: node scripts/generate-update-keypair.mjs <out-dir>");
|
|
process.exit(2);
|
|
}
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
const keyPath = path.join(outDir, "addon-update-key.pem");
|
|
if (fs.existsSync(keyPath)) {
|
|
console.error(`refuse to overwrite existing key at ${keyPath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
fs.writeFileSync(keyPath, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o600 });
|
|
|
|
// Extract the 32-byte raw pubkey from the SPKI DER.
|
|
const der = publicKey.export({ type: "spki", format: "der" });
|
|
const raw = der.subarray(der.length - 32);
|
|
const hex = raw.toString("hex");
|
|
|
|
console.log(`private key written to ${keyPath}`);
|
|
console.log(`public key (paste into addon-update-pubkeys.js PUBKEYS_HEX):`);
|
|
console.log(` "${hex}"`);
|