Skip to content

Commit

Permalink
Merge pull request #1238 from GPTechinno/noise_sv2_no_std
Browse files Browse the repository at this point in the history
Propose an implementation of `noise_sv2` with optional `no_std`
  • Loading branch information
plebhash authored Jan 10, 2025
2 parents a053dc7 + a474b57 commit 4ad657a
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 37 deletions.
17 changes: 14 additions & 3 deletions protocols/v2/noise-sv2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,24 @@ homepage = "https://stratumprotocol.org"
keywords = ["stratum", "mining", "bitcoin", "protocol"]

[dependencies]
secp256k1 = { version = "0.28.2", default-features = false, features =["hashes", "alloc","rand","rand-std"] }
rand = {version = "0.8.5", default-features = false, features = ["std","std_rng"] }
secp256k1 = { version = "0.28.2", default-features = false, features = ["hashes", "alloc", "rand"] }
rand = {version = "0.8.5", default-features = false }
aes-gcm = "0.10.2"
chacha20poly1305 = "0.10.1"
rand_chacha = "0.3.1"
rand_chacha = { version = "0.3.1", default-features = false }
const_sv2 = { version = "^3.0.0", path = "../../../protocols/v2/const-sv2"}

[features]
default = ["std"]
std = ["rand/std", "rand/std_rng", "rand_chacha/std", "secp256k1/rand-std"]

[dev-dependencies]
quickcheck = "1.0.3"
quickcheck_macros = "1"
rand = {version = "0.8.5", default-features = false, features = ["std", "std_rng"] }

[profile.dev]
panic = "unwind"

[profile.release]
panic = "abort"
6 changes: 6 additions & 0 deletions protocols/v2/noise-sv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ To include this crate in your project, run:
cargo add noise_sv2
```

This crate can be built with the following feature flags:

- `std`: Enable usage of rust `std` library, enabled by default.

In order to use this crate in a `#![no_std]` environment, use the `--no-default-features` to remove the `std` feature.

### Examples

This crate provides example on establishing a secure line:
Expand Down
33 changes: 33 additions & 0 deletions protocols/v2/noise-sv2/examples/handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,53 @@ fn main() {

let responder_key_pair = generate_key();

#[cfg(feature = "std")]
let mut initiator = Initiator::new(Some(responder_key_pair.public_key().into()));
#[cfg(not(feature = "std"))]
let mut initiator = Initiator::new_with_rng(
Some(responder_key_pair.public_key().into()),
&mut rand::thread_rng(),
);
#[cfg(feature = "std")]
let mut responder = Responder::new(responder_key_pair, RESPONDER_CERT_VALIDITY);
#[cfg(not(feature = "std"))]
let mut responder = Responder::new_with_rng(
responder_key_pair,
RESPONDER_CERT_VALIDITY,
&mut rand::thread_rng(),
);

let first_message = initiator
.step_0()
.expect("Initiator failed first step of handshake");

#[cfg(feature = "std")]
let (second_message, mut responder_state) = responder
.step_1(first_message)
.expect("Responder failed second step of handshake");
#[cfg(not(feature = "std"))]
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as u32;
#[cfg(not(feature = "std"))]
let (second_message, mut responder_state) = responder
.step_1_with_now_rng(first_message, now, &mut rand::thread_rng())
.expect("Responder failed second step of handshake");

#[cfg(feature = "std")]
let mut initiator_state = initiator
.step_2(second_message)
.expect("Initiator failed third step of handshake");
#[cfg(not(feature = "std"))]
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as u32;
#[cfg(not(feature = "std"))]
let mut initiator_state = initiator
.step_2_with_now(second_message, now)
.expect("Initiator failed third step of handshake");

initiator_state
.encrypt(&mut secret_message)
Expand Down
2 changes: 1 addition & 1 deletion protocols/v2/noise-sv2/src/cipher_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
// within the Noise protocol, ensuring secure data handling, key management, and nonce tracking
// throughout the communication session.

use std::ptr;
use core::ptr;

use crate::aed_cipher::AeadCipher;
use aes_gcm::Aes256Gcm;
Expand Down
2 changes: 2 additions & 0 deletions protocols/v2/noise-sv2/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// Defines error types and utilities for handling errors in the `noise_sv2` module.

use alloc::vec::Vec;

use aes_gcm::Error as AesGcm;

/// Noise protocol error handling.
Expand Down
32 changes: 29 additions & 3 deletions protocols/v2/noise-sv2/src/handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
// mandatory for communication across external networks (e.g., between a local mining proxy and a
// remote pool).

use alloc::{string::String, vec::Vec};

use crate::{aed_cipher::AeadCipher, cipher_state::CipherState, NOISE_HASHED_PROTOCOL_NAME_CHACHA};
use chacha20poly1305::ChaCha20Poly1305;
use secp256k1::{
Expand Down Expand Up @@ -99,14 +101,19 @@ pub trait HandshakeOp<Cipher: AeadCipher>: CipherState<Cipher> {
// 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.
#[cfg(feature = "std")]
fn generate_key() -> Keypair {
Self::generate_key_with_rng(&mut rand::thread_rng())
}
#[inline]
fn generate_key_with_rng<R: rand::Rng + ?Sized>(rng: &mut R) -> Keypair {
let secp = Secp256k1::new();
let (secret_key, _) = secp.generate_keypair(&mut rand::thread_rng());
let (secret_key, _) = secp.generate_keypair(rng);
let kp = Keypair::from_secret_key(&secp, &secret_key);
if kp.x_only_public_key().1 == crate::PARITY {
kp
} else {
Self::generate_key()
Self::generate_key_with_rng(rng)
}
}

Expand Down Expand Up @@ -273,10 +280,11 @@ pub trait HandshakeOp<Cipher: AeadCipher>: CipherState<Cipher> {
#[cfg(test)]
mod test {
use super::*;
use alloc::string::ToString;
use core::convert::TryInto;
use quickcheck::{Arbitrary, TestResult};
use quickcheck_macros;
use secp256k1::{ecdh::SharedSecret, SecretKey, XOnlyPublicKey};
use std::convert::TryInto;

struct TestHandShake {
k: Option<[u8; 32]>,
Expand Down Expand Up @@ -464,6 +472,7 @@ mod test {
}

#[test]
#[cfg(feature = "std")]
fn test_ecdh() {
let key_pair_1 = TestHandShake::generate_key();
let key_pair_2 = TestHandShake::generate_key();
Expand All @@ -480,6 +489,23 @@ mod test {
assert!(ecdh_1 == ecdh_2);
}

#[test]
fn test_ecdh_with_rng() {
let key_pair_1 = TestHandShake::generate_key_with_rng(&mut rand::thread_rng());
let key_pair_2 = TestHandShake::generate_key_with_rng(&mut rand::thread_rng());

let secret_1 = key_pair_1.secret_bytes();
let secret_2 = key_pair_2.secret_bytes();

let pub_1 = key_pair_1.x_only_public_key();
let pub_2 = key_pair_2.x_only_public_key();

let ecdh_1 = TestHandShake::ecdh(&secret_1, &pub_2.0.serialize());
let ecdh_2 = TestHandShake::ecdh(&secret_2, &pub_1.0.serialize());

assert!(ecdh_1 == ecdh_2);
}

#[derive(Clone, Debug)]
struct KeypairWrapper(pub Option<Keypair>);

Expand Down
86 changes: 79 additions & 7 deletions protocols/v2/noise-sv2/src/initiator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
// 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 alloc::{
boxed::Box,
string::{String, ToString},
};
use core::{convert::TryInto, ptr};

use crate::{
cipher_state::{Cipher, CipherState, GenericCipher},
Expand Down Expand Up @@ -93,8 +97,8 @@ pub struct Initiator {
c2: Option<GenericCipher>,
}

impl std::fmt::Debug for Initiator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Initiator {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Initiator").finish()
}
}
Expand Down Expand Up @@ -164,14 +168,31 @@ impl Initiator {
/// 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.
#[cfg(feature = "std")]
pub fn new(pk: Option<XOnlyPublicKey>) -> Box<Self> {
Self::new_with_rng(pk, &mut rand::thread_rng())
}

/// Creates a new [`Initiator`] instance with an optional responder public key and a custom
/// random number generator.
///
/// See [`Self::new`] for more details.
///
/// The custom random number generator is used to generate the ephemeral key pair. It should be
/// provided in order to not implicitely rely on `std` and allow `no_std` environments to
/// provide a hardware random number generator for example.
#[inline]
pub fn new_with_rng<R: rand::Rng + ?Sized>(
pk: Option<XOnlyPublicKey>,
rng: &mut R,
) -> Box<Self> {
let mut self_ = Self {
handshake_cipher: None,
k: None,
n: 0,
ck: [0; 32],
h: [0; 32],
e: Self::generate_key(),
e: Self::generate_key_with_rng(rng),
responder_authority_pk: pk,
c1: None,
c2: None,
Expand All @@ -187,19 +208,50 @@ impl Initiator {
/// valid [`XOnlyPublicKey`], an [`Error::InvalidRawPublicKey`] error is returned.
///
/// Typically used when the initiator is aware of the responder's public key in advance.
#[cfg(feature = "std")]
pub fn from_raw_k(key: [u8; 32]) -> Result<Box<Self>, Error> {
Self::from_raw_k_with_rng(key, &mut rand::thread_rng())
}

/// Creates a new [`Initiator`] instance using a raw 32-byte public key and a custom random
/// number generator.
///
/// See [`Self::from_raw_k`] for more details.
///
/// The custom random number generator should be provided in order to not implicitely rely on
/// `std` and allow `no_std` environments to provide a hardware random number generator for
/// example.
#[inline]
pub fn from_raw_k_with_rng<R: rand::Rng + ?Sized>(
key: [u8; 32],
rng: &mut R,
) -> Result<Box<Self>, Error> {
let pk =
secp256k1::XOnlyPublicKey::from_slice(&key).map_err(|_| Error::InvalidRawPublicKey)?;
Ok(Self::new(Some(pk)))
Ok(Self::new_with_rng(Some(pk), rng))
}

/// 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.
#[cfg(feature = "std")]
pub fn without_pk() -> Result<Box<Self>, Error> {
Ok(Self::new(None))
Self::without_pk_with_rng(&mut rand::thread_rng())
}

/// Creates a new [`Initiator`] instance without a responder's public key and using a custom
/// random number generator.
///
/// See [`Self::without_pk`] for more details.
///
/// The custom random number generator should be provided in order to not implicitely rely on
/// `std` and allow `no_std` environments to provide a hardware random number generator for
/// example.
#[inline]
pub fn without_pk_with_rng<R: rand::Rng + ?Sized>(rng: &mut R) -> Result<Box<Self>, Error> {
Ok(Self::new_with_rng(None, rng))
}

/// Executes the initial step of the Noise NX protocol handshake.
Expand Down Expand Up @@ -241,9 +293,29 @@ impl Initiator {
/// 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`].
#[cfg(feature = "std")]
pub fn step_2(
&mut self,
message: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE],
) -> Result<NoiseCodec, Error> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as u32;
self.step_2_with_now(message, now)
}

/// Processes the second step of the Noise NX protocol handshake for the initiator given the
/// current system time.
///
/// See [`Self::step_2`] for more details.
///
/// The current system time should be provided to avoid relying on `std` and allow `no_std`
/// environments to use another source of time.
pub fn step_2_with_now(
&mut self,
message: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE],
now: u32,
) -> Result<NoiseCodec, Error> {
// 2. interprets first 64 bytes as ElligatorSwift encoding of x-coordinate of public key
// from this is derived the 32-bytes remote ephemeral public key `re.public_key`
Expand Down Expand Up @@ -308,7 +380,7 @@ impl Initiator {
.0
.serialize();
let rs_pk_xonly = XOnlyPublicKey::from_slice(&rs_pub_key).unwrap();
if signature_message.verify(&rs_pk_xonly, &self.responder_authority_pk) {
if signature_message.verify_with_now(&rs_pk_xonly, &self.responder_authority_pk, now) {
let (temp_k1, temp_k2) = Self::hkdf_2(self.get_ck(), &[]);
let c1 = ChaCha20Poly1305::new(&temp_k1.into());
let c2 = ChaCha20Poly1305::new(&temp_k2.into());
Expand Down
9 changes: 7 additions & 2 deletions protocols/v2/noise-sv2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
//! 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.
#![cfg_attr(all(not(feature = "std"), not(test)), no_std)]

#[macro_use]
extern crate alloc;

use aes_gcm::aead::Buffer;
pub use aes_gcm::aead::Error as AeadError;
use cipher_state::GenericCipher;
Expand Down Expand Up @@ -66,8 +71,8 @@ pub struct NoiseCodec {
decryptor: GenericCipher,
}

impl std::fmt::Debug for NoiseCodec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for NoiseCodec {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("NoiseCodec").finish()
}
}
Expand Down
Loading

0 comments on commit 4ad657a

Please sign in to comment.