From 92e9aa0ed964a8568b5d37dfcb2b4ad61658560b Mon Sep 17 00:00:00 2001 From: Bastien Faivre Date: Wed, 8 Jan 2025 13:55:56 +0100 Subject: [PATCH] feat: transaction signature --- dog/src/behaviour.rs | 84 ++++++++++++++++++--- dog/src/config.rs | 9 ++- dog/src/dog.rs | 1 + dog/src/error.rs | 13 +++- dog/src/generated/dog/pb.rs | 8 ++ dog/src/generated/rpc.proto | 2 + dog/src/protocol.rs | 121 ++++++++++++++++++++++++++----- dog/src/types.rs | 13 +++- dog/tests/src/lib.rs | 4 +- dog/tests/tests/dog.rs | 2 +- examples/simple/src/behaviour.rs | 9 ++- 11 files changed, 228 insertions(+), 38 deletions(-) diff --git a/dog/src/behaviour.rs b/dog/src/behaviour.rs index 51cac72..6d7f7af 100644 --- a/dog/src/behaviour.rs +++ b/dog/src/behaviour.rs @@ -8,12 +8,14 @@ use std::{ use futures::FutureExt; use futures_timer::Delay; use libp2p::{ + identity::Keypair, swarm::{ behaviour::ConnectionEstablished, ConnectionClosed, FromSwarm, NetworkBehaviour, ToSwarm, }, PeerId, }; use lru::LruCache; +use quick_protobuf::{MessageWrite, Writer}; use rand::seq::IteratorRandom; use crate::{ @@ -21,7 +23,9 @@ use crate::{ dog::{Controller, Route, Router}, error::PublishError, handler::{Handler, HandlerEvent, HandlerIn}, + protocol::SIGNING_PREFIX, rpc::Sender, + rpc_proto::proto, transform::{DataTransform, IdentityTransform}, types::{ ControlAction, HaveTx, PeerConnections, RawTransaction, ResetRoute, RpcOut, Transaction, @@ -32,11 +36,11 @@ use crate::{ /// Determines if published transaction should be signed or not. #[derive(Debug)] pub enum TransactionAuthenticity { - // /// Transaction signing is enabled. The author will be the owner of the key and - // /// the sequence number will be linearly increasing. - // Signed(Keypair), + /// Transaction signing is enabled. The author will be the owner of the key and + /// the sequence number will be linearly increasing. + Signed(Keypair), /// Transaction signing is disabled. The specified [`PeerId`] will be used as the author - /// of all published transactions. The sequence number will be randomized. + /// of all published transactions. The sequence number will be linearly increasing. Author(PeerId), } @@ -61,12 +65,12 @@ pub enum Event { // A data structure for storing configuration for publishing transactions. enum PublishConfig { - // Signing { - // keypair: Keypair, - // author: PeerId, - // inline_key: Option>, - // last_seqno: SequenceNumber, - // }, + Signing { + keypair: Keypair, + author: PeerId, + inline_key: Option>, + last_seqno: SequenceNumber, + }, Author { author: PeerId, last_seqno: SequenceNumber, @@ -102,6 +106,7 @@ impl SequenceNumber { impl PublishConfig { pub(crate) fn get_own_id(&self) -> PeerId { match self { + Self::Signing { author, .. } => *author, Self::Author { author, .. } => *author, } } @@ -110,6 +115,26 @@ impl PublishConfig { impl From for PublishConfig { fn from(authenticity: TransactionAuthenticity) -> Self { match authenticity { + TransactionAuthenticity::Signed(keypair) => { + let public_key = keypair.public(); + let key_enc = public_key.encode_protobuf(); + let key = if key_enc.len() <= 42 { + // The public key can be inlined in [`rpc_proto::proto::Transaction::from`], so we + // don't include it specifically in the + // [`rpc_proto::proto::Transaction::key`] field. + None + } else { + // Include the protobuf encoding of the public key in the message. + Some(key_enc) + }; + + PublishConfig::Signing { + keypair, + author: public_key.to_peer_id(), + inline_key: key, + last_seqno: SequenceNumber::new(), + } + } TransactionAuthenticity::Author(author) => PublishConfig::Author { author, last_seqno: SequenceNumber::new(), @@ -248,6 +273,43 @@ where fn build_raw_transaction(&mut self, data: Vec) -> Result { match &mut self.publish_config { + PublishConfig::Signing { + ref keypair, + author, + inline_key, + last_seqno, + } => { + let seqno = last_seqno.next(); + + let signature = { + let transaction = proto::Transaction { + from: author.to_bytes(), + seqno, + data: data.clone(), + signature: vec![], + key: vec![], + }; + + let mut buf = Vec::with_capacity(transaction.get_size()); + let mut writer = Writer::new(&mut buf); + + transaction + .write_message(&mut writer) + .expect("Encoding to succeed"); + + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + keypair.sign(&signature_bytes)? + }; + + Ok(RawTransaction { + from: *author, + seqno, + data, + signature: Some(signature.to_vec()), + key: inline_key.clone(), + }) + } PublishConfig::Author { author, last_seqno } => { let seqno = last_seqno.next(); @@ -255,6 +317,8 @@ where from: *author, seqno, data, + signature: None, + key: None, }) } } diff --git a/dog/src/config.rs b/dog/src/config.rs index db2a503..03199e3 100644 --- a/dog/src/config.rs +++ b/dog/src/config.rs @@ -10,7 +10,7 @@ use crate::{ pub enum ValidationMode { /// This is the default setting. This settings validates all fields of the transaction. Strict, - /// This setting does not check any fields of the transaction. + /// This setting only checks that the author field is valid None, } @@ -222,6 +222,13 @@ impl ConfigBuilder { self } + /// Determines the level of validation used when receiving transactions. See [`ValidationMode`] + /// for the available types. the default is `ValidationMode::Strict`. + pub fn validation_mode(&mut self, validation_mode: ValidationMode) -> &mut Self { + self.config.protocol.validation_mode = validation_mode; + self + } + /// Constructs a `Config` from the parameters set in the builder. pub fn build(&self) -> Result { // TODO: validate config diff --git a/dog/src/dog.rs b/dog/src/dog.rs index 0e31e39..7e1b2f3 100644 --- a/dog/src/dog.rs +++ b/dog/src/dog.rs @@ -32,6 +32,7 @@ impl Display for Route { } pub(crate) struct Router { + // TODO: is it better to use a HashMap>? disabled_routes: Vec, } diff --git a/dog/src/error.rs b/dog/src/error.rs index b8a49a6..6d0049d 100644 --- a/dog/src/error.rs +++ b/dog/src/error.rs @@ -1,7 +1,11 @@ +use libp2p::identity::SigningError; + #[derive(Debug)] pub enum PublishError { /// This transaction has already been published. Duplicate, + /// An error occurred while signing the transaction. + SigningError(SigningError), /// There were no peers to send this transaction to. InsufficientPeers, /// The overal transaction was too large. @@ -34,11 +38,18 @@ impl From for PublishError { } } +impl From for PublishError { + fn from(error: SigningError) -> Self { + PublishError::SigningError(error) + } +} + #[derive(Debug)] pub enum ValidationError { /// The PeerId was invalid. InvalidPeerId, - // TODO: complete with more error types as the development progresses. + /// The signature was invalid. + InvalidSignature, } impl std::fmt::Display for ValidationError { diff --git a/dog/src/generated/dog/pb.rs b/dog/src/generated/dog/pb.rs index 5de1dc8..431c356 100644 --- a/dog/src/generated/dog/pb.rs +++ b/dog/src/generated/dog/pb.rs @@ -55,6 +55,8 @@ pub struct Transaction { pub from: Vec, pub seqno: u64, pub data: Vec, + pub signature: Vec, + pub key: Vec, } impl<'a> MessageRead<'a> for Transaction { @@ -65,6 +67,8 @@ impl<'a> MessageRead<'a> for Transaction { Ok(10) => msg.from = r.read_bytes(bytes)?.to_owned(), Ok(16) => msg.seqno = r.read_uint64(bytes)?, Ok(26) => msg.data = r.read_bytes(bytes)?.to_owned(), + Ok(34) => msg.signature = r.read_bytes(bytes)?.to_owned(), + Ok(42) => msg.key = r.read_bytes(bytes)?.to_owned(), Ok(t) => { r.read_unknown(bytes, t)?; } Err(e) => return Err(e), } @@ -79,12 +83,16 @@ impl MessageWrite for Transaction { + if self.from.is_empty() { 0 } else { 1 + sizeof_len((&self.from).len()) } + if self.seqno == 0u64 { 0 } else { 1 + sizeof_varint(*(&self.seqno) as u64) } + if self.data.is_empty() { 0 } else { 1 + sizeof_len((&self.data).len()) } + + if self.signature.is_empty() { 0 } else { 1 + sizeof_len((&self.signature).len()) } + + if self.key.is_empty() { 0 } else { 1 + sizeof_len((&self.key).len()) } } fn write_message(&self, w: &mut Writer) -> Result<()> { if !self.from.is_empty() { w.write_with_tag(10, |w| w.write_bytes(&**&self.from))?; } if self.seqno != 0u64 { w.write_with_tag(16, |w| w.write_uint64(*&self.seqno))?; } if !self.data.is_empty() { w.write_with_tag(26, |w| w.write_bytes(&**&self.data))?; } + if !self.signature.is_empty() { w.write_with_tag(34, |w| w.write_bytes(&**&self.signature))?; } + if !self.key.is_empty() { w.write_with_tag(42, |w| w.write_bytes(&**&self.key))?; } Ok(()) } } diff --git a/dog/src/generated/rpc.proto b/dog/src/generated/rpc.proto index 2c23d1b..c5435ab 100644 --- a/dog/src/generated/rpc.proto +++ b/dog/src/generated/rpc.proto @@ -11,6 +11,8 @@ message Transaction { bytes from = 1; uint64 seqno = 2; bytes data = 3; + bytes signature = 4; + bytes key = 5; } message ControlMessage { diff --git a/dog/src/protocol.rs b/dog/src/protocol.rs index 7b48b3f..d80b825 100644 --- a/dog/src/protocol.rs +++ b/dog/src/protocol.rs @@ -5,8 +5,10 @@ use futures::future; use libp2p::{ core::UpgradeInfo, futures::{AsyncRead, AsyncWrite}, + identity::PublicKey, InboundUpgrade, OutboundUpgrade, PeerId, StreamProtocol, }; +use quick_protobuf::Writer; use void::Void; use crate::{ @@ -17,6 +19,8 @@ use crate::{ types::{ControlAction, RawTransaction, ResetRoute, Rpc}, }; +pub(crate) const SIGNING_PREFIX: &[u8] = b"libp2p-dog:"; + const DOG_PROTOCOL: &str = "/dog/1.0.0"; const DEFAULT_MAX_TRANSMIT_SIZE: usize = 65536; @@ -98,6 +102,46 @@ impl DogCodec { codec, } } + + fn verify_signature(transaction: &proto::Transaction) -> bool { + use quick_protobuf::MessageWrite; + + let peer_id = match PeerId::from_bytes(&transaction.from) { + Ok(peer_id) => peer_id, + Err(_) => { + tracing::warn!("Signature verification failed: invalid peer id"); + return false; + } + }; + + let public_key = match PublicKey::try_decode_protobuf(&transaction.key) { + Ok(key) => key, + _ => match PublicKey::try_decode_protobuf(&peer_id.to_bytes()[2..]) { + Ok(v) => v, + Err(_) => { + tracing::warn!("Signature verification failed: invalid public key"); + return false; + } + }, + }; + + if peer_id != public_key.to_peer_id() { + tracing::warn!("Signature verification failed: peer id does not match public key"); + return false; + } + + let mut transaction_sig = transaction.clone(); + transaction_sig.signature = vec![]; + transaction_sig.key = vec![]; + let mut buf = Vec::with_capacity(transaction_sig.get_size()); + let mut writer = Writer::new(&mut buf); + transaction_sig + .write_message(&mut writer) + .expect("Encoding to succeed"); + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + public_key.verify(&signature_bytes, &transaction.signature) + } } impl Encoder for DogCodec { @@ -126,40 +170,79 @@ impl Decoder for DogCodec { let mut invalid_transactions = Vec::new(); for transaction in rpc.txs.into_iter() { - // TODO: Implement all the validation logic here. - let mut verify_source = true; + let mut verify_signature = false; match self.validation_mode { ValidationMode::Strict => { - verify_source = true; + verify_signature = true; } ValidationMode::None => {} } - let source = if verify_source { - match PeerId::from_bytes(&transaction.from) { - Ok(peer_id) => peer_id, - Err(_) => { - invalid_transactions.push(( - RawTransaction { - from: PeerId::random(), - seqno: transaction.seqno, - data: transaction.data, + // Always verify the author of the transaction + let source = match PeerId::from_bytes(&transaction.from) { + Ok(peer_id) => peer_id, + Err(_) => { + invalid_transactions.push(( + RawTransaction { + from: PeerId::random(), + seqno: transaction.seqno, + data: transaction.data, + signature: if transaction.signature.is_empty() { + None + } else { + Some(transaction.signature) }, - ValidationError::InvalidPeerId, - )); - continue; - } + key: if transaction.key.is_empty() { + None + } else { + Some(transaction.key) + }, + }, + ValidationError::InvalidPeerId, + )); + continue; } - } else { - // TODO: Temporary solution to showcase the validation logic. - PeerId::random() }; + if verify_signature && !DogCodec::verify_signature(&transaction) { + tracing::warn!("Invalid signature for the received transaction"); + + invalid_transactions.push(( + RawTransaction { + from: source, + seqno: transaction.seqno, + data: transaction.data, + signature: if transaction.signature.is_empty() { + None + } else { + Some(transaction.signature) + }, + key: if transaction.key.is_empty() { + None + } else { + Some(transaction.key) + }, + }, + ValidationError::InvalidSignature, + )); + continue; + } + transactions.push(RawTransaction { from: source, seqno: transaction.seqno, data: transaction.data, + signature: if transaction.signature.is_empty() { + None + } else { + Some(transaction.signature) + }, + key: if transaction.key.is_empty() { + None + } else { + Some(transaction.key) + }, }) } diff --git a/dog/src/types.rs b/dog/src/types.rs index 1d21986..5f101c2 100644 --- a/dog/src/types.rs +++ b/dog/src/types.rs @@ -48,7 +48,10 @@ pub struct RawTransaction { pub seqno: u64, /// The content of the transaction. pub data: Vec, - // TODO: plus some other fields such as signature, etc. + /// The signature of the transaction if it is signed. + pub signature: Option>, + /// The public key of the transaction if it is signed. + pub key: Option>, // TODO: carry history of reached peers? } @@ -58,6 +61,14 @@ impl From for proto::Transaction { from: tx.from.to_bytes(), seqno: tx.seqno, data: tx.data.to_vec(), + signature: match tx.signature { + Some(sig) => sig.to_vec(), + None => vec![], + }, + key: match tx.key { + Some(key) => key.to_vec(), + None => vec![], + }, } } } diff --git a/dog/tests/src/lib.rs b/dog/tests/src/lib.rs index 8e12a2e..44f01bd 100644 --- a/dog/tests/src/lib.rs +++ b/dog/tests/src/lib.rs @@ -144,9 +144,7 @@ impl TestNode { .unwrap() .with_behaviour(|key| { libp2p_dog::Behaviour::::new( - libp2p_dog::TransactionAuthenticity::Author(PeerId::from_public_key( - &key.public(), - )), + libp2p_dog::TransactionAuthenticity::Signed(key.clone()), config, ) .expect("Failed to create dog behaviour") diff --git a/dog/tests/tests/dog.rs b/dog/tests/tests/dog.rs index ac853a1..cd3bd44 100644 --- a/dog/tests/tests/dog.rs +++ b/dog/tests/tests/dog.rs @@ -143,7 +143,7 @@ pub async fn n_nodes_aligned() { // one of the transactions twice. For example, node 3 will receive the transaction originated from // node 1 twice: via node 0 and node 2. // We expect all nodes to request one of its neighbors to stop sending transactions that have the -// corresponding transaction's origin. +// same origin as the duplicated transaction. #[tokio::test] pub async fn simple_redundancy() { let config = libp2p_dog::ConfigBuilder::default() diff --git a/examples/simple/src/behaviour.rs b/examples/simple/src/behaviour.rs index f3b6150..dd75a53 100644 --- a/examples/simple/src/behaviour.rs +++ b/examples/simple/src/behaviour.rs @@ -1,4 +1,4 @@ -use libp2p::{identity::Keypair, swarm::NetworkBehaviour, PeerId}; +use libp2p::{identity::Keypair, swarm::NetworkBehaviour}; use crate::config::Config; @@ -22,8 +22,13 @@ pub(crate) struct MyBehaviour { impl MyBehaviour { pub(crate) fn new(_config: &Config, key: &Keypair) -> Self { let dog = libp2p_dog::Behaviour::new( - libp2p_dog::TransactionAuthenticity::Author(PeerId::from_public_key(&key.public())), + libp2p_dog::TransactionAuthenticity::Signed(key.clone()), libp2p_dog::Config::default(), + // libp2p_dog::TransactionAuthenticity::Author(key.public().to_peer_id()), + // libp2p_dog::ConfigBuilder::default() + // .validation_mode(libp2p_dog::ValidationMode::None) + // .build() + // .expect("Failed to create dog behaviour"), ) .expect("Failed to create dog behaviour");