lighthouse/common/validator_dir/tests/tests.rs
Kirk Baird 3db9072fee Reject invalid utf-8 characters during encryption (#1928)
## Issue Addressed

Closes #1889 

## Proposed Changes

- Error when passwords which use invalid UTF-8 characters during encryption. 
- Add some tests

## Additional Info

I've decided to error when bad characters are used to create/encrypt a keystore but think we should allow them during decryption since either the keystore was created
-  with invalid UTF-8 characters (possibly by another client or someone whose password is random bytes) in which case we'd want them to be able to decrypt their keystore using the right key.
-  without invalid characters then the password checksum would almost certainly fail.

Happy to add them to decryption if we want to make the decryption more trigger happy 😋 , it would only be a one line change and would tell the user which character index is causing the issue.

See https://eips.ethereum.org/EIPS/eip-2335#password-requirements
2020-11-19 00:37:43 +00:00

378 lines
11 KiB
Rust

#![cfg(not(debug_assertions))]
use eth2_keystore::{Keystore, KeystoreBuilder, PlainText};
use std::fs::{self, File};
use std::path::Path;
use tempfile::{tempdir, TempDir};
use types::{test_utils::generate_deterministic_keypair, EthSpec, Keypair, MainnetEthSpec};
use validator_dir::{
Builder, BuilderError, ValidatorDir, ETH1_DEPOSIT_DATA_FILE, ETH1_DEPOSIT_TX_HASH_FILE,
VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE,
};
/// A very weak password with which to encrypt the keystores.
pub const INSECURE_PASSWORD: &[u8] = &[50; 51];
/// Helper struct for configuring tests.
struct BuildConfig {
random_voting_keystore: bool,
random_withdrawal_keystore: bool,
deposit_amount: Option<u64>,
store_withdrawal_keystore: bool,
}
impl Default for BuildConfig {
fn default() -> Self {
Self {
random_voting_keystore: true,
random_withdrawal_keystore: true,
deposit_amount: None,
store_withdrawal_keystore: true,
}
}
}
/// Check that a keystore exists and can be decrypted with a password in password_dir
fn check_keystore<P: AsRef<Path>>(path: P, password_dir: P) -> Keypair {
let mut file = File::open(path).unwrap();
let keystore = Keystore::from_json_reader(&mut file).unwrap();
let pubkey = keystore.pubkey();
let password_path = password_dir.as_ref().join(format!("0x{}", pubkey));
let password = fs::read(password_path).unwrap();
keystore.decrypt_keypair(&password).unwrap()
}
/// Creates a keystore using `generate_deterministic_keypair`.
pub fn generate_deterministic_keystore(i: usize) -> Result<(Keystore, PlainText), String> {
let keypair = generate_deterministic_keypair(i);
let keystore = KeystoreBuilder::new(&keypair, INSECURE_PASSWORD, "".into())
.map_err(|e| format!("Unable to create keystore builder: {:?}", e))?
.build()
.map_err(|e| format!("Unable to build keystore: {:?}", e))?;
Ok((keystore, INSECURE_PASSWORD.to_vec().into()))
}
/// A testing harness for generating validator directories.
struct Harness {
validators_dir: TempDir,
password_dir: TempDir,
}
impl Harness {
/// Create a new harness using temporary directories.
pub fn new() -> Self {
Self {
validators_dir: tempdir().unwrap(),
password_dir: tempdir().unwrap(),
}
}
/// Create a `ValidatorDir` from the `config`, then assert that the `ValidatorDir` was generated
/// correctly with respect to the `config`.
pub fn create_and_test(&self, config: &BuildConfig) -> ValidatorDir {
let spec = MainnetEthSpec::default_spec();
/*
* Build the `ValidatorDir`.
*/
let builder = Builder::new(self.validators_dir.path().into())
.password_dir(self.password_dir.path())
// Note: setting the withdrawal keystore here ensure that it can get replaced by
// further calls to `random_withdrawal_keystore`.
.store_withdrawal_keystore(config.store_withdrawal_keystore);
let builder = if config.random_voting_keystore {
builder.random_voting_keystore().unwrap()
} else {
let (keystore, password) = generate_deterministic_keystore(0).unwrap();
builder.voting_keystore(keystore, password.as_bytes())
};
let builder = if config.random_withdrawal_keystore {
builder.random_withdrawal_keystore().unwrap()
} else {
let (keystore, password) = generate_deterministic_keystore(1).unwrap();
builder.withdrawal_keystore(keystore, password.as_bytes())
};
let builder = if let Some(amount) = config.deposit_amount {
builder.create_eth1_tx_data(amount, &spec)
} else {
builder
};
let mut validator = builder.build().unwrap();
/*
* Assert that the dir is consistent with the config.
*/
let withdrawal_keystore_path = validator.dir().join(WITHDRAWAL_KEYSTORE_FILE);
let password_dir = self.password_dir.path().into();
// Ensure the voting keypair exists and can be decrypted.
let voting_keypair =
check_keystore(&validator.dir().join(VOTING_KEYSTORE_FILE), &password_dir);
if !config.random_voting_keystore {
assert_eq!(voting_keypair.pk, generate_deterministic_keypair(0).pk)
}
// Use OR here instead of AND so we *always* check for the withdrawal keystores if random
// keystores were generated.
if config.random_withdrawal_keystore || config.store_withdrawal_keystore {
// Ensure the withdrawal keypair exists and can be decrypted.
let withdrawal_keypair = check_keystore(&withdrawal_keystore_path, &password_dir);
if !config.random_withdrawal_keystore {
assert_eq!(withdrawal_keypair.pk, generate_deterministic_keypair(1).pk)
}
// The withdrawal keys should be distinct from the voting keypairs.
assert_ne!(withdrawal_keypair.pk, voting_keypair.pk);
}
if !config.store_withdrawal_keystore && !config.random_withdrawal_keystore {
assert!(!withdrawal_keystore_path.exists())
}
if let Some(amount) = config.deposit_amount {
// Check that the deposit data can be decoded.
let data = validator.eth1_deposit_data().unwrap().unwrap();
// Ensure the amount is consistent.
assert_eq!(data.deposit_data.amount, amount);
// Check that the eth1 deposit data file is a valid format.
let hex =
String::from_utf8(fs::read(validator.dir().join(ETH1_DEPOSIT_DATA_FILE)).unwrap())
.unwrap();
assert!(hex.starts_with("0x"), "deposit data should have 0x prefix");
hex::decode(&hex[2..]).unwrap();
} else {
// If there was no deposit then we should return `Ok(None)`.
assert!(validator.eth1_deposit_data().unwrap().is_none());
}
let tx_hash_path = validator.dir().join(ETH1_DEPOSIT_TX_HASH_FILE);
// The eth1 deposit file should not exist, yet.
assert!(!tx_hash_path.exists());
let tx = "junk data";
// Save a tx hash.
validator.save_eth1_deposit_tx_hash(tx).unwrap();
// Ensure the saved tx hash is correct.
assert_eq!(fs::read(tx_hash_path).unwrap(), tx.as_bytes().to_vec());
// Saving a second tx hash should fail.
validator.save_eth1_deposit_tx_hash(tx).unwrap_err();
validator
}
}
#[test]
fn concurrency() {
let harness = Harness::new();
let val_dir = harness.create_and_test(&BuildConfig::default());
let path = val_dir.dir().clone();
// Should not re-open whilst opened after build.
ValidatorDir::open(&path).unwrap_err();
drop(val_dir);
// Should re-open after drop.
let val_dir = ValidatorDir::open(&path).unwrap();
// Should not re-open when opened via ValidatorDir.
ValidatorDir::open(&path).unwrap_err();
drop(val_dir);
// Should re-open again.
ValidatorDir::open(&path).unwrap();
}
#[test]
fn without_voting_keystore() {
let harness = Harness::new();
assert!(matches!(
Builder::new(harness.validators_dir.path().into(),)
.password_dir(harness.password_dir.path())
.random_withdrawal_keystore()
.unwrap()
.build(),
Err(BuilderError::UninitializedVotingKeystore)
))
}
#[test]
fn without_withdrawal_keystore() {
let harness = Harness::new();
let spec = &MainnetEthSpec::default_spec();
// Should build without withdrawal keystore if not storing the it or creating eth1 data.
Builder::new(harness.validators_dir.path().into())
.password_dir(harness.password_dir.path())
.random_voting_keystore()
.unwrap()
.store_withdrawal_keystore(false)
.build()
.unwrap();
assert!(
matches!(
Builder::new(harness.validators_dir.path().into(),)
.password_dir(harness.password_dir.path())
.random_voting_keystore()
.unwrap()
.store_withdrawal_keystore(true)
.build(),
Err(BuilderError::UninitializedWithdrawalKeystore)
),
"storing the keystore requires keystore"
);
assert!(
matches!(
Builder::new(harness.validators_dir.path().into(),)
.password_dir(harness.password_dir.path())
.random_voting_keystore()
.unwrap()
.create_eth1_tx_data(42, spec)
.build(),
Err(BuilderError::UninitializedWithdrawalKeystore)
),
"storing the keystore requires keystore"
);
}
#[test]
fn deterministic_voting_keystore() {
let harness = Harness::new();
let config = BuildConfig {
random_voting_keystore: false,
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn deterministic_withdrawal_keystore_without_saving() {
let harness = Harness::new();
let config = BuildConfig {
random_withdrawal_keystore: false,
store_withdrawal_keystore: false,
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn deterministic_withdrawal_keystore_with_saving() {
let harness = Harness::new();
let config = BuildConfig {
random_withdrawal_keystore: false,
store_withdrawal_keystore: true,
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn both_keystores_deterministic_without_saving() {
let harness = Harness::new();
let config = BuildConfig {
random_voting_keystore: false,
random_withdrawal_keystore: false,
store_withdrawal_keystore: false,
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn both_keystores_random_without_saving() {
let harness = Harness::new();
let config = BuildConfig {
random_voting_keystore: true,
random_withdrawal_keystore: true,
store_withdrawal_keystore: false,
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn both_keystores_deterministic_with_saving() {
let harness = Harness::new();
let config = BuildConfig {
random_voting_keystore: false,
random_withdrawal_keystore: false,
store_withdrawal_keystore: true,
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn eth1_data() {
let harness = Harness::new();
let config = BuildConfig {
deposit_amount: Some(123456),
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn store_withdrawal_keystore_without_eth1_data() {
let harness = Harness::new();
let config = BuildConfig {
store_withdrawal_keystore: false,
random_withdrawal_keystore: true,
deposit_amount: None,
..BuildConfig::default()
};
harness.create_and_test(&config);
}
#[test]
fn store_withdrawal_keystore_with_eth1_data() {
let harness = Harness::new();
let config = BuildConfig {
store_withdrawal_keystore: false,
random_withdrawal_keystore: true,
deposit_amount: Some(32000000000),
..BuildConfig::default()
};
harness.create_and_test(&config);
}