diff --git a/Cargo.lock b/Cargo.lock index a6f89982..0823d845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2074,6 +2074,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "tendermint-p2p" +version = "0.17.0-rc3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9c878e0b802847b36db51cb69f6d3ad0d7ff1cb49254596adbec010e760e2a" +dependencies = [ + "chacha20poly1305", + "ed25519-dalek", + "eyre", + "hkdf", + "merlin", + "prost", + "prost-amino", + "prost-amino-derive", + "rand_core", + "sha2", + "subtle", + "subtle-encoding", + "tendermint", + "tendermint-proto", + "thiserror", + "x25519-dalek", + "zeroize", +] + [[package]] name = "tendermint-proto" version = "0.17.0-rc3" @@ -2202,7 +2227,6 @@ dependencies = [ "abscissa_tokio", "byteorder", "bytes", - "chacha20poly1305", "chrono", "ed25519-dalek", "getrandom", @@ -2213,7 +2237,6 @@ dependencies = [ "hyper-rustls", "k256", "ledger", - "merlin", "once_cell", "prost", "prost-amino", @@ -2231,11 +2254,11 @@ dependencies = [ "subtle-encoding", "tempfile", "tendermint", + "tendermint-p2p", "tendermint-proto", "tendermint-rpc", "thiserror", "wait-timeout", - "x25519-dalek", "yubihsm", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 0cebcd36..bcdc6c70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ edition = "2018" abscissa_core = "0.5" abscissa_tokio = { version = "0.5", optional = true } bytes = "0.5" -chacha20poly1305 = "0.7" chrono = "0.4" ed25519-dalek = "1" getrandom = "0.1" @@ -25,7 +24,6 @@ hyper = { version = "0.13", optional = true } hyper-rustls = { version = "0.21", optional = true } k256 = { version = "0.5", features = ["ecdsa", "sha256"] } ledger = { version = "0.2", optional = true } -merlin = "2" once_cell = "1.5" prost = "0.6" prost-amino = "0.6" @@ -44,9 +42,9 @@ tempfile = "3" tendermint = { version = "=0.17.0-rc3", features = ["secp256k1"] } tendermint-rpc = { version = "=0.17.0-rc3", optional = true, features = ["http-client"] } tendermint-proto = "=0.17.0-rc3" +tendermint-p2p = { version = "=0.17.0-rc3", features = ["amino"] } thiserror = "1" wait-timeout = "0.2" -x25519-dalek = "1.1" yubihsm = { version = "0.35", features = ["secp256k1", "setup", "usb"], optional = true } zeroize = "1" diff --git a/src/commands/init.rs b/src/commands/init.rs index e4297672..ce84b81e 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -4,7 +4,7 @@ pub mod config_builder; pub mod networks; use self::{config_builder::ConfigBuilder, networks::Network}; -use crate::{config::CONFIG_FILE_NAME, connection::secret_connection, prelude::*}; +use crate::{config::CONFIG_FILE_NAME, key_utils, prelude::*}; use abscissa_core::{Command, Options, Runnable}; use std::{ fs, @@ -106,7 +106,7 @@ impl Runnable for InitCommand { status_ok!("Generated", "KMS configuration: {}", config_path.display()); let secret_connection_key = secrets_dir.join(SECRET_CONNECTION_KEY); - secret_connection::generate_key(&secret_connection_key).unwrap_or_else(|e| { + key_utils::generate_key(&secret_connection_key).unwrap_or_else(|e| { abort!( "couldn't generate `{}`: {}", secret_connection_key.display(), diff --git a/src/config/validator.rs b/src/config/validator.rs index efcf7dca..b528da9d 100644 --- a/src/config/validator.rs +++ b/src/config/validator.rs @@ -1,9 +1,9 @@ //! Validator configuration -use crate::connection::secret_connection; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use tendermint::{chain, net}; +use tendermint_p2p::secret_connection; /// Validator configuration #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/src/connection.rs b/src/connection.rs index 1d5cd169..90a6e48a 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,12 +1,14 @@ //! Connections to a validator (TCP or Unix socket) -pub mod secret_connection; +use std::io; + +use tendermint_p2p::secret_connection::SecretConnection; + +use self::unix::UnixConnection; + pub mod tcp; pub mod unix; -use self::{secret_connection::SecretConnection, unix::UnixConnection}; -use std::io; - /// Connections to a validator pub trait Connection: io::Read + io::Write + Sync + Send {} diff --git a/src/connection/secret_connection.rs b/src/connection/secret_connection.rs deleted file mode 100644 index 443e247f..00000000 --- a/src/connection/secret_connection.rs +++ /dev/null @@ -1,430 +0,0 @@ -//! `SecretConnection`: Transport layer encryption for Tendermint P2P connections. - -mod amino_types; -mod kdf; -mod nonce; -mod protocol; -mod public_key; - -pub use self::{ - amino_types::AuthSigMessage, kdf::Kdf, nonce::Nonce, protocol::Version, public_key::PublicKey, -}; -use crate::{ - error::{Error, ErrorKind}, - key_utils, -}; -use chacha20poly1305::{ - aead::{generic_array::GenericArray, AeadInPlace, NewAead}, - ChaCha20Poly1305, -}; -use ed25519_dalek::{self as ed25519, Signer, Verifier, SECRET_KEY_LENGTH}; -use merlin::Transcript; -use rand_core::{OsRng, RngCore}; -use std::{ - cmp, - convert::{TryFrom, TryInto}, - io::{self, Read, Write}, - marker::{Send, Sync}, - path::Path, - slice, -}; -use subtle::ConstantTimeEq; -use tendermint_proto as proto; -use x25519_dalek::{EphemeralSecret, PublicKey as EphemeralPublic}; -use zeroize::Zeroizing; - -/// Size of the MAC tag -pub const TAG_SIZE: usize = 16; - -/// Maximum size of a message -pub const DATA_MAX_SIZE: usize = 1024; - -/// 4 + 1024 == 1028 total frame size -const DATA_LEN_SIZE: usize = 4; -const TOTAL_FRAME_SIZE: usize = DATA_MAX_SIZE + DATA_LEN_SIZE; - -/// Generate a Secret Connection key at the given path -pub fn generate_key(path: impl AsRef) -> Result<(), Error> { - let mut secret_key = Zeroizing::new([0u8; SECRET_KEY_LENGTH]); - OsRng.fill_bytes(&mut *secret_key); - key_utils::write_base64_secret(path, &*secret_key) -} - -/// Encrypted connection between peers in a Tendermint network -pub struct SecretConnection { - io_handler: IoHandler, - protocol_version: Version, - recv_nonce: Nonce, - send_nonce: Nonce, - recv_cipher: ChaCha20Poly1305, - send_cipher: ChaCha20Poly1305, - remote_pubkey: Option, - recv_buffer: Vec, -} - -impl SecretConnection { - /// Returns authenticated remote pubkey - pub fn remote_pubkey(&self) -> PublicKey { - self.remote_pubkey.expect("remote_pubkey uninitialized") - } - - /// Performs handshake and returns a new authenticated SecretConnection. - pub fn new( - mut io_handler: IoHandler, - local_privkey: &ed25519::Keypair, - protocol_version: Version, - ) -> Result, Error> { - let local_pubkey = PublicKey::from(local_privkey); - - // Generate ephemeral keys for perfect forward secrecy. - let (local_eph_pubkey, local_eph_privkey) = gen_eph_keys(); - - // Write local ephemeral pubkey and receive one too. - // NOTE: every 32-byte string is accepted as a Curve25519 public key - // (see DJB's Curve25519 paper: http://cr.yp.to/ecdh/curve25519-20060209.pdf) - let remote_eph_pubkey = - share_eph_pubkey(&mut io_handler, &local_eph_pubkey, protocol_version)?; - - // Compute common shared secret. - let shared_secret = EphemeralSecret::diffie_hellman(local_eph_privkey, &remote_eph_pubkey); - - let mut transcript = Transcript::new(b"TENDERMINT_SECRET_CONNECTION_TRANSCRIPT_HASH"); - - // Reject all-zero outputs from X25519 (i.e. from low-order points) - // - // See the following for information on potential attacks this check - // aids in mitigating: - // - // - https://github.com/tendermint/kms/issues/142 - // - https://eprint.iacr.org/2019/526.pdf - if shared_secret.as_bytes().ct_eq(&[0x00; 32]).unwrap_u8() == 1 { - return Err(ErrorKind::InvalidKey.into()); - } - - // Sort by lexical order. - let local_eph_pubkey_bytes = *local_eph_pubkey.as_bytes(); - let (low_eph_pubkey_bytes, high_eph_pubkey_bytes) = - sort32(local_eph_pubkey_bytes, *remote_eph_pubkey.as_bytes()); - - transcript.append_message(b"EPHEMERAL_LOWER_PUBLIC_KEY", &low_eph_pubkey_bytes); - transcript.append_message(b"EPHEMERAL_UPPER_PUBLIC_KEY", &high_eph_pubkey_bytes); - transcript.append_message(b"DH_SECRET", shared_secret.as_bytes()); - - // Check if the local ephemeral public key - // was the least, lexicographically sorted. - let loc_is_least = local_eph_pubkey_bytes == low_eph_pubkey_bytes; - - let kdf = Kdf::derive_secrets_and_challenge(shared_secret.as_bytes(), loc_is_least); - - // Construct SecretConnection. - let mut sc = SecretConnection { - io_handler, - protocol_version, - recv_buffer: vec![], - recv_nonce: Nonce::default(), - send_nonce: Nonce::default(), - recv_cipher: ChaCha20Poly1305::new(&kdf.recv_secret.into()), - send_cipher: ChaCha20Poly1305::new(&kdf.send_secret.into()), - remote_pubkey: None, - }; - - let mut sc_mac: [u8; 32] = [0; 32]; - - transcript.challenge_bytes(b"SECRET_CONNECTION_MAC", &mut sc_mac); - - // Sign the challenge bytes for authentication. - let local_signature = if protocol_version.has_transcript() { - sign_challenge(&sc_mac, local_privkey)? - } else { - sign_challenge(&kdf.challenge, local_privkey)? - }; - - // Share (in secret) each other's pubkey & challenge signature - let auth_sig_msg = match local_pubkey { - PublicKey::Ed25519(ref pk) => share_auth_signature(&mut sc, pk, &local_signature)?, - }; - - let remote_pubkey = auth_sig_msg - .pub_key - .and_then(|pk| match pk.sum? { - proto::crypto::public_key::Sum::Ed25519(ref bytes) => { - ed25519::PublicKey::from_bytes(bytes).ok() - } - }) - .ok_or(ErrorKind::CryptoError)?; - - let remote_sig = ed25519::Signature::try_from(auth_sig_msg.sig.as_slice()) - .map_err(|_| ErrorKind::CryptoError)?; - - if protocol_version.has_transcript() { - remote_pubkey - .verify(&sc_mac, &remote_sig) - .map_err(|_| ErrorKind::CryptoError)?; - } else { - remote_pubkey - .verify(&kdf.challenge, &remote_sig) - .map_err(|_| ErrorKind::CryptoError)?; - } - - // We've authorized. - sc.remote_pubkey = Some(remote_pubkey.into()); - - Ok(sc) - } - - /// Encrypt AEAD authenticated data - fn encrypt( - &self, - chunk: &[u8], - sealed_frame: &mut [u8; TAG_SIZE + TOTAL_FRAME_SIZE], - ) -> Result<(), Error> { - debug_assert!(chunk.len() <= TOTAL_FRAME_SIZE - DATA_LEN_SIZE); - sealed_frame[..DATA_LEN_SIZE].copy_from_slice(&(chunk.len() as u32).to_le_bytes()); - sealed_frame[DATA_LEN_SIZE..DATA_LEN_SIZE + chunk.len()].copy_from_slice(chunk); - - let tag = self - .send_cipher - .encrypt_in_place_detached( - GenericArray::from_slice(self.send_nonce.to_bytes()), - b"", - &mut sealed_frame[..TOTAL_FRAME_SIZE], - ) - .map_err(|_| ErrorKind::CryptoError)?; - - sealed_frame[TOTAL_FRAME_SIZE..].copy_from_slice(tag.as_slice()); - - Ok(()) - } - - /// Decrypt AEAD authenticated data - fn decrypt(&self, ciphertext: &[u8], out: &mut [u8]) -> Result { - // Ensure ciphertext is at least as long as a Poly1305 tag - if ciphertext.len() < TAG_SIZE { - return Err(ErrorKind::CryptoError.into()); - } - - // Split ChaCha20 ciphertext from the Poly1305 tag - let (ct, tag) = ciphertext.split_at(ciphertext.len() - TAG_SIZE); - - // Return a length error if the output buffer is too small - if out.len() < ct.len() { - return Err(ErrorKind::CryptoError.into()); - } - - let in_out = &mut out[..ct.len()]; - in_out.copy_from_slice(ct); - - self.recv_cipher - .decrypt_in_place_detached( - GenericArray::from_slice(self.recv_nonce.to_bytes()), - b"", - in_out, - tag.into(), - ) - .map_err(|_| ErrorKind::CryptoError)?; - - Ok(in_out.len()) - } -} - -impl Read for SecretConnection -where - IoHandler: Read + Write + Send + Sync, -{ - // CONTRACT: data smaller than dataMaxSize is read atomically. - fn read(&mut self, data: &mut [u8]) -> Result { - if !self.recv_buffer.is_empty() { - let n = cmp::min(data.len(), self.recv_buffer.len()); - data.copy_from_slice(&self.recv_buffer[..n]); - let mut leftover_portion = vec![0; self.recv_buffer.len().checked_sub(n).unwrap()]; - leftover_portion.clone_from_slice(&self.recv_buffer[n..]); - self.recv_buffer = leftover_portion; - - return Ok(n); - } - - let mut sealed_frame = [0u8; TAG_SIZE + TOTAL_FRAME_SIZE]; - self.io_handler.read_exact(&mut sealed_frame)?; - - // decrypt the frame - let mut frame = [0u8; TOTAL_FRAME_SIZE]; - let res = self.decrypt(&sealed_frame, &mut frame); - - if res.is_err() { - return Err(io::Error::new( - io::ErrorKind::Other, - res.err().unwrap().to_string(), - )); - } - - self.recv_nonce.increment(); - // end decryption - - let chunk_length = u32::from_le_bytes(frame[..4].try_into().unwrap()); - - if chunk_length as usize > DATA_MAX_SIZE { - return Err(io::Error::new( - io::ErrorKind::Other, - "chunk_length is greater than dataMaxSize", - )); - } - - let mut chunk = vec![0; chunk_length as usize]; - chunk.clone_from_slice( - &frame[DATA_LEN_SIZE..(DATA_LEN_SIZE.checked_add(chunk_length as usize).unwrap())], - ); - - let n = cmp::min(data.len(), chunk.len()); - data[..n].copy_from_slice(&chunk[..n]); - self.recv_buffer.copy_from_slice(&chunk[n..]); - - Ok(n) - } -} - -impl Write for SecretConnection -where - IoHandler: Read + Write + Send + Sync, -{ - // Writes encrypted frames of `sealedFrameSize` - // CONTRACT: data smaller than dataMaxSize is read atomically. - fn write(&mut self, data: &[u8]) -> Result { - let mut n = 0usize; - let mut data_copy = &data[..]; - while !data_copy.is_empty() { - let chunk: &[u8]; - if DATA_MAX_SIZE < data.len() { - chunk = &data[..DATA_MAX_SIZE]; - data_copy = &data_copy[DATA_MAX_SIZE..]; - } else { - chunk = data_copy; - data_copy = &[0u8; 0]; - } - let sealed_frame = &mut [0u8; TAG_SIZE + TOTAL_FRAME_SIZE]; - let res = self.encrypt(chunk, sealed_frame); - if res.is_err() { - return Err(io::Error::new( - io::ErrorKind::Other, - res.err().unwrap().to_string(), - )); - } - self.send_nonce.increment(); - // end encryption - - self.io_handler.write_all(&sealed_frame[..])?; - n = n.checked_add(chunk.len()).unwrap(); - } - - Ok(n) - } - - fn flush(&mut self) -> Result<(), io::Error> { - self.io_handler.flush() - } -} - -/// Returns pubkey, private key -fn gen_eph_keys() -> (EphemeralPublic, EphemeralSecret) { - let local_privkey = EphemeralSecret::new(&mut OsRng); - let local_pubkey = EphemeralPublic::from(&local_privkey); - (local_pubkey, local_privkey) -} - -/// Returns remote_eph_pubkey -fn share_eph_pubkey( - handler: &mut IoHandler, - local_eph_pubkey: &EphemeralPublic, - protocol_version: Version, -) -> Result { - // Send our pubkey and receive theirs in tandem. - // TODO(ismail): on the go side this is done in parallel, here we do send and receive after - // each other. thread::spawn would require a static lifetime. - // Should still work though. - handler.write_all(&protocol_version.encode_initial_handshake(&local_eph_pubkey))?; - - let mut response_len = 0u8; - handler.read_exact(slice::from_mut(&mut response_len))?; - - let mut buf = vec![0; response_len as usize]; - handler.read_exact(&mut buf)?; - protocol_version.decode_initial_handshake(&buf) -} - -/// Return is of the form lo, hi -fn sort32(first: [u8; 32], second: [u8; 32]) -> ([u8; 32], [u8; 32]) { - if second > first { - (first, second) - } else { - (second, first) - } -} - -/// Sign the challenge with the local private key -fn sign_challenge( - challenge: &[u8; 32], - local_privkey: &dyn Signer, -) -> Result { - local_privkey - .try_sign(challenge) - .map_err(|_| ErrorKind::CryptoError.into()) -} - -// TODO(ismail): change from DecodeError to something more generic -// this can also fail while writing / sending -fn share_auth_signature( - sc: &mut SecretConnection, - pubkey: &ed25519::PublicKey, - local_signature: &ed25519::Signature, -) -> Result { - let buf = sc - .protocol_version - .encode_auth_signature(pubkey, &local_signature); - - sc.write_all(&buf)?; - - let mut buf = vec![0; sc.protocol_version.auth_sig_msg_response_len()]; - sc.read_exact(&mut buf)?; - sc.protocol_version.decode_auth_signature(&buf) -} - -#[cfg(tests)] -mod tests { - use super::*; - - #[test] - fn test_sort() { - // sanity check - let t1 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, - ]; - let t2 = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1, - ]; - let (ref t3, ref t4) = sort32(t1, t2); - assert_eq!(t1, *t3); - assert_eq!(t2, *t4); - } - - #[test] - fn test_dh_compatibility() { - let local_priv = &[ - 15, 54, 189, 54, 63, 255, 158, 244, 56, 168, 155, 63, 246, 79, 208, 192, 35, 194, 39, - 232, 170, 187, 179, 36, 65, 36, 237, 12, 225, 176, 201, 54, - ]; - let remote_pub = &[ - 193, 34, 183, 46, 148, 99, 179, 185, 242, 148, 38, 40, 37, 150, 76, 251, 25, 51, 46, - 143, 189, 201, 169, 218, 37, 136, 51, 144, 88, 196, 10, 20, - ]; - - // generated using computeDHSecret in go - let expected_dh = &[ - 92, 56, 205, 118, 191, 208, 49, 3, 226, 150, 30, 205, 230, 157, 163, 7, 36, 28, 223, - 84, 165, 43, 78, 38, 126, 200, 40, 217, 29, 36, 43, 37, - ]; - let got_dh = diffie_hellman(local_priv, remote_pub); - - assert_eq!(expected_dh, &got_dh); - } -} diff --git a/src/connection/secret_connection/amino_types.rs b/src/connection/secret_connection/amino_types.rs deleted file mode 100644 index e7fc5847..00000000 --- a/src/connection/secret_connection/amino_types.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Amino types used by Secret Connection - -use prost_amino_derive::Message; - -/// Authentication signature message -#[derive(Clone, PartialEq, Message)] -pub struct AuthSigMessage { - /// Public key - #[prost_amino(bytes, tag = "1", amino_name = "tendermint/PubKeyEd25519")] - pub pub_key: Vec, - - /// Signature - #[prost_amino(bytes, tag = "2")] - pub sig: Vec, -} diff --git a/src/connection/secret_connection/kdf.rs b/src/connection/secret_connection/kdf.rs deleted file mode 100644 index 3c79ee4a..00000000 --- a/src/connection/secret_connection/kdf.rs +++ /dev/null @@ -1,56 +0,0 @@ -use hkdf::Hkdf; -use sha2::Sha256; -use zeroize::Zeroize; - -/// "Info" parameter to HKDF we use to personalize the derivation -const HKDF_INFO: &[u8] = b"TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN"; - -/// Key Derivation Function for `SecretConnection` (HKDF) -pub struct Kdf { - /// Receiver's secret - pub recv_secret: [u8; 32], - - /// Sender's secret - pub send_secret: [u8; 32], - - /// Challenge to be signed by peer - pub challenge: [u8; 32], -} - -impl Kdf { - /// Returns recv secret, send secret, challenge as 32 byte arrays - pub fn derive_secrets_and_challenge(shared_secret: &[u8; 32], loc_is_lo: bool) -> Self { - let mut key_material = [0u8; 96]; - - Hkdf::::new(None, shared_secret) - .expand(HKDF_INFO, &mut key_material) - .unwrap(); - - let [mut recv_secret, mut send_secret, mut challenge] = [[0u8; 32]; 3]; - - if loc_is_lo { - recv_secret.copy_from_slice(&key_material[0..32]); - send_secret.copy_from_slice(&key_material[32..64]); - } else { - send_secret.copy_from_slice(&key_material[0..32]); - recv_secret.copy_from_slice(&key_material[32..64]); - } - - challenge.copy_from_slice(&key_material[64..96]); - key_material.as_mut().zeroize(); - - Kdf { - recv_secret, - send_secret, - challenge, - } - } -} - -impl Drop for Kdf { - fn drop(&mut self) { - self.recv_secret.zeroize(); - self.send_secret.zeroize(); - self.challenge.zeroize(); - } -} diff --git a/src/connection/secret_connection/nonce.rs b/src/connection/secret_connection/nonce.rs deleted file mode 100644 index 59c0038a..00000000 --- a/src/connection/secret_connection/nonce.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Secret Connection nonces - -use std::convert::TryInto; - -/// Size of a ChaCha20 (IETF) nonce -pub const SIZE: usize = 12; - -/// SecretConnection nonces (i.e. ChaCha20 nonces) -pub struct Nonce(pub [u8; SIZE]); - -impl Default for Nonce { - fn default() -> Nonce { - Nonce([0u8; SIZE]) - } -} - -impl Nonce { - /// Increment the nonce's counter by 1 - pub fn increment(&mut self) { - let counter: u64 = u64::from_le_bytes(self.0[4..].try_into().unwrap()); - self.0[4..].copy_from_slice(&counter.checked_add(1).unwrap().to_le_bytes()); - } - - /// Serialize nonce as bytes (little endian) - #[inline] - pub fn to_bytes(&self) -> &[u8] { - &self.0[..] - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::HashMap; - - #[test] - fn test_incr_nonce() { - // make sure we match the golang implementation - let mut check_points: HashMap = HashMap::new(); - check_points.insert(0, &[0u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]); - check_points.insert(1, &[0u8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]); - check_points.insert(510, &[0u8, 0, 0, 0, 255, 1, 0, 0, 0, 0, 0, 0]); - check_points.insert(511, &[0u8, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0]); - check_points.insert(512, &[0u8, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0]); - check_points.insert(1023, &[0u8, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0]); - - let mut nonce = Nonce::default(); - assert_eq!(nonce.to_bytes().len(), SIZE); - - for i in 0..1024 { - nonce.increment(); - if let Some(want) = check_points.get(&i) { - let got = &nonce.to_bytes(); - assert_eq!(got, want); - } - } - } - #[test] - #[should_panic] - fn test_incr_nonce_overflow() { - // other than in the golang implementation we panic if we incremented more than 64 - // bits allow. - // In golang this would reset to an all zeroes nonce. - let mut nonce = Nonce([0u8, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255]); - nonce.increment(); - } -} diff --git a/src/connection/secret_connection/protocol.rs b/src/connection/secret_connection/protocol.rs deleted file mode 100644 index d6bc0e20..00000000 --- a/src/connection/secret_connection/protocol.rs +++ /dev/null @@ -1,235 +0,0 @@ -//! Secret Connection Protocol: message framing and versioning - -use super::amino_types; -use crate::{ - error::{Error, ErrorKind}, - prelude::*, -}; -use ed25519_dalek as ed25519; -use prost::Message as _; -use prost_amino::Message as _; -use std::convert::TryInto; -use tendermint_proto as proto; -use x25519_dalek::PublicKey as EphemeralPublic; - -/// Size of an X25519 or Ed25519 public key -const PUBLIC_KEY_SIZE: usize = 32; - -/// Protocol version (based on the Tendermint version) -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[allow(non_camel_case_types)] -pub enum Version { - /// Tendermint v0.34 - V0_34, - - /// Tendermint v0.33 - V0_33, - - /// Pre-Tendermint v0.33 - Legacy, -} - -impl Version { - /// Does this version of Secret Connection use a transcript hash - pub fn has_transcript(self) -> bool { - self != Version::Legacy - } - - /// Are messages encoded using Protocol Buffers? - pub fn is_protobuf(self) -> bool { - match self { - Version::V0_34 => true, - Version::V0_33 | Version::Legacy => false, - } - } - - /// Encode the initial handshake message (i.e. first one sent by both peers) - pub fn encode_initial_handshake(self, eph_pubkey: &EphemeralPublic) -> Vec { - if self.is_protobuf() { - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/9e98c74/p2p/conn/secret_connection.go#L307-L312 - // TODO(tarcieri): proper protobuf framing - let mut buf = Vec::new(); - buf.extend_from_slice(&[0x22, 0x0a, 0x20]); - buf.extend_from_slice(eph_pubkey.as_bytes()); - buf - } else { - // Legacy Amino encoded handshake message - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/013b9ce/p2p/conn/secret_connection.go#L213-L217 - // - // Note: this is not regular protobuf encoding but raw length prefixed amino encoding; - // amino prefixes with the total length, and the raw bytes array's length, too: - let mut buf = Vec::new(); - buf.push(PUBLIC_KEY_SIZE as u8 + 1); - buf.push(PUBLIC_KEY_SIZE as u8); - buf.extend_from_slice(eph_pubkey.as_bytes()); - buf - } - } - - /// Decode the initial handshake message - pub fn decode_initial_handshake(self, bytes: &[u8]) -> Result { - let eph_pubkey = if self.is_protobuf() { - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/9e98c74/p2p/conn/secret_connection.go#L315-L323 - // TODO(tarcieri): proper protobuf framing - if bytes.len() != 34 || bytes[..2] != [0x0a, 0x20] { - fail!( - ErrorKind::ProtocolError, - "malformed handshake message (protocol version mismatch?)" - ); - } - - let eph_pubkey_bytes: [u8; 32] = bytes[2..].try_into().unwrap(); - EphemeralPublic::from(eph_pubkey_bytes) - } else { - // Equivalent Go implementation: - // https://github.com/tendermint/tendermint/blob/013b9ce/p2p/conn/secret_connection.go#L220-L225 - // - // Check that the length matches what we expect and the length prefix is correct - if bytes.len() != 33 || bytes[0] != 32 { - fail!( - ErrorKind::ProtocolError, - "malformed handshake message (protocol version mismatch?)" - ); - } - - let eph_pubkey_bytes: [u8; 32] = bytes[1..].try_into().unwrap(); - EphemeralPublic::from(eph_pubkey_bytes) - }; - - // Reject the key if it is of low order - if is_low_order_point(&eph_pubkey) { - return Err(ErrorKind::InvalidKey.into()); - } - - Ok(eph_pubkey) - } - - /// Encode signature which authenticates the handshake - pub fn encode_auth_signature( - self, - pub_key: &ed25519::PublicKey, - signature: &ed25519::Signature, - ) -> Vec { - if self.is_protobuf() { - // Protobuf `AuthSigMessage` - let pub_key = proto::crypto::PublicKey { - sum: Some(proto::crypto::public_key::Sum::Ed25519( - pub_key.as_ref().to_vec(), - )), - }; - - let msg = proto::p2p::AuthSigMessage { - pub_key: Some(pub_key), - sig: signature.as_ref().to_vec(), - }; - - let mut buf = Vec::new(); - msg.encode_length_delimited(&mut buf) - .expect("couldn't encode AuthSigMessage proto"); - buf - } else { - // TODO(tarcieri): proper protobuf message - // Legacy Amino encoded `AuthSigMessage` - let msg = amino_types::AuthSigMessage { - pub_key: pub_key.as_ref().to_vec(), - sig: signature.as_ref().to_vec(), - }; - - let mut buf = Vec::new(); - msg.encode_length_delimited(&mut buf) - .expect("encode_auth_signature failed"); - buf - } - } - - /// Get the length of the auth message response for this protocol version - pub fn auth_sig_msg_response_len(self) -> usize { - if self.is_protobuf() { - // 32 + 64 + (proto overhead = 1 prefix + 2 fields + 2 lengths + total length) - 103 - } else { - // 32 + 64 + (amino overhead = 2 fields + 2 lengths + 4 prefix bytes + total length) - 106 - } - } - - /// Decode signature message which authenticates the handshake - pub fn decode_auth_signature(self, bytes: &[u8]) -> Result { - if self.is_protobuf() { - // Parse Protobuf-encoded `AuthSigMessage` - proto::p2p::AuthSigMessage::decode_length_delimited(bytes).map_err(|e| { - format_err!( - ErrorKind::ProtocolError, - "malformed handshake message (protocol version mismatch?): {}", - e - ) - .into() - }) - } else { - // Legacy Amino encoded `AuthSigMessage` - let amino_msg = amino_types::AuthSigMessage::decode_length_delimited(bytes)?; - - let pub_key = proto::crypto::PublicKey { - sum: Some(proto::crypto::public_key::Sum::Ed25519(amino_msg.pub_key)), - }; - - Ok(proto::p2p::AuthSigMessage { - pub_key: Some(pub_key), - sig: amino_msg.sig, - }) - } - } -} - -/// Reject low order points listed on -/// -/// These points contain low-order X25519 field elements. Rejecting them is -/// suggested in the "May the Fourth" paper under Section 5: -/// Software Countermeasures (see "Rejecting Known Bad Points" subsection): -/// -/// -fn is_low_order_point(point: &EphemeralPublic) -> bool { - // Note: as these are public points and do not interact with secret-key - // material in any way, this check does not need to be performed in - // constant-time. - match point.as_bytes() { - // 0 (order 4) - &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] => { - true - } - - // 1 (order 1) - [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] => { - true - } - - // 325606250916557431795983626356110631294008115727848805560023387167927233504 (order 8) - &[0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00] => { - true - } - - // 39382357235489614581723060781553021112529911719440698176882885853963445705823 (order 8) - &[0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0x57] => { - true - } - - // p - 1 (order 2) - [0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f] => { - true - } - - // p (order 4) */ - [0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f] => { - true - } - - // p + 1 (order 1) - [0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f] => { - true - } - _ => false, - } -} diff --git a/src/connection/secret_connection/public_key.rs b/src/connection/secret_connection/public_key.rs deleted file mode 100644 index 1fc1bfb0..00000000 --- a/src/connection/secret_connection/public_key.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Secret Connection peer public keys - -use ed25519_dalek as ed25519; -use sha2::{digest::Digest, Sha256}; -use std::fmt::{self, Display}; -use tendermint::{ - error::{self, Error}, - node, -}; - -/// Secret Connection peer public keys (signing, presently Ed25519-only) -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PublicKey { - /// Ed25519 Secret Connection Keys - Ed25519(ed25519::PublicKey), -} - -impl PublicKey { - /// From raw Ed25519 public key bytes - pub fn from_raw_ed25519(bytes: &[u8]) -> Result { - ed25519::PublicKey::from_bytes(bytes) - .map(PublicKey::Ed25519) - .map_err(|_| error::Kind::Crypto.into()) - } - - /// Get Ed25519 public key - pub fn ed25519(self) -> Option { - match self { - PublicKey::Ed25519(pk) => Some(pk), - } - } - - /// Get the remote Peer ID - pub fn peer_id(self) -> node::Id { - match self { - PublicKey::Ed25519(pk) => { - // TODO(tarcieri): use `tendermint::node::Id::from` - let digest = Sha256::digest(pk.as_bytes()); - let mut bytes = [0u8; 20]; - bytes.copy_from_slice(&digest[..20]); - node::Id::new(bytes) - } - } - } -} - -impl Display for PublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.peer_id()) - } -} - -impl From<&ed25519::Keypair> for PublicKey { - fn from(sk: &ed25519::Keypair) -> PublicKey { - PublicKey::Ed25519(sk.public) - } -} - -impl From for PublicKey { - fn from(pk: ed25519::PublicKey) -> PublicKey { - PublicKey::Ed25519(pk) - } -} - -#[cfg(test)] -mod tests { - use super::PublicKey; - use subtle_encoding::hex; - - const EXAMPLE_SECRET_CONN_KEY: &str = - "F7FEB0B5BA0760B2C58893E329475D1EA81781DD636E37144B6D599AD38AA825"; - - #[test] - fn test_secret_connection_pubkey_serialization() { - let example_key = - PublicKey::from_raw_ed25519(&hex::decode_upper(EXAMPLE_SECRET_CONN_KEY).unwrap()) - .unwrap(); - - assert_eq!( - example_key.to_string(), - "117C95C4FD7E636C38D303493302D2C271A39669" - ); - } -} diff --git a/src/connection/tcp.rs b/src/connection/tcp.rs index 74195b26..dfb56793 100644 --- a/src/connection/tcp.rs +++ b/src/connection/tcp.rs @@ -1,14 +1,17 @@ //! TCP socket connection to a validator -use super::secret_connection::{self, PublicKey, SecretConnection}; +use std::{net::TcpStream, path::PathBuf, time::Duration}; + +use subtle::ConstantTimeEq; +use tendermint::node; +use tendermint_p2p::error::Error as TmError; +use tendermint_p2p::secret_connection::{self, PublicKey, SecretConnection}; + use crate::{ error::{Error, ErrorKind::*}, key_utils, prelude::*, }; -use std::{net::TcpStream, path::PathBuf, time::Duration}; -use subtle::ConstantTimeEq; -use tendermint::node; /// Default timeout in seconds const DEFAULT_TIMEOUT: u16 = 10; @@ -39,7 +42,15 @@ pub fn open_secret_connection( socket.set_read_timeout(Some(timeout))?; socket.set_write_timeout(Some(timeout))?; - let connection = SecretConnection::new(socket, &identity_key, protocol_version)?; + let connection = match SecretConnection::new(socket, &identity_key, protocol_version) { + Ok(conn) => conn, + Err(error) => match error.downcast_ref::() { + Some(TmError::CryptoError) => fail!(CryptoError, format!("{}", error)), + Some(TmError::ProtocolError) => fail!(ProtocolError, format!("{}", error)), + Some(TmError::InvalidKey) => fail!(InvalidKey, format!("{}", error)), + None => fail!(ProtocolError, format!("{}", error)), + }, + }; let actual_peer_id = connection.remote_pubkey().peer_id(); // TODO(tarcieri): move this into `SecretConnection::new` diff --git a/src/key_utils.rs b/src/key_utils.rs index f30b4333..82ad04f0 100644 --- a/src/key_utils.rs +++ b/src/key_utils.rs @@ -1,19 +1,23 @@ //! Utilities -use crate::{ - error::{Error, ErrorKind::*}, - prelude::*, -}; -use ed25519_dalek as ed25519; use std::{ fs::{self, OpenOptions}, io::Write, os::unix::fs::OpenOptionsExt, path::Path, }; + +use ed25519_dalek as ed25519; +use ed25519_dalek::SECRET_KEY_LENGTH; +use rand_core::{OsRng, RngCore}; use subtle_encoding::base64; use zeroize::Zeroizing; +use crate::{ + error::{Error, ErrorKind::*}, + prelude::*, +}; + /// File permissions for secret data pub const SECRET_FILE_PERMS: u32 = 0o600; @@ -74,3 +78,10 @@ pub fn write_base64_secret(path: impl AsRef, data: &[u8]) -> Result<(), Er .into() }) } + +/// Generate a Secret Connection key at the given path +pub fn generate_key(path: impl AsRef) -> Result<(), Error> { + let mut secret_key = Zeroizing::new([0u8; SECRET_KEY_LENGTH]); + OsRng.fill_bytes(&mut *secret_key); + write_base64_secret(path, &*secret_key) +} diff --git a/src/rpc.rs b/src/rpc.rs index ac67c787..11562dcb 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -3,18 +3,20 @@ // TODO: docs for everything #![allow(missing_docs)] +use std::io::Read; + +use bytes::Bytes; +use prost::Message as _; +use prost_amino::{encoding::decode_varint, Message as _}; +use tendermint_p2p::secret_connection::DATA_MAX_SIZE; +use tendermint_proto as proto; + use crate::{ amino_types, config::validator::ProtocolVersion, - connection::secret_connection::DATA_MAX_SIZE, error::{Error, ErrorKind}, prelude::*, }; -use bytes::Bytes; -use prost::Message as _; -use prost_amino::{encoding::decode_varint, Message as _}; -use std::io::Read; -use tendermint_proto as proto; /// RPC requests to the KMS #[derive(Debug)] diff --git a/tests/integration.rs b/tests/integration.rs index 26bff750..9ceba431 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,10 +1,5 @@ //! KMS integration test -use abscissa_core::prelude::warn; -use chrono::{DateTime, Utc}; -use ed25519_dalek::{self as ed25519, Verifier}; -use prost_amino::Message; -use rand::Rng; use std::{ convert::TryFrom, fs, @@ -13,14 +8,20 @@ use std::{ os::unix::net::{UnixListener, UnixStream}, process::{Child, Command}, }; + +use abscissa_core::prelude::warn; +use chrono::{DateTime, Utc}; +use ed25519_dalek::{self as ed25519, Verifier}; +use rand::Rng; use tempfile::NamedTempFile; + +use prost_amino::Message; +use tendermint_p2p::secret_connection::{self, SecretConnection}; + use tmkms::{ amino_types::{self, *}, config::validator::ProtocolVersion, - connection::{ - secret_connection::{self, SecretConnection}, - unix::UnixConnection, - }, + connection::unix::UnixConnection, }; /// Integration tests for the KMS command-line interface