Implement Bip32 for seed-phrase/passphrase signing (#16942)

* Add Keypair helpers for bip32 derivation

* Plumb bip32 for SignerSourceKind::Ask

* Support full-path querystring

* Use as_ref

* Add public wrappers for from_uri cases

* Support master root derivations (and fix too-deep print

* Add ask:// HD documentation

* Update ASK elsewhere in docs
This commit is contained in:
Tyera Eulberg 2021-05-03 19:58:56 -06:00 committed by GitHub
parent 6318705607
commit 694c674aa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 471 additions and 65 deletions

34
Cargo.lock generated
View File

@ -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",

View File

@ -57,7 +57,7 @@ pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option<Keypair> {
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<Vec<Keypair>>
.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()
}

View File

@ -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<S: AsRef<str>>(
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<DerivationPath>,
) -> Result<Keypair, Box<dyn error::Error>> {
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 {

View File

@ -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 `<KEYPAIR>` 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 `<KEYPAIR>` 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

View File

@ -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:

View File

@ -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=<ACCOUNT>/<CHANGE>`.
```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/<PURPOSE>/<COIN_TYPE>/<ACCOUNT>/<CHANGE>`.
```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 <PUBKEY> ASK
solana-keygen verify <PUBKEY> ask://
```
where `<PUBKEY>` 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 `<PUBKEY>` 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

View File

@ -589,7 +589,7 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
}
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)) => {

View File

@ -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",

View File

@ -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"

View File

@ -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<P: Into<Box<[ChildIndex]>>>(path: P) -> Self {
Self(DerivationPathInner::new(path))
@ -54,8 +60,12 @@ impl DerivationPath {
}
fn from_key_str_with_coin<T: Bip44>(path: &str, coin: T) -> Result<Self, DerivationPathError> {
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<Self, DerivationPathError> {
fn from_absolute_path_str(path: &str) -> Result<Self, DerivationPathError> {
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<Option<Self>, DerivationPathError> {
pub fn from_uri_key_query(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
Self::from_uri(uri, true)
}
pub fn from_uri_any_query(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
Self::from_uri(uri, false)
}
fn from_uri(
uri: &URIReference<'_>,
key_only: bool,
) -> Result<Option<Self>, 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<Self, Self::Err> {
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<str> 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(_))
));
}

View File

@ -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<Keypair, Box<dyn error::Error>>
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<DerivationPath>,
) -> Result<Keypair, Box<dyn error::Error>> {
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<Keypair, Bip32Error> {
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<Keypair, Box<dyn error::Error>> {
) -> Vec<u8> {
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, Box<dyn error::Error>> {
keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase(
seed_phrase,
passphrase,
))
}
#[cfg(test)]