Skip to content
This repository has been archived by the owner on Jun 3, 2020. It is now read-only.

Commit

Permalink
Merge pull request #219 from tendermint/peer-id-verification
Browse files Browse the repository at this point in the history
Optional peer ID verification
  • Loading branch information
tarcieri committed Mar 13, 2019
2 parents 0e9b385 + 6d56991 commit 1acf987
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 111 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ signatory = { version = "0.11.1", features = ["ed25519", "ecdsa"] }
signatory-dalek = "0.11"
signatory-secp256k1 = "0.11"
signatory-ledger-tm = { version = "0.11", optional = true }
subtle = "2"
subtle-encoding = { version = "0.3", features = ["bech32-preview"] }
tendermint = { version = "0.5.0-alpha1", path = "tendermint-rs" }
tiny-bip39 = "0.6"
Expand Down
31 changes: 18 additions & 13 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
//! To dance around the fact the KMS isn't actually a service, we refer to it
//! as a "Key Management System".

use signatory::{ed25519, Decode, Encode};
use crate::{
config::{ValidatorAddr, ValidatorConfig},
error::{KmsError, KmsErrorKind},
keyring::SecretKeyEncoding,
session::Session,
};
use signatory::{ed25519, Decode, Encode, PublicKeyed};
use signatory_dalek::Ed25519Signer;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
Expand All @@ -15,14 +21,7 @@ use std::{
thread::{self, JoinHandle},
time::Duration,
};
use tendermint::{chain, public_keys::SecretConnectionKey};

use crate::{
config::{ValidatorAddr, ValidatorConfig},
error::{KmsError, KmsErrorKind},
keyring::SecretKeyEncoding,
session::Session,
};
use tendermint::{chain, secret_connection};

/// How long to wait after a crash before respawning (in seconds)
pub const RESPAWN_DELAY: u64 = 1;
Expand Down Expand Up @@ -69,8 +68,12 @@ fn client_loop(config: ValidatorConfig, should_term: &Arc<AtomicBool>) {
}

let session_result = match &addr {
ValidatorAddr::Tcp { host, port } => match &secret_key {
Some(path) => tcp_session(chain_id, host, *port, path, should_term),
ValidatorAddr::Tcp {
peer_id,
host,
port,
} => match &secret_key {
Some(path) => tcp_session(chain_id, *peer_id, host, *port, path, should_term),
None => {
error!(
"config error: missing field `secret_key` for validator {}",
Expand Down Expand Up @@ -104,6 +107,7 @@ fn client_loop(config: ValidatorConfig, should_term: &Arc<AtomicBool>) {
/// Create a TCP connection to a validator (encrypted with SecretConnection)
fn tcp_session(
chain_id: chain::Id,
validator_peer_id: Option<secret_connection::PeerId>,
host: &str,
port: u16,
secret_key_path: &Path,
Expand All @@ -112,12 +116,13 @@ fn tcp_session(
let secret_key = load_secret_connection_key(secret_key_path)?;

let node_public_key =
SecretConnectionKey::from(ed25519::public_key(&Ed25519Signer::from(&secret_key)).unwrap());
secret_connection::PublicKey::from(Ed25519Signer::from(&secret_key).public_key().unwrap());

info!("KMS node ID: {}", &node_public_key);

panic::catch_unwind(move || {
let mut session = Session::connect_tcp(chain_id, host, port, &secret_key)?;
let mut session =
Session::connect_tcp(chain_id, validator_peer_id, host, port, &secret_key)?;

info!(
"[{}@tcp://{}:{}] connected to validator successfully",
Expand Down
23 changes: 19 additions & 4 deletions src/config/validator/addr.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
//! Validator addresses (`tcp://` or `unix://`)

use crate::error::{KmsError, KmsErrorKind::*};
use serde::{de::Error as DeError, Deserialize, Deserializer};
use std::{
fmt::{self, Display},
path::PathBuf,
str::{self, FromStr},
};

use crate::error::{KmsError, KmsErrorKind::*};
use tendermint::secret_connection;

#[derive(Clone, Debug)]
pub enum ValidatorAddr {
/// TCP connections (with SecretConnection transport encryption)
Tcp {
/// Remote peer ID of the validator
// TODO(tarcieri): make this mandatory
peer_id: Option<secret_connection::PeerId>,

/// Validator hostname or IP address
host: String,

Expand Down Expand Up @@ -53,7 +57,14 @@ impl FromStr for ValidatorAddr {
// TODO: less janky URL parser? (e.g. use `url` crate)
fn from_str(addr: &str) -> Result<Self, KmsError> {
if addr.starts_with("tcp://") {
let host_and_port: Vec<&str> = addr[6..].split(':').collect();
let authority_parts = addr[6..].split('@').collect::<Vec<_>>();
let (peer_id, authority) = match authority_parts.len() {
1 => (None, authority_parts[0]),
2 => (Some(authority_parts[0].parse()?), authority_parts[1]),
_ => fail!(ConfigError, "invalid tcp:// address: {}", addr),
};

let host_and_port: Vec<&str> = authority.split(':').collect();

if host_and_port.len() != 2 {
fail!(ConfigError, "invalid tcp:// address: {}", addr);
Expand All @@ -64,7 +75,11 @@ impl FromStr for ValidatorAddr {
.parse()
.map_err(|_| err!(ConfigError, "invalid tcp:// address (bad port): {}", addr))?;

Ok(ValidatorAddr::Tcp { host, port })
Ok(ValidatorAddr::Tcp {
peer_id,
host,
port,
})
} else if addr.starts_with("unix://") {
let socket_path = PathBuf::from(&addr[7..]);
Ok(ValidatorAddr::Unix { socket_path })
Expand Down
47 changes: 34 additions & 13 deletions src/session.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
//! A session with a validator node

use signatory::ed25519;
use crate::{
chain,
error::{KmsError, KmsErrorKind::VerificationError},
keyring::KeyRing,
prost::Message,
rpc::{Request, Response, TendermintRequest},
unix_connection::UnixConnection,
};
use signatory::{ed25519, PublicKeyed};
use signatory_dalek::Ed25519Signer;
use std::{
fmt::Debug,
Expand All @@ -14,19 +22,10 @@ use std::{
Arc,
},
};
use subtle::ConstantTimeEq;
use tendermint::{
amino_types::{PingRequest, PingResponse, PubKeyRequest},
public_keys::SecretConnectionKey,
SecretConnection,
};

use crate::{
chain,
error::KmsError,
keyring::KeyRing,
prost::Message,
rpc::{Request, Response, TendermintRequest},
unix_connection::UnixConnection,
secret_connection::{self, SecretConnection},
};

/// Encrypted session with a validator node
Expand All @@ -42,6 +41,7 @@ impl Session<SecretConnection<TcpStream>> {
/// Create a new session with the validator at the given address/port
pub fn connect_tcp(
chain_id: chain::Id,
validator_peer_id: Option<secret_connection::PeerId>,
host: &str,
port: u16,
secret_connection_key: &ed25519::Seed,
Expand All @@ -50,8 +50,29 @@ impl Session<SecretConnection<TcpStream>> {

let socket = TcpStream::connect(format!("{}:{}", host, port))?;
let signer = Ed25519Signer::from(secret_connection_key);
let public_key = SecretConnectionKey::from(ed25519::public_key(&signer)?);
let public_key = secret_connection::PublicKey::from(signer.public_key()?);
let connection = SecretConnection::new(socket, &public_key, &signer)?;
let actual_peer_id = connection.remote_pubkey().peer_id();

// TODO(tarcieri): move this logic into `SecretConnection::new`?
if let Some(expected_peer_id) = validator_peer_id {
if expected_peer_id.ct_eq(&actual_peer_id).unwrap_u8() == 0 {
fail!(
VerificationError,
"{}:{}: validator peer ID mismatch! (expected {}, got {})",
host,
port,
expected_peer_id,
actual_peer_id
);
}
} else {
// TODO(tarcieri): make peer verification mandatory
warn!(
"[{}] {}:{}: unverified validator peer ID! ({})",
chain_id, host, port, actual_peer_id
);
}

Ok(Self {
chain_id,
Expand Down
2 changes: 2 additions & 0 deletions tendermint-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ serde_derive = { version = "1", optional = true }
signatory = { version = "0.11.2", optional = true, features = ["ed25519", "ecdsa"] }
signatory-dalek = { version = "0.11", optional = true }
sha2 = { version = "0.8", optional = true, default-features = false }
subtle = { version = "2", optional = true }
subtle-encoding = { version = "0.3", features = ["bech32-preview"] }
tai64 = { version = "1", optional = true, features = ["chrono"] }
x25519-dalek = { version = "0.4.4", optional = true, default-features = false, features = ["u64_backend"] }
Expand All @@ -60,6 +61,7 @@ secret-connection = [
"signatory",
"signatory-dalek",
"sha2",
"subtle",
"x25519-dalek",
"zeroize"
]
Expand Down
68 changes: 2 additions & 66 deletions tendermint-rs/src/public_keys.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
//! Public keys used in Tendermint networks
// TODO:: account keys

use crate::{amino_types::PubKeyResponse, error::Error};
use sha2::{Digest, Sha256};
use signatory::{ecdsa::curve::secp256k1, ed25519};
use std::{
fmt::{self, Display},
ops::Deref,
};
use std::ops::Deref;
use subtle_encoding::{bech32, hex};

/// Public keys allowed in Tendermint protocols
Expand Down Expand Up @@ -112,70 +107,11 @@ impl Deref for TendermintKey {
}
}

/// Secret Connection signing keys
// TODO(tarcieri): unify with `TendermintKey`?
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub enum SecretConnectionKey {
/// Ed25519 Secret Connection keys
Ed25519(ed25519::PublicKey),
}

impl SecretConnectionKey {
/// From raw Ed25519 public key bytes
pub fn from_raw_ed25519(bytes: &[u8]) -> Result<SecretConnectionKey, Error> {
Ok(SecretConnectionKey::Ed25519(
ed25519::PublicKey::from_bytes(bytes)?,
))
}

/// Get Ed25519 public key
pub fn ed25519(self) -> Option<ed25519::PublicKey> {
match self {
SecretConnectionKey::Ed25519(pk) => Some(pk),
}
}
}

impl Display for SecretConnectionKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SecretConnectionKey::Ed25519(ref pk) => {
for byte in &Sha256::digest(pk.as_bytes())[..20] {
write!(f, "{:02X}", byte)?;
}
}
}
Ok(())
}
}

impl From<ed25519::PublicKey> for SecretConnectionKey {
fn from(pk: ed25519::PublicKey) -> SecretConnectionKey {
SecretConnectionKey::Ed25519(pk)
}
}

#[cfg(test)]
mod tests {
use super::{PublicKey, SecretConnectionKey, TendermintKey};
use super::{PublicKey, TendermintKey};
use subtle_encoding::hex;

const EXAMPLE_SECRET_CONN_KEY: &str =
"F7FEB0B5BA0760B2C58893E329475D1EA81781DD636E37144B6D599AD38AA825";

#[test]
fn test_address_serialization() {
let example_key = SecretConnectionKey::from_raw_ed25519(
&hex::decode_upper(EXAMPLE_SECRET_CONN_KEY).unwrap(),
)
.unwrap();

assert_eq!(
example_key.to_string(),
"117C95C4FD7E636C38D303493302D2C271A39669"
);
}

const EXAMPLE_CONSENSUS_KEY: &str =
"4A25C6640A1F72B9C975338294EF51B6D1C33158BB6ECBA69FBC3FB5A33C9DCE";

Expand Down
18 changes: 10 additions & 8 deletions tendermint-rs/src/secret_connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

mod kdf;
mod nonce;
mod peer_id;
mod public_key;

pub use self::{kdf::Kdf, nonce::Nonce};
use crate::{amino_types::AuthSigMessage, error::Error, public_keys::SecretConnectionKey};
pub use self::{kdf::Kdf, nonce::Nonce, peer_id::PeerId, public_key::PublicKey};
use crate::{amino_types::AuthSigMessage, error::Error};
use byteorder::{ByteOrder, LE};
use bytes::BufMut;
use prost::{encoding::encode_varint, Message};
Expand Down Expand Up @@ -34,20 +36,20 @@ pub struct SecretConnection<IoHandler: Read + Write + Send + Sync> {
send_nonce: Nonce,
recv_secret: aead::OpeningKey,
send_secret: aead::SealingKey,
remote_pubkey: SecretConnectionKey,
remote_pubkey: PublicKey,
recv_buffer: Vec<u8>,
}

impl<IoHandler: Read + Write + Send + Sync> SecretConnection<IoHandler> {
/// Returns authenticated remote pubkey
pub fn remote_pubkey(&self) -> SecretConnectionKey {
pub fn remote_pubkey(&self) -> PublicKey {
self.remote_pubkey
}
#[allow(clippy::new_ret_no_self)]
/// Performs handshake and returns a new authenticated SecretConnection.
pub fn new(
mut handler: IoHandler,
local_pubkey: &SecretConnectionKey,
local_pubkey: &PublicKey,
local_privkey: &dyn Signer<ed25519::Signature>,
) -> Result<SecretConnection<IoHandler>, Error> {
// Generate ephemeral keys for perfect forward secrecy.
Expand Down Expand Up @@ -82,7 +84,7 @@ impl<IoHandler: Read + Write + Send + Sync> SecretConnection<IoHandler> {
.map_err(|_| Error::Crypto)?,
send_secret: aead::SealingKey::new(&aead::CHACHA20_POLY1305, &kdf.send_secret)
.map_err(|_| Error::Crypto)?,
remote_pubkey: SecretConnectionKey::from(ed25519::PublicKey::from_bytes(
remote_pubkey: PublicKey::from(ed25519::PublicKey::from_bytes(
remote_eph_pubkey.as_bytes(),
)?),
};
Expand All @@ -92,7 +94,7 @@ impl<IoHandler: Read + Write + Send + Sync> SecretConnection<IoHandler> {

// Share (in secret) each other's pubkey & challenge signature
let auth_sig_msg = match local_pubkey {
SecretConnectionKey::Ed25519(ref pk) => {
PublicKey::Ed25519(ref pk) => {
share_auth_signature(&mut sc, pk.as_bytes(), local_signature)?
}
};
Expand All @@ -105,7 +107,7 @@ impl<IoHandler: Read + Write + Send + Sync> SecretConnection<IoHandler> {
ed25519::verify(&remote_verifier, &kdf.challenge, &remote_sig)?;

// We've authorized.
sc.remote_pubkey = SecretConnectionKey::from(remote_pubkey);
sc.remote_pubkey = PublicKey::from(remote_pubkey);

Ok(sc)
}
Expand Down
Loading

0 comments on commit 1acf987

Please sign in to comment.