Skip to content

Commit

Permalink
ElligatorSwift encoding
Browse files Browse the repository at this point in the history
Use ElligatorSwift encoding for representations of public keys and the
related ECDH procedure

For serializing and deserilizing the messages sxchanges during the
handshake is made use of constants for lenghts of message chunks

change protocol name for encryption
New protocol name "Noise_NX_Secp256k1+EllSwift_ChaChaPoly_SHA256"

downstream_sv1::diff_management::test::test_converge_to_spm_from_low
downstream_sv1::diff_management::test::test_diff_management
fail non-deterministically

On top of https://github.com/GitGab19/stratum/tree/fix-empty-mempool
  • Loading branch information
lorbax committed Feb 12, 2024
1 parent 146c945 commit cfcb638
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 2,045 deletions.
2 changes: 0 additions & 2 deletions clippy-on-all-workspaces.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,3 @@ for workspace in $WORKSPACES; do
done

echo "Clippy success, all tests passed!"


3 changes: 3 additions & 0 deletions protocols/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions protocols/v2/codec-sv2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl State {

pub fn step_1(
&mut self,
re_pub: [u8; 32],
re_pub: [u8; const_sv2::RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE],
) -> core::result::Result<(HandShakeFrame, Self), Error> {
match self {
Self::HandShake(h) => match h {
Expand All @@ -72,7 +72,10 @@ impl State {
}
}

pub fn step_2(&mut self, message: [u8; 170]) -> core::result::Result<Self, Error> {
pub fn step_2(
&mut self,
message: [u8; const_sv2::INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE],
) -> core::result::Result<Self, Error> {
match self {
Self::HandShake(h) => match h {
HandshakeRole::Initiator(i) => {
Expand All @@ -97,10 +100,10 @@ impl State {
pub fn not_initialized(role: &HandshakeRole) -> Self {
match role {
HandshakeRole::Initiator(_) => {
Self::NotInitialized(const_sv2::INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_LENGTH)
Self::NotInitialized(const_sv2::INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE)
}
HandshakeRole::Responder(_) => {
Self::NotInitialized(const_sv2::RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_LENGTH)
Self::NotInitialized(const_sv2::RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions protocols/v2/const-sv2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repository = "https://github.com/stratum-mining/stratum"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
secp256k1 = { version = "0.28.2", default-features = false, features =["hashes", "alloc","rand","rand-std"] }

#[dev-dependencies]
#cbindgen = "0.16.0"
17 changes: 12 additions & 5 deletions protocols/v2/const-sv2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ pub const NOISE_FRAME_HEADER_SIZE: usize = 2;
pub const NOISE_FRAME_HEADER_LEN_OFFSET: usize = 0;
pub const NOISE_FRAME_MAX_SIZE: usize = u16::MAX as usize;

pub const INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_LENGTH: usize = 170;
pub const RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_LENGTH: usize = 32;
pub const ELLSWIFT_ENCODING_SIZE: usize = 64;
pub const RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE: usize = ELLSWIFT_ENCODING_SIZE;
pub const MAC: usize = 16;
pub const ENCRYPTED_ELLSWIFT_ENCODING_SIZE: usize = ELLSWIFT_ENCODING_SIZE + MAC;
pub const SIGNATURE_NOISE_MESSAGE_SIZE: usize = 74;
pub const ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE: usize = SIGNATURE_NOISE_MESSAGE_SIZE + MAC;
pub const INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE: usize = ELLSWIFT_ENCODING_SIZE
+ ENCRYPTED_ELLSWIFT_ENCODING_SIZE
+ ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE;

/// If protocolName is less than or equal to 32 bytes in length, use protocolName with zero bytes
/// appended to make 32 bytes. Otherwise, apply HASH to it. For name =
/// "Noise_NX_secp256k1_ChaChaPoly_SHA256", we need the hash.
/// "Noise_NX_Secp256k1+EllSwift_ChaChaPoly_SHA256", we need the hash.
pub const NOISE_HASHED_PROTOCOL_NAME_CHACHA: [u8; 32] = [
168, 246, 65, 106, 218, 197, 235, 205, 62, 183, 118, 131, 234, 247, 6, 174, 180, 164, 162, 125,
30, 121, 156, 182, 95, 117, 218, 138, 122, 135, 4, 65,
46, 180, 120, 129, 32, 142, 158, 238, 31, 102, 159, 103, 198, 110, 231, 14, 169, 234, 136, 9,
13, 80, 63, 232, 48, 220, 75, 200, 62, 41, 191, 16,
];

// len = 1
Expand Down
1 change: 1 addition & 0 deletions protocols/v2/noise-sv2/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub enum Error {
InvalidRawPublicKey,
InvalidRawPrivateKey,
ExpectedIncomingHandshakeMessage,
InvalidMessageLength,
}

impl From<AesGcm> for Error {
Expand Down
99 changes: 75 additions & 24 deletions protocols/v2/noise-sv2/src/initiator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ use crate::{
};
use aes_gcm::KeyInit;
use chacha20poly1305::ChaCha20Poly1305;
use secp256k1::{Keypair, XOnlyPublicKey};
use const_sv2::{
ELLSWIFT_ENCODING_SIZE, ENCRYPTED_ELLSWIFT_ENCODING_SIZE,
ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE, INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE,
SIGNATURE_NOISE_MESSAGE_SIZE,
};
use secp256k1::{
ellswift::{ElligatorSwift, ElligatorSwiftParty},
Keypair, PublicKey, XOnlyPublicKey,
};

pub struct Initiator {
handshake_cipher: Option<ChaCha20Poly1305>,
Expand Down Expand Up @@ -112,7 +120,7 @@ impl Initiator {
/// Initiator generates ephemeral keypair and sends the public key to the responder:
///
/// 1. initializes empty output buffer
/// 2. generates ephemeral keypair `e`, appends `e.public_key` to the buffer (32 bytes plaintext public key)
/// 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
Expand All @@ -123,24 +131,24 @@ impl Initiator {
/// | ---------- | -------------------------------- |
/// | PUBKEY | Initiator's ephemeral public key |
///
/// Message length: 32 bytes
pub fn step_0(&mut self) -> Result<[u8; 32], aes_gcm::Error> {
let serialized = self.e.public_key().x_only_public_key().0.serialize();
self.mix_hash(&serialized);
/// Message length: 64 bytes
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);
self.encrypt_and_hash(&mut vec![])?;

let mut message = [0u8; 32];
message[..32].copy_from_slice(&serialized[..32]);
let mut message = [0u8; ELLSWIFT_ENCODING_SIZE];
message[..64].copy_from_slice(&elliswift_enc_pubkey[..ELLSWIFT_ENCODING_SIZE]);
Ok(message)
}

/// #### 4.5.2.2 Initiator
///
/// 1. receives NX-handshake part 2 message
/// 2. interprets first 32 bytes as `re.public_key`
/// 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 48 bytes with `DecryptAndHash()` and stores the results as `rs.public_key` which is **server's static public key** (note that 32 bytes is the public key and 16 bytes is MAC)
/// 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:
Expand All @@ -151,29 +159,72 @@ impl Initiator {
///
///
///
pub fn step_2(&mut self, message: [u8; 170]) -> Result<NoiseCodec, Error> {
// 2. interprets first 32 bytes as `re.public_key`
// 3. calls `MixHash(re.public_key)`
let remote_pub_key = &message[0..32];
self.mix_hash(remote_pub_key);
pub fn step_2(
&mut self,
message: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE],
) -> 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`
let mut elliswift_theirs_ephemeral_serialized: [u8; ELLSWIFT_ENCODING_SIZE] =
[0; ELLSWIFT_ENCODING_SIZE];
elliswift_theirs_ephemeral_serialized.clone_from_slice(&message[0..ELLSWIFT_ENCODING_SIZE]);
self.mix_hash(&elliswift_theirs_ephemeral_serialized);

// 3. calls `MixHash(re.public_key)`
// 4. calls `MixKey(ECDH(e.private_key, re.public_key))`
let e_private_key = self.e.secret_bytes();
self.mix_key(&Self::ecdh(&e_private_key[..], remote_pub_key)[..]);

// 5. decrypts next 48 bytes with `DecryptAndHash()` and stores the results as `rs.public_key` which is **server's static public key** (note that 32 bytes is the public key and 16 bytes is MAC)
let mut to_decrypt = message[32..80].to_vec();
let e_private_key = self.e.secret_key();
let elligatorswift_ours_ephemeral = ElligatorSwift::from_pubkey(self.e.public_key());
let elligatorswift_theirs_ephemeral =
ElligatorSwift::from_array(elliswift_theirs_ephemeral_serialized);
let ecdh_ephemeral: [u8; 32] = ElligatorSwift::shared_secret(
elligatorswift_ours_ephemeral,
elligatorswift_theirs_ephemeral,
e_private_key,
ElligatorSwiftParty::A,
None,
)
.to_secret_bytes();
self.mix_key(&ecdh_ephemeral);

// 5. decrypts next 80 bytes with `DecryptAndHash()` and stores the results as
// `rs.public_key` which is **server's static public key** (note that 64 bytes is the
// elligatorswift encoded public key and 16 bytes is MAC)
let mut to_decrypt = message
[ELLSWIFT_ENCODING_SIZE..ELLSWIFT_ENCODING_SIZE + ENCRYPTED_ELLSWIFT_ENCODING_SIZE]
.to_vec();
self.decrypt_and_hash(&mut to_decrypt)?;
let rs_pub_key = to_decrypt;

// 6. calls `MixKey(ECDH(e.private_key, rs.public_key)`
self.mix_key(&Self::ecdh(&e_private_key[..], &rs_pub_key[..])[..]);
let elligatorswift_theirs_static_serialized: [u8; ELLSWIFT_ENCODING_SIZE] = to_decrypt[..]
.try_into()
.expect("slice with incorrect length");
let elligatorswift_theirs_static =
ElligatorSwift::from_array(elligatorswift_theirs_static_serialized);
let ecdh_static: [u8; 32] = ElligatorSwift::shared_secret(
elligatorswift_ours_ephemeral,
elligatorswift_theirs_static,
e_private_key,
ElligatorSwiftParty::A,
None,
)
.to_secret_bytes();
self.mix_key(&ecdh_static);

// Decrypt and verify the SignatureNoiseMessage
let mut to_decrypt = message[ELLSWIFT_ENCODING_SIZE + ENCRYPTED_ELLSWIFT_ENCODING_SIZE
..INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE]
.to_vec();
if to_decrypt.len() != ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE {
return Err(Error::InvalidMessageLength);
}

let mut to_decrypt = message[80..170].to_vec();
self.decrypt_and_hash(&mut to_decrypt)?;
let plaintext: [u8; 74] = to_decrypt.try_into().unwrap();
let plaintext: [u8; SIGNATURE_NOISE_MESSAGE_SIZE] = to_decrypt.try_into().unwrap();
let signature_message: SignatureNoiseMessage = plaintext.into();
let rs_pub_key = PublicKey::from_ellswift(elligatorswift_theirs_static)
.x_only_public_key()
.0
.serialize();
let rs_pk_xonly = XOnlyPublicKey::from_slice(&rs_pub_key).unwrap();
if signature_message.verify(&rs_pk_xonly) {
let (temp_k1, temp_k2) = Self::hkdf_2(self.get_ck(), &[]);
Expand Down
91 changes: 63 additions & 28 deletions protocols/v2/noise-sv2/src/responder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ use crate::{
};
use aes_gcm::KeyInit;
use chacha20poly1305::ChaCha20Poly1305;
use secp256k1::{Keypair, Secp256k1, SecretKey};
use const_sv2::{
ELLSWIFT_ENCODING_SIZE, ENCRYPTED_ELLSWIFT_ENCODING_SIZE,
ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE, INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE,
};
use secp256k1::{ellswift::ElligatorSwift, Keypair, Secp256k1, SecretKey};

const VERSION: u16 = 0;

Expand Down Expand Up @@ -122,20 +126,20 @@ impl Responder {

/// #### 4.5.1.2 Responder
///
/// 1. receives ephemeral public key message (32 bytes plaintext public key)
/// 2. parses received public key as `re.public_key`
/// 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)
///
/// #### 4.5.2.1 Responder
///
/// 1. initializes empty output buffer
/// 2. generates ephemeral keypair `e`, appends `e.public_key` to the buffer (32 bytes plaintext public key)
/// 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)` (32 bytes encrypted public key, 16 bytes MAC)
/// 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)` to the buffer
/// 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)`
Expand All @@ -153,36 +157,66 @@ impl Responder {
/// | 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 |
///
/// Message length: 170 bytes
pub fn step_1(&mut self, re_pub: [u8; 32]) -> Result<([u8; 170], NoiseCodec), aes_gcm::Error> {
/// Message length: 234 bytes
pub fn step_1(
&mut self,
elligatorswift_theirs_ephemeral_serialized: [u8; ELLSWIFT_ENCODING_SIZE],
) -> Result<([u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE], NoiseCodec), aes_gcm::Error> {
// 4.5.1.2 Responder
Self::mix_hash(self, &re_pub[..]);
Self::mix_hash(self, &elligatorswift_theirs_ephemeral_serialized[..]);
Self::decrypt_and_hash(self, &mut vec![])?;

// 4.5.2.1 Responder
let mut out = [0; 170];
let serialized = self.e.x_only_public_key().0.serialize();
out[..32].copy_from_slice(&serialized[..32]);
let mut out = [0; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE];
let keypair = self.e;
let elligatorswitf_ours_ephemeral = ElligatorSwift::from_pubkey(keypair.public_key());
let elligatorswift_ours_ephemeral_serialized = elligatorswitf_ours_ephemeral.to_array();
out[..ELLSWIFT_ENCODING_SIZE]
.copy_from_slice(&elligatorswift_ours_ephemeral_serialized[..ELLSWIFT_ENCODING_SIZE]);

// 3. calls `MixHash(e.public_key)`
Self::mix_hash(self, &serialized);
// what is here is not the public key encoded with ElligatorSwift, but the x-coordinate of
// the public key (which is a point in the EC).

Self::mix_hash(self, &elligatorswift_ours_ephemeral_serialized);

// 4. calls `MixKey(ECDH(e.private_key, re.public_key))`
let e_private_key = self.e.secret_bytes();
let ecdh = Self::ecdh(&e_private_key[..], &re_pub[..]);
Self::mix_key(self, &ecdh);
let e_private_key = keypair.secret_key();
let elligatorswift_theirs_ephemeral =
ElligatorSwift::from_array(elligatorswift_theirs_ephemeral_serialized);
let ecdh_ephemeral = ElligatorSwift::shared_secret(
elligatorswift_theirs_ephemeral,
elligatorswitf_ours_ephemeral,
e_private_key,
secp256k1::ellswift::ElligatorSwiftParty::B,
None,
)
.to_secret_bytes();
Self::mix_key(self, &ecdh_ephemeral);

// 5. appends `EncryptAndHash(s.public_key)` (32 bytes encrypted public key, 16 bytes MAC)
let mut encrypted_static_pub_k = vec![0; 32];
let static_pub_k = self.s.x_only_public_key().0.serialize();
encrypted_static_pub_k[..32].copy_from_slice(&static_pub_k[..32]);
// 5. appends `EncryptAndHash(s.public_key)` (64 bytes encrypted elligatorswift public key, 16 bytes MAC)
let mut encrypted_static_pub_k = vec![0; ELLSWIFT_ENCODING_SIZE];
let elligatorswift_ours_static = ElligatorSwift::from_pubkey(self.s.public_key());
let elligatorswift_ours_static_serialized: [u8; ELLSWIFT_ENCODING_SIZE] =
elligatorswift_ours_static.to_array();
encrypted_static_pub_k[..ELLSWIFT_ENCODING_SIZE]
.copy_from_slice(&elligatorswift_ours_static_serialized[0..ELLSWIFT_ENCODING_SIZE]);
self.encrypt_and_hash(&mut encrypted_static_pub_k)?;
out[32..(32 + 16 + 32)].copy_from_slice(&encrypted_static_pub_k[..(32 + 16)]);
out[ELLSWIFT_ENCODING_SIZE..(ELLSWIFT_ENCODING_SIZE + ENCRYPTED_ELLSWIFT_ENCODING_SIZE)]
.copy_from_slice(&encrypted_static_pub_k[..(ENCRYPTED_ELLSWIFT_ENCODING_SIZE)]);
// note: 64+16+64 = 144

// 6. calls `MixKey(ECDH(s.private_key, re.public_key))`
let s_private_key = self.s.secret_bytes();
let ecdh = Self::ecdh(&s_private_key[..], &re_pub[..]);
Self::mix_key(self, &ecdh[..]);
let s_private_key = self.s.secret_key();
let ecdh_static = ElligatorSwift::shared_secret(
elligatorswift_theirs_ephemeral,
elligatorswift_ours_static,
s_private_key,
secp256k1::ellswift::ElligatorSwiftParty::B,
None,
)
.to_secret_bytes();
Self::mix_key(self, &ecdh_static[..]);

// 7. appends `EncryptAndHash(SIGNATURE_NOISE_MESSAGE)` to the buffer
let valid_from = std::time::SystemTime::now()
Expand All @@ -192,12 +226,13 @@ impl Responder {
let not_valid_after = valid_from as u32 + self.cert_validity;
let signature_noise_message =
self.get_signature(VERSION, valid_from as u32, not_valid_after);
let mut signature_part = Vec::with_capacity(74 + 16);
let mut signature_part = Vec::with_capacity(ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE);
signature_part.extend_from_slice(&signature_noise_message[..]);
Self::encrypt_and_hash(self, &mut signature_part)?;
for i in (32 + 48)..(32 + 48 + 74 + 16) {
out[i] = signature_part[i - (32 + 48)];
}
let ephemeral_plus_static_encrypted_length =
ELLSWIFT_ENCODING_SIZE + ENCRYPTED_ELLSWIFT_ENCODING_SIZE;
out[ephemeral_plus_static_encrypted_length..(INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE)]
.copy_from_slice(&signature_part[..ENCRYPTED_SIGNATURE_NOISE_MESSAGE_SIZE]);

// 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:
let ck = Self::get_ck(self);
Expand Down
Loading

0 comments on commit cfcb638

Please sign in to comment.