diff --git a/Cargo.lock b/Cargo.lock index cf63d5c..966d6c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,6 +1584,24 @@ dependencies = [ "rand 0.6.5", ] +[[package]] +name = "secp256k1" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2932dc07acd2066ff2e3921a4419606b220ba6cd03a9935123856cc534877056" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab2c26f0d3552a0f12e639ae8a64afc2e3db9c52fe32f5fc6c289d38519f220" +dependencies = [ + "cc", +] + [[package]] name = "secrecy" version = "0.6.0" @@ -1730,7 +1748,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98496421006efcfa95f6a9b8822ae9d798447147c07e5d3e175fc047740c3e4" dependencies = [ - "secp256k1", + "secp256k1 0.15.5", "signatory 0.18.0", "signature", ] @@ -2507,6 +2525,7 @@ dependencies = [ "pbkdf2", "ring", "rusb", + "secp256k1 0.17.2", "serde", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml index 903a89b..95f1123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,16 +42,17 @@ tendermint = "= 0.12.0-rc0" thiserror = "1" wait-timeout = "0.2" x25519-dalek = "0.6" -yubihsm = { version = "0.31", features = ["setup", "usb"], optional = true } zeroize = "1" +[dependencies.yubihsm] +version = "0.31" +optional = true +features = ["secp256k1", "setup", "usb"] + [dev-dependencies] +abscissa_core = { version = "0.5", features = ["testing"] } tempfile = "3" -[dev-dependencies.abscissa_core] -version = "0.5" -features = ["testing"] - [features] default = [] softsign = [] diff --git a/src/chain/registry.rs b/src/chain/registry.rs index b74cbee..2247a9d 100644 --- a/src/chain/registry.rs +++ b/src/chain/registry.rs @@ -3,7 +3,6 @@ use super::{Chain, Guard, Id}; use crate::{ error::{Error, ErrorKind::*}, - keyring, prelude::*, }; use once_cell::sync::Lazy; @@ -17,25 +16,6 @@ pub static REGISTRY: Lazy = Lazy::new(GlobalRegistry::default); pub struct Registry(BTreeMap); impl Registry { - /// Add a key to a keyring for a chain stored in the registry - pub fn add_to_keyring( - &mut self, - chain_id: &Id, - signer: keyring::ed25519::Signer, - ) -> Result<(), Error> { - // TODO(tarcieri): - let chain = self.0.get_mut(chain_id).ok_or_else(|| { - format_err!( - InvalidKey, - "can't add signer {} to unregistered chain: {}", - signer.provider(), - chain_id - ) - })?; - - chain.keyring.add(signer) - } - /// Register a `Chain` with the registry pub fn register_chain(&mut self, chain: Chain) -> Result<(), Error> { let chain_id = chain.id; @@ -52,6 +32,18 @@ impl Registry { pub fn get_chain(&self, chain_id: &Id) -> Option<&Chain> { self.0.get(chain_id) } + + /// Get a mutable reference to the given chain + pub(crate) fn get_chain_mut(&mut self, chain_id: &Id) -> Result<&mut Chain, Error> { + self.0.get_mut(chain_id).ok_or_else(|| { + format_err!( + InvalidKey, + "can't add signer to unregistered chain: {}", + chain_id + ) + .into() + }) + } } /// Global registry of blockchain networks known to the KMS diff --git a/src/config/provider/softsign.rs b/src/config/provider/softsign.rs index 103d70a..0ad1d21 100644 --- a/src/config/provider/softsign.rs +++ b/src/config/provider/softsign.rs @@ -7,6 +7,7 @@ use crate::{ }; use serde::Deserialize; use std::{ + fmt::{self, Display}, path::{Path, PathBuf}, str::FromStr, }; @@ -54,6 +55,17 @@ pub enum KeyFormat { Json, } +impl KeyFormat { + /// Get a string reference describing this key format + pub fn as_str(&self) -> &str { + match self { + KeyFormat::Raw => "raw", + KeyFormat::Base64 => "base64", + KeyFormat::Json => "json", + } + } +} + impl Default for KeyFormat { fn default() -> Self { // TODO(tarcieri): change to Base64 @@ -61,6 +73,12 @@ impl Default for KeyFormat { } } +impl Display for KeyFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + impl FromStr for KeyFormat { type Err = Error; diff --git a/src/keyring.rs b/src/keyring.rs index ef776a0..0258c3a 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -1,10 +1,10 @@ //! Signing keyring. Presently specialized for Ed25519. +pub mod ecdsa; pub mod ed25519; pub mod format; pub mod providers; -use self::ed25519::Signer; pub use self::{format::Format, providers::SigningProvider}; use crate::{ chain, @@ -12,7 +12,7 @@ use crate::{ error::{Error, ErrorKind::*}, prelude::*, }; -use std::collections::BTreeMap; +use std::collections::BTreeMap as Map; use subtle_encoding; use tendermint::TendermintKey; @@ -21,8 +21,11 @@ pub type SecretKeyEncoding = subtle_encoding::Base64; /// Signing keyring pub struct KeyRing { - /// Keys in the keyring - keys: BTreeMap, + /// ECDSA (secp256k1) keys in the keyring + ecdsa_keys: Map, + + /// Ed25519 keys in the keyring + ed25519_keys: Map, /// Formatting configuration when displaying keys (e.g. bech32) format: Format, @@ -32,14 +35,44 @@ impl KeyRing { /// Create a new keyring pub fn new(format: Format) -> Self { Self { - keys: BTreeMap::new(), + ecdsa_keys: Map::new(), + ed25519_keys: Map::new(), format, } } - /// Add a key to the keyring, returning an error if we already have a - /// signer registered for the given public key - pub fn add(&mut self, signer: Signer) -> Result<(), Error> { + /// Add an ECDSA key to the keyring, returning an error if we already + /// have a signer registered for the given public key. + pub fn add_ecdsa(&mut self, signer: ecdsa::Signer) -> Result<(), Error> { + let provider = signer.provider(); + let public_key = signer.public_key(); + let public_key_serialized = self.format.serialize(public_key); + let key_type = match public_key { + TendermintKey::AccountKey(_) => "account", + TendermintKey::ConsensusKey(_) => "consensus", + }; + + info!( + "[keyring:{}] added ECDSA {} key {}", + provider, key_type, public_key_serialized + ); + + if let Some(other) = self.ecdsa_keys.insert(public_key, signer) { + fail!( + InvalidKey, + "[keyring:{}] duplicate ECDSA key {} already registered as {}", + provider, + public_key_serialized, + other.provider(), + ) + } else { + Ok(()) + } + } + + /// Add an Ed25519 key to the keyring, returning an error if we already + /// have a signer registered for the given public key. + pub fn add_ed25519(&mut self, signer: ed25519::Signer) -> Result<(), Error> { let provider = signer.provider(); let public_key = signer.public_key(); let public_key_serialized = self.format.serialize(public_key); @@ -49,14 +82,14 @@ impl KeyRing { }; info!( - "[keyring:{}] added {} key {}", + "[keyring:{}] added Ed25519 {} key {}", provider, key_type, public_key_serialized ); - if let Some(other) = self.keys.insert(public_key, signer) { + if let Some(other) = self.ed25519_keys.insert(public_key, signer) { fail!( InvalidKey, - "[keyring:{}] duplicate key {} already registered as {}", + "[keyring:{}] duplicate Ed25519 key {} already registered as {}", provider, public_key_serialized, other.provider(), @@ -66,9 +99,9 @@ impl KeyRing { } } - /// Get the default public key for this keyring - pub fn default_pubkey(&self) -> Result { - let mut keys = self.keys.keys(); + /// Get the default Ed25519 public key for this keyring + pub fn default_ed25519_pubkey(&self) -> Result { + let mut keys = self.ed25519_keys.keys(); if keys.len() == 1 { Ok(*keys.next().unwrap()) @@ -77,19 +110,45 @@ impl KeyRing { } } - /// Sign a message using the secret key associated with the given public key - /// (if it is in our keyring) + /// Sign a message using the ECDSA secret key associated with the given + /// public key (if it is in our keyring) + pub fn sign_ecdsa( + &self, + public_key: Option<&TendermintKey>, + msg: &[u8], + ) -> Result { + let signer = match public_key { + Some(public_key) => self.ecdsa_keys.get(public_key).ok_or_else(|| { + format_err!(InvalidKey, "not in keyring: {}", public_key.to_bech32("")) + })?, + None => { + let mut vals = self.ecdsa_keys.values(); + + if vals.len() > 1 { + fail!(SigningError, "expected only one key in keyring"); + } else { + vals.next() + .ok_or_else(|| format_err!(InvalidKey, "keyring is empty"))? + } + } + }; + + signer.sign(msg) + } + + /// Sign a message using the Ed25519 secret key associated with the given + /// public key (if it is in our keyring) pub fn sign_ed25519( &self, public_key: Option<&TendermintKey>, msg: &[u8], ) -> Result { let signer = match public_key { - Some(public_key) => self.keys.get(public_key).ok_or_else(|| { + Some(public_key) => self.ed25519_keys.get(public_key).ok_or_else(|| { format_err!(InvalidKey, "not in keyring: {}", public_key.to_bech32("")) })?, None => { - let mut vals = self.keys.values(); + let mut vals = self.ed25519_keys.values(); if vals.len() > 1 { fail!(SigningError, "expected only one key in keyring"); diff --git a/src/keyring/ecdsa.rs b/src/keyring/ecdsa.rs new file mode 100644 index 0000000..ca547ef --- /dev/null +++ b/src/keyring/ecdsa.rs @@ -0,0 +1,9 @@ +//! ECDSA (secp256k1) signing keys + +pub use signatory::ecdsa::curve::secp256k1::{FixedSignature as Signature, PublicKey, SecretKey}; + +pub mod signer; +#[cfg(feature = "softsign")] +pub mod softsign; + +pub use self::signer::Signer; diff --git a/src/keyring/ecdsa/signer.rs b/src/keyring/ecdsa/signer.rs new file mode 100644 index 0000000..51d244b --- /dev/null +++ b/src/keyring/ecdsa/signer.rs @@ -0,0 +1,57 @@ +//! ECDSA (secp256k1) signer + +use super::Signature; +use crate::{ + error::{Error, ErrorKind::*}, + keyring::SigningProvider, + prelude::*, +}; +use signatory::signature; +use std::sync::Arc; +use tendermint::TendermintKey; + +/// Trait object wrapper for ECDSA signers +#[derive(Clone)] +pub struct Signer { + /// Provider for this signer + provider: SigningProvider, + + /// Tendermint public key + public_key: TendermintKey, + + /// Signer trait object + signer: Arc + Send + Sync>>, +} + +impl Signer { + /// Create a new ECDSA signer + pub fn new( + provider: SigningProvider, + public_key: TendermintKey, + signer: Box + Send + Sync>, + ) -> Self { + Self { + provider, + public_key, + signer: Arc::new(signer), + } + } + + /// Get the Tendermint public key for this signer + pub fn public_key(&self) -> TendermintKey { + self.public_key + } + + /// Get the provider for this signer + pub fn provider(&self) -> SigningProvider { + self.provider + } + + /// Sign the given message using this signer + pub fn sign(&self, msg: &[u8]) -> Result { + Ok(self + .signer + .try_sign(msg) + .map_err(|e| format_err!(SigningError, "{}", e))?) + } +} diff --git a/src/keyring/ecdsa/softsign.rs b/src/keyring/ecdsa/softsign.rs new file mode 100644 index 0000000..d70dcb8 --- /dev/null +++ b/src/keyring/ecdsa/softsign.rs @@ -0,0 +1,96 @@ +//! libsecp256k1 software-based signer +//! +//! This is mainly intended for testing/CI. Ideally validators will use HSMs. + +use super::{SecretKey, Signer}; +use crate::{ + chain, + config::provider::softsign::{KeyFormat, SoftsignConfig}, + error::{Error, ErrorKind::*}, + keyring::SigningProvider, + prelude::*, +}; +use signatory::public_key::PublicKeyed; +use signatory_secp256k1::EcdsaSigner; +use std::{convert::TryFrom, fs}; +use subtle_encoding::base64; +use tendermint::TendermintKey; +use zeroize::Zeroizing; + +/// Create software-backed ECDSA signer objects from the given configuration +pub fn init(chain_registry: &mut chain::Registry, configs: &[SoftsignConfig]) -> Result<(), Error> { + if configs.is_empty() { + return Ok(()); + } + + if configs.len() != 1 { + fail!( + ConfigError, + "expected one [providers.softsign] in config, found: {}", + configs.len() + ); + } + + let config = &configs[0]; + let key_format = config.key_format.as_ref().cloned().unwrap_or_default(); + + let secret_key: SecretKey = match key_format { + KeyFormat::Base64 => { + let secret_key_base64 = + Zeroizing::new(fs::read_to_string(&config.path).map_err(|e| { + format_err!( + ConfigError, + "couldn't read key from {}: {}", + &config.path.as_ref().display(), + e + ) + })?); + + // TODO(tarcieri): constant-time string trimming + let secret_key_bytes = Zeroizing::new( + base64::decode(secret_key_base64.trim_end().as_bytes()).map_err(|e| { + format_err!( + ConfigError, + "can't decode key from {}: {}", + config.path.as_ref().display(), + e + ) + })?, + ); + + SecretKey::try_from(secret_key_bytes.as_ref()).map_err(|e| { + format_err!( + ConfigError, + "can't decode key from {}: {}", + config.path.as_ref().display(), + e + ) + })? + } + other => fail!( + ConfigError, + "unsupported encoding `{}` for ECDSA key: {}", + other, + config.path.as_ref().display() + ), + }; + + let provider = EcdsaSigner::from(&secret_key); + let public_key = provider.public_key().map_err(|_| Error::from(InvalidKey))?; + let consensus_pubkey = TendermintKey::ConsensusKey(public_key.into()); + + let signer = Signer::new( + SigningProvider::SoftSign, + consensus_pubkey, + Box::new(provider), + ); + + for chain_id in &config.chain_ids { + chain_registry + .get_chain_mut(chain_id)? + .keyring + .add_ecdsa(signer.clone())?; + } + + Ok(()) +} diff --git a/src/keyring/ed25519/ledgertm.rs b/src/keyring/ed25519/ledgertm.rs index 4ad550a..f10633b 100644 --- a/src/keyring/ed25519/ledgertm.rs +++ b/src/keyring/ed25519/ledgertm.rs @@ -41,7 +41,10 @@ pub fn init( ); for chain_id in &ledgertm_configs[0].chain_ids { - chain_registry.add_to_keyring(chain_id, signer.clone())?; + chain_registry + .get_chain_mut(chain_id)? + .keyring + .add_ed25519(signer.clone())?; } Ok(()) diff --git a/src/keyring/ed25519/signer.rs b/src/keyring/ed25519/signer.rs index 639be2c..c104148 100644 --- a/src/keyring/ed25519/signer.rs +++ b/src/keyring/ed25519/signer.rs @@ -1,4 +1,4 @@ -//! Wrapper for Ed25519 signers +//! Ed25519 signer use crate::{ error::{Error, ErrorKind::*}, diff --git a/src/keyring/ed25519/softsign.rs b/src/keyring/ed25519/softsign.rs index 24e42d4..087f54d 100644 --- a/src/keyring/ed25519/softsign.rs +++ b/src/keyring/ed25519/softsign.rs @@ -106,7 +106,10 @@ pub fn init(chain_registry: &mut chain::Registry, configs: &[SoftsignConfig]) -> ); for chain_id in &config.chain_ids { - chain_registry.add_to_keyring(chain_id, signer.clone())?; + chain_registry + .get_chain_mut(chain_id)? + .keyring + .add_ed25519(signer.clone())?; } Ok(()) diff --git a/src/keyring/ed25519/yubihsm.rs b/src/keyring/ed25519/yubihsm.rs index 0f6fc06..e0bfcf1 100644 --- a/src/keyring/ed25519/yubihsm.rs +++ b/src/keyring/ed25519/yubihsm.rs @@ -51,7 +51,10 @@ pub fn init( let signer = Signer::new(SigningProvider::Yubihsm, consensus_pubkey, Box::new(signer)); for chain_id in &config.chain_ids { - chain_registry.add_to_keyring(chain_id, signer.clone())?; + chain_registry + .get_chain_mut(chain_id)? + .keyring + .add_ed25519(signer.clone())?; } } diff --git a/src/session.rs b/src/session.rs index 67cd202..ea7a0fb 100644 --- a/src/session.rs +++ b/src/session.rs @@ -194,7 +194,7 @@ impl Session { let chain = registry.get_chain(&self.config.chain_id).unwrap(); Ok(Response::PublicKey(PubKeyResponse::from( - *chain.keyring.default_pubkey()?, + *chain.keyring.default_ed25519_pubkey()?, ))) }