Merge branch 'unstable' of https://github.com/sigp/lighthouse into single_attestation

This commit is contained in:
Eitan Seri-Levi
2024-12-13 21:51:49 +07:00
35 changed files with 168 additions and 227 deletions

View File

@@ -40,7 +40,7 @@ jobs:
run: |
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli=1.3.1
sudo apt install -y kurtosis-cli
kurtosis analytics disable
- name: Download Docker image artifact
@@ -86,7 +86,7 @@ jobs:
run: |
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli=1.3.1
sudo apt install -y kurtosis-cli
kurtosis analytics disable
- name: Download Docker image artifact
@@ -121,7 +121,7 @@ jobs:
run: |
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli=1.3.1
sudo apt install -y kurtosis-cli
kurtosis analytics disable
- name: Download Docker image artifact

View File

@@ -83,6 +83,11 @@ jobs:
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
# Set Java version to 21. (required since Web3Signer 24.12.0).
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1

11
Cargo.lock generated
View File

@@ -36,6 +36,7 @@ dependencies = [
"tokio",
"types",
"validator_dir",
"zeroize",
]
[[package]]
@@ -2561,8 +2562,6 @@ dependencies = [
name = "eth2"
version = "0.1.0"
dependencies = [
"account_utils",
"bytes",
"derivative",
"eth2_keystore",
"ethereum_serde_utils",
@@ -2570,7 +2569,6 @@ dependencies = [
"ethereum_ssz_derive",
"futures",
"futures-util",
"libsecp256k1",
"lighthouse_network",
"mediatype",
"pretty_reqwest_error",
@@ -2578,7 +2576,6 @@ dependencies = [
"proto_array",
"psutil",
"reqwest",
"ring 0.16.20",
"sensitive_url",
"serde",
"serde_json",
@@ -2587,6 +2584,7 @@ dependencies = [
"store",
"tokio",
"types",
"zeroize",
]
[[package]]
@@ -4433,6 +4431,7 @@ dependencies = [
"url",
"validator_dir",
"validator_metrics",
"zeroize",
]
[[package]]
@@ -5287,6 +5286,7 @@ dependencies = [
"validator_client",
"validator_dir",
"validator_manager",
"zeroize",
]
[[package]]
@@ -9584,6 +9584,7 @@ dependencies = [
"validator_store",
"warp",
"warp_utils",
"zeroize",
]
[[package]]
@@ -9627,6 +9628,7 @@ dependencies = [
"tree_hash",
"types",
"validator_http_api",
"zeroize",
]
[[package]]
@@ -10562,6 +10564,7 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"serde",
"zeroize_derive",
]

View File

@@ -201,7 +201,7 @@ tree_hash_derive = "0.8"
url = "2"
uuid = { version = "0.8", features = ["serde", "v4"] }
warp = { version = "0.3.7", default-features = false, features = ["tls"] }
zeroize = { version = "1", features = ["zeroize_derive"] }
zeroize = { version = "1", features = ["zeroize_derive", "serde"] }
zip = "0.6"
# Local crates.

View File

@@ -240,7 +240,7 @@ install-audit:
cargo install --force cargo-audit
audit-CI:
cargo audit
cargo audit --ignore RUSTSEC-2024-0421
# Runs `cargo vendor` to make sure dependencies can be vendored for packaging, reproducibility and archival purpose.
vendor:

View File

@@ -27,6 +27,7 @@ safe_arith = { workspace = true }
slot_clock = { workspace = true }
filesystem = { workspace = true }
sensitive_url = { workspace = true }
zeroize = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@@ -294,7 +294,7 @@ pub fn read_wallet_password_from_cli(
eprintln!();
eprintln!("{}", WALLET_PASSWORD_PROMPT);
let password =
PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec());
PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec());
Ok(password)
}
}

View File

@@ -7,7 +7,7 @@ use account_utils::{
recursively_find_voting_keystores, PasswordStorage, ValidatorDefinition,
ValidatorDefinitions, CONFIG_FILENAME,
},
ZeroizeString, STDIN_INPUTS_FLAG,
STDIN_INPUTS_FLAG,
};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::FLAG_HEADER;
@@ -16,6 +16,7 @@ use std::fs;
use std::path::PathBuf;
use std::thread::sleep;
use std::time::Duration;
use zeroize::Zeroizing;
pub const CMD: &str = "import";
pub const KEYSTORE_FLAG: &str = "keystore";
@@ -148,7 +149,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
// Skip keystores that already exist, but exit early if any operation fails.
// Reuses the same password for all keystores if the `REUSE_PASSWORD_FLAG` flag is set.
let mut num_imported_keystores = 0;
let mut previous_password: Option<ZeroizeString> = None;
let mut previous_password: Option<Zeroizing<String>> = None;
for src_keystore in &keystore_paths {
let keystore = Keystore::from_json_file(src_keystore)
@@ -182,14 +183,17 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
let password = match keystore_password_path.as_ref() {
Some(path) => {
let password_from_file: ZeroizeString = fs::read_to_string(path)
let password_from_file: Zeroizing<String> = fs::read_to_string(path)
.map_err(|e| format!("Unable to read {:?}: {:?}", path, e))?
.into();
password_from_file.without_newlines()
password_from_file
.trim_end_matches(['\r', '\n'])
.to_string()
.into()
}
None => {
let password_from_user = read_password_from_user(stdin_inputs)?;
if password_from_user.as_ref().is_empty() {
if password_from_user.is_empty() {
eprintln!("Continuing without password.");
sleep(Duration::from_secs(1)); // Provides nicer UX.
break None;
@@ -314,7 +318,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
/// Otherwise, returns the keystore error.
fn check_password_on_keystore(
keystore: &Keystore,
password: &ZeroizeString,
password: &Zeroizing<String>,
) -> Result<bool, String> {
match keystore.decrypt_keypair(password.as_ref()) {
Ok(_) => {

View File

@@ -226,14 +226,14 @@ pub fn read_new_wallet_password_from_cli(
eprintln!();
eprintln!("{}", NEW_WALLET_PASSWORD_PROMPT);
let password =
PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec());
PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec());
// Ensure the password meets the minimum requirements.
match is_password_sufficiently_complex(password.as_bytes()) {
Ok(_) => {
eprintln!("{}", RETYPE_PASSWORD_PROMPT);
let retyped_password =
PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec());
PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec());
if retyped_password == password {
break Ok(password);
} else {

View File

@@ -213,6 +213,7 @@ impl<T: BeaconChainTypes> SubnetService<T> {
#[cfg(test)]
pub(crate) fn is_subscribed(&self, subnet: &Subnet) -> bool {
self.subscriptions.contains_key(subnet)
|| self.permanent_attestation_subscriptions.contains(subnet)
}
/// Processes a list of validator subscriptions.

View File

@@ -225,7 +225,7 @@ mod test {
let mut committee_count = 1;
let mut subnet = Subnet::Attestation(
SubnetId::compute_subnet::<MainnetEthSpec>(
current_slot,
subscription_slot,
committee_index,
committee_count,
&subnet_service.beacon_chain.spec,
@@ -250,7 +250,7 @@ mod test {
let subscriptions = vec![get_subscription(
committee_index,
current_slot,
subscription_slot,
committee_count,
true,
)];
@@ -556,7 +556,8 @@ mod test {
subnet_service.validator_subscriptions(vec![sub1, sub2].into_iter());
// Unsubscription event should happen at the end of the slot.
let events = get_events(&mut subnet_service, None, 1).await;
// We wait for 2 slots, to avoid timeout issues
let events = get_events(&mut subnet_service, None, 2).await;
let expected_subscription =
SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1));
@@ -567,6 +568,7 @@ mod test {
assert_eq!(expected_subscription, events[0]);
assert_eq!(expected_unsubscription, events[2]);
}
// Check that there are no more subscriptions
assert_eq!(subnet_service.subscriptions().count(), 0);
println!("{events:?}");

View File

@@ -1,6 +1,5 @@
# Security
========
Lighthouse takes security seriously. Please see our security policy on GitHub for our PGP key and information on reporting vulnerabilities:
- [GitHub: Security Policy](https://github.com/sigp/lighthouse/blob/stable/SECURITY.md)

View File

@@ -8,18 +8,14 @@ use eth2_wallet::{
};
use filesystem::{create_with_600_perms, Error as FsError};
use rand::{distributions::Alphanumeric, Rng};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::thread::sleep;
use std::time::Duration;
use std::{
fs::{self, File},
str::FromStr,
};
use zeroize::Zeroize;
use zeroize::Zeroizing;
pub mod validator_definitions;
@@ -69,8 +65,8 @@ pub fn read_password<P: AsRef<Path>>(path: P) -> Result<PlainText, io::Error> {
fs::read(path).map(strip_off_newlines).map(Into::into)
}
/// Reads a password file into a `ZeroizeString` struct, with new-lines removed.
pub fn read_password_string<P: AsRef<Path>>(path: P) -> Result<ZeroizeString, String> {
/// Reads a password file into a `Zeroizing<String>` struct, with new-lines removed.
pub fn read_password_string<P: AsRef<Path>>(path: P) -> Result<Zeroizing<String>, String> {
fs::read(path)
.map_err(|e| format!("Error opening file: {:?}", e))
.map(strip_off_newlines)
@@ -112,8 +108,8 @@ pub fn random_password() -> PlainText {
random_password_raw_string().into_bytes().into()
}
/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN` as `ZeroizeString`.
pub fn random_password_string() -> ZeroizeString {
/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN` as `Zeroizing<String>`.
pub fn random_password_string() -> Zeroizing<String> {
random_password_raw_string().into()
}
@@ -141,7 +137,7 @@ pub fn strip_off_newlines(mut bytes: Vec<u8>) -> Vec<u8> {
}
/// Reads a password from TTY or stdin if `use_stdin == true`.
pub fn read_password_from_user(use_stdin: bool) -> Result<ZeroizeString, String> {
pub fn read_password_from_user(use_stdin: bool) -> Result<Zeroizing<String>, String> {
let result = if use_stdin {
rpassword::prompt_password_stderr("")
.map_err(|e| format!("Error reading from stdin: {}", e))
@@ -150,7 +146,7 @@ pub fn read_password_from_user(use_stdin: bool) -> Result<ZeroizeString, String>
.map_err(|e| format!("Error reading from tty: {}", e))
};
result.map(ZeroizeString::from)
result.map(Zeroizing::from)
}
/// Reads a mnemonic phrase from TTY or stdin if `use_stdin == true`.
@@ -210,46 +206,6 @@ pub fn mnemonic_from_phrase(phrase: &str) -> Result<Mnemonic, String> {
Mnemonic::from_phrase(phrase, Language::English).map_err(|e| e.to_string())
}
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
///
/// Useful for ensuring that password memory is zeroed-out on drop.
#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)]
#[zeroize(drop)]
#[serde(transparent)]
pub struct ZeroizeString(String);
impl FromStr for ZeroizeString {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_owned()))
}
}
impl From<String> for ZeroizeString {
fn from(s: String) -> Self {
Self(s)
}
}
impl ZeroizeString {
pub fn as_str(&self) -> &str {
&self.0
}
/// Remove any number of newline or carriage returns from the end of a vector of bytes.
pub fn without_newlines(&self) -> ZeroizeString {
let stripped_string = self.0.trim_end_matches(['\r', '\n']).into();
Self(stripped_string)
}
}
impl AsRef<[u8]> for ZeroizeString {
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}
pub fn read_mnemonic_from_cli(
mnemonic_path: Option<PathBuf>,
stdin_inputs: bool,
@@ -294,54 +250,6 @@ pub fn read_mnemonic_from_cli(
mod test {
use super::*;
#[test]
fn test_zeroize_strip_off() {
let expected = "hello world";
assert_eq!(
ZeroizeString::from("hello world\n".to_string())
.without_newlines()
.as_str(),
expected
);
assert_eq!(
ZeroizeString::from("hello world\n\n\n\n".to_string())
.without_newlines()
.as_str(),
expected
);
assert_eq!(
ZeroizeString::from("hello world\r".to_string())
.without_newlines()
.as_str(),
expected
);
assert_eq!(
ZeroizeString::from("hello world\r\r\r\r\r".to_string())
.without_newlines()
.as_str(),
expected
);
assert_eq!(
ZeroizeString::from("hello world\r\n".to_string())
.without_newlines()
.as_str(),
expected
);
assert_eq!(
ZeroizeString::from("hello world\r\n\r\n".to_string())
.without_newlines()
.as_str(),
expected
);
assert_eq!(
ZeroizeString::from("hello world".to_string())
.without_newlines()
.as_str(),
expected
);
}
#[test]
fn test_strip_off() {
let expected = b"hello world".to_vec();

View File

@@ -3,9 +3,7 @@
//! Serves as the source-of-truth of which validators this validator client should attempt (or not
//! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct.
use crate::{
default_keystore_password_path, read_password_string, write_file_via_temporary, ZeroizeString,
};
use crate::{default_keystore_password_path, read_password_string, write_file_via_temporary};
use directory::ensure_dir_exists;
use eth2_keystore::Keystore;
use regex::Regex;
@@ -17,6 +15,7 @@ use std::io;
use std::path::{Path, PathBuf};
use types::{graffiti::GraffitiString, Address, PublicKey};
use validator_dir::VOTING_KEYSTORE_FILE;
use zeroize::Zeroizing;
/// The file name for the serialized `ValidatorDefinitions` struct.
pub const CONFIG_FILENAME: &str = "validator_definitions.yml";
@@ -52,7 +51,7 @@ pub enum Error {
/// Defines how a password for a validator keystore will be persisted.
pub enum PasswordStorage {
/// Store the password in the `validator_definitions.yml` file.
ValidatorDefinitions(ZeroizeString),
ValidatorDefinitions(Zeroizing<String>),
/// Store the password in a separate, dedicated file (likely in the "secrets" directory).
File(PathBuf),
/// Don't store the password at all.
@@ -93,7 +92,7 @@ pub enum SigningDefinition {
#[serde(skip_serializing_if = "Option::is_none")]
voting_keystore_password_path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
voting_keystore_password: Option<ZeroizeString>,
voting_keystore_password: Option<Zeroizing<String>>,
},
/// A validator that defers to a Web3Signer HTTP server for signing.
///
@@ -107,7 +106,7 @@ impl SigningDefinition {
matches!(self, SigningDefinition::LocalKeystore { .. })
}
pub fn voting_keystore_password(&self) -> Result<Option<ZeroizeString>, Error> {
pub fn voting_keystore_password(&self) -> Result<Option<Zeroizing<String>>, Error> {
match self {
SigningDefinition::LocalKeystore {
voting_keystore_password: Some(password),

View File

@@ -16,10 +16,7 @@ lighthouse_network = { workspace = true }
proto_array = { workspace = true }
ethereum_serde_utils = { workspace = true }
eth2_keystore = { workspace = true }
libsecp256k1 = { workspace = true }
ring = { workspace = true }
bytes = { workspace = true }
account_utils = { workspace = true }
zeroize = { workspace = true }
sensitive_url = { workspace = true }
ethereum_ssz = { workspace = true }
ethereum_ssz_derive = { workspace = true }

View File

@@ -1,6 +1,5 @@
use super::types::*;
use crate::Error;
use account_utils::ZeroizeString;
use reqwest::{
header::{HeaderMap, HeaderValue},
IntoUrl,
@@ -14,6 +13,7 @@ use std::path::Path;
pub use reqwest;
pub use reqwest::{Response, StatusCode, Url};
use types::graffiti::GraffitiString;
use zeroize::Zeroizing;
/// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a
/// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`).
@@ -21,7 +21,7 @@ use types::graffiti::GraffitiString;
pub struct ValidatorClientHttpClient {
client: reqwest::Client,
server: SensitiveUrl,
api_token: Option<ZeroizeString>,
api_token: Option<Zeroizing<String>>,
authorization_header: AuthorizationHeader,
}
@@ -79,18 +79,18 @@ impl ValidatorClientHttpClient {
}
/// Get a reference to this client's API token, if any.
pub fn api_token(&self) -> Option<&ZeroizeString> {
pub fn api_token(&self) -> Option<&Zeroizing<String>> {
self.api_token.as_ref()
}
/// Read an API token from the specified `path`, stripping any trailing whitespace.
pub fn load_api_token_from_file(path: &Path) -> Result<ZeroizeString, Error> {
pub fn load_api_token_from_file(path: &Path) -> Result<Zeroizing<String>, Error> {
let token = fs::read_to_string(path).map_err(|e| Error::TokenReadError(path.into(), e))?;
Ok(ZeroizeString::from(token.trim_end().to_string()))
Ok(token.trim_end().to_string().into())
}
/// Add an authentication token to use when making requests.
pub fn add_auth_token(&mut self, token: ZeroizeString) -> Result<(), Error> {
pub fn add_auth_token(&mut self, token: Zeroizing<String>) -> Result<(), Error> {
self.api_token = Some(token);
self.authorization_header = AuthorizationHeader::Bearer;

View File

@@ -1,7 +1,7 @@
use account_utils::ZeroizeString;
use eth2_keystore::Keystore;
use serde::{Deserialize, Serialize};
use types::{Address, Graffiti, PublicKeyBytes};
use zeroize::Zeroizing;
pub use slashing_protection::interchange::Interchange;
@@ -41,7 +41,7 @@ pub struct SingleKeystoreResponse {
#[serde(deny_unknown_fields)]
pub struct ImportKeystoresRequest {
pub keystores: Vec<KeystoreJsonStr>,
pub passwords: Vec<ZeroizeString>,
pub passwords: Vec<Zeroizing<String>>,
pub slashing_protection: Option<InterchangeJsonStr>,
}

View File

@@ -1,13 +1,12 @@
use account_utils::ZeroizeString;
pub use crate::lighthouse::Health;
pub use crate::lighthouse_vc::std_types::*;
pub use crate::types::{GenericResponse, VersionData};
use eth2_keystore::Keystore;
use graffiti::GraffitiString;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
pub use crate::lighthouse::Health;
pub use crate::lighthouse_vc::std_types::*;
pub use crate::types::{GenericResponse, VersionData};
pub use types::*;
use zeroize::Zeroizing;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ValidatorData {
@@ -44,7 +43,7 @@ pub struct ValidatorRequest {
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct CreateValidatorsMnemonicRequest {
pub mnemonic: ZeroizeString,
pub mnemonic: Zeroizing<String>,
#[serde(with = "serde_utils::quoted_u32")]
pub key_derivation_path_offset: u32,
pub validators: Vec<ValidatorRequest>,
@@ -74,7 +73,7 @@ pub struct CreatedValidator {
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct PostValidatorsResponseData {
pub mnemonic: ZeroizeString,
pub mnemonic: Zeroizing<String>,
pub validators: Vec<CreatedValidator>,
}
@@ -102,7 +101,7 @@ pub struct ValidatorPatchRequest {
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct KeystoreValidatorsPostRequest {
pub password: ZeroizeString,
pub password: Zeroizing<String>,
pub enable: bool,
pub keystore: Keystore,
#[serde(default)]
@@ -191,7 +190,7 @@ pub struct SingleExportKeystoresResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub validating_keystore: Option<KeystoreJsonStr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validating_keystore_password: Option<ZeroizeString>,
pub validating_keystore_password: Option<Zeroizing<String>>,
}
#[derive(Serialize, Deserialize, Debug)]

View File

@@ -26,7 +26,7 @@ use std::io::{Read, Write};
use std::path::Path;
use std::str;
use unicode_normalization::UnicodeNormalization;
use zeroize::Zeroize;
use zeroize::Zeroizing;
/// The byte-length of a BLS secret key.
const SECRET_KEY_LEN: usize = 32;
@@ -60,45 +60,6 @@ pub const HASH_SIZE: usize = 32;
/// The default iteraction count, `c`, for PBKDF2.
pub const DEFAULT_PBKDF2_C: u32 = 262_144;
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
///
/// Useful for ensuring that password memory is zeroed-out on drop.
#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)]
#[zeroize(drop)]
#[serde(transparent)]
struct ZeroizeString(String);
impl From<String> for ZeroizeString {
fn from(s: String) -> Self {
Self(s)
}
}
impl AsRef<[u8]> for ZeroizeString {
fn as_ref(&self) -> &[u8] {
self.0.as_bytes()
}
}
impl std::ops::Deref for ZeroizeString {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for ZeroizeString {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<char> for ZeroizeString {
fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
ZeroizeString(String::from_iter(iter))
}
}
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidSecretKeyLen { len: usize, expected: usize },
@@ -451,11 +412,12 @@ fn is_control_character(c: char) -> bool {
/// Takes a slice of bytes and returns a NFKD normalized string representation.
///
/// Returns an error if the bytes are not valid utf8.
fn normalize(bytes: &[u8]) -> Result<ZeroizeString, Error> {
fn normalize(bytes: &[u8]) -> Result<Zeroizing<String>, Error> {
Ok(str::from_utf8(bytes)
.map_err(|_| Error::InvalidPasswordBytes)?
.nfkd()
.collect::<ZeroizeString>())
.collect::<String>()
.into())
}
/// Generates a checksum to indicate that the `derived_key` is associated with the

View File

@@ -73,6 +73,7 @@ eth2 = { workspace = true }
beacon_processor = { workspace = true }
beacon_node_fallback = { workspace = true }
initialized_validators = { workspace = true }
zeroize = { workspace = true }
[[test]]

View File

@@ -15,7 +15,7 @@ use account_manager::{
use account_utils::{
eth2_keystore::KeystoreBuilder,
validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions},
ZeroizeString, STDIN_INPUTS_FLAG,
STDIN_INPUTS_FLAG,
};
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
use std::env;
@@ -27,6 +27,7 @@ use std::str::from_utf8;
use tempfile::{tempdir, TempDir};
use types::{Keypair, PublicKey};
use validator_dir::ValidatorDir;
use zeroize::Zeroizing;
/// Returns the `lighthouse account` command.
fn account_cmd() -> Command {
@@ -498,7 +499,7 @@ fn validator_import_launchpad() {
signing_definition: SigningDefinition::LocalKeystore {
voting_keystore_path,
voting_keystore_password_path: None,
voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())),
voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())),
},
};
@@ -650,7 +651,7 @@ fn validator_import_launchpad_no_password_then_add_password() {
signing_definition: SigningDefinition::LocalKeystore {
voting_keystore_path: dst_keystore_dir.join(KEYSTORE_NAME),
voting_keystore_password_path: None,
voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())),
voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())),
},
};
@@ -753,7 +754,7 @@ fn validator_import_launchpad_password_file() {
signing_definition: SigningDefinition::LocalKeystore {
voting_keystore_path,
voting_keystore_password_path: None,
voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())),
voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())),
},
};

View File

@@ -71,7 +71,7 @@ if [[ "$BEHAVIOR" == "failure" ]]; then
# This process should not last longer than 2 epochs
vc_1_range_start=0
vc_1_range_end=$(($KEYS_PER_NODE - 1))
vc_1_keys_artifact_id="1-lighthouse-geth-$vc_1_range_start-$vc_1_range_end-0"
vc_1_keys_artifact_id="1-lighthouse-geth-$vc_1_range_start-$vc_1_range_end"
service_name=vc-1-doppelganger
kurtosis service add \
@@ -107,7 +107,7 @@ if [[ "$BEHAVIOR" == "success" ]]; then
vc_4_range_start=$(($KEYS_PER_NODE * 3))
vc_4_range_end=$(($KEYS_PER_NODE * 4 - 1))
vc_4_keys_artifact_id="4-lighthouse-geth-$vc_4_range_start-$vc_4_range_end-0"
vc_4_keys_artifact_id="4-lighthouse-geth-$vc_4_range_start-$vc_4_range_end"
service_name=vc-4
kurtosis service add \

View File

@@ -66,6 +66,9 @@ impl GraffitiFile {
for line in lines {
let line = line.map_err(|e| Error::InvalidLine(e.to_string()))?;
if line.trim().is_empty() {
continue;
}
let (pk_opt, graffiti) = read_line(&line)?;
match pk_opt {
Some(pk) => {
@@ -133,9 +136,15 @@ mod tests {
const CUSTOM_GRAFFITI1: &str = "custom-graffiti1";
const CUSTOM_GRAFFITI2: &str = "graffitiwall:720:641:#ffff00";
const EMPTY_GRAFFITI: &str = "";
// Newline test cases
const CUSTOM_GRAFFITI4: &str = "newlines-tests";
const PK1: &str = "0x800012708dc03f611751aad7a43a082142832b5c1aceed07ff9b543cf836381861352aa923c70eeb02018b638aa306aa";
const PK2: &str = "0x80001866ce324de7d80ec73be15e2d064dcf121adf1b34a0d679f2b9ecbab40ce021e03bb877e1a2fe72eaaf475e6e21";
const PK3: &str = "0x9035d41a8bc11b08c17d0d93d876087958c9d055afe86fce558e3b988d92434769c8d50b0b463708db80c6aae1160c02";
const PK4: &str = "0x8c0fca2cc70f44188a4b79e5623ac85898f1df479e14a1f4ebb615907810b6fb939c3fb4ba2081b7a5b6e33dc73621d2";
const PK5: &str = "0x87998b0ea4a8826f03d1985e5a5ce7235bd3a56fb7559b02a55b737f4ebc69b0bf35444de5cf2680cb7eb2283eb62050";
const PK6: &str = "0xa2af9b128255568e2ee5c42af118cc4301198123d210dbdbf2ca7ec0222f8d491f308e85076b09a2f44a75875cd6fa0f";
// Create a graffiti file in the required format and return a path to the file.
fn create_graffiti_file() -> PathBuf {
@@ -143,6 +152,9 @@ mod tests {
let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap();
let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap();
let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap();
let pk4 = PublicKeyBytes::deserialize(&hex::decode(&PK4[2..]).unwrap()).unwrap();
let pk5 = PublicKeyBytes::deserialize(&hex::decode(&PK5[2..]).unwrap()).unwrap();
let pk6 = PublicKeyBytes::deserialize(&hex::decode(&PK6[2..]).unwrap()).unwrap();
let file_name = temp.into_path().join("graffiti.txt");
@@ -160,6 +172,29 @@ mod tests {
graffiti_file
.write_all(format!("{}:{}\n", pk3.as_hex_string(), EMPTY_GRAFFITI).as_bytes())
.unwrap();
// Test Lines with leading newlines - these empty lines will be skipped
graffiti_file.write_all(b"\n").unwrap();
graffiti_file.write_all(b" \n").unwrap();
graffiti_file
.write_all(format!("{}: {}\n", pk4.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes())
.unwrap();
// Test Empty lines between entries - these will be skipped
graffiti_file.write_all(b"\n").unwrap();
graffiti_file.write_all(b" \n").unwrap();
graffiti_file.write_all(b"\t\n").unwrap();
graffiti_file
.write_all(format!("{}: {}\n", pk5.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes())
.unwrap();
// Test Trailing empty lines - these will be skipped
graffiti_file
.write_all(format!("{}: {}\n", pk6.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes())
.unwrap();
graffiti_file.write_all(b"\n").unwrap();
graffiti_file.write_all(b" \n").unwrap();
graffiti_file.flush().unwrap();
file_name
}
@@ -172,6 +207,9 @@ mod tests {
let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap();
let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap();
let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap();
let pk4 = PublicKeyBytes::deserialize(&hex::decode(&PK4[2..]).unwrap()).unwrap();
let pk5 = PublicKeyBytes::deserialize(&hex::decode(&PK5[2..]).unwrap()).unwrap();
let pk6 = PublicKeyBytes::deserialize(&hex::decode(&PK6[2..]).unwrap()).unwrap();
// Read once
gf.read_graffiti_file().unwrap();
@@ -190,6 +228,20 @@ mod tests {
GraffitiString::from_str(EMPTY_GRAFFITI).unwrap().into()
);
// Test newline cases - all empty lines should be skipped
assert_eq!(
gf.load_graffiti(&pk4).unwrap().unwrap(),
GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into()
);
assert_eq!(
gf.load_graffiti(&pk5).unwrap().unwrap(),
GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into()
);
assert_eq!(
gf.load_graffiti(&pk6).unwrap().unwrap(),
GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into()
);
// Random pk should return the default graffiti
let random_pk = Keypair::random().pk.compress();
assert_eq!(

View File

@@ -43,6 +43,7 @@ validator_services = { workspace = true }
url = { workspace = true }
warp_utils = { workspace = true }
warp = { workspace = true }
zeroize = { workspace = true }
[dev-dependencies]
itertools = { workspace = true }

View File

@@ -2,7 +2,7 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}
use account_utils::{
eth2_keystore::Keystore,
eth2_wallet::{bip39::Mnemonic, WalletBuilder},
random_mnemonic, random_password, ZeroizeString,
random_mnemonic, random_password,
};
use eth2::lighthouse_vc::types::{self as api_types};
use slot_clock::SlotClock;
@@ -11,6 +11,7 @@ use types::ChainSpec;
use types::EthSpec;
use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder};
use validator_store::ValidatorStore;
use zeroize::Zeroizing;
/// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in
/// this validator client.
@@ -59,7 +60,7 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock,
for request in validator_requests {
let voting_password = random_password();
let withdrawal_password = random_password();
let voting_password_string = ZeroizeString::from(
let voting_password_string = Zeroizing::from(
String::from_utf8(voting_password.as_bytes().to_vec()).map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"locally generated password is not utf8: {:?}",
@@ -199,7 +200,7 @@ pub async fn create_validators_web3signer<T: 'static + SlotClock, E: EthSpec>(
pub fn get_voting_password_storage(
secrets_dir: &Option<PathBuf>,
voting_keystore: &Keystore,
voting_password_string: &ZeroizeString,
voting_password_string: &Zeroizing<String>,
) -> Result<PasswordStorage, warp::Rejection> {
if let Some(secrets_dir) = &secrets_dir {
let password_path = keystore_password_path(secrets_dir, voting_keystore);

View File

@@ -1,5 +1,5 @@
//! Implementation of the standard keystore management API.
use account_utils::{validator_definitions::PasswordStorage, ZeroizeString};
use account_utils::validator_definitions::PasswordStorage;
use eth2::lighthouse_vc::{
std_types::{
DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse,
@@ -22,6 +22,7 @@ use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder};
use validator_store::ValidatorStore;
use warp::Rejection;
use warp_utils::reject::{custom_bad_request, custom_server_error};
use zeroize::Zeroizing;
pub fn list<T: SlotClock + 'static, E: EthSpec>(
validator_store: Arc<ValidatorStore<T, E>>,
@@ -167,7 +168,7 @@ pub fn import<T: SlotClock + 'static, E: EthSpec>(
fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
keystore: Keystore,
password: ZeroizeString,
password: Zeroizing<String>,
validator_dir_path: PathBuf,
secrets_dir: Option<PathBuf>,
validator_store: &ValidatorStore<T, E>,

View File

@@ -2,7 +2,6 @@ use crate::{ApiSecret, Config as HttpConfig, Context};
use account_utils::validator_definitions::ValidatorDefinitions;
use account_utils::{
eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password,
ZeroizeString,
};
use deposit_contract::decode_eth1_tx_data;
use doppelganger_service::DoppelgangerService;
@@ -28,6 +27,7 @@ use task_executor::test_utils::TestRuntime;
use tempfile::{tempdir, TempDir};
use tokio::sync::oneshot;
use validator_store::{Config as ValidatorStoreConfig, ValidatorStore};
use zeroize::Zeroizing;
pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37];
pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42);
@@ -321,7 +321,7 @@ impl ApiTester {
.collect::<Vec<_>>();
let (response, mnemonic) = if s.specify_mnemonic {
let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string());
let mnemonic = Zeroizing::from(random_mnemonic().phrase().to_string());
let request = CreateValidatorsMnemonicRequest {
mnemonic: mnemonic.clone(),
key_derivation_path_offset: s.key_derivation_path_offset,

View File

@@ -9,7 +9,7 @@ use initialized_validators::{Config as InitializedValidatorsConfig, InitializedV
use crate::{ApiSecret, Config as HttpConfig, Context};
use account_utils::{
eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password,
random_password_string, validator_definitions::ValidatorDefinitions, ZeroizeString,
random_password_string, validator_definitions::ValidatorDefinitions,
};
use deposit_contract::decode_eth1_tx_data;
use eth2::{
@@ -33,6 +33,7 @@ use task_executor::test_utils::TestRuntime;
use tempfile::{tempdir, TempDir};
use types::graffiti::GraffitiString;
use validator_store::{Config as ValidatorStoreConfig, ValidatorStore};
use zeroize::Zeroizing;
const PASSWORD_BYTES: &[u8] = &[42, 50, 37];
pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42);
@@ -282,7 +283,7 @@ impl ApiTester {
.collect::<Vec<_>>();
let (response, mnemonic) = if s.specify_mnemonic {
let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string());
let mnemonic = Zeroizing::from(random_mnemonic().phrase().to_string());
let request = CreateValidatorsMnemonicRequest {
mnemonic: mnemonic.clone(),
key_derivation_path_offset: s.key_derivation_path_offset,

View File

@@ -14,8 +14,9 @@ use std::{collections::HashMap, path::Path};
use tokio::runtime::Handle;
use types::{attestation::AttestationBase, Address};
use validator_store::DEFAULT_GAS_LIMIT;
use zeroize::Zeroizing;
fn new_keystore(password: ZeroizeString) -> Keystore {
fn new_keystore(password: Zeroizing<String>) -> Keystore {
let keypair = Keypair::random();
Keystore(
KeystoreBuilder::new(&keypair, password.as_ref(), String::new())

View File

@@ -24,3 +24,4 @@ tokio = { workspace = true }
bincode = { workspace = true }
filesystem = { workspace = true }
validator_metrics = { workspace = true }
zeroize = { workspace = true }

View File

@@ -14,7 +14,6 @@ use account_utils::{
self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition,
CONFIG_FILENAME,
},
ZeroizeString,
};
use eth2_keystore::Keystore;
use lockfile::{Lockfile, LockfileError};
@@ -34,6 +33,7 @@ use types::graffiti::GraffitiString;
use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes};
use url::{ParseError, Url};
use validator_dir::Builder as ValidatorDirBuilder;
use zeroize::Zeroizing;
use key_cache::KeyCache;
@@ -74,7 +74,7 @@ pub enum OnDecryptFailure {
pub struct KeystoreAndPassword {
pub keystore: Keystore,
pub password: Option<ZeroizeString>,
pub password: Option<Zeroizing<String>>,
}
#[derive(Debug)]
@@ -262,7 +262,7 @@ impl InitializedValidator {
// If the password is supplied, use it and ignore the path
// (if supplied).
(_, Some(password)) => (
password.as_ref().to_vec().into(),
password.as_bytes().to_vec().into(),
keystore
.decrypt_keypair(password.as_ref())
.map_err(Error::UnableToDecryptKeystore)?,
@@ -282,7 +282,7 @@ impl InitializedValidator {
&keystore,
&keystore_path,
)?;
(password.as_ref().to_vec().into(), keypair)
(password.as_bytes().to_vec().into(), keypair)
}
},
)
@@ -455,7 +455,7 @@ fn build_web3_signer_client(
fn unlock_keystore_via_stdin_password(
keystore: &Keystore,
keystore_path: &Path,
) -> Result<(ZeroizeString, Keypair), Error> {
) -> Result<(Zeroizing<String>, Keypair), Error> {
eprintln!();
eprintln!(
"The {} file does not contain either of the following fields for {:?}:",
@@ -1172,14 +1172,14 @@ impl InitializedValidators {
voting_keystore_path,
} => {
let pw = if let Some(p) = voting_keystore_password {
p.as_ref().to_vec().into()
p.as_bytes().to_vec().into()
} else if let Some(path) = voting_keystore_password_path {
read_password(path).map_err(Error::UnableToReadVotingKeystorePassword)?
} else {
let keystore = open_keystore(voting_keystore_path)?;
unlock_keystore_via_stdin_password(&keystore, voting_keystore_path)?
.0
.as_ref()
.as_bytes()
.to_vec()
.into()
};
@@ -1425,7 +1425,7 @@ impl InitializedValidators {
/// This should only be used for testing, it's rather destructive.
pub fn delete_passwords_from_validator_definitions(
&mut self,
) -> Result<HashMap<PublicKey, ZeroizeString>, Error> {
) -> Result<HashMap<PublicKey, Zeroizing<String>>, Error> {
let mut passwords = HashMap::default();
for def in self.definitions.as_mut_slice() {

View File

@@ -21,6 +21,7 @@ eth2 = { workspace = true }
hex = { workspace = true }
tokio = { workspace = true }
derivative = { workspace = true }
zeroize = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@@ -1,5 +1,5 @@
use account_utils::strip_off_newlines;
pub use account_utils::STDIN_INPUTS_FLAG;
use account_utils::{strip_off_newlines, ZeroizeString};
use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr};
use eth2::{
lighthouse_vc::{
@@ -14,6 +14,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use tree_hash::TreeHash;
use types::*;
use zeroize::Zeroizing;
pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates";
pub const COUNT_FLAG: &str = "count";
@@ -41,7 +42,7 @@ pub enum UploadError {
#[derive(Clone, Serialize, Deserialize)]
pub struct ValidatorSpecification {
pub voting_keystore: KeystoreJsonStr,
pub voting_keystore_password: ZeroizeString,
pub voting_keystore_password: Zeroizing<String>,
pub slashing_protection: Option<InterchangeJsonStr>,
pub fee_recipient: Option<Address>,
pub gas_limit: Option<u64>,

View File

@@ -1,6 +1,6 @@
use super::common::*;
use crate::DumpConfig;
use account_utils::{eth2_keystore::Keystore, ZeroizeString};
use account_utils::eth2_keystore::Keystore;
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::FLAG_HEADER;
use derivative::Derivative;
@@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use types::Address;
use zeroize::Zeroizing;
pub const CMD: &str = "import";
pub const VALIDATORS_FILE_FLAG: &str = "validators-file";
@@ -167,7 +168,7 @@ pub struct ImportConfig {
pub vc_token_path: PathBuf,
pub ignore_duplicates: bool,
#[derivative(Debug = "ignore")]
pub password: Option<ZeroizeString>,
pub password: Option<Zeroizing<String>>,
pub fee_recipient: Option<Address>,
pub gas_limit: Option<u64>,
pub builder_proposals: Option<bool>,
@@ -184,7 +185,7 @@ impl ImportConfig {
vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?,
vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?,
ignore_duplicates: matches.get_flag(IGNORE_DUPLICATES_FLAG),
password: clap_utils::parse_optional(matches, PASSWORD)?,
password: clap_utils::parse_optional(matches, PASSWORD)?.map(Zeroizing::new),
fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT)?,
gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT)?,
builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS)?,
@@ -382,10 +383,7 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> {
pub mod tests {
use super::*;
use crate::create_validators::tests::TestBuilder as CreateTestBuilder;
use std::{
fs::{self, File},
str::FromStr,
};
use std::fs::{self, File};
use tempfile::{tempdir, TempDir};
use validator_http_api::{test_utils::ApiTester, Config as HttpConfig};
@@ -419,7 +417,7 @@ pub mod tests {
vc_url: vc.url.clone(),
vc_token_path,
ignore_duplicates: false,
password: Some(ZeroizeString::from_str("password").unwrap()),
password: Some(Zeroizing::new("password".into())),
fee_recipient: None,
builder_boost_factor: None,
gas_limit: None,

View File

@@ -1,6 +1,6 @@
use super::common::*;
use crate::DumpConfig;
use account_utils::{read_password_from_user, ZeroizeString};
use account_utils::read_password_from_user;
use clap::{Arg, ArgAction, ArgMatches, Command};
use eth2::{
lighthouse_vc::{
@@ -19,6 +19,7 @@ use std::str::FromStr;
use std::time::Duration;
use tokio::time::sleep;
use types::{Address, PublicKeyBytes};
use zeroize::Zeroizing;
pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move";
pub const VALIDATOR_SPECIFICATION_FILE: &str = "validator-specification.json";
@@ -48,7 +49,7 @@ pub enum PasswordSource {
}
impl PasswordSource {
fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result<ZeroizeString, String> {
fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result<Zeroizing<String>, String> {
match self {
PasswordSource::Interactive { stdin_inputs } => {
eprintln!("Please enter a password for keystore {:?}:", pubkey);