Skip to content

Commit

Permalink
Initial ledger impl in solana-keygen
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyera Eulberg committed Jan 31, 2020
1 parent 1579044 commit d69ace5
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 27 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clap-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ edition = "2018"
clap = "2.33.0"
rpassword = "4.0"
semver = "0.9.0"
solana-hardware-wallet = { path = "../hardware-wallet", version = "0.24.0" }
solana-sdk = { path = "../sdk", version = "0.24.0" }
tiny-bip39 = "0.7.0"
url = "2.1.0"
Expand Down
47 changes: 47 additions & 0 deletions clap-utils/src/input_parsers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG};
use chrono::DateTime;
use clap::ArgMatches;
use solana_hardware_wallet::hardware_wallet::DerivationPath;
use solana_sdk::{
clock::UnixTimestamp,
native_token::sol_to_lamports,
Expand Down Expand Up @@ -100,6 +101,16 @@ pub fn amount_of(matches: &ArgMatches<'_>, name: &str, unit: &str) -> Option<u64
}
}

pub fn derivation_of(matches: &ArgMatches<'_>, name: &str) -> Option<DerivationPath> {
matches.value_of(name).map(|derivation_str| {
let derivation_str = derivation_str.replace("'", "");
let mut parts = derivation_str.split("/");
let account = parts.next().unwrap().parse::<u16>().unwrap();
let change = parts.next().map(|change| change.parse::<u16>().unwrap());
DerivationPath { account, change }
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -277,4 +288,40 @@ mod tests {
.get_matches_from(vec!["test", "--single", "1.5", "--unit", "lamports"]);
assert_eq!(amount_of(&matches, "single", "unit"), None);
}

#[test]
fn test_derivation_of() {
let matches = app()
.clone()
.get_matches_from(vec!["test", "--single", "2/3"]);
assert_eq!(
derivation_of(&matches, "single"),
Some(DerivationPath {
account: 2,
change: Some(3)
})
);
assert_eq!(derivation_of(&matches, "another"), None);
let matches = app()
.clone()
.get_matches_from(vec!["test", "--single", "2"]);
assert_eq!(
derivation_of(&matches, "single"),
Some(DerivationPath {
account: 2,
change: None
})
);
assert_eq!(derivation_of(&matches, "another"), None);
let matches = app()
.clone()
.get_matches_from(vec!["test", "--single", "2'/3'"]);
assert_eq!(
derivation_of(&matches, "single"),
Some(DerivationPath {
account: 2,
change: Some(3)
})
);
}
}
42 changes: 42 additions & 0 deletions clap-utils/src/input_validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,45 @@ pub fn is_rfc3339_datetime(value: String) -> Result<(), String> {
.map(|_| ())
.map_err(|e| format!("{:?}", e))
}

pub fn is_derivation(value: String) -> Result<(), String> {
let value = value.replace("'", "");
let mut parts = value.split("/");
let account = parts.next().unwrap();
account
.parse::<u16>()
.map_err(|e| {
format!(
"Unable to parse derivation, provided: {}, err: {:?}",
account, e
)
})
.and_then(|_| {
if let Some(change) = parts.next() {
change.parse::<u16>().map_err(|e| {
format!(
"Unable to parse derivation, provided: {}, err: {:?}",
change, e
)
})
} else {
Ok(0)
}
})
.map(|_| ())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_is_derivation() {
assert_eq!(is_derivation("2".to_string()), Ok(()));
assert_eq!(is_derivation("0".to_string()), Ok(()));
assert_eq!(is_derivation("0/2".to_string()), Ok(()));
assert_eq!(is_derivation("0'/2'".to_string()), Ok(()));
assert!(is_derivation("a".to_string()).is_err());
assert!(is_derivation("a/b".to_string()).is_err());
}
}
29 changes: 26 additions & 3 deletions hardware-wallet/src/hardware_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use log::*;
use parking_lot::{Mutex, RwLock};
use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};
use std::{
fmt,
sync::Arc,
time::{Duration, Instant},
};
Expand Down Expand Up @@ -145,13 +146,12 @@ pub trait HardwareWallet {
) -> Result<HardwareWalletInfo, HardwareWalletError>;

/// Get solana pubkey from a HardwareWallet
fn get_pubkey(&self, account: u16, change: Option<u16>) -> Result<Pubkey, HardwareWalletError>;
fn get_pubkey(&self, derivation: DerivationPath) -> Result<Pubkey, HardwareWalletError>;

/// Sign transaction data with wallet managing pubkey at derivation path m/44'/501'/<account>'/<change>'.
fn sign_transaction(
&self,
account: u16,
change: Option<u16>,
derivation: DerivationPath,
transaction: Transaction,
) -> Result<Signature, HardwareWalletError>;
}
Expand Down Expand Up @@ -183,7 +183,30 @@ pub struct HardwareWalletInfo {
pub pubkey: Pubkey,
}

#[derive(Default, PartialEq)]
pub struct DerivationPath {
pub account: u16,
pub change: Option<u16>,
}

impl fmt::Debug for DerivationPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let change = if let Some(change) = self.change {
format!("/{:?}'", change)
} else {
"".to_string()
};
write!(f, "m/44'/501'/{:?}'{}", self.account, change)
}
}

/// Helper to determine if a device is a valid HID
pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool {
usage_page == HID_GLOBAL_USAGE_PAGE || interface_number == HID_USB_DEVICE_CLASS as i32
}

/// Helper to initialize hidapi and HardwareWalletManager
pub fn initialize_wallet_manager() -> Arc<HardwareWalletManager> {
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
HardwareWalletManager::new(hidapi)
}
39 changes: 20 additions & 19 deletions hardware-wallet/src/ledger.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::hardware_wallet::{HardwareWallet, HardwareWalletError, HardwareWalletInfo};
use crate::hardware_wallet::{
DerivationPath, HardwareWallet, HardwareWalletError, HardwareWalletInfo,
};
use log::*;
use semver::Version as FirmwareVersion;
use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};
Expand Down Expand Up @@ -225,13 +227,13 @@ impl LedgerWallet {
))
}

fn get_derivation_path(&self, account: u16, change: Option<u16>) -> Vec<u8> {
let byte = if change.is_some() { 4 } else { 3 };
fn get_derivation_path(&self, derivation: DerivationPath) -> Vec<u8> {
let byte = if derivation.change.is_some() { 4 } else { 3 };
let mut concat_derivation = vec![byte];
concat_derivation.extend_from_slice(&SOL_DERIVATION_PATH_BE);
concat_derivation.extend_from_slice(&[0x80, 0]);
concat_derivation.extend_from_slice(&account.to_be_bytes());
if let Some(change) = change {
concat_derivation.extend_from_slice(&derivation.account.to_be_bytes());
if let Some(change) = derivation.change {
concat_derivation.extend_from_slice(&[0x80, 0]);
concat_derivation.extend_from_slice(&change.to_be_bytes());
}
Expand All @@ -256,23 +258,24 @@ impl HardwareWallet for LedgerWallet {
.serial_number
.clone()
.unwrap_or_else(|| "Unknown".to_owned());
self.get_pubkey(0, None).map(|pubkey| HardwareWalletInfo {
name,
manufacturer,
serial,
pubkey,
})
self.get_pubkey(DerivationPath::default())
.map(|pubkey| HardwareWalletInfo {
name,
manufacturer,
serial,
pubkey,
})
}

fn get_pubkey(&self, account: u16, change: Option<u16>) -> Result<Pubkey, HardwareWalletError> {
fn get_pubkey(&self, derivation: DerivationPath) -> Result<Pubkey, HardwareWalletError> {
// let ledger_version = self.get_firmware_version()?;
// if ledger_version < FirmwareVersion::new(1, 0, 3) {
// return Err(HardwareWalletError::Protocol(
// "Ledger version 1.0.3 is required",
// ));
// }

let derivation_path = self.get_derivation_path(account, change);
let derivation_path = self.get_derivation_path(derivation);

let key = self.send_apdu(commands::GET_SOL_PUBKEY, 0, 0, &derivation_path)?;
if key.len() != 32 {
Expand All @@ -283,8 +286,7 @@ impl HardwareWallet for LedgerWallet {

fn sign_transaction(
&self,
account: u16,
change: Option<u16>,
derivation: DerivationPath,
transaction: Transaction,
) -> Result<Signature, HardwareWalletError> {
// TODO: see if need version check here
Expand All @@ -296,7 +298,7 @@ impl HardwareWallet for LedgerWallet {
// }

let mut chunk = [0_u8; MAX_CHUNK_SIZE];
let derivation_path = self.get_derivation_path(account, change);
let derivation_path = self.get_derivation_path(derivation);
let data = transaction.message_data();

// Copy the address of the key (only done once)
Expand Down Expand Up @@ -346,15 +348,14 @@ pub fn is_valid_ledger(vendor_id: u16, product_id: u16) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::hardware_wallet::HardwareWalletManager;
use crate::hardware_wallet::{initialize_wallet_manager, HardwareWalletManager};
use parking_lot::Mutex;
use std::sync::Arc;

/// This test can't be run without an actual ledger device connected with the `Ledger Wallet Solana application` running
#[test]
fn ledger_test() {
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
let wallet_manager = HardwareWalletManager::new(hidapi);
let wallet_manager = initialize_wallet_manager();

// Update device list
wallet_manager.update_devices().expect("No Ledger found, make sure you have a unlocked Ledger connected with the Ledger Wallet Solana running");
Expand Down
1 change: 1 addition & 0 deletions keygen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dirs = "2.0.2"
num_cpus = "1.12.0"
rpassword = "4.0"
solana-clap-utils = { path = "../clap-utils", version = "0.24.0" }
solana-hardware-wallet = { path = "../hardware-wallet", version = "0.24.0" }
solana-sdk = { path = "../sdk", version = "0.24.0" }
tiny-bip39 = "0.7.0"

Expand Down
77 changes: 72 additions & 5 deletions keygen/src/keygen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ use clap::{
SubCommand,
};
use num_cpus;
use solana_clap_utils::keypair::{
keypair_from_seed_phrase, prompt_passphrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
use solana_clap_utils::{
input_parsers::{derivation_of, pubkey_of},
input_validators::is_derivation,
keypair::{
keypair_from_seed_phrase, prompt_passphrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
},
};
use solana_hardware_wallet::hardware_wallet::{
initialize_wallet_manager, DerivationPath, HardwareWallet,
};
use solana_sdk::{
pubkey::write_pubkey_file,
Expand Down Expand Up @@ -300,6 +307,27 @@ fn main() -> Result<(), Box<dyn error::Error>> {
SubCommand::with_name("pubkey")
.about("Display the pubkey from a keypair file")
.setting(AppSettings::DisableVersion)
.subcommand(
SubCommand::with_name("hw")
.about("Use hardware wallet for key retrieval")
.setting(AppSettings::DisableVersion)
.arg(
Arg::with_name("derivation")
.index(1)
.value_name("ACCOUNT or ACCOUNT/CHANGE")
.takes_value(true)
.validator(is_derivation)
.help("Which derivation path to use: m/44'/501'/ACCOUNT'/CHANGE'; default key is device base pubkey: m/44'/501'/0'")
)
.arg(
Arg::with_name("wallet_base_pubkey")
.long("base-pubkey")
.value_name("PUBKEY")
.takes_value(true)
.help("Specify which hardware wallet to use")

)
)
.arg(
Arg::with_name("infile")
.index(1)
Expand Down Expand Up @@ -356,14 +384,53 @@ fn main() -> Result<(), Box<dyn error::Error>> {

match matches.subcommand() {
("pubkey", Some(matches)) => {
let keypair = get_keypair_from_matches(matches)?;
let pubkey = if let Some(matches) = matches.subcommand_matches("hw") {
// TODO: move all of this into helper(s) so it can be used in CLI as well
let wallet_manager = initialize_wallet_manager();
let device_count = wallet_manager.update_devices()?;
if device_count == 0 {
println!(
"No hardware wallets found. Make sure the Solana Wallet app is running."
);
exit(1);
}
let wallet_base_pubkey =
pubkey_of(matches, "wallet_base_pubkey").unwrap_or_else(|| {
if device_count > 1 {
// TODO: Prompt for choice
println!("Choosing between multiple wallets currently not supported");
exit(1);
}
wallet_manager.list_devices()[0].pubkey
});

let ledger = wallet_manager.get_ledger(&wallet_base_pubkey).unwrap_or_else(|_| {
println!("Wallet {:?} not found. Make sure the Solana Wallet app is still running.", wallet_base_pubkey);
exit(1);
});
println!(
"Getting key from wallet: {:?}",
wallet_manager.get_wallet_info(&wallet_base_pubkey).unwrap()
);

let derivation = derivation_of(matches, "derivation")
.unwrap_or_else(|| DerivationPath::default());
println!("Derivation path: {:?}", derivation);
ledger.get_pubkey(derivation).unwrap_or_else(|e| {
println!("Wallet did not return pubkey: {:?}", e);
exit(1);
})
} else {
let keypair = get_keypair_from_matches(matches)?;
keypair.pubkey()
};

if matches.is_present("outfile") {
let outfile = matches.value_of("outfile").unwrap();
check_for_overwrite(&outfile, &matches);
write_pubkey_file(outfile, keypair.pubkey())?;
write_pubkey_file(outfile, pubkey)?;
} else {
println!("{}", keypair.pubkey());
println!("{}", pubkey);
}
}
("new", Some(matches)) => {
Expand Down

0 comments on commit d69ace5

Please sign in to comment.