Allow secure keypair input for `solana-archiver` and `solana` cli tools (#7106)

* Add seed phrase keypair recover to archiver

* Add seed phrase keypair to cli with ASK keyword

* cli main tweaks
This commit is contained in:
Justin Starry 2019-11-23 11:55:43 -05:00 committed by GitHub
parent 7f87ac4b65
commit b8cd0a1bc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 218 additions and 108 deletions

View File

@ -1,15 +1,18 @@
use clap::{crate_description, crate_name, App, Arg};
use console::style;
use solana_clap_utils::input_validators::is_keypair;
use solana_clap_utils::{
input_validators::is_keypair,
keypair::{
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
SKIP_SEED_PHRASE_VALIDATION_ARG,
},
};
use solana_core::{
archiver::Archiver,
cluster_info::{Node, VALIDATOR_PORT_RANGE},
contact_info::ContactInfo,
};
use solana_sdk::{
commitment_config::CommitmentConfig,
signature::{read_keypair_file, Keypair, KeypairUtil},
};
use solana_sdk::{commitment_config::CommitmentConfig, signature::KeypairUtil};
use std::{net::SocketAddr, path::PathBuf, process::exit, sync::Arc};
fn main() {
@ -19,9 +22,9 @@ fn main() {
.about(crate_description!())
.version(solana_clap_utils::version!())
.arg(
Arg::with_name("identity")
Arg::with_name("identity_keypair")
.short("i")
.long("identity")
.long("identity-keypair")
.value_name("PATH")
.takes_value(true)
.validator(is_keypair)
@ -52,30 +55,48 @@ fn main() {
.long("storage-keypair")
.value_name("PATH")
.takes_value(true)
.required(true)
.validator(is_keypair)
.help("File containing the storage account keypair"),
)
.arg(
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
.long(ASK_SEED_PHRASE_ARG.long)
.value_name("KEYPAIR NAME")
.multiple(true)
.takes_value(true)
.possible_values(&["identity-keypair", "storage-keypair"])
.help(ASK_SEED_PHRASE_ARG.help),
)
.arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
.requires(ASK_SEED_PHRASE_ARG.name)
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
)
.get_matches();
let ledger_path = PathBuf::from(matches.value_of("ledger").unwrap());
let keypair = if let Some(identity) = matches.value_of("identity") {
read_keypair_file(identity).unwrap_or_else(|err| {
eprintln!("{}: Unable to open keypair file: {}", err, identity);
let identity_keypair = keypair_input(&matches, "identity_keypair")
.unwrap_or_else(|err| {
eprintln!("Identity keypair input failed: {}", err);
exit(1);
})
} else {
Keypair::new()
};
let storage_keypair = if let Some(storage_keypair) = matches.value_of("storage_keypair") {
read_keypair_file(storage_keypair).unwrap_or_else(|err| {
eprintln!("{}: Unable to open keypair file: {}", err, storage_keypair);
exit(1);
})
} else {
Keypair::new()
};
.keypair;
let KeypairWithSource {
keypair: storage_keypair,
source: storage_keypair_source,
} = keypair_input(&matches, "storage_keypair").unwrap_or_else(|err| {
eprintln!("Storage keypair input failed: {}", err);
exit(1);
});
if storage_keypair_source == keypair::Source::Generated {
clap::Error::with_description(
"The `storage-keypair` argument was not found",
clap::ErrorKind::ArgumentNotFound,
)
.exit();
}
let entrypoint_addr = matches
.value_of("entrypoint")
@ -91,8 +112,11 @@ fn main() {
addr.set_ip(solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap());
addr
};
let node =
Node::new_archiver_with_external_ip(&keypair.pubkey(), &gossip_addr, VALIDATOR_PORT_RANGE);
let node = Node::new_archiver_with_external_ip(
&identity_keypair.pubkey(),
&gossip_addr,
VALIDATOR_PORT_RANGE,
);
println!(
"{} version {} (branch={}, commit={})",
@ -101,10 +125,10 @@ fn main() {
option_env!("CI_BRANCH").unwrap_or("unknown"),
option_env!("CI_COMMIT").unwrap_or("unknown")
);
solana_metrics::set_host_id(keypair.pubkey().to_string());
solana_metrics::set_host_id(identity_keypair.pubkey().to_string());
println!(
"replicating the data with keypair={:?} gossip_addr={:?}",
keypair.pubkey(),
"replicating the data with identity_keypair={:?} gossip_addr={:?}",
identity_keypair.pubkey(),
gossip_addr
);
@ -113,7 +137,7 @@ fn main() {
&ledger_path,
node,
entrypoint_info,
Arc::new(keypair),
Arc::new(identity_keypair),
Arc::new(storage_keypair),
CommitmentConfig::recent(),
)

View File

@ -138,7 +138,7 @@ Note: Every time the testnet restarts, run the steps to setup the archiver accou
To start the archiver:
```bash
solana-archiver --entrypoint testnet.solana.com:8001 --identity archiver-keypair.json --storage-keypair storage-keypair.json --ledger archiver-ledger
solana-archiver --entrypoint testnet.solana.com:8001 --identity-keypair archiver-keypair.json --storage-keypair storage-keypair.json --ledger archiver-ledger
```
## Verify Archiver Setup

View File

@ -1,3 +1,4 @@
use crate::keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG};
use clap::ArgMatches;
use solana_sdk::{
native_token::sol_to_lamports,
@ -32,7 +33,12 @@ where
// Return the keypair for an argument with filename `name` or None if not present.
pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option<Keypair> {
if let Some(value) = matches.value_of(name) {
read_keypair_file(value).ok()
if value == ASK_KEYWORD {
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
keypair_from_seed_phrase(name, skip_validation).ok()
} else {
read_keypair_file(value).ok()
}
} else {
None
}

View File

@ -1,3 +1,4 @@
use crate::keypair::ASK_KEYWORD;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::read_keypair_file;
@ -16,6 +17,16 @@ pub fn is_keypair(string: String) -> Result<(), String> {
.map_err(|err| format!("{:?}", err))
}
// Return an error if a keypair file cannot be parsed
pub fn is_keypair_or_ask_keyword(string: String) -> Result<(), String> {
if string.as_str() == ASK_KEYWORD {
return Ok(());
}
read_keypair_file(&string)
.map(|_| ())
.map_err(|err| format!("{:?}", err))
}
// Return an error if string cannot be parsed as pubkey string or keypair file location
pub fn is_pubkey_or_keypair(string: String) -> Result<(), String> {
is_pubkey(string.clone()).or_else(|_| is_keypair(string))

View File

@ -1,3 +1,4 @@
use crate::ArgConstant;
use bip39::{Language, Mnemonic, Seed};
use clap::values_t;
use rpassword::prompt_password_stderr;
@ -7,11 +8,41 @@ use solana_sdk::signature::{
};
use std::error;
pub const ASK_SEED_PHRASE_ARG: &str = "ask_seed_phrase";
pub const SKIP_SEED_PHRASE_VALIDATION_ARG: &str = "skip_seed_phrase_validation";
// Keyword used to indicate that the user should be asked for a keypair seed phrase
pub const ASK_KEYWORD: &str = "ASK";
pub const ASK_SEED_PHRASE_ARG: ArgConstant<'static> = ArgConstant {
long: "ask-seed-phrase",
name: "ask_seed_phrase",
help: "Securely recover a keypair using a seed phrase and optional passphrase",
};
pub const SKIP_SEED_PHRASE_VALIDATION_ARG: ArgConstant<'static> = ArgConstant {
long: "skip-seed-phrase-validation",
name: "skip_seed_phrase_validation",
help: "Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list",
};
#[derive(Debug, PartialEq)]
pub enum Source {
File,
Generated,
SeedPhrase,
}
pub struct KeypairWithSource {
pub keypair: Keypair,
pub source: Source,
}
impl KeypairWithSource {
fn new(keypair: Keypair, source: Source) -> Self {
Self { keypair, source }
}
}
/// Reads user input from stdin to retrieve a seed phrase and passphrase for keypair derivation
pub fn keypair_from_seed_phrase(
pub(crate) fn keypair_from_seed_phrase(
keypair_name: &str,
skip_validation: bool,
) -> Result<Keypair, Box<dyn error::Error>> {
@ -33,17 +64,6 @@ pub fn keypair_from_seed_phrase(
}
}
pub struct KeypairWithGenerated {
pub keypair: Keypair,
pub generated: bool,
}
impl KeypairWithGenerated {
fn new(keypair: Keypair, generated: bool) -> Self {
Self { keypair, generated }
}
}
/// Checks CLI arguments to determine whether a keypair should be:
/// - inputted securely via stdin,
/// - read in from a file,
@ -51,33 +71,32 @@ impl KeypairWithGenerated {
pub fn keypair_input(
matches: &clap::ArgMatches,
keypair_name: &str,
) -> Result<KeypairWithGenerated, Box<dyn error::Error>> {
) -> Result<KeypairWithSource, Box<dyn error::Error>> {
let ask_seed_phrase_matches =
values_t!(matches.values_of(ASK_SEED_PHRASE_ARG), String).unwrap_or_default();
values_t!(matches.values_of(ASK_SEED_PHRASE_ARG.name), String).unwrap_or_default();
let keypair_match_name = keypair_name.replace('-', "_");
if ask_seed_phrase_matches
.iter()
.any(|s| s.as_str() == keypair_name)
{
if matches.value_of(keypair_match_name).is_some() {
let ask_seed_phrase_kebab = ASK_SEED_PHRASE_ARG.replace('_', "-");
clap::Error::with_description(
&format!(
"`--{} {}` cannot be used with `{} <PATH>`",
ask_seed_phrase_kebab, keypair_name, keypair_name
ASK_SEED_PHRASE_ARG.long, keypair_name, keypair_name
),
clap::ErrorKind::ArgumentConflict,
)
.exit();
}
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG);
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
keypair_from_seed_phrase(keypair_name, skip_validation)
.map(|keypair| KeypairWithGenerated::new(keypair, false))
.map(|keypair| KeypairWithSource::new(keypair, Source::SeedPhrase))
} else if let Some(keypair_file) = matches.value_of(keypair_match_name) {
read_keypair_file(keypair_file).map(|keypair| KeypairWithGenerated::new(keypair, false))
read_keypair_file(keypair_file).map(|keypair| KeypairWithSource::new(keypair, Source::File))
} else {
Ok(KeypairWithGenerated::new(Keypair::new(), true))
Ok(KeypairWithSource::new(Keypair::new(), Source::Generated))
}
}
@ -89,7 +108,7 @@ mod tests {
#[test]
fn test_keypair_input() {
let arg_matches = ArgMatches::default();
let KeypairWithGenerated { generated, .. } = keypair_input(&arg_matches, "").unwrap();
assert!(generated);
let KeypairWithSource { source, .. } = keypair_input(&arg_matches, "").unwrap();
assert_eq!(source, Source::Generated);
}
}

View File

@ -17,6 +17,12 @@ macro_rules! version {
};
}
pub struct ArgConstant<'a> {
pub long: &'a str,
pub name: &'a str,
pub help: &'a str,
}
pub mod input_parsers;
pub mod input_validators;
pub mod keypair;

View File

@ -219,19 +219,28 @@ pub struct CliConfig {
pub rpc_client: Option<RpcClient>,
}
impl Default for CliConfig {
fn default() -> CliConfig {
impl CliConfig {
pub fn default_keypair_path() -> String {
let mut keypair_path = dirs::home_dir().expect("home directory");
keypair_path.extend(&[".config", "solana", "id.json"]);
keypair_path.to_str().unwrap().to_string()
}
pub fn default_json_rpc_url() -> String {
"http://127.0.0.1:8899".to_string()
}
}
impl Default for CliConfig {
fn default() -> CliConfig {
CliConfig {
command: CliCommand::Balance {
pubkey: Some(Pubkey::default()),
use_lamports_unit: false,
},
json_rpc_url: "http://127.0.0.1:8899".to_string(),
json_rpc_url: Self::default_json_rpc_url(),
keypair: Keypair::new(),
keypair_path: Some(keypair_path.to_str().unwrap().to_string()),
keypair_path: Some(Self::default_keypair_path()),
rpc_client: None,
}
}

View File

@ -16,14 +16,14 @@ lazy_static! {
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct Config {
pub url: String,
pub keypair: String,
pub keypair_path: String,
}
impl Config {
pub fn new(url: &str, keypair: &str) -> Self {
pub fn new(url: &str, keypair_path: &str) -> Self {
Self {
url: url.to_string(),
keypair: keypair.to_string(),
keypair_path: keypair_path.to_string(),
}
}

View File

@ -1,7 +1,13 @@
use clap::{crate_description, crate_name, Arg, ArgGroup, ArgMatches, SubCommand};
use console::style;
use solana_clap_utils::input_validators::is_url;
use solana_clap_utils::{
input_validators::is_url,
keypair::{
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
SKIP_SEED_PHRASE_VALIDATION_ARG,
},
};
use solana_cli::{
cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliError},
config::{self, Config},
@ -15,22 +21,25 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
let parse_args = match matches.subcommand() {
("get", Some(subcommand_matches)) => {
if let Some(config_file) = matches.value_of("config_file") {
let default_cli_config = CliConfig::default();
let config = Config::load(config_file).unwrap_or_default();
if let Some(field) = subcommand_matches.value_of("specific_setting") {
let (value, default_value) = match field {
"url" => (config.url, default_cli_config.json_rpc_url),
"keypair" => (config.keypair, default_cli_config.keypair_path.unwrap()),
"url" => (config.url, CliConfig::default_json_rpc_url()),
"keypair" => (config.keypair_path, CliConfig::default_keypair_path()),
_ => unreachable!(),
};
println_name_value_or(&format!("* {}:", field), &value, &default_value);
} else {
println_name_value("Wallet Config:", config_file);
println_name_value_or("* url:", &config.url, &default_cli_config.json_rpc_url);
println_name_value_or(
"* url:",
&config.url,
&CliConfig::default_json_rpc_url(),
);
println_name_value_or(
"* keypair:",
&config.keypair,
&default_cli_config.keypair_path.unwrap(),
&config.keypair_path,
&CliConfig::default_keypair_path(),
);
}
} else {
@ -48,12 +57,12 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
config.url = url.to_string();
}
if let Some(keypair) = subcommand_matches.value_of("keypair") {
config.keypair = keypair.to_string();
config.keypair_path = keypair.to_string();
}
config.save(config_file)?;
println_name_value("Wallet Config Updated:", config_file);
println_name_value("* url:", &config.url);
println_name_value("* keypair:", &config.keypair);
println_name_value("* keypair:", &config.keypair_path);
} else {
println!(
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
@ -88,28 +97,37 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::
} = parse_command(&matches)?;
let (keypair, keypair_path) = if require_keypair {
let keypair_path = if matches.is_present("keypair") {
matches.value_of("keypair").unwrap().to_string()
} else if config.keypair != "" {
config.keypair
} else {
let default = CliConfig::default();
let maybe_keypair_path = default.keypair_path.unwrap();
if !std::path::Path::new(&maybe_keypair_path).exists() {
return Err(CliError::KeypairFileNotFound(
"Generate a new keypair with `solana-keygen new`".to_string(),
)
.into());
let KeypairWithSource { keypair, source } = keypair_input(&matches, "keypair")?;
match source {
keypair::Source::File => (
keypair,
Some(matches.value_of("keypair").unwrap().to_string()),
),
keypair::Source::SeedPhrase => (keypair, None),
keypair::Source::Generated => {
let keypair_path = if config.keypair_path != "" {
config.keypair_path
} else {
let default_keypair_path = CliConfig::default_keypair_path();
if !std::path::Path::new(&default_keypair_path).exists() {
return Err(CliError::KeypairFileNotFound(
"Generate a new keypair with `solana-keygen new`".to_string(),
)
.into());
}
default_keypair_path
};
let keypair = read_keypair_file(&keypair_path).or_else(|err| {
Err(CliError::BadParameter(format!(
"{}: Unable to open keypair file: {}",
err, keypair_path
)))
})?;
(keypair, Some(keypair_path))
}
maybe_keypair_path
};
let keypair = read_keypair_file(&keypair_path).or_else(|err| {
Err(CliError::BadParameter(format!(
"{}: Unable to open keypair file: {}",
err, keypair_path
)))
})?;
(keypair, Some(keypair_path.to_string()))
}
} else {
let default = CliConfig::default();
(default.keypair, None)
@ -164,6 +182,21 @@ fn main() -> Result<(), Box<dyn error::Error>> {
.takes_value(true)
.help("/path/to/id.json"),
)
.arg(
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
.long(ASK_SEED_PHRASE_ARG.long)
.value_name("KEYPAIR NAME")
.global(true)
.takes_value(true)
.possible_values(&["keypair"])
.help(ASK_SEED_PHRASE_ARG.help),
)
.arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
.global(true)
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
)
.subcommand(
SubCommand::with_name("get")
.about("Get cli config settings")

View File

@ -41,7 +41,7 @@ impl StakeSubCommands for App<'_, '_> {
.value_name("STAKE ACCOUNT")
.takes_value(true)
.required(true)
.validator(is_keypair)
.validator(is_keypair_or_ask_keyword)
.help("Keypair of the stake account to fund")
)
.arg(

View File

@ -35,7 +35,7 @@ impl StorageSubCommands for App<'_, '_> {
.value_name("STORAGE ACCOUNT")
.takes_value(true)
.required(true)
.validator(is_keypair),
.validator(is_keypair_or_ask_keyword),
),
)
.subcommand(
@ -55,7 +55,7 @@ impl StorageSubCommands for App<'_, '_> {
.value_name("STORAGE ACCOUNT")
.takes_value(true)
.required(true)
.validator(is_keypair),
.validator(is_keypair_or_ask_keyword),
),
)
.subcommand(

View File

@ -30,7 +30,7 @@ impl VoteSubCommands for App<'_, '_> {
.value_name("VOTE ACCOUNT KEYPAIR")
.takes_value(true)
.required(true)
.validator(is_keypair)
.validator(is_keypair_or_ask_keyword)
.help("Vote account keypair to fund"),
)
.arg(

View File

@ -18,7 +18,7 @@ while [[ -n $1 ]]; do
entrypoint=$2
args+=("$1" "$2")
shift 2
elif [[ $1 = --identity ]]; then
elif [[ $1 = --identity-keypair ]]; then
identity_keypair=$2
[[ -r $identity_keypair ]] || {
echo "$identity_keypair does not exist"
@ -74,7 +74,7 @@ if [[ ! -r $storage_keypair ]]; then
fi
default_arg --entrypoint "$entrypoint"
default_arg --identity "$identity_keypair"
default_arg --identity-keypair "$identity_keypair"
default_arg --storage-keypair "$storage_keypair"
default_arg --ledger "$ledger"

View File

@ -415,7 +415,7 @@ EOF
)
if [[ $airdropsEnabled != true ]]; then
# If this ever becomes a problem, we need to provide the `--identity`
# If this ever becomes a problem, we need to provide the `--identity-keypair`
# argument to an existing system account with lamports in it
echo "Error: archivers not supported without airdrops"
exit 1

View File

@ -7,7 +7,8 @@ use solana_clap_utils::{
input_parsers::pubkey_of,
input_validators::{is_keypair, is_pubkey_or_keypair},
keypair::{
keypair_input, KeypairWithGenerated, ASK_SEED_PHRASE_ARG, SKIP_SEED_PHRASE_VALIDATION_ARG,
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
SKIP_SEED_PHRASE_VALIDATION_ARG,
},
};
use solana_client::rpc_client::RpcClient;
@ -326,19 +327,19 @@ pub fn main() {
.help("Stream entries to this unix domain socket path")
)
.arg(
Arg::with_name(ASK_SEED_PHRASE_ARG)
.long("ask-seed-phrase")
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
.long(ASK_SEED_PHRASE_ARG.long)
.value_name("KEYPAIR NAME")
.multiple(true)
.takes_value(true)
.possible_values(&["identity-keypair", "storage-keypair", "voting-keypair"])
.help("Securely recover a keypair using a seed phrase and optional passphrase"),
.help(ASK_SEED_PHRASE_ARG.help),
)
.arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG)
.long("skip-seed-phrase-validation")
.requires(ASK_SEED_PHRASE_ARG)
.help("Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list"),
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
.requires(ASK_SEED_PHRASE_ARG.name)
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
)
.arg(
Arg::with_name("identity_keypair")
@ -546,13 +547,14 @@ pub fn main() {
})
.keypair,
);
let KeypairWithGenerated {
let KeypairWithSource {
keypair: voting_keypair,
generated: ephemeral_voting_keypair,
source: voting_keypair_source,
} = keypair_input(&matches, "voting-keypair").unwrap_or_else(|err| {
eprintln!("Voting keypair input failed: {}", err);
exit(1);
});
let ephemeral_voting_keypair = voting_keypair_source == keypair::Source::Generated;
let storage_keypair = keypair_input(&matches, "storage-keypair")
.unwrap_or_else(|err| {
eprintln!("Storage keypair input failed: {}", err);