Skip to content

feat(wallet): sign tx via browser wallet localhost handoff - #1079

Merged
Nanle-code merged 2 commits into
Nanle-code:masterfrom
Henrypave:fix-browser-wallet-signer-956
Oct 1, 2026
Merged

Nanle-code merged 2 commits into
Nanle-code:masterfrom
Henrypave:fix-browser-wallet-signer-956

Conversation

@Henrypave

Copy link
Copy Markdown
Contributor

Overview

Adds a browser-wallet signing path so StarForge users can keep keys in
Freighter (or any Stellar Wallets Kit compatible wallet) instead of importing a
secret into the CLI. starforge wallet sign-tx --signer browser starts a
one-time HTTP server bound to 127.0.0.1, opens a page that displays a decoded
view of the transaction and asks the wallet to sign it, then returns the signed
envelope XDR to the CLI. The handoff is authorised by a single-use 32-byte
nonce, served under a strict Content-Security-Policy, and expires after a
short timeout.

Related Issue

Addresses Nanle-code/StarForge#956:
--signer browser should serve a one-time localhost page with a nonce,
display the decoded transaction, return the signed XDR to the CLI, bind
127.0.0.1, use a tight CSP, and keep a short timeout.

No new dependencies were added and no lockfile was touched. The handoff uses
std::net::TcpListener with a minimal HTTP/1.1 request/response loop, plus
crates the crate already depends on (serde, serde_json, sha2, hex,
base64, rand).

Changes

  • [ADD] src/utils/browser_signer.rs
    • HandoffServer: binds 127.0.0.1:0, serves an ephemeral-port handoff, and
      stops as soon as a signature arrives or the timeout elapses.
    • NonceGuard: 32-byte OS-RNG nonce, constant-time comparison, single-use
      (consumed only after a well-formed signature is validated).
    • Routes: GET / (page, nonce required), GET /api/transaction (JSON with
      the unsigned XDR, network passphrase, and DecodedTransaction),
      POST /api/sign-result (validates base64 envelope XDR, consumes the nonce,
      returns the signed XDR to the waiting CLI). Everything else is 404.
    • content_security_policy: default-src 'none' with a script nonce plus a
      pinned Stellar Wallets Kit CDN origin, connect-src 'self',
      frame-ancestors 'none', base-uri 'none', form-action 'none'.
    • WalletSigner trait with LocalhostHandoffSigner (real, opens the browser)
      and MockWalletSigner (tests/dry runs) so the flow is testable without a
      browser.
    • DecodedTransaction: base64 validity, byte length, envelope-type label
      from the XDR discriminant, SHA-256 digest, and a hex preview.
    • Unit tests: nonce single-use, wrong nonce rejected without consuming,
      strict CSP, page nonce gating, malformed signature not burning the nonce,
      replaying a used nonce returning 410, a real TcpStream handshake
      round-trip, timeout, and mock-signer success/failure paths.
  • [MODIFY] src/utils/mod.rs
    • Registers pub mod browser_signer;.
  • [MODIFY] src/commands/wallet.rs
    • New wallet sign-tx subcommand with --transaction, --signer browser|local,
      --wallet, --network, --output, and --timeout.
    • --signer browser uses the handoff and never reads a secret key;
      --signer local reuses the existing utils::wallet_signer local path.
  • [MODIFY] docs/SECURITY_THREAT_MODEL.md
    • Adds the handoff asset and trust-boundary row, two security requirements
      (loopback-only + single-use nonce; validate before accepting), and a
      "Browser-wallet localhost handoff" threat/mitigation section.
  • [ADD] docs/BROWSER_WALLET_SIGNING.md
    • Manual Freighter testnet checklist, expected results, and troubleshooting.

Verification Results

Formatting: `rustfmt --edition 2021 --check` passes on the new and modified
            Rust files.
Tests:      Unit tests live in `src/utils/browser_signer.rs` (nonce single-use,
            wrong-nonce, CSP, page gating, invalid-signature, replay, TcpStream
            handshake, timeout, mock signer, decoded summary) but were NOT
            executed here: this environment is API-only (no clone, no cargo
            build, no package installs).
Build:      Not compiled here for the same reason; changes use only existing
            crates and mirror existing module patterns.
Manual:     The Freighter testnet checklist in docs/BROWSER_WALLET_SIGNING.md
            has not been run against a live wallet.
Acceptance Criteria Status
--signer browser serves a one-time localhost page with a nonce ✅ wallet sign-tx --signer browser → browser_signer::HandoffServer with NonceGuard
Displays the decoded tx; returns the signed XDR to the CLI ✅ DecodedTransaction page + /api/transaction and /api/sign-result
Tight CSP, bind 127.0.0.1, short timeout ✅ nonce-scoped Content-Security-Policy, loopback ephemeral bind, --timeout (default 120s)
Manual test checklist with Freighter on testnet ✅ Added in docs/BROWSER_WALLET_SIGNING.md (not executed in this environment)
Automated test with a mocked wallet ✅ Unit tests + MockWalletSigner in src/utils/browser_signer.rs (not executed in this environment)
Security notes in the threat model ✅ docs/SECURITY_THREAT_MODEL.md boundary row + handoff section

Note on the browser page: the page imports a pinned Stellar Wallets Kit bundle
from https://cdn.jsdelivr.net because the crate cannot vendor the JS without
adding build tooling. The exact CDN module entry point and kit API version were
not exercised in a browser here; the manual checklist is how that is confirmed.

Closes #956

@Nanle-code

Copy link
Copy Markdown
Owner

@Henrypave fix conflicts

@Nanle-code
Nanle-code merged commit 858a7ca into Nanle-code:master Oct 1, 2026
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.

[Advancement] Sign with browser wallets (Freighter etc.) via a localhost handoff

2 participants