diff --git a/Cargo.lock b/Cargo.lock index 02508ea0f..00edbf350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,6 +853,16 @@ dependencies = [ "subtle 2.2.2", ] +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array 0.14.3", + "subtle 2.2.2", +] + [[package]] name = "crypto-mac" version = "0.10.0" @@ -1094,6 +1104,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek-bip32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057f328f31294b5ab432e6c39642f54afd1531677d6d4ba2905932844cc242f3" +dependencies = [ + "derivation-path", + "ed25519-dalek", + "failure", + "hmac 0.9.0", + "sha2 0.9.2", +] + [[package]] name = "educe" version = "0.4.13" @@ -1666,6 +1689,16 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" +dependencies = [ + "crypto-mac 0.9.1", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.10.1" @@ -5092,6 +5125,7 @@ dependencies = [ "derivation-path", "digest 0.9.0", "ed25519-dalek", + "ed25519-dalek-bip32", "generic-array 0.14.3", "hex", "hmac 0.10.1", diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index 6ed8b4e5c..702d14ce4 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -57,7 +57,7 @@ pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option { if let Some(value) = matches.value_of(name) { if value == ASK_KEYWORD { let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); - keypair_from_seed_phrase(name, skip_validation, true).ok() + keypair_from_seed_phrase(name, skip_validation, true, None).ok() } else { read_keypair_file(value).ok() } @@ -72,7 +72,7 @@ pub fn keypairs_of(matches: &ArgMatches<'_>, name: &str) -> Option> .filter_map(|value| { if value == ASK_KEYWORD { let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); - keypair_from_seed_phrase(name, skip_validation, true).ok() + keypair_from_seed_phrase(name, skip_validation, true, None).ok() } else { read_keypair_file(value).ok() } diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index e3e752a5f..cd2e93017 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -18,8 +18,8 @@ use { message::Message, pubkey::Pubkey, signature::{ - keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair, - read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer, + generate_seed_from_seed_phrase_and_passphrase, keypair_from_seed_and_derivation_path, + read_keypair, read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer, }, }, std::{ @@ -181,14 +181,17 @@ pub(crate) fn parse_signer_source>( if let Some(scheme) = uri.scheme() { let scheme = scheme.as_str().to_ascii_lowercase(); match scheme.as_str() { - "ask" => Ok(SignerSource::new(SignerSourceKind::Ask)), + "ask" => Ok(SignerSource { + kind: SignerSourceKind::Ask, + derivation_path: DerivationPath::from_uri_any_query(&uri)?, + }), "file" => Ok(SignerSource::new(SignerSourceKind::Filepath( uri.path().to_string(), ))), "stdin" => Ok(SignerSource::new(SignerSourceKind::Stdin)), "usb" => Ok(SignerSource { kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?), - derivation_path: DerivationPath::from_uri(&uri)?, + derivation_path: DerivationPath::from_uri_key_query(&uri)?, }), _ => Err(SignerSourceError::UnrecognizedSource), } @@ -264,6 +267,7 @@ pub fn signer_from_path_with_config( keypair_name, skip_validation, false, + derivation_path, )?)) } SignerSourceKind::Filepath(path) => match read_keypair_file(&path) { @@ -341,7 +345,7 @@ pub fn resolve_signer_from_path( let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); // This method validates the seed phrase, but returns `None` because there is no path // on disk or to a device - keypair_from_seed_phrase(keypair_name, skip_validation, false).map(|_| None) + keypair_from_seed_phrase(keypair_name, skip_validation, false, derivation_path).map(|_| None) } SignerSourceKind::Filepath(path) => match read_keypair_file(&path) { Err(e) => Err(std::io::Error::new( @@ -407,6 +411,7 @@ pub fn keypair_from_seed_phrase( keypair_name: &str, skip_validation: bool, confirm_pubkey: bool, + derivation_path: Option, ) -> Result> { let seed_phrase = prompt_password_stderr(&format!("[{}] seed phrase: ", keypair_name))?; let seed_phrase = seed_phrase.trim(); @@ -417,7 +422,8 @@ pub fn keypair_from_seed_phrase( let keypair = if skip_validation { let passphrase = prompt_passphrase(&passphrase_prompt)?; - keypair_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase)? + let seed = generate_seed_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase); + keypair_from_seed_and_derivation_path(&seed, derivation_path)? } else { let sanitized = sanitize_seed_phrase(seed_phrase); let parse_language_fn = || { @@ -440,7 +446,7 @@ pub fn keypair_from_seed_phrase( let mnemonic = parse_language_fn()?; let passphrase = prompt_passphrase(&passphrase_prompt)?; let seed = Seed::new(&mnemonic, &passphrase); - keypair_from_seed(seed.as_bytes())? + keypair_from_seed_and_derivation_path(&seed.as_bytes(), derivation_path)? }; if confirm_pubkey { diff --git a/docs/src/cli/conventions.md b/docs/src/cli/conventions.md index 590bbd399..bdfc777a1 100644 --- a/docs/src/cli/conventions.md +++ b/docs/src/cli/conventions.md @@ -46,15 +46,15 @@ on your wallet type. #### Paper Wallet In a paper wallet, the keypair is securely derived from the seed words and -optional passphrase you entered when the wallet was create. To use a paper wallet -keypair anywhere the `` text is shown in examples or help documents, -enter the word `ASK` and the program will prompt you to enter your seed words -when you run the command. +optional passphrase you entered when the wallet was create. To use a paper +wallet keypair anywhere the `` text is shown in examples or help +documents, enter the uri scheme `ask://` and the program will prompt you to +enter your seed words when you run the command. To display the wallet address of a Paper Wallet: ```bash -solana-keygen pubkey ASK +solana-keygen pubkey ask:// ``` #### File System Wallet diff --git a/docs/src/running-validator/validator-start.md b/docs/src/running-validator/validator-start.md index ecedf9660..5c303d027 100644 --- a/docs/src/running-validator/validator-start.md +++ b/docs/src/running-validator/validator-start.md @@ -155,7 +155,7 @@ solana-keygen new --no-outfile The corresponding identity public key can now be viewed by running: ```bash -solana-keygen pubkey ASK +solana-keygen pubkey ask:// ``` and then entering your seed phrase. @@ -294,8 +294,8 @@ The ledger will be placed in the `ledger/` directory by default, use the > [paper wallet seed phrase](../wallet-guide/paper-wallet.md) > for your `--identity` and/or > `--authorized-voter` keypairs. To use these, pass the respective argument as -> `solana-validator --identity ASK ... --authorized-voter ASK ...` and you will be -> prompted to enter your seed phrases and optional passphrase. +> `solana-validator --identity ask:// ... --authorized-voter ask:// ...` +> and you will be prompted to enter your seed phrases and optional passphrase. Confirm your validator connected to the network by opening a new terminal and running: diff --git a/docs/src/wallet-guide/paper-wallet.md b/docs/src/wallet-guide/paper-wallet.md index d70c72308..15d5fe528 100644 --- a/docs/src/wallet-guide/paper-wallet.md +++ b/docs/src/wallet-guide/paper-wallet.md @@ -85,12 +85,13 @@ solana-keygen new --help ### Public Key Derivation Public keys can be derived from a seed phrase and a passphrase if you choose to -use one. This is useful for using an offline-generated seed phrase to -derive a valid public key. The `solana-keygen pubkey` command will walk you -through entering your seed phrase and a passphrase if you chose to use one. +use one. This is useful for using an offline-generated seed phrase to derive a +valid public key. The `solana-keygen pubkey` command will walk you through how +to use your seed phrase (and a passphrase if you chose to use one) as a signer +with the solana command-line tools using the `ask` uri scheme. ```bash -solana-keygen pubkey ASK +solana-keygen pubkey ask:// ``` > Note that you could potentially use different passphrases for the same seed phrase. Each unique passphrase will yield a different keypair. @@ -102,11 +103,11 @@ will need to pass the `--skip-seed-phrase-validation` argument and forego this validation. ```bash -solana-keygen pubkey ASK --skip-seed-phrase-validation +solana-keygen pubkey ask:// --skip-seed-phrase-validation ``` -After entering your seed phrase with `solana-keygen pubkey ASK` the console -will display a string of base-58 character. This is the _wallet address_ +After entering your seed phrase with `solana-keygen pubkey ask://` the console +will display a string of base-58 character. This is the base _wallet address_ associated with your seed phrase. > Copy the derived address to a USB stick for easy usage on networked computers @@ -119,20 +120,48 @@ For full usage details run: solana-keygen pubkey --help ``` +### Hierarchical Derivation + +The solana-cli supports +[BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and +[BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) +hierarchical derivation of private keys from your seed phrase and passphrase by +adding either the `?key=` query string or the `?full-path=` query string. + +To use solana's BIP44 derivation path `m/44'/501'`, supply the `?key=m` query +string, or `?key=/`. + +```bash +solana-keygen pubkey ask://?key=0/1 +``` + +To use a derivation path other than solana's standard BIP44, you can supply `?full-path=m////`. + +```bash +solana-keygen pubkey ask://?full-path=m/44/2017/0/1 +``` + +Because Solana uses Ed25519 keypairs, as per +[SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) all +derivation-path indexes will be promoted to hardened indexes -- eg. +`?key=0'/0'`, `?full-path=m/44'/2017'/0'/1'` -- regardless of whether ticks are +included in the query-string input. + ## Verifying the Keypair To verify you control the private key of a paper wallet address, use `solana-keygen verify`: ```bash -solana-keygen verify ASK +solana-keygen verify ask:// ``` -where `` is replaced with the wallet address and they keyword `ASK` tells the -command to prompt you for the keypair's seed phrase. Note that for security -reasons, your seed phrase will not be displayed as you type. After entering your -seed phrase, the command will output "Success" if the given public key matches the -keypair generated from your seed phrase, and "Failed" otherwise. +where `` is replaced with the wallet address and they keyword `ask://` +tells the command to prompt you for the keypair's seed phrase; `key` and +`full-path` query-strings accepted. Note that for security reasons, your seed +phrase will not be displayed as you type. After entering your seed phrase, the +command will output "Success" if the given public key matches the keypair +generated from your seed phrase, and "Failed" otherwise. ## Checking Account Balance diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index a6473b07a..7274cdc5f 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -589,7 +589,7 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { } let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name); - let keypair = keypair_from_seed_phrase("recover", skip_validation, true)?; + let keypair = keypair_from_seed_phrase("recover", skip_validation, true, None)?; output_keypair(&keypair, &outfile, "recovered")?; } ("grind", Some(matches)) => { diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 2e660aa0c..fb37c11ed 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -629,6 +629,16 @@ dependencies = [ "subtle 2.2.2", ] +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array 0.14.3", + "subtle 2.2.2", +] + [[package]] name = "crypto-mac" version = "0.10.0" @@ -786,6 +796,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek-bip32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057f328f31294b5ab432e6c39642f54afd1531677d6d4ba2905932844cc242f3" +dependencies = [ + "derivation-path", + "ed25519-dalek", + "failure", + "hmac 0.9.0", + "sha2 0.9.2", +] + [[package]] name = "educe" version = "0.4.14" @@ -860,6 +883,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" dependencies = [ + "backtrace", "failure_derive", ] @@ -1217,6 +1241,16 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" +dependencies = [ + "crypto-mac 0.9.1", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.10.1" @@ -3450,6 +3484,7 @@ dependencies = [ "derivation-path", "digest 0.9.0", "ed25519-dalek", + "ed25519-dalek-bip32", "generic-array 0.14.3", "hex", "hmac 0.10.1", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index cb7aa674a..88e6b3a23 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -28,6 +28,7 @@ full = [ "rand_chacha", "serde_json", "ed25519-dalek", + "ed25519-dalek-bip32", "solana-logger", "solana-crate-features", "libsecp256k1", @@ -46,6 +47,7 @@ curve25519-dalek = { version = "2.1.0", optional = true } derivation-path = { version = "0.1.3", default-features = false } digest = { version = "0.9.0", optional = true } ed25519-dalek = { version = "=1.0.1", optional = true } +ed25519-dalek-bip32 = { version = "0.1.1", optional = true } generic-array = { version = "0.14.3", default-features = false, features = ["serde", "more_lengths"], optional = true } hex = "0.4.2" hmac = "0.10.1" diff --git a/sdk/src/derivation_path.rs b/sdk/src/derivation_path.rs index f1c7b8354..11c563177 100644 --- a/sdk/src/derivation_path.rs +++ b/sdk/src/derivation_path.rs @@ -44,6 +44,12 @@ impl TryFrom<&str> for DerivationPath { } } +impl AsRef<[ChildIndex]> for DerivationPath { + fn as_ref(&self) -> &[ChildIndex] { + &self.0.as_ref() + } +} + impl DerivationPath { fn new>>(path: P) -> Self { Self(DerivationPathInner::new(path)) @@ -54,8 +60,12 @@ impl DerivationPath { } fn from_key_str_with_coin(path: &str, coin: T) -> Result { - let path = format!("m/{}", path); - let extend = DerivationPathInner::from_str(&path) + let master_path = if path == "m" { + path.to_string() + } else { + format!("m/{}", path) + }; + let extend = DerivationPathInner::from_str(&master_path) .map_err(|err| DerivationPathError::InvalidDerivationPath(err.to_string()))?; let mut extend = extend.into_iter(); let account = extend.next().map(|index| index.to_u32()); @@ -69,7 +79,7 @@ impl DerivationPath { Ok(Self::new_bip44_with_coin(coin, account, change)) } - fn _from_absolute_path_str(path: &str) -> Result { + fn from_absolute_path_str(path: &str) -> Result { let inner = DerivationPath::_from_absolute_path_insecure_str(path)? .into_iter() .map(|c| ChildIndex::Hardened(c.to_u32())) @@ -123,8 +133,18 @@ impl DerivationPath { } } - // Only accepts single query string pair of type `key` - pub fn from_uri(uri: &URIReference<'_>) -> Result, DerivationPathError> { + pub fn from_uri_key_query(uri: &URIReference<'_>) -> Result, DerivationPathError> { + Self::from_uri(uri, true) + } + + pub fn from_uri_any_query(uri: &URIReference<'_>) -> Result, DerivationPathError> { + Self::from_uri(uri, false) + } + + fn from_uri( + uri: &URIReference<'_>, + key_only: bool, + ) -> Result, DerivationPathError> { if let Some(query) = uri.query() { let query_str = query.as_str(); if query_str.is_empty() { @@ -136,16 +156,26 @@ impl DerivationPath { "invalid query string, extra fields not supported".to_string(), )); } - let key = query.get("key"); - if key.is_none() { + let key = query.get(QueryKey::Key.as_ref()); + if let Some(key) = key { + // Use from_key_str instead of TryInto here to make it more explicit that this + // generates a Solana bip44 DerivationPath + return Self::from_key_str(key).map(Some); + } + if key_only { return Err(DerivationPathError::InvalidDerivationPath(format!( "invalid query string `{}`, only `key` supported", query_str, ))); } - // Use from_key_str instead of TryInto here to make it a little more explicit that this - // generates a Solana bip44 DerivationPath - key.map(Self::from_key_str).transpose() + let full_path = query.get(QueryKey::FullPath.as_ref()); + if let Some(full_path) = full_path { + return Self::from_absolute_path_str(full_path).map(Some); + } + Err(DerivationPathError::InvalidDerivationPath(format!( + "invalid query string `{}`, only `key` and `full-path` supported", + query_str, + ))) } else { Ok(None) } @@ -170,6 +200,46 @@ impl<'a> IntoIterator for &'a DerivationPath { } } +const QUERY_KEY_FULL_PATH: &str = "full-path"; +const QUERY_KEY_KEY: &str = "key"; + +#[derive(Clone, Debug, Error, PartialEq)] +#[error("invalid query key `{0}`")] +struct QueryKeyError(String); + +enum QueryKey { + FullPath, + Key, +} + +impl FromStr for QueryKey { + type Err = QueryKeyError; + fn from_str(s: &str) -> Result { + let lowercase = s.to_ascii_lowercase(); + match lowercase.as_str() { + QUERY_KEY_FULL_PATH => Ok(Self::FullPath), + QUERY_KEY_KEY => Ok(Self::Key), + _ => Err(QueryKeyError(s.to_string())), + } + } +} + +impl AsRef for QueryKey { + fn as_ref(&self) -> &str { + match self { + Self::FullPath => QUERY_KEY_FULL_PATH, + Self::Key => QUERY_KEY_KEY, + } + } +} + +impl std::fmt::Display for QueryKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s: &str = self.as_ref(); + write!(f, "{}", s) + } +} + trait Bip44 { const PURPOSE: u32 = 44; const COIN: u32; @@ -240,41 +310,41 @@ mod tests { fn test_from_absolute_path_str() { let s = "m/44/501"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::default() ); let s = "m/44'/501'"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::default() ); let s = "m/44'/501'/1/2"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::new_bip44(Some(1), Some(2)) ); let s = "m/44'/501'/1'/2'"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::new_bip44(Some(1), Some(2)) ); // Test non-Solana Bip44 let s = "m/44'/999'/1/2"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2)) ); let s = "m/44'/999'/1'/2'"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2)) ); // Test non-bip44 paths let s = "m/501'/0'/0/0"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::new(vec![ ChildIndex::Hardened(501), ChildIndex::Hardened(0), @@ -284,7 +354,7 @@ mod tests { ); let s = "m/501'/0'/0'/0'"; assert_eq!( - DerivationPath::_from_absolute_path_str(s).unwrap(), + DerivationPath::from_absolute_path_str(s).unwrap(), DerivationPath::new(vec![ ChildIndex::Hardened(501), ChildIndex::Hardened(0), @@ -295,7 +365,7 @@ mod tests { } #[test] - fn test_new_from_uri() { + fn test_from_uri() { let derivation_path = DerivationPath::new_bip44(Some(0), Some(0)); // test://path?key=0/0 @@ -311,7 +381,7 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert_eq!( - DerivationPath::from_uri(&uri).unwrap(), + DerivationPath::from_uri(&uri, true).unwrap(), Some(derivation_path.clone()) ); @@ -328,7 +398,7 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert_eq!( - DerivationPath::from_uri(&uri).unwrap(), + DerivationPath::from_uri(&uri, true).unwrap(), Some(derivation_path.clone()) ); @@ -345,10 +415,27 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert_eq!( - DerivationPath::from_uri(&uri).unwrap(), + DerivationPath::from_uri(&uri, true).unwrap(), Some(derivation_path) ); + // test://path?key=m + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("key=m")) + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + DerivationPath::from_uri(&uri, true).unwrap(), + Some(DerivationPath::new_bip44(None, None)) + ); + // test://path let mut builder = URIReferenceBuilder::new(); builder @@ -359,7 +446,7 @@ mod tests { .try_path("") .unwrap(); let uri = builder.build().unwrap(); - assert_eq!(DerivationPath::from_uri(&uri).unwrap(), None); + assert_eq!(DerivationPath::from_uri(&uri, true).unwrap(), None); // test://path? let mut builder = URIReferenceBuilder::new(); @@ -373,7 +460,7 @@ mod tests { .try_query(Some("")) .unwrap(); let uri = builder.build().unwrap(); - assert_eq!(DerivationPath::from_uri(&uri).unwrap(), None); + assert_eq!(DerivationPath::from_uri(&uri, true).unwrap(), None); // test://path?key=0/0/0 let mut builder = URIReferenceBuilder::new(); @@ -388,7 +475,7 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert!(matches!( - DerivationPath::from_uri(&uri), + DerivationPath::from_uri(&uri, true), Err(DerivationPathError::InvalidDerivationPath(_)) )); @@ -405,7 +492,7 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert!(matches!( - DerivationPath::from_uri(&uri), + DerivationPath::from_uri(&uri, true), Err(DerivationPathError::InvalidDerivationPath(_)) )); @@ -422,7 +509,7 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert!(matches!( - DerivationPath::from_uri(&uri), + DerivationPath::from_uri(&uri, true), Err(DerivationPathError::InvalidDerivationPath(_)) )); @@ -439,7 +526,7 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert!(matches!( - DerivationPath::from_uri(&uri), + DerivationPath::from_uri(&uri, true), Err(DerivationPathError::InvalidDerivationPath(_)) )); @@ -456,7 +543,7 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert!(matches!( - DerivationPath::from_uri(&uri), + DerivationPath::from_uri(&uri, true), Err(DerivationPathError::InvalidDerivationPath(_)) )); @@ -473,7 +560,182 @@ mod tests { .unwrap(); let uri = builder.build().unwrap(); assert!(matches!( - DerivationPath::from_uri(&uri), + DerivationPath::from_uri(&uri, true), + Err(DerivationPathError::InvalidDerivationPath(_)) + )); + } + + #[test] + fn test_from_uri_full_path() { + let derivation_path = DerivationPath::from_absolute_path_str("m/44'/999'/1'").unwrap(); + + // test://path?full-path=m/44/999/1 + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=m/44/999/1")) + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + DerivationPath::from_uri(&uri, false).unwrap(), + Some(derivation_path.clone()) + ); + + // test://path?full-path=m/44'/999'/1' + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=m/44'/999'/1'")) + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + DerivationPath::from_uri(&uri, false).unwrap(), + Some(derivation_path.clone()) + ); + + // test://path?full-path=m/44\'/999\'/1\' + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=m/44\'/999\'/1\'")) + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + DerivationPath::from_uri(&uri, false).unwrap(), + Some(derivation_path) + ); + + // test://path?full-path=m + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=m")) + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + DerivationPath::from_uri(&uri, false).unwrap(), + Some(DerivationPath(DerivationPathInner::from_str("m").unwrap())) + ); + + // test://path?full-path=m/44/999/1, only `key` supported + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=m/44/999/1")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + DerivationPath::from_uri(&uri, true), + Err(DerivationPathError::InvalidDerivationPath(_)) + )); + + // test://path?key=0/0&full-path=m/44/999/1 + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("key=0/0&full-path=m/44/999/1")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + DerivationPath::from_uri(&uri, false), + Err(DerivationPathError::InvalidDerivationPath(_)) + )); + + // test://path?full-path=m/44/999/1&bad-key=0/0 + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=m/44/999/1&bad-key=0/0")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + DerivationPath::from_uri(&uri, false), + Err(DerivationPathError::InvalidDerivationPath(_)) + )); + + // test://path?full-path=bad-value + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=bad-value")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + DerivationPath::from_uri(&uri, false), + Err(DerivationPathError::InvalidDerivationPath(_)) + )); + + // test://path?full-path= + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path=")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + DerivationPath::from_uri(&uri, false), + Err(DerivationPathError::InvalidDerivationPath(_)) + )); + + // test://path?full-path + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("test")) + .unwrap() + .try_authority(Some("path")) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("full-path")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + DerivationPath::from_uri(&uri, false), Err(DerivationPathError::InvalidDerivationPath(_)) )); } diff --git a/sdk/src/signature.rs b/sdk/src/signature.rs index 6d045f9b6..9874ddcdb 100644 --- a/sdk/src/signature.rs +++ b/sdk/src/signature.rs @@ -1,8 +1,9 @@ //! The `signature` module provides functionality for public, and private keys. #![cfg(feature = "full")] -use crate::{pubkey::Pubkey, transaction::TransactionError}; +use crate::{derivation_path::DerivationPath, pubkey::Pubkey, transaction::TransactionError}; use ed25519_dalek::Signer as DalekSigner; +use ed25519_dalek_bip32::Error as Bip32Error; use generic_array::{typenum::U64, GenericArray}; use hmac::Hmac; use itertools::Itertools; @@ -396,10 +397,37 @@ pub fn keypair_from_seed(seed: &[u8]) -> Result> Ok(Keypair(dalek_keypair)) } -pub fn keypair_from_seed_phrase_and_passphrase( +/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided; +/// otherwise builds standard Keypair using the seed as SecretKey +pub fn keypair_from_seed_and_derivation_path( + seed: &[u8], + derivation_path: Option, +) -> Result> { + if let Some(derivation_path) = derivation_path { + bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into()) + } else { + keypair_from_seed(seed) + } +} + +/// Generates a Keypair using Bip32 Hierarchical Derivation +fn bip32_derived_keypair( + seed: &[u8], + derivation_path: DerivationPath, +) -> Result { + let extended = ed25519_dalek_bip32::ExtendedSecretKey::from_seed(seed) + .and_then(|extended| extended.derive(&derivation_path))?; + let extended_public_key = extended.public_key(); + Ok(Keypair(ed25519_dalek::Keypair { + secret: extended.secret_key, + public: extended_public_key, + })) +} + +pub fn generate_seed_from_seed_phrase_and_passphrase( seed_phrase: &str, passphrase: &str, -) -> Result> { +) -> Vec { const PBKDF2_ROUNDS: u32 = 2048; const PBKDF2_BYTES: usize = 64; @@ -412,7 +440,17 @@ pub fn keypair_from_seed_phrase_and_passphrase( PBKDF2_ROUNDS, &mut seed, ); - keypair_from_seed(&seed[..]) + seed +} + +pub fn keypair_from_seed_phrase_and_passphrase( + seed_phrase: &str, + passphrase: &str, +) -> Result> { + keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase( + seed_phrase, + passphrase, + )) } #[cfg(test)]