Sign with your phone: OpenLV remote signing - #159
Conversation
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>
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>
|
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 |
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>
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
left a comment
There was a problem hiding this comment.
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 auditedfreedom-bridgeartifact 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>
|
Re the P1 blocker (interim bridge origin in 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 What landed now ( 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
left a comment
There was a problem hiding this comment.
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 tohttps://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
left a comment
There was a problem hiding this comment.
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.
Pin the new release together with the SHA256SUMS trust digest. This picks up the upstream phantom postage batch fixes while preserving reproducible binary downloads.
This reverts commit 8ebd3f0.
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
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.
How it works
src/main/wallet/remote/): a third backend behindgetSigner(walletIndex)(type: 'remote'). Nothing from the phone is trusted: signatures must recover to the stored account address (REMOTE_WRONG_ACCOUNTotherwise), tx hashes are shape-checked and their on-chain sender best-effort verified. StableREMOTE_*error codes mirror theLEDGER_*registry.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.Signer.sendTransactioncapability: phone wallets can't hand over raw signed txs — they sign and broadcast themselves.transaction-serviceprefers 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-3085wallet_addEthereumChainfallback) — in-app wallet sessions default to mainnet otherwise.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 towindow.ethereumbehind a method allowlist. The session secret rides in the URL fragment and is never sent to the hosting server.Testing
@openlvstack, both roles, against an in-test local MQTT broker — including an assertion that the relay sees only role-addressed ciphertext framestest-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 verifiedpersonal_sign— the "phone" is the actual bridge page in a plain Chromium with a test-keywindow.ethereum, over real WebRTC, fully offlinehttps://freedom.florianglatz.eth.limo(BRIDGE_ORIGINinsrc/renderer/lib/wallet/remote-session.js). Decide the production hostname, deploybridge/(three static files:index.html,bridge.js,openlv.esm.js) there, and update the constant.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).test.mosquitto.org, ciphertext only); overridable viaFREEDOM_OPENLV_SIGNALING— a user-facing setting and/or a self-hosted default is a possible follow-up.🤖 Generated with Claude Code