Skip to content

Sign auth entries from Ledger identities#2569

Merged
fnando merged 3 commits intomainfrom
ledger-soroban-auth-signing
May 8, 2026
Merged

Sign auth entries from Ledger identities#2569
fnando merged 3 commits intomainfrom
ledger-soroban-auth-signing

Conversation

@fnando
Copy link
Copy Markdown
Member

@fnando fnando commented May 7, 2026

What

Sign Soroban authorization entries when the resolved Address argument is a Ledger-backed identity. Previously, invoking a contract function that called require_auth() on an address resolved from a --ledger-backed alias panicked at todo!("ledger device is not implemented") in Signer::get_public_key whenever the device was connected.

Why

Closes #2566. After #2563 added stellar keys add --ledger, read-only operations and source-account signing worked for Ledger identities, but the Soroban auth path was unfinished: SignerKind::Ledger held a live HID transport (Ledger<TransportNativeHID>) eagerly opened during argument parsing, yet get_public_key and sign_payload were unimplemented for it. The transport was also held across parse → simulate → RPC → sign, which conflicted with the second HID transport opened by --source <ledger-alias> (per-process hidapi cannot host both at once).

This change mirrors the existing SecureStoreEntry pattern: SignerKind::Ledger now carries a pure-data LedgerEntry { hd_path, public_key: Option<PublicKey> } with no live transport. HID is opened lazily inside each sign call (LedgerEntry::sign_tx_hash, LedgerEntry::sign_payload) and dropped immediately, so two transports can never coexist. Signer::get_public_key for Ledger now returns the cached strkey synchronously (always populated from Secret::Ledger), and Signer::sign_payload is async with a working Ledger arm that calls the same sign_blob primitive used today for transaction-hash signing.

Secret::signer, parse_function_arguments, and build_host_function_parameters no longer need to be async — the only thing that did was the now-removed eager Ledger HID open during arg parsing.

Reject --hd-path overrides on Ledger aliases

The hd-path of a Ledger alias is fixed at keys add --ledger time. Accepting a caller-supplied --hd-path against a Ledger alias was either redundant (matching the cached value) or unsafe (mismatching it silently used the wrong cached public key for auth-entry matching and signature-hint derivation while the device signed at a different path). Secret::signer and Secret::public_key now error with LedgerHdPathFixed for any non-None hd_path argument when the secret is Secret::Ledger. Discovery on other paths goes through the existing --ledger --hd-path N escape hatch on keys public-key / keys address, which queries the live device directly.

usizeu32 for hd_path

BIP32 derivation paths are 32-bit by spec — that is the type used by the Stellar Ledger app and by LedgerEntry::hd_path. The codebase had been carrying Option<usize> end-to-end and converting to u32 only at the device-call boundary via try_into().unwrap_or_default(), which silently truncated values above u32::MAX to 0 on 64-bit systems and signed with the wrong account. Switching the chain (Secret::*, SecureStoreEntry, secure_store::*, keyring::*, locator::get_secret_key_with_hd_path, UnresolvedMuxedAccount::resolve_muxed_account, UnresolvedScAddress::resolve, Args::hd_path(), every command's clap hd_path field) to Option<u32> means clap range-validates at parse time, the silent truncation is gone, and three now-unreachable Error::HdPathOutOfRange variants are removed. The single as usize cast now lives at sep5's from_path_index(usize, ...) boundary. On-disk TOML format is unaffected — serde serializes either type as a plain number.

Known limitations

The Soroban auth-payload path uses sign_transaction_hash (the existing sign_blob primitive) — the firmware accepts arbitrary 32-byte payloads when hash signing is enabled. There is no HID emulator wired into soroban-test yet, so the actual signing path is covered only by manual validation. Unit tests cover the deferred-construction plumbing and the cached-pubkey path.

Manual validation against a real Ledger device:

$ cat ~/.config/stellar/identity/hw.toml
hardware = "ledger"
public_key = "GDLTRI3WHFQQBQLZWRERAIW7OVJKCNCAMSA7HUVMIW7DZRROA2GWFEGS"
hd_path = 9

$ stellar env | grep ACCOUNT
STELLAR_ACCOUNT=hw    # use

$ stellar contract invoke --id auth --send=yes --network testnet -- auth --value hw
ℹ️ Simulating transaction…
ℹ️ Signing transaction: ae47a1d5085259c2d0bfc0f55f32f6ac5503a2e3cdf8c6c0f34c50685c2a0d7d
🌎 Sending transaction…
✅ Transaction submitted successfully!
🔗 https://stellar.expert/explorer/testnet/tx/ae47a1d5085259c2d0bfc0f55f32f6ac5503a2e3cdf8c6c0f34c50685c2a0d7d
"GDLTRI3WHFQQBQLZWRERAIW7OVJKCNCAMSA7HUVMIW7DZRROA2GWFEGS"

Copilot AI review requested due to automatic review settings May 7, 2026 19:53
@github-project-automation github-project-automation Bot moved this to Backlog (Not Ready) in DevX May 7, 2026
@fnando fnando changed the title Sign Soroban auth entries from Ledger identities Sign auth entries from Ledger identities May 7, 2026
@fnando fnando moved this from Backlog (Not Ready) to Needs Review in DevX May 7, 2026
@fnando fnando self-assigned this May 7, 2026
@fnando fnando requested review from leighmcculloch and mootz12 May 7, 2026 19:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR completes Soroban auth-entry signing for Ledger-backed identities by replacing eager, long-lived HID transports with a pure-data LedgerEntry that opens the Ledger connection only during signing, and by wiring Ledger support into the Soroban authorization payload signing path.

Changes:

  • Introduces LedgerEntry { hd_path, public_key } and updates SignerKind::Ledger to use it, enabling cached-pubkey lookups and lazy HID opens for signing.
  • Makes Soroban auth signing (sign_soroban_authorizations / sign_payload) async and removes unnecessary async from argument parsing / signer construction.
  • Updates SEP-53 message signing helper to use the now-async Signer::sign_payload.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cmd/soroban-cli/src/signer/mod.rs Makes Soroban auth signing async; switches Ledger signer to LedgerEntry; implements Ledger payload signing path.
cmd/soroban-cli/src/signer/ledger.rs Adds LedgerEntry that lazily opens HID per sign call; adds sign_payload for Ledger.
cmd/soroban-cli/src/config/sign_with.rs Builds Ledger signers using LedgerEntry (no eager HID open).
cmd/soroban-cli/src/config/secret.rs Makes Secret::signer synchronous; constructs LedgerEntry with cached pubkey/hd_path.
cmd/soroban-cli/src/config/mod.rs Awaits async Soroban auth signing when preparing transactions.
cmd/soroban-cli/src/commands/message/sign.rs Adapts SEP-53 signing helper/tests to async sign_payload.
cmd/soroban-cli/src/commands/contract/invoke.rs Removes now-unneeded awaits for host function parameter building.
cmd/soroban-cli/src/commands/contract/deploy/wasm.rs Removes now-unneeded await for constructor parameter building.
cmd/soroban-cli/src/commands/contract/arg_parsing.rs Makes host function arg parsing synchronous; resolves signers without eager Ledger device access.

Comment thread cmd/soroban-cli/src/config/secret.rs
Comment thread cmd/soroban-cli/src/signer/mod.rs
Comment thread cmd/soroban-cli/src/config/sign_with.rs Outdated
Copy link
Copy Markdown
Contributor

@mootz12 mootz12 left a comment

Choose a reason for hiding this comment

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

One ask - could we add an emulation test like hello_world::invoke_contract's invoke_auth_with_non_source_identity(sandbox, id, "test", "testone", &addr_1); test where testone is a ledger device?

Comment thread cmd/soroban-cli/src/commands/keys/add.rs
Comment thread cmd/soroban-cli/src/signer/ledger.rs
@fnando fnando force-pushed the ledger-soroban-auth-signing branch from 4304a94 to 4739d43 Compare May 8, 2026 17:03
@fnando
Copy link
Copy Markdown
Member Author

fnando commented May 8, 2026

@mootz12 added in 4739d43. Mirrors the shape of invoke_auth_with_non_source_identity: source is a regular keypair (test), auth identity (testone) is a Ledger-backed alias.

Three test cases for NanoS / NanoX / NanoS Plus. Exercises the full add-then-sign flow:

  1. stellar keys add testone --ledger --hd-path N — queries the live emulator and persists Secret::Ledger.
  2. stellar keys address testone — cached-pubkey path, no device call.
  3. stellar contract invoke --source test … -- auth --addr testone --world world — one approve_tx_hash_signature cycle drives the device prompt for the auth-entry hash; asserts stdout matches the testone strkey.

@fnando fnando enabled auto-merge (squash) May 8, 2026 17:12
@fnando fnando merged commit bc9282d into main May 8, 2026
210 of 211 checks passed
@fnando fnando deleted the ledger-soroban-auth-signing branch May 8, 2026 17:20
@github-project-automation github-project-automation Bot moved this from Needs Review to Done in DevX May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Soroban auth signing with Ledger-backed identities

3 participants