diff --git a/examples/ping-pong-encrypted/Cargo.toml b/examples/ping-pong-encrypted/Cargo.toml new file mode 100644 index 0000000000..ccf3694b70 --- /dev/null +++ b/examples/ping-pong-encrypted/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ping-pong-encrypted" +version = "0.1.0" +edition = "2021" +authors = [ "SRI Community" ] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +binary_sv2 = { path = "../../protocols/v2/binary-sv2/binary-sv2" } +codec_sv2 = { path = "../../protocols/v2/codec-sv2", features = [ "noise_sv2" ] } +noise_sv2 = { path = "../../protocols/v2/noise-sv2" } +key-utils = { version = "^1.0.0", path = "../../utils/key-utils" } +network_helpers_sv2 = { version = "2.0.0", path = "../../roles/roles-utils/network-helpers", features =["with_tokio","with_buffer_pool"] } +rand = "0.8" +tokio = { version = "1", features = ["full"] } +async-channel = "1.5.1" \ No newline at end of file diff --git a/examples/ping-pong-encrypted/README.md b/examples/ping-pong-encrypted/README.md new file mode 100644 index 0000000000..ab4da5813e --- /dev/null +++ b/examples/ping-pong-encrypted/README.md @@ -0,0 +1,20 @@ +`ping-pong-encrypted` is an example of how to encode and decode SV2 binary frames (without any encryption layer) while leveraging the following crates: +- [`binary_sv2`](http://docs.rs/binary_sv2) +- [`codec_sv2`](http://docs.rs/codec_sv2) +- [`framing_sv2`](http://docs.rs/framing_sv2) (which is actually just re-exported by `codec_sv2`) +- [`noise_sv2`](http://docs.rs/noise_sv2) + +We establish a simple `Ping`-`Pong` protocol with a server and a client communicating over a TCP socket. + +The server expects to receive a `Ping` message encoded as a SV2 binary frame. +The `Ping` message contains a `nonce`, which is a `u8` generated randomly by the client. + +The client expects to get a `Pong` message in response, also encoded as a SV2 binary frame, with the same `nonce`. + +The messages are assigned arbitrary values for binary encoding: +```rust +pub const PING_MSG_TYPE: u8 = 0xfe; +pub const PONG_MSG_TYPE: u8 = 0xff; +``` + +All communication is encrypted with [SV2 Noise Protocol](https://stratumprotocol.org/specification/04-Protocol-Security/). \ No newline at end of file diff --git a/examples/ping-pong-encrypted/src/client.rs b/examples/ping-pong-encrypted/src/client.rs new file mode 100644 index 0000000000..8cff096331 --- /dev/null +++ b/examples/ping-pong-encrypted/src/client.rs @@ -0,0 +1,77 @@ +use crate::messages::{Message, Ping, Pong, PING_MSG_TYPE, PONG_MSG_TYPE}; +use codec_sv2::{Frame, HandshakeRole, Initiator, StandardSv2Frame}; +use key_utils::Secp256k1PublicKey; +use network_helpers_sv2::noise_connection_tokio::Connection; +use tokio::net::TcpStream; + +use crate::error::Error; + +pub async fn start_client(address: &str, k_pub: String) -> Result<(), Error> { + let stream = TcpStream::connect(address).await?; + + println!("CLIENT: Connected to server on {}", address); + + // parse server pubkey + let k_pub: Secp256k1PublicKey = k_pub.try_into()?; + + // noise handshake initiator + let initiator = Initiator::from_raw_k(k_pub.into_bytes())?; + + // channels for encrypted connection + let (receiver, sender, _, _) = + Connection::new(stream, HandshakeRole::Initiator(initiator)).await?; + + // create Ping message + let ping = Ping::new()?; + let ping_nonce = ping.get_nonce(); + let message = Message::Ping(ping); + + // create Ping frame + let ping_frame = + StandardSv2Frame::::from_message(message.clone(), PING_MSG_TYPE, 0, false) + .ok_or(Error::FrameFromMessage)?; + + // send Ping frame (sender takes care of encryption) + println!( + "CLIENT: Sending encrypted Ping to server with nonce: {}", + ping_nonce + ); + match sender.send(ping_frame.into()).await { + Ok(_) => {} + Err(_) => return Err(Error::Sender), + } + + // ok, we have successfully sent the ping message + // now it's time to receive and verify the pong response + // receiver already took care of decryption + let mut frame: StandardSv2Frame = match receiver.recv().await { + Ok(f) => f.try_into()?, + Err(_) => return Err(Error::Receiver), + }; + + let frame_header = frame.get_header().ok_or(Error::FrameHeader)?; + + // check message type on header + if frame_header.msg_type() != PONG_MSG_TYPE { + return Err(Error::FrameHeader); + } + + // decode frame payload + let decoded_payload: Pong = match binary_sv2::from_bytes(frame.payload()) { + Ok(pong) => pong, + Err(e) => return Err(Error::BinarySv2(e)), + }; + + // check if nonce is the same as ping + let pong_nonce = decoded_payload.get_nonce(); + if ping_nonce == pong_nonce { + println!( + "CLIENT: Received encrypted Pong with identical nonce as Ping: {}", + pong_nonce + ); + } else { + return Err(Error::Nonce); + } + + Ok(()) +} diff --git a/examples/ping-pong-encrypted/src/error.rs b/examples/ping-pong-encrypted/src/error.rs new file mode 100644 index 0000000000..4ba7af4c87 --- /dev/null +++ b/examples/ping-pong-encrypted/src/error.rs @@ -0,0 +1,59 @@ +#[derive(std::fmt::Debug)] +pub enum Error { + Io(std::io::Error), + CodecSv2(codec_sv2::Error), + FramingSv2(codec_sv2::framing_sv2::Error), + BinarySv2(binary_sv2::Error), + NoiseSv2(noise_sv2::Error), + NetworkHelpersSv2(network_helpers_sv2::Error), + KeyUtils(key_utils::Error), + Receiver, + Sender, + FrameHeader, + FrameFromMessage, + Nonce, + WrongMessage, + Tcp(std::io::Error), +} + +impl From for Error { + fn from(e: std::io::Error) -> Error { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: codec_sv2::Error) -> Error { + Error::CodecSv2(e) + } +} + +impl From for Error { + fn from(e: network_helpers_sv2::Error) -> Error { + Error::NetworkHelpersSv2(e) + } +} + +impl From for Error { + fn from(e: binary_sv2::Error) -> Error { + Error::BinarySv2(e) + } +} + +impl From for Error { + fn from(e: noise_sv2::Error) -> Error { + Error::NoiseSv2(e) + } +} + +impl From for Error { + fn from(e: key_utils::Error) -> Error { + Error::KeyUtils(e) + } +} + +impl From for Error { + fn from(e: codec_sv2::framing_sv2::Error) -> Error { + Error::FramingSv2(e) + } +} diff --git a/examples/ping-pong-encrypted/src/main.rs b/examples/ping-pong-encrypted/src/main.rs new file mode 100644 index 0000000000..4fe11461a2 --- /dev/null +++ b/examples/ping-pong-encrypted/src/main.rs @@ -0,0 +1,33 @@ +mod client; +mod error; +mod messages; +mod server; + +const ADDR: &str = "127.0.0.1:3333"; +const SERVER_PUBLIC_K: &str = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72"; +const SERVER_PRIVATE_K: &str = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n"; +const SERVER_CERT_VALIDITY: std::time::Duration = std::time::Duration::from_secs(3600); + +#[tokio::main] +async fn main() { + // Start the server in a separate thread + tokio::spawn(async { + server::start_server( + ADDR, + SERVER_PUBLIC_K.to_string(), + SERVER_PRIVATE_K.to_string(), + SERVER_CERT_VALIDITY, + ) + .await + .expect("Server failed"); + }); + + // Give the server a moment to start up + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Start the client + // Note: it only knows the server's pubkey! + client::start_client(ADDR, SERVER_PUBLIC_K.to_string()) + .await + .expect("Client failed"); +} diff --git a/examples/ping-pong-encrypted/src/messages.rs b/examples/ping-pong-encrypted/src/messages.rs new file mode 100644 index 0000000000..7aae0afd18 --- /dev/null +++ b/examples/ping-pong-encrypted/src/messages.rs @@ -0,0 +1,83 @@ +use crate::error::Error; +use binary_sv2::{ + binary_codec_sv2, + decodable::{DecodableField, FieldMarker}, + Deserialize, Serialize, +}; + +use rand::Rng; + +pub const PING_MSG_TYPE: u8 = 0xfe; +pub const PONG_MSG_TYPE: u8 = 0xff; + +// we derive binary_sv2::{Serialize, Deserialize} +// to allow for binary encoding +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Ping { + nonce: u8, +} + +impl Ping { + pub fn new() -> Result { + let mut rng = rand::thread_rng(); + let random: u8 = rng.gen(); + Ok(Self { nonce: random }) + } + + pub fn get_nonce(&self) -> u8 { + self.nonce + } +} + +// we derive binary_sv2::{Serialize, Deserialize} +// to allow for binary encoding +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Pong { + nonce: u8, +} + +impl<'decoder> Pong { + pub fn new(nonce: u8) -> Result { + Ok(Self { nonce }) + } + + pub fn get_nonce(&self) -> u8 { + self.nonce + } +} + +// unifies message types for noise_connection_tokio::Connection +#[derive(Clone)] +pub enum Message { + Ping(Ping), + Pong(Pong), +} + +impl binary_sv2::GetSize for Message { + fn get_size(&self) -> usize { + match self { + Self::Ping(ping) => ping.get_size(), + Self::Pong(pong) => pong.get_size(), + } + } +} + +impl From for binary_sv2::encodable::EncodableField<'_> { + fn from(m: Message) -> Self { + match m { + Message::Ping(p) => p.into(), + Message::Pong(p) => p.into(), + } + } +} + +impl Deserialize<'_> for Message { + fn get_structure(_v: &[u8]) -> std::result::Result, binary_sv2::Error> { + unimplemented!() + } + fn from_decoded_fields( + _v: Vec, + ) -> std::result::Result { + unimplemented!() + } +} diff --git a/examples/ping-pong-encrypted/src/server.rs b/examples/ping-pong-encrypted/src/server.rs new file mode 100644 index 0000000000..d58291ed5c --- /dev/null +++ b/examples/ping-pong-encrypted/src/server.rs @@ -0,0 +1,101 @@ +use crate::{ + error::Error, + messages::{Message, Ping, Pong, PING_MSG_TYPE, PONG_MSG_TYPE}, +}; +use codec_sv2::{Frame, StandardEitherFrame, StandardSv2Frame}; + +use codec_sv2::{HandshakeRole, Responder}; +use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; +use network_helpers_sv2::noise_connection_tokio::Connection; + +use async_channel::{Receiver, Sender}; +use tokio::net::TcpListener; + +pub async fn start_server( + address: &str, + k_pub: String, + k_priv: String, + cert_validity: std::time::Duration, +) -> Result<(), Error> { + let listener = TcpListener::bind(address).await?; + + // parse keys + let k_pub: Secp256k1PublicKey = k_pub.to_string().try_into()?; + let k_priv: Secp256k1SecretKey = k_priv.to_string().try_into()?; + + println!("SERVER: Listening on {}", address); + + loop { + let (stream, _) = listener.accept().await?; + tokio::spawn(async move { + // noise handshake responder + let responder = Responder::from_authority_kp( + &k_pub.into_bytes(), + &k_priv.into_bytes(), + cert_validity, + )?; + + // channels for encrypted connection + let (receiver, sender, _, _) = + Connection::new(stream, HandshakeRole::Responder(responder)).await?; + + // handle encrypted connection + handle_connection(receiver, sender).await?; + Ok::<(), Error>(()) + }); + } +} + +async fn handle_connection( + receiver: Receiver>, + sender: Sender>, +) -> Result<(), Error> { + // first, we need to read the ping frame + // receiver already took care of decryption + let mut frame: StandardSv2Frame = match receiver.recv().await { + Ok(f) => f.try_into()?, + Err(_) => return Err(Error::Receiver), + }; + + let frame_header = frame.get_header().ok_or(Error::FrameHeader)?; + + // check message type on header + if frame_header.msg_type() != PING_MSG_TYPE { + return Err(Error::WrongMessage); + } + + // decode frame payload + let decoded_payload: Ping = match binary_sv2::from_bytes(frame.payload()) { + Ok(ping) => ping, + Err(e) => return Err(Error::BinarySv2(e)), + }; + + // ok, we have successfully received the ping message + // now it's time to send the pong response + + // we need the ping nonce to create our pong response + let ping_nonce = decoded_payload.get_nonce(); + + println!("SERVER: Received encrypted Ping with nonce: {}", ping_nonce); + + // create Pong message + let pong = Pong::new(ping_nonce)?; + let message = Message::Pong(pong.clone()); + + // create Pong frame + let pong_frame = + StandardSv2Frame::::from_message(message.clone(), PONG_MSG_TYPE, 0, false) + .ok_or(Error::FrameFromMessage)?; + + // respond Pong (sender takes care of encryption) + println!( + "SERVER: Sending encrypted Pong to client with nonce: {}", + pong.get_nonce() + ); + match sender.send(pong_frame.into()).await { + Ok(_) => {} + Err(_) => return Err(Error::Sender), + } + + Ok(()) +}