diff --git a/src/commands/yubihsm/keys/generate.rs b/src/commands/yubihsm/keys/generate.rs index 1cd8348..d0b8404 100644 --- a/src/commands/yubihsm/keys/generate.rs +++ b/src/commands/yubihsm/keys/generate.rs @@ -1,19 +1,11 @@ use abscissa::Callable; use std::process; +use super::*; use signatory::ed25519; use tendermint::public_keys::ConsensusKey; use yubihsm; -/// Default key type to generate -pub const DEFAULT_KEY_TYPE: &str = "ed25519"; - -/// Default YubiHSM2 domain (internal partitioning) -pub const DEFAULT_DOMAINS: yubihsm::Domain = yubihsm::Domain::DOM1; - -/// Default YubiHSM2 permissions for generated keys -pub const DEFAULT_CAPABILITIES: yubihsm::Capability = yubihsm::Capability::ASYMMETRIC_SIGN_EDDSA; - /// The `yubihsm keys generate` subcommand #[derive(Debug, Default, Options)] pub struct GenerateCommand { diff --git a/src/commands/yubihsm/keys/import.rs b/src/commands/yubihsm/keys/import.rs new file mode 100644 index 0000000..7591f32 --- /dev/null +++ b/src/commands/yubihsm/keys/import.rs @@ -0,0 +1,125 @@ +extern crate serde_json; + +use super::*; + +use std::fs::File; +use std::io::prelude::*; +use std::process; +use std::str; + +use abscissa::Callable; +use signatory::ed25519; +use signatory::ed25519::Seed; +use subtle_encoding::base64; +use tendermint::public_keys::ConsensusKey; +use yubihsm; + +use serde_json::Value; + +/// The `yubihsm keys import` subcommand +#[derive(Debug, Default, Options)] +pub struct ImportCommand { + /// Path to the validator configuration file + #[options(short = "p", long = "path")] + pub path: Option, + + /// Type of key to import (default 'ed25519') + #[options(short = "t")] + pub key_type: Option, + + /// Label for imported key + #[options(short = "l", long = "label")] + pub label: Option, + + /// Key ID for imported key + #[options(free)] + key_id: Option, +} + +impl Callable for ImportCommand { + fn call(&self) { + if self.path.is_none() { + status_err!("must provide a valid path to priv_validator.json"); + process::exit(1); + } + + if self.key_id.is_none() { + status_err!("must provide a unique key_id"); + process::exit(1); + } + + match &self.key_type { + Some(ref key_type) => if key_type != DEFAULT_KEY_TYPE { + status_err!( + "only supported key type is: ed25519 (given: \"{}\")", + key_type + ); + process::exit(1); + }, + None => (), + } + + if let Some(path) = &self.path { + let mut f = File::open(path).unwrap_or_else(|e| { + status_err!("couldn't open validator config file {}: {}", path, e); + process::exit(1); + }); + + let mut contents = Vec::new(); + f.read_to_end(&mut contents).unwrap_or_else(|e| { + status_err!("couldn't read validator config file {}: {}", path, e); + process::exit(1); + }); + let v: Value = serde_json::from_slice(&contents).unwrap(); + let s = v["priv_key"]["value"].as_str().unwrap_or_else(|| { + status_err!("couldn't read validator private key from config: {}", path); + process::exit(1); + }); + + let key = base64::decode(s).unwrap_or_else(|e| { + status_err!("couldn't decode validator private key from config: {}", e); + process::exit(1); + }); + Seed::from_keypair(&key).unwrap_or_else(|e| { + status_err!("invalid key in validator config: {}", e); + process::exit(1); + }); + let label = + yubihsm::ObjectLabel::from(self.label.as_ref().map(|l| l.as_ref()).unwrap_or("")); + + let mut hsm = yubihsm::get_hsm_client(); + + if let Err(e) = hsm.put_asymmetric_key( + self.key_id.unwrap(), + label, + DEFAULT_DOMAINS, + DEFAULT_CAPABILITIES, + yubihsm::AsymmetricAlg::Ed25519, + key, + ) { + status_err!("couldn't import key #{}: {}", self.key_id.unwrap(), e); + process::exit(1); + } + + let public_key = ed25519::PublicKey::from_bytes( + hsm.get_pubkey(self.key_id.unwrap()).unwrap_or_else(|e| { + status_err!( + "couldn't get public key for key #{}: {}", + self.key_id.unwrap(), + e + ); + process::exit(1); + }), + ).unwrap(); + + status_ok!( + "Imported", + "key #{}: {}", + self.key_id.unwrap(), + ConsensusKey::from(public_key) + ); + } + } +} + +impl_command!(ImportCommand); diff --git a/src/commands/yubihsm/keys/mod.rs b/src/commands/yubihsm/keys/mod.rs index fd28173..b71ce2a 100644 --- a/src/commands/yubihsm/keys/mod.rs +++ b/src/commands/yubihsm/keys/mod.rs @@ -1,10 +1,23 @@ mod generate; mod help; +mod import; mod list; use abscissa::Callable; +use yubihsm; -use self::{generate::GenerateCommand, help::HelpCommand, list::ListCommand}; +use self::{ + generate::GenerateCommand, help::HelpCommand, import::ImportCommand, list::ListCommand, +}; + +/// Default key type to generate +pub const DEFAULT_KEY_TYPE: &str = "ed25519"; + +/// Default YubiHSM2 domain (internal partitioning) +pub const DEFAULT_DOMAINS: yubihsm::Domain = yubihsm::Domain::DOM1; + +/// Default YubiHSM2 permissions for generated keys +pub const DEFAULT_CAPABILITIES: yubihsm::Capability = yubihsm::Capability::ASYMMETRIC_SIGN_EDDSA; /// The `yubihsm keys` subcommand #[derive(Debug, Options)] @@ -15,6 +28,9 @@ pub enum KeysCommand { #[options(help = "show help for the 'yubihsm keys' subcommand")] Help(HelpCommand), + #[options(help = "import validator signing key for the 'yubihsm keys' subcommand")] + Import(ImportCommand), + #[options(help = "list all suitable Ed25519 keys in the HSM")] List(ListCommand), } @@ -37,6 +53,7 @@ impl Callable for KeysCommand { match self { KeysCommand::Generate(generate) => generate.call(), KeysCommand::Help(help) => help.call(), + KeysCommand::Import(import) => import.call(), KeysCommand::List(list) => list.call(), } } diff --git a/tests/cli/yubihsm/keys/import.rs b/tests/cli/yubihsm/keys/import.rs new file mode 100644 index 0000000..f228f2d --- /dev/null +++ b/tests/cli/yubihsm/keys/import.rs @@ -0,0 +1,2 @@ +//! Integration tests for the `yubihsm keys import` subcommand +// TODO: `yubihsm keys import` tests