Skip to content

Sign auth entries from Ledger identities#2569

Open
fnando wants to merge 3 commits intomainfrom
ledger-soroban-auth-signing
Open

Sign auth entries from Ledger identities#2569
fnando wants to merge 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

Soroban auth signing with Ledger-backed identities

2 participants