diff --git a/Cargo.lock b/Cargo.lock index ce3566b2..95b2fe51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5783,8 +5783,8 @@ dependencies = [ "ts_capabilityversion", "ts_hexdump", "ts_keys", + "ts_noise", "zerocopy", - "zeroize", ] [[package]] @@ -6026,6 +6026,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "ts_noise" +version = "0.3.3" +dependencies = [ + "aead 0.5.2", + "blake2", + "chacha20poly1305", + "hex", + "hkdf 0.12.4", + "itertools", + "x25519-dalek 3.0.0-pre.6", + "zerocopy", + "zeroize", +] + [[package]] name = "ts_overlay_router" version = "0.3.3" @@ -6187,7 +6202,6 @@ dependencies = [ "chacha20poly1305", "clap", "hex", - "hkdf 0.12.4", "itertools", "proptest", "rand 0.10.1", @@ -6195,9 +6209,9 @@ dependencies = [ "tracing", "ts_cli_util", "ts_keys", + "ts_noise", "ts_packet", "ts_time", - "x25519-dalek 3.0.0-pre.6", "zerocopy", ] diff --git a/Cargo.toml b/Cargo.toml index cf369c19..248d9d15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "ts_netstack_smoltcp_core", "ts_netstack_smoltcp_socket", "ts_nodecapability", + "ts_noise", "ts_overlay_router", "ts_packet", "ts_packetfilter", @@ -99,9 +100,10 @@ tokio-util = { version = "0.7", default-features = false } tracing = { version = "0.1", default-features = false, features = ["attributes"] } tracing-test = "0.2" url = { version = "2", default-features = false } -x25519-dalek = { version = "3.0.0-pre.6", features = ["getrandom", "reusable_secrets", "static_secrets"] } +x25519-dalek = { version = "3.0.0-pre.6", features = ["getrandom", "reusable_secrets", "static_secrets", "zeroize"] } yoke = { version = "0.8", default-features = false } zerocopy = { version = "0.8", features = ["derive"] } +zeroize = { version = "1.8.2", features = ["zeroize_derive"] } # local workspace deps tailscale = { path = ".", version = "0.3.3" } @@ -125,6 +127,7 @@ ts_netstack_smoltcp = { path = "ts_netstack_smoltcp", version = "0.3.3" } ts_netstack_smoltcp_core = { path = "ts_netstack_smoltcp_core", version = "0.3.3" } ts_netstack_smoltcp_socket = { path = "ts_netstack_smoltcp_socket", version = "0.3.3" } ts_nodecapability = { path = "ts_nodecapability", version = "0.3.3" } +ts_noise = { path = "ts_noise", version = "0.3.3" } ts_overlay_router = { path = "ts_overlay_router", version = "0.3.3" } ts_packet = { path = "ts_packet", version = "0.3.3" } ts_packetfilter = { path = "ts_packetfilter", version = "0.3.3" } diff --git a/ts_control/src/control_dialer.rs b/ts_control/src/control_dialer.rs index 00d9f65e..dcccd267 100644 --- a/ts_control/src/control_dialer.rs +++ b/ts_control/src/control_dialer.rs @@ -255,7 +255,9 @@ where CapabilityVersion::CURRENT, ); - let conn = crate::tokio::upgrade_ts2021(url, &init_msg, handshake, h1_client).await?; + let conn = + crate::tokio::upgrade_ts2021(url, &init_msg, handshake, machine_keys.private, h1_client) + .await?; let conn = crate::tokio::read_challenge_packet(conn).await?; let h2_conn = ts_http_util::http2::connect(conn).await?; diff --git a/ts_control/src/tokio/connect.rs b/ts_control/src/tokio/connect.rs index 7643c5c8..b6d15877 100644 --- a/ts_control/src/tokio/connect.rs +++ b/ts_control/src/tokio/connect.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite}; use ts_capabilityversion::CapabilityVersion; use ts_http_util::{BytesBody, ClientExt, EmptyBody, HeaderName, HeaderValue, Http2, ResponseExt}; -use ts_keys::{MachineKeyPair, MachinePublicKey}; +use ts_keys::{MachineKeyPair, MachinePrivateKey, MachinePublicKey}; use url::Url; use zerocopy::network_endian::U32; @@ -166,7 +166,14 @@ pub async fn connect( CapabilityVersion::CURRENT, ); - let conn = upgrade_ts2021(control_url, &init_msg, handshake, h1_client).await?; + let conn = upgrade_ts2021( + control_url, + &init_msg, + handshake, + machine_keys.private, + h1_client, + ) + .await?; // The early payload (challenge packet) is optional. The server may send // the magic prefix [FF FF FF 'T' 'S'] followed by a JSON challenge, or it @@ -225,7 +232,8 @@ pub async fn fetch_control_key(control_url: &Url) -> Result, ) -> Result { let ts2021_url = control_url.join("/ts2021")?; @@ -251,7 +259,7 @@ pub async fn upgrade_ts2021( ConnectionError::Internal(InternalErrorKind::Http) })?; - let conn = handshake.complete(upgraded).await?; + let conn = handshake.complete(upgraded, &machine_private_key).await?; tracing::debug!("upgraded control connection from HTTP/1.1 to TS2021"); diff --git a/ts_control_noise/Cargo.toml b/ts_control_noise/Cargo.toml index bfe95eb9..f9ef08ea 100644 --- a/ts_control_noise/Cargo.toml +++ b/ts_control_noise/Cargo.toml @@ -15,6 +15,7 @@ rust-version.workspace = true ts_capabilityversion.workspace = true ts_hexdump.workspace = true ts_keys.workspace = true +ts_noise.workspace = true # Unconditionally required dependencies. base64.workspace = true @@ -30,7 +31,6 @@ tokio = { workspace = true, features = ["io-util"] } tokio-util = { workspace = true, features = ["codec"] } tracing.workspace = true zerocopy.workspace = true -zeroize = "1.8" [dev-dependencies] rand = "0.10" diff --git a/ts_control_noise/src/codec.rs b/ts_control_noise/src/codec.rs index 96955570..29bc99b5 100644 --- a/ts_control_noise/src/codec.rs +++ b/ts_control_noise/src/codec.rs @@ -1,35 +1,42 @@ -use std::io::ErrorKind; +use std::{io::ErrorKind, marker::PhantomData}; use bytes::{Buf, BufMut, BytesMut}; -use noise_protocol::{Cipher, CipherState}; +use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, Key, KeyInit, Nonce}; use tokio_util::codec::{Decoder, Encoder}; -use zerocopy::{IntoBytes, TryCastError, TryFromBytes, U16}; +use ts_noise::core::Session; +use zerocopy::{IntoBytes, TryCastError, TryFromBytes, network_endian::U16}; use crate::messages::{Header, MessageType}; /// The maximum wire size of a message to control over noise. pub const MAX_MESSAGE_SIZE: usize = 4096; +/// Overhead required by the AEAD's authentication tag. +const AEAD_OVERHEAD: usize = 16; + +/// Maximum size of a data chunk, without the per-message overhead. +const MAX_CHUNK_SIZE: usize = MAX_MESSAGE_SIZE - size_of::
() - AEAD_OVERHEAD; + +/// Marker type to indicate that a [`Codec`] instance can only be used for receiving, not sending. +pub struct Rx {} + +/// Marker type to indicate that a [`Codec`] instance can only be used for sending, not receiving. +pub struct Tx {} + /// Control noise codec that uses a different cipher state for the up and down directions. /// /// Just a wrapper containing two [`Codec`]s, one of which provides [`Encoder`] and the /// other [`Decoder`]. -pub struct BiCodec -where - Tx: Cipher, - Rx: Cipher, -{ +pub struct BiCodec { /// The transmit codec, used for encoding messages to control. pub tx: Codec, /// The receive codec, used for decoding messages from control. pub rx: Codec, } -impl Encoder for BiCodec +impl Encoder for BiCodec where B: AsRef<[u8]>, - Tx: Cipher, - Rx: Cipher, { type Error = as Encoder>::Error; @@ -38,11 +45,7 @@ where } } -impl Decoder for BiCodec -where - Tx: Cipher, - Rx: Cipher, -{ +impl Decoder for BiCodec { type Item = as Decoder>::Item; type Error = as Decoder>::Error; @@ -51,65 +54,93 @@ where } } +impl From for BiCodec { + fn from(session: Session) -> Self { + Self { + tx: Codec::::from(session.send), + rx: Codec::::from(session.send), + } + } +} + /// Codec supporting encrypting and decrypting data according to the control noise protocol /// using the specified cipher state. -pub struct Codec -where - C: Cipher, -{ - /// The cipher state to use to encode and decode message payloads. - pub cipher_state: CipherState, +/// +/// In accordance with Noise session semantics, a particular Codec instance can only be used for +/// sending or receiving, never both. The type parameter should be [`Tx`] for sending sessions and +/// [`Rx`] for receiving sessions. +pub struct Codec { + cipher: ChaCha20Poly1305, + next_nonce: u64, + _phantom: PhantomData, } -impl From> for Codec -where - C: Cipher, -{ - fn from(value: CipherState) -> Self { +impl Codec { + fn next_nonce(&mut self) -> Nonce { + assert_ne!(self.next_nonce, u64::MAX); + let mut ret = [0; 12]; + ret[4..].copy_from_slice(&self.next_nonce.to_be_bytes()); + self.next_nonce += 1; + ret.into() + } +} + +impl From for Codec { + fn from(key: Key) -> Self { + Codec { + cipher: ChaCha20Poly1305::new(&key), + next_nonce: 0, + _phantom: PhantomData, + } + } +} + +#[cfg(test)] +impl From<([u8; 32], u64)> for Codec { + fn from((key, nonce): ([u8; 32], u64)) -> Self { Codec { - cipher_state: value, + cipher: ChaCha20Poly1305::new(&key.into()), + next_nonce: nonce, + _phantom: PhantomData, } } } -impl Encoder for Codec +impl Encoder for Codec where - C: Cipher, B: AsRef<[u8]>, { type Error = std::io::Error; fn encode(&mut self, b: B, dst: &mut BytesMut) -> Result<(), Self::Error> { let b = b.as_ref(); - let max_data_chunk = MAX_MESSAGE_SIZE - (size_of::
() + C::tag_len()); - for chunk in b.chunks(max_data_chunk) { + for chunk in b.chunks(MAX_CHUNK_SIZE) { let hdr = Header { typ: MessageType::Record, - len: U16::new(chunk.len() as u16 + C::tag_len() as u16), + len: U16::new(chunk.len() as u16 + AEAD_OVERHEAD as u16), }; dst.put(hdr.as_bytes()); let data_start = dst.len(); - dst.put(chunk); - dst.put_bytes(0, C::tag_len()); - self.cipher_state - .encrypt_in_place(&mut dst[data_start..], chunk.len()); + let nonce = self.next_nonce(); + let tag = self + .cipher + .encrypt_in_place_detached(&nonce, &[], &mut dst[data_start..]) + .unwrap(); + dst.put(tag.as_ref()); } Ok(()) } } -impl Decoder for Codec -where - C: Cipher, -{ - type Error = std::io::Error; +impl Decoder for Codec { type Item = BytesMut; + type Error = std::io::Error; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { let (header, rest_len) = match Header::try_ref_from_prefix(src) { @@ -131,12 +162,15 @@ where match header.typ { MessageType::Record => { - match self.cipher_state.decrypt_in_place(&mut body, len) { - Ok(n) => body.truncate(n), - Err(()) => { - tracing::error!("decryption failed"); - return Err(ErrorKind::InvalidData.into()); - } + let nonce = self.next_nonce(); + let tag = body.split_off(body.len() - AEAD_OVERHEAD); + if self + .cipher + .decrypt_in_place_detached(&nonce, &[], body.as_mut(), tag.as_ref().into()) + .is_err() + { + tracing::error!("decryption failed"); + return Err(ErrorKind::InvalidData.into()); } Ok(Some(body)) @@ -173,21 +207,11 @@ mod test { type Cipher = crate::ChaCha20Poly1305BigEndian; - fn init_codec_pair(key: [u8; 32], nonce: u64) -> (Codec, Codec) { - let encrypt_state = CipherState::::new(&key, nonce); - let decrypt_state = encrypt_state.clone(); - - ( - Codec { - cipher_state: encrypt_state, - }, - Codec { - cipher_state: decrypt_state, - }, - ) + fn init_codec_pair(key: [u8; 32], nonce: u64) -> (Codec, Codec) { + ((key, nonce).into(), (key, nonce).into()) } - fn rand_codec_pair() -> (Codec, Codec) { + fn rand_codec_pair() -> (Codec, Codec) { init_codec_pair(rand::random(), rand::random()) } diff --git a/ts_control_noise/src/handshake.rs b/ts_control_noise/src/handshake.rs index ed22449f..3bf04e6d 100644 --- a/ts_control_noise/src/handshake.rs +++ b/ts_control_noise/src/handshake.rs @@ -1,29 +1,25 @@ use base64::{Engine, engine::general_purpose::STANDARD}; use bytes::BytesMut; -use noise_protocol::{HandshakeState, HandshakeStateBuilder, patterns::noise_ik}; -use noise_rust_crypto::{Blake2s, X25519, sensitive::Sensitive}; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio_util::codec::Framed; use ts_hexdump::{AsHexExt, Case}; use ts_keys::{MachinePrivateKey, MachinePublicKey}; +use ts_noise::ik::SentHandshake; use zerocopy::{IntoBytes, TryFromBytes}; -use zeroize::Zeroizing; use crate::{ - ChaCha20Poly1305BigEndian, Error, + Error, codec::BiCodec, framed_io::FramedIo, messages::{Header, Initiation, MessageType}, }; -type Cipher = ChaCha20Poly1305BigEndian; -type Codec = BiCodec; -type NoiseFramed = Framed; +type NoiseFramed = Framed; type WrappedIo = FramedIo, BytesMut>; /// Noise handshake state. pub struct Handshake { - state: HandshakeState, + state: SentHandshake, } impl Handshake { @@ -39,31 +35,27 @@ impl Handshake { control_public_key: &MachinePublicKey, capability_version: ts_capabilityversion::CapabilityVersion, ) -> (Self, String) { - let key = Sensitive::from(Zeroizing::from(node_machine_private_key.to_bytes())); - - let mut builder = HandshakeStateBuilder::new(); - builder.set_pattern(noise_ik()); - builder.set_is_initiator(true); - builder.set_rs(control_public_key.to_bytes()); - builder.set_prologue(prologue.as_bytes()); - builder.set_s(key); - - let mut state = builder.build_handshake_state(); - - let overhead = state.get_next_message_overhead(); - let mut ciphertext = [0u8; Initiation::PAYLOAD_LEN]; - state - .write_message(&[], &mut ciphertext) - .expect("initiation payload size too small"); - let init_msg = Initiation::new(capability_version.into(), overhead as u16, ciphertext); + let mut ciphertext = [0; SentHandshake::INIT_SIZE]; + let state = SentHandshake::new( + node_machine_private_key.into(), + control_public_key.into(), + prologue.as_bytes(), + &mut ciphertext, + ); + let init_msg = Initiation::new( + capability_version.into(), + SentHandshake::INIT_SIZE as u16, + ciphertext, + ); (Self { state }, STANDARD.encode(init_msg.as_bytes())) } /// Complete the handshake by reading the control server's response. pub async fn complete( - &mut self, + mut self, mut conn: T, + node_machine_private_key: &MachinePrivateKey, ) -> Result, Error> { let mut hdr_bytes = [0u8; 3]; conn.read_exact(&mut hdr_bytes[..]).await?; @@ -86,18 +78,22 @@ impl Handshake { return Err(Error::BadFormat); } - let data = self.state.read_message_vec(&packet)?; - if !data.is_empty() || !self.state.completed() { - return Err(Error::HandshakeFailed); - } - - let (tx, rx) = self.state.get_ciphers(); + let session = match self + .state + .try_finish(&mut packet, node_machine_private_key.into()) + { + Ok(session) => session, + Err(state) => { + self.state = state; + return Err(Error::HandshakeFailed); + } + }; Ok(FramedIo::new(Framed::new( conn, BiCodec { - tx: tx.into(), - rx: rx.into(), + tx: session.send.into(), + rx: session.recv.into(), }, ))) } diff --git a/ts_control_noise/src/lib.rs b/ts_control_noise/src/lib.rs index 42feec95..4947a39e 100644 --- a/ts_control_noise/src/lib.rs +++ b/ts_control_noise/src/lib.rs @@ -8,7 +8,7 @@ mod handshake; mod messages; pub use cipher::ChaCha20Poly1305BigEndian; -pub use codec::{BiCodec, Codec, MAX_MESSAGE_SIZE}; +pub use codec::{BiCodec, Codec, MAX_MESSAGE_SIZE, Rx, Tx}; pub use error::Error; pub use handshake::Handshake; pub use messages::{Header, Initiation, MessageType}; diff --git a/ts_noise/Cargo.toml b/ts_noise/Cargo.toml new file mode 100644 index 00000000..df2b2176 --- /dev/null +++ b/ts_noise/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ts_noise" +description = "Noise protocol implementations for tailscale" +categories = ["cryptography"] +keywords = ["tailscale", "noise"] + +edition.workspace = true +license.workspace = true +publish.workspace = true +version.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +aead.workspace = true +blake2.workspace = true +chacha20poly1305.workspace = true +hkdf.workspace = true +itertools.workspace = true +x25519-dalek.workspace = true +zerocopy.workspace = true +zeroize.workspace = true + +[dev-dependencies] +hex.workspace = true + +[lints] +workspace = true diff --git a/ts_noise/README.md b/ts_noise/README.md new file mode 100644 index 00000000..61c7cf48 --- /dev/null +++ b/ts_noise/README.md @@ -0,0 +1,23 @@ +# ts_noise + +Noise protocol implementations for tailscale. + +This is not a general purpose Noise protocol library. It only implements the two specific +handshake patterns that Tailscale requires (Noise IK for the control protocol, and Noise IKpsk2 +for wireguard). + +## Architecture + +The `core` module provides handshake state types that expose the primitive operations that Noise +handshake patterns are built from. It tries to limit some incorrect combinations of the primitives +through the type system. For example, performing an encryption/decryption of part of the handshake +is represented as a separate type that can only be obtained by first performing a handshake step that +derives a single-use AEAD key. + +the `ik` and `ikpsk2` modules build on `core` and provide strongly typed implementations of the +corresponding handshake patterns. + +This crate's responsibility ends when a handshake successfully concludes and produces a pair of +session keys. It's up to the caller to use those keys appropriately for the duration of the session, +in accordance with the upper level protocol being implemented. See the `ts_control_noise` and +`ts_tunnel` crates for the two uses in this codebase. \ No newline at end of file diff --git a/ts_noise/src/core.rs b/ts_noise/src/core.rs new file mode 100644 index 00000000..1114e144 --- /dev/null +++ b/ts_noise/src/core.rs @@ -0,0 +1,243 @@ +//! Supporting machinery for executing Noise handshakes. +//! +//! This is not a general-purpose Noise protocol library. The provided functionality is +//! sufficient to execute the two protocols that Tailscale cares about (IK and IKpsk2). +//! +//! This module only provides the primitive operations found in Noise handshakes. It is +//! the caller's responsibility to chain the primitives together correctly to produce the +//! desired handshake pattern. +//! +//! This module uses typestates to make some invalid sequences of operations compile-time +//! errors, in an effort to make protocol construction errors more obvious. This does not +//! cover all possible invalid sequences, it is still the caller's responsibility to ensure +//! that their sequence correctly reflects the desired handshake pattern. + +use aead::AeadInPlace; +use blake2::{Blake2s256, Digest}; +use chacha20poly1305::{ChaCha20Poly1305, KeyInit}; +use hkdf::SimpleHkdf; +use zerocopy::IntoBytes; +use zeroize::ZeroizeOnDrop; + +/// Initialize a ChaCha20Poly1305 cipher with the given key. +fn must_cipher(key: [u8; 32]) -> ChaCha20Poly1305 { + ChaCha20Poly1305::new(&key.into()) +} + +/// Use HKDF to derive two 32-byte values. +fn must_hkdf2(chaining_key: &[u8; 32], key: &[u8]) -> ([u8; 32], [u8; 32]) { + let kdf = SimpleHkdf::::new(Some(chaining_key), key); + let mut expanded = [0; 64]; + // Expansion only fails if you request more bytes than the KDF can provide. This KDF can always + // provide 64 bytes. + kdf.expand(&[], &mut expanded).unwrap(); + ( + expanded[..32].try_into().unwrap(), + expanded[32..].try_into().unwrap(), + ) +} + +/// Use HKDF to derive three 32-byte values. +fn must_hkdf3(chaining_key: &[u8; 32], key: &[u8]) -> ([u8; 32], [u8; 32], [u8; 32]) { + let kdf = SimpleHkdf::::new(Some(chaining_key), key); + let mut expanded = [0; 96]; + // Expansion only fails if you request more bytes than the KDF can provide. This KDF can always + // provide 96 bytes. + kdf.expand(&[], &mut expanded).unwrap(); + ( + expanded[..32].try_into().unwrap(), + expanded[32..64].try_into().unwrap(), + expanded[64..].try_into().unwrap(), + ) +} + +/// A symmetric session. +pub struct Session { + /// The key to send data. + pub send: chacha20poly1305::Key, + /// The key to receive data. + pub recv: chacha20poly1305::Key, +} + +/// Base Noise handshake state. +#[derive(Clone, ZeroizeOnDrop)] +pub struct State { + hash: [u8; 32], + chaining_key: [u8; 32], +} + +impl State { + /// Initialize a new Noise handshake. + /// + /// `protocol_name` is the Noise protocol name as specified + /// in . + pub fn new(protocol_name: &[u8]) -> State { + let init = Blake2s256::digest(protocol_name); + State { + hash: init.into(), + chaining_key: init.into(), + } + } + + /// Mix data into the handshake state. + /// + /// This is the MixHash() operation in the Noise spec. + #[inline] + pub fn mix_hash(self, data: &[u8]) -> Self { + self.mix_hash_gather(&[data]) + } + + /// Like mix_hash, but the data can be provided in multiple non-contiguous pieces. + pub fn mix_hash_gather(mut self, data: &[&[u8]]) -> Self { + let mut h = Blake2s256::new_with_prefix(self.hash); + for d in data { + h.update(d); + } + h.finalize_into(self.hash.as_mut_bytes().into()); + self + } + + /// Mix a public key into the handshake state. + /// + /// This should only be used to mix in the ephemeral public key in psk handshake variants, + /// in accordance with sections 9.2 and 9.3 of the Noise spec. Use [`State::mix_dh`] if + /// you're looking to MixKey the result of an X25519 operation. + /// + /// This is the MixHash(ephemeral)+MixKey(ephemeral) operations in the Noise spec. + pub fn mix_hash_and_key(mut self, public: &x25519_dalek::PublicKey) -> State { + self = self.mix_hash(public.as_ref()); + let (ck, _) = must_hkdf2(&self.chaining_key, public.as_ref()); + State { + hash: self.hash, + chaining_key: ck, + } + } + + /// Perform an X25519 operation, and mix the result into the handshake state. + /// + /// This is the MixKey(DH(private, public)) operation in the Noise spec. + pub fn mix_dh( + self, + private: &x25519_dalek::StaticSecret, + public: &x25519_dalek::PublicKey, + ) -> StateWithAead { + let shared = private.diffie_hellman(public); + let (ck, k) = must_hkdf2(&self.chaining_key, shared.as_ref()); + StateWithAead { + state: State { + hash: self.hash, + chaining_key: ck, + }, + aead: must_cipher(k), + } + } + + /// Finalize the handshake as the initiator role. + /// + /// This is the Split() operation in the Noise spec. + pub fn finish_as_initiator(self) -> Session { + let (send, recv) = must_hkdf2(&self.chaining_key, &[]); + + Session { + send: send.into(), + recv: recv.into(), + } + } + + /// Finalize the handshake as the responder role. + /// + /// This is the Split() operation in the Noise spec. + pub fn finish_as_responder(self) -> Session { + let (recv, send) = must_hkdf2(&self.chaining_key, &[]); + + Session { + send: send.into(), + recv: recv.into(), + } + } +} + +/// A pre-shared symmetric key. +pub type Psk = [u8; 32]; + +/// The authentication tag of an AEAD ciphertext. +pub type AeadTag = [u8; 16]; + +/// Noise handshake state when AEAD operations are available. +/// +/// For the supported handshake patterns, when the handshake is in this state there are +/// only two valid ways to continue: +/// +/// - Perform an AEAD operation ([`StateWithAead::seal`] or [`StateWithAead::open`]), which +/// consumes the AEAD and returns a plain [`State`]. +/// - Mix additional key material into the handshake ([`StateWithAead::mix_dh`] or +/// [`StateWithAead::mix_psk`], which returns an updated [`StateWithAead`]. +pub struct StateWithAead { + state: State, + aead: ChaCha20Poly1305, +} + +impl StateWithAead { + /// Perform an X25519 operation, and mix the result into the handshake state. + /// + /// This is the MixKey(DH(private, public)) operation in the Noise spec. + pub fn mix_dh( + self, + private: &x25519_dalek::StaticSecret, + public: &x25519_dalek::PublicKey, + ) -> StateWithAead { + self.state.mix_dh(private, public) + } + + /// Mix a pre-shared symmetric key into the handshake state. + /// + /// This is the MixKeyAndHash() operation in the Noise spec. + pub fn mix_psk(self, psk: &Psk) -> StateWithAead { + let (ck, h, k) = must_hkdf3(&self.state.chaining_key, psk); + StateWithAead { + state: State { + hash: self.state.hash, + chaining_key: ck, + } + .mix_hash(&h), + aead: must_cipher(k), + } + } + + /// Seal `cleartext` in place. + /// + /// `cleartext` is overwritten with ciphertext, and the authentication tag is written to `tag`. + pub fn seal(self, cleartext: &mut [u8], tag: &mut AeadTag) -> State { + let nonce = [0; 12]; + let res = self + .aead + .encrypt_in_place_detached(&nonce.into(), &self.state.hash, cleartext) + .unwrap(); + res.write_to(tag).unwrap(); + + self.state.mix_hash_gather(&[cleartext, tag]) + } + + /// Decrypt `ciphertext` in place, authenticating with `tag`. + /// + /// If successful, `ciphertext` is overwritten with cleartext. Returns `None` if decryption + /// fails, implicitly terminating the handshake. + /// + /// This is the DecryptAndHash() operation in the Noise spec. + pub fn open(self, ciphertext: &mut [u8], tag: &AeadTag) -> Option { + // On successful decryption, we have to mix the ciphertext into the handshake state. + // This pairs awkwardly with the in place crypto we're doing, where a successful decryption + // overwrites the ciphertext. + // Instead, update the handshake hash before the decryption attempt. This is okay because + // we discard the handshake state entirely on decryption failure. + let hash = self.state.hash; + let state = self.state.mix_hash_gather(&[ciphertext, tag]); + + let nonce = [0; 12]; + self.aead + .decrypt_in_place_detached(&nonce.into(), &hash, ciphertext, tag.into()) + .ok()?; + + Some(state) + } +} diff --git a/ts_noise/src/ik.rs b/ts_noise/src/ik.rs new file mode 100644 index 00000000..cb1075b8 --- /dev/null +++ b/ts_noise/src/ik.rs @@ -0,0 +1,225 @@ +//! Implementation of the Noise IK handshake pattern. + +use zerocopy::FromBytes; + +use crate::{ + core::{Session, State}, + messages::{Init, Resp}, +}; + +const PROTOCOL: &[u8] = b"Noise_IK_25519_ChaChaPoly_BLAKE2s"; + +/// A partially completed handshake, where the peer is the handshake's initiator. +pub struct ReceivedHandshake { + state: State, + peer_ephemeral_pub: x25519_dalek::PublicKey, + /// The peer's static identity. + pub peer_static_pub: x25519_dalek::PublicKey, +} + +impl ReceivedHandshake { + /// Size of the packet expected by [`ReceivedHandshake::finish`]. + pub const RESP_SIZE: usize = size_of::(); + + /// Process an incoming handshake initiation packet. + /// + /// Returns a [`ReceivedHandshake`] with information about the peer on success, or + /// None if the handshake message is invalid in some way. + pub fn new( + packet: &mut [u8], + prologue: &[u8], + my_static: &x25519_dalek::StaticSecret, + ) -> Option { + let packet: &mut Init<()> = Init::mut_from_bytes(packet).ok()?; + + let peer_ephemeral_pub = x25519_dalek::PublicKey::from(packet.ephemeral_pub); + let my_static_pub = x25519_dalek::PublicKey::from(my_static); + + let handshake = State::new(PROTOCOL) + .mix_hash(prologue) // prologue + .mix_hash(my_static_pub.as_ref()) // <- s ... + .mix_hash(&packet.ephemeral_pub) // -> e + .mix_dh(my_static, &peer_ephemeral_pub) // es + .open(&mut packet.static_pub, &packet.static_pub_tag)? // s + .mix_dh(my_static, &packet.static_pub.into()) // ss + .open(&mut [], &packet.payload_tag)?; // payload + + Some(ReceivedHandshake { + state: handshake, + peer_ephemeral_pub, + peer_static_pub: packet.static_pub.into(), + }) + } + + /// Finalize the handshake and generate a response. + /// + /// The response is written to `packet`, which must be exactly + /// [`ReceivedHandshake::RESP_SIZE`] bytes. + /// + /// # Panics + /// + /// If `packet` is the wrong size. + #[inline] + pub fn finish(self, packet: &mut [u8]) -> Session { + let my_ephemeral = x25519_dalek::StaticSecret::random(); + self.finish_with_ephemeral(packet, my_ephemeral) + } + + fn finish_with_ephemeral( + self, + packet: &mut [u8], + my_ephemeral: x25519_dalek::StaticSecret, + ) -> Session { + assert_eq!(packet.len(), Self::RESP_SIZE); + let response = Resp::mut_from_bytes(packet).unwrap(); + + let my_ephemeral_pub = x25519_dalek::PublicKey::from(&my_ephemeral); + response.ephemeral_pub = my_ephemeral_pub.to_bytes(); + + self.state + .mix_hash(my_ephemeral_pub.as_ref()) // <- e + .mix_dh(&my_ephemeral, &self.peer_ephemeral_pub) // ee + .mix_dh(&my_ephemeral, &self.peer_static_pub) // se + .seal(&mut [], &mut response.auth_tag) // payload + .finish_as_responder() + } +} + +/// A partially completed handshake, where the peer is the handshake's responder. +pub struct SentHandshake { + state: State, + my_ephemeral: x25519_dalek::StaticSecret, +} + +impl SentHandshake { + /// Size of the output packet to be provided to [`SentHandshake::new`]. + pub const INIT_SIZE: usize = size_of::>(); + + /// Generate an outgoing handshake initiation for the given peer identity. + /// + /// # Panics + /// + /// If `packet` is not [`SentHandshake::INIT_SIZE`] bytes. + #[inline] + pub fn new( + my_static: x25519_dalek::StaticSecret, + peer_static: x25519_dalek::PublicKey, + prologue: &[u8], + out: &mut [u8], + ) -> Self { + let ephemeral = x25519_dalek::StaticSecret::random(); + SentHandshake::new_with_ephemeral(my_static, ephemeral, peer_static, prologue, out) + } + + fn new_with_ephemeral( + my_static: x25519_dalek::StaticSecret, + my_ephemeral: x25519_dalek::StaticSecret, + peer_static: x25519_dalek::PublicKey, + prologue: &[u8], + out: &mut [u8], + ) -> Self { + assert_eq!(out.len(), Self::INIT_SIZE); + let out: &mut Init<()> = Init::mut_from_bytes(out).unwrap(); + + out.ephemeral_pub = x25519_dalek::PublicKey::from(&my_ephemeral).to_bytes(); + out.static_pub = x25519_dalek::PublicKey::from(&my_static).to_bytes(); + + let state = State::new(PROTOCOL) + .mix_hash(prologue) // prologue + .mix_hash(peer_static.as_ref()) // <- s + .mix_hash(&out.ephemeral_pub) // -> e + .mix_dh(&my_ephemeral, &peer_static) // es + .seal(&mut out.static_pub, &mut out.static_pub_tag) // s + .mix_dh(&my_static, &peer_static) // ss + .seal(&mut [], &mut out.payload_tag); // payload + + SentHandshake { + state, + my_ephemeral, + } + } + + /// Try to finalize the handshake and generate a response. + /// + /// If successful, consumes `self` and returns keys for the new session. + /// If the response is invalid, returns `Err(self)` to allow for another finalization + /// attempt later. + pub fn try_finish( + self, + packet: &mut [u8], + my_static: x25519_dalek::StaticSecret, + ) -> Result { + let Ok(packet) = Resp::mut_from_bytes(packet) else { + return Err(self); + }; + + let peer_ephemeral_pub = x25519_dalek::PublicKey::from(packet.ephemeral_pub); + let state = self.state.clone(); + + let ret = state + .mix_hash(&packet.ephemeral_pub) // e + .mix_dh(&self.my_ephemeral, &peer_ephemeral_pub) // ee + .mix_dh(&my_static, &peer_ephemeral_pub) // se + .open(&mut [], &packet.auth_tag) + .ok_or(self)? // payload + .finish_as_initiator(); + + Ok(ret) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use itertools::Itertools; + + use super::*; + + fn test_key(r: Range) -> (x25519_dalek::StaticSecret, x25519_dalek::PublicKey) { + assert_eq!(r.len(), 32); + let private = x25519_dalek::StaticSecret::from(r.collect_array().unwrap()); + let public = x25519_dalek::PublicKey::from(&private); + (private, public) + } + + #[test] + fn test_handshake() { + let (init_static, init_static_pub) = test_key(0..32); + let (init_ephemeral, _) = test_key(32..64); + + let (resp_static, resp_static_pub) = test_key(64..96); + let (resp_ephemeral, _) = test_key(96..128); + + const PROLOGUE: &[u8] = b"TEST HANDSHAKE"; + + // These values were verified by hand to be identical to the packets produced by the + // third-party noise-protocol crate. + let expected_init_packet = hex::decode("358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254ad5b8febedeb97415be53612205e6bfab385e34cb127dd8854c4f9afb10f9b0e49075a6f14f9d5bc61412f096ae4950589aef8286944be93ca02ab76a5483b51").unwrap(); + let expected_resp_packet = hex::decode("675dd574ed7789310b3d2e7681f3790b466c773b1521fecf36577958371ea52f5ef5508032efff8066fc858410f411e8").unwrap(); + + let mut init_packet = [0; SentHandshake::INIT_SIZE]; + let init_sent = SentHandshake::new_with_ephemeral( + init_static.clone(), + init_ephemeral, + resp_static_pub, + PROLOGUE, + &mut init_packet, + ); + assert_eq!(init_packet, expected_init_packet.as_ref()); + + let resp_recv = ReceivedHandshake::new(&mut init_packet, PROLOGUE, &resp_static).unwrap(); + assert_eq!(resp_recv.peer_static_pub, init_static_pub); + + let mut resp_packet = [0; ReceivedHandshake::RESP_SIZE]; + let resp_session = resp_recv.finish_with_ephemeral(&mut resp_packet, resp_ephemeral); + assert_eq!(resp_packet, expected_resp_packet.as_ref()); + + let Ok(init_session) = init_sent.try_finish(&mut resp_packet, init_static) else { + panic!("initiator failed to finalize handshake"); + }; + + assert_eq!(init_session.send, resp_session.recv); + assert_eq!(init_session.recv, resp_session.send); + } +} diff --git a/ts_noise/src/ikpsk2.rs b/ts_noise/src/ikpsk2.rs new file mode 100644 index 00000000..d62ba981 --- /dev/null +++ b/ts_noise/src/ikpsk2.rs @@ -0,0 +1,247 @@ +//! Implementation of the Noise IKpsk2 handshake pattern. + +use std::marker::PhantomData; + +use zerocopy::FromBytes; + +use crate::{ + core::{Psk, Session, State}, + messages::{Init, Pod, Resp}, +}; + +const PROTOCOL: &[u8] = b"Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s"; + +/// A partially completed handshake, where the peer is the handshake's initiator. +pub struct ReceivedHandshake { + state: State, + peer_ephemeral_pub: x25519_dalek::PublicKey, + /// The peer's static identity. + pub peer_static_pub: x25519_dalek::PublicKey, +} + +impl ReceivedHandshake { + /// Size of the packet expected by [`ReceivedHandshake::finish`]. + pub const RESP_SIZE: usize = size_of::(); + + /// Process an incoming handshake initiation packet. + /// + /// Returns a [`ReceivedHandshake`] and the decrypted handshake payload, or None + /// if the handshake message is invalid in some way. + pub fn new<'packet, P: Pod>( + packet: &'packet mut [u8], + prologue: &[u8], + my_static: x25519_dalek::StaticSecret, + ) -> Option<(Self, &'packet mut P)> { + let packet: &mut Init

= Init::mut_from_bytes(packet).ok()?; + + let peer_ephemeral_pub = x25519_dalek::PublicKey::from(packet.ephemeral_pub); + let my_static_pub = x25519_dalek::PublicKey::from(&my_static); + + let handshake = State::new(PROTOCOL) + .mix_hash(prologue) // prologue + .mix_hash(my_static_pub.as_ref()) // <- s ... + .mix_hash_and_key(&peer_ephemeral_pub) // -> e + .mix_dh(&my_static, &peer_ephemeral_pub) // es + .open(&mut packet.static_pub, &packet.static_pub_tag)? // s + .mix_dh(&my_static, &packet.static_pub.into()) // ss + .open(packet.payload.as_mut_bytes(), &packet.payload_tag)?; // payload + + Some(( + ReceivedHandshake { + state: handshake, + peer_ephemeral_pub, + peer_static_pub: packet.static_pub.into(), + }, + &mut packet.payload, + )) + } + + /// Finalize the handshake and generate a response. + /// + /// The response is written to `packet`, which must be exactly + /// [`ReceivedHandshake::RESP_SIZE`] bytes. + /// + /// # Panics + /// + /// If `packet` is the wrong size. + #[inline] + pub fn finish(self, psk: &Psk, out: &mut [u8]) -> Session { + let ephemeral = x25519_dalek::StaticSecret::random(); + self.finish_with_ephemeral(psk, ephemeral, out) + } + + fn finish_with_ephemeral( + self, + psk: &Psk, + my_ephemeral: x25519_dalek::StaticSecret, + out: &mut [u8], + ) -> Session { + assert_eq!(out.len(), Self::RESP_SIZE); + let response = Resp::mut_from_bytes(out).unwrap(); + + let my_ephemeral_pub = x25519_dalek::PublicKey::from(&my_ephemeral); + response.ephemeral_pub = my_ephemeral_pub.to_bytes(); + + self.state + .mix_hash_and_key(&my_ephemeral_pub) // <- e + .mix_dh(&my_ephemeral, &self.peer_ephemeral_pub) // ee + .mix_dh(&my_ephemeral, &self.peer_static_pub) // se + .mix_psk(psk) // psk + .seal(&mut [], &mut response.auth_tag) // payload + .finish_as_responder() + } +} + +/// A partially completed handshake, where the peer is the handshake's responder. +pub struct SentHandshake { + state: State, + my_ephemeral: x25519_dalek::StaticSecret, + _phantom: PhantomData

, +} + +impl SentHandshake

{ + /// Size of the output packet to be provided to [`SentHandshake::new`]. + pub const INIT_SIZE: usize = size_of::>(); + + /// Generate an outgoing handshake initiation for the given peer identity. + /// + /// # Panics + /// + /// If `packet` is not [`SentHandshake::INIT_SIZE`] bytes. + #[inline] + pub fn new( + my_static: x25519_dalek::StaticSecret, + peer_static: x25519_dalek::PublicKey, + prologue: &[u8], + payload: P, + out: &mut [u8], + ) -> Self { + let ephemeral = x25519_dalek::StaticSecret::random(); + Self::new_with_ephemeral(my_static, ephemeral, peer_static, prologue, payload, out) + } + + fn new_with_ephemeral( + my_static: x25519_dalek::StaticSecret, + my_ephemeral: x25519_dalek::StaticSecret, + peer_static: x25519_dalek::PublicKey, + prologue: &[u8], + payload: P, + out: &mut [u8], + ) -> Self { + assert_eq!(out.len(), Self::INIT_SIZE); + let out: &mut Init

= Init::mut_from_bytes(out).unwrap(); + + let ephemeral_pub = x25519_dalek::PublicKey::from(&my_ephemeral); + + out.ephemeral_pub = ephemeral_pub.to_bytes(); + out.static_pub = x25519_dalek::PublicKey::from(&my_static).to_bytes(); + out.payload = payload; + + let state = State::new(PROTOCOL) + .mix_hash(prologue) // prologue + .mix_hash(peer_static.as_ref()) // <- s + .mix_hash_and_key(&ephemeral_pub) // -> e + .mix_dh(&my_ephemeral, &peer_static) // es + .seal(&mut out.static_pub, &mut out.static_pub_tag) // s + .mix_dh(&my_static, &peer_static) // ss + .seal(out.payload.as_mut_bytes(), &mut out.payload_tag); // payload + + SentHandshake { + state, + my_ephemeral, + _phantom: PhantomData, + } + } + + /// Try to finalize the handshake and generate a response. + /// + /// If successful, consumes `self` and returns keys for the new session. + /// If the response is invalid, returns `Err(self)` to allow for another finalization + /// attempt later. + pub fn try_finish( + self, + packet: &mut [u8], + my_static: x25519_dalek::StaticSecret, + psk: &Psk, + ) -> Result { + let Ok(packet) = Resp::mut_from_bytes(packet) else { + return Err(self); + }; + + let peer_ephemeral_pub = x25519_dalek::PublicKey::from(packet.ephemeral_pub); + let state = self.state.clone(); + + let ret = state + .mix_hash_and_key(&peer_ephemeral_pub) // e + .mix_dh(&self.my_ephemeral, &peer_ephemeral_pub) // ee + .mix_dh(&my_static, &peer_ephemeral_pub) // se + .mix_psk(psk) // psk + .open(&mut [], &packet.auth_tag) + .ok_or(self)? + .finish_as_initiator(); + + Ok(ret) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use itertools::Itertools; + + use super::*; + + fn test_key(r: Range) -> (x25519_dalek::StaticSecret, x25519_dalek::PublicKey) { + assert_eq!(r.len(), 32); + let private = x25519_dalek::StaticSecret::from(r.collect_array().unwrap()); + let public = x25519_dalek::PublicKey::from(&private); + (private, public) + } + + #[test] + fn test_handshake() { + let (init_static, init_static_pub) = test_key(0..32); + let (init_ephemeral, _) = test_key(32..64); + + let (resp_static, resp_static_pub) = test_key(64..96); + let (resp_ephemeral, _) = test_key(96..128); + + let psk: Psk = (128..160).collect_array().unwrap(); + + const PROLOGUE: &[u8] = b"TEST HANDSHAKE"; + const PAYLOAD: &[u8; 12] = b"TEST PAYLOAD"; + + // These values were verified by hand to be identical to the packets produced by the + // third-party noise-protocol crate. + let expected_init_packet = hex::decode("358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd1662544df99f4e2d9c658302e247ec71c206e70c31df074049bd3d8ab636b27d8a20c512e4beeec758c4b2bee13e5d6c4a6abfe18cb917c010702e44515036af229780d44f50cf68cf7cdaabd81372").unwrap(); + let expected_resp_packet = hex::decode("675dd574ed7789310b3d2e7681f3790b466c773b1521fecf36577958371ea52f1d31e903c203fe7ca9187d9ef4059b09").unwrap(); + + let mut init_packet = [0; SentHandshake::<[u8; 12]>::INIT_SIZE]; + let init_sent = SentHandshake::<[u8; 12]>::new_with_ephemeral( + init_static.clone(), + init_ephemeral, + resp_static_pub, + PROLOGUE, + *PAYLOAD, + &mut init_packet, + ); + assert_eq!(init_packet, expected_init_packet.as_ref()); + + let (resp_recv, resp_payload) = + ReceivedHandshake::new::<[u8; 12]>(&mut init_packet, PROLOGUE, resp_static).unwrap(); + assert_eq!(resp_recv.peer_static_pub, init_static_pub); + assert_eq!(resp_payload, PAYLOAD); + + let mut resp_packet = [0; ReceivedHandshake::RESP_SIZE]; + let resp_session = resp_recv.finish_with_ephemeral(&psk, resp_ephemeral, &mut resp_packet); + assert_eq!(resp_packet, expected_resp_packet.as_ref()); + + let Ok(init_session) = init_sent.try_finish(&mut resp_packet, init_static, &psk) else { + panic!("initiator failed to finalize handshake"); + }; + + assert_eq!(init_session.send, resp_session.recv); + assert_eq!(init_session.recv, resp_session.send); + } +} diff --git a/ts_noise/src/lib.rs b/ts_noise/src/lib.rs new file mode 100644 index 00000000..07e49c66 --- /dev/null +++ b/ts_noise/src/lib.rs @@ -0,0 +1,8 @@ +//! Implementation of the Noise protocol framework instantiations we require. +//! +//! For details on the Noise protocol framework, see + +pub mod core; +pub mod ik; +pub mod ikpsk2; +mod messages; diff --git a/ts_noise/src/messages.rs b/ts_noise/src/messages.rs new file mode 100644 index 00000000..129eef33 --- /dev/null +++ b/ts_noise/src/messages.rs @@ -0,0 +1,68 @@ +//! Wire message types for Noise handshakes. + +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; + +use crate::core::AeadTag; + +/// A type that implements all the zerocopy traits for unconditional conversion +/// to/from bytes. +/// +/// This is the trait bound for handshake payloads. +pub trait Pod: FromBytes + IntoBytes + Immutable + KnownLayout + Unaligned {} + +impl Pod for T {} + +/// A Noise handshake initiation message. +#[repr(C)] +#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Unaligned)] +pub struct Init { + /// The initiator's ephemeral public key. + pub ephemeral_pub: [u8; 32], + /// The initiator's static public key. + /// + /// Encrypted on the wire by the handshake-derived AEAD. + pub static_pub: [u8; 32], + /// The AEAD authenticator tag for the static public key. + pub static_pub_tag: AeadTag, + /// The handshake initiation payload. This is a freeform field that + /// higher level protocols (e.g. WireGuard) can use to exchange further + /// information during the handshake without requiring additional round + /// trips. + /// + /// The security guarantees for this payload are weaker than the security + /// guarantees provided by a fully established session (i.e. after handshake + /// completion): + /// - No forward secrecy protection: if an attacker compromises the responder's + /// static private key, they can decrypt all past handshake initiation payloads + /// that they've recorded. + /// - There is no replay protection, the handshake (including its payload) can + /// be replayed by an attacker at a later date. If replay protection is desired, + /// the higher level protocol must provide that property (and may use the payload + /// field as a component of that). + /// - The initiation as a whole is vulnerable to key compromise impersonation: an + /// attacker who compromises the responder's static private key can forge handshake + /// initiations from any other peer identity. This is normally not a significant + /// concern because once a static private key is compromised it's broadly considered + /// to be "game over" in our specific threat model, but it does mean that you have + /// to be careful to not allow forged payload data to e.g. execute arbitrary code or + /// grant out-of-protocol capabilities. + /// + /// Encrypted on the wire by the handshake-derived AEAD. + pub payload: P, + /// The AEAD authenticator tag for the payload. + pub payload_tag: AeadTag, +} + +/// A Noise handshake response message. +#[repr(C)] +#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Unaligned)] +pub struct Resp { + /// The responder's ephemeral public key. + pub ephemeral_pub: [u8; 32], + /// The AEAD authenticator tag for the response. + /// + /// Technically this is the authenticator for the response payload (see the [`Init`] message + /// for details), but in the protocols we need to implement the response payload is empty, + /// so the payload tag acts purely as an overall authenticator for the entire handshake. + pub auth_tag: AeadTag, +} diff --git a/ts_runtime/src/dataplane.rs b/ts_runtime/src/dataplane.rs index 86ea08dc..e5a03fe5 100644 --- a/ts_runtime/src/dataplane.rs +++ b/ts_runtime/src/dataplane.rs @@ -155,7 +155,7 @@ impl Message> for DataplaneActor { ts_tunnel::PeerId(upsert.0), ts_tunnel::PeerConfig { key: node.node_key, - psk: [0u8; 32].into(), + psk: [0u8; 32], }, ); } diff --git a/ts_tunnel/Cargo.toml b/ts_tunnel/Cargo.toml index 439d1662..d18e47bf 100644 --- a/ts_tunnel/Cargo.toml +++ b/ts_tunnel/Cargo.toml @@ -15,16 +15,15 @@ rust-version.workspace = true ts_keys.workspace = true ts_packet.workspace = true ts_time.workspace = true +ts_noise.workspace = true # Unconditionally required dependencies. aead.workspace = true blake2.workspace = true chacha20poly1305.workspace = true -hkdf.workspace = true itertools.workspace = true rand.workspace = true tracing.workspace = true -x25519-dalek.workspace = true zerocopy.workspace = true [dev-dependencies] diff --git a/ts_tunnel/examples/handshake.rs b/ts_tunnel/examples/handshake.rs index 2370bd84..ce925e79 100644 --- a/ts_tunnel/examples/handshake.rs +++ b/ts_tunnel/examples/handshake.rs @@ -91,7 +91,7 @@ async fn main() -> BoxResult<()> { peer_id, ts_tunnel::PeerConfig { key: peer_key, - psk: [0; 32].into(), + psk: [0; 32], } ) .is_none() diff --git a/ts_tunnel/src/config.rs b/ts_tunnel/src/config.rs index ac828315..8b5d8079 100644 --- a/ts_tunnel/src/config.rs +++ b/ts_tunnel/src/config.rs @@ -1,7 +1,3 @@ -use rand::{ - Rng, RngExt, - distr::{Distribution, StandardUniform}, -}; use ts_keys::NodePublicKey; /// A handle for a wireguard peer. @@ -9,32 +5,7 @@ use ts_keys::NodePublicKey; pub struct PeerId(pub u32); /// A wireguard symmetric pre-shared key. -#[derive(Copy, Clone)] -pub struct Psk([u8; 32]); - -impl From<[u8; 32]> for Psk { - fn from(bytes: [u8; 32]) -> Self { - Psk(bytes) - } -} - -impl AsRef<[u8]> for Psk { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl AsMut<[u8]> for Psk { - fn as_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl Distribution for StandardUniform { - fn sample(&self, rng: &mut R) -> Psk { - Psk(rng.random()) - } -} +pub type Psk = ts_noise::core::Psk; /// The cryptographic configuration for a wireguard peer. pub struct PeerConfig { diff --git a/ts_tunnel/src/endpoint.rs b/ts_tunnel/src/endpoint.rs index 35fc0975..b05ef49a 100644 --- a/ts_tunnel/src/endpoint.rs +++ b/ts_tunnel/src/endpoint.rs @@ -15,7 +15,7 @@ use crate::{ config::{PeerConfig, PeerId}, handshake::{Handshake, ReceivedHandshake, SessionPair, initiate_handshake}, macs::{MACReceiver, MACSender}, - messages::{CookieReply, HandshakeResponse, Message, SessionId}, + messages::{CookieReply, HandshakeResponse, Message, MessageMut, SessionId}, session::{ReceiveSession, TransmitSession}, time::{TAI64N, TAI64NClock}, }; @@ -345,21 +345,21 @@ impl Peer { ) { let pre_len = packets.len(); - packets.retain_mut(|packet| match Message::try_from(packet.as_ref()) { + packets.retain_mut(|packet| match MessageMut::try_from(packet.as_mut()) { Err(()) => { tracing::trace!("dropping invalid packet"); false } - Ok(Message::TransportDataHeader(_)) => true, - Ok(Message::HandshakeResponse(resp)) => { + Ok(MessageMut::TransportDataHeader(_)) => true, + Ok(MessageMut::HandshakeResponse(resp)) => { self.recv_handshake_response(endpoint, resp, out); false } - Ok(Message::CookieReply(resp)) => { + Ok(MessageMut::CookieReply(resp)) => { self.recv_cookie_reply(resp); false } - Ok(Message::HandshakeInitiation(_)) => { + Ok(MessageMut::HandshakeInitiation(_)) => { debug_assert!( false, "handshake initiations should have been filtered out prior to calling recv" @@ -388,11 +388,12 @@ impl Peer { fn recv_handshake_response( &mut self, endpoint: &mut EndpointState, - packet: &HandshakeResponse, + packet: &mut HandshakeResponse, out: &mut RecvResult, ) { let Some(session) = self.handshake.finish( packet, + &endpoint.my_key.private, &self.config.psk, &endpoint.my_cookie, Instant::now(), @@ -676,8 +677,9 @@ impl Endpoint { ret } - fn process_one_handshake(&mut self, packet: PacketMut, out: &mut RecvResult) { - let Ok(Message::HandshakeInitiation(init)) = Message::try_from(packet.as_ref()) else { + fn process_one_handshake(&mut self, mut packet: PacketMut, out: &mut RecvResult) { + let Ok(MessageMut::HandshakeInitiation(init)) = MessageMut::try_from(packet.as_mut()) + else { tracing::error!("message parsing failed"); return; }; diff --git a/ts_tunnel/src/handshake.rs b/ts_tunnel/src/handshake.rs index 49b62830..4f196869 100644 --- a/ts_tunnel/src/handshake.rs +++ b/ts_tunnel/src/handshake.rs @@ -1,13 +1,10 @@ use std::time::Instant; -use aead::AeadInPlace; -use blake2::{Blake2s256, Digest}; -use chacha20poly1305::{ChaCha20Poly1305, KeyInit}; -use hkdf::SimpleHkdf; use ts_keys::{NodeKeyPair, NodePrivateKey, NodePublicKey}; +use ts_noise::ikpsk2; use ts_packet::PacketMut; use ts_time::Handle; -use zerocopy::{FromZeros, IntoBytes}; +use zerocopy::IntoBytes; use crate::{ config::Psk, @@ -18,205 +15,20 @@ use crate::{ time::TAI64N, }; -/// The symmetric session keys produced by a WireGuard handshake. -struct SessionKeys { - initiator_to_responder: chacha20poly1305::Key, - responder_to_initiator: chacha20poly1305::Key, -} - -/// The state of a partially processed handshake. -/// -/// Has to be cloneable because we may have to attempt finalization of the handshake -/// as the initiator multiple times, if rogue invalid responses are received. It's -/// deliberately not Copy, because cloning and allowing potential reuse of the cipher -/// state is risky and needs to be a deliberate act. -#[derive(Clone)] -struct HandshakeState { - hash: [u8; 32], - chaining_key: [u8; 32], - cipher: Option, -} - -/// Initialize a ChaCha20Poly1305 cipher with the given key. -/// -/// # Panics -/// Panics if the key isn't exactly 32 bytes. -fn must_cipher(key: &[u8]) -> ChaCha20Poly1305 { - assert_eq!(key.len(), 32); - ChaCha20Poly1305::new_from_slice(key).unwrap() -} - -/// Use HKDF to derive two 32-byte values. -fn must_hkdf2(chaining_key: &[u8; 32], key: &[u8]) -> ([u8; 32], [u8; 32]) { - let kdf = SimpleHkdf::::new(Some(chaining_key), key); - let mut expanded = [0; 64]; - // Expansion only fails if you request more bytes than the KDF can provide. This KDF can always - // provide 64 bytes. - kdf.expand(&[], &mut expanded).unwrap(); - ( - expanded[..32].try_into().unwrap(), - expanded[32..].try_into().unwrap(), - ) -} - -/// Use HKDF to derive three 32-byte values. -fn must_hkdf3(chaining_key: &[u8; 32], key: &[u8]) -> ([u8; 32], [u8; 32], [u8; 32]) { - let kdf = SimpleHkdf::::new(Some(chaining_key), key); - let mut expanded = [0; 96]; - // Expansion only fails if you request more bytes than the KDF can provide. This KDF can always - // provide 96 bytes. - kdf.expand(&[], &mut expanded).unwrap(); - ( - expanded[..32].try_into().unwrap(), - expanded[32..64].try_into().unwrap(), - expanded[64..].try_into().unwrap(), - ) -} - -impl HandshakeState { - fn new(responder_static: NodePublicKey) -> HandshakeState { - // TODO: precompute initial hash and chaining key, unless the compiler - // is clever enough to figure it out by itself? - let init = Blake2s256::digest("Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s"); - HandshakeState { - hash: init.into(), - chaining_key: init.into(), - cipher: None, - } - .mix_hash(b"WireGuard v1 zx2c4 Jason@zx2c4.com") - .mix_hash(responder_static.as_bytes()) - } - - /// Mix data into the handshake state. - /// - /// This is the MixHash() operation in the Noise spec. - fn mix_hash(mut self, data: &[u8]) -> Self { - let mut h = Blake2s256::new_with_prefix(self.hash); - h.update(data); - h.finalize_into(self.hash.as_mut_bytes().into()); - self - } - - /// Mix a symmetric key into the handshake state, producing a single-use AEAD - /// cipher able to encrypt/decrypt the next portion of the handshake. - /// - /// This is the MixKey() operation in the Noise spec. - fn mix_key(self, key: &[u8; 32]) -> HandshakeState { - let (ck, k) = must_hkdf2(&self.chaining_key, key); - HandshakeState { - hash: self.hash, - chaining_key: ck, - cipher: Some(must_cipher(&k)), - } - } - - /// Derive a one-time AEAD from the pre-shared symmetric key. - /// - /// This is the `psk` handshake step. - fn mix_psk(self, psk: &Psk) -> HandshakeState { - let (ck, h, k) = must_hkdf3(&self.chaining_key, psk.as_ref()); - HandshakeState { - hash: self.hash, - chaining_key: ck, - cipher: Some(must_cipher(&k)), - } - .mix_hash(&h) - } - - /// Finalize the handshake and return a pair of symmetric session keys. - /// - /// This is the Split() operation in the Noise spec. - fn finish(self) -> SessionKeys { - let (k1, k2) = must_hkdf2(&self.chaining_key, &[]); - SessionKeys { - initiator_to_responder: chacha20poly1305::Key::from(k1), - responder_to_initiator: chacha20poly1305::Key::from(k2), - } - } - - /// Encrypt cleartext into dst. - /// - /// dst must be 16 bytes longer than cleartext, and is overwritten. - /// - /// This is the EncryptAndHash() operation in the Noise spec. - /// - /// # Panics - /// Panics if dst is not exactly 16 bytes longer than cleartext, or if called at an - /// incorrect stage of the handshake where encryption is forbidden. - fn encrypt(mut self, cleartext: &[u8], dst: &mut [u8]) -> HandshakeState { - assert_eq!( - dst.len(), - cleartext.len() + 16, - "output slice provided to encrypt must be 16 bytes longer than the input" - ); - let cipher = self.cipher.take().unwrap(); - // The cipher API here is awkward: we can either encrypt into a fresh Vec (causing an alloc), or we - // can encrypt in place. The operation we want, encrypting into a provided slice of the right size, - // isn't available. - // - // So, we do a little dance of copying the cleartext to the destination slice, then encrypt in place - // and add the authentication tag to the end. This is unwieldy, but being able to pass in a destination - // slice plays much nicer with zerocopy's transmutations. - cleartext.write_to_prefix(dst).unwrap(); // destination size verified by assert above - let nonce = [0; 12]; - // ChaCha20Poly1305 only fails if you try to encrypt more than ~274GiB in a single call. - // If you're from the future with 300GiB MTUs and debugging a panic here: hello! - let tag = cipher - .encrypt_in_place_detached(&nonce.into(), &self.hash, &mut dst[..cleartext.len()]) - .unwrap(); - tag.write_to_suffix(dst).unwrap(); // destination size verified by assert above - self.mix_hash(dst) - } - - /// Decrypt ciphertext and return the cleartext. - /// - /// This is the DecryptAndHash() operation in the Noise spec. - /// - /// # Panics - /// Panics if ciphertext is not exactly 16 bytes longer than dst, or if called at an - /// incorrect stage of the handshake where decryption is forbidden. - fn decrypt(mut self, ciphertext: &[u8], dst: &mut [u8]) -> Option { - assert_eq!( - dst.len(), - ciphertext.len() - 16, - "output slice provided to decrypt must be 16 bytes shorter than the input" - ); - let cipher = self.cipher.take().unwrap(); - // Awkward API, see the longer comment in encrypt() for details. - ciphertext[..dst.len()].write_to(dst).unwrap(); // destination size verified by assert above - let nonce = [0; 12]; - cipher - .decrypt_in_place_detached( - &nonce.into(), - &self.hash, - dst, - ciphertext[dst.len()..].into(), - ) - .inspect_err(|e| { - tracing::warn!(error = %e, "decryption failed"); - }) - .ok()?; - Some(self.mix_hash(ciphertext)) - } -} +const PROLOGUE: &[u8] = b"WireGuard v1 zx2c4 Jason@zx2c4.com"; /// A partially completed incoming handshake. pub struct ReceivedHandshake { send_id: SessionId, - + noise: ikpsk2::ReceivedHandshake, // Info decrypted from the HandshakeInitiation - peer_ephemeral: x25519_dalek::PublicKey, - peer_static: NodePublicKey, pub timestamp: TAI64N, - - // State needed to complete the handshake - handshake: HandshakeState, } impl ReceivedHandshake { /// Process a peer's handshake initiation message. pub fn new( - pkt: &HandshakeInitiation, + pkt: &mut HandshakeInitiation, my_static: &NodeKeyPair, macs: &MACReceiver, ) -> Option { @@ -224,30 +36,13 @@ impl ReceivedHandshake { return None; }; - // TODO: cookie DoS protection. Deferring implementation until more of the surrounding code is in place, - // because the right place to do cookie enforcement might be outside of the core Noise handshake logic. - let peer_ephemeral = x25519_dalek::PublicKey::from(pkt.ephemeral_pub); - let my_static_dalek = x25519_dalek::StaticSecret::from(my_static.private); - let mut peer_static_bytes = [0; 32]; - let mut timestamp = TAI64N::new_zeroed(); - let handshake = HandshakeState::new(my_static.public) - .mix_hash(&pkt.ephemeral_pub) // e - .mix_key(&pkt.ephemeral_pub) // e (extra mixing required by psk variant) - .mix_key(my_static_dalek.diffie_hellman(&peer_ephemeral).as_bytes()) // es (reversed because this is the responder) - .decrypt(&pkt.static_pub_sealed, &mut peer_static_bytes)? // s - .mix_key( - my_static_dalek - .diffie_hellman(&x25519_dalek::PublicKey::from(peer_static_bytes)) - .as_bytes(), - ) // ss - .decrypt(&pkt.timestamp_sealed, timestamp.as_mut_bytes())?; // payload + let (noise, timestamp) = + ikpsk2::ReceivedHandshake::new(&mut pkt.noise, PROLOGUE, my_static.private.into())?; Some(ReceivedHandshake { - handshake, - timestamp, - peer_static: NodePublicKey::from(peer_static_bytes), - peer_ephemeral: x25519_dalek::PublicKey::from(pkt.ephemeral_pub), send_id: pkt.sender_id, + noise, + timestamp: *timestamp, }) } @@ -259,31 +54,16 @@ impl ReceivedHandshake { macs: &MACSender, now: Instant, ) -> (SessionPair, PacketMut) { - let my_ephemeral = x25519_dalek::ReusableSecret::random(); - let my_ephemeral_pub = x25519_dalek::PublicKey::from(&my_ephemeral); let mut response = HandshakeResponse { sender_id: session_id, receiver_id: self.send_id, - ephemeral_pub: my_ephemeral_pub.to_bytes(), ..Default::default() }; - let session_keys = self - .handshake - .mix_hash(&my_ephemeral_pub.to_bytes()) // e - .mix_key(&my_ephemeral_pub.to_bytes()) // e (extra mixing required by psk variant) - .mix_key(my_ephemeral.diffie_hellman(&self.peer_ephemeral).as_bytes()) // ee - .mix_key( - my_ephemeral - .diffie_hellman(&self.peer_static.into()) - .as_bytes(), - ) // se (reversed because this is the responder) - .mix_psk(psk) // psk - .encrypt(&[], &mut response.auth_tag) // payload (empty, but must encrypt to generate an auth tag) - .finish(); - - let send = TransmitSession::new(session_keys.responder_to_initiator, self.send_id, now); - let recv = ReceiveSession::new(session_keys.initiator_to_responder, session_id, now); + let session_keys = self.noise.finish(psk, response.noise.as_mut_bytes()); + + let send = TransmitSession::new(session_keys.send, self.send_id, now); + let recv = ReceiveSession::new(session_keys.recv, session_id, now); let mut pkt = PacketMut::new(size_of::()); // Packet is allocated above with the correct size. response.write_to(pkt.as_mut()).unwrap(); @@ -292,7 +72,7 @@ impl ReceivedHandshake { } pub fn peer_static(&self) -> NodePublicKey { - self.peer_static + self.noise.peer_static_pub.to_bytes().into() } } @@ -303,33 +83,22 @@ pub fn initiate_handshake( session_id: SessionId, timestamp: TAI64N, ) -> (SentHandshake, HandshakeInitiation) { - let ephemeral = x25519_dalek::ReusableSecret::random(); - let ephemeral_pub = x25519_dalek::PublicKey::from(&ephemeral); - let endpoint_static_pub = NodePublicKey::from(endpoint_static); - let mut pkt = HandshakeInitiation { sender_id: session_id, - ephemeral_pub: ephemeral_pub.to_bytes(), ..Default::default() }; - let handshake = HandshakeState::new(peer_static) - .mix_hash(ephemeral_pub.as_bytes()) // e - .mix_key(ephemeral_pub.as_bytes()) // e (extra mixing required by psk variant) - .mix_key(ephemeral.diffie_hellman(&peer_static.into()).as_bytes()) // es - .encrypt(endpoint_static_pub.as_bytes(), &mut pkt.static_pub_sealed) // s - .mix_key( - x25519_dalek::StaticSecret::from(endpoint_static) - .diffie_hellman(&peer_static.into()) - .as_bytes(), - ) // ss - .encrypt(timestamp.as_bytes(), &mut pkt.timestamp_sealed); // payload + let noise = ikpsk2::SentHandshake::new( + endpoint_static.into(), + peer_static.into(), + PROLOGUE, + timestamp, + pkt.noise.as_mut_bytes(), + ); let ret = SentHandshake { id: session_id, - my_ephemeral: ephemeral, - my_static: endpoint_static, - handshake, + noise, }; (ret, pkt) @@ -338,9 +107,7 @@ pub fn initiate_handshake( /// A partially completed sent handshake. pub struct SentHandshake { pub id: SessionId, - my_ephemeral: x25519_dalek::ReusableSecret, - my_static: NodePrivateKey, - handshake: HandshakeState, + noise: ikpsk2::SentHandshake, } pub struct SessionPair { @@ -375,6 +142,16 @@ impl Handshake { } } + pub(crate) fn take_initiated(&mut self) -> Option<(SentHandshake, Handle, Mac)> { + match std::mem::replace(self, Handshake::None) { + Handshake::Initiated(sent, timeout, mac) => Some((sent, timeout, mac)), + other => { + *self = other; + None + } + } + } + /// Respond to a peer's handshake initiation, and switch to the responder state to await /// session confirmation. /// @@ -412,45 +189,33 @@ impl Handshake { /// completion of the handshake. pub(crate) fn finish( &mut self, - packet: &HandshakeResponse, + packet: &mut HandshakeResponse, + endpoint_static: &NodePrivateKey, psk: &Psk, cookies: &MACReceiver, now: Instant, ) -> Option { - let Handshake::Initiated(sent_handshake, ..) = self else { - return None; - }; + let (mut sent_handshake, timeout, _) = self.take_initiated()?; if !cookies.verify_macs(packet.as_bytes()) { return None; }; - let peer_ephemeral = x25519_dalek::PublicKey::from(packet.ephemeral_pub); - let handshake = sent_handshake.handshake.clone(); - let session_keys = handshake - .mix_hash(&packet.ephemeral_pub) // e - .mix_key(&packet.ephemeral_pub) // e (extra mixing required by psk variant) - .mix_key( - sent_handshake - .my_ephemeral - .diffie_hellman(&peer_ephemeral) - .as_bytes(), - ) // ee - .mix_key( - x25519_dalek::StaticSecret::from(sent_handshake.my_static) - .diffie_hellman(&peer_ephemeral) - .as_bytes(), - ) // se - .mix_psk(psk) // psk - .decrypt(&packet.auth_tag, &mut Vec::new()) // payload (empty, but must decrypt to verify auth tag) - .map(|handshake| handshake.finish())?; - - let send = TransmitSession::new(session_keys.initiator_to_responder, packet.sender_id, now); - let recv = ReceiveSession::new(session_keys.responder_to_initiator, sent_handshake.id, now); - - let Handshake::Initiated(_, timeout, _) = std::mem::replace(self, Handshake::None) else { - unreachable!(); - }; + let session_keys = + match sent_handshake + .noise + .try_finish(&mut packet.noise, endpoint_static.into(), psk) + { + Ok(session_keys) => session_keys, + Err(handshake) => { + sent_handshake.noise = handshake; + return None; + } + }; + + let send = TransmitSession::new(session_keys.send, packet.sender_id, now); + let recv = ReceiveSession::new(session_keys.recv, sent_handshake.id, now); + timeout.cancel(); Some(SessionPair { send, recv }) @@ -523,24 +288,28 @@ mod tests { let mut a_handshake = Handshake::Initiated(a_handshake, timeout, handshake_mac); // Peer B receives it and responds - let init_pkt = HandshakeInitiation::try_ref_from_bytes(init_pkt.as_ref()) + let init_pkt = HandshakeInitiation::try_mut_from_bytes(init_pkt.as_mut()) .expect("init_pkt should be a valid handshake initiation message"); let b_mac_send = MACSender::new(&a_static.public); let b_mac_recv = MACReceiver::new(&b_static.public); let b_handshake = ReceivedHandshake::new(init_pkt, &b_static, &b_mac_recv) .expect("peer B should successfully process A's handshake initiation"); - assert_eq!(b_handshake.peer_static, a_static.public); + assert_eq!(b_handshake.peer_static(), a_static.public); assert_eq!(b_handshake.timestamp, a_init_time); let b_session = SessionId::random(); // B wants to receive at this ID - let (mut b_session, response_pkt) = + let (mut b_session, mut response_pkt) = b_handshake.respond(b_session, &psk, &b_mac_send, Instant::now()); // Peer A receives response - let response_pkt = HandshakeResponse::try_ref_from_bytes(response_pkt.as_ref()) + let response_pkt = HandshakeResponse::try_mut_from_bytes(response_pkt.as_mut()) .expect("response_pkt should be a valid handshake response message"); - let Some(mut a_session) = - a_handshake.finish(response_pkt, &psk, &a_mac_recv, Instant::now()) - else { + let Some(mut a_session) = a_handshake.finish( + response_pkt, + &a_static.private, + &psk, + &a_mac_recv, + Instant::now(), + ) else { panic!("failed to process handshake response from peer B"); }; diff --git a/ts_tunnel/src/messages.rs b/ts_tunnel/src/messages.rs index 989c5a5a..e616ba46 100644 --- a/ts_tunnel/src/messages.rs +++ b/ts_tunnel/src/messages.rs @@ -1,10 +1,13 @@ use core::fmt::{Debug, Formatter}; +use ts_noise::ikpsk2; use zerocopy::{ FromBytes, Immutable, IntoBytes, KnownLayout, TryFromBytes, Unaligned, byteorder::little_endian::{U32, U64}, }; +use crate::time::TAI64N; + #[repr(transparent)] #[derive( Debug, @@ -60,9 +63,7 @@ pub struct HandshakeInitiation { pub msg_type: MessageType, pub _reserved: [u8; 3], pub sender_id: SessionId, - pub ephemeral_pub: [u8; 32], - pub static_pub_sealed: [u8; 32 + 16], - pub timestamp_sealed: [u8; 12 + 16], + pub noise: [u8; ikpsk2::SentHandshake::::INIT_SIZE], pub mac1: [u8; 16], pub mac2: [u8; 16], } @@ -73,9 +74,7 @@ impl Default for HandshakeInitiation { msg_type: MessageType::HandshakeInitiation, _reserved: Default::default(), sender_id: Default::default(), - ephemeral_pub: Default::default(), - static_pub_sealed: [0; 32 + 16], // no Default impl for such a large array - timestamp_sealed: Default::default(), + noise: [0; ikpsk2::SentHandshake::::INIT_SIZE], mac1: Default::default(), mac2: Default::default(), } @@ -89,8 +88,7 @@ pub struct HandshakeResponse { pub _reserved: [u8; 3], pub sender_id: SessionId, pub receiver_id: SessionId, - pub ephemeral_pub: [u8; 32], - pub auth_tag: [u8; 16], + pub noise: [u8; ikpsk2::ReceivedHandshake::RESP_SIZE], pub mac1: [u8; 16], pub mac2: [u8; 16], } @@ -102,8 +100,7 @@ impl Default for HandshakeResponse { _reserved: Default::default(), sender_id: Default::default(), receiver_id: Default::default(), - ephemeral_pub: Default::default(), - auth_tag: Default::default(), + noise: [0; ikpsk2::ReceivedHandshake::RESP_SIZE], mac1: Default::default(), mac2: Default::default(), } @@ -172,7 +169,7 @@ impl Default for CookieReply { } pub enum Message<'packet> { - HandshakeInitiation(&'packet HandshakeInitiation), + HandshakeInitiation(#[allow(dead_code)] &'packet HandshakeInitiation), HandshakeResponse(&'packet HandshakeResponse), TransportDataHeader(&'packet TransportDataHeader), CookieReply(&'packet CookieReply), @@ -213,3 +210,35 @@ impl Message<'_> { } } } + +pub enum MessageMut<'packet> { + HandshakeInitiation(&'packet mut HandshakeInitiation), + HandshakeResponse(&'packet mut HandshakeResponse), + TransportDataHeader(#[allow(dead_code)] &'packet mut TransportDataHeader), + CookieReply(&'packet mut CookieReply), +} + +impl<'packet> TryFrom<&'packet mut [u8]> for MessageMut<'packet> { + type Error = (); + + fn try_from(raw: &'packet mut [u8]) -> Result, Self::Error> { + let Ok((msg_type, _)) = MessageType::try_ref_from_prefix(raw) else { + return Err(()); + }; + + match msg_type { + MessageType::HandshakeInitiation => HandshakeInitiation::try_mut_from_bytes(raw) + .map(MessageMut::HandshakeInitiation) + .map_err(|_| ()), + MessageType::HandshakeResponse => HandshakeResponse::try_mut_from_bytes(raw) + .map(MessageMut::HandshakeResponse) + .map_err(|_| ()), + MessageType::TransportData => TransportDataHeader::try_mut_from_prefix(raw) + .map(|(header, _)| MessageMut::TransportDataHeader(header)) + .map_err(|_| ()), + MessageType::CookieReply => CookieReply::try_mut_from_bytes(raw) + .map(MessageMut::CookieReply) + .map_err(|_| ()), + } + } +} diff --git a/ts_tunnel/src/time.rs b/ts_tunnel/src/time.rs index 03c9d783..f2e5f8fe 100644 --- a/ts_tunnel/src/time.rs +++ b/ts_tunnel/src/time.rs @@ -2,14 +2,24 @@ use core::fmt::{Debug, Display, Formatter}; use std::time::{SystemTime, UNIX_EPOCH}; use zerocopy::{ - FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout, + FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout, Unaligned, byteorder::big_endian::{U32, U64}, }; /// An instant in time in the TAI64 format. #[repr(C)] #[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromBytes, IntoBytes, Immutable, KnownLayout, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + FromBytes, + IntoBytes, + Immutable, + KnownLayout, + Unaligned, )] pub struct TAI64N { secs: U64,