From cdbbb0f8fc9a3b6d39cbddc45bbce0e26be2f429 Mon Sep 17 00:00:00 2001 From: RJ Rybarczyk Date: Fri, 11 Oct 2024 14:19:30 -0400 Subject: [PATCH 01/15] Add codec unencrypted and encrypted ex Co-authored-by: plebhash <147345153+plebhash@users.noreply.github.com> --- protocols/v2/codec-sv2/Cargo.toml | 5 +- protocols/v2/codec-sv2/examples/encrypted.rs | 208 ++++++++++++++++++ .../v2/codec-sv2/examples/unencrypted.rs | 135 ++++++++++++ 3 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 protocols/v2/codec-sv2/examples/encrypted.rs create mode 100644 protocols/v2/codec-sv2/examples/unencrypted.rs diff --git a/protocols/v2/codec-sv2/Cargo.toml b/protocols/v2/codec-sv2/Cargo.toml index 092bcb986..f527277e2 100644 --- a/protocols/v2/codec-sv2/Cargo.toml +++ b/protocols/v2/codec-sv2/Cargo.toml @@ -20,9 +20,10 @@ const_sv2 = { version = "2.0.0", path = "../../../protocols/v2/const-sv2"} buffer_sv2 = { version = "1.0.0", path = "../../../utils/buffer"} tracing = { version = "0.1"} - +[dev-dependencies] +key-utils = { version = "^1.0.0", path = "../../../utils/key-utils" } [features] with_serde = ["binary_sv2/with_serde", "serde", "framing_sv2/with_serde", "buffer_sv2/with_serde"] with_buffer_pool = ["framing_sv2/with_buffer_pool"] -no_std = [] \ No newline at end of file +no_std = [] diff --git a/protocols/v2/codec-sv2/examples/encrypted.rs b/protocols/v2/codec-sv2/examples/encrypted.rs new file mode 100644 index 000000000..6713c4679 --- /dev/null +++ b/protocols/v2/codec-sv2/examples/encrypted.rs @@ -0,0 +1,208 @@ +// # Using Sv2 Codec with Noise Encryption +// +// This example demonstrates how to use the `codec-sv2` crate to encode and decode Sv2 frames +// with Noise protocol encryption over a TCP connection. It showcases how to: +// +// * Perform a Noise handshake between the sender and receiver. +// * Create an arbitrary custom message type (`CustomMessage`) for encryption/encoding and +// decryption/decoding. +// * Encode the message into an encrypted Sv2 frame using Noise. +// * Send the encrypted frame over a TCP connection. +// * Decode the encrypted Sv2 frame on the receiving side after completing the Noise handshake. +// +// ## Run +// +// ``` +// cargo run --example encrypted --features noise_sv2 +// ``` + +use binary_sv2::{binary_codec_sv2, Deserialize, Serialize}; +#[cfg(feature = "noise_sv2")] +use codec_sv2::{ + Error, HandshakeRole, Initiator, NoiseEncoder, Responder, StandardEitherFrame, + StandardNoiseDecoder, StandardSv2Frame, State, Sv2Frame, +}; +#[cfg(feature = "noise_sv2")] +use const_sv2::{ + INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE, RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE, +}; +#[cfg(feature = "noise_sv2")] +use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; +use std::convert::TryInto; +#[cfg(feature = "noise_sv2")] +use std::{ + io::{Read, Write}, + net::{TcpListener, TcpStream}, +}; + +// Arbitrary message type. +// Supported Sv2 message types are listed in the [Sv2 Spec Message +// Types](https://github.com/stratum-mining/sv2-spec/blob/main/08-Message-Types.md). +#[cfg(feature = "noise_sv2")] +const CUSTOM_MSG_TYPE: u8 = 0xff; + +// Emulate a TCP connection +#[cfg(feature = "noise_sv2")] +const TCP_ADDR: &str = "127.0.0.1:3333"; + +#[cfg(feature = "noise_sv2")] +const AUTHORITY_PUBLIC_K: &str = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72"; +#[cfg(feature = "noise_sv2")] +const AUTHORITY_PRIVATE_K: &str = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n"; +#[cfg(feature = "noise_sv2")] +const CERT_VALIDITY: std::time::Duration = std::time::Duration::from_secs(3600); + +// Example message type. +// In practice, all Sv2 messages are defined in the following crates: +// * `common_messages_sv2` +// * `mining_sv2` +// * `job_declaration_sv2` +// * `template_distribution_sv2` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CustomMessage { + nonce: u16, +} + +#[cfg(feature = "noise_sv2")] +fn main() { + // Start a receiving listener + let listener_receiver = TcpListener::bind(TCP_ADDR).expect("Failed to bind TCP listener"); + + // Start a sender stream + let mut stream_sender: TcpStream = + TcpStream::connect(TCP_ADDR).expect("Failed to connect to TCP stream"); + + // Start a receiver stream + let mut stream_receiver: TcpStream = listener_receiver + .incoming() + .next() + .expect("Failed to accept incoming TCP stream") + .expect("Failed to connect to incoming TCP stream"); + + // Handshake + let authority_public_k: Secp256k1PublicKey = AUTHORITY_PUBLIC_K + .to_string() + .try_into() + .expect("Failed to convert receiver public key to Secp256k1PublicKey"); + + let authority_private_k: Secp256k1SecretKey = AUTHORITY_PRIVATE_K + .to_string() + .try_into() + .expect("Failed to convert receiver private key to Secp256k1PublicKey"); + + let initiator = Initiator::from_raw_k(authority_public_k.into_bytes()) + .expect("Failed to create initiator role from raw pub key"); + + let responder = Responder::from_authority_kp( + &authority_public_k.into_bytes(), + &authority_private_k.into_bytes(), + CERT_VALIDITY, + ) + .expect("Failed to initialize responder from pub/key pair and/or cert"); + + let mut sender_state = State::initialized(HandshakeRole::Initiator(initiator)); + let mut receiver_state = State::initialized(HandshakeRole::Responder(responder)); + + let first_message = sender_state + .step_0() + .expect("Initiator failed first step of handshake"); + let first_message: [u8; RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE] = first_message + .get_payload_when_handshaking() + .try_into() + .expect("Handshake remote invlaid message"); + + let (second_message, receiver_state) = receiver_state + .step_1(first_message) + .expect("Responder failed second step of handshake"); + let second_message: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE] = second_message + .get_payload_when_handshaking() + .try_into() + .expect("Handshake remote invlaid message"); + + let sender_state = sender_state + .step_2(second_message) + .expect("Initiator failed third step of handshake"); + + let mut sender_state = match sender_state { + State::Transport(c) => State::with_transport_mode(c), + _ => panic!("todo"), + }; + + let mut receiver_state = match receiver_state { + State::Transport(c) => State::with_transport_mode(c), + _ => panic!("todo"), + }; + + // Create a message + let nonce = 1337; + let msg = CustomMessage { nonce }; + let msg_type = CUSTOM_MSG_TYPE; + // Unique identifier of the extension describing the protocol message, as defined by Sv2 Framing + let extension_type = 0; + // This message is intended for the receiver, so set to false + let channel_msg = false; + + let frame = StandardEitherFrame::::Sv2( + Sv2Frame::from_message(msg, msg_type, extension_type, channel_msg) + .expect("Failed to create the frame"), + ); + + let mut encoder = NoiseEncoder::::new(); + let encoded_frame = encoder + .encode(frame, &mut sender_state) + .expect("Failed to encode the frame"); + + // Send the encoded frame + stream_sender + .write_all(encoded_frame.as_slice()) + .expect("Failed to send the encoded frame"); + + // Initialize the decoder + let mut decoder = StandardNoiseDecoder::::new(); + + let mut decoded_frame; + + // Continuously read the frame from the TCP stream into the decoder buffer until the full + // message is received. + // + // Note: The length of the payload is defined in a header field. Every call to `next_frame` + // will return a `MissingBytes` error, until the full payload is received. + loop { + let decoder_buf = decoder.writable(); + + // Read the frame header into the decoder buffer + stream_receiver + .read_exact(decoder_buf) + .expect("Failed to read the encoded frame header"); + + let result = decoder.next_frame(&mut receiver_state); + match result { + Ok(frame) => { + let frame: StandardSv2Frame = frame + .try_into() + .expect("Failed to decode frame into Sv2Frame"); + decoded_frame = frame; + break; + } + Err(Error::MissingBytes(_)) => {} + Err(_) => panic!("Failed to decode the frame"), + } + } + + // Parse the decoded frame header and payload + let decoded_frame_header = decoded_frame + .get_header() + .expect("Failed to get the frame header"); + + let decoded_msg: CustomMessage = binary_sv2::from_bytes(decoded_frame.payload()) + .expect("Failed to extract the message from the payload"); + + // Assert that the decoded message is as expected + assert_eq!(decoded_frame_header.msg_type(), CUSTOM_MSG_TYPE); + assert_eq!(decoded_msg.nonce, nonce); +} + +#[cfg(not(feature = "noise_sv2"))] +fn main() { + eprintln!("Noise feature not enabled. Skipping example."); +} diff --git a/protocols/v2/codec-sv2/examples/unencrypted.rs b/protocols/v2/codec-sv2/examples/unencrypted.rs new file mode 100644 index 000000000..4b5d53c56 --- /dev/null +++ b/protocols/v2/codec-sv2/examples/unencrypted.rs @@ -0,0 +1,135 @@ +// # Using Sv2 Codec Without Encryption +// +// This example demonstrates how to use the `codec-sv2` crate to encode and decode Sv2 frames +// without encryption over a TCP connection. It showcases how to: +// +// * Create an arbitrary custom message type (`CustomMessage`) for encoding and decoding. +// * Encode the message into a Sv2 frame. +// * Send the encoded frame over a TCP connection. +// * Decode the Sv2 frame on the receiving side and extract the original message. +// +// ## Run +// +// ``` +// cargo run --example unencrypted +// ``` + +use binary_sv2::{binary_codec_sv2, Deserialize, Serialize}; +use codec_sv2::{Encoder, Error, StandardDecoder, StandardSv2Frame, Sv2Frame}; +use std::{ + convert::TryInto, + io::{Read, Write}, + net::{TcpListener, TcpStream}, +}; + +// Arbitrary message type. +// Supported Sv2 message types are listed in the [Sv2 Spec Message +// Types](https://github.com/stratum-mining/sv2-spec/blob/main/08-Message-Types.md). +const CUSTOM_MSG_TYPE: u8 = 0xff; + +// Emulate a TCP connection +const TCP_ADDR: &str = "127.0.0.1:3333"; + +// Example message type. +// In practice, all Sv2 messages are defined in the following crates: +// * `common_messages_sv2` +// * `mining_sv2` +// * `job_declaration_sv2` +// * `template_distribution_sv2` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CustomMessage { + nonce: u16, +} + +fn main() { + // Start a receiving listener + let listener_receiver = TcpListener::bind(TCP_ADDR).expect("Failed to bind TCP listener"); + + // Start a sender stream + let stream_sender: TcpStream = + TcpStream::connect(TCP_ADDR).expect("Failed to connect to TCP stream"); + + // Start a receiver stream + let stream_receiver: TcpStream = listener_receiver + .incoming() + .next() + .expect("Failed to accept incoming TCP stream") + .expect("Failed to connect to incoming TCP stream"); + + // Server Side + + // Create a message + let nonce = 1337; + let msg = CustomMessage { nonce }; + let msg_type = CUSTOM_MSG_TYPE; + // Unique identifier of the extension describing the protocol message, as defined by Sv2 Framing + let extension_type = 0; + // This message is intended for the receiver, so set to false + let channel_msg = false; + sender_side(stream_sender, msg, msg_type, extension_type, channel_msg); + + // Receiver Side + let mut decoded_frame = receiver_side(stream_receiver); + + // Parse the decoded frame header and payload + let decoded_frame_header = decoded_frame + .get_header() + .expect("Failed to get the frame header"); + let decoded_msg: CustomMessage = binary_sv2::from_bytes(decoded_frame.payload()) + .expect("Failed to extract the message from the payload"); + + // Assert that the decoded message is as expected + assert_eq!(decoded_frame_header.msg_type(), CUSTOM_MSG_TYPE); + assert_eq!(decoded_msg.nonce, nonce); +} + +fn sender_side( + mut stream_sender: TcpStream, + msg: CustomMessage, + msg_type: u8, + extension_type: u16, + channel_msg: bool, +) { + // Create the frame + let frame = + StandardSv2Frame::::from_message(msg, msg_type, extension_type, channel_msg) + .expect("Failed to create the frame"); + + // Encode the frame + let mut encoder = Encoder::::new(); + let encoded_frame = encoder + .encode(frame.clone()) + .expect("Failed to encode the frame"); + + // Send the encoded frame + stream_sender + .write_all(encoded_frame) + .expect("Failed to send the encoded frame"); +} + +fn receiver_side(mut stream_receiver: TcpStream) -> Sv2Frame> { + // Initialize the decoder + let mut decoder = StandardDecoder::::new(); + + // Continuously read the frame from the TCP stream into the decoder buffer until the full + // message is received. + // + // Note: The length of the payload is defined in a header field. Every call to `next_frame` + // will return a `MissingBytes` error, until the full payload is received. + loop { + let decoder_buf = decoder.writable(); + + // Read the frame header into the decoder buffer + stream_receiver + .read_exact(decoder_buf) + .expect("Failed to read the encoded frame header"); + + match decoder.next_frame() { + Ok(decoded_frame) => { + return decoded_frame; + } + Err(Error::MissingBytes(_)) => {} + Err(_) => panic!("Failed to decode the frame"), + } + } +} From 4d9017f9c83ef61515e46ed328b1f86cf0e5c8b6 Mon Sep 17 00:00:00 2001 From: RJ Rybarczyk Date: Fri, 11 Oct 2024 14:22:20 -0400 Subject: [PATCH 02/15] codec doc cmts --- protocols/v2/codec-sv2/src/decoder.rs | 235 +++++++++++++++++++++++--- protocols/v2/codec-sv2/src/encoder.rs | 121 +++++++++++++ protocols/v2/codec-sv2/src/error.rs | 158 +++++++++++------ protocols/v2/codec-sv2/src/lib.rs | 146 ++++++++++++++-- protocols/v2/sv2-ffi/sv2.h | 36 ++-- 5 files changed, 594 insertions(+), 102 deletions(-) diff --git a/protocols/v2/codec-sv2/src/decoder.rs b/protocols/v2/codec-sv2/src/decoder.rs index 4d1440018..751ae17d5 100644 --- a/protocols/v2/codec-sv2/src/decoder.rs +++ b/protocols/v2/codec-sv2/src/decoder.rs @@ -1,3 +1,28 @@ +// # Decoder +// +// Provides utilities for decoding messages held by Sv2 frames, with or without Noise protocol +// support. +// +// It includes primitives to both decode encoded standard Sv2 frames and to decrypt and decode +// Noise-encrypted encoded Sv2 frames, ensuring secure communication when required. +// +// ## Usage +// All messages passed between Sv2 roles are encoded as Sv2 frames. These frames are decoded using +// primitives in this module. There are two types of decoders for reading these frames: one for +// regular Sv2 frames [`StandardDecoder`], and another for Noise-encrypted frames +// [`StandardNoiseDecoder`]. Both decoders manage the deserialization of incoming data and, when +// applicable, the decryption of the data upon receiving the transmitted message. +// +// ### Buffer Management +// +// The decoders rely on buffers to hold intermediate data during the decoding process. +// +// - When the `with_buffer_pool` feature is enabled, the internal `Buffer` type is backed by a +// pool-allocated buffer [`binary_sv2::BufferPool`], providing more efficient memory usage, +// particularly in high-throughput scenarios. +// - If this feature is not enabled, a system memory buffer [`binary_sv2::BufferFromSystemMemory`] +// is used for simpler applications where memory efficiency is less critical. + #[cfg(feature = "noise_sv2")] use binary_sv2::Deserialize; #[cfg(feature = "noise_sv2")] @@ -18,38 +43,101 @@ use framing_sv2::{ #[cfg(feature = "noise_sv2")] use noise_sv2::NoiseCodec; +#[cfg(feature = "noise_sv2")] +use crate::error::Error; +use crate::error::Result; + +use crate::Error::MissingBytes; +#[cfg(feature = "noise_sv2")] +use crate::State; + #[cfg(not(feature = "with_buffer_pool"))] use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory as Buffer}; #[cfg(feature = "with_buffer_pool")] use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory, BufferPool}; + +// The buffer type for holding intermediate data during decoding. +// +// When the `with_buffer_pool` feature is enabled, `Buffer` is a pool-allocated buffer type +// [`BufferPool`], which allows for more efficient memory management. Otherwise, it defaults to +// [`BufferFromSystemMemory`]. +// +// `Buffer` is used for storing both serialized Sv2 frames and encrypted Noise data. #[cfg(feature = "with_buffer_pool")] type Buffer = BufferPool; -#[cfg(feature = "noise_sv2")] -use crate::error::Error; -use crate::error::Result; +/// An encoded or decoded Sv2 frame containing either a regular or Noise-protected message. +/// +/// A wrapper around the [`Frame`] enum that represents either a regular or Noise-protected Sv2 +/// frame containing the generic message type (`T`). +pub type StandardEitherFrame = Frame::Slice>; -use crate::Error::MissingBytes; -#[cfg(feature = "noise_sv2")] -use crate::State; +/// An encoded or decoded Sv2 frame. +/// +/// A wrapper around the [`Sv2Frame`] that represents a regular Sv2 frame containing the generic +/// message type (`T`). +pub type StandardSv2Frame = Sv2Frame::Slice>; +/// Standard Sv2 decoder with Noise protocol support. +/// +/// Used for decoding and decrypting generic message types (`T`) encoded in Sv2 frames and +/// encrypted via the Noise protocol. #[cfg(feature = "noise_sv2")] pub type StandardNoiseDecoder = WithNoise; -pub type StandardEitherFrame = Frame::Slice>; -pub type StandardSv2Frame = Sv2Frame::Slice>; + +/// Standard Sv2 decoder without Noise protocol support. +/// +/// Used for decoding generic message types (`T`) encoded in Sv2 frames. pub type StandardDecoder = WithoutNoise; +/// Decoder for Sv2 frames with Noise protocol support. +/// +/// Accumulates the encrypted data into a dedicated buffer until the entire encrypted frame is +/// received. The Noise protocol is then used to decrypt the accumulated data into another +/// dedicated buffer, converting it back into its original serialized form. This decrypted data is +/// then deserialized into the original Sv2 frame and message format. #[cfg(feature = "noise_sv2")] +#[derive(Debug)] pub struct WithNoise { + // Marker for the type of frame being decoded. + // + // Used to maintain the generic type (`T`) information of the message payload held by the + // frame. `T` refers to a type that implements the necessary traits for serialization + // [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`]. frame: PhantomData, + + // Tracks the number of bytes remaining until the full frame is received. + // + // Ensures that the full encrypted Noise frame has been received by keeping track of the + // remaining bytes. Once the complete frame is received, decoding can proceed. missing_noise_b: usize, + + // Buffer for holding incoming encrypted Noise data to be decrypted. + // + // Stores the incoming encrypted data, allowing the decoder to accumulate the necessary bytes + // for full decryption. Once the entire encrypted frame is received, the decoder processes the + // buffer to extract the underlying frame. noise_buffer: B, + + // Buffer for holding decrypted data to be decoded. + // + // Stores the decrypted data until it is ready to be processed and converted into a Sv2 frame. sv2_buffer: B, } #[cfg(feature = "noise_sv2")] impl<'a, T: Serialize + GetSize + Deserialize<'a>, B: IsBuffer + AeadBuffer> WithNoise { + /// Attempts to decode the next Noise encrypted frame. + /// + /// On success, the decoded and decrypted frame is returned. Otherwise, an error indicating the + /// number of missing bytes required to complete the encoded frame, an error on a badly + /// formatted message header, or an error on decryption failure is returned. + /// + /// In this case of the `Error::MissingBytes`, the user should resize the decoder buffer using + /// `writable`, read another chunk from the incoming message stream, and then call `next_frame` + /// again. This process should be repeated until `next_frame` returns `Ok`, indicating that the + /// full message has been received, and the decoding and decryption of the frame can proceed. #[inline] pub fn next_frame(&mut self, state: &mut State) -> Result> { match state { @@ -96,6 +184,57 @@ impl<'a, T: Serialize + GetSize + Deserialize<'a>, B: IsBuffer + AeadBuffer> Wit } } + /// Provides a writable buffer for receiving incoming Noise-encrypted Sv2 data. + /// + /// This buffer is used to store incoming data, and its size is adjusted based on the number + /// of missing bytes. As new data is read, it is written into this buffer until enough data has + /// been received to fully decode a frame. The buffer must have the correct number of bytes + /// available to progress to the decoding process. + #[inline] + pub fn writable(&mut self) -> &mut [u8] { + self.noise_buffer.get_writable(self.missing_noise_b) + } + + /// Determines whether the decoder's internal buffers can be safely dropped. + /// + /// For more information, refer to the [`buffer_sv2` + /// crate](https://docs.rs/buffer_sv2/latest/buffer_sv2/). + pub fn droppable(&self) -> bool { + self.noise_buffer.is_droppable() && self.sv2_buffer.is_droppable() + } + + // Processes and decodes a Sv2 frame during the Noise protocol handshake phase. + // + // Handles the decoding of a handshake frame from the `noise_buffer`. It converts the received + // data into a `HandShakeFrame` and encapsulates it into a `Frame` for further processing by + // the codec. + // + // This is used exclusively during the initial handshake phase of the Noise protocol, before + // transitioning to regular frame encryption and decryption. + fn while_handshaking(&mut self) -> Frame { + let src = self.noise_buffer.get_data_owned().as_mut().to_vec(); + + // Since the frame length is already validated during the handshake process, this + // operation is infallible + let frame = HandShakeFrame::from_bytes_unchecked(src.into()); + + frame.into() + } + + // Decodes a Noise-encrypted Sv2 frame, handling both the message header and payload + // decryption. + // + // Processes Noise-encrypted Sv2 frames by first decrypting the header, followed by the + // payload. If the frame's data is received in chunks, it ensures that decryption occurs + // incrementally as more encrypted data becomes available. The decrypted data is then stored in + // the `sv2_buffer`, from which the resulting Sv2 frame is extracted and returned. + // + // On success, the decoded frame is returned. Otherwise, an error indicating the number of + // missing bytes required to complete the encoded frame, an error on a badly formatted message + // header, or a decryption failure error is returned. If there are still bytes missing to + // complete the frame, the function will return an `Error::MissingBytes` with the number of + // additional bytes required to fully decrypt the frame. Once all bytes are available, the + // decryption process completes and the frame can be successfully decoded. #[inline] fn decode_noise_frame(&mut self, noise_codec: &mut NoiseCodec) -> Result> { match ( @@ -144,27 +283,14 @@ impl<'a, T: Serialize + GetSize + Deserialize<'a>, B: IsBuffer + AeadBuffer> Wit } } } - - fn while_handshaking(&mut self) -> Frame { - let src = self.noise_buffer.get_data_owned().as_mut().to_vec(); - - // below is inffalible as noise frame length has been already checked - let frame = HandShakeFrame::from_bytes_unchecked(src.into()); - - frame.into() - } - - #[inline] - pub fn writable(&mut self) -> &mut [u8] { - self.noise_buffer.get_writable(self.missing_noise_b) - } - pub fn droppable(&self) -> bool { - self.noise_buffer.is_droppable() && self.sv2_buffer.is_droppable() - } } #[cfg(feature = "noise_sv2")] impl WithNoise { + /// Crates a new [`WithNoise`] decoder with default buffer sizes. + /// + /// Initializes the decoder with default buffer sizes and sets the number of missing bytes to + /// 0. pub fn new() -> Self { Self { frame: PhantomData, @@ -182,14 +308,46 @@ impl Default for WithNoise { } } +/// Decoder for standard Sv2 frames. +/// +/// Accumulates the data into a dedicated buffer until the entire Sv2 frame is received. This data +/// is then deserialized into the original Sv2 frame and message format. #[derive(Debug)] pub struct WithoutNoise { + // Marker for the type of frame being decoded. + // + // Used to maintain the generic type (`T`) information of the message payload held by the + // frame. `T` refers to a type that implements the necessary traits for serialization + // [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`]. frame: PhantomData, + + // Tracks the number of bytes remaining until the full frame is received. + // + // Ensures that the full Sv2 frame has been received by keeping track of the remaining bytes. + // Once the complete frame is received, decoding can proceed. missing_b: usize, + + // Buffer for holding incoming data to be decoded into a Sv2 frame. + // + // This buffer stores incoming data as it is received, allowing the decoder to accumulate the + // necessary bytes until a full frame is available. Once the full encoded frame has been + // received, the buffer's contents are processed and decoded into an Sv2 frame. buffer: B, } impl WithoutNoise { + /// Attempts to decode the next frame, returning either a frame or an error indicating how many + /// bytes are missing. + /// + /// Attempts to decode the next Sv2 frame. + /// + /// On success, the decoded frame is returned. Otherwise, an error indicating the number of + /// missing bytes required to complete the frame is returned. + /// + /// In the case of `Error::MissingBytes`, the user should resize the decoder buffer using + /// `writable`, read another chunk from the incoming message stream, and then call `next_frame` + /// again. This process should be repeated until `next_frame` returns `Ok`, indicating that the + /// full message has been received, and the frame can be fully decoded. #[inline] pub fn next_frame(&mut self) -> Result> { let len = self.buffer.len(); @@ -210,12 +368,22 @@ impl WithoutNoise { } } + /// Provides a writable buffer for receiving incoming Sv2 data. + /// + /// This buffer is used to store incoming data, and its size is adjusted based on the number of + /// missing bytes. As new data is read, it is written into this buffer until enough data has + /// been received to fully decode a frame. The buffer must have the correct number of bytes + /// available to progress to the decoding process. pub fn writable(&mut self) -> &mut [u8] { self.buffer.get_writable(self.missing_b) } } impl WithoutNoise { + /// Creates a new [`WithoutNoise`] with a buffer of default size. + /// + /// Initializes the decoder with a default buffer size and sets the number of missing bytes to + /// the size of the header. pub fn new() -> Self { Self { frame: PhantomData, @@ -230,3 +398,20 @@ impl Default for WithoutNoise { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use binary_sv2::{binary_codec_sv2, Serialize}; + + #[derive(Serialize)] + pub struct TestMessage {} + + #[test] + fn unencrypted_writable_with_missing_b_initialized_as_header_size() { + let mut decoder = StandardDecoder::::new(); + let actual = decoder.writable(); + let expect = [0u8; Header::SIZE]; + assert_eq!(actual, expect); + } +} diff --git a/protocols/v2/codec-sv2/src/encoder.rs b/protocols/v2/codec-sv2/src/encoder.rs index 21618fda5..62d41cae9 100644 --- a/protocols/v2/codec-sv2/src/encoder.rs +++ b/protocols/v2/codec-sv2/src/encoder.rs @@ -1,3 +1,26 @@ +// # Encoder +// +// Provides utilities for encoding messages into Sv2 frames, with or without Noise protocol +// support. +// +// ## Usage +// +// All messages passed between Sv2 roles are encoded as Sv2 frames using primitives in this module. +// There are two types of encoders for creating these frames: one for regular Sv2 frames +// [`Encoder`], and another for Noise-encrypted frames [`NoiseEncoder`]. Both encoders manage the +// serialization of outgoing data and, when applicable, the encryption of the data before +// transmission. +// +// ### Buffer Management +// +// The encoders rely on buffers to hold intermediate data during the encoding process. +// +// - When the `with_buffer_pool` feature is enabled, the internal `Buffer` type is backed by a +// pool-allocated buffer [`binary_sv2::BufferPool`], providing more efficient memory usage, +// particularly in high-throughput scenarios. +// - If the feature is not enabled, a system memory buffer [`binary_sv2::BufferFromSystemMemory`] +// is used for simpler applications where memory efficiency is less critical. + use alloc::vec::Vec; use binary_sv2::{GetSize, Serialize}; #[allow(unused_imports)] @@ -25,28 +48,93 @@ use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory as Buffer}; #[cfg(feature = "with_buffer_pool")] use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory, BufferPool}; +// The buffer type for holding intermediate data during encoding. +// +// When the `with_buffer_pool` feature is enabled, `Buffer` uses a pool-allocated buffer +// [`BufferPool`], providing more efficient memory management, particularly in high-throughput +// environments. If the feature is not enabled, it defaults to [`BufferFromSystemMemory`], a +// simpler system memory buffer. +// +// `Buffer` is utilized for storing both serialized Sv2 frames and encrypted Noise data during the +// encoding process, ensuring that all frames are correctly handled before transmission. #[cfg(feature = "noise_sv2")] #[cfg(feature = "with_buffer_pool")] type Buffer = BufferPool; +// A simple buffer slice for holding serialized Sv2 frame data before transmission. +// +// When the `with_buffer_pool` feature is disabled, [`Slice`] defaults to a `Vec`, which serves +// as a dynamically allocated array to hold the serialized bytes of Sv2 frames. This provides +// flexibility in managing the encoded data during transmission or further processing, though it +// may not offer the same memory efficiency as the pool-allocated version [`BufferPool`]. #[cfg(not(feature = "with_buffer_pool"))] type Slice = Vec; +// A buffer slice used for holding serialized Sv2 frame data before transmission. +// +// When the `with_buffer_pool` feature is enabled, [`Slice`] defaults to a `buffer_sv2::Slice`, +// which serves as a slice of the `Buffer` that stores the encoded data. It holds the frame's +// serialized bytes temporarily, ensuring the data is ready for transmission or encryption, +// depending on whether Noise protocol support is enabled. #[cfg(feature = "with_buffer_pool")] type Slice = buffer_sv2::Slice; +/// Encoder for Sv2 frames with Noise protocol encryption. +/// +/// Serializes the Sv2 frame into a dedicated buffer. Encrypts this serialized data using the Noise +/// protocol, storing it into another dedicated buffer. Encodes the serialized and encrypted data, +/// such that it is ready for transmission. #[cfg(feature = "noise_sv2")] pub struct NoiseEncoder { + // Buffer for holding encrypted Noise data to be transmitted. + // + // Stores the encrypted data after the Sv2 frame has been processed by the Noise protocol + // and is ready for transmission. This buffer holds the outgoing encrypted data, ensuring + // that the full frame is correctly prepared before being sent. noise_buffer: Buffer, + + // Buffer for holding serialized Sv2 data before encryption. + // + // Stores the data after it has been serialized into an Sv2 frame but before it is encrypted + // by the Noise protocol. The buffer accumulates the frame's serialized bytes before they are + // encrypted and then encoded for transmission. sv2_buffer: Buffer, + + // Marker for the type of frame being encoded. + // + // Used to maintain the generic type information for `T`, which represents the message payload + // contained within the Sv2 frame. `T` refers to a type that implements the necessary traits + // for serialization [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`], + // ensuring that the encoder can handle different message types correctly during the encoding + // process. frame: PhantomData, } +// A Sv2 frame that will be encoded and optionally encrypted using the Noise protocol. +// +// Represent a Sv2 frame during the encoding process. It encapsulates the frame's generic payload +// message type (`T`) and is stored in a [`Slice`] buffer. The `Item` is passed to the encoder, +// which either processes it for normal transmission or applies Noise encryption, depending on the +// codec's state. #[cfg(feature = "noise_sv2")] type Item = Frame; #[cfg(feature = "noise_sv2")] impl NoiseEncoder { + /// Encodes an Sv2 frame and encrypts it using the Noise protocol. + /// + /// Takes an `item`, which is an Sv2 frame containing a payload of type `T`, and encodes it for + /// transmission. The frame is encrypted after being serialized. The `state` parameter + /// determines whether the encoder is in the handshake or transport phase, guiding the + /// appropriate encoding and encryption action. + /// + /// - In the handshake phase, the initial handshake messages are processed to establish secure + /// communication. + /// - In the transport phase, the full frame is serialized, encrypted, and stored in a buffer + /// for transmission. + /// + /// On success, the method returns an encrypted (`Slice`) (buffer) ready for transmission. + /// Otherwise, errors on an encryption or serialization failure. #[inline] pub fn encode(&mut self, item: Item, state: &mut State) -> Result { match state { @@ -99,6 +187,13 @@ impl NoiseEncoder { Ok(self.noise_buffer.get_data_owned()) } + // Encodes Sv2 frames during the handshake phase of the Noise protocol. + // + // Used when the encoder is in the handshake phase, before secure communication is fully + // established. It encodes the provided `item` into a handshake frame, storing the resulting + // data in the `noise_buffer`. The handshake phase is necessary to exchange initial messages + // and set up the Noise encryption state before transitioning to the transport phase, where + // full frames are encrypted and transmitted. #[inline(never)] fn while_handshaking(&mut self, item: Item) -> Result<()> { // ENCODE THE SV2 FRAME @@ -114,6 +209,7 @@ impl NoiseEncoder { Ok(()) } + /// Determines whether the encoder's internal buffers can be safely dropped. pub fn droppable(&self) -> bool { self.noise_buffer.is_droppable() && self.sv2_buffer.is_droppable() } @@ -121,6 +217,7 @@ impl NoiseEncoder { #[cfg(feature = "noise_sv2")] impl NoiseEncoder { + /// Creates a new `NoiseEncoder` with default buffer sizes. pub fn new() -> Self { #[cfg(not(feature = "with_buffer_pool"))] let size = 512; @@ -141,13 +238,36 @@ impl Default for NoiseEncoder { } } +/// Encoder for standard Sv2 frames. +/// +/// Serializes the Sv2 frame into a dedicated buffer then encodes it, such that it is ready for +/// transmission. #[derive(Debug)] pub struct Encoder { + // Buffer for holding serialized Sv2 data. + // + // Stores the serialized bytes of the Sv2 frame after it has been encoded. Once the frame is + // serialized, the resulting bytes are stored in this buffer to be transmitted. The buffer is + // dynamically resized to accommodate the size of the encoded frame. buffer: Vec, + + // Marker for the type of frame being encoded. + // + // Used to maintain the generic type information for `T`, which represents the message payload + // contained within the Sv2 frame. `T` refers to a type that implements the necessary traits + // for serialization [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`], + // ensuring that the encoder can handle different message types correctly during the encoding + // process. frame: PhantomData, } impl Encoder { + /// Encodes a standard Sv2 frame for transmission. + /// + /// Takes a standard Sv2 frame containing a payload of type `T` and serializes it into a byte + /// stream. The resulting serialized bytes are stored in the internal `buffer`, preparing the + /// frame for transmission. On success, the method returns a reference to the serialized bytes + /// stored in the internal buffer. Otherwise, errors on a serialization failure. pub fn encode( &mut self, item: Sv2Frame, @@ -161,6 +281,7 @@ impl Encoder { Ok(&self.buffer[..]) } + /// Creates a new `Encoder` with a buffer of default size. pub fn new() -> Self { Self { buffer: Vec::with_capacity(512), diff --git a/protocols/v2/codec-sv2/src/error.rs b/protocols/v2/codec-sv2/src/error.rs index 3201a6f2a..b5810cb85 100644 --- a/protocols/v2/codec-sv2/src/error.rs +++ b/protocols/v2/codec-sv2/src/error.rs @@ -1,3 +1,9 @@ +//! # Error Handling and Result Types +//! +//! This module defines error types and utilities for handling errors in the `codec_sv2` module. +//! It includes the [`Error`] enum for representing various errors, a C-compatible [`CError`] enum +//! for FFI, and a `Result` type alias for convenience. + #[cfg(test)] use core::cmp; use core::fmt; @@ -5,65 +11,95 @@ use framing_sv2::Error as FramingError; #[cfg(feature = "noise_sv2")] use noise_sv2::{AeadError, Error as NoiseError}; +/// A type alias for results returned by the `codec_sv2` modules. +/// +/// `Result` is a convenient wrapper around the [`core::result::Result`] type, using the [`Error`] +/// enum defined in this crate as the error type. pub type Result = core::result::Result; +/// Enumeration of possible errors in the `codec_sv2` module. +/// +/// This enum represents various errors that can occur within the `codec_sv2` module, including +/// errors from related crates like [`binary_sv2`], [`framing_sv2`], and [`noise_sv2`]. #[derive(Debug, PartialEq, Eq)] pub enum Error { - /// Errors from the `binary_sv2` crate + /// AEAD (`snow`) error in the Noise protocol. + #[cfg(feature = "noise_sv2")] + AeadError(AeadError), + + /// Binary Sv2 data format error. BinarySv2Error(binary_sv2::Error), + + /// Framing Sv2 error. + FramingError(FramingError), + + /// Framing Sv2 error. FramingSv2Error(framing_sv2::Error), - /// Errors if there are missing bytes in the Noise protocol - MissingBytes(usize), - /// Errors from the `noise_sv2` crate - #[cfg(feature = "noise_sv2")] - NoiseSv2Error(NoiseError), + + /// Invalid step for initiator in the Noise protocol. #[cfg(feature = "noise_sv2")] - AeadError(AeadError), - /// Error if Noise protocol state is not as expected - UnexpectedNoiseState, + InvalidStepForInitiator, + + /// Invalid step for responder in the Noise protocol. #[cfg(feature = "noise_sv2")] InvalidStepForResponder, + + /// Incomplete frame with the number of missing bytes remaining to completion. + MissingBytes(usize), + + /// Sv2 Noise protocol error. #[cfg(feature = "noise_sv2")] - InvalidStepForInitiator, + NoiseSv2Error(NoiseError), + + /// Noise protocol is not in the expected handshake state. #[cfg(feature = "noise_sv2")] NotInHandShakeState, - FramingError(FramingError), + + /// Unexpected state in the Noise protocol. + UnexpectedNoiseState, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Error::*; match self { + #[cfg(feature = "noise_sv2")] + AeadError(e) => write!(f, "Aead Error: `{:?}`", e), BinarySv2Error(e) => write!(f, "Binary Sv2 Error: `{:?}`", e), + FramingError(e) => write!(f, "Framing error in codec: `{:?}`", e), FramingSv2Error(e) => write!(f, "Framing Sv2 Error: `{:?}`", e), - MissingBytes(u) => write!(f, "Missing `{}` Noise bytes", u), - #[cfg(feature = "noise_sv2")] - NoiseSv2Error(e) => write!(f, "Noise SV2 Error: `{:?}`", e), #[cfg(feature = "noise_sv2")] - AeadError(e) => write!(f, "Aead Error: `{:?}`", e), - UnexpectedNoiseState => { - write!(f, "Noise state is incorrect") - } + InvalidStepForInitiator => write!( + f, + "This noise handshake step can not be executed by an initiato" + ), #[cfg(feature = "noise_sv2")] InvalidStepForResponder => write!( f, "This noise handshake step can not be executed by a responder" ), + MissingBytes(u) => write!(f, "Missing `{}` Noise bytes", u), #[cfg(feature = "noise_sv2")] - InvalidStepForInitiator => write!( - f, - "This noise handshake step can not be executed by an initiato" - ), + NoiseSv2Error(e) => write!(f, "Noise SV2 Error: `{:?}`", e), #[cfg(feature = "noise_sv2")] NotInHandShakeState => write!( f, "This operation can be executed only during the noise handshake" ), - FramingError(e) => write!(f, "Framing error in codec: `{:?}`", e), + UnexpectedNoiseState => { + write!(f, "Noise state is incorrect") + } } } } +#[cfg(feature = "noise_sv2")] +impl From for Error { + fn from(e: AeadError) -> Self { + Error::AeadError(e) + } +} + impl From for Error { fn from(e: binary_sv2::Error) -> Self { Error::BinarySv2Error(e) @@ -76,13 +112,6 @@ impl From for Error { } } -#[cfg(feature = "noise_sv2")] -impl From for Error { - fn from(e: AeadError) -> Self { - Error::AeadError(e) - } -} - #[cfg(feature = "noise_sv2")] impl From for Error { fn from(e: NoiseError) -> Self { @@ -90,28 +119,49 @@ impl From for Error { } } +/// C-compatible enumeration of possible errors in the `codec_sv2` module. +/// +/// This enum mirrors the [`Error`] enum but is designed to be used in C code through FFI. It +/// represents the same set of errors as [`Error`], making them accessible to C programs. #[repr(C)] #[derive(Debug)] pub enum CError { - /// Errors from the `binary_sv2` crate + /// AEAD (`snow`) error in the Noise protocol. + AeadError, + + /// Binary Sv2 data format error. BinarySv2Error, - /// Errors from the `framing_sv2` crate + + /// Framing Sv2 error. + FramingError, + + /// Framing Sv2 error. FramingSv2Error, - /// Errors if there are missing bytes in the Noise protocol + + /// Invalid step for initiator in the Noise protocol. + InvalidStepForInitiator, + + /// Invalid step for responder in the Noise protocol. + InvalidStepForResponder, + + /// Missing bytes in the Noise protocol. MissingBytes(usize), - /// Errors from the `noise_sv2` crate + + /// Sv2 Noise protocol error. NoiseSv2Error, - /// `snow` errors - AeadError, - /// Error if Noise protocol state is not as expected - UnexpectedNoiseState, - InvalidStepForResponder, - InvalidStepForInitiator, + + /// Noise protocol is not in the expected handshake state. NotInHandShakeState, - FramingError, + + /// Unexpected state in the Noise protocol. + UnexpectedNoiseState, } -/// Here only to force cbindgen to create header for CError +/// Force `cbindgen` to create a header for [`CError`]. +/// +/// It ensures that [`CError`] is included in the generated C header file. This function is not meant +/// to be called and will panic if called. Its only purpose is to make [`CError`] visible to +/// `cbindgen`. #[no_mangle] pub extern "C" fn export_cerror() -> CError { unimplemented!() @@ -120,21 +170,21 @@ pub extern "C" fn export_cerror() -> CError { impl From for CError { fn from(e: Error) -> CError { match e { + #[cfg(feature = "noise_sv2")] + Error::AeadError(_) => CError::AeadError, Error::BinarySv2Error(_) => CError::BinarySv2Error, Error::FramingSv2Error(_) => CError::FramingSv2Error, - Error::MissingBytes(u) => CError::MissingBytes(u), - #[cfg(feature = "noise_sv2")] - Error::NoiseSv2Error(_) => CError::NoiseSv2Error, + Error::FramingError(_) => CError::FramingError, #[cfg(feature = "noise_sv2")] - Error::AeadError(_) => CError::AeadError, - Error::UnexpectedNoiseState => CError::UnexpectedNoiseState, + Error::InvalidStepForInitiator => CError::InvalidStepForInitiator, #[cfg(feature = "noise_sv2")] Error::InvalidStepForResponder => CError::InvalidStepForResponder, + Error::MissingBytes(u) => CError::MissingBytes(u), #[cfg(feature = "noise_sv2")] - Error::InvalidStepForInitiator => CError::InvalidStepForInitiator, + Error::NoiseSv2Error(_) => CError::NoiseSv2Error, #[cfg(feature = "noise_sv2")] Error::NotInHandShakeState => CError::NotInHandShakeState, - Error::FramingError(_) => CError::FramingError, + Error::UnexpectedNoiseState => CError::UnexpectedNoiseState, } } } @@ -142,16 +192,16 @@ impl From for CError { impl Drop for CError { fn drop(&mut self) { match self { + CError::AeadError => (), CError::BinarySv2Error => (), + CError::FramingError => (), CError::FramingSv2Error => (), + CError::InvalidStepForInitiator => (), + CError::InvalidStepForResponder => (), CError::MissingBytes(_) => (), CError::NoiseSv2Error => (), - CError::AeadError => (), - CError::UnexpectedNoiseState => (), - CError::InvalidStepForResponder => (), - CError::InvalidStepForInitiator => (), CError::NotInHandShakeState => (), - CError::FramingError => (), + CError::UnexpectedNoiseState => (), }; } } diff --git a/protocols/v2/codec-sv2/src/lib.rs b/protocols/v2/codec-sv2/src/lib.rs index a4eec1efd..8b2c05ce9 100644 --- a/protocols/v2/codec-sv2/src/lib.rs +++ b/protocols/v2/codec-sv2/src/lib.rs @@ -1,3 +1,37 @@ +//! # Stratum V2 Codec Library +//! +//! `codec_sv2` provides the message encoding and decoding functionality for the Stratum V2 (Sv2) +//! protocol, handling secure communication between Sv2 roles. +//! +//! This crate abstracts the complexity of message encoding/decoding with optional Noise protocol +//! support, ensuring both regular and encrypted messages can be serialized, transmitted, and +//! decoded consistently and reliably. +//! +//! +//! ## Usage +//! `codec-sv2` supports both standard Sv2 frames (unencrypted) and Noise-encrypted Sv2 frames to +//! ensure secure communication. To encode messages for transmission, choose between the +//! [`Encoder`] for standard Sv2 frames or the [`NoiseEncoder`] for encrypted frames. To decode +//! received messages, choose between the [`StandardDecoder`] for standard Sv2 frames or +//! [`StandardNoiseDecoder`] to decrypt Noise frames. +//! +//! ## Build Options +//! +//! This crate can be built with the following features: +//! +//! - `noise_sv2`: Enables support for Noise protocol encryption and decryption. +//! - `with_buffer_pool`: Enables buffer pooling for more efficient memory management. +//! - `with_serde`: builds [`binary_sv2`] and [`buffer_sv2`] crates with `serde`-based encoding and +//! decoding. Note that this feature flag is only used for the Message Generator, and deprecated +//! for any other kind of usage. It will likely be fully deprecated in the future. +//! +//! ## Examples +//! +//! See the examples for more information: +//! +//! - [Unencrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/unencrypted.rs) +//! - [Encrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/encrypted.rs) + #![cfg_attr(feature = "no_std", no_std)] pub use framing_sv2::framing::Frame; @@ -34,20 +68,75 @@ pub use buffer_sv2; pub use framing_sv2::{self, framing::handshake_message_to_frame as h2f}; +/// Represents the role in the Noise handshake process, either as an initiator or a responder. +/// +/// The Noise protocol requires two roles during the handshake process: +/// - **Initiator**: The party that starts the handshake by sending the initial message. +/// - **Responder**: The party that waits for the initiator's message and responds to it. +/// +/// This enum distinguishes between these two roles, allowing the codec to handle the handshake +/// process accordingly. +#[allow(clippy::large_enum_variant)] +#[cfg(feature = "noise_sv2")] +#[derive(Debug)] +pub enum HandshakeRole { + /// The initiator role in the Noise handshake process. + /// + /// The initiator starts the handshake by sending the initial message. This variant stores an + /// `Initiator` object, which contains the necessary state and cryptographic materials for the + /// initiator's part in the Noise handshake. + Initiator(Box), + + /// The responder role in the Noise handshake process. + /// + /// The responder waits for the initiator's handshake message and then responds. This variant + /// stores a `Responder` object, which contains the necessary state and cryptographic materials + /// for the responder's part in the Noise handshake. + Responder(Box), +} + +/// Represents the state of the Noise protocol codec during different phases: initialization, +/// handshake, or transport mode, where encryption and decryption are fully operational. +/// +/// The state transitions from initialization [`State::NotInitialized`] to handshake +/// [`State::HandShake`] and finally to transport mode [`State::Transport`] as the encryption +/// handshake is completed. #[cfg(feature = "noise_sv2")] #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum State { - /// Not yet initialized + /// The codec has not been initialized yet. + /// + /// This state is used when the codec is first created or reset, before the handshake process + /// begins. The variant carries the expected size of the handshake message, which can vary + /// depending on whether the codec is acting as an initiator or a responder. NotInitialized(usize), - /// Handshake mode where codec is negotiating keys + + /// The codec is in the handshake phase, where cryptographic keys are being negotiated. + /// + /// In this state, the codec is in the process of establishing secure communication by + /// exchanging handshake messages. Once the handshake is complete, the codec transitions to + /// [`State::Transport`] mode. HandShake(HandshakeRole), - /// Transport mode where AEAD is fully operational. The `TransportMode` object in this variant - /// as able to perform encryption and decryption resp. + + /// The codec is in transport mode, where AEAD encryption and decryption are fully operational. + /// + /// In this state, the codec is performing full encryption and decryption using the Noise + /// protocol in transport mode. The [`NoiseCodec`] object is responsible for handling the + /// encryption and decryption of data. Transport(NoiseCodec), } + #[cfg(feature = "noise_sv2")] impl State { + /// Initiates the first step of the handshake process for the initiator. + /// + /// Creates and sends the initial handshake message for the initiator. It is the first step in + /// establishing a secure communication channel. Responders cannot perform this step. + /// + /// nb: This method returns a [`HandShakeFrame`] but does not change the current state + /// (`self`). The state remains `State::HandShake(HandshakeRole::Initiator)` until `step_1` is + /// called to advance the handshake process. pub fn step_0(&mut self) -> core::result::Result { match self { Self::HandShake(h) => match h { @@ -58,6 +147,15 @@ impl State { } } + /// Processes the second step of the handshake process for the responder. + /// + /// The responder receives the public key from the initiator, generates a response message + /// containing the handshake frame, and prepares the [`NoiseCodec`] for transitioning the + /// initiator state to transport mode in `step_2`. + /// + /// nb: Returns a new state [`State::Transport`] but does not update the current state + /// (`self`). The caller is responsible for updating the state, allowing for more flexible + /// control over the handshake process as the caller decides what to do with this state. pub fn step_1( &mut self, re_pub: [u8; const_sv2::RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE], @@ -74,6 +172,14 @@ impl State { } } + /// Processes the final step of the handshake process for the initiator. + /// + /// Receives the response message from the responder containing the handshake frame, and + /// transitions the state to transport mode. This finalizes the secure communication setup and + /// enables full encryption and decryption in [`State::Transport`] mode. + /// + /// nb: Directly updates the current state (`self`) to [`State::Transport`], completing the + /// handshake process. pub fn step_2( &mut self, message: [u8; const_sv2::INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE], @@ -89,16 +195,16 @@ impl State { } } } -#[allow(clippy::large_enum_variant)] -#[cfg(feature = "noise_sv2")] -#[derive(Debug)] -pub enum HandshakeRole { - Initiator(Box), - Responder(Box), -} #[cfg(feature = "noise_sv2")] impl State { + /// Creates a new uninitialized handshake [`State`]. + /// + /// Sets the codec to the initial state, [`State::NotInitialized`], based on the provided + /// handshake role. This state is used before the handshake process begins, and the handshake + /// message size guides the codec on how much data to expect before advancing to the next step. + /// The expected size of the handshake message is determined by whether the codec is acting as + /// an initiator or responder. pub fn not_initialized(role: &HandshakeRole) -> Self { match role { HandshakeRole::Initiator(_) => { @@ -110,10 +216,28 @@ impl State { } } + /// Initializes the codec state to [`State::HandShake`] mode with the given handshake role. + /// + /// Transitions the codec into the handshake phase by accepting a [`HandshakeRole`], which + /// determines whether the codec is the initiator or responder in the handshake process. Once + /// in [`State::HandShake`] mode, the codec begins negotiating cryptographic keys with the + /// peer, eventually transitioning to the secure [`State::Transport`] phase. + /// + /// The role passed to this method defines how the handshake proceeds: + /// - [`HandshakeRole::Initiator`]: The codec will start the handshake process. + /// - [`HandshakeRole::Responder`]: The codec will wait for the initiator's handshake message. pub fn initialized(inner: HandshakeRole) -> Self { Self::HandShake(inner) } + /// Transitions the codec state to [`State::Transport`] mode with the given [`NoiseCodec`]. + /// + /// Finalizes the handshake process and transitions the codec into [`State::Transport`] mode, + /// where full encryption and decryption are active. The codec uses the provided [`NoiseCodec`] + /// to perform encryption and decryption for all communication in this mode, ensuring secure + /// data transmission. + /// + /// Once in [`State::Transport`] mode, the codec is fully operational for secure communication. pub fn with_transport_mode(tm: NoiseCodec) -> Self { Self::Transport(tm) } diff --git a/protocols/v2/sv2-ffi/sv2.h b/protocols/v2/sv2-ffi/sv2.h index 23fbc9807..534c12e6e 100644 --- a/protocols/v2/sv2-ffi/sv2.h +++ b/protocols/v2/sv2-ffi/sv2.h @@ -451,24 +451,32 @@ void free_submit_solution(CSubmitSolution s); #include #include +/// C-compatible enumeration of possible errors in the `codec_sv2` module. +/// +/// This enum mirrors the [`Error`] enum but is designed to be used in C code through FFI. It +/// represents the same set of errors as [`Error`], making them accessible to C programs. struct CError { enum class Tag { - /// Errors from the `binary_sv2` crate + /// AEAD (`snow`) error in the Noise protocol. + AeadError, + /// Binary Sv2 data format error. BinarySv2Error, - /// Errors from the `framing_sv2` crate + /// Framing Sv2 error. + FramingError, + /// Framing Sv2 error. FramingSv2Error, - /// Errors if there are missing bytes in the Noise protocol + /// Invalid step for initiator in the Noise protocol. + InvalidStepForInitiator, + /// Invalid step for responder in the Noise protocol. + InvalidStepForResponder, + /// Missing bytes in the Noise protocol. MissingBytes, - /// Errors from the `noise_sv2` crate + /// Sv2 Noise protocol error. NoiseSv2Error, - /// `snow` errors - AeadError, - /// Error if Noise protocol state is not as expected - UnexpectedNoiseState, - InvalidStepForResponder, - InvalidStepForInitiator, + /// Noise protocol is not in the expected handshake state. NotInHandShakeState, - FramingError, + /// Unexpected state in the Noise protocol. + UnexpectedNoiseState, }; struct MissingBytes_Body { @@ -483,7 +491,11 @@ struct CError { extern "C" { -/// Here only to force cbindgen to create header for CError +/// Force `cbindgen` to create a header for [`CError`]. +/// +/// It ensures that [`CError`] is included in the generated C header file. This function is not meant +/// to be called and will panic if called. Its only purpose is to make [`CError`] visible to +/// `cbindgen`. CError export_cerror(); } // extern "C" From 7b912e690540470caa2cbbc338aa79dd7074638e Mon Sep 17 00:00:00 2001 From: RJ Rybarczyk Date: Fri, 11 Oct 2024 14:22:43 -0400 Subject: [PATCH 03/15] Add codec README --- protocols/v2/codec-sv2/README.md | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 protocols/v2/codec-sv2/README.md diff --git a/protocols/v2/codec-sv2/README.md b/protocols/v2/codec-sv2/README.md new file mode 100644 index 000000000..a57a4a2fd --- /dev/null +++ b/protocols/v2/codec-sv2/README.md @@ -0,0 +1,49 @@ +# `codec_sv2` + +[![crates.io](https://img.shields.io/crates/v/codec_sv2.svg)](https://crates.io/crates/codec_sv2) +[![docs.rs](https://docs.rs/codec_sv2/badge.svg)](https://docs.rs/codec_sv2) +[![rustc+](https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg)](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html) +[![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/stratum-mining/stratum/blob/main/LICENSE.md) +[![codecov](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg?flag=codec_sv2-coverage)](https://codecov.io/gh/stratum-mining/stratum) + +`codec_sv2` provides the message encoding and decoding functionality for the Stratum V2 (Sv2) +protocol, handling secure communication between Sv2 roles. This crate abstracts the complexity of +message encoding/decoding with optional Noise protocol support, ensuring both regular and encrypted +messages can be serialized, transmitted, and decoded consistently and reliably. + +## Main Components + +- **Encoder**: Encodes Sv2 messages with or without Noise protocol support. +- **Decoder**: Decodes Sv2 messages with or without Noise protocol support. +- **Handshake State**: Manages the current Noise protocol handshake state of the codec. + + +## Usage + +To include this crate in your project, run: + +```bash +cargo add codec_sv2 +``` + +This crate can be built with the following feature flags: + +- `noise_sv2`: Enables support for Noise protocol encryption and decryption. +- `with_buffer_pool`: Enables buffer pooling for more efficient memory management. +- `with_serde`: builds [`binary_sv2`](https://crates.io/crates/binary_sv2) and + [`buffer_sv2`](https://crates.io/crates/buffer_sv2) crates with `serde`-based encoding and + decoding. Note that this feature flag is only used for the Message Generator, and deprecated + for any other kind of usage. It will likely be fully deprecated in the future. + +### Examples + +This crate provides two examples demonstrating how to encode and decode Sv2 frames: + +1. **[Unencrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/unencrypted.rs)**: + Encode and decode standard Sv2 frames, detailing the message serialization and transmission + process for unencrypted communications. + +2. **[Encrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/encrypted.rs)**: + Encode and decode Sv2 frames with Noise protocol encryption, detailing the entire encryption + handshake and transport phase and serialization and transmission process for encrypted + communications. From 602e8b83de3f2aec9bb9016f23ed6f364a134049 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:46:56 +0530 Subject: [PATCH 04/15] add docs for signature_message.rs --- .../v2/noise-sv2/src/signature_message.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/protocols/v2/noise-sv2/src/signature_message.rs b/protocols/v2/noise-sv2/src/signature_message.rs index 827199bed..df997f7ad 100644 --- a/protocols/v2/noise-sv2/src/signature_message.rs +++ b/protocols/v2/noise-sv2/src/signature_message.rs @@ -1,14 +1,53 @@ +// # Signature-Based Message Handling +// +// Defines the [`SignatureNoiseMessage`] struct, which represents a signed message used in the +// Noise protocol to authenticate and verify the identity of a party during the handshake. +// +// This module provides utilities for creating, signing, and verifying Noise protocol messages +// using Schnorr signatures over the [`secp256k1`] elliptic curve. It encapsulates signed messages +// along with versioning and validity timestamps. The following capabilities are supported: +// +// - Conversion of raw byte arrays into structured [`SignatureNoiseMessage`] instances. +// - Message signing using Schnorr signatures and the [`secp256k1`] curve. +// - Verification of signed messages, ensuring they fall within valid time periods and are signed +// by an authorized public key. +// +// ## Usage +// +// The [`SignatureNoiseMessage`] is used by both the [`crate::Responder`] and [`crate::Initiator`] +// roles. The [`crate::Responder`] uses the `sign` method to generate a Schnorr signature over the +// initial message sent by the initiator. The [`crate::Initiator`] uses the `verify` method to +// check the validity of the signed message from the responder, comparing it against the provided +// public key and optional authority key, while ensuring the message falls within the specified +// validity period. + use secp256k1::{hashes::sha256, schnorr::Signature, Keypair, Message, Secp256k1, XOnlyPublicKey}; use std::{convert::TryInto, time::SystemTime}; +/// `SignatureNoiseMessage` represents a signed message used in the Noise NX protocol +/// for authentication during the handshake process. It encapsulates the necessary +/// details for signature verification, including protocol versioning, validity periods, +/// and a Schnorr signature over the message. +/// +/// This structure ensures that messages are authenticated and valid only within +/// a specified time window, using Schnorr signatures over the `secp256k1` elliptic curve. pub struct SignatureNoiseMessage { + // Version of the protocol being used. pub version: u16, + // Start of the validity period for the message, expressed as a Unix timestamp. pub valid_from: u32, + // End of the validity period for the message, expressed as a Unix timestamp. pub not_valid_after: u32, + // 64-byte Schnorr signature that authenticates the message. pub signature: [u8; 64], } impl From<[u8; 74]> for SignatureNoiseMessage { + // Converts a 74-byte array into a [`SignatureNoiseMessage`]. + // + // Allows a raw 74-byte array to be converted into a [`SignatureNoiseMessage`], extracting the + // version, validity periods, and signature from the provided data. Panics if the byte array + // cannot be correctly converted into the struct fields. fn from(value: [u8; 74]) -> Self { let version = u16::from_le_bytes(value[0..2].try_into().unwrap()); let valid_from = u32::from_le_bytes(value[2..6].try_into().unwrap()); @@ -24,6 +63,13 @@ impl From<[u8; 74]> for SignatureNoiseMessage { } impl SignatureNoiseMessage { + // Verifies the [`SignatureNoiseMessage`] against the provided public key and an optional + // authority public key. The verification checks that the message is currently valid + // (i.e., within the `valid_from` and `not_valid_after` time window) and that the signature + // is correctly signed by the authority. + // + // If an authority public key is not provided, the function assumes that the signature + // is already valid without further verification. pub fn verify(self, pk: &XOnlyPublicKey, authority_pk: &Option) -> bool { if let Some(authority_pk) = authority_pk { let now = SystemTime::now() @@ -48,6 +94,12 @@ impl SignatureNoiseMessage { true } } + + // Signs a [`SignatureNoiseMessage`] using the provided keypair (`kp`). + // + // Creates a Schnorr signature for the message, combining the version, validity period, and + // the static public key of the server (`static_pk`). The resulting signature is then written + // into the provided message buffer (`msg`). pub fn sign(msg: &mut [u8; 74], static_pk: &XOnlyPublicKey, kp: &Keypair) { let secp = Secp256k1::signing_only(); let m = [&msg[0..10], &static_pk.serialize()].concat(); @@ -58,6 +110,12 @@ impl SignatureNoiseMessage { } } + // Splits the [`SignatureNoiseMessage`] into its component parts: the message hash and the + // signature. + // + // Separates the message into the first 10 bytes (containing the version and validity period) + // and the 64-byte Schnorr signature, returning them in a tuple. Used internally during the + // verification process. fn split(self) -> ([u8; 10], [u8; 64]) { let mut m = [0; 10]; m[0] = self.version.to_le_bytes()[0]; From d4917dfd4b5a9db42660711ff4a6d3c8ef08b967 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:47:23 +0530 Subject: [PATCH 05/15] add docs for aed_cipher.rs --- protocols/v2/noise-sv2/src/aed_cipher.rs | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/protocols/v2/noise-sv2/src/aed_cipher.rs b/protocols/v2/noise-sv2/src/aed_cipher.rs index 857524f37..4ce0dc70f 100644 --- a/protocols/v2/noise-sv2/src/aed_cipher.rs +++ b/protocols/v2/noise-sv2/src/aed_cipher.rs @@ -1,9 +1,52 @@ +// # AEAD Cipher +// +// Abstracts the encryption and decryption operations for authenticated encryption with associated +// data (AEAD) ciphers used in the Noise protocol. +// +// The [`AeadCipher`] trait provides a unified interface for AEAD ciphers, including +// [`ChaCha20Poly1305`] and [`Aes256Gcm`], allowing flexible cryptographic operations in different +// contexts. +// +// The trait supports core AEAD operations, including: +// +// - Key initialization via the `from_key` method to derive a cipher instance from a 32-byte key. +// - Authenticated encryption via the `encrypt` method to securely encrypt data with a nonce and +// additional associated data (AAD). +// - Authenticated decryption via the `decrypt` method to securely decrypt data using the provided +// nonce and AAD. +// +// ## Usage +// +// The `AeadCipher` trait can be implemented for any AEAD cipher, enabling encryption and decryption +// of Noise protocol messages. Two default implementations are provided for the +// [`ChaCha20Poly1305`] and [`Aes256Gcm`] ciphers. + use aes_gcm::Aes256Gcm; use chacha20poly1305::{aead::Buffer, AeadInPlace, ChaCha20Poly1305, ChaChaPoly1305, KeyInit}; +// Defines the interface for AEAD ciphers. +// +// The [`AeadCipher`] trait provides a standard interface for initializing AEAD ciphers, and for +// performing encryption and decryption operations with additional Authenticated Associated Data (AAD). This trait is implemented +// by either the [`ChaCha20Poly1305`] or [`Aes256Gcm`] specific cipher types, allowing them to be +// used interchangeably in cryptographic protocols. It is utilized by the +// [`crate::handshake::HandshakeOp`] trait to secure the handshake process. +// +// The `T: Buffer` represents the data buffer to be encrypted or decrypted. The buffer must +// implement the [`Buffer`] trait, which provides necessary operations for in-place encryption and +// decryption. pub trait AeadCipher { + // Creates a new instance of the cipher from a 32-byte key. + // + // Initializes the AEAD cipher with the provided key (`k`), preparing it for + // encryption and decryption operations. fn from_key(k: [u8; 32]) -> Self; + // Encrypts the data in place using the provided 12-byte `nonce` and AAD (`ad`). + // + // Performs authenticated encryption on the provided mutable data buffer (`data`), modifying + // it in place to contain the ciphertext. The encryption is performed using the provided nonce + // and AAD, which ensures that the data has not been tampered with during transit. fn encrypt( &mut self, nonce: &[u8; 12], @@ -11,6 +54,11 @@ pub trait AeadCipher { data: &mut T, ) -> Result<(), aes_gcm::Error>; + // Decrypts the data in place using the provided 12-byte nonce (`n`) and AAD (`ad`). + // + // Performs authenticated decryption on the provided mutable data buffer, modifying it in + // place to contain the plaintext. The decryption is performed using the provided nonce and + // AAD, ensuring that the data has not been tampered with during transit. fn decrypt( &mut self, nonce: &[u8; 12], @@ -47,6 +95,7 @@ impl AeadCipher for Aes256Gcm { fn from_key(k: [u8; 32]) -> Self { Aes256Gcm::new(&k.into()) } + fn encrypt( &mut self, nonce: &[u8; 12], @@ -55,6 +104,7 @@ impl AeadCipher for Aes256Gcm { ) -> Result<(), aes_gcm::Error> { self.encrypt_in_place(nonce.into(), ad, data) } + fn decrypt( &mut self, nonce: &[u8; 12], From 9980ee587623d5005aca21a507b776edf8707141 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:47:59 +0530 Subject: [PATCH 06/15] add docs for cipher_state.rs --- protocols/v2/noise-sv2/src/cipher_state.rs | 143 +++++++++++++++++++-- 1 file changed, 135 insertions(+), 8 deletions(-) diff --git a/protocols/v2/noise-sv2/src/cipher_state.rs b/protocols/v2/noise-sv2/src/cipher_state.rs index b76f247c1..3f0542f1e 100644 --- a/protocols/v2/noise-sv2/src/cipher_state.rs +++ b/protocols/v2/noise-sv2/src/cipher_state.rs @@ -1,19 +1,91 @@ +// # Cipher State Management +// +// Defines the [`CipherState`] trait and the [`GenericCipher`] enum, which manage the state of +// AEAD ciphers used in the Noise protocol. This includes managing the encryption key, nonce, and +// the cipher instance itself, facilitating secure encryption and decryption during communication. +// +// The [`CipherState`] trait abstracts the management of core elements for AEAD ciphers: +// - Manages the encryption key lifecycle used by the AEAD cipher. +// - Generates and tracks unique nonces for each encryption operation, preventing replay attacks. +// - Initializes the appropriate cipher (e.g., [`ChaCha20Poly1305`] or [`Aes256Gcm`]) for secure +// communication. +// +// The trait provides methods for encrypting and decrypting data using additional associated data +// (AAD) and securely erasing sensitive cryptographic material when no longer needed. +// +// The [`GenericCipher`] enum enables flexible use of either [`ChaCha20Poly1305`] or [`Aes256Gcm`] +// ciphers. It abstracts away the specific cipher being used while ensuring consistent handling of +// cryptographic operations (e.g., encryption, decryption, key erasure) across both ciphers. +// +// ## Usage +// +// The [`CipherState`] trait is used by the [`crate::handshake::HandshakeOp`] trait to manage +// stateful encryption and decryption tasks during the Noise protocol handshake. By implementing +// [`CipherState`], the handshake process securely manages cryptographic material and transforms +// messages exchanged between the initiator and responder. +// +// Once the Noise handshake is complete, the [`crate::Initiator`] and [`crate::Responder`] use +// [`GenericCipher`] instances (`c1` and `c2`) to perform symmetric encryption and decryption. +// These ciphers, initialized and managed through the [`CipherState`] trait, ensure ongoing +// communication remains confidential and authenticated. +// +// The [`CipherState`] trait and [`GenericCipher`] enum are essential for managing AEAD ciphers +// within the Noise protocol, ensuring secure data handling, key management, and nonce tracking +// throughout the communication session. + use std::ptr; use crate::aed_cipher::AeadCipher; use aes_gcm::Aes256Gcm; use chacha20poly1305::{aead::Buffer, ChaCha20Poly1305}; +/// The `CipherState` trait manages AEAD ciphers for secure communication, handling the encryption key, nonce, +/// and cipher instance. It supports encryption and decryption with ciphers like [`ChaCha20Poly1305`] and +/// [`Aes256Gcm`], ensuring proper key and nonce management. +/// +/// Key responsibilities: +/// - **Key management**: Set and retrieve the 32-byte encryption key. +/// - **Nonce management**: Track unique nonces for encryption operations. +/// - **Cipher handling**: Initialize and manage AEAD ciphers for secure data encryption. +/// +/// Used in protocols like Noise, `CipherState` ensures secure communication by managing cryptographic material +/// during and after handshakes. pub trait CipherState where Self: Sized, { + // Retrieves a mutable reference to the 32-byte encryption key (`k`). fn get_k(&mut self) -> &mut Option<[u8; 32]>; + + // Sets the 32-byte encryption key to the optionally provided value (`k`). + // + // Allows the encryption key to be explicitly set, typically after it has been derived or + // initialized during the handshake process. If `None`, the encryption key is unset. fn set_k(&mut self, k: Option<[u8; 32]>); + + // Retrieves the current nonce (`n`) used for encryption. + // + // The nonce is a counter that is incremented with each encryption/decryption operations to + // ensure that each encryption operation with the same key produces a unique ciphertext. fn get_n(&self) -> u64; + + // Sets the nonce (`n`) to the provided value. + // + // Allows the nonce to be explicitly set, typically after it has been initialized, incremented + // during the encryption process, or reset. fn set_n(&mut self, n: u64); + + // Retrieves a mutable reference to the optional cipher instance. + // + // Provides access to the underlying AEAD cipher instance used for encryption and decryption + // operations. fn get_cipher(&mut self) -> &mut Option; + // Converts the current 64-bit nonce value (`n`) to a 12-byte array. + // + // Converts the 64-bit nonce value to a 12-byte array suitable for use with AEAD ciphers, + // which typically expect a 96-bit (12-byte) nonce. The result is a correctly formatted nonce + // for use in encryption and decryption operations. fn nonce_to_bytes(&self) -> [u8; 12] { let mut res = [0u8; 12]; let n = self.get_n(); @@ -37,6 +109,11 @@ where Some(Cipher::from_cipher(c)) } + // Encrypts the provided `data` in place using the cipher and AAD (`ad`). + // + // Performs authenticated encryption on the provided `data` buffer, modifying it in place to + // contain the ciphertext. The encryption is performed using the current nonce and the AAD. + // The nonce is incremented after each successful encryption. fn encrypt_with_ad( &mut self, ad: &[u8], @@ -58,6 +135,11 @@ where } } + // Decrypts the data in place using the cipher and AAD (`ad`). + // + // Performs authenticated decryption on the provided `data` buffer, modifying it in place to + // contain the plaintext. The decryption is performed using the current nonce and the provided + // AAD. The nonce is incremented after each successful decryption. fn decrypt_with_ad( &mut self, ad: &[u8], @@ -80,6 +162,15 @@ where } } +/// The `GenericCipher` enum abstracts the use of two AEAD ciphers: [`ChaCha20Poly1305`] and [`Aes256Gcm`]. +/// It provides a unified interface for secure encryption and decryption, allowing flexibility in choosing +/// the cipher while ensuring consistent cryptographic operations. +/// +/// Variants: +/// - **ChaCha20Poly1305**: Uses the `ChaCha20Poly1305` cipher for encryption. +/// - **Aes256Gcm**: Uses the `Aes256Gcm` cipher for encryption. +/// +/// `GenericCipher` enables easy switching between ciphers while maintaining secure key and nonce management. #[allow(clippy::large_enum_variant)] pub enum GenericCipher { ChaCha20Poly1305(Cipher), @@ -88,24 +179,45 @@ pub enum GenericCipher { } impl Drop for GenericCipher { + // Securely erases the encryption key when the [`GenericCipher`] is dropped. + // + // Ensures that the encryption key is securely erased from memory when the [`GenericCipher`] + // instance is dropped, preventing any potential leakage of sensitive cryptographic material. fn drop(&mut self) { self.erase_k(); } } impl GenericCipher { + // Encrypts the data (`msg`) in place using the underlying cipher. + // + // Performs authenticated encryption on the provided data buffer, modifying it in place to + // contain the ciphertext. The encryption is performed using the current nonce and an empty + // additional associated data (AAD) buffer. pub fn encrypt(&mut self, msg: &mut T) -> Result<(), aes_gcm::Error> { match self { GenericCipher::ChaCha20Poly1305(c) => c.encrypt_with_ad(&[], msg), GenericCipher::Aes256Gcm(c) => c.encrypt_with_ad(&[], msg), } } + + // Decrypts the data (`msg`) in place using the underlying cipher. + // + // Performs authenticated decryption on the provided data buffer, modifying it in place to + // contain the plaintext. The decryption is performed using the current nonce and an empty + // additional associated data (AAD) buffer. pub fn decrypt(&mut self, msg: &mut T) -> Result<(), aes_gcm::Error> { match self { GenericCipher::ChaCha20Poly1305(c) => c.decrypt_with_ad(&[], msg), GenericCipher::Aes256Gcm(c) => c.decrypt_with_ad(&[], msg), } } + + // Securely erases the encryption key (`k`) from memory. + // + // Overwrites the encryption key stored within the [`GenericCipher`] with zeros and sets it to + // `None`, ensuring that the key cannot be recovered after the [`GenericCipher`] is dropped or + // no longer needed. pub fn erase_k(&mut self) { match self { GenericCipher::ChaCha20Poly1305(c) => { @@ -180,20 +292,35 @@ impl CipherState for GenericCipher { } } +// Represents the state of an AEAD cipher, including the optional 32-byte encryption key (`k`), +// nonce (`n`), and optional cipher instance (`cipher`). +// +// Manages the cryptographic state required to perform AEAD encryption and decryption operations. +// It stores the optional encryption key, the nonce, and the optional cipher instance itself. The +// [`CipherState`] trait is implemented to provide a consistent interface for managing cipher +// state across different AEAD ciphers. pub struct Cipher { + // Optional 32-byte encryption key. k: Option<[u8; 32]>, + // Nonce value. n: u64, + // Optional cipher instance. cipher: Option, } -// Make sure that Cipher is not sync so we do not need to worry about what other memory accessor see -// after that we zeroize k is send cause if we send it the original thread can not access -// anymore it -//impl !Sync for Cipher {} -//impl !Copy for Cipher {} - +// Ensures that the `Cipher` type is not `Sync`, which prevents multiple threads from +// simultaneously accessing the same instance of `Cipher`. This eliminates the need to handle +// potential issues related to visibility of changes across threads. +// +// After sending the `k` value, we immediately clear it to prevent the original thread from +// accessing the value again, thereby enhancing security by ensuring the sensitive data is no +// longer available in memory. +// +// The `Cipher` struct is neither `Sync` nor `Copy` due to its `cipher` field, which implements +// the `AeadCipher` trait. This trait requires mutable access, making the entire struct non-`Sync` +// and non-`Copy`, even though the key and nonce are simple types. impl Cipher { - /// Internal use only, we need k for handshake + // Internal use only, we need k for handshake pub fn from_key_and_cipher(k: [u8; 32], c: C) -> Self { Self { k: Some(k), @@ -202,7 +329,7 @@ impl Cipher { } } - /// At the end of the handshake we return a cipher with hidden key + // At the end of the handshake we return a cipher with hidden key #[allow(dead_code)] pub fn from_cipher(c: C) -> Self { Self { From 30a94c00e07275914d9c03a59dbdc74b9061b69e Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:48:19 +0530 Subject: [PATCH 07/15] add docs for error.rs --- protocols/v2/noise-sv2/src/error.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/protocols/v2/noise-sv2/src/error.rs b/protocols/v2/noise-sv2/src/error.rs index f8f10a6ea..5e5be3fff 100644 --- a/protocols/v2/noise-sv2/src/error.rs +++ b/protocols/v2/noise-sv2/src/error.rs @@ -1,18 +1,46 @@ +// # Error Handling +// +// Defines error types and utilities for handling errors in the `noise_sv2` module. + use aes_gcm::Error as AesGcm; +/// Noise protocol error handling. #[derive(Debug, PartialEq, Eq)] pub enum Error { + /// The handshake has not been completed when a finalization step is executed. HandshakeNotFinalized, + + /// Error on an empty cipher list is provided where one is required. CipherListMustBeNonEmpty, + + /// Error on unsupported ciphers. UnsupportedCiphers(Vec), + + /// Provided cipher list is invalid or malformed. InvalidCipherList(Vec), + + /// Chosen cipher is invalid or unsupported. InvalidCipherChosed(Vec), + + /// Wraps AES-GCM errors during encryption/decryption. AesGcm(AesGcm), + + /// Cipher is in an invalid state during encryption/decryption operations. InvalidCipherState, + + /// Provided certificate is invalid or cannot be verified. InvalidCertificate([u8; 74]), + + /// A raw public key is invalid or cannot be parsed. InvalidRawPublicKey, + + /// A raw private key is invalid or cannot be parsed. InvalidRawPrivateKey, + + /// An incoming handshake message is expected but not received. ExpectedIncomingHandshakeMessage, + + /// A message has an incorrect or unexpected length. InvalidMessageLength, } From b1ecb9e2cf0eea1b16bf1daca34a2fab7867f25e Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:48:45 +0530 Subject: [PATCH 08/15] add docs for lib.rs --- protocols/v2/noise-sv2/src/lib.rs | 51 +++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/protocols/v2/noise-sv2/src/lib.rs b/protocols/v2/noise-sv2/src/lib.rs index 85ead6e6e..46316b2ed 100644 --- a/protocols/v2/noise-sv2/src/lib.rs +++ b/protocols/v2/noise-sv2/src/lib.rs @@ -1,6 +1,36 @@ -//! Implement Sv2 noise https://github.com/stratum-mining/sv2-spec/blob/main/04-Protocol-Security.md#4-protocol-security - -// #![feature(negative_impls)] +//! # Noise-SV2: Noise Protocol Implementation for Stratum V2 +//! +//! `noise_sv2` ensures secure communication between Sv2 roles by handling encryption, decryption, +//! and authentication through Noise protocol handshakes and cipher operations. +//! +//! Implementation of the [Sv2 Noise protocol specification](https://github.com/stratum-mining/sv2-spec/blob/main/04-Protocol-Security.md#4-protocol-security). +//! +//! ## Features +//! - Noise Protocol: Establishes secure communication via the Noise protocol handshake between the +//! [`Initiator`] and [`Responder`] roles. +//! - Diffie-Hellman with [`secp256k1`]: Securely establishes a shared secret between two Sv2 +//! roles, using the same elliptic curve used in Bitcoin. +//! - AEAD: Ensures confidentiality and integrity of the data. +//! - `AES-GCM` and `ChaCha20-Poly1305`: Provides encryption, with hardware-optimized and +//! software-optimized options. +//! - Schnorr Signatures: Authenticates messages and verifies the identity of the Sv2 roles. +//! In practice, the primitives exposed by this crate should be used to secure communication +//! channels between Sv2 roles. Securing communication between two Sv2 roles on the same local +//! network (e.g., local mining devices communicating with a local mining proxy) is optional. +//! However, it is mandatory to secure the communication between two Sv2 roles communicating over a +//! remote network (e.g., a local mining proxy communicating with a remote pool sever). +//! +//! The Noise protocol establishes secure communication between two Sv2 roles via a handshake +//! performed at the beginning of the connection. The initiator (e.g., a local mining proxy) and +//! the responder (e.g., a mining pool) establish a shared secret using Elliptic Curve +//! Diffie-Hellman (ECDH) with the [`secp256k1`] elliptic curve (the same elliptic curve used by +//! Bitcoin). Once both Sv2 roles compute the shared secret from the ECDH exchange, the Noise +//! protocol derives symmetric encryption keys for secure communication. These keys are used with +//! AEAD (using either `AES-GCM` or `ChaCha20-Poly1305`) to encrypt and authenticate all +//! communication between the roles. This encryption ensures that sensitive data, such as share +//! submissions, remains confidential and tamper-resistant. Additionally, Schnorr signatures are +//! used to authenticate messages and validate the identities of the Sv2 roles, ensuring that +//! critical messages like job templates and share submissions originate from legitimate sources. use aes_gcm::aead::Buffer; pub use aes_gcm::aead::Error as AeadError; @@ -17,10 +47,22 @@ mod test; pub use const_sv2::{NOISE_HASHED_PROTOCOL_NAME_CHACHA, NOISE_SUPPORTED_CIPHERS_MESSAGE}; +// The parity value used in the Schnorr signature process. +// +// Used to define whether a public key corresponds to an even or odd point on the elliptic curve. +// In this case, `Parity::Even` is used. const PARITY: secp256k1::Parity = secp256k1::Parity::Even; +/// A codec for managing encrypted communication in the Noise protocol. +/// +/// Manages the encryption and decryption of messages between two parties, the [`Initiator`] and +/// [`Responder`], using the Noise protocol. A symmetric cipher is used for both encrypting +/// outgoing messages and decrypting incoming messages. pub struct NoiseCodec { + // Cipher to encrypt outgoing messages. encryptor: GenericCipher, + + // Cipher to decrypt incoming messages. decryptor: GenericCipher, } @@ -31,9 +73,12 @@ impl std::fmt::Debug for NoiseCodec { } impl NoiseCodec { + /// Encrypts a message (`msg`) in place using the stored cipher. pub fn encrypt(&mut self, msg: &mut T) -> Result<(), aes_gcm::Error> { self.encryptor.encrypt(msg) } + + /// Decrypts a message (`msg`) in place using the stored cipher. pub fn decrypt(&mut self, msg: &mut T) -> Result<(), aes_gcm::Error> { self.decryptor.decrypt(msg) } From ec41fcdc070e8dfe31e32fcee3249b64ba549a33 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:49:13 +0530 Subject: [PATCH 09/15] add docs for handshake.rs --- protocols/v2/noise-sv2/src/handshake.rs | 138 +++++++++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/protocols/v2/noise-sv2/src/handshake.rs b/protocols/v2/noise-sv2/src/handshake.rs index 3ab021374..0d71fde44 100644 --- a/protocols/v2/noise-sv2/src/handshake.rs +++ b/protocols/v2/noise-sv2/src/handshake.rs @@ -1,3 +1,35 @@ +// # Noise Handshake Operations +// +// The [`HandshakeOp`] trait defines the cryptographic operations and utilities required to perform +// the Noise protocol handshake between Sv2 roles. +// +// This trait abstracts key management, encryption, and hashing for the Noise protocol handshake, +// outlining core operations implemented by the [`crate::Initiator`] and [`crate::Responder`] +// roles. The trait governs the following processes: +// +// - Elliptic curve Diffie-Hellman (ECDH) key exchange using the [`secp256k1`] curve to establish a +// shared secret. +// - HMAC and HKDF for deriving encryption keys from the shared secret. +// - AEAD encryption and decryption using either [`ChaCha20Poly1305`] or `AES-GCM` ciphers to +// ensure message confidentiality and integrity. +// - Chaining key and handshake hash updates to maintain the security of the session. +// +// The handshake begins with the exchange of ephemeral key pairs, followed by the derivation of +// shared secrets, which are then used to securely encrypt all subsequent communication. +// +// ## Usage +// The handshake secures communication between two Sv2 roles, with one acting as the +// [`crate::Initiator`] (e.g., a local mining proxy) and the other as the [`crate::Responder`] +// (e.g., a remote pool). Both roles implement the [`HandshakeOp`] trait to manage cryptographic +// state, updating the handshake hash (`h`), chaining key (`ck`), and encryption key (`k`) to +// ensure confidentiality and integrity throughout the handshake. +// +// Securing communication via the Noise protocol guarantees the confidentiality and authenticity of +// sensitive data, such as share submissions. While the use of a secure channel is optional for Sv2 +// roles within a local network (e.g., between a local mining device and mining proxy), it is +// mandatory for communication across external networks (e.g., between a local mining proxy and a +// remote pool). + use crate::{aed_cipher::AeadCipher, cipher_state::CipherState, NOISE_HASHED_PROTOCOL_NAME_CHACHA}; use chacha20poly1305::ChaCha20Poly1305; use secp256k1::{ @@ -6,16 +38,54 @@ use secp256k1::{ rand, Keypair, Secp256k1, SecretKey, XOnlyPublicKey, }; +// Represents the operations needed during a Noise protocol handshake. +// +// The [`HandshakeOp`] trait defines the necessary functions for managing the state and +// cryptographic operations required during the Noise protocol handshake. It provides methods for +// key generation, hash mixing, encryption, decryption, and key derivation, ensuring that the +// handshake process is secure and consistent. pub trait HandshakeOp: CipherState { + // Returns the name of the entity implementing the handshake operation. + // + // Provides a string that identifies the entity (e.g., "Initiator" or "Responder") that is + // performing the handshake. It is primarily used for debugging or logging purposes. + #[allow(dead_code)] fn name(&self) -> String; + + // Retrieves a mutable reference to the handshake hash (`h`). + // + // The handshake hash accumulates the state of the handshake, incorporating all exchanged + // messages to ensure integrity and prevent tampering. This method provides access to the + // current state of the handshake hash, allowing it to be updated as the handshake progresses. fn get_h(&mut self) -> &mut [u8; 32]; + // Retrieves a mutable reference to the chaining key (`ck`). + // + // The chaining key is used during the key derivation process to generate new keys throughout + // the handshake. This method provides access to the current chaining key, which is updated + // as the handshake progresses and new keys are derived. fn get_ck(&mut self) -> &mut [u8; 32]; + // Sets the handshake hash (`h`) to the provided value. + // + // This method allows the handshake hash to be explicitly set, typically after it has been + // initialized or updated during the handshake process. The handshake hash ensures the + // integrity of the handshake by incorporating all exchanged messages. fn set_h(&mut self, data: [u8; 32]); + // Sets the chaining key (`ck`) to the provided value. + // + // This method allows the chaining key to be explicitly set, typically after it has been + // initialized or updated during the handshake process. The chaining key is crucial for + // deriving new keys as the handshake progresses. fn set_ck(&mut self, data: [u8; 32]); + // Mixes the data into the handshake hash (`h`). + // + // Updates the current handshake hash by combining it with the provided `data`. The result is + // a new SHA-256 hash digest that reflects all previous handshake messages, ensuring the + // integrity of the handshake process. This method is typically called whenever a new piece of + // data (e.g., a public key or ciphertext) needs to be incorporated into the handshake state. fn mix_hash(&mut self, data: &[u8]) { let h = self.get_h(); let mut to_hash = Vec::with_capacity(32 + data.len()); @@ -24,6 +94,11 @@ pub trait HandshakeOp: CipherState { *h = Sha256Hash::hash(&to_hash).to_byte_array(); } + // Generates a new cryptographic key pair using the [`Secp256k1`] curve. + // + // Generates a fresh key pair, consisting of a secret key and a corresponding public key, + // using the [`Secp256k1`] elliptic curve. If the generated public key does not match the + // expected parity, a new key pair is generated to ensure consistency. fn generate_key() -> Keypair { let secp = Secp256k1::new(); let (secret_key, _) = secp.generate_keypair(&mut rand::thread_rng()); @@ -35,6 +110,17 @@ pub trait HandshakeOp: CipherState { } } + // Computes an HMAC-SHA256 (Hash-based Message Authentication Code) hash of the provided data + // using the given key. + // + // This method implements the HMAC-SHA256 hashing algorithm, which combines a key and data to + // produce a 32-byte hash. It is used during the handshake to securely derive new keys from + // existing material, ensuring that the resulting keys are cryptographically strong. + // + // This method uses a two-step process: + // 1. The key is XORed with an inner padding (`ipad`) and hashed with the data. + // 2. The result is XORed with the outer padding (`opad`) and hashed again to produce the + // final HMAC. fn hmac_hash(key: &[u8; 32], data: &[u8]) -> [u8; 32] { #[allow(clippy::identity_op)] let mut ipad = [(0 ^ 0x36); 64]; @@ -59,6 +145,21 @@ pub trait HandshakeOp: CipherState { Sha256Hash::hash(&to_hash).to_byte_array() } + // Derives two new keys using the HKDF (HMAC-based Key Derivation Function) process. + // + // Performs the HKDF key derivation process, which uses an initial chaining key and input key + // material to produce two new 32-byte keys. This process is used throughout the handshake to + // generate fresh keys for encryption and authentication, ensuring that each step of the + // handshake is securely linked. + // + // This method performs the following steps: + // 1. Performs a HMAC hash on the chaining key and input key material to derive a temporary + // key. + // 2. Performs a HMAC hash on the temporary key and specific byte sequence (`0x01`) to derive + // the first output. + // 3. Performs a HMAC hash on the temporary key and the concatenation of the first output and + // a specific byte sequence (`0x02`). + // 4. Returns both outputs. fn hkdf_2(chaining_key: &[u8; 32], input_key_material: &[u8]) -> ([u8; 32], [u8; 32]) { let temp_key = Self::hmac_hash(chaining_key, input_key_material); let out_1 = Self::hmac_hash(&temp_key, &[0x1]); @@ -77,6 +178,13 @@ pub trait HandshakeOp: CipherState { (out_1, out_2, out_3) } + // Mixes the input key material into the current chaining key (`ck`) and initializes the + // handshake cipher with an updated encryption key (`k`). + // + // Updates the chaining key by incorporating the provided input key material (e.g., the result + // of a Diffie-Hellman exchange) and uses the updated chaining key to derive a new encryption + // key. The encryption key is then used to initialize the handshake cipher, preparing it for + // use in the next step of the handshake. fn mix_key(&mut self, input_key_material: &[u8]) { let ck = self.get_ck(); let (ck, temp_k) = Self::hkdf_2(ck, input_key_material); @@ -84,6 +192,14 @@ pub trait HandshakeOp: CipherState { self.initialize_key(temp_k); } + // Encrypts the provided plaintext and updates the hash `h` value. + // + // The `encrypt_and_hash` method encrypts the given plaintext using the + // current encryption key and then updates `h` with the resulting ciphertext. + // If an encryption key is present `k`, the method encrypts the data using + // using AEAD, where the associated data is the current hash value. After + // encryption, the ciphertext is mixed into the hash to ensure integrity + // and authenticity of the messages exchanged during the handshake. fn encrypt_and_hash(&mut self, plaintext: &mut Vec) -> Result<(), aes_gcm::Error> { if self.get_k().is_some() { #[allow(clippy::clone_on_copy)] @@ -95,6 +211,14 @@ pub trait HandshakeOp: CipherState { Ok(()) } + // Decrypts the provided ciphertext and updates the handshake hash (`h`). + // + // Decrypts the given ciphertext using the handshake cipher and then mixes the ciphertext + // (before decryption) into the handshake hash. If the encryption key (`k`) is present, the + // data is decrypted using AEAD, where the associated data is the current handshake hash. This + // ensures that each decryption step is securely linked to the previous handshake state, + // maintaining the integrity of the + // handshake. fn decrypt_and_hash(&mut self, ciphertext: &mut Vec) -> Result<(), aes_gcm::Error> { let encrypted = ciphertext.clone(); if self.get_k().is_some() { @@ -113,8 +237,12 @@ pub trait HandshakeOp: CipherState { res.secret_bytes() } - /// Prior to starting first round of NX-handshake, both initiator and responder initializes - /// handshake variables h (hash output), ck (chaining key) and k (encryption key): + // Initializes the handshake state by setting the initial chaining key (`ck`) and handshake + // hash (`h`). + // + // Prepares the handshake state for use by setting the initial chaining key and handshake + // hash. The chaining key is typically derived from a protocol name or other agreed-upon + // value, and the handshake hash is initialized to reflect this starting state. fn initialize_self(&mut self) { let ck = NOISE_HASHED_PROTOCOL_NAME_CHACHA; let h = Sha256Hash::hash(&ck[..]); @@ -123,6 +251,11 @@ pub trait HandshakeOp: CipherState { self.set_k(None); } + // Initializes the handshake cipher with the provided encryption key (`k`). + // + // Resets the nonce (`n`) to 0 and initializes the handshake cipher using the given 32-byte + // encryption key. It also updates the internal key storage (`k`) with the new key, preparing + // the cipher for encrypting or decrypting subsequent messages in the handshake. fn initialize_key(&mut self, key: [u8; 32]) { self.set_n(0); let cipher = ChaCha20Poly1305::from_key(key); @@ -143,6 +276,7 @@ mod test { use super::*; use quickcheck::{Arbitrary, TestResult}; use quickcheck_macros; + use secp256k1::{ecdh::SharedSecret, SecretKey, XOnlyPublicKey}; use std::convert::TryInto; struct TestHandShake { From 579ad8dbddd0b8304266b5563488b388b8af32c3 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:49:48 +0530 Subject: [PATCH 10/15] add docs for initiator.rs --- protocols/v2/noise-sv2/src/initiator.rs | 178 +++++++++++++++++------- 1 file changed, 131 insertions(+), 47 deletions(-) diff --git a/protocols/v2/noise-sv2/src/initiator.rs b/protocols/v2/noise-sv2/src/initiator.rs index d789aef20..24a3423f3 100644 --- a/protocols/v2/noise-sv2/src/initiator.rs +++ b/protocols/v2/noise-sv2/src/initiator.rs @@ -1,3 +1,39 @@ +// # Initiator Role +// +// Manages the [`Initiator`] role in the Noise protocol handshake for communication between Sv2 +// roles. The initiator is responsible for starting the handshake process by sending the first +// cryptographic message to the [`crate::Responder`] role (e.g., a mining pool). +// +// The [`Initiator`] role is equipped with utilities for generating and managing the initiator's +// key pairs, performing elliptic curve Diffie-Hellman (ECDH) key exchanges, and encrypting +// messages during the handshake phase. The initiator's responsibilities include: +// +// - Generating an ephemeral key pair for the handshake. +// - Using the [`secp256k1`] elliptic curve for ECDH to derive a shared secret. +// - Encrypting the initial handshake message to securely exchange cryptographic material. +// - Managing the state transitions between handshake steps, including updating the handshake hash, +// chaining key, and encryption key as the session progresses. +// +// ## Usage +// The initiator role is typically used by a downstream Sv2 role (e.g., a local mining proxy) to +// establish a secure connection with an upstream responder (e.g., a remote mining pool). The +// initiator begins the handshake by generating a public key and sending it to the responder. After +// receiving a response, the initiator computes a shared secret and encrypts further messages using +// this key. +// +// The [`Initiator`] struct implements the [`HandshakeOp`] trait, which defines the core +// cryptographic operations during the handshake. It ensures secure communication by supporting +// both the [`ChaCha20Poly1305`] or `AES-GCM` cipher, providing both confidentiality and message +// authentication for all subsequent communication. +// +// ### Secure Data Erasure +// +// The [`Initiator`] includes functionality for securely erasing sensitive cryptographic material, +// ensuring that private keys and other sensitive data are wiped from memory when no longer needed. +// +// The [`Drop`] trait is implemented to automatically trigger secure erasure when the [`Initiator`] +// instance goes out of scope, preventing potential misuse or leakage of cryptographic material. + use std::{convert::TryInto, ptr}; use crate::{ @@ -19,20 +55,41 @@ use secp256k1::{ Keypair, PublicKey, XOnlyPublicKey, }; +/// Manages the initiator's role in the Noise NX handshake, handling key exchange, encryption, and +/// handshake state. It securely generates and manages cryptographic keys, performs Diffie-Hellman +/// exchanges, and maintains the handshake hash, chaining key, and nonce for message encryption. +/// After the handshake, it facilitates secure communication using either [`ChaCha20Poly1305`] or +/// `AES-GCM` ciphers. Sensitive data is securely erased when no longer needed. pub struct Initiator { + // Cipher used for encrypting and decrypting messages during the handshake. + // + // It is initialized once enough information is available from the handshake process. handshake_cipher: Option, + // Optional static key used in the handshake. This key may be derived from the pre-shared key + // (PSK) or generated during the handshake. k: Option<[u8; 32]>, + // Current nonce used in the encryption process. + // + // Ensures that the same plaintext encrypted twice will produce different ciphertexts. n: u64, - // Chaining key + // Chaining key used in the key derivation process to generate new keys throughout the + // handshake. ck: [u8; 32], - // Handshake hash + // Handshake hash which accumulates all handshake messages to ensure integrity and prevent + // tampering. h: [u8; 32], - // ephemeral keypair + // Ephemeral key pair generated by the initiator for this session, used for generating the + // shared secret with the responder. e: Keypair, - // upstream pub key + // Optional public key of the responder, used to authenticate the responder during the + // handshake. #[allow(unused)] responder_authority_pk: Option, + // First [`CipherState`] used for encrypting messages from the initiator to the responder + // after the handshake is complete. c1: Option, + // Second [`CipherState`] used for encrypting messages from the responder to the initiator + // after the handshake is complete. c2: Option, } @@ -42,22 +99,30 @@ impl std::fmt::Debug for Initiator { } } -// Make sure that Initiator is not sync so we do not need to worry about what other memory accessor see -// after that we zeroize k is send cause if we send it the original thread can not access -// anymore it -//impl !Sync for Initiator {} -//impl !Copy for Initiator {} - +// Ensures that the `Cipher` type is not `Sync`, which prevents multiple threads from +// simultaneously accessing the same instance of `Cipher`. This eliminates the need to handle +// potential issues related to visibility of changes across threads. +// +// After sending the `k` value, we immediately clear it to prevent the original thread from +// accessing the value again, thereby enhancing security by ensuring the sensitive data is no +// longer available in memory. +// +// The `Cipher` struct is neither `Sync` nor `Copy` due to its `cipher` field, which implements +// the `AeadCipher` trait. This trait requires mutable access, making the entire struct non-`Sync` +// and non-`Copy`, even though the key and nonce are simple types. impl CipherState for Initiator { fn get_k(&mut self) -> &mut Option<[u8; 32]> { &mut self.k } + fn get_n(&self) -> u64 { self.n } + fn set_n(&mut self, n: u64) { self.n = n; } + fn get_cipher(&mut self) -> &mut Option { &mut self.handshake_cipher } @@ -71,6 +136,7 @@ impl HandshakeOp for Initiator { fn name(&self) -> String { "Initiator".to_string() } + fn get_h(&mut self) -> &mut [u8; 32] { &mut self.h } @@ -93,16 +159,11 @@ impl HandshakeOp for Initiator { } impl Initiator { - pub fn from_raw_k(key: [u8; 32]) -> Result, Error> { - let pk = - secp256k1::XOnlyPublicKey::from_slice(&key).map_err(|_| Error::InvalidRawPublicKey)?; - Ok(Self::new(Some(pk))) - } - - pub fn without_pk() -> Result, Error> { - Ok(Self::new(None)) - } - + /// Creates a new [`Initiator`] instance with an optional responder public key. + /// + /// If the responder public key is provided, the initiator uses this key to authenticate the + /// responder during the handshake. The initial initiator state is instantiated with the + /// ephemeral key pair and handshake hash. pub fn new(pk: Option) -> Box { let mut self_ = Self { handshake_cipher: None, @@ -119,23 +180,39 @@ impl Initiator { Box::new(self_) } - /// #### 4.5.1.1 Initiator + /// Creates a new [`Initiator`] instance using a raw 32-byte public key. /// - /// Initiator generates ephemeral keypair and sends the public key to the responder: + /// Constructs a [`XOnlyPublicKey`] from the provided raw key slice and initializes a new + /// [`Initiator`] with the derived public key. If the provided key cannot be converted into a + /// valid [`XOnlyPublicKey`], an [`Error::InvalidRawPublicKey`] error is returned. /// - /// 1. initializes empty output buffer - /// 2. generates ephemeral keypair `e`, appends `e.public_key` to the buffer (64 bytes plaintext public key encoded with ElligatorSwift) - /// 3. calls `MixHash(e.public_key)` - /// 4. calls `EncryptAndHash()` with empty payload and appends the ciphertext to the buffer (note that _k_ is empty at this point, so this effectively reduces down to `MixHash()` on empty data) - /// 5. submits the buffer for sending to the responder in the following format - /// - /// ##### Ephemeral public key message: + /// Typically used when the initiator is aware of the responder's public key in advance. + pub fn from_raw_k(key: [u8; 32]) -> Result, Error> { + let pk = + secp256k1::XOnlyPublicKey::from_slice(&key).map_err(|_| Error::InvalidRawPublicKey)?; + Ok(Self::new(Some(pk))) + } + + /// Creates a new [`Initiator`] without requiring the responder's authority public key. + /// This function initializes the [`Initiator`] with a default empty state and is intended + /// for use when both the initiator and responder are within the same network. In this case, + /// the initiator does not validate the responder's static key from a certificate. However, + /// the connection remains encrypted. + pub fn without_pk() -> Result, Error> { + Ok(Self::new(None)) + } + + /// Executes the initial step of the Noise NX protocol handshake. /// - /// | Field name | Description | - /// | ---------- | -------------------------------- | - /// | PUBKEY | Initiator's ephemeral public key | + /// This step involves generating an ephemeral keypair and encoding the public key using + /// Elligator Swift encoding, which obscures the key to prevent identification. The encoded + /// public key is then mixed into the handshake state, and an empty payload is encrypted. + /// This operation currently only affects the handshake hash, as the key (`k`) is not yet + /// established. The function returns the encoded public key, which is ready to be sent to + /// the responder. /// - /// Message length: 64 bytes + /// On success, the function returns a 64-byte array containing the encoded public key. + /// If an error occurs during encryption, it returns an [`aes_gcm::Error`]. pub fn step_0(&mut self) -> Result<[u8; ELLSWIFT_ENCODING_SIZE], aes_gcm::Error> { let elliswift_enc_pubkey = ElligatorSwift::from_pubkey(self.e.public_key()).to_array(); self.mix_hash(&elliswift_enc_pubkey); @@ -146,23 +223,24 @@ impl Initiator { Ok(message) } - /// #### 4.5.2.2 Initiator - /// - /// 1. receives NX-handshake part 2 message - /// 2. interprets first 64 bytes as ElligatorSwift encoding of `re.public_key` - /// 3. calls `MixHash(re.public_key)` - /// 4. calls `MixKey(ECDH(e.private_key, re.public_key))` - /// 5. decrypts next 80 bytes (64 bytes for ElligatorSwift encoded pubkey + 16 bytes MAC) with `DecryptAndHash()` and stores the results as `rs.public_key` which is **server's static public key**. - /// 6. calls `MixKey(ECDH(e.private_key, rs.public_key)` - /// 7. decrypts next 90 bytes with `DecryptAndHash()` and deserialize plaintext into `SIGNATURE_NOISE_MESSAGE` (74 bytes data + 16 bytes MAC) - /// 8. return pair of CipherState objects, the first for encrypting transport messages from initiator to responder, and the second for messages in the other direction: - /// 1. sets `temp_k1, temp_k2 = HKDF(ck, zerolen, 2)` - /// 2. creates two new CipherState objects `c1` and `c2` - /// 3. calls `c1.InitializeKey(temp_k1)` and `c2.InitializeKey(temp_k2)` - /// 4. returns the pair `(c1, c2)` + /// Processes the second step of the Noise NX protocol handshake for the initiator. /// + /// This method handles the responder's reply in the Noise NX protocol handshake, processing + /// the message to derive shared secrets and authenticate the responder. It interprets the + /// first 64 bytes of the message as the responder's ephemeral public key, decodes it, and + /// mixes it into the handshake state. It then derives a shared secret from the ephemeral keys + /// and updates the state accordingly. /// + /// The next 80 bytes of the message contain the responder's static public key, encrypted and + /// authenticated. The method decrypts this segment and derives another shared secret using the + /// responder's static public key, further securing the handshake state. Finally, the method + /// decrypts and verifies the signature included in the message to ensure the responder's + /// authenticity. /// + /// On success, this method returns a [`NoiseCodec`] instance initialized with session ciphers + /// for secure communication. If the provided `message` has an incorrect length, it returns an + /// [`Error::InvalidMessageLength`]. If decryption or signature verification fails, it returns + /// an [`Error::InvalidCertificate`]. pub fn step_2( &mut self, message: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE], @@ -252,6 +330,12 @@ impl Initiator { } } + // Securely erases sensitive data from the [`Initiator`] memory. + // + // Clears all sensitive cryptographic material within the [`Initiator`] to prevent any + // accidental leakage or misuse. It overwrites the stored keys, chaining key, handshake hash, + // and session ciphers with zeros. This method is typically + // called when the [`Initiator`] instance is no longer needed or before deallocation. fn erase(&mut self) { if let Some(k) = self.k.as_mut() { for b in k { From 82e2814cdd6493f2190b3cbe5ef40fc1d2408d51 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:50:05 +0530 Subject: [PATCH 11/15] add docs for responder.rs --- protocols/v2/noise-sv2/src/responder.rs | 190 +++++++++++++++++------- 1 file changed, 134 insertions(+), 56 deletions(-) diff --git a/protocols/v2/noise-sv2/src/responder.rs b/protocols/v2/noise-sv2/src/responder.rs index f5a3ddcc2..7fd6dca72 100644 --- a/protocols/v2/noise-sv2/src/responder.rs +++ b/protocols/v2/noise-sv2/src/responder.rs @@ -1,3 +1,39 @@ +// # Responder Role +// +// Manages the [`Responder`] role in the Noise protocol handshake for secure communication between +// Sv2 roles. The responder is responsible for handling incoming handshake messages from the +// [`crate::Initiator`] (e.g., a mining proxy) and respond with the appropriate cryptographic +// data. +// +// The [`Responder`] role is equipped with utilities for handling elliptic curve Diffie-Hellman +// (ECDH) key exchanges, decrypting messages, and securely managing cryptographic state during the +// handshake phase. The responder's responsibilities include: +// +// - Generating an ephemeral key pair for the handshake. +// - Using the [`secp256k1`] elliptic curve for ECDH to compute a shared secret based on the +// initiator's public key. +// - Decrypting and processing incoming handshake messages from the initiator. +// - Managing state transitions, including updates to the handshake hash, chaining key, and +// encryption key as the session progresses. +// +// ## Usage +// The responder role is typically used by an upstream Sv2 role (e.g., a remote mining pool) to +// respond to an incoming handshake initiated by a downstream role (e.g., a local mining proxy). +// After receiving the initiator's public key, the responder computes a shared secret, which is +// used to securely encrypt further communication. +// +// The [`Responder`] struct implements the [`HandshakeOp`] trait, which defines the core +// cryptographic operations during the handshake. It ensures secure communication by supporting +// both the [`ChaCha20Poly1305`] or `AES-GCM` cipher, providing both confidentiality and message +// authentication for all subsequent communication. +// +// ### Secure Data Erasure +// +// The [`Responder`] includes functionality for securely erasing sensitive cryptographic material, +// ensuring that private keys and other sensitive data are wiped from memory when no longer needed. +// The [`Drop`] trait is implemented to automatically trigger secure erasure when the [`Responder`] +// instance goes out of scope, preventing potential misuse or leakage of cryptographic material. + use std::{ptr, time::Duration}; use crate::{ @@ -17,22 +53,47 @@ use secp256k1::{ellswift::ElligatorSwift, Keypair, Secp256k1, SecretKey}; const VERSION: u16 = 0; +/// Represents the state and operations of the responder in the Noise NX protocol handshake. +/// It handles cryptographic key exchanges, manages handshake state, and securely establishes +/// a connection with the initiator. The responder manages key generation, Diffie-Hellman exchanges, +/// message decryption, and state transitions, ensuring secure communication. Sensitive cryptographic +/// material is securely erased when no longer needed. pub struct Responder { + // Cipher used for encrypting and decrypting messages during the handshake. + // + // It is initialized once enough information is available from the handshake process. handshake_cipher: Option, + // Optional static key used in the handshake. This key may be derived from the pre-shared key + // (PSK) or generated during the handshake. k: Option<[u8; 32]>, + // Current nonce used in the encryption process. + // + // Ensures that the same plaintext encrypted twice will produce different ciphertexts. n: u64, - // Chaining key + // Chaining key used in the key derivation process to generate new keys throughout the + // handshake. ck: [u8; 32], - // Handshake hash + // Handshake hash which accumulates all handshake messages to ensure integrity and prevent + // tampering. h: [u8; 32], - // ephemeral keypair + // Ephemeral key pair generated by the responder for this session, used for generating the + // shared secret with the initiator. e: Keypair, - // Static pub keypair + // Static key pair of the responder, used to establish long-term identity and authenticity. + // + // Remains consistent across handshakes. s: Keypair, - // Authority pub keypair + // Authority key pair, representing the responder's authority credentials. + // + // Used to sign messages and verify the identity of the responder. a: Keypair, + // First [`CipherState`] used for encrypting messages from the initiator to the responder + // after the handshake is complete. c1: Option, + // Second [`CipherState`] used for encrypting messages from the responder to the initiator + // after the handshake is complete. c2: Option, + // Validity duration of the responder's certificate, in seconds. cert_validity: u32, } @@ -42,19 +103,27 @@ impl std::fmt::Debug for Responder { } } -// Make sure that Respoder is not sync so we do not need to worry about what other memory accessor see -// after that we zeroize k is send cause if we send it the original thread can not access -// anymore it -//impl !Sync for Responder {} -//impl !Copy for Responder {} +// Ensures that the `Cipher` type is not `Sync`, which prevents multiple threads from +// simultaneously accessing the same instance of `Cipher`. This eliminates the need to handle +// potential issues related to visibility of changes across threads. +// +// After sending the `k` value, we immediately clear it to prevent the original thread from +// accessing the value again, thereby enhancing security by ensuring the sensitive data is no +// longer available in memory. +// +// The `Cipher` struct is neither `Sync` nor `Copy` due to its `cipher` field, which implements +// the `AeadCipher` trait. This trait requires mutable access, making the entire struct non-`Sync` +// and non-`Copy`, even though the key and nonce are simple types. impl CipherState for Responder { fn get_k(&mut self) -> &mut Option<[u8; 32]> { &mut self.k } + fn get_n(&self) -> u64 { self.n } + fn set_n(&mut self, n: u64) { self.n = n; } @@ -62,6 +131,7 @@ impl CipherState for Responder { fn set_k(&mut self, k: Option<[u8; 32]>) { self.k = k; } + fn get_cipher(&mut self) -> &mut Option { &mut self.handshake_cipher } @@ -71,6 +141,7 @@ impl HandshakeOp for Responder { fn name(&self) -> String { "Responder".to_string() } + fn get_h(&mut self) -> &mut [u8; 32] { &mut self.h } @@ -93,22 +164,12 @@ impl HandshakeOp for Responder { } impl Responder { - pub fn from_authority_kp( - public: &[u8; 32], - private: &[u8; 32], - cert_validity: Duration, - ) -> Result, Error> { - let secp = Secp256k1::new(); - let secret = SecretKey::from_slice(private).map_err(|_| Error::InvalidRawPrivateKey)?; - let kp = Keypair::from_secret_key(&secp, &secret); - let pub_ = kp.x_only_public_key().0.serialize(); - if public == &pub_[..] { - Ok(Self::new(kp, cert_validity.as_secs() as u32)) - } else { - Err(Error::InvalidRawPublicKey) - } - } - + /// Creates a new [`Responder`] instance with the provided authority keypair and certificate + /// validity. + /// + /// Constructs a new [`Responder`] with the necessary cryptographic state for the Noise NX protocol + /// handshake. It generates ephemeral and static key pairs for the responder and prepares the + /// handshake state. The authority keypair and certificate validity period are also configured. pub fn new(a: Keypair, cert_validity: u32) -> Box { let mut self_ = Self { handshake_cipher: None, @@ -127,40 +188,43 @@ impl Responder { Box::new(self_) } - /// #### 4.5.1.2 Responder - /// - /// 1. receives ephemeral public key message with ElligatorSwift encoding (64 bytes plaintext) - /// 2. parses these 64 byte as PubKey and interprets is as `re.public_key` - /// 3. calls `MixHash(re.public_key)` - /// 4. calls `DecryptAndHash()` on remaining bytes (i.e. on empty data with empty _k_, thus effectively only calls `MixHash()` on empty data) + /// Creates a new [`Responder`] instance with the provided 32-byte authority key pair. /// - /// #### 4.5.2.1 Responder - /// - /// 1. initializes empty output buffer - /// 2. generates ephemeral keypair `e`, appends the 64 bytes ElligatorSwift encoding of `e.public_key` to the buffer - /// 3. calls `MixHash(e.public_key)` - /// 4. calls `MixKey(ECDH(e.private_key, re.public_key))` - /// 5. appends `EncryptAndHash(s.public_key)` (80 bytes: 64 bytes encrypted elliswift public key, 16 bytes MAC) - /// 6. calls `MixKey(ECDH(s.private_key, re.public_key))` - /// 7. appends `EncryptAndHash(SIGNATURE_NOISE_MESSAGE)` (74 + 16 bytes) to the buffer - /// 8. submits the buffer for sending to the initiator - /// 9. return pair of CipherState objects, the first for encrypting transport messages from initiator to responder, and the second for messages in the other direction: - /// 1. sets `temp_k1, temp_k2 = HKDF(ck, zerolen, 2)` - /// 2. creates two new CipherState objects `c1` and `c2` - /// 3. calls `c1.InitializeKey(temp_k1)` and `c2.InitializeKey(temp_k2)` - /// 4. returns the pair `(c1, c2)` + /// Constructs a new [`Responder`] with a given public and private key pair, which represents + /// the responder's authority credentials. It verifies that the provided public key matches the + /// corresponding private key, ensuring the authenticity of the authority key pair. The + /// certificate validity duration is also set here. Fails if the key pair is mismatched. + pub fn from_authority_kp( + public: &[u8; 32], + private: &[u8; 32], + cert_validity: Duration, + ) -> Result, Error> { + let secp = Secp256k1::new(); + let secret = SecretKey::from_slice(private).map_err(|_| Error::InvalidRawPrivateKey)?; + let kp = Keypair::from_secret_key(&secp, &secret); + let pub_ = kp.x_only_public_key().0.serialize(); + if public == &pub_[..] { + Ok(Self::new(kp, cert_validity.as_secs() as u32)) + } else { + Err(Error::InvalidRawPublicKey) + } + } + + /// Processes the first step of the Noise NX protocol handshake for the responder. /// - /// ##### Message format of NX-handshake part 2 + /// This function manages the responder's side of the handshake after receiving the initiator's + /// initial message. It processes the ephemeral public key provided by the initiator, derives + /// the necessary shared secrets, and constructs the response message. The response includes + /// the responder's ephemeral public key (in its ElligatorSwift-encoded form), the encrypted + /// static public key, and a signature noise message. Additionally, it establishes the session + /// ciphers for encrypting and decrypting further communication. /// - /// | Field name | Description | - /// | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | - /// | PUBKEY | Responder's plaintext ephemeral public key | - /// | PUBKEY | Responder's encrypted static public key | - /// | MAC | Message authentication code for responder's static public key | - /// | SIGNATURE_NOISE_MESSAGE | Signed message containing Responder's static key. Signature is issued by authority that is generally known to operate the server acting as the noise responder | - /// | MAC | Message authentication code for SIGNATURE_NOISE_MESSAGE | + /// On success, it returns a tuple containing the response message to be sent back to the + /// initiator and a [`NoiseCodec`] instance, which is configured with the session ciphers for + /// secure transmission of subsequent messages. /// - /// Message length: 234 bytes + /// On failure, the method returns an error if there is an issue during encryption, decryption, + /// or any other step of the handshake process. pub fn step_1( &mut self, elligatorswift_theirs_ephemeral_serialized: [u8; ELLSWIFT_ENCODING_SIZE], @@ -258,6 +322,12 @@ impl Responder { Ok((to_send, codec)) } + // Generates a signature noise message for the responder's certificate. + // + // This method creates a signature noise message that includes the protocol version, + // certificate validity period, and a cryptographic signature. The signature is created using + // the responder's static public key and authority keypair, ensuring that the responder's + // identity and certificate validity are cryptographically verifiable. fn get_signature(&self, version: u16, valid_from: u32, not_valid_after: u32) -> [u8; 74] { let mut ret = [0; 74]; let version = version.to_le_bytes(); @@ -277,6 +347,12 @@ impl Responder { ret } + // Securely erases sensitive data in the responder's memory. + // + // Clears all sensitive cryptographic material within the [`Responder`] to prevent any + // accidental leakage or misuse. It overwrites the stored keys, chaining key, handshake hash, + // and session ciphers with zeros. This function is typically + // called when the [`Responder`] instance is no longer needed or before deallocation. fn erase(&mut self) { if let Some(k) = self.k.as_mut() { for b in k { @@ -302,6 +378,8 @@ impl Responder { } impl Drop for Responder { + /// Ensures that sensitive data is securely erased when the [`Responder`] instance is dropped, + /// preventing any potential leakage of cryptographic material. fn drop(&mut self) { self.erase(); } From 443726ce818e18ccd5d529a716da4b8ab9d20afd Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:50:26 +0530 Subject: [PATCH 12/15] add readme to noise_sv2 crate --- protocols/v2/noise-sv2/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 protocols/v2/noise-sv2/README.md diff --git a/protocols/v2/noise-sv2/README.md b/protocols/v2/noise-sv2/README.md new file mode 100644 index 000000000..96b7576dc --- /dev/null +++ b/protocols/v2/noise-sv2/README.md @@ -0,0 +1,23 @@ + +# noise_sv2 + +[![crates.io](https://img.shields.io/crates/v/const_sv2.svg)](https://crates.io/crates/const_sv2) +[![docs.rs](https://docs.rs/const_sv2/badge.svg)](https://docs.rs/const_sv2) +[![rustc+](https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg)](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html) +[![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/stratum-mining/stratum/blob/main/LICENSE.md) +[![docs.rs](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg)](https://app.codecov.io/gh/stratum-mining/stratum/tree/main/protocols%2Fv2%2Fnoise-sv2) + +`noise_sv2` is primarily intended to secure communication in the Stratum V2 (Sv2) protocol. It handles the necessary Noise handshakes, encrypts outgoing messages, and decrypts incoming responses, ensuring privacy and integrity across the communication link between Sv2 roles. See the [Protocol Security specification](https://github.com/stratum-mining/sv2-spec/blob/main/04-Protocol-Security.md) for more details. + +## Key Capabilities +* **Secure Communication**: Provides encryption and authentication for messages exchanged between different Sv2 roles. +* **Cipher Support**: Includes support for both `AES-GCM` and `ChaCha20-Poly1305`. +* **Handshake Roles**: Implements the `Initiator` and `Responder` roles required by the Noise handshake, allowing both sides of a connection to establish secure communication. +* **Cryptographic Helpers**: Facilitates the management of cryptographic state and encryption operations. + +## Usage +To include this crate in your project, run: + +```bash +cargo add noise_sv2 +``` \ No newline at end of file From e03e33a2353823961cbddcf4bfd0a3d39d461132 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 18:50:48 +0530 Subject: [PATCH 13/15] add noise handshake example --- protocols/v2/noise-sv2/README.md | 10 ++- protocols/v2/noise-sv2/examples/handshake.rs | 68 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 protocols/v2/noise-sv2/examples/handshake.rs diff --git a/protocols/v2/noise-sv2/README.md b/protocols/v2/noise-sv2/README.md index 96b7576dc..85c91a34d 100644 --- a/protocols/v2/noise-sv2/README.md +++ b/protocols/v2/noise-sv2/README.md @@ -20,4 +20,12 @@ To include this crate in your project, run: ```bash cargo add noise_sv2 -``` \ No newline at end of file +``` + +### Examples + +This crate provides example on establishing a secure line: + +1. **[Noise Handshake Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/noise-sv2/examples/handshake.rs)**: + Establish a secure line of communication between an Initiator and Responder via the Noise + protocol, allowing for the encryption and decryption of a secret message. \ No newline at end of file diff --git a/protocols/v2/noise-sv2/examples/handshake.rs b/protocols/v2/noise-sv2/examples/handshake.rs new file mode 100644 index 000000000..d3d6a2264 --- /dev/null +++ b/protocols/v2/noise-sv2/examples/handshake.rs @@ -0,0 +1,68 @@ +// # Noise Protocol Handshake +// +// This example demonstrates how to use the `noise-sv2` crate to establish a Noise handshake +// between and initiator and responder, and encrypt and decrypt a secret message. It showcases how +// to: +// +// - Generate a cryptographic keypair using the `secp256k1` library. +// - Perform a Noise handshake between an initiator and responder. +// - Transition from handshake to secure communication mode. +// - Encrypt a message as the initiator role. +// - Decrypt the message as the responder role. +// +// ## Run +// +// ```sh +// cargo run --example handshake +// ``` + +use noise_sv2::{Initiator, Responder}; +use secp256k1::{Keypair, Parity, Secp256k1}; + +// Even parity used in the Schnorr signature process +const PARITY: Parity = Parity::Even; +// Validity duration of the responder's certificate, seconds +const RESPONDER_CERT_VALIDITY: u32 = 3600; + +// Generates a secp256k1 public/private key pair for the responder. +fn generate_key() -> Keypair { + let secp = Secp256k1::new(); + let (secret_key, _) = secp.generate_keypair(&mut rand::thread_rng()); + let kp = Keypair::from_secret_key(&secp, &secret_key); + if kp.x_only_public_key().1 == PARITY { + kp + } else { + generate_key() + } +} + +fn main() { + let mut secret_message = "Ciao, Mondo!".as_bytes().to_vec(); + + let responder_key_pair = generate_key(); + + let mut initiator = Initiator::new(Some(responder_key_pair.public_key().into())); + let mut responder = Responder::new(responder_key_pair, RESPONDER_CERT_VALIDITY); + + let first_message = initiator + .step_0() + .expect("Initiator failed first step of handshake"); + + let (second_message, mut responder_state) = responder + .step_1(first_message) + .expect("Responder failed second step of handshake"); + + let mut initiator_state = initiator + .step_2(second_message) + .expect("Initiator failed third step of handshake"); + + initiator_state + .encrypt(&mut secret_message) + .expect("Initiator failed to encrypt the secret message"); + assert!(secret_message != "Ciao, Mondo!".as_bytes().to_vec()); + + responder_state + .decrypt(&mut secret_message) + .expect("Responder failed to decrypt the secret message"); + assert!(secret_message == "Ciao, Mondo!".as_bytes().to_vec()); +} From a3b436979ddcd5d749150ff9ed8ba770ce044026 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Fri, 11 Oct 2024 22:22:29 +0530 Subject: [PATCH 14/15] add noise specific code coverage badge --- protocols/v2/noise-sv2/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocols/v2/noise-sv2/README.md b/protocols/v2/noise-sv2/README.md index 85c91a34d..481ee8a2f 100644 --- a/protocols/v2/noise-sv2/README.md +++ b/protocols/v2/noise-sv2/README.md @@ -5,7 +5,7 @@ [![docs.rs](https://docs.rs/const_sv2/badge.svg)](https://docs.rs/const_sv2) [![rustc+](https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg)](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html) [![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/stratum-mining/stratum/blob/main/LICENSE.md) -[![docs.rs](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg)](https://app.codecov.io/gh/stratum-mining/stratum/tree/main/protocols%2Fv2%2Fnoise-sv2) +[![codecov](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg?flag=noise_sv2-coverage)](https://codecov.io/gh/stratum-mining/stratum) `noise_sv2` is primarily intended to secure communication in the Stratum V2 (Sv2) protocol. It handles the necessary Noise handshakes, encrypts outgoing messages, and decrypts incoming responses, ensuring privacy and integrity across the communication link between Sv2 roles. See the [Protocol Security specification](https://github.com/stratum-mining/sv2-spec/blob/main/04-Protocol-Security.md) for more details. From ba3dbddc91444d74030b9d1f83c3bb64434144f8 Mon Sep 17 00:00:00 2001 From: bit-aloo Date: Mon, 14 Oct 2024 18:40:03 +0530 Subject: [PATCH 15/15] removed doc comments with general comments --- protocols/v2/noise-sv2/src/cipher_state.rs | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/protocols/v2/noise-sv2/src/cipher_state.rs b/protocols/v2/noise-sv2/src/cipher_state.rs index 3f0542f1e..dc324328f 100644 --- a/protocols/v2/noise-sv2/src/cipher_state.rs +++ b/protocols/v2/noise-sv2/src/cipher_state.rs @@ -39,17 +39,17 @@ use crate::aed_cipher::AeadCipher; use aes_gcm::Aes256Gcm; use chacha20poly1305::{aead::Buffer, ChaCha20Poly1305}; -/// The `CipherState` trait manages AEAD ciphers for secure communication, handling the encryption key, nonce, -/// and cipher instance. It supports encryption and decryption with ciphers like [`ChaCha20Poly1305`] and -/// [`Aes256Gcm`], ensuring proper key and nonce management. -/// -/// Key responsibilities: -/// - **Key management**: Set and retrieve the 32-byte encryption key. -/// - **Nonce management**: Track unique nonces for encryption operations. -/// - **Cipher handling**: Initialize and manage AEAD ciphers for secure data encryption. -/// -/// Used in protocols like Noise, `CipherState` ensures secure communication by managing cryptographic material -/// during and after handshakes. +// The `CipherState` trait manages AEAD ciphers for secure communication, handling the encryption key, nonce, +// and cipher instance. It supports encryption and decryption with ciphers like [`ChaCha20Poly1305`] and +// [`Aes256Gcm`], ensuring proper key and nonce management. +// +// Key responsibilities: +// - **Key management**: Set and retrieve the 32-byte encryption key. +// - **Nonce management**: Track unique nonces for encryption operations. +// - **Cipher handling**: Initialize and manage AEAD ciphers for secure data encryption. +// +// Used in protocols like Noise, `CipherState` ensures secure communication by managing cryptographic material +// during and after handshakes. pub trait CipherState where Self: Sized, @@ -162,15 +162,15 @@ where } } -/// The `GenericCipher` enum abstracts the use of two AEAD ciphers: [`ChaCha20Poly1305`] and [`Aes256Gcm`]. -/// It provides a unified interface for secure encryption and decryption, allowing flexibility in choosing -/// the cipher while ensuring consistent cryptographic operations. -/// -/// Variants: -/// - **ChaCha20Poly1305**: Uses the `ChaCha20Poly1305` cipher for encryption. -/// - **Aes256Gcm**: Uses the `Aes256Gcm` cipher for encryption. -/// -/// `GenericCipher` enables easy switching between ciphers while maintaining secure key and nonce management. +// The `GenericCipher` enum abstracts the use of two AEAD ciphers: [`ChaCha20Poly1305`] and [`Aes256Gcm`]. +// It provides a unified interface for secure encryption and decryption, allowing flexibility in choosing +// the cipher while ensuring consistent cryptographic operations. +// +// Variants: +// - **ChaCha20Poly1305**: Uses the `ChaCha20Poly1305` cipher for encryption. +// - **Aes256Gcm**: Uses the `Aes256Gcm` cipher for encryption. +// +// `GenericCipher` enables easy switching between ciphers while maintaining secure key and nonce management. #[allow(clippy::large_enum_variant)] pub enum GenericCipher { ChaCha20Poly1305(Cipher),