Skip to content

Signed app updates (update zip hash comes from the same host as the zip) #40

Description

@tarwin

Split out of #30 (last line of item 7).

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.

Activity

  1. tarwin commented on Oct 5, 2026

    @tarwin
    OwnerAuthor

    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.

  2. slabbdev commented on Oct 7, 2026

    @slabbdev
    Contributor

    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.exe the 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, 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:

    1. 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.
    2. 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions