Skip to content

Commit bc9282d

Browse files
authored
Sign auth entries from Ledger identities (#2569)
1 parent fb1cd67 commit bc9282d

27 files changed

Lines changed: 302 additions & 172 deletions

File tree

cmd/crates/soroban-test/tests/it/emulator.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use stellar_ledger::Blob;
22

3-
use soroban_test::{AssertExt, TestEnv};
3+
use soroban_test::{AssertExt, TestEnv, Wasm};
44
use std::sync::Arc;
55

66
use stellar_ledger::emulator_test_support::*;
@@ -12,6 +12,8 @@ use soroban_cli::{
1212

1313
use test_case::test_case;
1414

15+
const HELLO_WORLD: &Wasm = &Wasm::Custom("test-wasms", "test_hello_world");
16+
1517
#[test_case("nanos", 0; "when the device is NanoS")]
1618
#[test_case("nanox", 1; "when the device is NanoX")]
1719
#[test_case("nanosp", 2; "when the device is NanoS Plus")]
@@ -88,3 +90,90 @@ async fn test_signer(ledger_device_model: &str, hd_path: u32) {
8890
)
8991
.unwrap();
9092
}
93+
94+
// Mirrors `invoke_auth_with_non_source_identity` from the integration tests:
95+
// invoke a contract whose `auth(addr, world)` calls `addr.require_auth()`,
96+
// where the auth identity (`testone`) is a Ledger-backed alias and the
97+
// transaction source (`test`) is a regular keypair. Exercises the Soroban
98+
// auth-entry signing path through the Ledger device.
99+
#[test_case("nanos", 0; "when the device is NanoS")]
100+
#[test_case("nanox", 1; "when the device is NanoX")]
101+
#[test_case("nanosp", 2; "when the device is NanoS Plus")]
102+
#[tokio::test]
103+
async fn invoke_auth_with_ledger_identity(ledger_device_model: &str, hd_path: u32) {
104+
let sandbox = Arc::new(TestEnv::new());
105+
let container = TestEnv::speculos_container(ledger_device_model).await;
106+
let host_port = container.get_host_port_ipv4(9998).await.unwrap();
107+
let ui_host_port = container.get_host_port_ipv4(5000).await.unwrap();
108+
109+
sandbox
110+
.new_assert_cmd("keys")
111+
.arg("fund")
112+
.arg("test")
113+
.assert()
114+
.success();
115+
116+
sandbox
117+
.new_assert_cmd("keys")
118+
.arg("add")
119+
.arg("testone")
120+
.arg("--ledger")
121+
.arg("--hd-path")
122+
.arg(hd_path.to_string())
123+
.env("SPECULOS_PORT", host_port.to_string())
124+
.assert()
125+
.success();
126+
127+
let addr = sandbox
128+
.new_assert_cmd("keys")
129+
.arg("address")
130+
.arg("testone")
131+
.assert()
132+
.success()
133+
.stdout_as_str();
134+
135+
let id = sandbox
136+
.new_assert_cmd("contract")
137+
.arg("deploy")
138+
.arg("--source")
139+
.arg("test")
140+
.arg("--wasm")
141+
.arg(HELLO_WORLD.path())
142+
.arg("--ignore-checks")
143+
.assert()
144+
.success()
145+
.stdout_as_str();
146+
147+
let invoke = tokio::task::spawn_blocking({
148+
let sandbox = Arc::clone(&sandbox);
149+
let id = id.clone();
150+
let addr = addr.clone();
151+
move || {
152+
let stdout = sandbox
153+
.new_assert_cmd("contract")
154+
.arg("invoke")
155+
.arg("--source")
156+
.arg("test")
157+
.arg("--id")
158+
.arg(&id)
159+
.arg("--")
160+
.arg("auth")
161+
.arg("--addr")
162+
.arg("testone")
163+
.arg("--world=world")
164+
.env("SPECULOS_PORT", host_port.to_string())
165+
.assert()
166+
.success()
167+
.stdout_as_str();
168+
assert_eq!(stdout, format!("\"{addr}\""));
169+
}
170+
});
171+
172+
let approve = tokio::task::spawn(approve_tx_hash_signature(
173+
ui_host_port,
174+
ledger_device_model.to_string(),
175+
));
176+
177+
invoke.await.unwrap();
178+
approve.await.unwrap();
179+
}

cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl FromStr for DeviceModel {
8181
"nanos" => Ok(DeviceModel::NanoS),
8282
"nanosp" => Ok(DeviceModel::NanoSP),
8383
"nanox" => Ok(DeviceModel::NanoX),
84-
_ => Err(format!("Unsupported device model: {}", s)),
84+
_ => Err(format!("Unsupported device model: {s}")),
8585
}
8686
}
8787
}

cmd/crates/stellar-ledger/src/emulator_test_support/util.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub async fn click(ui_host_port: u16, url: &str) {
4747
let current_events = get_emulator_events(ui_host_port).await;
4848

4949
if !(previous_events == current_events) {
50-
screen_has_changed = true
50+
screen_has_changed = true;
5151
}
5252
}
5353

cmd/soroban-cli/src/commands/contract/arg_parsing.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,25 +93,25 @@ fn running_cmd() -> String {
9393
format!("{} --", args.join(" "))
9494
}
9595

96-
pub async fn build_host_function_parameters(
96+
pub fn build_host_function_parameters(
9797
contract_id: &stellar_strkey::Contract,
9898
slop: &[OsString],
9999
spec_entries: &[ScSpecEntry],
100100
config: &config::Args,
101101
) -> Result<HostFunctionParameters, Error> {
102-
build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, true).await
102+
build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, true)
103103
}
104104

105-
pub async fn build_constructor_parameters(
105+
pub fn build_constructor_parameters(
106106
contract_id: &stellar_strkey::Contract,
107107
slop: &[OsString],
108108
spec_entries: &[ScSpecEntry],
109109
config: &config::Args,
110110
) -> Result<HostFunctionParameters, Error> {
111-
build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, false).await
111+
build_host_function_parameters_with_filter(contract_id, slop, spec_entries, config, false)
112112
}
113113

114-
async fn build_host_function_parameters_with_filter(
114+
fn build_host_function_parameters_with_filter(
115115
contract_id: &stellar_strkey::Contract,
116116
slop: &[OsString],
117117
spec_entries: &[ScSpecEntry],
@@ -122,7 +122,7 @@ async fn build_host_function_parameters_with_filter(
122122
let cmd = build_clap_command(&spec, filter_constructor)?;
123123
let (function, matches_) = parse_command_matches(cmd, slop)?;
124124
let func = get_function_spec(&spec, &function)?;
125-
let (parsed_args, signers) = parse_function_arguments(&func, &matches_, &spec, config).await?;
125+
let (parsed_args, signers) = parse_function_arguments(&func, &matches_, &spec, config)?;
126126
let invoke_args = build_invoke_contract_args(contract_id, &function, parsed_args)?;
127127

128128
Ok((function, spec, invoke_args, signers))
@@ -187,7 +187,7 @@ fn get_function_spec(spec: &Spec, function: &str) -> Result<ScSpecFunctionV0, Er
187187
})
188188
}
189189

190-
async fn parse_function_arguments(
190+
fn parse_function_arguments(
191191
func: &ScSpecFunctionV0,
192192
matches_: &clap::ArgMatches,
193193
spec: &Spec,
@@ -197,13 +197,13 @@ async fn parse_function_arguments(
197197
let mut signers = Vec::<Signer>::new();
198198

199199
for i in func.inputs.iter() {
200-
parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args).await?;
200+
parse_single_argument(i, matches_, spec, config, &mut signers, &mut parsed_args)?;
201201
}
202202

203203
Ok((parsed_args, signers))
204204
}
205205

206-
async fn parse_single_argument(
206+
fn parse_single_argument(
207207
input: &stellar_xdr::curr::ScSpecFunctionInputV0,
208208
matches_: &clap::ArgMatches,
209209
spec: &Spec,
@@ -234,7 +234,7 @@ async fn parse_single_argument(
234234
ScSpecTypeDef::Address | ScSpecTypeDef::MuxedAddress
235235
) {
236236
let trimmed_s = s.trim_matches('"');
237-
if let Some(signer) = resolve_signer(trimmed_s, config).await {
237+
if let Some(signer) = resolve_signer(trimmed_s, config) {
238238
signers.push(signer);
239239
}
240240
}
@@ -464,10 +464,10 @@ fn resolve_address(addr_or_alias: &str, config: &config::Args) -> Result<String,
464464
Ok(account)
465465
}
466466

467-
async fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<Signer> {
467+
fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<Signer> {
468468
let secret = config.locator.get_secret_key(addr_or_alias).ok()?;
469469
let print = Print::new(false);
470-
let signer = secret.signer(config.hd_path(), print).await.ok()?;
470+
let signer = secret.signer(config.hd_path(), print).ok()?;
471471
Some(signer)
472472
}
473473

cmd/soroban-cli/src/commands/contract/deploy/wasm.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,7 @@ impl Cmd {
385385
&slop,
386386
&entries,
387387
config,
388-
)
389-
.await?
388+
)?
390389
.2,
391390
)
392391
}

cmd/soroban-cli/src/commands/contract/invoke.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ impl Cmd {
270270

271271
if let Some(spec_entries) = &spec_entries {
272272
// For testing wasm arg parsing
273-
build_host_function_parameters(&contract_id, &self.slop, spec_entries, config).await?;
273+
build_host_function_parameters(&contract_id, &self.slop, spec_entries, config)?;
274274
}
275275

276276
let client = network.rpc_client()?;
@@ -295,7 +295,7 @@ impl Cmd {
295295
.map_err(Error::from)?;
296296

297297
let params =
298-
build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config).await?;
298+
build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config)?;
299299

300300
let (function, spec, host_function_params, signers) = params;
301301

cmd/soroban-cli/src/commands/keys/add.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ pub enum Error {
4545

4646
#[error("--hd-path is not valid with a secret key; secret keys cannot be derived")]
4747
HdPathNotSupportedForSecretKey,
48-
49-
#[error("--hd-path {0} is out of range for a Ledger account index")]
50-
HdPathOutOfRange(usize),
5148
}
5249

5350
#[derive(Debug, clap::Parser, Clone)]
@@ -94,7 +91,7 @@ pub struct Cmd {
9491
/// without re-passing the flag. Not valid with `--public-key` or a raw
9592
/// secret key.
9693
#[arg(long)]
97-
pub hd_path: Option<usize>,
94+
pub hd_path: Option<u32>,
9895
}
9996

10097
impl Cmd {
@@ -125,9 +122,10 @@ impl Cmd {
125122
}
126123

127124
async fn derive_ledger_secret(&self) -> Result<Secret, Error> {
128-
let raw = self.hd_path.unwrap_or(0);
129-
let index: u32 = raw.try_into().map_err(|_| Error::HdPathOutOfRange(raw))?;
130-
let public_key = ledger::new(index).await?.public_key().await?;
125+
let public_key = ledger::new(self.hd_path.unwrap_or_default())
126+
.await?
127+
.public_key()
128+
.await?;
131129
Ok(Secret::Ledger {
132130
hardware: HardwareKind::Ledger,
133131
public_key: public_key.to_string(),
@@ -181,7 +179,7 @@ impl Cmd {
181179
}
182180
}
183181

184-
fn build_secret(input: &str, hd_path: Option<usize>) -> Result<Secret, Error> {
182+
fn build_secret(input: &str, hd_path: Option<u32>) -> Result<Secret, Error> {
185183
let secret: Secret = input.parse()?;
186184
match (secret, hd_path) {
187185
(Secret::SecretKey { .. }, Some(_)) => Err(Error::HdPathNotSupportedForSecretKey),
@@ -248,7 +246,7 @@ mod tests {
248246

249247
fn cmd_with_public_key(
250248
public_key: &str,
251-
hd_path: Option<usize>,
249+
hd_path: Option<u32>,
252250
) -> (tempfile::TempDir, locator::Args, Cmd) {
253251
let (temp_dir, locator, mut cmd) = set_up_test();
254252
cmd.public_key = Some(public_key.to_string());

cmd/soroban-cli/src/commands/keys/generate.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub struct Cmd {
5555
/// `--secure-store` or plain seed-phrase storage it is persisted on the identity
5656
/// so later commands derive the same account without re-passing the flag.
5757
#[arg(long)]
58-
pub hd_path: Option<usize>,
58+
pub hd_path: Option<u32>,
5959

6060
#[command(flatten)]
6161
pub network: network::Args,

cmd/soroban-cli/src/commands/keys/public_key.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ pub enum Error {
1111

1212
#[error(transparent)]
1313
Ledger(#[from] ledger::Error),
14-
15-
#[error("--hd-path {0} is out of range for a Ledger account index")]
16-
HdPathOutOfRange(usize),
1714
}
1815

1916
#[derive(Debug, clap::Parser, Clone)]
@@ -26,7 +23,7 @@ pub struct Cmd {
2623
/// If identity is a seed phrase use this hd path, default is 0.
2724
/// With --ledger this is the Ledger account index (default 0).
2825
#[arg(long)]
29-
pub hd_path: Option<usize>,
26+
pub hd_path: Option<u32>,
3027

3128
/// Derive the address from a connected Ledger hardware wallet at
3229
/// `m/44'/148'/N'`, where `N` defaults to 0 and can be set with
@@ -46,9 +43,10 @@ impl Cmd {
4643

4744
pub async fn public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
4845
if self.ledger {
49-
let raw = self.hd_path.unwrap_or(0);
50-
let index: u32 = raw.try_into().map_err(|_| Error::HdPathOutOfRange(raw))?;
51-
return Ok(ledger::new(index).await?.public_key().await?);
46+
return Ok(ledger::new(self.hd_path.unwrap_or_default())
47+
.await?
48+
.public_key()
49+
.await?);
5250
}
5351
let name = self
5452
.name

cmd/soroban-cli/src/commands/keys/secret.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub struct Cmd {
2929

3030
/// If identity is a seed phrase use this hd path, default is 0
3131
#[arg(long, conflicts_with = "phrase")]
32-
pub hd_path: Option<usize>,
32+
pub hd_path: Option<u32>,
3333

3434
#[command(flatten)]
3535
pub locator: locator::Args,

0 commit comments

Comments
 (0)