Allow VC to create password files via HTTP API

This commit is contained in:
Paul Hauner
2022-08-29 15:30:02 +10:00
parent be8463770f
commit 2abde0b666
15 changed files with 189 additions and 47 deletions

View File

@@ -200,6 +200,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.required(false)
.takes_value(false),
)
.arg(
Arg::with_name("http-store-passwords-in-secrets-dir")
.long("http-store-passwords-in-secrets-dir")
.value_name("ORIGIN")
.help("If present, any validators created via the HTTP will have keystore \
passwords stored in the secrets-dir rather than the validator \
definitions file.")
.required(false)
.takes_value(false),
)
/* Prometheus metrics HTTP server related arguments */
.arg(
Arg::with_name("metrics")

View File

@@ -259,6 +259,10 @@ impl Config {
config.http_api.allow_keystore_export = true;
}
if cli_args.is_present("http-store-passwords-in-secrets-dir") {
config.http_api.store_passwords_in_secrets_dir = true;
}
/*
* Prometheus metrics HTTP server
*/

View File

@@ -1,15 +1,15 @@
use crate::ValidatorStore;
use account_utils::validator_definitions::ValidatorDefinition;
use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition};
use account_utils::{
eth2_wallet::{bip39::Mnemonic, WalletBuilder},
random_mnemonic, random_password, ZeroizeString,
};
use eth2::lighthouse_vc::types::{self as api_types};
use slot_clock::SlotClock;
use std::path::Path;
use std::path::{Path, PathBuf};
use types::ChainSpec;
use types::EthSpec;
use validator_dir::Builder as ValidatorDirBuilder;
use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder};
/// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in
/// this validator client.
@@ -27,6 +27,7 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock,
key_derivation_path_offset: Option<u32>,
validator_requests: &[api_types::ValidatorRequest],
validator_dir: P,
secrets_dir: Option<PathBuf>,
validator_store: &ValidatorStore<T, E>,
spec: &ChainSpec,
) -> Result<(Vec<api_types::CreatedValidator>, Mnemonic), warp::Rejection> {
@@ -95,7 +96,20 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock,
))
})?;
let voting_password_storage = if let Some(secrets_dir) = &secrets_dir {
let password_path = keystore_password_path(secrets_dir, &keystores.voting);
if password_path.exists() {
return Err(warp_utils::reject::custom_server_error(
"Duplicate keystore password path".to_string(),
));
}
PasswordStorage::File(password_path)
} else {
PasswordStorage::ValidatorDefinitions(voting_password_string.clone())
};
let validator_dir = ValidatorDirBuilder::new(validator_dir.as_ref().into())
.password_dir_opt(secrets_dir.clone())
.voting_keystore(keystores.voting, voting_password.as_bytes())
.withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes())
.create_eth1_tx_data(request.deposit_gwei, spec)
@@ -136,7 +150,7 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock,
validator_store
.add_validator_keystore(
voting_keystore_path,
voting_password_string,
voting_password_storage,
request.enable,
request.graffiti.clone(),
request.suggested_fee_recipient,

View File

@@ -3,7 +3,7 @@ use crate::{
initialized_validators::Error, signing_method::SigningMethod, InitializedValidators,
ValidatorStore,
};
use account_utils::ZeroizeString;
use account_utils::{validator_definitions::PasswordStorage, ZeroizeString};
use eth2::lighthouse_vc::{
std_types::{
DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse,
@@ -20,7 +20,7 @@ use std::sync::Arc;
use task_executor::TaskExecutor;
use tokio::runtime::Handle;
use types::{EthSpec, PublicKeyBytes};
use validator_dir::Builder as ValidatorDirBuilder;
use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder};
use warp::Rejection;
use warp_utils::reject::{custom_bad_request, custom_server_error};
@@ -61,6 +61,7 @@ pub fn list<T: SlotClock + 'static, E: EthSpec>(
pub fn import<T: SlotClock + 'static, E: EthSpec>(
request: ImportKeystoresRequest,
validator_dir: PathBuf,
secrets_dir: Option<PathBuf>,
validator_store: Arc<ValidatorStore<T, E>>,
task_executor: TaskExecutor,
log: Logger,
@@ -131,6 +132,7 @@ pub fn import<T: SlotClock + 'static, E: EthSpec>(
keystore,
password,
validator_dir.clone(),
secrets_dir.clone(),
&validator_store,
handle,
) {
@@ -161,6 +163,7 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
keystore: Keystore,
password: ZeroizeString,
validator_dir_path: PathBuf,
secrets_dir: Option<PathBuf>,
validator_store: &ValidatorStore<T, E>,
handle: Handle,
) -> Result<ImportKeystoreStatus, String> {
@@ -182,6 +185,16 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
}
}
let password_storage = if let Some(secrets_dir) = &secrets_dir {
let password_path = keystore_password_path(secrets_dir, &keystore);
if password_path.exists() {
return Ok(ImportKeystoreStatus::Duplicate);
}
PasswordStorage::File(password_path)
} else {
PasswordStorage::ValidatorDefinitions(password.clone())
};
// Check that the password is correct.
// In future we should re-structure to avoid the double decryption here. It's not as simple
// as removing this check because `add_validator_keystore` will break if provided with an
@@ -192,6 +205,7 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
.map_err(|e| format!("incorrect password: {:?}", e))?;
let validator_dir = ValidatorDirBuilder::new(validator_dir_path)
.password_dir_opt(secrets_dir)
.voting_keystore(keystore, password.as_ref())
.store_withdrawal_keystore(false)
.build()
@@ -204,7 +218,7 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
handle
.block_on(validator_store.add_validator_keystore(
voting_keystore_path,
password,
password_storage,
true,
None,
None,

View File

@@ -9,7 +9,9 @@ pub mod test_utils;
use crate::ValidatorStore;
use account_utils::{
mnemonic_from_phrase,
validator_definitions::{SigningDefinition, ValidatorDefinition, Web3SignerDefinition},
validator_definitions::{
PasswordStorage, SigningDefinition, ValidatorDefinition, Web3SignerDefinition,
},
};
pub use api_secret::ApiSecret;
use create_validator::{create_validators_mnemonic, create_validators_web3signer};
@@ -28,7 +30,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use task_executor::TaskExecutor;
use types::{ChainSpec, ConfigAndPreset, EthSpec};
use validator_dir::Builder as ValidatorDirBuilder;
use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder};
use warp::{
http::{
header::{HeaderValue, CONTENT_TYPE},
@@ -64,6 +66,7 @@ pub struct Context<T: SlotClock, E: EthSpec> {
pub api_secret: ApiSecret,
pub validator_store: Option<Arc<ValidatorStore<T, E>>>,
pub validator_dir: Option<PathBuf>,
pub secrets_dir: Option<PathBuf>,
pub spec: ChainSpec,
pub config: Config,
pub log: Logger,
@@ -78,6 +81,7 @@ pub struct Config {
pub listen_port: u16,
pub allow_origin: Option<String>,
pub allow_keystore_export: bool,
pub store_passwords_in_secrets_dir: bool,
}
impl Default for Config {
@@ -88,6 +92,7 @@ impl Default for Config {
listen_port: 5062,
allow_origin: None,
allow_keystore_export: false,
store_passwords_in_secrets_dir: false,
}
}
}
@@ -113,6 +118,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
) -> Result<(SocketAddr, impl Future<Output = ()>), Error> {
let config = &ctx.config;
let allow_keystore_export = config.allow_keystore_export;
let store_passwords_in_secrets_dir = config.store_passwords_in_secrets_dir;
let log = ctx.log.clone();
// Configure CORS.
@@ -179,6 +185,17 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
})
});
let inner_secrets_dir = ctx.secrets_dir.clone();
let secrets_dir_filter = warp::any().map(move || inner_secrets_dir.clone()).and_then(
|secrets_dir: Option<_>| async move {
secrets_dir.ok_or_else(|| {
warp_utils::reject::custom_not_found(
"secrets_dir directory is not initialized.".to_string(),
)
})
},
);
let inner_ctx = ctx.clone();
let log_filter = warp::any().map(move || inner_ctx.log.clone());
@@ -290,18 +307,21 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(warp::path::end())
.and(warp::body::json())
.and(validator_dir_filter.clone())
.and(secrets_dir_filter.clone())
.and(validator_store_filter.clone())
.and(spec_filter.clone())
.and(signer.clone())
.and(task_executor_filter.clone())
.and_then(
|body: Vec<api_types::ValidatorRequest>,
validator_dir: PathBuf,
validator_store: Arc<ValidatorStore<T, E>>,
spec: Arc<ChainSpec>,
signer,
task_executor: TaskExecutor| {
move |body: Vec<api_types::ValidatorRequest>,
validator_dir: PathBuf,
secrets_dir: PathBuf,
validator_store: Arc<ValidatorStore<T, E>>,
spec: Arc<ChainSpec>,
signer,
task_executor: TaskExecutor| {
blocking_signed_json_task(signer, move || {
let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir);
if let Some(handle) = task_executor.handle() {
let (validators, mnemonic) =
handle.block_on(create_validators_mnemonic(
@@ -309,6 +329,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
None,
&body,
&validator_dir,
secrets_dir,
&validator_store,
&spec,
))?;
@@ -333,18 +354,21 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(warp::path::end())
.and(warp::body::json())
.and(validator_dir_filter.clone())
.and(secrets_dir_filter.clone())
.and(validator_store_filter.clone())
.and(spec_filter)
.and(signer.clone())
.and(task_executor_filter.clone())
.and_then(
|body: api_types::CreateValidatorsMnemonicRequest,
validator_dir: PathBuf,
validator_store: Arc<ValidatorStore<T, E>>,
spec: Arc<ChainSpec>,
signer,
task_executor: TaskExecutor| {
move |body: api_types::CreateValidatorsMnemonicRequest,
validator_dir: PathBuf,
secrets_dir: PathBuf,
validator_store: Arc<ValidatorStore<T, E>>,
spec: Arc<ChainSpec>,
signer,
task_executor: TaskExecutor| {
blocking_signed_json_task(signer, move || {
let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir);
if let Some(handle) = task_executor.handle() {
let mnemonic =
mnemonic_from_phrase(body.mnemonic.as_str()).map_err(|e| {
@@ -359,6 +383,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
Some(body.key_derivation_path_offset),
&body.validators,
&validator_dir,
secrets_dir,
&validator_store,
&spec,
))?;
@@ -379,15 +404,17 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(warp::path::end())
.and(warp::body::json())
.and(validator_dir_filter.clone())
.and(secrets_dir_filter.clone())
.and(validator_store_filter.clone())
.and(signer.clone())
.and(task_executor_filter.clone())
.and_then(
|body: api_types::KeystoreValidatorsPostRequest,
validator_dir: PathBuf,
validator_store: Arc<ValidatorStore<T, E>>,
signer,
task_executor: TaskExecutor| {
move |body: api_types::KeystoreValidatorsPostRequest,
validator_dir: PathBuf,
secrets_dir: PathBuf,
validator_store: Arc<ValidatorStore<T, E>>,
signer,
task_executor: TaskExecutor| {
blocking_signed_json_task(signer, move || {
// Check to ensure the password is correct.
let keypair = body
@@ -400,7 +427,21 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
))
})?;
let secrets_dir = store_passwords_in_secrets_dir.then(|| secrets_dir);
let password_storage = if let Some(secrets_dir) = &secrets_dir {
let password_path = keystore_password_path(secrets_dir, &body.keystore);
if password_path.exists() {
return Err(warp_utils::reject::custom_server_error(
"Duplicate keystore password path".to_string(),
));
}
PasswordStorage::File(password_path)
} else {
PasswordStorage::ValidatorDefinitions(body.password.clone())
};
let validator_dir = ValidatorDirBuilder::new(validator_dir.clone())
.password_dir_opt(secrets_dir)
.voting_keystore(body.keystore.clone(), body.password.as_ref())
.store_withdrawal_keystore(false)
.build()
@@ -414,7 +455,6 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
// Drop validator dir so that `add_validator_keystore` can re-lock the keystore.
let voting_keystore_path = validator_dir.voting_keystore_path();
drop(validator_dir);
let voting_password = body.password.clone();
let graffiti = body.graffiti.clone();
let suggested_fee_recipient = body.suggested_fee_recipient;
let gas_limit = body.gas_limit;
@@ -425,7 +465,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
handle
.block_on(validator_store.add_validator_keystore(
voting_keystore_path,
voting_password,
password_storage,
body.enable,
graffiti,
suggested_fee_recipient,
@@ -850,13 +890,28 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
.and(warp::body::json())
.and(signer.clone())
.and(validator_dir_filter)
.and(secrets_dir_filter)
.and(validator_store_filter.clone())
.and(task_executor_filter.clone())
.and(log_filter.clone())
.and_then(
|request, signer, validator_dir, validator_store, task_executor, log| {
move |request,
signer,
validator_dir,
secrets_dir,
validator_store,
task_executor,
log| {
let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir);
blocking_signed_json_task(signer, move || {
keystores::import(request, validator_dir, validator_store, task_executor, log)
keystores::import(
request,
validator_dir,
secrets_dir,
validator_store,
task_executor,
log,
)
})
},
);

View File

@@ -121,6 +121,7 @@ impl ApiTester {
task_executor: test_runtime.task_executor.clone(),
api_secret,
validator_dir: Some(validator_dir.path().into()),
secrets_dir: Some(secrets_dir.path().into()),
validator_store: Some(validator_store.clone()),
spec: E::default_spec(),
config: HttpConfig {
@@ -129,6 +130,7 @@ impl ApiTester {
listen_port: 0,
allow_origin: None,
allow_keystore_export: true,
store_passwords_in_secrets_dir: false,
},
log,
_phantom: PhantomData,

View File

@@ -520,6 +520,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
api_secret,
validator_store: Some(self.validator_store.clone()),
validator_dir: Some(self.config.validator_dir.clone()),
secrets_dir: Some(self.config.secrets_dir.clone()),
spec: self.context.eth2_config.spec.clone(),
config: self.config.http_api.clone(),
log: log.clone(),

View File

@@ -5,7 +5,7 @@ use crate::{
signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod},
Config,
};
use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString};
use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition};
use parking_lot::{Mutex, RwLock};
use slashing_protection::{
interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase,
@@ -161,7 +161,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
pub async fn add_validator_keystore<P: AsRef<Path>>(
&self,
voting_keystore_path: P,
password: ZeroizeString,
password_storage: PasswordStorage,
enable: bool,
graffiti: Option<GraffitiString>,
suggested_fee_recipient: Option<Address>,
@@ -170,7 +170,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
) -> Result<ValidatorDefinition, String> {
let mut validator_def = ValidatorDefinition::new_keystore_with_password(
voting_keystore_path,
Some(password),
password_storage,
graffiti.map(Into::into),
suggested_fee_recipient,
gas_limit,