You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Signed app updates (update zip hash comes from the same host as the zip) #40
tinyjs publish / app self-update (runtime/update.js) downloads the
update 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.json gets an update.publicKey (ed25519), baked into the built
app.
tinyjs publish signs the manifest (or the zip's hash) with a private
key kept outside the repo (a file path or an env var), and refuses to
publish without one once a publicKey is set.
update.js verifies the signature before installing and refuses
unsigned 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.
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.
Your ed25519 question, answered empirically: txiki's WebCrypto has Ed25519. Tested against the bundled runtime (bin/tjs, v26.6.0, macOS arm64) — keygen, sign, verify round-trip, and tampered-signature rejection all pass:
$ tjs eval 'await crypto.subtle.generateKey("Ed25519", true, ["sign","verify"])' → OK
crypto.subtle is the WebCrypto global (not tjs:crypto), so no pure-JS verifier dependency is needed: keygen, sign and verify all run in the backend, and tinyjs publish can 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/ed25519 is 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 next behind 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 publicKey through the same generateBuild inline that every other update field rides — it must be compiled into the running app, not read from the fetched manifest (the #33 lesson).
Activity
tarwin commentedon Oct 5, 2026
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.
slabbdev commentedon Oct 7, 2026
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):
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:
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.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).