Skip to content

Commit

Permalink
Add an encrypted config option to pcli (#4343)
Browse files Browse the repository at this point in the history
This adds a new option to encrypt the `soft-kms` and `threshold` custody
backends with a password, so that spend-key related material is
encrypted at rest. This is implemented by:

1. Having a `pcli init --encrypted` flag that applies to both of these
backends, which prompts a user for a password (and confirmation) before
using that to encrypt the config.
2. Having a `pcli init re-encrypt` command to read an existing config
and encrypt its backend, if necessary, to allow importing existing
configs.

This is also implemented internally in a lazy way, so that a password is
only prompted when the custody services methods are actually called,
allowing us to not need a password for view only commands.

Closes #4293.

- [x] If this code contains consensus-breaking changes, I have added the
"consensus-breaking" label. Otherwise, I declare my belief that there
are not consensus-breaking changes, for the following reason:

  > This is a client-only change.

---------

Co-authored-by: cratelyn <[email protected]>
  • Loading branch information
2 people authored and hdevalence committed May 8, 2024
1 parent 288ef1e commit 5dcc768
Show file tree
Hide file tree
Showing 12 changed files with 570 additions and 99 deletions.
26 changes: 25 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

132 changes: 103 additions & 29 deletions crates/bin/pcli/src/command/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ pub struct InitCmd {
parse(try_from_str = Url::parse),
)]
grpc_url: Url,
/// For configs with spend authority, this will enable password encryption.
///
/// This has no effect on a view only service.
#[clap(long, action)]
encrypted: bool,
}

#[derive(Debug, Clone, clap::Subcommand)]
Expand Down Expand Up @@ -62,8 +67,11 @@ pub enum InitSubCmd {
Threshold(ThresholdInitCmd),
// This is not accessible directly by the user, because it's impermissible to initialize the
// governance subkey as view-only.
#[clap(skip)]
#[clap(skip, display_order = 200)]
ViewOnly { full_viewing_key: String },
/// If relevant, change the current config to an encrypted config, with a password.
#[clap(display_order = 800)]
ReEncrypt,
}

#[derive(Debug, Clone, clap::Subcommand)]
Expand Down Expand Up @@ -268,35 +276,35 @@ impl InitCmd {
}
let home_dir = home_dir.as_ref();

match &init_type {
InitType::SpendKey => {
// Check that the data_dir is empty before running init:
if home_dir.exists() && home_dir.read_dir()?.next().is_some() {
anyhow::bail!(
"home directory {:?} is not empty; refusing to initialize",
home_dir
);
}
}
InitType::GovernanceKey => {
// Check that there is no existing governance key before running init:
let config_path = home_dir.join(crate::CONFIG_FILE_NAME);
let config = PcliConfig::load(config_path)?;
if config.governance_custody.is_some() {
anyhow::bail!(
"governance key already exists in config file at {:?}; refusing to overwrite it",
home_dir
);
}
let existing_config = {
let config_path = home_dir.join(crate::CONFIG_FILE_NAME);
if config_path.exists() {
Some(PcliConfig::load(config_path)?)
} else {
None
}
}
};
let relevant_config_exists = match &init_type {
InitType::SpendKey => existing_config.is_some(),
InitType::GovernanceKey => existing_config
.as_ref()
.is_some_and(|x| x.governance_custody.is_some()),
};

let (full_viewing_key, custody) = match (&init_type, &subcmd) {
(_, InitSubCmd::SoftKms(cmd)) => {
let (full_viewing_key, custody) = match (&init_type, &subcmd, relevant_config_exists) {
(_, InitSubCmd::SoftKms(cmd), false) => {
let spend_key = cmd.spend_key(init_type)?;
(
spend_key.full_viewing_key().clone(),
CustodyConfig::SoftKms(spend_key.into()),
if self.encrypted {
let password = ActualTerminal.get_confirmed_password().await?;
CustodyConfig::Encrypted(penumbra_custody::encrypted::Config::create(
&password,
penumbra_custody::encrypted::InnerConfig::SoftKms(spend_key.into()),
)?)
} else {
CustodyConfig::SoftKms(spend_key.into())
},
)
}
(
Expand All @@ -305,20 +313,82 @@ impl InitCmd {
threshold,
num_participants,
}),
false,
) => {
let config = threshold::dkg(*threshold, *num_participants, &ActualTerminal).await?;
(config.fvk().clone(), CustodyConfig::Threshold(config))
let fvk = config.fvk().clone();
let custody_config = if self.encrypted {
let password = ActualTerminal.get_confirmed_password().await?;
CustodyConfig::Encrypted(penumbra_custody::encrypted::Config::create(
&password,
penumbra_custody::encrypted::InnerConfig::Threshold(config),
)?)
} else {
CustodyConfig::Threshold(config)
};
(fvk, custody_config)
}
(_, InitSubCmd::Threshold(ThresholdInitCmd::Deal { .. })) => {
(_, InitSubCmd::Threshold(ThresholdInitCmd::Deal { .. }), _) => {
unreachable!("this should already have been handled above")
}
(InitType::SpendKey, InitSubCmd::ViewOnly { full_viewing_key }) => {
(InitType::SpendKey, InitSubCmd::ViewOnly { full_viewing_key }, false) => {
let full_viewing_key = full_viewing_key.parse()?;
(full_viewing_key, CustodyConfig::ViewOnly)
}
(InitType::GovernanceKey, InitSubCmd::ViewOnly { .. }) => {
(InitType::GovernanceKey, InitSubCmd::ViewOnly { .. }, false) => {
unreachable!("governance keys can't be initialized in view-only mode")
}
(typ, InitSubCmd::ReEncrypt, true) => {
let config = existing_config.expect("the config should exist in this branch");
let fvk = config.full_viewing_key;
let custody = match typ {
InitType::SpendKey => config.custody,
InitType::GovernanceKey => match config
.governance_custody
.expect("the governence custody should exist in this branch")
{
GovernanceCustodyConfig::SoftKms(c) => CustodyConfig::SoftKms(c),
GovernanceCustodyConfig::Threshold(c) => CustodyConfig::Threshold(c),
GovernanceCustodyConfig::Encrypted { config, .. } => {
CustodyConfig::Encrypted(config)
}
},
};
let custody = match custody {
x @ CustodyConfig::ViewOnly => x,
x @ CustodyConfig::Encrypted(_) => x,
CustodyConfig::SoftKms(spend_key) => {
let password = ActualTerminal.get_confirmed_password().await?;
CustodyConfig::Encrypted(penumbra_custody::encrypted::Config::create(
&password,
penumbra_custody::encrypted::InnerConfig::SoftKms(spend_key.into()),
)?)
}
CustodyConfig::Threshold(c) => {
let password = ActualTerminal.get_confirmed_password().await?;
CustodyConfig::Encrypted(penumbra_custody::encrypted::Config::create(
&password,
penumbra_custody::encrypted::InnerConfig::Threshold(c),
)?)
}
};
(fvk, custody)
}
(_, InitSubCmd::ReEncrypt, false) => {
anyhow::bail!("re-encrypt requires existing config to exist",);
}
(InitType::SpendKey, _, true) => {
anyhow::bail!(
"home directory {:?} is not empty; refusing to initialize",
home_dir
);
}
(InitType::GovernanceKey, _, true) => {
anyhow::bail!(
"governance key already exists in config file at {:?}; refusing to overwrite it",
home_dir
);
}
};

let config = if let InitType::SpendKey = init_type {
Expand All @@ -336,6 +406,10 @@ impl InitCmd {
let governance_custody = match custody {
CustodyConfig::SoftKms(config) => GovernanceCustodyConfig::SoftKms(config),
CustodyConfig::Threshold(config) => GovernanceCustodyConfig::Threshold(config),
CustodyConfig::Encrypted(config) => GovernanceCustodyConfig::Encrypted {
fvk: full_viewing_key,
config,
},
_ => unreachable!("governance keys can't be initialized in view-only mode"),
};
config.governance_custody = Some(governance_custody);
Expand Down
23 changes: 17 additions & 6 deletions crates/bin/pcli/src/command/threshold.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use penumbra_custody::threshold::Terminal;

use crate::{
config::{CustodyConfig, GovernanceCustodyConfig},
Expand All @@ -21,19 +22,29 @@ impl ThresholdCmd {

#[tracing::instrument(skip(self, app))]
pub async fn exec(&self, app: &mut App) -> Result<()> {
let config = match &app.config.custody {
let config = match app.config.custody.clone() {
CustodyConfig::Threshold(config) => Some(config),
CustodyConfig::Encrypted(config) => {
let password = ActualTerminal.get_password().await?;
config.convert_to_threshold(&password)?
}
_ => None, // If not threshold, we can't sign using threshold config
};
let governance_config = match &app.config.governance_custody {
Some(GovernanceCustodyConfig::Threshold(governance_config)) => Some(governance_config),
None => config, // If no governance config, use regular one
_ => None, // If not threshold, we can't sign using governance config
Some(GovernanceCustodyConfig::Threshold(governance_config)) => {
Some(governance_config.clone())
}
None => config.clone(), // If no governance config, use regular one
_ => None, // If not threshold, we can't sign using governance config
};
match self {
ThresholdCmd::Sign => {
penumbra_custody::threshold::follow(config, governance_config, &ActualTerminal)
.await
penumbra_custody::threshold::follow(
config.as_ref(),
governance_config.as_ref(),
&ActualTerminal,
)
.await
}
}
}
Expand Down
13 changes: 12 additions & 1 deletion crates/bin/pcli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use url::Url;

use penumbra_custody::{soft_kms::Config as SoftKmsConfig, threshold::Config as ThresholdConfig};
use penumbra_custody::{
encrypted::Config as EncryptedConfig, soft_kms::Config as SoftKmsConfig,
threshold::Config as ThresholdConfig,
};
use penumbra_keys::FullViewingKey;

/// Configuration data for `pcli`.
Expand Down Expand Up @@ -50,6 +53,7 @@ impl PcliConfig {
spend_key.full_viewing_key()
}
Some(GovernanceCustodyConfig::Threshold(threshold_config)) => threshold_config.fvk(),
Some(GovernanceCustodyConfig::Encrypted { fvk, .. }) => fvk,
None => &self.full_viewing_key,
};
GovernanceKey(fvk.spend_verification_key().clone())
Expand All @@ -67,6 +71,8 @@ pub enum CustodyConfig {
SoftKms(SoftKmsConfig),
/// A manual threshold custody service.
Threshold(ThresholdConfig),
/// An encrypted custody service.
Encrypted(EncryptedConfig),
}

/// The governance custody backend to use.
Expand All @@ -78,6 +84,11 @@ pub enum GovernanceCustodyConfig {
SoftKms(SoftKmsConfig),
/// A manual threshold custody service.
Threshold(ThresholdConfig),
/// An encrypted custody service.
Encrypted {
fvk: FullViewingKey,
config: EncryptedConfig,
},
}

impl Default for CustodyConfig {
Expand Down
14 changes: 14 additions & 0 deletions crates/bin/pcli/src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ impl Opt {
let custody_svc = CustodyServiceServer::new(threshold_kms);
CustodyServiceClient::new(box_grpc_svc::local(custody_svc))
}
CustodyConfig::Encrypted(config) => {
tracing::info!("using encrypted custody service");
let encrypted_kms =
penumbra_custody::encrypted::Encrypted::new(config.clone(), ActualTerminal);
let custody_svc = CustodyServiceServer::new(encrypted_kms);
CustodyServiceClient::new(box_grpc_svc::local(custody_svc))
}
};

// Build the governance custody service...
Expand All @@ -98,6 +105,13 @@ impl Opt {
let custody_svc = CustodyServiceServer::new(threshold_kms);
CustodyServiceClient::new(box_grpc_svc::local(custody_svc))
}
GovernanceCustodyConfig::Encrypted { config, .. } => {
tracing::info!("using separate encrypted custody service for validator voting");
let encrypted_kms =
penumbra_custody::encrypted::Encrypted::new(config.clone(), ActualTerminal);
let custody_svc = CustodyServiceServer::new(encrypted_kms);
CustodyServiceClient::new(box_grpc_svc::local(custody_svc))
}
},
None => custody.clone(), // If no separate custody for validator voting, use the same one
};
Expand Down
Loading

0 comments on commit 5dcc768

Please sign in to comment.