Skip to content

Commit

Permalink
Implement Bip32 for seed-phrase/passphrase signing (solana-labs#16942)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
CriesofCarrots authored May 4, 2021
1 parent 6318705 commit 694c674
Show file tree
Hide file tree
Showing 11 changed files with 471 additions and 65 deletions.
34 changes: 34 additions & 0 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions clap-utils/src/input_parsers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand Down
20 changes: 13 additions & 7 deletions clap-utils/src/keypair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand All @@ -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 = || {
Expand All @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions docs/src/cli/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/src/running-validator/validator-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
55 changes: 42 additions & 13 deletions docs/src/wallet-guide/paper-wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion keygen/src/keygen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)) => {
Expand Down
35 changes: 35 additions & 0 deletions programs/bpf/Cargo.lock

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

2 changes: 2 additions & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ full = [
"rand_chacha",
"serde_json",
"ed25519-dalek",
"ed25519-dalek-bip32",
"solana-logger",
"solana-crate-features",
"libsecp256k1",
Expand All @@ -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"
Expand Down
Loading

0 comments on commit 694c674

Please sign in to comment.