Skip to content

Commit

Permalink
feat(sidecar): keystore secrets path in config
Browse files Browse the repository at this point in the history
  • Loading branch information
thedevbirb committed Oct 17, 2024
1 parent a043309 commit abea1cc
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 35 deletions.
2 changes: 1 addition & 1 deletion bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async fn main() -> Result<()> {
bail!("Failed to initialize the sidecar driver with local signer: {:?}", err)
}
}
} else if opts.signing.keystore_password.is_some() {
} else if opts.signing.keystore.is_some() {
match SidecarDriver::with_keystore_signer(&opts).await {
Ok(driver) => driver.run_forever().await,
Err(err) => {
Expand Down
38 changes: 25 additions & 13 deletions bolt-sidecar/src/config/signing.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{fmt, net::SocketAddr};
use std::{fmt, net::SocketAddr, path::PathBuf};

use clap::{ArgGroup, Args};
use lighthouse_account_utils::ZeroizeString;
Expand All @@ -7,10 +7,10 @@ use serde::Deserialize;
use crate::common::{BlsSecretKeyWrapper, JwtSecretConfig};

/// Command-line options for signing
#[derive(Args, Deserialize)]
#[derive(Args, Deserialize, Debug)]
#[clap(
group = ArgGroup::new("signing-opts").required(true)
.args(&["private_key", "commit_boost_address", "keystore_password"])
.args(&["private_key", "commit_boost_address", "keystore_opts"])
)]
pub struct SigningOpts {
/// Private key to use for signing preconfirmation requests
Expand All @@ -22,28 +22,40 @@ pub struct SigningOpts {
/// JWT in hexadecimal format for authenticating with the commit-boost service
#[clap(long, env = "BOLT_SIDECAR_CB_JWT_HEX", requires("commit_boost_address"))]
pub commit_boost_jwt_hex: Option<JwtSecretConfig>,
/// Options for the ERC-2335 keystore
#[clap(flatten)]
pub keystore: Option<KeystoreOps>,
/// Path to the delegations file. If not provided, the default path is used.
#[clap(long, env = "BOLT_SIDECAR_DELEGATIONS_PATH")]
pub delegations_path: Option<PathBuf>,
}

#[derive(Args, Deserialize)]
#[clap(
group = ArgGroup::new("keystore-opts").required(true)
.args(&["keystore_password", "keystore_secrets_path"])
)]
pub struct KeystoreOps {
/// The password for the ERC-2335 keystore.
/// Reference: https://eips.ethereum.org/EIPS/eip-2335
#[clap(long, env = "BOLT_SIDECAR_KEYSTORE_PASSWORD")]
pub keystore_password: Option<ZeroizeString>,
/// The path to the ERC-2335 keystore secret passwords
/// Reference: https://eips.ethereum.org/EIPS/eip-2335
#[clap(long, env = "BOLT_SIDECAR_KEYSTORE_SECRETS_PATH", conflicts_with("keystore_password"))]
pub keystore_secrets_path: Option<PathBuf>,
/// Path to the keystores folder. If not provided, the default path is used.
#[clap(long, env = "BOLT_SIDECAR_KEYSTORE_PATH", requires("keystore_password"))]
pub keystore_path: Option<String>,
/// Path to the delegations file. If not provided, the default path is used.
#[clap(long, env = "BOLT_SIDECAR_DELEGATIONS_PATH")]
pub delegations_path: Option<String>,
#[clap(long, env = "BOLT_SIDECAR_KEYSTORE_PATH")]
pub keystore_path: Option<PathBuf>,
}

// Implement Debug manually to hide the keystore_password field
impl fmt::Debug for SigningOpts {
impl fmt::Debug for KeystoreOps {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SigningOpts")
.field("private_key", &self.private_key)
.field("commit_boost_url", &self.commit_boost_address)
.field("commit_boost_jwt_hex", &self.commit_boost_jwt_hex)
.field("keystore_password", &"********") // Hides the actual password
.field("keystore_path", &self.keystore_path)
.field("delegations_path", &self.delegations_path)
.field("keystore_secrets_path", &self.keystore_secrets_path)
.finish()
}
}
22 changes: 17 additions & 5 deletions bolt-sidecar/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,23 @@ impl SidecarDriver<StateClient, PrivateKeySigner> {
// The default state client simply uses the execution API URL to fetch state updates.
let state_client = StateClient::new(opts.execution_api_url.clone());

let keystore_signer = SignerBLS::Keystore(KeystoreSigner::new(
opts.signing.keystore_path.as_deref(),
opts.signing.keystore_password.as_ref().expect("keystore password").as_ref(),
opts.chain,
)?);
let keystore_opts = opts.signing.keystore.as_ref().expect("keystore is some");

let keystore = if let Some(psw) = keystore_opts.keystore_password.as_ref() {
KeystoreSigner::from_password(
keystore_opts.keystore_path.as_ref(),
psw.as_ref(),
opts.chain,
)?
} else {
KeystoreSigner::from_secrets_directory(
keystore_opts.keystore_path.as_ref(),
keystore_opts.keystore_secrets_path.as_ref(),
opts.chain,
)?
};

let keystore_signer = SignerBLS::Keystore(keystore);

// Commitment responses are signed with a regular Ethereum wallet private key.
// This is now generated randomly because slashing is not yet implemented.
Expand Down
13 changes: 9 additions & 4 deletions bolt-sidecar/src/primitives/delegation.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::fs;
use std::{fs, path::PathBuf};

use alloy::signers::k256::sha2::{Digest, Sha256};
use ethereum_consensus::crypto::{PublicKey as BlsPublicKey, Signature as BlsSignature};
Expand Down Expand Up @@ -48,7 +48,9 @@ impl SignableBLS for DelegationMessage {
}

/// read the delegaitons from disk if they exist and add them to the constraints client
pub fn read_signed_delegations_from_file(file_path: &str) -> eyre::Result<Vec<SignedDelegation>> {
pub fn read_signed_delegations_from_file(
file_path: &PathBuf,
) -> eyre::Result<Vec<SignedDelegation>> {
match fs::read_to_string(file_path) {
Ok(contents) => match serde_json::from_str::<Vec<SignedDelegation>>(&contents) {
Ok(delegations) => Ok(delegations),
Expand Down Expand Up @@ -91,11 +93,14 @@ impl SignableBLS for RevocationMessage {

#[cfg(test)]
mod tests {
use std::path::PathBuf;

#[test]
fn test_read_signed_delegations_from_file() {
let file = env!("CARGO_MANIFEST_DIR").to_string() + "/test_data/delegations.json";
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("test_data/delegations.json");

let delegations = super::read_signed_delegations_from_file(&file)
let delegations = super::read_signed_delegations_from_file(&path)
.expect("Failed to read delegations from file");

assert_eq!(delegations.len(), 1);
Expand Down
66 changes: 54 additions & 12 deletions bolt-sidecar/src/signer/keystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ use crate::{builder::signature::compute_signing_root, crypto::bls::BLSSig, Chain
use super::SignerResult;

pub const KEYSTORES_DEFAULT_PATH: &str = "keys";
pub const KEYSTORES_SECRETS_DEFAULT_PATH: &str = "keys";

#[derive(Debug, thiserror::Error)]
pub enum KeystoreError {
#[error("failed to read keystore directory: {0}")]
ReadFromDirectory(#[from] std::io::Error),
#[error("failed to read keystore from JSON file {0}: {1}")]
ReadFromJSON(PathBuf, String),
#[error("failed to read keystore secret from file: {0}")]
ReadFromSecretFile(String),
#[error("failed to decrypt keypair from JSON file {0} with the provided password: {1}")]
KeypairDecryption(PathBuf, String),
#[error("could not find private key associated to public key {0}")]
Expand All @@ -44,14 +47,18 @@ pub struct KeystoreSigner {

impl KeystoreSigner {
/// Creates a new `KeystoreSigner` from the keystore files in the `keys_path` directory.
pub fn new(keys_path: Option<&str>, password: &[u8], chain: ChainConfig) -> SignerResult<Self> {
pub fn from_password(
keys_path: Option<&PathBuf>,
password: &[u8],
chain: ChainConfig,
) -> SignerResult<Self> {
let keystores_paths = keystore_paths(keys_path)?;
let mut keypairs = Vec::with_capacity(keystores_paths.len());

for path in keystores_paths {
let keypair = Keystore::from_json_file(path.clone());
let keypair = keypair
.map_err(|e| KeystoreError::ReadFromJSON(path.clone(), format!("{e:?}")))?
let keystore = Keystore::from_json_file(path.clone())
.map_err(|e| KeystoreError::ReadFromJSON(path.clone(), format!("{e:?}")))?;
let keypair = keystore
.decrypt_keypair(password)
.map_err(|e| KeystoreError::KeypairDecryption(path.clone(), format!("{e:?}")))?;
keypairs.push(keypair);
Expand All @@ -60,6 +67,37 @@ impl KeystoreSigner {
Ok(Self { keypairs, chain })
}

pub fn from_secrets_directory(
keys_path: Option<&PathBuf>,
secrets_path: Option<&PathBuf>,
chain: ChainConfig,
) -> SignerResult<Self> {
let keystores_paths = keystore_paths(keys_path)?;
let keystore_secrets_path = parse_path(secrets_path, KEYSTORES_SECRETS_DEFAULT_PATH);

let mut keypairs = Vec::with_capacity(keystores_paths.len());

for path in keystores_paths {
let keystore = Keystore::from_json_file(path.clone())
.map_err(|e| KeystoreError::ReadFromJSON(path.clone(), format!("{e:?}")))?;

let pubkey = keystore.pubkey();

let mut secret_path = keystore_secrets_path.clone();
secret_path.push(pubkey);

let password = fs::read_to_string(secret_path)
.map_err(|e| KeystoreError::ReadFromSecretFile(format!("{e:?}")))?;

let keypair = keystore
.decrypt_keypair(password.as_bytes())
.map_err(|e| KeystoreError::KeypairDecryption(path.clone(), format!("{e:?}")))?;
keypairs.push(keypair);
}

Ok(Self { keypairs, chain })
}

/// Returns the public keys of the keypairs in the keystore.
pub fn pubkeys(&self) -> HashSet<BlsPublicKey> {
self.keypairs
Expand Down Expand Up @@ -121,14 +159,9 @@ impl Debug for KeystoreSigner {
/// -- 0x1234.../validator.json
/// -- 0x5678.../validator.json
/// -- ...
fn keystore_paths(keys_path: Option<&str>) -> SignerResult<Vec<PathBuf>> {
fn keystore_paths(keys_path: Option<&PathBuf>) -> SignerResult<Vec<PathBuf>> {
// Create the path to the keystore directory, starting from the root of the project
let keys_path = if let Some(keys_path) = keys_path {
Path::new(&keys_path).to_path_buf()
} else {
let project_root = env!("CARGO_MANIFEST_DIR");
Path::new(project_root).join(keys_path.unwrap_or(KEYSTORES_DEFAULT_PATH))
};
let keys_path = parse_path(keys_path, KEYSTORES_DEFAULT_PATH);

let json_extension = OsString::from("json");

Expand All @@ -149,6 +182,15 @@ fn keystore_paths(keys_path: Option<&str>) -> SignerResult<Vec<PathBuf>> {
Ok(keystores_paths)
}

fn parse_path(path: Option<&PathBuf>, fallback_relative_path: &str) -> PathBuf {
if let Some(path) = path {
path.clone()
} else {
let project_root = env!("CARGO_MANIFEST_DIR");
Path::new(project_root).join(fallback_relative_path)
}
}

fn read_dir(path: PathBuf) -> SignerResult<ReadDir> {
Ok(fs::read_dir(path).map_err(KeystoreError::ReadFromDirectory)?)
}
Expand Down Expand Up @@ -292,7 +334,7 @@ mod tests {
}

let keystore_signer =
KeystoreSigner::new(None, keystore_password.as_bytes(), chain_config)
KeystoreSigner::from_password(None, keystore_password.as_bytes(), chain_config)
.expect("to create keystore signer");

assert_eq!(keystore_signer.keypairs.len(), 1);
Expand Down

0 comments on commit abea1cc

Please sign in to comment.