Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propose an implementation of noise_sv2 with optional no_std #1238

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
Comment on lines +22 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we keeping std for this crate, but we haven't done the same for the others in the previous PRs?

Copy link
Contributor Author

@Georges760 Georges760 Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to not break current API relying on std.

I basically added an intermediary API for no_std (so it is only a semver MINOR upgrade) and keep the std dependant API under the std feature.

For all other crates, there were no std dependencies than couldn't be replaced by their core/alloc equivalent, so they bacame trully no_std. For noise_sv2 two std deps (system time and random number generator) cannot be done by core/alloc so to not break the current API, an other have to be added where we provided them and not assume using the one from 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};

Georges760 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Georges760 marked this conversation as resolved.
Show resolved Hide resolved
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)
Georges760 marked this conversation as resolved.
Show resolved Hide resolved
}
}

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());
plebhash marked this conversation as resolved.
Show resolved Hide resolved

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 @@
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 {

Check warning on line 101 in protocols/v2/noise-sv2/src/initiator.rs

View check run for this annotation

Codecov / codecov/patch

protocols/v2/noise-sv2/src/initiator.rs#L101

Added line #L101 was not covered by tests
f.debug_struct("Initiator").finish()
}
}
Expand Down Expand Up @@ -164,14 +168,31 @@
/// 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>(
Georges760 marked this conversation as resolved.
Show resolved Hide resolved
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 @@
/// 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())

Check warning on line 213 in protocols/v2/noise-sv2/src/initiator.rs

View check run for this annotation

Codecov / codecov/patch

protocols/v2/noise-sv2/src/initiator.rs#L213

Added line #L213 was not covered by tests
}

/// 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>(

Check warning on line 225 in protocols/v2/noise-sv2/src/initiator.rs

View check run for this annotation

Codecov / codecov/patch

protocols/v2/noise-sv2/src/initiator.rs#L225

Added line #L225 was not covered by tests
Georges760 marked this conversation as resolved.
Show resolved Hide resolved
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))

Check warning on line 231 in protocols/v2/noise-sv2/src/initiator.rs

View check run for this annotation

Codecov / codecov/patch

protocols/v2/noise-sv2/src/initiator.rs#L231

Added line #L231 was not covered by tests
}

/// 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())

Check warning on line 241 in protocols/v2/noise-sv2/src/initiator.rs

View check run for this annotation

Codecov / codecov/patch

protocols/v2/noise-sv2/src/initiator.rs#L241

Added line #L241 was not covered by tests
}

/// 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> {
Georges760 marked this conversation as resolved.
Show resolved Hide resolved
Ok(Self::new_with_rng(None, rng))

Check warning on line 254 in protocols/v2/noise-sv2/src/initiator.rs

View check run for this annotation

Codecov / codecov/patch

protocols/v2/noise-sv2/src/initiator.rs#L253-L254

Added lines #L253 - L254 were not covered by tests
}

/// Executes the initial step of the Noise NX protocol handshake.
Expand Down Expand Up @@ -241,9 +293,29 @@
/// 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 @@
.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
Loading