diff --git a/docs/DESIGN-one-extension-catalogue.md b/docs/DESIGN-one-extension-catalogue.md index aac49fb..6dc8705 100644 --- a/docs/DESIGN-one-extension-catalogue.md +++ b/docs/DESIGN-one-extension-catalogue.md @@ -35,6 +35,49 @@ is a list of ids in the app's config, not a folder that gets copied. Changing the default set should be editing that list, not moving directories around and rebuilding. +## Which keys may sign what + +Multiple signing keys already work, and there is already an allow-list: the +`PUBKEYS_HEX` array in `addon-update-pubkeys.js`. `verifySignature` walks it +and accepts an update if any entry verifies, so adding a second operator key +is a line in that file plus a Theseus release. The file's own comment sets +out the rotation: sign with both, ship a release carrying both, then drop +the old one. + +The problem is not that the list is missing. It is that the list is +**global**. Every add-on is verified against the same array, so any key on +it can sign an update for any id — a key meant for the screenshot tool can +push code for the wallet. The signed message is +`silentmode.addon-update-v1|||`, which binds a +signature to an extension, but nothing binds an extension to a signer. + +The publisher path does not have this problem. An entry carrying +`publisherSig` is checked against the current owner of that extension's +name, so the permission is per-extension by construction and rotates on +chain without an app release. That is the model to generalise, not replace. + +So: keep an allow-list, and scope it. + +- **Per-extension, not global.** Each extension names who may sign it — a + BNS name for anything in the catalogue, or a pinned pubkey for a member of + the default set. `verifySignature` takes that extension's keys instead of + the global array, and its callers look the binding up by id. +- **Plural per extension.** Two or three acceptable keys, any one + sufficient, so a rotation or a lost key doesn't need an emergency release. +- **Prefer the name-owner path for anything new.** The chain is an + allow-list that can be corrected the day a key is lost; a compiled-in + array can only be corrected by shipping a build. +- **Consider co-signing for the default set alone.** Those extensions reach + every fresh profile without anyone choosing them, which makes a single + compromised key worst exactly there. Requiring two signatures is a bounded + amount of work when it applies to a handful of ids rather than to the + whole catalogue. + +The narrowing itself is small — `verifySignature` and its two callers. The +work is in deciding where the binding lives and showing it honestly in +Settings, so "who is allowed to update this" is something a user can read +rather than infer. + ## The one thing that needs care If defaults are fetched from the catalogue on first run, a fresh install with @@ -66,7 +109,9 @@ everywhere else. row, next to the publisher's name. The separate Community section and its Install button fold into the single catalogue view. - **`addon-updater.js`**: drop the distinction between the two channel - shapes; an extension has a publisher and an updates URL. + shapes; an extension has a publisher and an updates URL. `verifySignature` + takes the keys allowed for *that* id rather than the global array, and + `pickChannelEntry` resolves the binding before it verifies anything. - **Gateway**: the catalogue gains first-party entries. Nothing changes structurally — `/api/ext` already stores whatever a name owner signs. - **Publishing**: `scripts/sign-addon-update.mjs` and the operator key stop @@ -85,3 +130,15 @@ everywhere else. - **Does the operator key survive at all?** Keeping it as an accepted signer for the default set is a hedge against the name-owner key being lost; keeping it forever means the two-rules problem never fully goes away. +- **Where does a per-extension key binding live?** In the manifest, where + the extension states its own publisher and an attacker who can rewrite the + manifest can restate it; in the app's config, where it is trustworthy but + only changes with a release; or on chain, where the name already answers + the question. Probably: on chain for the catalogue, pinned in config for + the default set, and never simply believed from the manifest. +- **What happens to an extension whose signer is no longer allowed?** It + should stop updating, but stopping it from *running* is a different and + much ruder decision, and the two shouldn't be conflated by accident. +- **Does co-signing need a threshold, or just two named keys?** Two keys is + simple to implement and to explain; m-of-n is where this quietly turns + into key management.