From f10a931a304ea7ee8da570cc014fad40add19fb9 Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Tue, 13 Dec 2022 21:53:22 -0500 Subject: [PATCH] [noise] work on handshake init --- quicktap/src/lib.rs | 4 +- quicktap/src/noise/error.rs | 40 +++++++++ quicktap/src/noise/handshake.rs | 140 ++++++++++++++++++++++++++++++++ quicktap/src/noise/mod.rs | 4 + quicktap/src/qcrypto/hashes.rs | 8 ++ quicktap/src/qcrypto/pki.rs | 23 +++++- quicktap/src/qcrypto/tests.rs | 27 ++++-- 7 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 quicktap/src/noise/error.rs create mode 100644 quicktap/src/noise/handshake.rs create mode 100644 quicktap/src/noise/mod.rs diff --git a/quicktap/src/lib.rs b/quicktap/src/lib.rs index b91e455..7879fce 100644 --- a/quicktap/src/lib.rs +++ b/quicktap/src/lib.rs @@ -1,4 +1,5 @@ //! A simple, almost pure-rust, cross-platform `WireGuard` implementation. +//! Designed to function similarly to boringtun, this crate has modules for cross-platform device drivers, the Noise_IKpsk2 handshake, and cryptography constructs required for the above. #![warn(clippy::pedantic)] #![warn(clippy::nursery)] @@ -7,7 +8,8 @@ // This is an annoyance #![allow(clippy::must_use_candidate)] -pub mod drivers; // Baremetal network drivers for various platforms +pub mod drivers; pub mod qcrypto; +pub mod noise; pub use cidr; \ No newline at end of file diff --git a/quicktap/src/noise/error.rs b/quicktap/src/noise/error.rs new file mode 100644 index 0000000..0211fc4 --- /dev/null +++ b/quicktap/src/noise/error.rs @@ -0,0 +1,40 @@ +//! `Noise_IKpsk2` handshake errors +#![allow(clippy::module_name_repetitions)] + +use std::error::Error; +use std::fmt::{Display, Formatter}; + +/// Represents a error while doing Noise handshake operations +#[derive(Debug, Clone)] +pub enum NoiseError { + /// Represents an error while parsing a Noise packet + PacketParseError(NoisePacketParseError), + /// Represents an opaque error from ChaCha + ChaCha20Error(chacha20poly1305::Error) +} +impl Error for NoiseError {} +impl Display for NoiseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + Self::PacketParseError(err) => write!(f, "{}", err), + Self::ChaCha20Error(error) => write!(f, "Encryption error: {}", error) + } + } +} + +/// Represents and error while parsing a Noise packet +#[derive(Debug, Clone)] +pub enum NoisePacketParseError { + /// Represents an invalid packet length while parsing a packet. The first was the expected length, the second was the actual length + InvalidLength(usize, usize), + /// Represents that the packet being parsed is of the incorrect type. The first value is the expected packet type, the second was the actual packet type + WrongPacketType(usize, usize), +} +impl Display for NoisePacketParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + Self::InvalidLength(expected, got) => write!(f, "Invalid packet length: expected {} got {}", expected, got), + Self::WrongPacketType(expected, got) => write!(f, "Incorrect packet type: expected {} got {}", expected, got) + } + } +} \ No newline at end of file diff --git a/quicktap/src/noise/handshake.rs b/quicktap/src/noise/handshake.rs new file mode 100644 index 0000000..53bc964 --- /dev/null +++ b/quicktap/src/noise/handshake.rs @@ -0,0 +1,140 @@ +//! `Noise_IKpsk2` handshake packets + +use tai64::Tai64N; +use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; +use crate::noise::error::NoiseError; +use crate::qcrypto::aead::qcrypto_aead; +use crate::qcrypto::hashes::{qcrypto_hash_twice, qcrypto_mac}; +use crate::qcrypto::hkdf::qcrypto_hkdf; +use crate::qcrypto::pki::{qcrypto_dh_ephemeral, qcrypto_dh_generate_ephemeral, qcrypto_dh_longterm}; +use crate::qcrypto::{LABEL_MAC1, timestamp}; + +/// The Blake2s hash of the construction +pub const HANDSHAKE_INITIATOR_CHAIN_KEY: [u8; 32] = [ + 96, 226, 109, 174, 243, 39, 239, 192, 46, 195, 53, 226, 160, 37, 210, 208, 22, 235, 66, 6, 248, + 114, 119, 245, 45, 56, 209, 152, 139, 120, 205, 54, +]; + +/// The hashed chaining key +pub const HANDSHAKE_INITIATOR_CHAIN_KEY_HASH: [u8; 32] = [ + 34, 17, 179, 97, 8, 26, 197, 102, 105, 18, 67, 219, 69, 138, 213, 50, 45, 156, 108, 102, 34, + 147, 232, 183, 14, 225, 156, 101, 186, 7, 158, 243, +]; + +/// Represents a cookie we got from the other peer +pub struct Cookie { + time: Tai64N, + cookie: [u8; 16] +} + +/// Represents the internal handshake state. This does not really need to be messed with by outside users +#[allow(missing_docs)] +#[allow(clippy::module_name_repetitions)] +pub struct HandshakeState { + pub h_i: [u8; 32], + pub c_i: [u8; 32], + + pub e_pub_i: PublicKey, + + pub s_pub_i: PublicKey, + pub s_pub_r: PublicKey, + + pub e_priv_me: EphemeralSecret, + pub s_priv_me: StaticSecret, + + pub i_i: u32, + pub i_r: u32, + + pub cookies: Vec +} + +/// Generate a handshake initiator packet and encrypt it using the given session state, starting a new handshake state +/// # Errors +/// This function will error if encryption was unsuccessful +/// # Panics +/// While containing unwraps, this function will never panic. +#[allow(clippy::module_name_repetitions)] +pub fn handshake_init_to(session: &mut HandshakeState) -> Result<[u8; 148], NoiseError> { + let mut msg = HandshakeInitiatorRaw { + sender: session.i_i.to_le_bytes(), + ephemeral: [0u8; 32], + static_pub: [0u8; 32 + 16], + timestamp: [0u8; 12 + 16], + }; + + session.c_i = HANDSHAKE_INITIATOR_CHAIN_KEY; + session.h_i = HANDSHAKE_INITIATOR_CHAIN_KEY_HASH; + session.h_i = qcrypto_hash_twice(&session.h_i, session.s_pub_r.as_bytes()); + + let eph_keypair = qcrypto_dh_generate_ephemeral(); + + session.c_i = qcrypto_hkdf::<1>(&session.c_i, eph_keypair.1.as_bytes())[0]; + + msg.ephemeral = eph_keypair.1.to_bytes(); + + session.h_i = qcrypto_hash_twice(&session.h_i, &msg.ephemeral); + + let ci_k_pair = qcrypto_hkdf::<2>(&session.c_i, qcrypto_dh_ephemeral(eph_keypair.0, &session.s_pub_r).as_bytes()); + session.c_i = ci_k_pair[0]; + let k = ci_k_pair[1]; + + // This unwrap is safe because the output length is a known constant with these inputs + msg.static_pub = match qcrypto_aead(&k, 0, session.s_pub_i.as_bytes(), &session.h_i) { + Ok(s) => s, + Err(e) => return Err(NoiseError::ChaCha20Error(e)) + }.try_into().unwrap(); + + session.h_i = qcrypto_hash_twice(&session.h_i, &msg.static_pub); + + let ci_k_pair = qcrypto_hkdf::<2>(&session.c_i, qcrypto_dh_longterm(&session.s_priv_me, &session.s_pub_r).as_bytes()); + session.c_i = ci_k_pair[0]; + let k = ci_k_pair[1]; + + // This unwrap is safe because the output length is a known constant with these inputs + msg.timestamp = match qcrypto_aead(&k, 0, ×tamp().to_bytes(), &session.h_i) { + Ok(s) => s, + Err(e) => return Err(NoiseError::ChaCha20Error(e)) + }.try_into().unwrap(); + + session.h_i = qcrypto_hash_twice(&session.h_i, &msg.timestamp); + + Ok(msg.to_bytes(session)) +} + + +struct HandshakeInitiatorRaw { + sender: [u8; 4], + ephemeral: [u8; 32], + static_pub: [u8; 32 + 16], + timestamp: [u8; 12 + 16] +} +impl HandshakeInitiatorRaw { + fn to_bytes(&self, session: &HandshakeState) -> [u8; 148] { + let mut output = [0u8; 148]; + + output[0] = 1u8; + output[4..8].copy_from_slice(&self.sender); + output[8..40].copy_from_slice(&self.ephemeral); + output[40..88].copy_from_slice(&self.static_pub); + output[88..116].copy_from_slice(&self.timestamp); + + let mac1: [u8; 16] = qcrypto_mac(&qcrypto_hash_twice(LABEL_MAC1.as_bytes(), session.s_pub_i.as_bytes()), &output[..116]); + + output[116..132].copy_from_slice(&mac1); + + let mut mac2 = [0u8; 16]; + + let past_cookie_timeout = match session.cookies[session.cookies.len()-1].time.duration_since(×tamp()) { + Ok(t) => t.as_secs() >= 120, + Err(e) => true + }; + + if !session.cookies.is_empty() && !past_cookie_timeout { + mac2 = qcrypto_mac(&session.cookies[session.cookies.len()-1].cookie, &output[..132]); + } + + output[132..148].copy_from_slice(&mac2); + + output + } +} \ No newline at end of file diff --git a/quicktap/src/noise/mod.rs b/quicktap/src/noise/mod.rs new file mode 100644 index 0000000..ecac1d8 --- /dev/null +++ b/quicktap/src/noise/mod.rs @@ -0,0 +1,4 @@ +//! Contains structs and functions for serializing and deserializing different packets in the Noise_IKpsk2 handshake and data frames + +pub mod handshake; +pub mod error; \ No newline at end of file diff --git a/quicktap/src/qcrypto/hashes.rs b/quicktap/src/qcrypto/hashes.rs index f37d6f4..33e3289 100644 --- a/quicktap/src/qcrypto/hashes.rs +++ b/quicktap/src/qcrypto/hashes.rs @@ -13,6 +13,14 @@ pub fn qcrypto_hash(input: &[u8]) -> [u8; 32] { hasher.finalize().into() } +/// Given two varied length inputs, produce a 32-byte Blake2s hash digest +pub fn qcrypto_hash_twice(input: &[u8], input2: &[u8]) -> [u8; 32] { + let mut hasher = Blake2s256::new(); + Update::update(&mut hasher, input); + Update::update(&mut hasher, input2); + hasher.finalize().into() +} + /// Given a varied length MAC key and a varied length input, produce a 16-byte MAC digest using Blake2s /// # Panics /// This function will panic if the key is an incorrect size. diff --git a/quicktap/src/qcrypto/pki.rs b/quicktap/src/qcrypto/pki.rs index e01e2b4..c48a78e 100644 --- a/quicktap/src/qcrypto/pki.rs +++ b/quicktap/src/qcrypto/pki.rs @@ -1,18 +1,33 @@ //! Various public-key cryptography functions use rand::rngs::OsRng; -use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; +use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret, StaticSecret}; -type Keypair = (StaticSecret, PublicKey); +type LongtermKeypair = (StaticSecret, PublicKey); /// Generate a X25519 keypair -pub fn qcrypto_dh_generate() -> Keypair { +pub fn qcrypto_dh_generate_longterm() -> LongtermKeypair { let secret = StaticSecret::new(OsRng); let public = PublicKey::from(&secret); (secret, public) } /// Perform Elliptic-Curve Diffie-Hellman between a secret and public key to get a shared secret value -pub fn qcrypto_dh(secret: &StaticSecret, public: &PublicKey) -> SharedSecret { +pub fn qcrypto_dh_longterm(secret: &StaticSecret, public: &PublicKey) -> SharedSecret { + secret.diffie_hellman(public) +} + + +type EphemeralKeypair = (EphemeralSecret, PublicKey); + +/// Generate a X25519 keypair +pub fn qcrypto_dh_generate_ephemeral() -> EphemeralKeypair { + let secret = EphemeralSecret::new(OsRng); + let public = PublicKey::from(&secret); + (secret, public) +} + +/// Perform Elliptic-Curve Diffie-Hellman between a secret and public key to get a shared secret value +pub fn qcrypto_dh_ephemeral(secret: EphemeralSecret, public: &PublicKey) -> SharedSecret { secret.diffie_hellman(public) } \ No newline at end of file diff --git a/quicktap/src/qcrypto/tests.rs b/quicktap/src/qcrypto/tests.rs index dac8bbc..f45aa9a 100644 --- a/quicktap/src/qcrypto/tests.rs +++ b/quicktap/src/qcrypto/tests.rs @@ -1,17 +1,26 @@ use hex_lit::hex; use x25519_dalek::PublicKey; use crate::qcrypto::aead::{qcrypto_aead, qcrypto_aead_decrypt, qcrypto_xaead, qcrypto_xaead_decrypt}; -use crate::qcrypto::hashes::{qcrypto_hash, qcrypto_hmac, qcrypto_mac}; +use crate::qcrypto::{CONSTURCTION, IDENTIFIER}; +use crate::qcrypto::hashes::{qcrypto_hash, qcrypto_hash_twice, qcrypto_hmac, qcrypto_mac}; use crate::qcrypto::hkdf::qcrypto_hkdf; -use crate::qcrypto::pki::{qcrypto_dh, qcrypto_dh_generate}; +use crate::qcrypto::pki::{qcrypto_dh_longterm, qcrypto_dh_generate_longterm}; #[test] fn qcrypto_hash_test() { - assert_eq!(qcrypto_hash(&[0u8; 32]), hex!("320b5ea99e653bc2b593db4130d10a4efd3a0b4cc2e1a6672b678d71dfbd33ad")) + assert_eq!(qcrypto_hash(&[0u8; 32]), hex!("320b5ea99e653bc2b593db4130d10a4efd3a0b4cc2e1a6672b678d71dfbd33ad")); + assert_eq!(qcrypto_hash(CONSTURCTION.as_bytes()), [ + 96, 226, 109, 174, 243, 39, 239, 192, 46, 195, 53, 226, 160, 37, 210, 208, 22, 235, 66, 6, 248, + 114, 119, 245, 45, 56, 209, 152, 139, 120, 205, 54, + ]); + assert_eq!(qcrypto_hash_twice(&qcrypto_hash(CONSTURCTION.as_bytes()), IDENTIFIER.as_bytes()), [ + 34, 17, 179, 97, 8, 26, 197, 102, 105, 18, 67, 219, 69, 138, 213, 50, 45, 156, 108, 102, 34, + 147, 232, 183, 14, 225, 156, 101, 186, 7, 158, 243, + ]); } #[test] fn qcrypto_mac_test() { - assert_eq!(qcrypto_mac(&[0u8; 32], &[0u8; 32]), hex!("086de86cfb256a2bc40740062bcf4dcc")) + assert_eq!(qcrypto_mac(&[0u8; 32], &[0u8; 32]), hex!("086de86cfb256a2bc40740062bcf4dcc")); } #[test] fn qcrypto_hmac_test() { @@ -20,16 +29,16 @@ fn qcrypto_hmac_test() { #[test] fn qcrypto_pki_generate_test() { - let keypair = qcrypto_dh_generate(); + let keypair = qcrypto_dh_generate_longterm(); assert_eq!(keypair.1, PublicKey::from(&keypair.0)) } #[test] fn qcrypto_dh_test() { - let alice = qcrypto_dh_generate(); - let bob = qcrypto_dh_generate(); + let alice = qcrypto_dh_generate_longterm(); + let bob = qcrypto_dh_generate_longterm(); - let secret = qcrypto_dh(&alice.0, &bob.1); - let secret2 = qcrypto_dh(&bob.0, &alice.1); + let secret = qcrypto_dh_longterm(&alice.0, &bob.1); + let secret2 = qcrypto_dh_longterm(&bob.0, &alice.1); assert_eq!(secret.as_bytes(), secret2.as_bytes()) }