Repository navigation
Signed app updates (update zip hash comes from the same host as the zip) #40
Description
Activity
Related: PR #33 (Windows signer pin) should be designed together with this one. Both pins need to live in the built app, not in the manifest.
This design is the right anchor — and it composes cleanly with the baked-pin idea from #33 (they protect different links of the chain; both are worth having):
- ed25519 over the manifest answers "who authorized this release" — survives even a compromised update host, because the private key never lives there.
- The baked Authenticode pin (feat(update): Windows update provenance — pin the signer in the manifest #33 sketch) answers "is this exe the one my publisher signs" — an OS-verifiable anchor that also covers the
launcher.exethe zip swaps beside the app.
Defense in depth, not either/or.
Your ed25519 question, answered empirically: txiki's WebCrypto has Ed25519. Tested against the bundled runtime (
bin/tjs, v26.6.0, macOS arm64) — keygen,sign,verifyround-trip, and tampered-signature rejection all pass:$ tjs eval 'await crypto.subtle.generateKey("Ed25519", true, ["sign","verify"])' → OKcrypto.subtleis the WebCrypto global (nottjs:crypto), so no pure-JS verifier dependency is needed: keygen,signandverifyall run in the backend, andtinyjs publishcan use the exact same calls so the two sides can't disagree on curve or encoding. One caveat worth a one-line check on the release runners: Ed25519 in WebCrypto is still flag-gated in some engines — if a platform trips,@noble/ed25519is the documented fallback.Key rotation, in increasing strength:
- Ship a second baked key (
publicKeys: [primary, next]) — verify against any, publish with one; deploy the new primary by shipping an app update (which the old key authorized). The chain never breaks. - For the paranoid: put
nextbehind a threshold of existing keys. Probably overkill here.
Rotation bootstraps through an authorized update, which is the elegant property of signing the manifest rather than the host.
How #19 fits: orthogonal layers. ed25519 protects the distribution channel (works for unsigned/ad-hoc builds, and for Linux where no Authenticode story exists yet — so it fixes update provenance on every OS at once); the Authenticode pin protects the binary identity and rides Windows' own trust (SmartScreen, Defender). Suggest documenting them as two independent toggles in tinyjs.json — an app can have either, both, or neither, each fail posture documented.
One implementation note: bake
publicKeythrough the samegenerateBuildinline that every otherupdatefield rides — it must be compiled into the running app, not read from the fetched manifest (the #33 lesson).
Split out of #30 (last line of item 7).
tinyjs publish/ app self-update (runtime/update.js) downloads theupdate zip and checks its sha256 against the update manifest. Both come
from the same place, so whoever can change the zip on the update host (or
the host's TLS) can change the hash too. The check catches corruption, not
tampering.
Proposal: signed updates.
tinyjs.jsongets anupdate.publicKey(ed25519), baked into the builtapp.
tinyjs publishsigns the manifest (or the zip's hash) with a privatekey kept outside the repo (a file path or an env var), and refuses to
publish without one once a
publicKeyis set.update.jsverifies the signature before installing and refusesunsigned or badly signed updates when the app has a key. Apps without a
key keep today's behaviour.
Open questions: does txiki's crypto have ed25519 (or do we need a small
pure-JS verifier), key rotation, and how macOS notarization and Windows
signing (#19) fit alongside it.