From 8aa819c22412195df161b2f3fe96fd9a823895c7 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 2 May 2024 14:10:55 -0400 Subject: [PATCH] feat: first pass at using ledger to sign --- .gitignore | 1 + Cargo.lock | 1 + Cargo.toml | 4 ++ cmd/crates/stellar-ledger/src/lib.rs | 52 ++++++++++++--- cmd/soroban-cli/Cargo.toml | 2 + cmd/soroban-cli/src/commands/txn/sign.rs | 60 +++++++++++++---- cmd/soroban-cli/src/signer.rs | 83 +++++++++++++++++------- 7 files changed, 157 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index bb94c12be..77af5e3ee 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ captive-core/ !test.toml *.sqlite test_snapshots +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0fbcc1f9d..c0e470555 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4694,6 +4694,7 @@ dependencies = [ "soroban-spec-rust", "soroban-spec-tools", "soroban-spec-typescript", + "stellar-ledger", "stellar-rpc-client", "stellar-strkey 0.0.8", "stellar-xdr", diff --git a/Cargo.toml b/Cargo.toml index 213ae8b65..f9a429737 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,10 @@ version = "=21.0.1-preview.1" version = "=21.0.0-preview.1" path = "cmd/soroban-cli" +[workspace.dependencies.stellar-ledger] +version = "=21.0.0-preview.1" +path = "./cmd/crates/stellar-ledger" + [workspace.dependencies.soroban-rpc] package = "stellar-rpc-client" version = "=21.0.1" diff --git a/cmd/crates/stellar-ledger/src/lib.rs b/cmd/crates/stellar-ledger/src/lib.rs index 89b6725c2..c3f7b202d 100644 --- a/cmd/crates/stellar-ledger/src/lib.rs +++ b/cmd/crates/stellar-ledger/src/lib.rs @@ -1,6 +1,9 @@ use futures::executor::block_on; use ledger_transport::{APDUCommand, Exchange}; -use ledger_transport_hid::{hidapi::HidError, LedgerHIDError}; +use ledger_transport_hid::{ + hidapi::{self, HidError}, + LedgerHIDError, TransportNativeHID, +}; use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{Hash, Transaction}; @@ -11,7 +14,7 @@ use stellar_xdr::curr::{ TransactionV1Envelope, WriteXdr, }; -use crate::signer::{Error, Stellar}; +pub use crate::signer::{Error, Stellar}; mod signer; mod speculos; @@ -71,11 +74,41 @@ pub struct LedgerOptions { exchange: T, hd_path: slip10::BIP32Path, } +// let hidapi = HidApi::new().map_err(NEARLedgerError::HidApiError)?; +// TransportNativeHID::new(&hidapi).map_err(NEARLedgerError::LedgerHidError) +impl LedgerOptions { + pub fn new(hd_path: u32) -> Self { + let hd_path = bip_path_from_index(hd_path); + let hidapi = hidapi::HidApi::new().unwrap(); + LedgerOptions { + exchange: TransportNativeHID::new(&hidapi).unwrap(), + hd_path, + } + } +} pub struct LedgerSigner { network_passphrase: String, transport: T, - hd_path: slip10::BIP32Path, + pub hd_path: slip10::BIP32Path, +} + +pub struct NativeSigner(LedgerSigner); + +impl AsRef> for NativeSigner { + fn as_ref(&self) -> &LedgerSigner { + &self.0 + } +} + +impl From<(String, u32)> for NativeSigner { + fn from((network_passphrase, hd_path): (String, u32)) -> Self { + Self(LedgerSigner { + network_passphrase, + transport: TransportNativeHID::new(&hidapi::HidApi::new().unwrap()).unwrap(), + hd_path: bip_path_from_index(hd_path), + }) + } } impl LedgerSigner @@ -114,7 +147,7 @@ where pub async fn sign_transaction_hash( &self, hd_path: slip10::BIP32Path, - transaction_hash: Vec, + transaction_hash: &[u8], ) -> Result, LedgerError> { let mut hd_path_to_bytes = hd_path_to_bytes(&hd_path); @@ -123,7 +156,7 @@ where data.insert(0, HD_PATH_ELEMENTS_COUNT); data.append(&mut hd_path_to_bytes); - data.append(&mut transaction_hash.clone()); + data.extend_from_slice(transaction_hash); let command = APDUCommand { cla: CLA, @@ -284,7 +317,7 @@ impl Stellar for LedgerSigner { txn: [u8; 32], _source_account: &stellar_strkey::Strkey, ) -> Result { - let signature = block_on(self.sign_transaction_hash(self.hd_path.clone(), txn.to_vec())) //TODO: refactor sign_transaction_hash + let signature = block_on(self.sign_transaction_hash(self.hd_path.clone(), &txn)) //TODO: refactor sign_transaction_hash .unwrap(); // FIXME: handle error let sig_bytes = signature.try_into().unwrap(); // FIXME: handle error @@ -516,10 +549,9 @@ mod test { let ledger = LedgerSigner::new(TEST_NETWORK_PASSPHRASE, ledger_options); let path = slip10::BIP32Path::from_str("m/44'/148'/0'").unwrap(); - let test_hash = - "3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889".as_bytes(); + let test_hash = b"3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889"; - let result = ledger.sign_transaction_hash(path, test_hash.into()).await; + let result = ledger.sign_transaction_hash(path, test_hash).await; if let Err(LedgerError::APDUExchangeError(msg)) = result { assert_eq!(msg, "Ledger APDU retcode: 0x6C66"); // this error code is SW_TX_HASH_SIGNING_MODE_NOT_ENABLED https://github.com/LedgerHQ/app-stellar/blob/develop/docs/COMMANDS.md @@ -563,7 +595,7 @@ mod test { } } - let result = ledger.sign_transaction_hash(path, test_hash).await; + let result = ledger.sign_transaction_hash(path, &test_hash).await; match result { Ok(response) => { diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index b60684f1f..99147b4d8 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -46,6 +46,8 @@ soroban-ledger-snapshot = { workspace = true } stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } soroban-rpc = { workspace = true } +stellar-ledger ={ workspace = true } + cargo_toml = "0.20.1" clap = { workspace = true, features = [ "derive", diff --git a/cmd/soroban-cli/src/commands/txn/sign.rs b/cmd/soroban-cli/src/commands/txn/sign.rs index ec4bb1155..c5d919a12 100644 --- a/cmd/soroban-cli/src/commands/txn/sign.rs +++ b/cmd/soroban-cli/src/commands/txn/sign.rs @@ -5,7 +5,9 @@ use std::io; // execute, // terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, // }; -use soroban_sdk::xdr::{self, Limits, TransactionEnvelope, WriteXdr}; +use soroban_sdk::xdr::{self, Limits, Transaction, TransactionEnvelope, WriteXdr}; +use stellar_ledger::NativeSigner; +use stellar_strkey::Strkey; use crate::signer::{self, InMemory, Stellar}; @@ -31,35 +33,39 @@ pub enum Error { #[group(skip)] pub struct Cmd { /// Confirm that a signature can be signed by the given keypair automatically. - #[arg(long, short = 'y')] + #[arg(long, short = 'y', short = 'Y')] yes: bool, #[clap(flatten)] pub xdr_args: super::xdr::Args, #[clap(flatten)] pub config: super::super::config::Args, + + #[arg(long, value_enum, default_value = "file")] + pub signer: SignerType, +} + +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum SignerType { + File, + Ledger, } impl Cmd { #[allow(clippy::unused_async)] pub async fn run(&self) -> Result<(), Error> { - let envelope = self.sign()?; + let envelope = self.sign().await?; println!("{}", envelope.to_xdr_base64(Limits::none())?.trim()); Ok(()) } - pub fn sign(&self) -> Result { + pub async fn sign(&self) -> Result { let source = &self.config.source_account; tracing::debug!("signing transaction with source account {}", source); let txn = self.xdr_args.txn()?; - let key = self.config.key_pair()?; - let address = - stellar_strkey::ed25519::PublicKey::from_payload(key.verifying_key().as_bytes())?; - let in_memory = InMemory { - network_passphrase: self.config.get_network()?.network_passphrase, - keypairs: vec![key], - }; - self.prompt_user()?; - Ok(in_memory.sign_txn(txn, &stellar_strkey::Strkey::PublicKeyEd25519(address))?) + match self.signer { + SignerType::File => self.sign_file(txn).await, + SignerType::Ledger => self.sign_ledger(txn).await, + } } pub fn prompt_user(&self) -> Result<(), Error> { @@ -94,4 +100,32 @@ impl Cmd { // execute!(stdout, LeaveAlternateScreen)?; // Ok(()) } + + pub async fn sign_file(&self, txn: Transaction) -> Result { + let key = self.config.key_pair()?; + let address = + stellar_strkey::ed25519::PublicKey::from_payload(key.verifying_key().as_bytes())?; + let in_memory = InMemory { + network_passphrase: self.config.get_network()?.network_passphrase, + keypairs: vec![key], + }; + self.prompt_user()?; + Ok(in_memory + .sign_txn(txn, &Strkey::PublicKeyEd25519(address)) + .await?) + } + + pub async fn sign_ledger(&self, txn: Transaction) -> Result { + let index: u32 = self + .config + .hd_path + .unwrap_or_default() + .try_into() + .expect("usize bigger than u32"); + let signer: NativeSigner = (self.config.get_network()?.network_passphrase, index).into(); + let account = + Strkey::PublicKeyEd25519(signer.as_ref().get_public_key(index).await.unwrap()); + let bx_signer = Box::new(signer); + Ok(bx_signer.sign_txn(txn, &account).await.unwrap()) + } } diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index bd5605598..a160af587 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -1,14 +1,15 @@ use ed25519_dalek::Signer; -use sha2::{Digest, Sha256}; +use sha2::{digest::typenum::Le, Digest, Sha256}; use soroban_env_host::xdr::{ self, AccountId, DecoratedSignature, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, - InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol, - ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, - SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, - TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ReadXdr, ScAddress, ScMap, + ScSymbol, ScVal, Signature, SignatureHint, SorobanAddressCredentials, + SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanCredentials, Transaction, + TransactionEnvelope, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, Uint256, WriteXdr, }; +use stellar_ledger::{LedgerSigner, NativeSigner}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -50,7 +51,7 @@ pub trait Stellar { &self, txn: [u8; 32], source_account: &stellar_strkey::Strkey, - ) -> Result; + ) -> impl std::future::Future> + Send; /// Sign a Soroban authorization entry with the given address /// # Errors @@ -60,13 +61,13 @@ pub trait Stellar { unsigned_entry: &SorobanAuthorizationEntry, signature_expiration_ledger: u32, address: &[u8; 32], - ) -> Result; + ) -> impl std::future::Future> + Send; /// Sign a Stellar transaction with the given source account /// This is a default implementation that signs the transaction hash and returns a decorated signature /// # Errors /// Returns an error if the source account is not found - fn sign_txn( + async fn sign_txn( &self, txn: Transaction, source_account: &stellar_strkey::Strkey, @@ -76,7 +77,7 @@ pub trait Stellar { tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(txn.clone()), }; let hash = Sha256::digest(signature_payload.to_xdr(Limits::none())?).into(); - let decorated_signature = self.sign_txn_hash(hash, source_account)?; + let decorated_signature = self.sign_txn_hash(hash, source_account).await?; Ok(TransactionEnvelope::Tx(TransactionV1Envelope { tx: txn, signatures: vec![decorated_signature].try_into()?, @@ -86,7 +87,7 @@ pub trait Stellar { /// Sign a Soroban authorization entries for a given transaction and set the expiration ledger /// # Errors /// Returns an error if the address is not found - fn sign_soroban_authorizations( + async fn sign_soroban_authorizations( &self, raw: &Transaction, signature_expiration_ledger: u32, @@ -104,16 +105,13 @@ pub trait Stellar { return Ok(None); }; - let signed_auths = body - .auth - .as_slice() - .iter() - .map(|raw_auth| { - self.maybe_sign_soroban_authorization_entry(raw_auth, signature_expiration_ledger) - }) - .collect::, Error>>()?; - - body.auth = signed_auths.try_into()?; + let mut auths = body.auth.to_vec(); + for auth in auths.iter_mut() { + *auth = self + .maybe_sign_soroban_authorization_entry(auth, signature_expiration_ledger) + .await?; + } + body.auth = auths.try_into()?; tx.operations = vec![op].try_into()?; Ok(Some(tx)) } @@ -121,7 +119,7 @@ pub trait Stellar { /// Sign a Soroban authorization entry if the address is public key /// # Errors /// Returns an error if the address in entry is a contract - fn maybe_sign_soroban_authorization_entry( + async fn maybe_sign_soroban_authorization_entry( &self, unsigned_entry: &SorobanAuthorizationEntry, signature_expiration_ledger: u32, @@ -149,6 +147,7 @@ pub trait Stellar { signature_expiration_ledger, needle, ) + .await } else { Ok(unsigned_entry.clone()) } @@ -190,7 +189,7 @@ impl Stellar for InMemory { } } - fn sign_txn_hash( + async fn sign_txn_hash( &self, txn: [u8; 32], source_account: &stellar_strkey::Strkey, @@ -208,7 +207,7 @@ impl Stellar for InMemory { }) } - fn sign_soroban_authorization_entry( + async fn sign_soroban_authorization_entry( &self, unsigned_entry: &SorobanAuthorizationEntry, signature_expiration_ledger: u32, @@ -275,3 +274,41 @@ impl Stellar for InMemory { xdr::Hash(Sha256::digest(self.network_passphrase.as_bytes()).into()) } } + +impl Stellar for Box { + type Init = u32; + + fn new(network_passphrase: &str, options: Option) -> Self { + Box::new((network_passphrase.to_owned(), options.unwrap_or_default()).into()) + } + + fn network_hash(&self) -> xdr::Hash { + use stellar_ledger::Stellar; + self.as_ref().as_ref().network_hash() + } + + async fn sign_txn_hash( + &self, + txn: [u8; 32], + _source_account: &stellar_strkey::Strkey, + ) -> Result { + Ok(DecoratedSignature::from_xdr( + self.as_ref() + .as_ref() + .sign_transaction_hash(self.as_ref().as_ref().hd_path.clone(), &txn) + .await + .unwrap(), + Limits::none(), + ) + .unwrap()) + } + + async fn sign_soroban_authorization_entry( + &self, + unsigned_entry: &SorobanAuthorizationEntry, + signature_expiration_ledger: u32, + address: &[u8; 32], + ) -> Result { + todo!() + } +}