Send salted hash of node_id

This commit is contained in:
Dennis Schwerdel 2020-09-28 12:50:08 +02:00
parent 941ac62bac
commit af7a7f6a29
6 changed files with 128 additions and 52 deletions

View File

@ -292,7 +292,12 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
peers.partial_shuffle(&mut rng, 20);
peers.truncate(20);
}
NodeInfo { peers, claims: self.claims.clone(), peer_timeout: Some(self.peer_timeout_publish) }
NodeInfo {
node_id: self.node_id,
peers,
claims: self.claims.clone(),
peer_timeout: Some(self.peer_timeout_publish)
}
}
fn connect_sock(&mut self, addr: SocketAddr) -> Result<(), Error> {
@ -584,12 +589,12 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
Ok(())
}
fn add_new_peer(&mut self, addr: SocketAddr, node_id: NodeId, info: NodeInfo) -> Result<(), Error> {
fn add_new_peer(&mut self, addr: SocketAddr, info: NodeInfo) -> Result<(), Error> {
info!("Added peer {}", addr_nice(addr));
if let Some(init) = self.pending_inits.remove(&addr) {
self.peers.insert(addr, PeerData {
crypto: init,
node_id,
node_id: info.node_id,
peer_timeout: info.peer_timeout.unwrap_or(DEFAULT_PEER_TIMEOUT),
last_seen: TS::now(),
timeout: TS::now() + self.config.peer_timeout as Time
@ -684,9 +689,9 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
_ => return Err(Error::Message("Unknown message type"))
}
}
MessageResult::Initialized(node_id, info) => self.add_new_peer(src, node_id, info)?,
MessageResult::InitializedWithReply(node_id, info) => {
self.add_new_peer(src, node_id, info)?;
MessageResult::Initialized(info) => self.add_new_peer(src, info)?,
MessageResult::InitializedWithReply(info) => {
self.add_new_peer(src, info)?;
self.send_to(src, data)?
}
MessageResult::Reply => self.send_to(src, data)?,

View File

@ -417,7 +417,7 @@ pub struct Args {
pub version: bool,
/// Generate and print a key-pair and exit
#[structopt(long, conflicts_with_all=&["password", "private_key"])]
#[structopt(long, conflicts_with="private_key")]
pub genkey: bool,
/// Disable automatic port forwarding

View File

@ -55,7 +55,7 @@ use super::{
core::{CryptoCore, EXTRA_LEN},
Algorithms, EcdhPrivateKey, EcdhPublicKey, Ed25519PublicKey, Error, MsgBuffer, Payload
};
use crate::types::{NodeId, NODE_ID_BYTES};
use crate::types::NodeId;
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
use ring::{
aead::{Algorithm, LessSafeKey, UnboundKey, AES_128_GCM, AES_256_GCM, CHACHA20_POLY1305},
@ -79,20 +79,35 @@ pub const STAGE_PENG: u8 = 3;
pub const WAITING_TO_CLOSE: u8 = 4;
pub const CLOSING: u8 = 5;
pub const SALTED_NODE_ID_HASH_LEN: usize = 20;
pub type SaltedNodeIdHash = [u8; SALTED_NODE_ID_HASH_LEN];
#[allow(clippy::large_enum_variant)]
pub enum InitMsg {
Ping { node_id: NodeId, ecdh_public_key: EcdhPublicKey, algorithms: Algorithms },
Pong { node_id: NodeId, ecdh_public_key: EcdhPublicKey, algorithms: Algorithms, encrypted_payload: MsgBuffer },
Peng { node_id: NodeId, encrypted_payload: MsgBuffer }
// TODO: include only salted hashes of node_id and use public_key for leader election
Ping {
salted_node_id_hash: SaltedNodeIdHash,
ecdh_public_key: EcdhPublicKey,
algorithms: Algorithms
},
Pong {
salted_node_id_hash: SaltedNodeIdHash,
ecdh_public_key: EcdhPublicKey,
algorithms: Algorithms,
encrypted_payload: MsgBuffer
},
Peng {
salted_node_id_hash: SaltedNodeIdHash,
encrypted_payload: MsgBuffer
}
}
impl InitMsg {
const PART_ALGORITHMS: u8 = 4;
const PART_ECDH_PUBLIC_KEY: u8 = 3;
const PART_END: u8 = 0;
const PART_NODE_ID: u8 = 2;
const PART_PAYLOAD: u8 = 5;
const PART_SALTED_NODE_ID_HASH: u8 = 2;
const PART_STAGE: u8 = 1;
fn stage(&self) -> u8 {
@ -103,9 +118,11 @@ impl InitMsg {
}
}
fn node_id(&self) -> NodeId {
fn salted_node_id_hash(&self) -> &SaltedNodeIdHash {
match self {
InitMsg::Ping { node_id, .. } | InitMsg::Pong { node_id, .. } | InitMsg::Peng { node_id, .. } => *node_id
InitMsg::Ping { salted_node_id_hash, .. }
| InitMsg::Pong { salted_node_id_hash, .. }
| InitMsg::Peng { salted_node_id_hash, .. } => salted_node_id_hash
}
}
@ -140,7 +157,7 @@ impl InitMsg {
}
let mut stage = None;
let mut node_id = None;
let mut salted_node_id_hash = None;
let mut ecdh_public_key = None;
let mut encrypted_payload = None;
let mut algorithms = None;
@ -158,13 +175,13 @@ impl InitMsg {
}
stage = Some(r.read_u8().map_err(|_| Error::Parse("Init message too short"))?)
}
Self::PART_NODE_ID => {
if field_len != NODE_ID_BYTES {
return Err(Error::CryptoInit("Invalid size for node id field"))
Self::PART_SALTED_NODE_ID_HASH => {
if field_len != SALTED_NODE_ID_HASH_LEN {
return Err(Error::CryptoInit("Invalid size for salted node id hash field"))
}
let mut id = [0; NODE_ID_BYTES];
let mut id = [0; SALTED_NODE_ID_HASH_LEN];
r.read_exact(&mut id).map_err(|_| Error::Parse("Init message too short"))?;
node_id = Some(id)
salted_node_id_hash = Some(id)
}
Self::PART_ECDH_PUBLIC_KEY => {
let mut pub_key_data = smallvec![0; field_len];
@ -223,7 +240,7 @@ impl InitMsg {
Some(val) => val,
None => return Err(Error::CryptoInit("Init message without stage"))
};
let node_id = match node_id {
let salted_node_id_hash = match salted_node_id_hash {
Some(val) => val,
None => return Err(Error::CryptoInit("Init message without node id"))
};
@ -238,7 +255,7 @@ impl InitMsg {
Some(val) => val,
None => return Err(Error::CryptoInit("Init message without algorithms"))
};
Self::Ping { node_id, ecdh_public_key, algorithms }
Self::Ping { salted_node_id_hash, ecdh_public_key, algorithms }
}
STAGE_PONG => {
let ecdh_public_key = match ecdh_public_key {
@ -253,14 +270,14 @@ impl InitMsg {
Some(val) => val,
None => return Err(Error::CryptoInit("Init message without payload"))
};
Self::Pong { node_id, ecdh_public_key, algorithms, encrypted_payload }
Self::Pong { salted_node_id_hash, ecdh_public_key, algorithms, encrypted_payload }
}
STAGE_PENG => {
let encrypted_payload = match encrypted_payload {
Some(val) => val,
None => return Err(Error::CryptoInit("Init message without payload"))
};
Self::Peng { node_id, encrypted_payload }
Self::Peng { salted_node_id_hash, encrypted_payload }
}
_ => return Err(Error::CryptoInit("Invalid stage"))
};
@ -285,10 +302,12 @@ impl InitMsg {
w.write_u8(self.stage())?;
match &self {
Self::Ping { node_id, .. } | Self::Pong { node_id, .. } | Self::Peng { node_id, .. } => {
w.write_u8(Self::PART_NODE_ID)?;
w.write_u16::<NetworkEndian>(NODE_ID_BYTES as u16)?;
w.write_all(node_id)?;
Self::Ping { salted_node_id_hash, .. }
| Self::Pong { salted_node_id_hash, .. }
| Self::Peng { salted_node_id_hash, .. } => {
w.write_u8(Self::PART_SALTED_NODE_ID_HASH)?;
w.write_u16::<NetworkEndian>(SALTED_NODE_ID_HASH_LEN as u16)?;
w.write_all(salted_node_id_hash)?;
}
}
@ -354,12 +373,13 @@ impl InitMsg {
#[derive(PartialEq, Debug)]
pub enum InitResult<P: Payload> {
Continue,
Success { peer_payload: P, node_id: NodeId, is_initiator: bool }
Success { peer_payload: P, is_initiator: bool }
}
pub struct InitState<P: Payload> {
node_id: NodeId,
salted_node_id_hash: SaltedNodeIdHash,
payload: P,
key_pair: Arc<Ed25519KeyPair>,
trusted_keys: Arc<Vec<Ed25519PublicKey>>,
@ -378,8 +398,15 @@ impl<P: Payload> InitState<P> {
algorithms: Algorithms
) -> Self
{
let mut hash = [0; SALTED_NODE_ID_HASH_LEN];
let rng = SystemRandom::new();
rng.fill(&mut hash[0..4]).unwrap();
hash[4..].clone_from_slice(&node_id);
let d = digest::digest(&digest::SHA256, &hash);
hash[4..].clone_from_slice(&d.as_ref()[..16]);
Self {
node_id,
salted_node_id_hash: hash,
payload,
key_pair,
trusted_keys,
@ -462,6 +489,14 @@ impl<P: Payload> InitState<P> {
Ok(P::read_from(Cursor::new(data.message()))?)
}
fn check_salted_node_id_hash(&self, hash: &SaltedNodeIdHash, node_id: NodeId) -> bool {
let mut h2 = [0; SALTED_NODE_ID_HASH_LEN];
h2[0..4].clone_from_slice(&hash[0..4]);
h2[4..].clone_from_slice(&node_id);
let d = digest::digest(&digest::SHA256, &h2);
hash == d.as_ref()
}
fn send_message(
&mut self, stage: u8, ecdh_public_key: Option<EcdhPublicKey>, out: &mut MsgBuffer
) -> Result<(), Error> {
@ -472,20 +507,25 @@ impl<P: Payload> InitState<P> {
let msg = match stage {
STAGE_PING => {
InitMsg::Ping {
node_id: self.node_id,
salted_node_id_hash: self.salted_node_id_hash,
ecdh_public_key: ecdh_public_key.unwrap(),
algorithms: self.algorithms.clone()
}
}
STAGE_PONG => {
InitMsg::Pong {
node_id: self.node_id,
salted_node_id_hash: self.salted_node_id_hash,
ecdh_public_key: ecdh_public_key.unwrap(),
algorithms: self.algorithms.clone(),
encrypted_payload: self.encrypt_payload()?
}
}
STAGE_PENG => InitMsg::Peng { node_id: self.node_id, encrypted_payload: self.encrypt_payload()? },
STAGE_PENG => {
InitMsg::Peng {
salted_node_id_hash: self.salted_node_id_hash,
encrypted_payload: self.encrypt_payload()?
}
}
_ => unreachable!()
};
let mut bytes = out.buffer();
@ -535,16 +575,18 @@ impl<P: Payload> InitState<P> {
let (msg, _peer_key) = InitMsg::read_from(out.buffer(), &self.trusted_keys)?;
out.clear();
let stage = msg.stage();
let node_id = msg.node_id();
let salted_node_id_hash = *msg.salted_node_id_hash();
debug!("Received init with stage={}, expected stage={}", stage, self.next_stage);
if self.node_id == node_id {
if self.salted_node_id_hash == salted_node_id_hash
|| self.check_salted_node_id_hash(&salted_node_id_hash, self.node_id)
{
return Err(Error::CryptoInit("Connected to self"))
}
if stage != self.next_stage {
if self.next_stage == STAGE_PONG && stage == STAGE_PING {
// special case for concurrent init messages in both directions
// the node with the higher node_id "wins" and gets to initialize the connection
if node_id > self.node_id {
if salted_node_id_hash > self.salted_node_id_hash {
// reset to initial state
self.next_stage = STAGE_PING;
self.last_message = None;
@ -571,7 +613,7 @@ impl<P: Payload> InitState<P> {
let algorithm = self.select_algorithm(&algorithms)?;
if let Some((algorithm, _speed)) = algorithm {
let master_key = self.derive_master_key(algorithm, my_ecdh_private_key, &ecdh_public_key);
self.crypto = Some(CryptoCore::new(master_key, self.node_id > node_id));
self.crypto = Some(CryptoCore::new(master_key, self.salted_node_id_hash > salted_node_id_hash));
}
// create and send stage 2 reply
@ -586,7 +628,7 @@ impl<P: Payload> InitState<P> {
let algorithm = self.select_algorithm(&algorithms)?;
if let Some((algorithm, _speed)) = algorithm {
let master_key = self.derive_master_key(algorithm, ecdh_private_key, &ecdh_public_key);
self.crypto = Some(CryptoCore::new(master_key, self.node_id > node_id));
self.crypto = Some(CryptoCore::new(master_key, self.salted_node_id_hash > salted_node_id_hash));
}
// decrypt the payload
@ -598,7 +640,7 @@ impl<P: Payload> InitState<P> {
self.next_stage = WAITING_TO_CLOSE;
self.close_time = 60;
Ok(InitResult::Success { peer_payload, node_id, is_initiator: true })
Ok(InitResult::Success { peer_payload, is_initiator: true })
}
InitMsg::Peng { mut encrypted_payload, .. } => {
// decrypt the payload
@ -606,7 +648,7 @@ impl<P: Payload> InitState<P> {
self.decrypt(&mut encrypted_payload).map_err(|_| Error::CryptoInit("Failed to decrypt payload"))?;
self.next_stage = CLOSING; // force resend when receiving any message
Ok(InitResult::Success { peer_payload, node_id, is_initiator: false })
Ok(InitResult::Success { peer_payload, is_initiator: false })
}
}
}
@ -620,6 +662,7 @@ impl<P: Payload> InitState<P> {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::NODE_ID_BYTES;
impl Payload for Vec<u8> {
fn write_to(&self, buffer: &mut MsgBuffer) {

View File

@ -127,10 +127,23 @@ impl Crypto {
Ok(Self { node_id, key_pair: Arc::new(key_pair), trusted_keys: Arc::new(trusted_keys), algorithms: algos })
}
pub fn generate_keypair() -> (String, String) {
let rng = SystemRandom::new();
pub fn generate_keypair(password: Option<&str>) -> (String, String) {
let mut bytes = [0; 32];
rng.fill(&mut bytes).unwrap();
match password {
None => {
let rng = SystemRandom::new();
rng.fill(&mut bytes).unwrap();
}
Some(password) => {
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA256,
NonZeroU32::new(4096).unwrap(),
SALT,
password.as_bytes(),
&mut bytes
);
}
}
let keypair = Ed25519KeyPair::from_seed_unchecked(&bytes).unwrap();
let privkey = to_base62(&bytes);
let pubkey = to_base62(keypair.public_key().as_ref());
@ -183,8 +196,8 @@ impl Crypto {
#[derive(Debug, PartialEq)]
pub enum MessageResult<P: Payload> {
Message(u8),
Initialized(NodeId, P),
InitializedWithReply(NodeId, P),
Initialized(P),
InitializedWithReply(P),
Reply,
None
}
@ -262,7 +275,7 @@ impl<P: Payload> PeerCrypto<P> {
}
match result {
InitResult::Continue => Ok(MessageResult::Reply),
InitResult::Success { peer_payload, node_id, is_initiator } => {
InitResult::Success { peer_payload, is_initiator } => {
self.core = self.get_init()?.take_core();
if self.core.is_none() {
self.unencrypted = true;
@ -275,13 +288,13 @@ impl<P: Payload> PeerCrypto<P> {
}
if !is_initiator {
if self.unencrypted {
return Ok(MessageResult::Initialized(node_id, peer_payload))
}
return Ok(MessageResult::Initialized(peer_payload))
}
assert!(!buffer.is_empty());
buffer.prepend_byte(MESSAGE_TYPE_ROTATION);
self.encrypt_message(buffer)?;
}
Ok(MessageResult::InitializedWithReply(node_id, peer_payload))
Ok(MessageResult::InitializedWithReply(peer_payload))
}
}
}
@ -415,12 +428,12 @@ mod tests {
debug!("Node1 <- Node2");
let res = node1.handle_message(&mut msg).unwrap();
assert_eq!(res, MessageResult::InitializedWithReply(node2.node_id, vec![]));
assert_eq!(res, MessageResult::InitializedWithReply(vec![]));
assert!(!msg.is_empty());
debug!("Node1 -> Node2");
let res = node2.handle_message(&mut msg).unwrap();
assert_eq!(res, MessageResult::InitializedWithReply(node1.node_id, vec![]));
assert_eq!(res, MessageResult::InitializedWithReply(vec![]));
assert!(!msg.is_empty());
debug!("Node1 <- Node2");

View File

@ -226,7 +226,7 @@ fn main() {
return
}
if args.genkey {
let (privkey, pubkey) = Crypto::generate_keypair();
let (privkey, pubkey) = Crypto::generate_keypair(args.password.as_deref());
println!("Private key: {}\nPublic key: {}\n", privkey, pubkey);
println!(
"Attention: Keep the private key secret and use only the public key on other nodes to establish trust."

View File

@ -33,12 +33,14 @@ pub struct PeerInfo {
#[derive(Debug, PartialEq)]
pub struct NodeInfo {
pub node_id: NodeId,
pub peers: PeerList,
pub claims: RangeList,
pub peer_timeout: Option<u16>
}
impl NodeInfo {
const PART_NODEID: u8 = 4;
const PART_CLAIMS: u8 = 2;
const PART_END: u8 = 0;
const PART_PEERS: u8 = 1;
@ -89,6 +91,7 @@ impl NodeInfo {
let mut peers = smallvec![];
let mut claims = smallvec![];
let mut peer_timeout = None;
let mut node_id = None;
loop {
let part = r.read_u8().map_err(|_| Error::Message("Truncated message"))?;
if part == Self::PART_END {
@ -105,6 +108,11 @@ impl NodeInfo {
peer_timeout =
Some(rp.read_u16::<NetworkEndian>().map_err(|_| Error::Message("Truncated message"))?)
}
Self::PART_NODEID => {
let mut data = [0; NODE_ID_BYTES];
rp.read_exact(&mut data).map_err(|_| Error::Message("Truncated message"))?;
node_id = Some(data);
}
_ => {
let mut data = vec![0; part_len];
rp.read_exact(&mut data).map_err(|_| Error::Message("Truncated message"))?;
@ -112,7 +120,11 @@ impl NodeInfo {
}
r = rp.into_inner();
}
Ok(Self { peers, claims, peer_timeout })
let node_id = match node_id {
Some(node_id) => node_id,
None => return Err(Error::Message("Payload without node_id"))
};
Ok(Self { node_id, peers, claims, peer_timeout })
}
pub fn decode<R: Read>(r: R) -> Result<Self, Error> {
@ -174,6 +186,9 @@ impl NodeInfo {
let len;
{
let mut cursor = Cursor::new(buffer.buffer());
Self::encode_part(&mut cursor, Self::PART_NODEID, |cursor| {
cursor.write_all(&self.node_id)
})?;
Self::encode_part(&mut cursor, Self::PART_PEERS, |cursor| self.encode_peer_list_part(cursor))?;
Self::encode_part(&mut cursor, Self::PART_CLAIMS, |mut cursor| {
for c in &self.claims {