diff --git a/protocols/v2/noise-sv2/README.md b/protocols/v2/noise-sv2/README.md new file mode 100644 index 000000000..481ee8a2f --- /dev/null +++ b/protocols/v2/noise-sv2/README.md @@ -0,0 +1,31 @@ + +# 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) +[![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. + +## 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 +``` + +### 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()); +} 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], diff --git a/protocols/v2/noise-sv2/src/cipher_state.rs b/protocols/v2/noise-sv2/src/cipher_state.rs index b76f247c1..dc324328f 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 { 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, } 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 { 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 { 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) } 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(); } 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];