Skip to content

Sign with your phone: OpenLV remote signing - #159

Open
flotob wants to merge 26 commits into
feature/hardware-walletfrom
feature/openlv
Open

Sign with your phone: OpenLV remote signing#159
flotob wants to merge 26 commits into
feature/hardware-walletfrom
feature/openlv

Conversation

@flotob

@flotob flotob commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What

Adds remote (phone) signing over the Open Lavatory protocol (spec 002, P2P, end-to-end encrypted): add a wallet that lives on your phone as a browser account, and every signing request — dapp transactions and messages, manual sends, x402 payments — shows a QR code; scan it, confirm on the phone, and the result lands back in the browser. Works with the vault locked; the key never leaves the phone.

Stacked on #149 (feature/hardware-wallet) — it builds on the signer seam introduced there. Merge #149 first, then re-target this PR to main.

How it works

  • Signer backend (src/main/wallet/remote/): a third backend behind getSigner(walletIndex) (type: 'remote'). Nothing from the phone is trusted: signatures must recover to the stored account address (REMOTE_WRONG_ACCOUNT otherwise), tx hashes are shape-checked and their on-chain sender best-effort verified. Stable REMOTE_* error codes mirror the LEDGER_* registry.
  • Session broker (renderer, remote-session.js): hosts one openlv session per request (no persisted session secrets), publishes the QR contents, tunnels the wallet JSON-RPC over an encrypted WebRTC channel. Signaling relays only ever see ciphertext.
  • Optional Signer.sendTransaction capability: phone wallets can't hand over raw signed txs — they sign and broadcast themselves. transaction-service prefers the capability when present; only intent fields (from/to/value/data/chainId) go to the phone, which picks nonce/gas with its own fee UI. Before the tx request, the broker switches the phone's wallet to the target chain (wallet_switchEthereumChain, with an EIP-3085 wallet_addEthereumChain fallback) — in-app wallet sessions default to mainnet otherwise.
  • Dual-purpose QR: encodes https://<bridge-host>/#openlv://…. openlv-native apps (freedom mobile, later) can claim the URL; any other phone opens the in-repo bridge page (bridge/ — static, no backend) inside a wallet app's browser, which forwards requests to window.ethereum behind a method allowlist. The session secret rides in the URL fragment and is never sent to the hosting server.
  • UI: "Connect Phone / Other Device" flow (QR → account picker → add, vault can stay locked), signing QR panel with live status / cancel / fresh-QR retry, per-device copy across the approval flows (generalizing the Ledger-only helpers).

Testing

  • 60+ new unit tests (signer verification, IPC bridge, broker lifecycle incl. retry/staleness, error mapping, account model)
  • Protocol integration test running the real @openlv stack, both roles, against an in-test local MQTT broker — including an assertion that the relay sees only role-addressed ciphertext frames
  • Playwright E2E (test-e2e/remote-signing.spec.js): boots the real app with a pre-seeded locked vault, drives the sidebar to connect a phone account and round-trip a verified personal_sign — the "phone" is the actual bridge page in a plain Chromium with a test-key window.ethereum, over real WebRTC, fully offline
  • Smoke-tested on real iPhones with Trust Wallet and Rainbow, including a Gnosis Chain contract call with the chain-switch pre-flight

⚠️ Before merge

  • Replace the interim bridge-page URL. The QR currently points at the test deployment https://freedom.florianglatz.eth.limo (BRIDGE_ORIGIN in src/renderer/lib/wallet/remote-session.js). Decide the production hostname, deploy bridge/ (three static files: index.html, bridge.js, openlv.esm.js) there, and update the constant.
  • Merge Hardware wallet support: Ledger accounts across wallet, dApps, and x402 #149 and re-target this PR to main.

Notes

  • @openlv/* packages are LGPL-3.0: bundled into a single clearly-labelled, regenerable vendor file (npm run vendor:openlv), lazy-loaded off the renderer boot path (76 KiB).
  • Default signaling relay is the openlv spec default (test.mosquitto.org, ciphertext only); overridable via FREEDOM_OPENLV_SIGNALING — a user-facing setting and/or a self-hosted default is a possible follow-up.
  • Follow-ups deferred by design: one-tap "open in wallet" deep links on the bridge page, persistent sessions, freedom-mobile (iOS) as an openlv-native wallet endpoint claiming the bridge URL.

🤖 Generated with Claude Code

meinharrd and others added 9 commits July 7, 2026 20:29
Bump PINNED_RELEASE_TAG v0.5.33 -> v0.5.36 together with
PINNED_SHA256SUMS_DIGEST (sha256 of the release's SHA256SUMS asset,
recorded at pin time).

Notable upstream changes since 0.5.33:
- 0.5.36: HTTP API + control socket bind before chain init (peer count
  visible from the first poll, chainReady flag in /health); socket-path
  failures non-fatal (solardev-xyz/ant#38, solardev-xyz/ant#39)
- 0.5.35: push-path throughput (~250 MiB upload stall fix), feed
  head-finding fix, concurrent chain init
- 0.5.34: upload-side Reed-Solomon encoding, Swarm content encryption
  end to end, local pinning, ACT access control, stewardship

Verified: ant:download installs and checksum-verifies all six targets,
check-binaries passes, bee-to-ant migration integration test passes
against the released binary, and a startup probe of the bundled binary
confirms /health answers 200 within 0.5s of spawn with live /peers
counts.
Freedom had no find-in-page. Add a Chrome-style overlay bar anchored to
the top-right of the webview area, driving the active tab's <webview>
via findInPage()/stopFindInPage() with match counts read back from the
found-in-page event.

- Cmd/Ctrl+F opens and focuses the bar (Edit-menu accelerator when the
  page has focus, renderer keydown fallback when the chrome does);
  prefills from the page's text selection when present
- Find-as-you-type (debounced), Enter / Shift+Enter cycle matches, Esc
  closes and clears highlights; zero matches shows "0/0" with a subtle
  error tint on the input
- Find state follows the foreground page: switching tabs closes the bar
  and clears the outgoing tab's highlights, navigating the active tab
  resets results while keeping the query for re-search on Enter
- New find:open IPC channel (main -> renderer) behind an explicit Edit
  menu item so the shortcut is discoverable; macOS keeps the editMenu
  role with a spelled-out submenu to append the item
- Dark/light theme support via the existing CSS variables

Tests: jest unit coverage for the find-bar state machine (mocked
webview) plus a Playwright harness spec covering match cycling, zero
matches, and tab-switch close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverts 73ee4a0, which was pushed to main by accident (local
push.default=upstream resolved a bare-name push through the feature
branch's origin/main tracking ref). The feature re-lands through a
reviewed PR from feature/find-in-page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign with a wallet on your phone over the Open Lavatory protocol
(openlv.sh): main publishes signing jobs to a renderer-hosted openlv
session (P2P WebRTC, end-to-end encrypted, per-request QR); the phone's
answer is verified against the account record before use.

- remote signer backend behind the getSigner seam (type 'remote'):
  personal_sign / eth_signTypedData_v4 with recover-and-compare
  verification (REMOTE_WRONG_ACCOUNT), stable REMOTE_* error codes
  mirroring the LEDGER_* registry, EIP-1193 rejection mapping
- optional Signer.sendTransaction capability: phones broadcast
  themselves; transaction-service prefers it and best-effort verifies
  the reported tx's sender on-chain; coded device errors now pass
  through its catch unwrapped
- renderer session broker (one openlv session per job, dual-purpose QR
  URL with the session secret in the fragment) + preload IPC bridge
  that only accepts responses from the chrome renderer
- shared signing-utils for EIP-712 wire-payload construction (was
  duplicated across vault/ledger, now also used by remote)
- vendored @openlv bundle (LGPL-3.0, regenerable via npm run
  vendor:openlv), lazy-loaded off the renderer boot path
- integration test: real @openlv stack, both roles, against an in-test
  MQTT broker — handshake, encryption, signing round-trips, and a
  relay-sees-only-ciphertext assertion

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add remote (phone) accounts to the wallet list and the sidebar flow to
connect one: show an openlv QR, receive the phone's accounts over the
encrypted P2P session (eth_requestAccounts), pick and name one, and
switch to it. Works with the vault locked — the key stays on the phone.

- identity-manager: addDeviceWallet generalizes the Ledger add path
  (shared validation/dedup/auto-naming via DEVICE_LABELS);
  addRemoteWallet + wallet:add-remote-wallet IPC; device-neutral
  key-guard messages
- broker connectPhone(): local discovery job reusing the session/
  teardown/cancel plumbing via a per-job respond seam
- Connect Phone subscreen with QR/status/retry, account picker, and
  success step; wallet selector gains the entry point and per-type
  badges (Ledger/Phone)
- shared device-account picker + inline-error helpers +
  activateAddedWallet extracted; connect-ledger migrated to them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a phone account signs (dapp tx/sign, manual send, x402 payment),
a QR panel overlays the approval flow: scan with the phone, live
connection status, cancel, and a fresh-QR retry that supersedes the
session without dropping the pending request.

- wallet-utils: accountType/isDeviceAccount/deviceLabel generalize the
  ledger-only helpers; signingButtonLabel and the unlock-gate bypass
  (renamed bypassUnlockGateForDevice) now cover phone accounts, so all
  four approval flows skip the vault gate and label buttons per device
- broker: job events carry kind ('signing'|'connect'); retryJob mints a
  new QR for a live job with staleness-guarded attempts; aborted
  attempts release their session immediately (Promise.race) instead of
  parking on the SDK's long response timeout
- send/x402 copy adapts to the device type (Ledger vs phone)
- shared PHASE_STATUS_TEXT + generateScannableQr used by both QR UIs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The other half of the dual-purpose QR: phones without an openlv-native
wallet open the bridge page (static, in-repo under bridge/, no backend)
inside their wallet app's browser. It joins the session from the URL
fragment — never sent to any server — and forwards freedom's requests
to the wallet's window.ethereum behind a method allowlist, so the
wallet's own confirmation UI still gates every signature.

- bridge/: self-contained page + the same openlv bundle the renderer
  vendors (built once, copied; npm run bridge:serve for local dev)
- FREEDOM_OPENLV_SIGNALING env override (preload nodeConfig → broker)
  so tests pin the signaling relay to a local broker
- Playwright E2E: real Electron app with a pre-seeded locked vault,
  bridge page in a plain Chromium as the "phone" (fake window.ethereum
  signing with a test key in Node) — connects a phone account through
  the sidebar UI and round-trips a verified personal_sign through the
  remote signer, QR panel included, all offline (local MQTT + local
  WebRTC)
- shared test/helpers/local-mqtt-broker.js for the jest protocol test
  and the E2E fixture

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
freedom.florianglatz.eth.limo hosts the bridge page for smoke testing;
final hostname still TBD.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phone-side dapp sessions default to Ethereum mainnet regardless of the
wallet's selected network, so remote sendTransaction landed on the
wrong chain (found in the Gnosis smoke test). The chainId field inside
eth_sendTransaction params is ignored by wallets — the chain is session
state, switched explicitly like any dapp does.

- remote signer ships an EIP-3085 chain descriptor with each
  eth_sendTransaction job (name/currency/explorer from the chain
  registry; public https RPC endpoints only — a user's local node
  config never leaves the machine)
- broker pre-flights wallet_switchEthereumChain over the same session
  (same QR); unknown chain (4902) → wallet_addEthereumChain with the
  descriptor, then switch again; a declined switch fails the job before
  the tx is ever sent
- QR panel shows a "confirm the network switch" status; bridge page
  allowlists the two wallet_* methods

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@flotob
flotob marked this pull request as draft July 9, 2026 09:49
flotob and others added 5 commits July 9, 2026 16:17
The freedom mobile wallet endpoint (swarm-mobile-ios, WP-R4a) runs the
vendored openlv SDK inside a hidden WKWebView, and WKWebView refuses
ES-module imports from file:// pages while the SDK hard-requires a
secure context for crypto.subtle — so bundle-openlv.js now also emits
bridge/openlv.iife.js (window.OpenLV, classic script) and refreshes the
sibling iOS checkout's vendored copy when present.

npm run openlv:ios-harness plays the desktop host role for the iOS
XCTest suite: local aedes MQTT broker + a headless Chromium page
running the same SDK (Node has no WebRTC) + an HTTP control surface on
127.0.0.1:8798, sending the signing-job sequence (eth_requestAccounts,
wallet_switchEthereumChain pre-flight, personal_sign) and recording the
responses. The Chromium mDNS ICE workaround moves to
test/helpers/webrtc.js, shared with the remote-signing E2E fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each XCTest consumes one host session; GET /reset reloads the Chromium
host page so the next test gets a fresh session and a clean exchange
log instead of a consumed one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two findings from the freedom-mobile smoke test:

Freedom's signing jobs never send eth_requestAccounts (the account was
captured in an earlier connect session), and strict wallets — freedom
mobile's own in-tab provider included — reject signing from an origin
that never connected ("Connect first … isn't authorized"). The bridge
now connects lazily before the first signing request; lenient wallets
treat the extra connect as a no-op.

The page also offers "Open in Freedom app" via the freedom:// custom
scheme, so scanning the QR with the plain camera can hand the session
to the native endpoint today, before the universal-link claim on this
origin is deployed. On phones without the app the tap is a no-op and
every other path stays available.

Redeploy bridge/ to Swarm to take effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/reset?mode=tx makes the host send the desktop signing-job shape for a
transaction (eth_requestAccounts → chain-switch pre-flight →
eth_sendTransaction) instead of personal_sign, so the iOS suite can
prove the wallet endpoint's self-built transaction actually mines on a
local anvil chain — not just that it was signed and broadcast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bridge page is a standalone static site deployed independently to
Swarm on its own cadence — it now lives in
github.com/solardev-xyz/freedom-bridge. Everything here that touched
bridge/ points at a sibling checkout instead: serve-bridge.js (and
through it the remote-signing E2E) resolves ../freedom-bridge with a
FREEDOM_BRIDGE_DIR override, the iOS harness reads the esm bundle from
the renderer vendor copy (byte-identical), and bundle-openlv.js syncs
both sibling repos' vendored bundles from its one build so nothing can
drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@flotob

flotob commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

The bridge page moved to its own repo: https://github.com/solardev-xyz/freedom-bridge (commit d9239ee removes bridge/ here and points serve-bridge.js, the E2E, and the vendor bundler at a sibling checkout). The pre-merge checklist item about deploying the bridge now means deploying from that repo — its vendored openlv.esm.js is kept in sync by this repo's npm run vendor:openlv.

flotob and others added 3 commits July 9, 2026 21:49
serve-bridge.js threw at require time when the sibling checkout was
missing, which failed the whole `npm run test:e2e` sweep for anyone
without it. The availability check moves behind bridgeAvailable() and
the spec skips itself with a clone hint instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The require was one directory short (resolved to src/test/... instead
of the repo-root test/helpers/), so the suite failed to load — the CI
red on PR #159. Six directories up, six dots of shame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The openlv mqtt signaling layer uses the browser WebSocket global,
which Node only ships from v22 — CI runs Node 20, so the suite failed
with "WebSocket is not defined" there while passing on dev machines.
Polyfill from `ws`, which the local broker helper already depends on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@flotob
flotob requested a review from meinharrd July 10, 2026 07:05
@flotob
flotob marked this pull request as ready for review July 10, 2026 11:42
simon-jentzsch and others added 3 commits July 10, 2026 16:07
This uses the 64bit SP1 v6 zk-proof and once every one has updated, we can stop generating the old v5 proofs.
Update colibri-stateless to version 2.0.0

@flotob flotob left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review of head 17dd043: 1 explicit pre-merge security blocker. CI is green and the stacked branch is clean. This PR must also remain behind #149 until that base lands.

  • [P1] Do not merge with the interim personal/test bridge origin (src/renderer/lib/wallet/remote-session.js:25). The bridge page receives the fragment containing the OpenLV session secret and executes the wallet handoff; whoever controls or compromises this origin can join the encrypted session and mediate signing requests. Deploy the audited freedom-bridge artifact on a canonical production-controlled origin, pin it here, and include the deployed artifact/version in release verification. This matches the unchecked pre-merge item in the PR description.

Distributable builds now fail while BRIDGE_ORIGIN still points at the
interim test deployment, so the pre-merge checklist item on #159 is
enforced rather than remembered. FREEDOM_ALLOW_INTERIM_BRIDGE=1
overrides for local experiments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@flotob

flotob commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

Re the P1 blocker (interim bridge origin in src/renderer/lib/wallet/remote-session.js):

Acknowledged — this is the unchecked pre-merge item in the description, and it stays a merge blocker. The remaining work is not a code change: the maintainer needs to decide the canonical production hostname and deploy the audited freedom-bridge artifact (three static files) there. Once that origin exists, the fix here is pinning BRIDGE_ORIGIN to it and recording the deployed artifact/version in release verification.

What landed now (15ab32d9): a dist-build guard in scripts/build.js that refuses to produce any distributable while BRIDGE_ORIGIN still points at the interim test deployment (FREEDOM_ALLOW_INTERIM_BRIDGE=1 overrides for local experiments only). So the interim origin can no longer ship by accident — the checklist item is enforced by the build, not just remembered. Verified: npm run dist -- --mac exits 1 with a pointed error on this branch; the override proceeds normally.

The PR remains blocked until the production origin is deployed and pinned; hostname decision is with the maintainer (tracked alongside the AASA/hostname issue on the iOS side).

@flotob flotob left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of head 15ab32d9: the new distribution guard is useful defense in depth and prevents the interim bridge from shipping by default, but the original pre-merge blocker is intentionally still unresolved.

  • [P1] Replace the interim personal/test bridge origin before merge (src/renderer/lib/wallet/remote-session.js:25). Runtime still sends the OpenLV session secret to https://freedom.florianglatz.eth.limo. The build guard does not establish production ownership/audit provenance, and it has an explicit override. This PR cannot be cleared until the audited bridge artifact is deployed on the canonical production-controlled origin, the constant is pinned to it, and the deployed artifact/version is included in release verification.

Current status: CLEAN stacked diff, 36/36 checks successful. It remains dependent on #149.

@flotob flotob left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-re-review of unchanged head 15ab32d9: disposition unchanged. The distribution guard is useful, but the runtime bridge constant still points at the interim personal/test deployment.

  • [P1] Production bridge origin remains a pre-merge blocker (src/renderer/lib/wallet/remote-session.js:25). Deploy the audited bridge artifact on the canonical production-controlled origin, pin that origin, and record the deployed artifact/version for release verification. The opt-out build guard cannot substitute for that trust anchor.

CLEAN stacked diff; 36/36 checks successful; still dependent on #149.

flotob and others added 5 commits July 12, 2026 16:21
Pin the new release together with the SHA256SUMS trust digest. This picks up the upstream phantom postage batch fixes while preserving reproducible binary downloads.
Pin the new release together with the SHA256SUMS trust digest. This picks up the upstream phantom postage batch fixes while preserving reproducible binary downloads.
chore(build): update bundled Ant to v0.5.41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants