Skip to content

Design: signed web app code (#970), for review - #1025

Open
pixari wants to merge 1 commit into
mainfrom
design/signed-web-app
Open

pixari wants to merge 1 commit into
mainfrom
design/signed-web-app

Conversation

@pixari

@pixari pixari commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator

Design only, for review: please don't merge until the design is approved. Implementation is after 1.0, as decided on 2026-11-06. Part of #970 (hardening item 6.13d of external-security-review).

The change is the OpenSpec change openspec/changes/signed-web-app/: proposal, design, spec deltas (signed-web-app new, web-app and native-shells modified) and tasks. make openspec-validate passes.

The design in short

  • What is signed. Each build writes app-manifest.json: the version, the trusted key set, and every file of the web app with its SHA-256 (the page, every chunk, the locales, the help, the icons, and the worker, which now includes push-sw.js). CI signs it on release tags only, in a protected web-signing environment. The key never reaches a server.
  • The scheme. ECDSA P-256, because it verifies with no added code in WebCrypto, on Android (minSdk 26), in CryptoKit and in Go's standard library. Ed25519 would need a bundled verifier on Android and newer browsers. There is one release key and one offline recovery key. Keys rotate by signing with both for a while. A lost key is replaced through the recovery key.
  • Updates. The worker checks the signature, refuses older versions, downloads and hashes every file, and switches in one step. Otherwise it keeps the last good version and shows "This update wasn't signed by OpenTodo. It wasn't installed."
  • The worker is the only door. App files come only from the verified set, and lazy chunks are checked on first load. No script ever loads from /api/. Server documents open sandboxed. The worker writes script-src 'self' itself.
  • Trust on first use. The first install decides the mode: signed or self-built. Signed never goes back to unchecked, and signing out keeps the trust.
  • What a normal browser can't stop, said plainly. A server can replace the service worker itself, clear the site's data, or attack a first visit. The Android app is the binding form: the key is in the APK, and the app serves every file to its web view only after checking it. The iOS app can only warn for now.
  • About. "Signed by OpenTodo" with a short fingerprint, a link to the Security page, and Verify. Verify opens the website's verify page with the fingerprint in the address fragment, so the app sends no request. A self-built server says "Self-built: updates are not signature-checked". One build setting, WEB_SIGNING_KEY, signs a build with the operator's own key.
  • Size. About +0.3 to 0.6 KB gzip on first load. The verifier replaces three Workbox dependencies, and nothing is added.
  • Docs afterwards. The texts for the threat model, the limits page and docs/e2ee.md are in design D14. What stays undefended is in D15.

Questions for the maintainer

  1. Is the browser part worth shipping as it is? In a normal browser, a hostile server can still replace the part of the app that does the checking. Only the Android app makes the check impossible to get around. Recommendation: ship both, and recommend the Android app to people who don't trust their server.
  2. Should every release wait for your click before it's signed? It's safer, because CI alone couldn't sign, but releases stop being fully automatic. Recommendation: yes.
  3. Who keeps the spare (recovery) key? Recommendation: you, offline, with a second copy somewhere else.
  4. Should the latest image built from every merge to main be signed? Recommendation: no. It would say "Preview build: updates are not signature-checked".
  5. How does someone get unstuck after a legitimate rollback? Option A: the docs explain how to clear the site's data or reinstall. Option B: a "Reset this app" button. Recommendation: A, because a hostile operator could talk people into pressing a button.
  6. Should the iPhone app later load the web app in a way it can check? Today it can only warn. Recommendation: yes, as a separate change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bkw4TZLFDGeMdZR3vWZF13

The OpenSpec change signed-web-app, design only: a signed file list per
release, a service worker that installs and serves only verified files,
the Android app as the binding check, About with the fingerprint and
Verify, own keys for self-built servers, and the limits stated plainly.
Questions for the maintainer at the end of design.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkw4TZLFDGeMdZR3vWZF13

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant