diff --git a/Cargo.toml b/Cargo.toml index 575115f..3cbffb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "crates/lyrebird", - # "crates/o5", + "crates/o5", "crates/o7", "crates/obfs4", "crates/ptrs", diff --git a/crates/lyrebird/Cargo.toml b/crates/lyrebird/Cargo.toml index eec093b..8c48fec 100644 --- a/crates/lyrebird/Cargo.toml +++ b/crates/lyrebird/Cargo.toml @@ -38,7 +38,7 @@ anyhow = "1.0" clap = { version = "4.4", features = ["derive"]} fast-socks5 = "0.9.1" futures = "0.3.29" -safelog = "0.3.5" +safelog = "0.4.0" thiserror = "1.0.56" tokio = { version = "1.34", features = ["io-util", "net", "macros", "sync", "signal"] } tokio-util = "0.7.10" diff --git a/crates/o5/Cargo.toml b/crates/o5/Cargo.toml index 82aada2..60a8622 100644 --- a/crates/o5/Cargo.toml +++ b/crates/o5/Cargo.toml @@ -1,30 +1,79 @@ [package] name = "o5" -version = "0.1.0" +version = "0.1.0-alpha.1" edition = "2021" +authors = ["Jack Wampler "] +rust-version = "1.81" +license = "MIT OR Apache-2.0" +description = "Pure rust implementation of the o5 pluggable transport" +keywords = ["tor", "censorship", "pluggable", "transports"] +categories = ["network-programming", "cryptography"] +repository = "https://github.com/jmwample/ptrs" + [lib] name = "o5" crate-type = ["cdylib", "rlib"] [dependencies] +## Local +ptrs = { path="../ptrs", version="0.1.0" } + +## PRNG getrandom = "0.2.11" rand = { version="0.8.5", features=["getrandom"]} rand_core = "0.6.4" +## Crypto +digest = { version = "0.10.7", features=["mac", "core-api"]} +typenum = "1.17.0" +block-buffer = "0.10.4" +siphasher = "1.0.0" +sha2 = "0.10.8" +hmac = { version="0.12.1", features=["reset"]} +hkdf = "0.12.3" +crypto_secretbox = { version="0.1.1", features=["chacha20"]} subtle = "2.5.0" -x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets", "elligator2"], git = "https://github.com/jmwample/curve25519-dalek.git", branch = "elligator2-ntor"} +x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets"]} + +## Utils +pin-project = "1.1.3" +hex = "0.4.3" +futures = "0.3.29" +tracing = "0.1.40" +colored = "2.0.4" +serde_json = "1.0.114" +serde = "1.0.197" +base64 = "0.22.0" -# ntor_arti +## Networking tools +tokio = { version = "1.33", features = ["io-util", "rt-multi-thread", "net", "rt", "macros", "sync", "signal", "time", "fs"] } +tokio-util = { version = "0.7.10", features = ["codec", "io"]} +bytes = "1.5.0" + +## ntor_arti +tor-cell = "0.23.0" +tor-llcrypto = "0.23.0" +tor-error = "0.23.0" +tor-bytes = "0.23.0" +cipher = "0.4.4" zeroize = "1.7.0" +thiserror = "1.0.56" + +curve25519-elligator2 = { version="0.1.0-alpha.1", features=["elligator2"] } + +# o5 pqc +ml-kem = "0.2.1" +kem = "0.3.0-pre.0" +# kemeleon = { version="0.1.0-rc.1", path="../../../../elligantt/kemeleon"} +kemeleon = { version="0.1.0-rc.1", git="https://github.com/jmwample/kemeleon", branch="cleanup"} [dev-dependencies] -hex = "0.4.3" anyhow = "1.0" +tracing-subscriber = "0.3.18" +hex-literal = "0.4.1" +tor-basic-utils = "0.22.0" -# o5 pqc test -# pqc_kyber = {version="0.7.1", features=["kyber1024", "std"]} -ml-kem = "0.1.0" [lints.rust] # unexpected_cfgs are used to disable incomplete / WIP features and tests. This is diff --git a/crates/o5/README.md b/crates/o5/README.md index 11c6c84..da76dca 100644 --- a/crates/o5/README.md +++ b/crates/o5/README.md @@ -1,7 +1,7 @@ # o5 Pluggable Transport Library -A randomizing look like nothing pluggable transport library, spiritually a successor -to `obfs4`. +This is a spiritual successor to `obfs4` updating some of the more annoying / out of +date elements without worrying about being backward compatible. ⚠️ 🚧 WARNING This crate is still under construction 🚧 ⚠️ @@ -10,30 +10,38 @@ to `obfs4`. - Not production ready - do not rely on this for any security critical applications - -## Changes from obfs4: - - -* adds `Kyber1024` to the Key exchange making it hybrid `Kyber1024X25519` (or `Kyber1024X`) - * Are Kyber1024 keys uniform random? I assume not. -* aligns algorithm with vanilla ntor - - obfs4 does an extra hash -* change mark and MAC from sha256-128 to sha256 - - not sure why this was done in the first place -* padding change (/fix?) -* padding is a frame type, not just appended bytes -* version / params frame for negotiating (non-forward secret in the first exchange alongside PRNG seed) -* might add - - session tickets and resumption - - bidirectional heartbeats - - handshake complete frame type - +## Differences from obfs4 + +- Frame / Packet / Message construction + - In obfs4 a "frame" consists of a signle "packet", encoded using xsalsa20Poly1305. + we use the same frame construction, but change a few key elements: + - the concept of "packets" is now called "messages" + - a frame can contain multiple messages + - update from xsalsa20poly1305 -> chacha20poly1305 + - padding is given an explicit message type different than that of a payload and uses the mesage length header field + - (In obfs4 a frame that decodes to a payload packet type `\x00` with packet length 0 is asummed to all be padding) + - move payload to message type `\x01` + - padding takes message type `\x00` + - (Maybe) add bidirectional heartbeat messages +- Handshake + - x25519 key-exchange -> Kyber1024X25519 key-exchange + - the overhead padding of the current obfs4 handshake (resulting in paket length in [4096:8192]) is mostly unused + we exchange some of this unused padding for a kyber key to provide post-quantum security to the handshake. + - Are Kyber1024 keys uniform random? I assume not. + - NTor V3 handshake + - the obfs4 handshake uses (a custom version of) the ntor handshake to derive key materials + - (Maybe) change mark and MAC from sha256-128 to sha256 + - handshake parameters encrypted under the key exchange public keys + - the client can provide initial parameters during the handshake, knowing that they are not forward secure. + - the server can provide messages with parameters / extensions in the handshake response (like prngseed) + - like the kyber key, this takes space out of the padding already used in the client handshake. + - (Maybe) session tickets and resumption + - (Maybe) handshake complete frame type ### Goals * Stick closer to Codec / Framed implementation for all packets (hadshake included) * use the tor/arti ntor v3 implementation - ### Features to keep - once a session is established, unrecognized frame types are ignored diff --git a/crates/o5/src/client.rs b/crates/o5/src/client.rs new file mode 100644 index 0000000..0dfb8cd --- /dev/null +++ b/crates/o5/src/client.rs @@ -0,0 +1,175 @@ +#![allow(unused)] + +use crate::{ + common::{colorize, mlkem1024_x25519, HmacSha256}, + constants::*, + framing::{FrameError, Marshall, O5Codec, TryParse, KEY_LENGTH, KEY_MATERIAL_LENGTH}, + handshake::IdentityPublicKey, + proto::{MaybeTimeout, O5Stream}, + sessions, Error, Result, +}; + +use bytes::{Buf, BufMut, BytesMut}; +use hmac::{Hmac, Mac}; +use ptrs::{debug, info, trace, warn}; +use rand::prelude::*; +use subtle::ConstantTimeEq; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::time::{Duration, Instant}; + +use std::{ + fmt, + io::{Error as IoError, ErrorKind as IoErrorKind}, + pin::Pin, + sync::{Arc, Mutex}, +}; + +#[derive(Clone, Debug)] +pub struct ClientBuilder { + pub station_pubkey: [u8; PUBLIC_KEY_LEN], + pub station_id: [u8; NODE_ID_LENGTH], + pub statefile_path: Option, + pub(crate) handshake_timeout: MaybeTimeout, +} + +impl Default for ClientBuilder { + fn default() -> Self { + Self { + station_pubkey: [0u8; PUBLIC_KEY_LEN], + station_id: [0_u8; NODE_ID_LENGTH], + statefile_path: None, + handshake_timeout: MaybeTimeout::Default_, + } + } +} + +impl ClientBuilder { + /// TODO: implement client builder from statefile + pub fn from_statefile(location: &str) -> Result { + Ok(Self { + station_pubkey: [0_u8; PUBLIC_KEY_LEN], + station_id: [0_u8; NODE_ID_LENGTH], + statefile_path: Some(location.into()), + handshake_timeout: MaybeTimeout::Default_, + }) + } + + /// TODO: implement client builder from string args + pub fn from_params(param_strs: Vec>) -> Result { + Ok(Self { + station_pubkey: [0_u8; PUBLIC_KEY_LEN], + station_id: [0_u8; NODE_ID_LENGTH], + statefile_path: None, + handshake_timeout: MaybeTimeout::Default_, + }) + } + + pub fn with_node_pubkey(&mut self, pubkey: [u8; PUBLIC_KEY_LEN]) -> &mut Self { + self.station_pubkey = pubkey; + self + } + + pub fn with_statefile_path(&mut self, path: &str) -> &mut Self { + self.statefile_path = Some(path.into()); + self + } + + pub fn with_node_id(&mut self, id: [u8; NODE_ID_LENGTH]) -> &mut Self { + self.station_id = id; + self + } + + pub fn with_handshake_timeout(&mut self, d: Duration) -> &mut Self { + self.handshake_timeout = MaybeTimeout::Length(d); + self + } + + pub fn with_handshake_deadline(&mut self, deadline: Instant) -> &mut Self { + self.handshake_timeout = MaybeTimeout::Fixed(deadline); + self + } + + pub fn fail_fast(&mut self) -> &mut Self { + self.handshake_timeout = MaybeTimeout::Unset; + self + } + + pub fn build(&self) -> Client { + Client { + station_pubkey: IdentityPublicKey::new(self.station_pubkey, self.station_id) + .expect("failed to build client - bad options."), + handshake_timeout: self.handshake_timeout.duration(), + } + } + + pub fn as_opts(&self) -> String { + //TODO: String self as command line options + "".into() + } +} + +impl fmt::Display for ClientBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + //TODO: string self + write!(f, "") + } +} + +/// Client implementing the obfs4 protocol. +pub struct Client { + station_pubkey: IdentityPublicKey, + handshake_timeout: Option, +} + +impl Client { + /// TODO: extract args to create new builder + pub fn get_args(&mut self, _args: &dyn std::any::Any) {} + + /// On a failed handshake the client will read for the remainder of the + /// handshake timeout and then close the connection. + pub async fn wrap<'a, T>(self, mut stream: T) -> Result> + where + T: AsyncRead + AsyncWrite + Unpin + 'a, + { + let session = sessions::new_client_session(self.station_pubkey); + + let deadline = self.handshake_timeout.map(|d| Instant::now() + d); + + session.handshake(stream, deadline).await + } + + /// On a failed handshake the client will read for the remainder of the + /// handshake timeout and then close the connection. + pub async fn establish<'a, T, E>( + self, + mut stream_fut: Pin>, + ) -> Result> + where + T: AsyncRead + AsyncWrite + Unpin + 'a, + E: std::error::Error + Send + Sync + 'static, + { + let stream = stream_fut.await.map_err(|e| Error::Other(Box::new(e)))?; + + let session = sessions::new_client_session(self.station_pubkey); + + let deadline = self.handshake_timeout.map(|d| Instant::now() + d); + + session.handshake(stream, deadline).await + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Result; + + #[test] + fn parse_params() -> Result<()> { + let test_args = [["", "", ""]]; + + for (i, test_case) in test_args.iter().enumerate() { + let cb = ClientBuilder::from_params(test_case.to_vec())?; + } + Ok(()) + } +} diff --git a/crates/o5/src/common/README.md b/crates/o5/src/common/README.md new file mode 100644 index 0000000..6e4cdc9 --- /dev/null +++ b/crates/o5/src/common/README.md @@ -0,0 +1,10 @@ + +# Common tools + + + + +### TODO + +- [ ] better probdist actually using [Distribution]() for [Rng]()? + - we want this to be possible to implement in both rust and golang. diff --git a/crates/o5/src/common/ct.rs b/crates/o5/src/common/ct.rs new file mode 100644 index 0000000..ba0d48a --- /dev/null +++ b/crates/o5/src/common/ct.rs @@ -0,0 +1,17 @@ +//! Constant-time utilities. +use subtle::{Choice, ConstantTimeEq}; + +/// Convert a boolean into a Choice. +/// +/// This isn't necessarily a good idea or constant-time. +pub(crate) fn bool_to_choice(v: bool) -> Choice { + Choice::from(u8::from(v)) +} + +/// Return true if two slices are equal. Performs its operation in constant +/// time, but returns a bool instead of a subtle::Choice. +#[allow(unused)] +pub(crate) fn bytes_eq(a: &[u8], b: &[u8]) -> bool { + let choice = a.ct_eq(b); + choice.unwrap_u8() == 1 +} diff --git a/crates/o5/src/common/drbg.rs b/crates/o5/src/common/drbg.rs new file mode 100644 index 0000000..92c98fa --- /dev/null +++ b/crates/o5/src/common/drbg.rs @@ -0,0 +1,348 @@ +/// +/// # DRBG +/// +/// Implements a simple Hash based Deterministic Random Bit Generator (DRBG) +/// algorithm in order to match the golang implementation of obfs4. +use crate::{Error, Result}; + +use std::fmt; +use std::str::FromStr; + +use getrandom::getrandom; +use hex::FromHex; +use rand_core::{impls, Error as RandError, RngCore}; +use siphasher::{prelude::*, sip::SipHasher24}; + +pub(crate) const SIZE: usize = 8; +pub(crate) const SEED_LENGTH: usize = 16 + SIZE; + +/// Hash-DRBG seed +#[derive(Debug, PartialEq, Clone)] +pub struct Seed([u8; SEED_LENGTH]); + +impl Seed { + pub fn new() -> Result { + let mut seed = Self([0_u8; SEED_LENGTH]); + getrandom(&mut seed.0)?; + Ok(seed) + } + + // Calling unwraps here is safe because the size of the key is fixed + fn to_pieces(&self) -> ([u8; 16], [u8; SIZE]) { + let key: [u8; 16] = self.0[..16].try_into().unwrap(); + + let ofb: [u8; SIZE] = self.0[16..].try_into().unwrap(); + (key, ofb) + } + + fn to_new_drbg(&self) -> Drbg { + let (key, ofb) = self.to_pieces(); + Drbg { + hash: SipHasher24::new_with_key(&key), + ofb, + } + } + + pub fn to_bytes(&self) -> [u8; SEED_LENGTH] { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl FromHex for Seed { + type Error = Error; + + fn from_hex>(msg: T) -> Result { + let buffer = <[u8; SEED_LENGTH]>::from_hex(msg)?; + Ok(Seed(buffer)) + } +} + +impl TryFrom for Seed { + type Error = Error; + + fn try_from(msg: String) -> Result { + let buffer = <[u8; SEED_LENGTH]>::from_hex(msg)?; + Ok(Seed(buffer)) + } +} + +impl TryFrom<&String> for Seed { + type Error = Error; + + fn try_from(msg: &String) -> Result { + let buffer = <[u8; SEED_LENGTH]>::from_hex(msg)?; + Ok(Seed(buffer)) + } +} + +impl TryFrom<&str> for Seed { + type Error = Error; + + fn try_from(msg: &str) -> Result { + let buffer = <[u8; SEED_LENGTH]>::from_hex(msg)?; + Ok(Seed(buffer)) + } +} + +impl FromStr for Seed { + type Err = Error; + fn from_str(s: &str) -> Result { + Seed::from_hex(s) + } +} + +impl TryFrom<&[u8]> for Seed { + type Error = Error; + fn try_from(arr: &[u8]) -> Result { + let mut seed = Seed::new()?; + if arr.len() != SEED_LENGTH { + let e = format!("incorrect drbg seed length {}!={SEED_LENGTH}", arr.len()); + return Err(Error::Other(e.into())); + } + + seed.0 = arr + .try_into() + .map_err(|e| Error::Other(format!("{e}").into()))?; + + Ok(seed) + } +} + +impl From<[u8; SEED_LENGTH]> for Seed { + fn from(arr: [u8; SEED_LENGTH]) -> Self { + Seed(arr) + } +} + +impl TryFrom> for Seed { + type Error = Error; + fn try_from(arr: Vec) -> Result { + Seed::try_from(arr.as_slice()) + } +} + +impl fmt::Display for Seed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0[..])) + } +} + +pub struct Drbg { + #[allow(deprecated)] + hash: SipHasher24, + ofb: [u8; SIZE], +} + +impl Drbg { + /// Makes a 'Drbg' instance based off an optional seed. The seed + /// is truncated to SeedLength. + pub fn new(seed_in: Option) -> Result { + let seed = match seed_in { + Some(s) => s, + None => Seed::new()?, + }; + Ok(seed.to_new_drbg()) + } + + /// Returns a uniformly distributed random uint [0, 1 << 64). + pub fn uint64(&mut self) -> u64 { + let ret: u64 = { + self.hash.write(&self.ofb[..]); + self.hash.finish().to_be() + }; + self.ofb = ret.to_be_bytes(); + + ret + } + + /// returns a random u16 selected in the same way as the golang implementation + /// of obfs4 which takes the high bits when casting to u16. + pub(crate) fn length_mask(&mut self) -> u16 { + let ret: u64 = { + self.hash.write(&self.ofb[..]); + self.hash.finish().to_be() + }; + self.ofb = ret.to_be_bytes(); + + (ret >> 48) as u16 + } + + /// Returns a uniformly distributed random integer [0, 1 << 63). + pub fn int63(&mut self) -> i64 { + let mut ret = self.uint64(); + + // This is a safe unwrap as we bit-mask to below overflow + // ret &= (1<<63) -1; + ret &= >::try_into(i64::MAX).unwrap(); + i64::try_from(ret).unwrap() + } + + /// NextBlock returns the next 8 byte DRBG block. + pub fn next_block(&mut self) -> [u8; SIZE] { + let h = self.uint64(); + h.to_be_bytes() + } +} + +impl RngCore for Drbg { + fn next_u32(&mut self) -> u32 { + self.next_u64() as u32 + } + + fn next_u64(&mut self) -> u64 { + self.uint64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + impls::fill_bytes_via_next(self, dest) + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> std::result::Result<(), RandError> { + self.fill_bytes(dest); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn rand() -> Result<()> { + let seed = Seed::new()?; + + let mut drbg = Drbg::new(Some(seed))?; + + let mut u: u64; + let mut i: i64; + for n in 0..100_000 { + i = drbg.int63(); + assert!(i > 0, "i63 error - {i} < 0 iter:{n}"); + + u = drbg.uint64(); + assert_ne!(u, 0); + } + + Ok(()) + } + + #[test] + fn from_() -> Result<()> { + let expected = Seed([0_u8; SEED_LENGTH]); + + let input = "000000000000000000000000000000000000000000000000"; + assert_eq!(Seed::try_from(input).unwrap(), expected); + assert_eq!(Seed::from_hex(input).unwrap(), expected); + assert_eq!(Seed::from_str(input).unwrap(), expected); + + let input: String = input.into(); + assert_eq!(Seed::try_from(input.clone()).unwrap(), expected); + assert_eq!(Seed::from_hex(input.clone()).unwrap(), expected); + assert_eq!(Seed::try_from(&input.clone()).unwrap(), expected); + assert_eq!(Seed::from_hex(input.clone()).unwrap(), expected); + assert_eq!(Seed::from_str(&input.clone()).unwrap(), expected); + + let input = [0_u8; SEED_LENGTH]; + assert_eq!(Seed::from(input), expected); + assert_eq!(Seed::try_from(&input[..]).unwrap(), expected); + + let input = vec![0_u8; SEED_LENGTH]; + assert_eq!(Seed::try_from(input.clone()).unwrap(), expected); + assert_eq!(Seed::try_from(&input.clone()[..]).unwrap(), expected); + + Ok(()) + } + + /// Make sure bitmasks, overflows, and type assertions work the way I think they do. + #[test] + fn conversions() { + let mut u64_max = u64::MAX; + >::try_into(u64_max).unwrap_err(); + + u64_max &= >::try_into(i64::MAX).unwrap(); + let i: i64 = u64_max.try_into().unwrap(); + // println!("{i:x}, {:x}", i64::MAX); + assert_eq!(i, i64::MAX); + + let mut u64_max = u64::MAX; + u64_max &= (1 << 63) - 1; + let i: i64 = u64_max.try_into().unwrap(); + assert_eq!(i, i64::MAX); + assert_eq!(i, i64::MAX); + + let u64_max: u64 = (1 << 63) - 1; + let i: i64 = u64_max.try_into().unwrap(); + assert_eq!(i, i64::MAX); + assert_eq!(i, i64::MAX); + } + + /// Ensure that we are compatible with the golang hash-drbg so that the + /// libraries are interchangeable. + #[test] + fn sample_compat_compare() -> Result<()> { + struct Case { + seed: &'static str, + out: Vec, + } + + // if we can generate multiple correct rounds for multiple seeds we should be just fine. + let cases = vec![ + Case { + seed: "000000000000000000000000000000000000000000000000", + out: vec![ + 7432626515892259304, + 5773523046280711756, + 4537542203639783680, + ], + }, + // (0, 0) "00000000000000000000000000000000" + // &{v0:8317987319222330741 v1:7237128888997146477 v2:7816392313619706465 v3:8387220255154660723 } k0:0 k1:0 x:[0 0 0 0 0 0 0 0] nx:0 size:8 t:0} + // { v0:8317987319222330741 v1:7237128888997146477 v2:7816392313619706465 v3:8387220255154660723 } + Case { + seed: "0c10867722204c856e78315d669449dcb6e66f2fe5247a80", + out: vec![ + 9059004827137905928, + 6853924365612632173, + 1485252377529977150, + ], + }, + // 854c20227786100c dc4994665d31786e k0:9605087437680676876 k1:15873381529015122030 + // (9605087437680676876, 15873381529015122030) "0c10867722204c856e78315d669449dc" + Case { + seed: "ddbb886aefbe2a65c2509dfc3bb0932c5e881965afca80a0", + out: vec![ + 3952461850862704951, + 6715353867928838006, + 5560038622741453571, + ], + }, + Case { + seed: "e691b1eaa81018e8b16bbf84d71f3ba0c5f965bace2da7cc", + out: vec![ + 8251725530906761037, + 5718043109939568014, + 7585544303175018394, + ], + }, + ]; + + for (j, c) in cases.into_iter().enumerate() { + let seed = Seed::try_from(c.seed)?; + let drbg = &mut Drbg::new(Some(seed))?; + // println!(); + // println!("{:?}", drbg.hash); + + for (k, expected) in c.out.into_iter().enumerate() { + let i = drbg.int63(); + // println!("{:?}", drbg.hash); + assert_eq!(i, expected, "[{},{}]\n0x{i:x}\n0x{expected:x}", j, k); + } + } + + Ok(()) + } +} diff --git a/crates/o5/src/common/kdf.rs b/crates/o5/src/common/kdf.rs new file mode 100644 index 0000000..f0a9401 --- /dev/null +++ b/crates/o5/src/common/kdf.rs @@ -0,0 +1,158 @@ +//! Key derivation functions +//! +//! Tor has three relevant key derivation functions that it uses for +//! deriving keys used for relay encryption. +//! +//! The *KDF-TOR* KDF (implemented by `LegacyKdf`) is used with the old +//! TAP handshake. It is ugly, it is based on SHA-1, and it should be +//! avoided for new uses. It is not even linked here as we will not use it in +//! this obfuscated protocol implementation. +//! +//! The *HKDF-SHA256* KDF (implemented by `Ntor1Kdf`) is used with the +//! Ntor handshake. It is based on RFC5869 and SHA256. +//! +//! The *SHAKE* KDF (implemented by `ShakeKdf` is used with v3 onion +//! services, and is likely to be used by other places in the future. +//! It is based on SHAKE-256. + +use crate::{Error, Result}; + +use digest::{ExtendableOutput, Update, XofReader}; +use tor_bytes::SecretBuf; +use tor_llcrypto::d::{Sha256, Shake256}; + +/// A trait for a key derivation function. +pub(crate) trait Kdf { + /// Derive `n_bytes` of key data from some secret `seed`. + fn derive(&self, seed: &[u8], n_bytes: usize) -> Result; +} + +/// A parameterized KDF, for use with ntor. +/// +/// This KDF is based on HKDF-SHA256. +pub(crate) struct Ntor1Kdf<'a, 'b> { + /// A constant for parameterizing the kdf, during the key extraction + /// phase. + t_key: &'a [u8], + /// Another constant for parameterizing the kdf, during the key + /// expansion phase. + m_expand: &'b [u8], +} + +/// A modern KDF, for use with v3 onion services. +/// +/// This KDF is based on SHAKE256 +pub(crate) struct ShakeKdf(); + +impl<'a, 'b> Ntor1Kdf<'a, 'b> { + /// Instantiate an Ntor1Kdf, with given values for t_key and m_expand. + pub(crate) fn new(t_key: &'a [u8], m_expand: &'b [u8]) -> Self { + Ntor1Kdf { t_key, m_expand } + } +} + +impl Kdf for Ntor1Kdf<'_, '_> { + fn derive(&self, seed: &[u8], n_bytes: usize) -> Result { + let hkdf = hkdf::Hkdf::::new(Some(self.t_key), seed); + + let mut result: SecretBuf = vec![0; n_bytes].into(); + hkdf.expand(self.m_expand, result.as_mut()) + .map_err(|_| Error::InvalidKDFOutputLength)?; + Ok(result) + } +} + +impl ShakeKdf { + /// Instantiate a ShakeKdf. + pub(crate) fn new() -> Self { + ShakeKdf() + } +} +impl Kdf for ShakeKdf { + fn derive(&self, seed: &[u8], n_bytes: usize) -> Result { + let mut xof = Shake256::default(); + xof.update(seed); + let mut result: SecretBuf = vec![0; n_bytes].into(); + xof.finalize_xof().read(result.as_mut()); + Ok(result) + } +} + +#[cfg(test)] +// @@ begin test lint list maintained by maint/add_warning @@ +#[allow(clippy::bool_assert_comparison)] +#[allow(clippy::clone_on_copy)] +#[allow(clippy::dbg_macro)] +#[allow(clippy::print_stderr)] +#[allow(clippy::print_stdout)] +#[allow(clippy::single_char_pattern)] +#[allow(clippy::unwrap_used)] +#[allow(clippy::unchecked_duration_subtraction)] +#[allow(clippy::useless_vec)] +#[allow(clippy::needless_pass_by_value)] +// +mod test { + use super::*; + use hex_literal::hex; + + #[test] + fn clearbox_ntor1_kdf() { + // Calculate Ntor1Kdf, and make sure we get the same result by + // following the calculation in the spec. + let input = b"another example key seed that we will expand"; + let result = Ntor1Kdf::new(&b"key"[..], &b"expand"[..]) + .derive(input, 99) + .unwrap(); + + let kdf = hkdf::Hkdf::::new(Some(&b"key"[..]), &input[..]); + let mut expect_result = vec![0_u8; 99]; + kdf.expand(&b"expand"[..], &mut expect_result[..]).unwrap(); + + assert_eq!(&expect_result[..], &result[..]); + } + + #[test] + fn testvec_ntor1_kdf() { + // From Tor's test_crypto.c; generated with ntor_ref.py + fn expand(b: &[u8]) -> SecretBuf { + let t_key = b"ntor-curve25519-sha256-1:key_extract"; + let m_expand = b"ntor-curve25519-sha256-1:key_expand"; + Ntor1Kdf::new(&t_key[..], &m_expand[..]) + .derive(b, 100) + .unwrap() + } + + let expect = hex!( + "5521492a85139a8d9107a2d5c0d9c91610d0f95989975ebee6 + c02a4f8d622a6cfdf9b7c7edd3832e2760ded1eac309b76f8d + 66c4a3c4d6225429b3a016e3c3d45911152fc87bc2de9630c3 + 961be9fdb9f93197ea8e5977180801926d3321fa21513e59ac" + ); + assert_eq!(&expand(&b"Tor"[..])[..], &expect[..]); + + let brunner_quote = b"AN ALARMING ITEM TO FIND ON YOUR CREDIT-RATING STATEMENT"; + let expect = hex!( + "a2aa9b50da7e481d30463adb8f233ff06e9571a0ca6ab6df0f + b206fa34e5bc78d063fc291501beec53b36e5a0e434561200c + 5f8bd13e0f88b3459600b4dc21d69363e2895321c06184879d + 94b18f078411be70b767c7fc40679a9440a0c95ea83a23efbf" + ); + assert_eq!(&expand(&brunner_quote[..])[..], &expect[..]); + } + + #[test] + fn testvec_shake_kdf() { + // This is just one of the shake test vectors from tor-llcrypto + let input = hex!( + "76891a7bcc6c04490035b743152f64a8dd2ea18ab472b8d36ecf45 + 858d0b0046" + ); + let expected = hex!( + "e8447df87d01beeb724c9a2a38ab00fcc24e9bd17860e673b02122 + 2d621a7810e5d3" + ); + + let result = ShakeKdf::new().derive(&input[..], expected.len()); + assert_eq!(&result.unwrap()[..], &expected[..]); + } +} diff --git a/crates/o5/src/common/mlkem1024_x25519.rs b/crates/o5/src/common/mlkem1024_x25519.rs new file mode 100644 index 0000000..414f1c5 --- /dev/null +++ b/crates/o5/src/common/mlkem1024_x25519.rs @@ -0,0 +1,324 @@ +//! Combined Kyber (ML-KEM) 1024 and X25519 Hybrid Public Key Encryption (HPKE) scheme. +//! +//! > For the client's share, the key_exchange value contains the +//! > concatenation of the client's X25519 ephemeral share (32 bytes) and +//! > the client's Kyber768Draft00 public key (1184 bytes). The resulting +//! > key_exchange value is 1216 bytes in length. +//! > +//! > For the server's share, the key_exchange value contains the +//! > concatenation of the server's X25519 ephemeral share (32 bytes) and +//! > the Kyber768Draft00 ciphertext (1088 bytes) returned from +//! > encapsulation for the client's public key. The resulting +//! > key_exchange value is 1120 bytes in length. +//! > +//! > The shared secret is calculated as the concatenation of the X25519 +//! > shared secret (32 bytes) and the Kyber768Draft00 shared secret (32 +//! > bytes). The resulting shared secret value is 64 bytes in length. + +use kem::{Decapsulate, Encapsulate}; +use kemeleon::{DecapsulationKey, EncapsulationKey, Encode, OKemCore}; +use rand::{CryptoRng, RngCore}; +use rand_core::CryptoRngCore; +use subtle::ConstantTimeEq; + +use crate::{Error, Result}; + +pub(crate) use kemeleon::EncodeError; + +pub(crate) const X25519_PUBKEY_LEN: usize = 32; +pub(crate) const X25519_PRIVKEY_LEN: usize = 32; +pub(crate) const MLKEM1024_PUBKEY_LEN: usize = 1530; +pub(crate) const PUBKEY_LEN: usize = MLKEM1024_PUBKEY_LEN + X25519_PUBKEY_LEN; +pub(crate) const PRIVKEY_LEN: usize = 1; + +pub struct StaticSecret(HybridKey); + +struct HybridKey { + x25519: x25519_dalek::StaticSecret, + mlkem: DecapsulationKey, + pub_key: PublicKey, +} + +#[derive(Clone, PartialEq)] +pub(crate) struct PublicKey { + x25519: x25519_dalek::PublicKey, + mlkem: EncapsulationKey, + pub_key: [u8; PUBKEY_LEN], +} + +impl StaticSecret { + pub fn random_from_rng(rng: &mut R) -> Self { + Self(HybridKey::new(rng)) + } + + // TODO: THIS NEEDS TESTED + pub fn try_from_bytes(bytes: impl AsRef<[u8]>) -> Result { + let buf = bytes.as_ref(); + + let sk: [u8; X25519_PRIVKEY_LEN] = core::array::from_fn(|i| buf[i]); + let x25519 = x25519_dalek::StaticSecret::from(sk); + + let mlkem = DecapsulationKey::from_fips_bytes(&buf[X25519_PRIVKEY_LEN..]) + .map_err(|e| Error::EncodeError(e.into()))?; + + let pubkey_buf: [u8; PUBKEY_LEN] = core::array::from_fn(|i| buf[PRIVKEY_LEN + i]); + let pub_key = PublicKey::try_from(pubkey_buf)?; + + Ok(Self(HybridKey { + pub_key, + mlkem, + x25519, + })) + } + + pub fn as_bytes(&self) -> [u8; PRIVKEY_LEN + PUBKEY_LEN] { + let mut out = [0u8; PRIVKEY_LEN + PUBKEY_LEN]; + out[..X25519_PRIVKEY_LEN].copy_from_slice(&self.0.x25519.to_bytes()[..]); + out[X25519_PRIVKEY_LEN..PRIVKEY_LEN].copy_from_slice(&self.0.mlkem.to_fips_bytes()[..]); + out[PRIVKEY_LEN..PRIVKEY_LEN + PUBKEY_LEN].copy_from_slice(&self.0.pub_key.as_bytes()); + out + } + + pub fn with_pub<'a>(&'a self, pubkey: &'a PublicKey) -> KeyMix<'a> { + self.0.with_pub(pubkey) + } + + /// Hybrid Public Key Encryption (HPKE) handshake for ML-KEM1024 + X25519 + /// + /// This is a custom interface for now as there isn't an example interface that I am aware of. + /// (Read - this will likely change in the future) + pub fn hpke( + &self, + rng: &mut R, + pubkey: &PublicKey, + ) -> Result<(Ciphertext, SharedSecret)> { + self.with_pub(&pubkey) + .encapsulate(rng) + .map_err(|e| Error::Crypto(e.to_string())) + } +} + +impl core::fmt::Debug for PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.as_bytes())) + } +} + +impl PublicKey { + pub fn as_bytes(&self) -> &[u8] { + &self.pub_key + } +} + +impl From<&StaticSecret> for PublicKey { + fn from(value: &StaticSecret) -> Self { + value.0.public_key().clone() + } +} + +impl TryFrom<[u8; PUBKEY_LEN]> for PublicKey { + type Error = Error; + fn try_from(value: [u8; PUBKEY_LEN]) -> Result { + let mut x25519 = [0u8; X25519_PUBKEY_LEN]; + x25519.copy_from_slice(&value[..X25519_PUBKEY_LEN]); + + let mlkem = EncapsulationKey::try_from_bytes(&value[X25519_PUBKEY_LEN..]) + .map_err(|e| Error::EncodeError(e.into()))?; + + Ok(Self { + x25519: x25519.into(), + mlkem, + pub_key: value, + }) + } +} + +pub struct SharedSecret { + x25519: x25519_dalek::SharedSecret, + x25519_raw: [u8; 32], + mlkem: [u8; 32], +} + +impl PartialEq for SharedSecret { + fn eq(&self, other: &Self) -> bool { + self.x25519_raw.ct_eq(&other.x25519_raw).into() && self.mlkem.ct_eq(&other.mlkem).into() + } +} + +impl SharedSecret { + // TODO: Test this layout to make sure this works. + // SAFETY: the type of the SharedSecret object means this should never fail + pub fn as_bytes(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.x25519_raw.as_ptr(), 64) } + } + + /// Ensure in constant-time that the x25519 portion of this shared secret did not result + /// from a key exchange with non-contributory behaviour. + pub fn was_contributory(&self) -> bool { + self.x25519.was_contributory() + } +} + +impl core::fmt::Debug for SharedSecret { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{} {}", + hex::encode(self.x25519_raw), + hex::encode(self.mlkem) + ) + } +} + +impl HybridKey { + fn new(rng: &mut R) -> Self { + let (dk, ek) = kemeleon::MlKem1024::generate(rng); + let x25519 = x25519_dalek::StaticSecret::random_from_rng(rng); + let x25519_pub = x25519_dalek::PublicKey::from(&x25519); + let mut pub_key = [0u8; PUBKEY_LEN]; + pub_key[..X25519_PUBKEY_LEN].copy_from_slice(x25519_pub.as_bytes()); + pub_key[X25519_PUBKEY_LEN..].copy_from_slice(&ek.as_bytes()); + + Self { + pub_key: PublicKey { + x25519: x25519_dalek::PublicKey::from(&x25519), + mlkem: ek, + pub_key, + }, + mlkem: dk, + x25519, + } + } + + fn public_key(&self) -> &PublicKey { + &self.pub_key + } + + fn with_pub<'a>(&'a self, pubkey: &'a PublicKey) -> KeyMix<'a> { + KeyMix { + local_private: self, + remote_public: pubkey, + } + } +} + +pub struct KeyMix<'a> { + local_private: &'a HybridKey, + remote_public: &'a PublicKey, +} + +impl Encapsulate for KeyMix<'_> { + type Error = EncodeError; + + // Diffie Helman / Encapsulate + fn encapsulate( + &self, + rng: &mut impl CryptoRngCore, + ) -> std::result::Result<(Ciphertext, SharedSecret), Self::Error> { + let (ciphertext, local_ss_mlkem) = self.remote_public.mlkem.encapsulate(rng).unwrap(); + let local_ss_x25519 = self + .local_private + .x25519 + .diffie_hellman(&self.remote_public.x25519); + let ss = SharedSecret { + x25519_raw: (&local_ss_x25519).to_bytes(), + mlkem: local_ss_mlkem.into(), + x25519: local_ss_x25519, + }; + let mut ct = x25519_dalek::PublicKey::from(&self.local_private.x25519) + .as_bytes() + .to_vec(); + ct.append(&mut ciphertext.as_bytes().to_vec()); + Ok((ct, ss)) + } +} + +pub type Ciphertext = Vec; + +impl Decapsulate for HybridKey { + type Error = EncodeError; + + // Required method + fn decapsulate( + &self, + encapsulated_key: &Ciphertext, + ) -> std::result::Result { + let arr = kemeleon::Ciphertext::try_from(&encapsulated_key[32..])?; + let local_ss_mlkem = self.mlkem.decapsulate(&arr)?; + + let mut remote_public = [0u8; 32]; + remote_public[..32].copy_from_slice(&encapsulated_key[..32]); + let local_ss_x25519 = self + .x25519 + .diffie_hellman(&x25519_dalek::PublicKey::from(remote_public)); + + Ok(SharedSecret { + x25519_raw: (&local_ss_x25519).to_bytes(), + mlkem: local_ss_mlkem.into(), + x25519: local_ss_x25519, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use kemeleon::MlKem1024; + + #[test] + fn example_lib_usage() { + let rng = &mut rand::thread_rng(); + let alice_priv_key = HybridKey::new(rng); + let alice_pub = alice_priv_key.public_key(); + + let bob_priv_key = HybridKey::new(rng); + let (ct, bob_ss) = bob_priv_key.with_pub(alice_pub).encapsulate(rng).unwrap(); + + let alice_ss = alice_priv_key.decapsulate(&ct).unwrap(); + assert_eq!(alice_ss, bob_ss); + } + + #[test] + fn it_works() { + let mut rng = rand::thread_rng(); + + // --- Generate Keypair (Alice) --- + // x25519 + let alice_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let alice_public = x25519_dalek::PublicKey::from(&alice_secret); + // mlkem + let (alice_mlkem_dk, alice_mlkem_ek) = MlKem1024::generate(&mut rng); + + // --- alice -> bob (public keys) --- + // alice sends bob the public key for her mlkem1024 keypair with her + // x25519 key appended to the end. + let mut mlkem1024x_pubkey = alice_public.as_bytes().to_vec(); + mlkem1024x_pubkey.extend_from_slice(&alice_mlkem_ek.as_bytes()); + + assert_eq!(mlkem1024x_pubkey.len(), 1562); + + // --- Generate Keypair (Bob) --- + // x25519 + let bob_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let bob_public = x25519_dalek::PublicKey::from(&bob_secret); + + // (Imagine) upon receiving the mlkemx25519 public key bob parses them + // into their respective structs from bytes + + // Bob encapsulates a shared secret using Alice's public key + let (ciphertext, shared_secret_bob) = alice_mlkem_ek.encapsulate(&mut rng).unwrap(); + let bob_shared_secret = bob_secret.diffie_hellman(&alice_public); + + // // Alice decapsulates a shared secret using the ciphertext sent by Bob + let shared_secret_alice = alice_mlkem_dk.decapsulate(&ciphertext).unwrap(); + let alice_shared_secret = alice_secret.diffie_hellman(&bob_public); + + assert_eq!(alice_shared_secret.as_bytes(), bob_shared_secret.as_bytes()); + assert_eq!(shared_secret_bob, shared_secret_alice); + println!( + "{} ?= {}", + hex::encode(shared_secret_bob), + hex::encode(shared_secret_alice) + ); + } +} diff --git a/crates/o5/src/common/mod.rs b/crates/o5/src/common/mod.rs new file mode 100644 index 0000000..b3a1e9b --- /dev/null +++ b/crates/o5/src/common/mod.rs @@ -0,0 +1,38 @@ +use crate::Result; + +use colored::Colorize; +use hmac::Hmac; +use sha2::Sha256; + +pub(crate) mod ct; +pub(crate) mod kdf; +pub(crate) mod utils; + +mod skip; +pub use skip::discard; + +pub mod drbg; +pub mod mlkem1024_x25519; +pub mod ntor_arti; +pub mod probdist; +pub mod replay_filter; +pub mod x25519_elligator2; + +pub trait ArgParse { + type Output; + + fn parse_args() -> Result; +} + +pub(crate) type HmacSha256 = Hmac; + +pub(crate) fn colorize(b: impl AsRef<[u8]>) -> String { + let id = b.as_ref(); + if id.len() < 3 { + return hex::encode(id); + } + let r = id[0]; + let g = id[1]; + let b = id[2]; + hex::encode(id).truecolor(r, g, b).to_string() +} diff --git a/crates/o5/src/common/ntor_arti.rs b/crates/o5/src/common/ntor_arti.rs new file mode 100644 index 0000000..897f429 --- /dev/null +++ b/crates/o5/src/common/ntor_arti.rs @@ -0,0 +1,234 @@ +//! Generic Handshake for Tor. Extension of Tor circuit creation handshake design. +//! +//! Tor circuit handshakes all implement a one-way-authenticated key +//! exchange, where a client that knows a public "onion key" for a +//! relay sends a "client onionskin" to extend to a relay, and receives a +//! "relay onionskin" in response. When the handshake is successful, +//! both the client and relay share a set of session keys, and the +//! client knows that nobody _else_ shares those keys unless they +//! relay's private onion key. +//! +//! Currently, this module implements only the "ntor" handshake used +//! for circuits on today's Tor. +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use crate::{ + common::{colorize, mlkem1024_x25519}, + Error, Result, +}; +//use zeroize::Zeroizing; +use tor_bytes::SecretBuf; + +pub const SESSION_ID_LEN: usize = 8; +#[derive(PartialEq, PartialOrd, Clone, Copy)] +pub struct SessionID([u8; SESSION_ID_LEN]); + +impl core::fmt::Display for SessionID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}]", colorize(hex::encode(&self.0))) + } +} + +impl core::fmt::Debug for SessionID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} + +impl From<[u8; SESSION_ID_LEN]> for SessionID { + fn from(value: [u8; SESSION_ID_LEN]) -> Self { + SessionID(value) + } +} + +impl TryFrom<&[u8]> for SessionID { + type Error = Error; + fn try_from(buf: &[u8]) -> Result { + if buf.len() < SESSION_ID_LEN { + return Err( + IoError::new(IoErrorKind::InvalidInput, "too few bytes for session id").into(), + ); + } + let v: [u8; SESSION_ID_LEN] = core::array::from_fn(|i| buf[i]); + Ok(SessionID(v)) + } +} + +pub trait ClientHandshakeMaterials { + /// The type for the onion key. + type IdentityKeyType; + /// Type of extra data sent from client (without forward secrecy). + type ClientAuxData: ?Sized; + + fn node_pubkey(&self) -> &Self::IdentityKeyType; + fn aux_data(&self) -> Option<&Self::ClientAuxData>; +} + +/// A ClientHandshake is used to generate a client onionskin and +/// handle a relay onionskin. +pub trait ClientHandshake { + type HandshakeMaterials: ClientHandshakeMaterials; + /// The type for the state that the client holds while waiting for a reply. + type StateType; + /// A type that is returned and used to generate session keys.x + type KeyGen; + /// Type of extra data returned by server (without forward secrecy). + type ServerAuxData; + + /// Generate a new client onionskin for a relay with a given onion key, + /// including `client_aux_data` to be sent without forward secrecy. + /// + /// On success, return a state object that will be used to + /// complete the handshake, along with the message to send. + fn client1(materials: Self::HandshakeMaterials) -> Result<(Self::StateType, Vec)>; + /// Handle an onionskin from a relay, and produce aux data returned + /// from the server, and a key generator. + /// + /// The state object must match the one that was used to make the + /// client onionskin that the server is replying to. + fn client2>( + state: Self::StateType, + msg: T, + ) -> Result<(Self::ServerAuxData, Self::KeyGen)>; +} + +/// Trait for an object that handles incoming auxiliary data and +/// returns the server's auxiliary data to be included in the reply. +/// +/// This is implemented for `FnMut(&H::ClientAuxData) -> Option` automatically. +pub(crate) trait AuxDataReply +where + H: ServerHandshake + ?Sized, +{ + /// Given a list of extensions received from a client, decide + /// what extensions to send in reply. + /// + /// Return None if the handshake should fail. + fn reply(&mut self, msg: &H::ClientAuxData) -> Option; +} + +impl AuxDataReply for F +where + H: ServerHandshake + ?Sized, + F: FnMut(&H::ClientAuxData) -> Option, +{ + fn reply(&mut self, msg: &H::ClientAuxData) -> Option { + self(msg) + } +} + +/// A ServerHandshake is used to handle a client onionskin and generate a +/// server onionskin. It is assumed that the (long term identity) keys are stored +/// as part of the object implementing this trait. +pub(crate) trait ServerHandshake { + /// Custom parameters used per handshake rather than long lived config stored + /// in the object implementing this trait. + type HandshakeParams; + /// The returned key generator type. + type KeyGen; + /// Type of extra data sent from client (without forward secrecy). + type ClientAuxData: ?Sized; + /// Type of extra data returned by server (without forward secrecy). + type ServerAuxData; + + /// Perform the server handshake. Take as input a function for processing + /// requested extensions, a slice of all our private onion keys, and the + /// client's message. + /// + /// On success, return a key generator and a server handshake message + /// to send in reply. + /// + /// The self parameter is a type / struct for (potentially shared) state + /// accessible during the server handshake. + fn server, T: AsRef<[u8]>>( + &self, + reply_fn: &mut REPLY, + materials: &Self::HandshakeParams, + msg: T, + ) -> RelayHandshakeResult<(Self::KeyGen, Vec)>; +} + +/// A KeyGenerator is returned by a handshake, and used to generate +/// session keys for the protocol. +/// +/// Typically, it wraps a KDF function, and some seed key material. +/// +/// It can only be used once. +#[allow(unreachable_pub)] // This is only exported depending on enabled features. +pub trait KeyGenerator { + /// Consume the key + fn expand(self, keylen: usize) -> Result; +} + +pub trait SessionIdentifier { + type ID: core::fmt::Display + core::fmt::Debug + PartialEq; + fn session_id(&mut self) -> Self::ID; +} + +/// Generates keys based on SHAKE-256. +pub(crate) struct ShakeKeyGenerator { + /// Seed for the key generator + seed: SecretBuf, +} + +impl ShakeKeyGenerator { + /// Create a key generator based on a provided seed + #[allow(dead_code)] // We'll construct these for v3 onion services + pub(crate) fn new(seed: SecretBuf) -> Self { + ShakeKeyGenerator { seed } + } +} + +impl KeyGenerator for ShakeKeyGenerator { + fn expand(self, keylen: usize) -> Result { + use crate::common::kdf::{Kdf, ShakeKdf}; + ShakeKdf::new().derive(&self.seed[..], keylen) + } +} + +/// An error produced by a Relay's attempt to handle a client's onion handshake. +#[derive(Debug, thiserror::Error)] +pub enum RelayHandshakeError { + /// Occurs when a check did not fail, but requires updated input from the + /// calling context. For example, a handshake that requires more bytes to + /// before it can succeed or fail. + #[error("try again with updated input")] + EAgain, + + /// An error in parsing a handshake message. + #[error("Problem decoding onion handshake")] + Fmt(#[from] tor_bytes::Error), + + /// Error happened during cryptographic handshake + #[error("")] + CryptoError(mlkem1024_x25519::EncodeError), + + /// The client asked for a key we didn't have. + #[error("Client asked for a key or ID that we don't have")] + MissingKey, + + /// The client did something wrong with their handshake or cryptography. + #[error("Bad handshake from client")] + BadClientHandshake, + + /// The server did something wrong with their handshake or cryptography or + /// an otherwise invalid response was received + #[error("Bad handshake from server")] + BadServerHandshake, + + /// The client's handshake matched a previous handshake indicating a potential replay attack. + #[error("Handshake from client was seen recently -- potentially replayed.")] + ReplayedHandshake, + + /// Error occured while creating a frame. + #[error("Problem occured while building handshake")] + FrameError(String), + + /// An internal error. + #[error("Internal error")] + Internal(#[from] tor_error::Bug), +} + +/// Type alias for results from a relay's attempt to handle a client's onion +/// handshake. +pub(crate) type RelayHandshakeResult = std::result::Result; diff --git a/crates/o5/src/common/probdist.rs b/crates/o5/src/common/probdist.rs new file mode 100644 index 0000000..d2a176a --- /dev/null +++ b/crates/o5/src/common/probdist.rs @@ -0,0 +1,268 @@ +//! The probdist module implements a weighted probability distribution suitable for +//! protocol parameterization. To allow for easy reproduction of a given +//! distribution, the drbg package is used as the random number source. + +use crate::common::drbg; + +use std::cmp::{max, min}; +use std::fmt; +use std::sync::{Arc, Mutex}; + +use rand::{seq::SliceRandom, Rng}; + +const MIN_VALUES: i32 = 1; +const MAX_VALUES: i32 = 100; + +/// A weighted distribution of integer values. +#[derive(Clone)] +pub struct WeightedDist(Arc>); + +struct InnerWeightedDist { + min_value: i32, + max_value: i32, + biased: bool, + + values: Vec, + weights: Vec, + + alias: Vec, + prob: Vec, +} + +impl WeightedDist { + /// New creates a weighted distribution of values ranging from min to max + /// based on a HashDrbg initialized with seed. Optionally, bias the weight + /// generation to match the ScrambleSuit non-uniform distribution from + /// obfsproxy. + pub fn new(seed: drbg::Seed, min: i32, max: i32, biased: bool) -> Self { + let w = WeightedDist(Arc::new(Mutex::new(InnerWeightedDist { + min_value: min, + max_value: max, + biased, + values: vec![], + weights: vec![], + alias: vec![], + prob: vec![], + }))); + let _ = &w.reseed(seed); + + w + } + + /// Generates a random value according to the generated distribution. + pub fn sample(&self) -> i32 { + let dist = self.0.lock().unwrap(); + + let mut buf = [0_u8; 8]; + // Generate a fair die roll fro a $n$-sided die; call the side $i$. + getrandom::getrandom(&mut buf).unwrap(); + + #[cfg(target_pointer_width = "64")] + let i = usize::from_ne_bytes(buf) % dist.values.len(); + + #[cfg(target_pointer_width = "32")] + let i = usize::from_ne_bytes(buf[0..4].try_into().unwrap()) % dist.values.len(); + + // flip a coin that comes up heads with probability $prob[i]$. + getrandom::getrandom(&mut buf).unwrap(); + let f = f64::from_ne_bytes(buf); + if f < dist.prob[i] { + // if the coin comes up "heads", use $i$ + dist.min_value + dist.values[i] + } else { + // otherwise use $alias[i]$. + dist.min_value + dist.values[dist.alias[i]] + } + } + + /// Generates a new distribution with the same min/max based on a new seed. + pub fn reseed(&self, seed: drbg::Seed) { + let mut drbg = drbg::Drbg::new(Some(seed)).unwrap(); + + let mut dist = self.0.lock().unwrap(); + dist.gen_values(&mut drbg); + if dist.biased { + dist.gen_biased_weights(&mut drbg); + } else { + dist.gen_uniform_weights(&mut drbg); + } + dist.gen_tables(); + } +} + +impl InnerWeightedDist { + // Creates a slice containing a random number of random values that, when + // scaled by adding self.min_value, will fall into [min, max]. + fn gen_values(&mut self, rng: &mut R) { + let mut n_values = self.max_value - self.min_value; + + let mut values: Vec = (0..=n_values).collect(); + values.shuffle(rng); + n_values = max(n_values, MIN_VALUES); + n_values = min(n_values, MAX_VALUES); + + let n_values = rng.gen_range(1..=n_values) as usize; + self.values = values[..n_values].to_vec(); + } + + // generates a non-uniform weight list, similar to the scramblesuit + // prob_dist mode. + fn gen_biased_weights(&mut self, rng: &mut R) { + self.weights = vec![0_f64; self.values.len()]; + + let mut cumul_prob: f64 = 0.0; + for i in 0..self.weights.len() { + self.weights[i] = (1.0 - cumul_prob) * rng.gen::(); + cumul_prob += self.weights[i]; + } + } + + // generates a uniform weight list. + fn gen_uniform_weights(&mut self, rng: &mut R) { + self.weights = vec![0_f64; self.values.len()]; + + for i in 0..self.weights.len() { + self.weights[i] = rng.gen(); + } + } + + // Calculates the alias and prob tables use for Vose's alias Method. + // Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/ + fn gen_tables(&mut self) { + let n = self.weights.len(); + let sum: f64 = self.weights.iter().sum(); + + let mut alias = vec![0_usize; n]; + let mut prob = vec![0_f64; n]; + + // multiply each probability by $n$. + let mut scaled: Vec = self.weights.iter().map(|f| f * (n as f64) / sum).collect(); + // if $p$ < 1$ add $i$ to $small$. + let mut small: Vec = scaled + .iter() + .enumerate() + .filter(|(_, f)| **f < 1.0) + .map(|(i, _)| i) + .collect(); + // if $p$ >= 1$ add $i& to $large$. + let mut large: Vec = scaled + .iter() + .enumerate() + .filter(|(_, f)| **f >= 1.0) + .map(|(i, _)| i) + .collect(); + + // While $small$ and $large$ are not empty: ($large$ might be emptied first) + // remove the first element from $small$ and call it $l$. + // remove the first element from $large$ and call it $g$. + // set $prob[l] = p_l$ + // set $alias[l] = g$ + // set $p_g = (p_g+p_l) - 1$ (This is a more numerically stable option) + // if $p_g < 1$ add $g$ to $small$. + // otherwise add $g$ to $large$ as %p_g >= 1$ + while !small.is_empty() && !large.is_empty() { + let l = small.remove(0); + let g = large.remove(0); + + prob[l] = scaled[l]; + alias[l] = g; + + scaled[g] = scaled[g] + scaled[l] - 1.0; + if scaled[g] < 1.0 { + small.push(g); + } else { + large.push(g); + } + } + + // while $large$ is not empty, remove the first element ($g$) and + // set $prob[g] = 1$. + while !large.is_empty() { + prob[large.remove(0)] = 1.0; + } + + // while $small$ is not empty, remove the first element ($l$) and + // set $prob[l] = 1$. + while !small.is_empty() { + prob[small.remove(0)] = 1.0; + } + + self.prob = prob; + self.alias = alias; + } +} + +impl fmt::Display for WeightedDist { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let dist = self.0.lock().unwrap(); + write!(f, "{dist}") + } +} + +impl fmt::Display for InnerWeightedDist { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut buf: String = "[ ".into(); + + for (i, v) in self.values.iter().enumerate() { + let p = self.weights[i]; + if p > 0.01 { + buf.push_str(&format!("{v}: {p}, ")); + } + } + write!(f, "]") + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::init_subscriber; + use crate::Result; + + use ptrs::trace; + use tracing::{span_enabled, Level}; + + #[test] + fn weighted_dist_uniformity() -> Result<()> { + init_subscriber(); + let seed = drbg::Seed::new()?; + + let n_trials = 1_000_000; + let mut hist = [0_usize; 1000]; + + let w = WeightedDist::new(seed, 0, 999, true); + + if span_enabled!(Level::TRACE) { + trace!("Table:"); + trace!("{w}"); + let wi = w.0.lock().unwrap(); + let sum: f64 = wi.weights.iter().sum(); + let min_value = wi.min_value; + let values = &wi.values; + + for (i, weight) in wi.weights.iter().enumerate() { + let p = weight / sum; + if p > 0.000001 { + // filter out tiny values + trace!(" [{}]: {p}", min_value + values[i]); + } + } + } + + for _ in 0..n_trials { + let idx: usize = w.sample().try_into().unwrap(); + hist[idx] += 1; + } + + if span_enabled!(Level::TRACE) { + trace!("Generated:"); + for (val, count) in hist.iter().enumerate() { + if *count != 0 { + trace!(" {val}: {:} ({count})", *count as f64 / n_trials as f64); + } + } + } + + Ok(()) + } +} diff --git a/crates/o5/src/common/replay_filter.rs b/crates/o5/src/common/replay_filter.rs new file mode 100644 index 0000000..400484f --- /dev/null +++ b/crates/o5/src/common/replay_filter.rs @@ -0,0 +1,255 @@ +use ptrs::trace; +/// The replayfilter module implements a generic replay detection filter with a +/// caller specifiable time-to-live. It only detects if a given byte sequence +/// has been seen before based on the SipHash-2-4 digest of the sequence. +/// Collisions are treated as positive matches, though the probability of this +/// happening is negligible. +use siphasher::{prelude::*, sip::SipHasher24}; + +use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +// maxFilterSize is the maximum capacity of a replay filter. This value is +// more as a safeguard to prevent runaway filter growth, and is sized to be +// serveral orders of magnitude greater than the number of connections a busy +// bridge sees in one day, so in practice should never be reached. +const MAX_FILTER_SIZE: usize = 100 * 1024; + +#[derive(Clone, PartialEq)] +struct Entry { + digest: u64, + first_seen: Instant, +} + +// ReplayFilter is a simple filter designed only to detect if a given byte +// sequence has been seen in the recent history. +pub struct ReplayFilter(Arc>); + +impl ReplayFilter { + pub fn new(ttl: Duration) -> Self { + Self(Arc::new(Mutex::new(InnerReplayFilter::new( + ttl, + MAX_FILTER_SIZE, + )))) + } + + // Queries the filter for a given byte sequence, inserts the + // sequence, and returns if it was present before the insertion operation. + pub fn test_and_set(&self, now: Instant, buf: impl AsRef<[u8]>) -> bool { + let mut inner = self.0.lock().unwrap(); + inner.test_and_set(now, buf) + } +} + +struct InnerReplayFilter { + filter: HashMap, + fifo: VecDeque, + + key: [u8; 16], + ttl_limit: Duration, + max_cap: usize, +} + +impl InnerReplayFilter { + fn new(ttl_limit: Duration, max_cap: usize) -> Self { + let mut key = [0_u8; 16]; + getrandom::getrandom(&mut key).unwrap(); + + Self { + filter: HashMap::new(), + fifo: VecDeque::new(), + key, + ttl_limit, + max_cap, + } + } + + fn test_and_set(&mut self, now: Instant, buf: impl AsRef<[u8]>) -> bool { + self.garbage_collect(now); + + let mut hash = SipHasher24::new_with_key(&self.key); + let digest: u64 = { + hash.write(buf.as_ref()); + hash.finish().to_be() + }; + + trace!("checking inner"); + if self.filter.contains_key(&digest) { + return true; + } + + trace!("not found: {digest}... inserting"); + let e = Entry { + digest, + first_seen: now, + }; + + self.fifo.push_front(e.clone()); + self.filter.insert(digest, e); + + trace!("inserted: {}", self.filter.len()); + false + } + + fn garbage_collect(&mut self, now: Instant) { + if self.fifo.is_empty() { + return; + } + + while !self.fifo.is_empty() { + let e = match self.fifo.back() { + Some(e) => e, + None => return, + }; + + trace!( + "{}/{}[/{}] - {:?}", + self.fifo.len(), + self.filter.len(), + self.max_cap, + self.ttl_limit + ); + // If the filter is not full, only purge entries that have exceedded + // the TTL, otherwise purge one entry and test to see if we are + // still over max length. This should not (typically) be possible as + // we garbage collect on insert. + if self.fifo.len() < self.max_cap && self.ttl_limit > Duration::from_millis(0) { + let delta_t = now - e.first_seen; + trace!("{:?} > {:?}", now, e.first_seen); + if now < e.first_seen { + trace!("Invalid time"); + // Aeeeeeee, the system time jumped backwards, potentially by + // a lot. This will eventually self-correct, but "eventually" + // could be a long time. As much as this sucks, jettison the + // entire filter. + self.reset(); + return; + } else if delta_t < self.ttl_limit { + return; + } + } + + trace!("removing entry"); + // remove the entry + _ = self.filter.remove(&e.digest); + _ = self.fifo.pop_back(); + } + } + + fn reset(&mut self) { + trace!("RESETING"); + self.filter = HashMap::new(); + self.fifo = VecDeque::new(); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::init_subscriber; + use crate::Result; + + #[test] + fn replay_filter_ops() -> Result<()> { + init_subscriber(); + let ttl = Duration::from_secs(10); + + let f = &mut ReplayFilter::new(ttl); + + let buf = b"For a moment, nothing happened. Then, after a second or so, nothing continued to happen."; + let mut now = Instant::now(); + + // test_and_set into empty filter, returns false (not present). + assert!( + !f.test_and_set(now, buf), + "test_and_set (mutex) empty filter returned true" + ); + + // test_and_set into filter containing entry, should return true(present). + assert!( + f.test_and_set(now, buf), + "test_and_set (mutex) populated filter (replayed) returned false" + ); + + let f = &mut InnerReplayFilter::new(ttl, 2); + + // test_and_set into empty filter, returns false (not present). + assert!( + !f.test_and_set(now, buf), + "test_and_set empty filter returned true" + ); + + // test_and_set into filter containing entry, should return true(present). + assert!( + f.test_and_set(now, buf), + "test_and_set populated filter (replayed) returned false" + ); + + // test_and_set with time advanced. + let buf2 = b"We demand rigidly defined areas of doubt and uncertainty!"; + now += ttl; + assert!( + !f.test_and_set(now, buf2), + "test_and_set populated filter, 2nd entry returned true" + ); + assert!( + f.test_and_set(now, buf2), + "test_and_set populated filter, 2nd entry (replayed) returned false" + ); + + // Ensure that the first entry has been removed by compact. + assert!( + !f.test_and_set(now, buf), + "test_and_set populated filter, compact check returned true" + ); + + // Ensure that the filter gets reaped if the clock jumps backwards. + now = Instant::now(); + assert!( + !f.test_and_set(now, buf), + "test_and_set populated filter, backward time jump returned true" + ); + assert_eq!( + f.fifo.len(), + 1, + "filter fifo has a unexpected number of entries: {}", + f.fifo.len() + ); + assert_eq!( + f.filter.len(), + 1, + "filter map has a unexpected number of entries: {}", + f.filter.len() + ); + + // Ensure that the entry is properly added after reaping. + assert!( + f.test_and_set(now, buf), + "test_and_set populated filter, post-backward clock jump (replayed) returned false" + ); + + // Ensure that when the capacity limit is hit entries are evicted + f.test_and_set(now, "message2"); + for i in 0..10 { + assert_eq!( + f.fifo.len(), + 2, + "filter fifo has a unexpected number of entries: {}", + f.fifo.len() + ); + assert_eq!( + f.filter.len(), + 2, + "filter map has a unexpected number of entries: {}", + f.filter.len() + ); + assert!( + !f.test_and_set(now, &format!("message-1{i}")), + "unique message failed insert (returned true)" + ); + } + + Ok(()) + } +} diff --git a/crates/o5/src/common/skip.rs b/crates/o5/src/common/skip.rs new file mode 100644 index 0000000..0ecaf3a --- /dev/null +++ b/crates/o5/src/common/skip.rs @@ -0,0 +1,62 @@ +use crate::Result; + +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; + +use std::time::Duration; + +/// copies all data from the reader to a sink. If the reader closes before +/// the timeout due to na error or an EoF that result will be returned. +/// Otherwise if the timeout is reached, the stream will be shutdown +/// and the result of that shutdown will be returned. +/// +/// TODO: determine if it is possible to empty / flush write buffer before +/// shutdown to ensure consistent RST / FIN behavior on shutdown. +pub async fn discard(stream: S, d: Duration) -> Result<()> +where + S: AsyncRead + AsyncWrite + Unpin, +{ + let (mut r, mut w) = tokio::io::split(stream); + let result = tokio::time::timeout(d, async move { + tokio::io::copy(&mut r, &mut tokio::io::sink()).await + }) + .await; + if let Ok(r) = result { + // Error Occurred in coppy or connection hit EoF which means the + // connection should already be closed. + r.map(|_| ()).map_err(|e| e.into()) + } else { + // stream out -- connection may not be closed -- close manually. + w.shutdown().await.map_err(|e| e.into()) + } +} + +#[cfg(test)] +mod test { + use tokio::time::Instant; + + use super::*; + + #[tokio::test] + async fn discard_and_close_after_delay() { + let (mut c, s) = tokio::io::duplex(1024); + let start = Instant::now(); + let d = Duration::from_secs(3); + let expected_end = start + d; + let discard_fut = discard(s, d); + + tokio::spawn(async move { + const MSG: &str = "abcdefghijklmnopqrstuvwxyz"; + loop { + if let Err(e) = c.write(MSG.as_bytes()).await { + assert!(Instant::now() > expected_end); + println!("closed with error {e}"); + break; + } + } + }); + + discard_fut.await.unwrap(); + + assert!(Instant::now() > expected_end); + } +} diff --git a/crates/o5/src/common/utils.rs b/crates/o5/src/common/utils.rs new file mode 100644 index 0000000..fc89649 --- /dev/null +++ b/crates/o5/src/common/utils.rs @@ -0,0 +1,264 @@ +use crate::{constants::*, Result}; + +use std::time::{SystemTime, UNIX_EPOCH}; + +use rand_core::RngCore; +use subtle::ConstantTimeEq; + +use ptrs::trace; + +pub fn get_epoch_hour() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + / 3600 +} + +pub fn make_hs_pad(pad_len: usize) -> Result> { + trace!("[make_hs_pad] generating {pad_len}B"); + let mut pad = vec![u8::default(); pad_len]; + rand::thread_rng() + .try_fill_bytes(&mut pad) + .expect("rng failure"); + Ok(pad) +} + +pub fn find_mac_mark( + mark: [u8; MARK_LENGTH], + buf: impl AsRef<[u8]>, + start_pos: usize, + max_pos: usize, + from_tail: bool, +) -> Option { + let buffer = buf.as_ref(); + if buffer.len() < MARK_LENGTH { + return None; + } + trace!( + "finding mac mark: buf: {}B, {}-{}, from_tail: {}", + buffer.len(), + start_pos, + max_pos, + from_tail + ); + + if start_pos > buffer.len() { + return None; + } + + let mut end_pos = buffer.len(); + if end_pos > max_pos { + end_pos = max_pos; + } + + if end_pos - start_pos < MARK_LENGTH + MAC_LENGTH { + return None; + } + + let mut pos: usize; + if from_tail { + // The server can optimize the search process by only examining the + // tail of the buffer. The client can't send valid data past M_C | + // MAC_C as it does not have the server's public key yet. + pos = end_pos - (MARK_LENGTH + MAC_LENGTH); + // trace!("{pos}\n{}\n{}", hex::encode(mark), hex::encode(&buffer[pos..pos + MARK_LENGTH])); + if mark[..] + .ct_eq(buffer[pos..pos + MARK_LENGTH].as_ref()) + .into() + { + return Some(pos); + } + return None; + } + + // The client has to actually do a substring search since the server can + // and will send payload trailing the response. + // + // XXX: .windows().position() uses a naive search, which kind of sucks. + // but better algorithms (like `contains` for String) aren't implemented + // for byte slices in std. + pos = buffer[start_pos..end_pos] + .windows(MARK_LENGTH) + .position(|window| window.ct_eq(&mark[..]).into())?; + + // Ensure that there is enough trailing data for the MAC. + if start_pos + pos + MARK_LENGTH + MAC_LENGTH > end_pos { + return None; + } + + // Return the index relative to the start of the slice. + pos += start_pos; + Some(pos) +} + +#[cfg(test)] +mod test { + use super::*; + use bytes::Bytes; + + struct MacMarkTest { + mark: [u8; MARK_LENGTH], + buf: Vec, + start_pos: usize, + max_pos: usize, + from_tail: bool, + expected: Option, + } + + #[test] + fn find_mac_mark_thorough() -> Result<()> { + let cases = vec![ + MacMarkTest { + mark: [0_u8; MARK_LENGTH], + buf: vec![0_u8; 100], + start_pos: 0, + max_pos: 100, + from_tail: false, + expected: Some(0), + }, + MacMarkTest { + mark: hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(), + buf: hex::decode( + "00112233445566778899aabbccddeeff00000000000000000000000000000000", + ) + .unwrap(), + start_pos: 0, + max_pos: 100, + from_tail: false, + expected: Some(0), + }, + MacMarkTest { + // from tail + mark: hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(), + buf: hex::decode( + "00112233445566778899aabbccddeeff00000000000000000000000000000000", + ) + .unwrap(), + start_pos: 0, + max_pos: 100, + from_tail: true, + expected: Some(0), + }, + MacMarkTest { + // from tail not align with start + mark: hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(), + buf: hex::decode( + "000000112233445566778899aabbccddeeff00000000000000000000000000000000", + ) + .unwrap(), + start_pos: 0, + max_pos: 100, + from_tail: true, + expected: Some(2), + }, + MacMarkTest { + mark: hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(), + buf: hex::decode( + "000000112233445566778899aabbccddeeff00000000000000000000000000000000", + ) + .unwrap(), + start_pos: 0, + max_pos: 100, + from_tail: false, + expected: Some(2), + }, + MacMarkTest { + mark: hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(), + buf: hex::decode( + "00000000112233445566778899aabbccddeeff00000000000000000000000000000000", + ) + .unwrap(), + start_pos: 2, + max_pos: 100, + from_tail: false, + expected: Some(3), + }, + MacMarkTest { + // Not long enough to contain MAC + mark: hex::decode("00112233445566778899aabbccddeeff") + .unwrap() + .try_into() + .unwrap(), + buf: hex::decode("00112233445566778899aabbccddeeff").unwrap(), + start_pos: 0, + max_pos: 100, + from_tail: false, + expected: None, + }, + MacMarkTest { + // Access from tail success + mark: [0_u8; MARK_LENGTH], + buf: vec![0_u8; 100], + start_pos: 0, + max_pos: 100, + from_tail: true, + expected: Some(100 - MARK_LENGTH - MAC_LENGTH), + }, + MacMarkTest { + // from tail fail + mark: [0_u8; MARK_LENGTH], + buf: hex::decode( + "00112233445566778899aabbccddeeff00000000000000000000000000000000", + ) + .unwrap(), + start_pos: 0, + max_pos: 100, + from_tail: true, + expected: None, + }, + MacMarkTest { + // provided buf too short + mark: [0_u8; MARK_LENGTH], + buf: vec![0_u8; MARK_LENGTH - 1], + start_pos: 0, + max_pos: 100, + from_tail: false, + expected: None, + }, + MacMarkTest { + // provided buf cant contain mark and mac + mark: [0_u8; MARK_LENGTH], + buf: vec![0_u8; MARK_LENGTH + MAC_LENGTH - 1], + start_pos: 0, + max_pos: 100, + from_tail: false, + expected: None, + }, + ]; + + for m in cases { + let actual = find_mac_mark( + m.mark, + &Bytes::from(m.buf), + m.start_pos, + m.max_pos, + m.from_tail, + ); + assert_eq!(actual, m.expected); + } + + Ok(()) + } + + #[test] + fn epoch_format() { + let _h = format!("{}", get_epoch_hour()); + // println!("{h} {}", hex::encode(h.as_bytes())); + } +} diff --git a/crates/o5/src/common/x25519_elligator2.rs b/crates/o5/src/common/x25519_elligator2.rs new file mode 100644 index 0000000..85a9b2e --- /dev/null +++ b/crates/o5/src/common/x25519_elligator2.rs @@ -0,0 +1,403 @@ +//! Re-exporting Curve25519 implementations. +//! +//! *TODO*: Eventually we should probably recommend using this code via some +//! key-agreement trait, but for now we are just wrapping and re-using the APIs +//! from [`x25519_dalek`]. + +pub use curve25519_elligator2::{MapToPointVariant, Randomized}; +use getrandom::getrandom; +#[allow(unused)] +pub use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; + +/// Ephemeral Key for X25519 Handshakes which intentionally makes writing out the +/// private key value difficult. +/// +/// You can do a Diffie-Hellman exchange with this key multiple times and derive +/// the elligator2 representative, but you cannot write it out, as it is +/// intended to be used only ONCE (i.e. ephemerally). If the key generation +/// succeeds, the key is guaranteed to have a valid elligator2 representative. +#[derive(Clone)] +pub struct EphemeralSecret(x25519_dalek::StaticSecret, u8); + +#[allow(unused)] +impl EphemeralSecret { + pub fn random() -> Self { + Keys::random_ephemeral() + } + + pub fn random_from_rng(csprng: T) -> Self { + Keys::ephemeral_from_rng(csprng) + } + + pub fn diffie_hellman(&self, their_public: &PublicKey) -> SharedSecret { + self.0.diffie_hellman(their_public) + } + + #[cfg(test)] + /// As this function allows building an ['EphemeralSecret'] with a custom secret key, + /// it is not guaranteed to have a valid elligator2 representative. As such, it + /// is intended for testing purposes only. + pub(crate) fn from_parts(sk: StaticSecret, tweak: u8) -> Self { + Self(sk, tweak) + } +} + +impl From for PublicKey { + fn from(value: EphemeralSecret) -> Self { + let pk_bytes = Randomized::mul_base_clamped(value.0.to_bytes()).to_montgomery(); + PublicKey::from(*pk_bytes.as_bytes()) + } +} + +impl<'a> From<&'a EphemeralSecret> for PublicKey { + fn from(val: &'a EphemeralSecret) -> Self { + let pk_bytes = Randomized::mul_base_clamped(val.0.to_bytes()).to_montgomery(); + PublicKey::from(*pk_bytes.as_bytes()) + } +} + +/// [`PublicKey`] transformation to a format indistinguishable from uniform +/// random. +/// +/// This allows public keys to be sent over an insecure channel without +/// revealing that an x25519 public key is being shared. +/// +/// # Example +/// ``` +/// use o5::common::x25519_elligator2::{Keys, PublicRepresentative, PublicKey}; +/// +/// // Generate Alice's key pair. +/// let alice_secret = Keys::random_ephemeral(); +/// let alice_representative = PublicRepresentative::from(&alice_secret); +/// +/// // Generate Bob's key pair. +/// let bob_secret = Keys::random_ephemeral(); +/// let bob_representative = PublicRepresentative::from(&bob_secret); +/// +/// // Alice and Bob should now exchange their representatives and reveal the +/// // public key from the other person. +/// let bob_public = PublicKey::from(&bob_representative); +/// +/// let alice_public = PublicKey::from(&alice_representative); +/// +/// // Once they've done so, they may generate a shared secret. +/// let alice_shared = alice_secret.diffie_hellman(&bob_public); +/// let bob_shared = bob_secret.diffie_hellman(&alice_public); +/// +/// assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); +/// ``` +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +pub struct PublicRepresentative([u8; 32]); + +impl PublicRepresentative { + /// View this public representative as a byte array. + #[inline] + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Extract this representative's bytes for serialization. + #[inline] + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + +impl AsRef<[u8]> for PublicRepresentative { + /// View this shared secret key as a byte array. + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl From<[u8; 32]> for PublicRepresentative { + /// Build a Elligator2 Public key Representative from bytes + fn from(r: [u8; 32]) -> PublicRepresentative { + PublicRepresentative(r) + } +} + +impl<'a> From<&'a [u8; 32]> for PublicRepresentative { + /// Build a Elligator2 Public key Representative from bytes by reference + fn from(r: &'a [u8; 32]) -> PublicRepresentative { + PublicRepresentative(*r) + } +} + +impl<'a> From<&'a EphemeralSecret> for PublicRepresentative { + /// Given an x25519 [`EphemeralSecret`] key, compute its corresponding [`PublicRepresentative`]. + fn from(secret: &'a EphemeralSecret) -> PublicRepresentative { + let res: Option<[u8; 32]> = + Randomized::to_representative(secret.0.as_bytes(), secret.1).into(); + PublicRepresentative(res.unwrap()) + } +} + +impl<'a> From<&'a PublicRepresentative> for PublicKey { + /// Given an elligator2 [`PublicRepresentative`], compute its corresponding [`PublicKey`]. + fn from(representative: &'a PublicRepresentative) -> PublicKey { + let point = curve25519_elligator2::MontgomeryPoint::map_to_point(&representative.0); + PublicKey::from(*point.as_bytes()) + } +} + +impl From for PublicKey { + /// Given an elligator2 [`PublicRepresentative`], compute its corresponding [`PublicKey`]. + fn from(representative: PublicRepresentative) -> PublicKey { + let point = curve25519_elligator2::MontgomeryPoint::map_to_point(&representative.0); + PublicKey::from(*point.as_bytes()) + } +} + +use rand_core::{CryptoRng, RngCore}; + +pub const REPRESENTATIVE_LENGTH: usize = 32; + +/// A collection of functions for generating x25519 keys wrapping `x25519_dalek`. +// ['EphemeralSecret'] keys are guaranteed to have a valid elligator2 representative. In general +// ['StaticSecret'] should not be converted to PublicRepresentative, use an EphemeralSecret instead. +pub struct Keys; + +trait RetryLimit { + const RETRY_LIMIT: usize = 128; +} + +impl RetryLimit for Keys {} + +#[allow(unused)] +impl Keys { + /// Generate a new Elligator2 representable ['StaticSecret'] with the supplied RNG. + pub fn static_from_rng(mut rng: R) -> StaticSecret { + StaticSecret::random_from_rng(&mut rng) + } + + /// Generate a new Elligator2 representable ['StaticSecret']. + pub fn random_static() -> StaticSecret { + StaticSecret::random() + } + + /// Generate a new Elligator2 representable ['EphemeralSecret'] with the supplied RNG. + /// + /// May panic if the provided csprng fails to generate random values such that no generated + /// secret key maps to a valid elligator2 representative. This should never happen + /// when system CSPRNGs are used (i.e `rand::thread_rng`). + pub fn ephemeral_from_rng(mut csprng: R) -> EphemeralSecret { + let mut private = StaticSecret::random_from_rng(&mut csprng); + + // tweak only needs generated once as it doesn't affect the elligator2 representative validity. + let mut tweak = [0u8]; + csprng.fill_bytes(&mut tweak); + + let mut repres: Option<[u8; 32]> = + Randomized::to_representative(&private.to_bytes(), tweak[0]).into(); + + for _ in 0..Self::RETRY_LIMIT { + if repres.is_some() { + return EphemeralSecret(private, tweak[0]); + } + private = StaticSecret::random_from_rng(&mut csprng); + repres = Randomized::to_representative(&private.to_bytes(), tweak[0]).into(); + } + + panic!("failed to generate representable secret, bad RNG provided"); + } + + /// Generate a new Elligator2 representable ['EphemeralSecret']. + /// + /// May panic if the system random genereator fails such that no generated + /// secret key maps to a valid elligator2 representative. This should never + /// happen under normal use. + pub fn random_ephemeral() -> EphemeralSecret { + let mut private = StaticSecret::random(); + // + // tweak only needs generated once as it doesn't affect the elligator2 representative validity. + let mut tweak = [0u8]; + getrandom(&mut tweak); + + let mut repres: Option<[u8; 32]> = + Randomized::to_representative(&private.to_bytes(), tweak[0]).into(); + + for _ in 0..Self::RETRY_LIMIT { + if repres.is_some() { + return EphemeralSecret(private, tweak[0]); + } + private = StaticSecret::random(); + repres = Randomized::to_representative(&private.to_bytes(), tweak[0]).into(); + } + + panic!("failed to generate representable secret, getrandom failed"); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Result; + + use curve25519_elligator2::{ + traits::IsIdentity, EdwardsPoint, MapToPointVariant, MontgomeryPoint, Randomized, RFC9380, + }; + use hex::FromHex; + + use rand::Rng; + + #[test] + fn representative_match() { + let repres = <[u8; 32]>::from_hex( + "8781b04fefa49473ca5943ab23a14689dad56f8118d5869ad378c079fd2f4079", + ) + .unwrap(); + let incorrect = "1af2d7ac95b5dd1ab2b5926c9019fa86f211e77dd796f178f3fe66137b0d5d15"; + let expected = "a946c3dd16d99b8c38972584ca599da53e32e8b13c1e9a408ff22fdb985c2d79"; + + let r = PublicRepresentative::from(repres); + let p = PublicKey::from(&r); + assert_ne!(incorrect, hex::encode(p.as_bytes())); + assert_eq!(expected, hex::encode(p.as_bytes())); + } + + /// This test confirms that only about half of the `StaticSecret`s generated have + /// valid representatives. This is expected - in ['Keys'] we rely on this fact + /// to ensure that (given the provided csprng works) generating in a loop + /// should statiscally never fail to generate a representable key. + #[test] + fn about_half() -> Result<()> { + let mut rng = rand::thread_rng(); + + let mut success = 0; + let mut not_found = 0; + let mut not_match = 0; + for _ in 0..1_000 { + let sk = StaticSecret::random_from_rng(&mut rng); + let rp: Option<[u8; 32]> = Randomized::to_representative(sk.as_bytes(), 0_u8).into(); + let repres = match rp { + Some(r) => PublicRepresentative::from(r), + None => { + not_found += 1; + continue; + } + }; + + let pk_bytes = Randomized::mul_base_clamped(sk.to_bytes()).to_montgomery(); + + let pk = PublicKey::from(*pk_bytes.as_bytes()); + + let decoded_pk = PublicKey::from(&repres); + if hex::encode(pk) != hex::encode(decoded_pk) { + not_match += 1; + continue; + } + success += 1; + } + + if not_match != 0 { + println!("{not_found}/{not_match}/{success}/10_000"); + assert_eq!(not_match, 0); + } + assert!(not_found < 600); + assert!(not_found > 400); + Ok(()) + } + + #[test] + fn it_works() { + // if this panics we are in trouble + let k = EphemeralSecret::random(); + + // internal serialization and deserialization works + let k_bytes = k.0.as_bytes(); + let _k1 = EphemeralSecret::from_parts(StaticSecret::from(*k_bytes), 0u8); + + // if we send our representative over the wire then recover the pubkey they should match + let pk = PublicKey::from(&k); + let r = PublicRepresentative::from(&k); + let r_bytes = r.to_bytes(); + // send r_bytes over the network + let r1 = PublicRepresentative::from(r_bytes); + let pk1 = PublicKey::from(r1); + assert_eq!(hex::encode(pk.to_bytes()), hex::encode(pk1.to_bytes())); + } + + const BASEPOINT_ORDER_MINUS_ONE: [u8; 32] = [ + 0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, + 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x10, + ]; + + // Generates a new Keypair using, and returns the public key representative + // along, with its public key as a newly allocated edwards25519.Point. + fn generate(rng: &mut R) -> ([u8; 32], EdwardsPoint) { + for _ in 0..63 { + let y_sk = rng.gen::<[u8; 32]>(); + + let y_repr_bytes = match Randomized::to_representative(&y_sk, 0xff).into() { + Some(r) => r, + None => continue, + }; + let y_pk = Randomized::mul_base_clamped(y_sk); + + assert_eq!( + MontgomeryPoint::from_representative::(&y_repr_bytes) + .expect("failed to re-derive point from representative"), + y_pk.to_montgomery() + ); + + return (y_repr_bytes, y_pk); + } + panic!("failed to generate a valid keypair"); + } + + /// Returns a new edwards25519.Point that is v multiplied by the subgroup order. + /// + /// BASEPOINT_ORDER_MINUS_ONE is the same as scMinusOne in filippo.io/edwards25519. + /// https://github.com/FiloSottile/edwards25519/blob/v1.0.0/scalar.go#L34 + fn scalar_mult_order(v: &EdwardsPoint) -> EdwardsPoint { + let order = curve25519_elligator2::Scalar::from_bytes_mod_order(BASEPOINT_ORDER_MINUS_ONE); + + // v * (L - 1) + v => v * L + let p = v * order; + p + v + } + + #[test] + fn off_subgroup_check_edw() { + let mut count = 0; + let n_trials = 100; + let mut rng = rand::thread_rng(); + for _ in 0..n_trials { + let (repr, pk) = generate(&mut rng); + + // check if the generated public key is off the subgroup + let v = scalar_mult_order(&pk); + let _pk_off = !v.is_identity(); + + // ---- + + // check if the public key derived from the representative (top bit 0) + // is off the subgroup + let mut yr_255 = repr; + yr_255[31] &= 0xbf; + let pk_255 = EdwardsPoint::from_representative::(&yr_255) + .expect("from_repr_255, should never fail"); + let v = scalar_mult_order(&pk_255); + let off_255 = !v.is_identity(); + + // check if the public key derived from the representative (top two bits 0 - as + // our representatives are) is off the subgroup. + let mut yr_254 = repr; + yr_254[31] &= 0x3f; + let pk_254 = EdwardsPoint::from_representative::(&yr_254) + .expect("from_repr_254, should never fail"); + let v = scalar_mult_order(&pk_254); + let off_254 = !v.is_identity(); + + // println!("pk_gen: {pk_off}, pk_255: {off_255}, pk_254: {off_254}"); + if off_254 && off_255 { + count += 1; + } + } + assert!(count > 0); + assert!(count < n_trials); + } +} diff --git a/crates/o5/src/constants.rs b/crates/o5/src/constants.rs new file mode 100644 index 0000000..9af94e8 --- /dev/null +++ b/crates/o5/src/constants.rs @@ -0,0 +1,79 @@ +#![allow(unused)] + +use tor_llcrypto::pk::ed25519::ED25519_ID_LEN; + +pub use crate::common::ntor_arti::SESSION_ID_LEN; +use crate::{ + common::{drbg, mlkem1024_x25519, x25519_elligator2::REPRESENTATIVE_LENGTH}, + framing, + handshake::AUTHCODE_LENGTH, +}; + +use std::time::Duration; + +pub const PUBLIC_KEY_LEN: usize = mlkem1024_x25519::PUBKEY_LEN; + +//=========================[Framing/Msgs]=====================================// + +/// Maximum handshake size including padding +pub const MAX_HANDSHAKE_LENGTH: usize = 8192; + +pub const SHA256_SIZE: usize = 32; +pub const MARK_LENGTH: usize = SHA256_SIZE / 2; +pub const MAC_LENGTH: usize = SHA256_SIZE / 2; + +/// Minimum padding allowed in a client handshake message +pub const CLIENT_MIN_PAD_LENGTH: usize = + (SERVER_MIN_HANDSHAKE_LENGTH + INLINE_SEED_FRAME_LENGTH) - CLIENT_MIN_HANDSHAKE_LENGTH; +pub const CLIENT_MAX_PAD_LENGTH: usize = MAX_HANDSHAKE_LENGTH - CLIENT_MIN_HANDSHAKE_LENGTH; +pub const CLIENT_MIN_HANDSHAKE_LENGTH: usize = REPRESENTATIVE_LENGTH + MARK_LENGTH + MAC_LENGTH; + +pub const SERVER_MIN_PAD_LENGTH: usize = 0; +pub const SERVER_MAX_PAD_LENGTH: usize = + MAX_HANDSHAKE_LENGTH - (SERVER_MIN_HANDSHAKE_LENGTH + INLINE_SEED_FRAME_LENGTH); +pub const SERVER_MIN_HANDSHAKE_LENGTH: usize = + REPRESENTATIVE_LENGTH + AUTHCODE_LENGTH + MARK_LENGTH + MAC_LENGTH; + +pub const INLINE_SEED_FRAME_LENGTH: usize = + framing::FRAME_OVERHEAD + MESSAGE_OVERHEAD + SEED_MESSAGE_PAYLOAD_LENGTH; + +pub const MESSAGE_OVERHEAD: usize = 2 + 1; +pub const MAX_MESSAGE_PAYLOAD_LENGTH: usize = framing::MAX_FRAME_PAYLOAD_LENGTH - MESSAGE_OVERHEAD; +pub const MAX_MESSAGE_PADDING_LENGTH: usize = MAX_MESSAGE_PAYLOAD_LENGTH; +pub const SEED_MESSAGE_PAYLOAD_LENGTH: usize = drbg::SEED_LENGTH; +pub const SEED_MESSAGE_LENGTH: usize = + framing::LENGTH_LENGTH + MESSAGE_OVERHEAD + drbg::SEED_LENGTH + MAC_LENGTH; + +pub const CONSUME_READ_SIZE: usize = framing::MAX_SEGMENT_LENGTH * 16; + +//===============================[Proto]======================================// + +pub const TRANSPORT_NAME: &str = "obfs4"; + +pub const NODE_ID_ARG: &str = "node-id"; +pub const PUBLIC_KEY_ARG: &str = "public-key"; +pub const PRIVATE_KEY_ARG: &str = "private-key"; +pub const SEED_ARG: &str = "drbg-seed"; +pub const CERT_ARG: &str = "cert"; + +pub const BIAS_CMD_ARG: &str = "obfs4-distBias"; + +pub const REPLAY_TTL: Duration = Duration::from_secs(60); +#[cfg(test)] +pub const CLIENT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5); +#[cfg(test)] +pub const SERVER_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5); +#[cfg(not(test))] +pub const CLIENT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(60); +#[cfg(not(test))] +pub const SERVER_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(60); + +pub const MAX_IPT_DELAY: usize = 100; +pub const MAX_CLOSE_DELAY: usize = 60; +pub const MAX_CLOSE_DELAY_BYTES: usize = MAX_HANDSHAKE_LENGTH; + +pub const SEED_LENGTH: usize = drbg::SEED_LENGTH; +pub const HEADER_LENGTH: usize = framing::FRAME_OVERHEAD + framing::MESSAGE_OVERHEAD; + +pub const NODE_ID_LENGTH: usize = ED25519_ID_LEN; +pub const NODE_PUBKEY_LENGTH: usize = mlkem1024_x25519::PUBKEY_LEN; diff --git a/crates/o5/src/error.rs b/crates/o5/src/error.rs new file mode 100644 index 0000000..901ede1 --- /dev/null +++ b/crates/o5/src/error.rs @@ -0,0 +1,275 @@ +use crate::framing::FrameError; + +use std::array::TryFromSliceError; +use std::num::NonZeroUsize; +use std::string::FromUtf8Error; +use std::{fmt::Display, str::FromStr}; + +use hex::FromHexError; +use sha2::digest::InvalidLength; + +use crate::common::ntor_arti::RelayHandshakeError; + +/// Result type returning [`Error`] or `T` +pub type Result = std::result::Result; + +/// Errors that can occur when using the transports, including wrapped from dependencies. +impl std::error::Error for Error {} +#[derive(Debug)] +pub enum Error { + Bug(tor_error::Bug), + + Other(Box), + IOError(std::io::Error), + EncodeError(Box), + Utf8Error(FromUtf8Error), + RngSourceErr(getrandom::Error), + Crypto(String), + NullTransport, + NotImplemented, + NotSupported, + Cancelled, + HandshakeTimeout, + BadCircHandshakeAuth, + InvalidKDFOutputLength, + + /// An error that occurred in the tor_bytes crate while decoding an + /// object. + // #[error("Unable to parse {object}")] + BytesError { + /// What we were trying to parse. + object: &'static str, + /// The error that occurred while parsing it. + // #[source] + err: tor_bytes::Error, + }, + // TODO: do we need to keep this? + CellDecodeErr { + /// What we were trying to parse. + object: &'static str, + /// The error that occurred while parsing it. + err: tor_cell::Error, + }, + HandshakeErr(RelayHandshakeError), + + O5Framing(FrameError), +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + Error::Bug(_) => write!(f, "Internal error occured (bug)"), + Error::Cancelled => write!(f, "cancelled"), + Error::Other(e) => write!(f, "{}", e), + Error::IOError(e) => write!(f, "{}", e), + Error::EncodeError(e) => write!(f, "{}", e), + Error::Utf8Error(e) => write!(f, "{}", e), + Error::RngSourceErr(e) => write!(f, "{}", e), + Error::Crypto(e) => write!(f, "cryptographic err: {}", e), + Error::NotImplemented => write!(f, "NotImplemented"), + Error::NotSupported => write!(f, "NotSupported"), + Error::NullTransport => write!(f, "NullTransport"), + Error::HandshakeTimeout => write!(f, "handshake timed out"), + Error::BadCircHandshakeAuth => write!(f, "failed authentication for circuit handshake"), + Error::InvalidKDFOutputLength => { + write!(f, "Tried to extract too many bytes from a KDF") + } + Error::BytesError { object, err } => write!(f, "Unable to parse {object}: {err}"), + Error::CellDecodeErr { object, err } => { + write!(f, "Unable to decode cell {object}: {err}") + } + Error::HandshakeErr(err) => write!(f, "handshake failed or unable to complete: {err}"), + + Error::O5Framing(e) => write!(f, "obfs4 framing error: {e}"), + } + } +} + +unsafe impl Send for Error {} + +impl Error { + pub fn new>>(e: T) -> Self { + Error::Other(e.into()) + } +} + +impl From for std::io::Error { + fn from(e: Error) -> Self { + match e { + Error::IOError(io_err) => io_err, + e => std::io::Error::new(std::io::ErrorKind::Other, format!("{e}")), + } + } +} + +impl FromStr for Error { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + Ok(Error::new(s)) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::IOError(e) + } +} + +impl From> for Error { + fn from(e: Box) -> Self { + Error::IOError(*e) + } +} + +impl From for Error { + fn from(e: FromHexError) -> Self { + Error::EncodeError(Box::new(e)) + } +} + +impl From> for Error { + fn from(e: Box) -> Self { + Error::Other(e) + } +} + +impl From for Error { + fn from(e: String) -> Self { + Error::Other(e.into()) + } +} + +impl From<&str> for Error { + fn from(e: &str) -> Self { + Error::Other(e.into()) + } +} + +impl From for Error { + fn from(e: FromUtf8Error) -> Self { + Error::Utf8Error(e) + } +} + +impl From for Error { + fn from(e: getrandom::Error) -> Self { + Error::RngSourceErr(e) + } +} + +impl From for Error { + fn from(e: TryFromSliceError) -> Self { + Error::Other(Box::new(e)) + } +} + +impl From for Error { + fn from(e: InvalidLength) -> Self { + Error::Other(Box::new(e)) + } +} + +impl From for Error { + fn from(e: FrameError) -> Self { + Error::O5Framing(e) + } +} + +impl From for Error { + fn from(value: RelayHandshakeError) -> Self { + Error::HandshakeErr(value) + } +} + +impl From for Error { + fn from(value: tor_error::Bug) -> Self { + Error::Bug(value) + } +} + +impl From for Error { + fn from(value: tor_cell::Error) -> Self { + Error::CellDecodeErr { + object: "", + err: value, + } + } +} + +impl Error { + /// Create an error for a tor_bytes error that occurred while encoding + /// something of type `object`. + pub(crate) fn from_bytes_enc(err: tor_bytes::EncodeError, _object: &'static str) -> Error { + Error::EncodeError(Box::new(err)) + } + + /// Create an error for a tor_bytes error that occurred while parsing + /// something of type `object`. + pub(crate) fn from_bytes_err(err: tor_bytes::Error, object: &'static str) -> Error { + Error::BytesError { err, object } + } + + pub(crate) fn incomplete_error(_deficit: NonZeroUsize) -> tor_bytes::Error { + tor_bytes::Error::Truncated + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_other_error() { + let err = Error::new("some other error"); + assert_eq!(format!("{}", err), "some other error"); + } + + #[test] + fn test_display_io_error() { + let err = Error::IOError(std::io::Error::new( + std::io::ErrorKind::Other, + "some io error", + )); + assert_eq!(format!("{}", err), "some io error"); + } + + #[test] + fn test_display_encode_error() { + let err = Error::EncodeError(Box::new(FromHexError::InvalidHexCharacter { + c: 'z', + index: 0, + })); + assert_eq!(format!("{}", err), "Invalid character 'z' at position 0"); + } + + #[test] + fn test_display_null_transport_error() { + let err = Error::NullTransport; + assert_eq!(format!("{}", err), "NullTransport"); + } + + #[test] + fn test_from_io_error() { + let io_err = std::io::Error::new(std::io::ErrorKind::Other, "some io error"); + let err = Error::from(io_err); + assert_eq!(format!("{}", err), "some io error"); + } + + #[test] + fn test_from_encode_error() { + let hex_err = FromHexError::InvalidHexCharacter { c: 'z', index: 0 }; + let err = Error::from(hex_err); + assert_eq!(format!("{}", err), "Invalid character 'z' at position 0"); + } + + #[test] + fn test_from_other_error() { + let other_err = Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + "some other error", + )); + let err = Error::from(other_err); + assert_eq!(format!("{}", err), "some other error"); + } +} diff --git a/crates/o5/src/framing/codecs.rs b/crates/o5/src/framing/codecs.rs new file mode 100644 index 0000000..15cf707 --- /dev/null +++ b/crates/o5/src/framing/codecs.rs @@ -0,0 +1,371 @@ +use crate::{ + common::drbg::{self, Drbg, Seed}, + constants::MESSAGE_OVERHEAD, + framing::{FrameError, Messages}, +}; + +use bytes::{Buf, BufMut, BytesMut}; +use crypto_secretbox::{ + aead::{generic_array::GenericArray, Aead, KeyInit}, + XChaCha20Poly1305, +}; +use ptrs::{debug, error, trace}; +use rand::prelude::*; +use tokio_util::codec::{Decoder, Encoder}; + +/// MaximumSegmentLength is the length of the largest possible segment +/// including overhead. +pub(crate) const MAX_SEGMENT_LENGTH: usize = 1500 - (40 + 12); + +/// secret box overhead is fixed length prefix and counter +const SECRET_BOX_OVERHEAD: usize = TAG_SIZE; + +/// FrameOverhead is the length of the framing overhead. +pub(crate) const FRAME_OVERHEAD: usize = LENGTH_LENGTH + SECRET_BOX_OVERHEAD; + +/// MaximumFramePayloadLength is the length of the maximum allowed payload +/// per frame. +pub(crate) const MAX_FRAME_PAYLOAD_LENGTH: usize = MAX_SEGMENT_LENGTH - FRAME_OVERHEAD; + +pub(crate) const MAX_FRAME_LENGTH: usize = MAX_SEGMENT_LENGTH - LENGTH_LENGTH; +pub(crate) const MIN_FRAME_LENGTH: usize = FRAME_OVERHEAD - LENGTH_LENGTH; + +pub(crate) const NONCE_PREFIX_LENGTH: usize = 16; +pub(crate) const NONCE_COUNTER_LENGTH: usize = 8; +pub(crate) const NONCE_LENGTH: usize = NONCE_PREFIX_LENGTH + NONCE_COUNTER_LENGTH; + +pub(crate) const LENGTH_LENGTH: usize = 2; + +/// KEY_LENGTH is the length of the Encoder/Decoder secret key. +pub(crate) const KEY_LENGTH: usize = 32; + +pub(crate) const TAG_SIZE: usize = 16; + +pub(crate) const KEY_MATERIAL_LENGTH: usize = KEY_LENGTH + NONCE_PREFIX_LENGTH + drbg::SEED_LENGTH; + +// TODO: make this (Codec) threadsafe +pub struct EncryptingCodec { + // key: [u8; KEY_LENGTH], + encoder: EncryptingEncoder, + decoder: EncryptingDecoder, + + pub(crate) handshake_complete: bool, +} + +impl EncryptingCodec { + pub fn new( + encoder_key_material: [u8; KEY_MATERIAL_LENGTH], + decoder_key_material: [u8; KEY_MATERIAL_LENGTH], + ) -> Self { + // let mut key: [u8; KEY_LENGTH] = key_material[..KEY_LENGTH].try_into().unwrap(); + Self { + // key, + encoder: EncryptingEncoder::new(encoder_key_material), + decoder: EncryptingDecoder::new(decoder_key_material), + handshake_complete: false, + } + } + + pub(crate) fn handshake_complete(&mut self) { + self.handshake_complete = true; + } +} + +///Decoder is a frame decoder instance. +struct EncryptingDecoder { + key: [u8; KEY_LENGTH], + nonce: NonceBox, + drbg: Drbg, + + next_nonce: [u8; NONCE_LENGTH], + next_length: u16, + next_length_invalid: bool, +} + +impl EncryptingDecoder { + // Creates a new Decoder instance. It must be supplied a slice + // containing exactly KeyLength bytes of keying material. + fn new(key_material: [u8; KEY_MATERIAL_LENGTH]) -> Self { + trace!("new decoder key_material: {}", hex::encode(key_material)); + let key: [u8; KEY_LENGTH] = key_material[..KEY_LENGTH].try_into().unwrap(); + let nonce = NonceBox::new(&key_material[KEY_LENGTH..(KEY_LENGTH + NONCE_PREFIX_LENGTH)]); + let seed = Seed::try_from(&key_material[(KEY_LENGTH + NONCE_PREFIX_LENGTH)..]).unwrap(); + let d = Drbg::new(Some(seed)).unwrap(); + + Self { + key, + drbg: d, + nonce, + + next_nonce: [0_u8; NONCE_LENGTH], + next_length: 0, + next_length_invalid: false, + } + } +} + +impl Decoder for EncryptingCodec { + type Item = Messages; + type Error = FrameError; + + // Decode decodes a stream of data and returns the length if any. ErrAgain is + // a temporary failure, all other errors MUST be treated as fatal and the + // session aborted. + fn decode( + &mut self, + src: &mut BytesMut, + ) -> std::result::Result, Self::Error> { + trace!( + "decoding src:{}B {} {}", + src.remaining(), + self.decoder.next_length, + self.decoder.next_length_invalid + ); + // A length of 0 indicates that we do not know the expected size of + // the next frame. we use this to store the length of a packet when we + // receive the length at the beginning, but not the whole packet, since + // future reads may not have the who packet (including length) available + if self.decoder.next_length == 0 { + // Attempt to pull out the next frame length + if LENGTH_LENGTH > src.remaining() { + return Ok(None); + } + + // derive the nonce that the peer would have used + self.decoder.next_nonce = self.decoder.nonce.next()?; + + // Remove the field length from the buffer + // let mut len_buf: [u8; LENGTH_LENGTH] = src[..LENGTH_LENGTH].try_into().unwrap(); + let mut length = src.get_u16(); + + // De-obfuscate the length field + let length_mask = self.decoder.drbg.length_mask(); + trace!( + "decoding {length:04x}^{length_mask:04x} {:04x}B", + length ^ length_mask + ); + length ^= length_mask; + if MAX_FRAME_LENGTH < length as usize || MIN_FRAME_LENGTH > length as usize { + // Per "Plaintext Recovery Attacks Against SSH" by + // Martin R. Albrecht, Kenneth G. Paterson and Gaven J. Watson, + // there are a class of attacks againt protocols that use similar + // sorts of framing schemes. + // + // While obfs4 should not allow plaintext recovery (CBC mode is + // not used), attempt to mitigate out of bound frame length errors + // by pretending that the length was a random valid range as pe + // the countermeasure suggested by Denis Bider in section 6 of the + // paper. + + let invalid_length = length; + self.decoder.next_length_invalid = true; + + length = rand::thread_rng().gen::() + % (MAX_FRAME_LENGTH - MIN_FRAME_LENGTH) as u16 + + MIN_FRAME_LENGTH as u16; + error!( + "invalid length {invalid_length} {length} {}", + self.decoder.next_length_invalid + ); + } + + self.decoder.next_length = length; + } + + let next_len = self.decoder.next_length as usize; + + if next_len > src.len() { + // The full string has not yet arrived. + // + // We reserve more space in the buffer. This is not strictly + // necessary, but is a good idea performance-wise. + if !self.decoder.next_length_invalid { + src.reserve(next_len - src.len()); + } + + trace!( + "next_len > src.len --> reading more {} {}", + self.decoder.next_length, + self.decoder.next_length_invalid + ); + + // We inform the Framed that we need more bytes to form the next + // frame. + return Ok(None); + } + + // Use advance to modify src such that it no longer contains this frame. + let data = src.get(..next_len).unwrap().to_vec(); + + // Unseal the frame + let key = GenericArray::from_slice(&self.decoder.key); + let cipher = XChaCha20Poly1305::new(key); + let nonce = GenericArray::from_slice(&self.decoder.next_nonce); // unique per message + + let res = cipher.decrypt(nonce, data.as_ref()); + if res.is_err() { + let e = res.unwrap_err(); + trace!("failed to decrypt result: {e}"); + return Err(e.into()); + } + let plaintext = res?; + if plaintext.len() < MESSAGE_OVERHEAD { + return Err(FrameError::InvalidMessage); + } + + // Clean up and prepare for the next frame + // + // we read a whole frame, we no longer know the size of the next pkt + self.decoder.next_length = 0; + src.advance(next_len); + + debug!("decoding {next_len}B src:{}B", src.remaining()); + match Messages::try_parse(&mut BytesMut::from(plaintext.as_slice())) { + Ok(Messages::Padding(_)) => Ok(None), + Ok(m) => Ok(Some(m)), + Err(FrameError::UnknownMessageType(_)) => Ok(None), + Err(e) => Err(e), + } + } +} + +/// Encoder is a frame encoder instance. +struct EncryptingEncoder { + key: [u8; KEY_LENGTH], + nonce: NonceBox, + drbg: Drbg, +} + +impl EncryptingEncoder { + /// Creates a new Encoder instance. It must be supplied a slice + /// containing exactly KeyLength bytes of keying material + fn new(key_material: [u8; KEY_MATERIAL_LENGTH]) -> Self { + trace!("new encoder key_material: {}", hex::encode(key_material)); + let key: [u8; KEY_LENGTH] = key_material[..KEY_LENGTH].try_into().unwrap(); + let nonce = NonceBox::new(&key_material[KEY_LENGTH..(KEY_LENGTH + NONCE_PREFIX_LENGTH)]); + let seed = Seed::try_from(&key_material[(KEY_LENGTH + NONCE_PREFIX_LENGTH)..]).unwrap(); + let d = Drbg::new(Some(seed)).unwrap(); + + Self { + key, + nonce, + drbg: d, + } + } +} + +impl Encoder for EncryptingCodec { + type Error = FrameError; + + /// Encode encodes a single frame worth of payload and returns. Plaintext + /// should either be a handshake message OR a buffer containing one or more + /// [`Message`]s already properly marshalled. The proided plaintext can + /// be no longer than [`MAX_FRAME_PAYLOAD_LENGTH`]. + /// + /// [`InvalidPayloadLength`] is recoverable, all other errors MUST be + /// treated as fatal and the session aborted. + fn encode(&mut self, plaintext: T, dst: &mut BytesMut) -> std::result::Result<(), Self::Error> { + trace!( + "encoding {}/{MAX_FRAME_PAYLOAD_LENGTH}", + plaintext.remaining() + ); + + // Don't send a frame if it is longer than the other end will accept. + if plaintext.remaining() > MAX_FRAME_PAYLOAD_LENGTH { + return Err(FrameError::InvalidPayloadLength(plaintext.remaining())); + } + + let mut plaintext_frame = BytesMut::new(); + + plaintext_frame.put(plaintext); + + // Generate a new nonce + let nonce_bytes = self.encoder.nonce.next()?; + + // Encrypt and MAC payload + let key = GenericArray::from_slice(&self.encoder.key); + let cipher = XChaCha20Poly1305::new(key); + let nonce = GenericArray::from_slice(&nonce_bytes); // unique per message + + let ciphertext = cipher.encrypt(nonce, plaintext_frame.as_ref())?; + + // Obfuscate the length + let mut length = ciphertext.len() as u16; + let length_mask: u16 = self.encoder.drbg.length_mask(); + debug!( + "encoding➡️ {length}B, {length:04x}^{length_mask:04x} {:04x}", + length ^ length_mask + ); + length ^= length_mask; + + trace!( + "prng_ciphertext: {}{}", + hex::encode(length.to_be_bytes()), + hex::encode(&ciphertext) + ); + + // Write the length and payload to the buffer. + dst.extend_from_slice(&length.to_be_bytes()[..]); + dst.extend_from_slice(&ciphertext); + Ok(()) + } +} + +/// internal nonce management for NaCl secret boxes +pub(crate) struct NonceBox { + prefix: [u8; NONCE_PREFIX_LENGTH], + counter: u64, +} + +impl NonceBox { + pub fn new(prefix: impl AsRef<[u8]>) -> Self { + assert!( + prefix.as_ref().len() >= NONCE_PREFIX_LENGTH, + "prefix too short: {} < {NONCE_PREFIX_LENGTH}", + prefix.as_ref().len() + ); + Self { + prefix: prefix.as_ref()[..NONCE_PREFIX_LENGTH].try_into().unwrap(), + counter: 1, + } + } + + pub fn next(&mut self) -> std::result::Result<[u8; NONCE_LENGTH], FrameError> { + // The security guarantee of Poly1305 is broken if a nonce is ever reused + // for a given key. Detect this by checking for counter wraparound since + // we start each counter at 1. If it ever happens that more than 2^64 - 1 + // frames are transmitted over a given connection, support for rekeying + // will be neccecary, but that's unlikely to happen. + + if self.counter == u64::MAX { + return Err(FrameError::NonceCounterWrapped); + } + let mut nonce = self.prefix.clone().to_vec(); + nonce.append(&mut self.counter.to_be_bytes().to_vec()); + + let nonce_l: [u8; NONCE_LENGTH] = nonce[..].try_into().unwrap(); + + trace!("fresh nonce: {}", hex::encode(nonce_l)); + self.inc(); + Ok(nonce_l) + } + + fn inc(&mut self) { + self.counter += 1; + } +} + +#[cfg(test)] +mod testing { + use super::*; + use crate::Result; + + #[test] + fn nonce_wrap() -> Result<()> { + let mut nb = NonceBox::new([0_u8; NONCE_PREFIX_LENGTH]); + nb.counter = u64::MAX; + + assert_eq!(nb.next().unwrap_err(), FrameError::NonceCounterWrapped); + Ok(()) + } +} diff --git a/crates/o5/src/framing/generic_test.rs b/crates/o5/src/framing/generic_test.rs new file mode 100644 index 0000000..2471715 --- /dev/null +++ b/crates/o5/src/framing/generic_test.rs @@ -0,0 +1,143 @@ +/// testing out tokio_util::codec for building transports. +/// +/// useful links: +/// - https://dev.to/jtenner/creating-a-tokio-codec-1f0l +/// - example telnet implementation using codecs +/// - https://github.com/jtenner/telnet_codec +/// +/// - https://docs.rs/tokio-util/latest/tokio_util/codec/index.html +/// - tokio_util codec docs +/// +use crate::Result; + +use bytes::{Buf, BytesMut}; +use futures::{SinkExt, StreamExt}; + +use tokio_util::codec::{Decoder, Encoder}; + +const MAX: usize = 8 * 1024 * 1024; + +struct O5Codec {} + +impl O5Codec { + fn new() -> Self { + Self {} + } +} + +impl Decoder for O5Codec { + type Item = String; + type Error = std::io::Error; + + fn decode( + &mut self, + src: &mut BytesMut, + ) -> std::result::Result, Self::Error> { + if src.len() < 4 { + // Not enough data to read length marker. + return Ok(None); + } + + // Read length marker. + let mut length_bytes = [0u8; 4]; + length_bytes.copy_from_slice(&src[..4]); + let length = u32::from_le_bytes(length_bytes) as usize; + + // Check that the length is not too large to avoid a denial of + // service attack where the server runs out of memory. + if length > MAX { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Frame of length {} is too large.", length), + )); + } + + if src.len() < 4 + length { + // The full string has not yet arrived. + // + // We reserve more space in the buffer. This is not strictly + // necessary, but is a good idea performance-wise. + src.reserve(4 + length - src.len()); + + // We inform the Framed that we need more bytes to form the next + // frame. + return Ok(None); + } + + // Use advance to modify src such that it no longer contains + // this frame. + let data = src[4..4 + length].to_vec(); + src.advance(4 + length); + + // Convert the data to a string, or fail if it is not valid utf-8. + match String::from_utf8(data) { + Ok(string) => Ok(Some(string)), + Err(utf8_error) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + utf8_error.utf8_error(), + )), + } + } +} + +impl Encoder for O5Codec { + type Error = std::io::Error; + + fn encode(&mut self, item: String, dst: &mut BytesMut) -> std::result::Result<(), Self::Error> { + // Don't send a string if it is longer than the other end will + // accept. + if item.len() > MAX { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Frame of length {} is too large.", item.len()), + )); + } + + // Convert the length into a byte array. + // The cast to u32 cannot overflow due to the length check above. + let len_slice = u32::to_le_bytes(item.len() as u32); + + // Reserve space in the buffer. + dst.reserve(4 + item.len()); + + // Write the length and string to the buffer. + dst.extend_from_slice(&len_slice); + dst.extend_from_slice(item.as_bytes()); + Ok(()) + } +} + +#[tokio::test] +async fn framing_flow() -> Result<()> { + let (c, s) = tokio::io::duplex(16 * 1024); + + tokio::spawn(async move { + let codec = O5Codec::new(); + + let (mut sink, mut input) = codec.framed(s).split(); + + while let Some(Ok(event)) = input.next().await { + // println!("Event {:?}", event); + sink.send(event).await.expect("server response failed"); + } + }); + + let message = "Hello there"; + let client_codec = O5Codec::new(); + let (mut c_sink, mut c_stream) = client_codec.framed(c).split(); + + c_sink + .send(message.into()) + .await + .expect("client send failed"); + + let m: String = c_stream + .next() + .await + .expect("you were supposed to call me back!") + .expect("an error occured when you called back"); + + assert_eq!(m, message); + + Ok(()) +} diff --git a/crates/o5/src/framing/handshake.rs b/crates/o5/src/framing/handshake.rs new file mode 100644 index 0000000..ed88886 --- /dev/null +++ b/crates/o5/src/framing/handshake.rs @@ -0,0 +1,184 @@ +use crate::{ + common::{ + utils::get_epoch_hour, // make_hs_pad}, + HmacSha256, + }, + handshake::{Authcode, CHSMaterials, AUTHCODE_LENGTH}, + sessions::SessionPublicKey, + Result, +}; + +use bytes::BufMut; +use block_buffer::Eager; +use digest::{core_api::{BlockSizeUser, CoreProxy, UpdateCore, FixedOutputCore, BufferKindUser}, HashMarker}; +use hmac::{Hmac, Mac}; +use ptrs::trace; +use typenum::{consts::U256, operator_aliases::Le, type_operators::IsLess, marker_traits::NonZero}; +use tor_cell::relaycell::extend::NtorV3Extension; +use tor_llcrypto::d::Sha3_256; + +// -----------------------------[ Server ]----------------------------- + +pub struct ServerHandshakeMessage { + server_auth: [u8; AUTHCODE_LENGTH], + pad_len: usize, + session_pubkey: SessionPublicKey, + epoch_hour: String, + aux_data: Vec, +} + +impl ServerHandshakeMessage { + pub fn new(_client_pubkey: SessionPublicKey, _session_pubkey: SessionPublicKey) -> Self { + todo!("SHS MSG - this should probably be built directly from the client HS MSG"); + // Self { + // server_auth: [0u8; AUTHCODE_LENGTH], + // pad_len: rand::thread_rng().gen_range(SERVER_MIN_PAD_LENGTH..SERVER_MAX_PAD_LENGTH), + // epoch_hour: epoch_hr, + // } + } + + pub fn with_pad_len(&mut self, pad_len: usize) -> &Self { + self.pad_len = pad_len; + self + } + + pub fn with_aux_data(&mut self, aux_data: Vec) -> &Self { + self.aux_data = aux_data; + self + } + + pub fn server_pubkey(&mut self) -> SessionPublicKey { + self.session_pubkey.clone() + } + + pub fn server_auth(self) -> Authcode { + self.server_auth + } + + pub fn marshall(&mut self, _buf: &mut impl BufMut, mut _h: HmacSha256) -> Result<()> { + trace!("serializing server handshake"); + todo!("marshall server hello"); + + // h.reset(); + // h.update(self.session_pubkey.as_bytes().as_ref()); + // let mark: &[u8] = &h.finalize_reset().into_bytes()[..MARK_LENGTH]; + + // // The server handshake is Y | AUTH | P_S | M_S | MAC(Y | AUTH | P_S | M_S | E) where: + // // * Y is the server's ephemeral Curve25519 public key representative. + // // * AUTH is the ntor handshake AUTH value. + // // * P_S is [serverMinPadLength,serverMaxPadLength] bytes of random padding. + // // * M_S is HMAC-SHA256-128(serverIdentity | NodeID, Y) + // // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, Y .... E) + // // * E is the string representation of the number of hours since the UNIX + // // epoch. + + // // Generate the padding + // let pad: &[u8] = &make_hs_pad(self.pad_len)?; + + // // Write Y, AUTH, P_S, M_S. + // let mut params = vec![]; + // params.extend_from_slice(self.session_pubkey.as_bytes()); + // params.extend_from_slice(&self.server_auth); + // params.extend_from_slice(pad); + // params.extend_from_slice(mark); + // buf.put(params.as_slice()); + + // // Calculate and write MAC + // h.update(¶ms); + // h.update(self.epoch_hour.as_bytes()); + // buf.put(&h.finalize_reset().into_bytes()[..MAC_LENGTH]); + + // Ok(()) + } +} + +// -----------------------------[ Client ]----------------------------- + +/// Preliminary message sent in an obfs4 handshake attempting to open a +/// connection from a client to a potential server. +pub struct ClientHandshakeMessage<'a> { + hs_materials: &'a CHSMaterials, + client_session_pubkey: SessionPublicKey, + + // only used when parsing (i.e. on the server side) + pub(crate) epoch_hour: String, +} + +impl<'a> ClientHandshakeMessage<'a> { + pub fn new(client_session_pubkey: SessionPublicKey, hs_materials: &'a CHSMaterials) -> Self { + Self { + hs_materials, + client_session_pubkey, + + // only used when parsing (i.e. on the server side) + epoch_hour: get_epoch_hour().to_string(), + } + } + + pub fn get_public(&mut self) -> SessionPublicKey { + // trace!("repr: {}", hex::encode(self.client_session_pubkey.id); + self.client_session_pubkey.clone() + } + + /// return the epoch hour used in the ntor handshake. + pub fn get_epoch_hr(&self) -> String { + self.epoch_hour.clone() + } + + pub fn marshall( + &mut self, + buf: &mut impl BufMut, + key: &[u8], + ) -> Result<()> { + trace!("serializing client handshake"); + + let h = Hmac::::new_from_slice(key).unwrap(); + + self.marshall_inner(buf, h) + } + + pub fn marshall_inner(&mut self, _buf: &mut impl BufMut, _h: Hmac) -> Result<()> + where + D: CoreProxy, + D::Core: HashMarker + UpdateCore + FixedOutputCore + BufferKindUser + Default + Clone, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, + { + todo!("when this is causing panic re-visit"); + // NtorV3Extension::write_many_onto(client_aux_data.borrow(), &mut message) + // .map_err(|e| Error::from_bytes_enc(e, "ntor3 handshake extensions"))?; + + // h.reset(); // disambiguate reset() implementations Mac v digest + // h.update(self.repres.as_bytes().as_ref()); + // let mark: &[u8] = &h.finalize_reset().into_bytes()[..MARK_LENGTH]; + + // // The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E) where: + // // * X is the client's ephemeral Curve25519 public key representative. + // // * P_C is [clientMinPadLength,clientMaxPadLength] bytes of random padding. + // // * M_C is HMAC-SHA256-128(serverIdentity | NodeID, X) + // // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, X .... E) + // // * E is the string representation of the number of hours since the UNIX + // // epoch. + + // // Generate the padding + // let pad = make_hs_pad(self.pad_len)?; + + // // Write X, P_C, M_C + // let mut params = vec![]; + // params.extend_from_slice(self.repres.as_bytes()); + // params.extend_from_slice(&pad); + // params.extend_from_slice(mark); + // buf.put(params.as_slice()); + + // // Calculate and write MAC + // h.update(¶ms); + // self.epoch_hour = format!("{}", get_epoch_hour()); + // h.update(self.epoch_hour.as_bytes()); + // let mac = &h.finalize_reset().into_bytes()[..MARK_LENGTH]; + // buf.put(mac); + + // trace!("mark: {}, mac: {}", hex::encode(mark), hex::encode(mac)); + + // Ok(()) + } +} diff --git a/crates/o5/src/framing/messages_base.rs b/crates/o5/src/framing/messages_base.rs new file mode 100644 index 0000000..2a8eed6 --- /dev/null +++ b/crates/o5/src/framing/messages_base.rs @@ -0,0 +1,75 @@ +use crate::framing::{self, FrameError}; + +// TODO: drbg for size sampling +//common::drbg, +// +// use futures::sink::{Sink, SinkExt}; + +use tokio_util::bytes::{Buf, BufMut}; + +use ptrs::trace; + +pub(crate) const MESSAGE_OVERHEAD: usize = 2 + 1; +pub(crate) const MAX_MESSAGE_PAYLOAD_LENGTH: usize = + framing::MAX_FRAME_PAYLOAD_LENGTH - MESSAGE_OVERHEAD; +// pub(crate) const MAX_MESSAGE_PADDING_LENGTH: usize = MAX_MESSAGE_PAYLOAD_LENGTH; + +pub type MessageType = u8; +pub trait Message { + type Output; + fn as_pt(&self) -> MessageType; + + fn marshall(&self, dst: &mut T) -> Result<(), FrameError>; + + fn try_parse(buf: &mut T) -> Result; +} + +/// Frames are: +/// ```txt +/// +----- +/// | type u8; // Message Type +/// M1 | length u16 // Message Length (Big Endian). +/// | payload [u8; length]; // Message Data +/// +----- +/// ... // (optional) more messages M2, M3 ... +/// +----- +/// | type \x00 // minimum padding is 3 bytes (type=\x00 + u16 pad_len=\x00\x00) +/// PAD| pad_len u16 +/// | padding [0u8; pad_len]; +/// +----- +/// ``` +/// +/// Frames must always be composed of COMPLETE mesages, i.e. a message should +/// never be split across multiple frames. +pub fn build_and_marshall( + dst: &mut T, + pt: MessageType, + data: impl AsRef<[u8]>, + pad_len: usize, +) -> Result<(), FrameError> { + // is the provided pad_len too long? + if pad_len > u16::MAX as usize { + Err(FrameError::InvalidPayloadLength(pad_len))? + } + + // is the provided data a reasonable size? + let buf = data.as_ref(); + let total_size = buf.len() + pad_len; + trace!( + "building: total size = {}+{}={} / {MAX_MESSAGE_PAYLOAD_LENGTH}", + buf.len(), + pad_len, + total_size, + ); + if total_size >= MAX_MESSAGE_PAYLOAD_LENGTH { + Err(FrameError::InvalidPayloadLength(total_size))? + } + + dst.put_u8(pt); + dst.put_u16(buf.len() as u16); + dst.put(buf); + if pad_len != 0 { + dst.put_bytes(0_u8, pad_len); + } + Ok(()) +} diff --git a/crates/o5/src/framing/messages_v1/crypto.rs b/crates/o5/src/framing/messages_v1/crypto.rs new file mode 100644 index 0000000..4f2e8f3 --- /dev/null +++ b/crates/o5/src/framing/messages_v1/crypto.rs @@ -0,0 +1,152 @@ +use super::MessageTypes; +use crate::framing::{FrameError, Message, MessageType}; + +#[derive(PartialEq, Debug)] +pub enum CryptoExtension { + Kyber, +} + +#[allow(unused)] +impl CryptoExtension { + pub(crate) fn get_offer() -> impl Message { + KyberOfferMessage {} + } + + pub(crate) fn create_accept() -> impl Message { + KyberAcceptMessage {} + } +} + +#[derive(PartialEq, Debug)] +struct KyberOfferMessage {} + +impl Message for KyberOfferMessage { + type Output = (); + fn as_pt(&self) -> MessageType { + MessageTypes::CryptoOffer.into() + } + + fn marshall(&self, _dst: &mut T) -> Result<(), FrameError> { + Ok(()) + } + + fn try_parse(_buf: &mut T) -> Result { + Ok(()) + } +} + +#[derive(PartialEq, Debug)] +struct KyberAcceptMessage {} + +impl Message for KyberAcceptMessage { + type Output = (); + fn as_pt(&self) -> MessageType { + MessageTypes::CryptoAccept.into() + } + + fn marshall(&self, _dst: &mut T) -> Result<(), FrameError> { + Ok(()) + } + + fn try_parse(_buf: &mut T) -> Result { + Ok(()) + } +} + +#[cfg(test)] +#[allow(unused)] +mod tests { + use pqc_kyber::*; + + use crate::common::curve25519::{PublicKey, Representable}; + use crate::handshake::O5NtorSecretKey; + + type Result = std::result::Result; + + #[derive(Debug)] + enum Error { + PQCError(pqc_kyber::KyberError), + Other(Box), + } + + impl From for Error { + fn from(e: pqc_kyber::KyberError) -> Self { + Error::PQCError(e) + } + } + + struct Kyber1024XPublicKey { + pub kyber1024: pqc_kyber::PublicKey, + pub x25519: PublicKey, + } + + impl From<&Kyber1024XIdentityKeys> for Kyber1024XPublicKey { + fn from(value: &Kyber1024XIdentityKeys) -> Self { + Kyber1024XPublicKey { + x25519: value.x25519.pk.pk, + kyber1024: value.kyber1024.public, + } + } + } + + struct Kyber1024XIdentityKeys { + pub kyber1024: pqc_kyber::Keypair, + pub x25519: O5NtorSecretKey, + } + + impl Kyber1024XIdentityKeys { + fn new() -> Self { + let mut rng = rand::thread_rng(); + + Kyber1024XIdentityKeys { + x25519: O5NtorSecretKey::getrandom(), + kyber1024: pqc_kyber::keypair(&mut rng).expect("kyber1024 key generation failed"), + } + } + + fn from_random(rng: &mut R) -> Self { + Kyber1024XIdentityKeys { + x25519: O5NtorSecretKey::getrandom(), + kyber1024: pqc_kyber::keypair(rng).expect("kyber1024 key generation failed"), + } + } + + fn from_x25519(keys: O5NtorSecretKey, rng: &mut R) -> Self { + Kyber1024XIdentityKeys { + x25519: keys, + kyber1024: pqc_kyber::keypair(rng).expect("kyber1024 key generation failed"), + } + } + } + + #[test] + fn kyber_handshake() -> Result<()> { + let mut rng = rand::thread_rng(); + + // Generate Keypair + let alice_secret = Representable::ephemeral_from_rng(&mut rng); + let alice_public = PublicKey::from(&alice_secret); + let keys_alice = keypair(&mut rng)?; + // alice -> bob public keys + let mut kyber1024x_pubkey = alice_public.as_bytes().to_vec(); + kyber1024x_pubkey.extend_from_slice(&keys_alice.public); + + assert_eq!(kyber1024x_pubkey.len(), 1600); + + let bob_secret = Representable::ephemeral_from_rng(&mut rng); + let bob_public = PublicKey::from(&bob_secret); + + // Bob encapsulates a shared secret using Alice's public key + let (ciphertext, shared_secret_bob) = encapsulate(&keys_alice.public, &mut rng)?; + let bob_shared_secret = bob_secret.diffie_hellman(&alice_public); + + // // Alice decapsulates a shared secret using the ciphertext sent by Bob + let shared_secret_alice = decapsulate(&ciphertext, &keys_alice.secret)?; + let alice_shared_secret = alice_secret.diffie_hellman(&bob_public); + + assert_eq!(alice_shared_secret.as_bytes(), bob_shared_secret.as_bytes()); + assert_eq!(shared_secret_bob, shared_secret_alice); + + Ok(()) + } +} diff --git a/crates/o5/src/framing/messages_v1/mod.rs b/crates/o5/src/framing/messages_v1/mod.rs new file mode 100644 index 0000000..c28d73f --- /dev/null +++ b/crates/o5/src/framing/messages_v1/mod.rs @@ -0,0 +1,256 @@ +//! Version 1 of the Protocol Messagess to be included in constructed frames. + +use crate::{ + constants::*, + framing::{FrameError, MESSAGE_OVERHEAD}, +}; + +use tokio_util::bytes::{Buf, BufMut}; +use tracing::trace; + +#[derive(Debug, PartialEq)] +pub enum MessageTypes { + Payload, + PrngSeed, + Padding, + HeartbeatPing, + HeartbeatPong, + + HandshakeVersion, + ClientParams, + ServerParams, + + HandshakeEnd, +} + +impl MessageTypes { + // Steady state message types (and required backwards compatibility messages) + const PAYLOAD: u8 = 0x00; + const PRNG_SEED: u8 = 0x01; + const PADDING: u8 = 0x02; + const HEARTBEAT_PING: u8 = 0x03; + const HEARTBEAT_PONG: u8 = 0x04; + + // Handshake messages + const HANDSHAKE_VERSION: u8 = 0x10; + const CLIENT_PARAMS: u8 = 0x11; + const SERVER_PARAMS: u8 = 0x11; + //... + + const HANDSHAKE_END: u8 = 0x1f; +} + +impl From for u8 { + fn from(value: MessageTypes) -> Self { + match value { + MessageTypes::Payload => MessageTypes::PAYLOAD, + MessageTypes::PrngSeed => MessageTypes::PRNG_SEED, + MessageTypes::Padding => MessageTypes::PADDING, + MessageTypes::HeartbeatPing => MessageTypes::HEARTBEAT_PING, + MessageTypes::HeartbeatPong => MessageTypes::HEARTBEAT_PONG, + MessageTypes::HandshakeVersion => MessageTypes::HANDSHAKE_VERSION, + MessageTypes::ClientParams => MessageTypes::CLIENT_PARAMS, + MessageTypes::ServerParams => MessageTypes::SERVER_PARAMS, + MessageTypes::HandshakeEnd => MessageTypes::HANDSHAKE_END, + } + } +} + +impl TryFrom for MessageTypes { + type Error = FrameError; + fn try_from(value: u8) -> Result { + match value { + MessageTypes::PAYLOAD => Ok(MessageTypes::Payload), + MessageTypes::PRNG_SEED => Ok(MessageTypes::PrngSeed), + _ => Err(FrameError::UnknownMessageType(value)), + } + } +} + +#[derive(Debug, PartialEq)] +pub enum Messages { + Payload(Vec), + PrngSeed([u8; SEED_LENGTH]), + Padding(u16), + HeartbeatPing, + HeartbeatPong, + + ClientParams, + ServerParams, + HandshakeVersion, + + HandshakeEnd, +} + +impl Messages { + pub(crate) fn as_pt(&self) -> MessageTypes { + match self { + Messages::Payload(_) => MessageTypes::Payload, + Messages::PrngSeed(_) => MessageTypes::PrngSeed, + Messages::Padding(_) => MessageTypes::Padding, + Messages::HeartbeatPing => MessageTypes::HeartbeatPing, + Messages::HeartbeatPong => MessageTypes::HeartbeatPong, + Messages::HandshakeVersion => MessageTypes::HandshakeVersion, + Messages::ClientParams => MessageTypes::ClientParams, + Messages::ServerParams => MessageTypes::ServerParams, + Messages::HandshakeEnd => MessageTypes::HandshakeEnd, + } + } + + pub(crate) fn marshall(&self, dst: &mut T) -> Result<(), FrameError> { + dst.put_u8(self.as_pt().into()); + match self { + Messages::Payload(buf) => { + dst.put_u16(buf.len() as u16); + dst.put(&buf[..]); + } + Messages::PrngSeed(buf) => { + dst.put_u16(buf.len() as u16); + dst.put(&buf[..]); + } + Messages::Padding(pad_len) => { + dst.put_u16(*pad_len); + if *pad_len > 0 { + let buf = vec![0_u8; *pad_len as usize]; + dst.put(&buf[..]); + } + } + + _ => { + dst.put_u16(0_u16); + } + } + Ok(()) + } + + pub(crate) fn try_parse(buf: &mut T) -> Result { + if buf.remaining() < MESSAGE_OVERHEAD { + Err(FrameError::InvalidMessage)? + } + let pt: MessageTypes = buf.get_u8().try_into()?; + let length = buf.get_u16() as usize; + + match pt { + MessageTypes::Payload => { + let mut dst = vec![]; + dst.put(buf.take(length)); + trace!("{}B remainng", buf.remaining()); + assert_eq!(buf.remaining(), Self::drain_padding(buf)); + Ok(Messages::Payload(dst)) + } + + MessageTypes::PrngSeed => { + let mut seed = [0_u8; 24]; + buf.copy_to_slice(&mut seed[..]); + assert_eq!(buf.remaining(), Self::drain_padding(buf)); + Ok(Messages::PrngSeed(seed)) + } + + MessageTypes::Padding => Ok(Messages::Padding(length as u16)), + + MessageTypes::HeartbeatPing => Ok(Messages::HeartbeatPing), + + MessageTypes::HeartbeatPong => Ok(Messages::HeartbeatPong), + + MessageTypes::HandshakeVersion => Ok(Messages::HandshakeVersion), + + MessageTypes::ClientParams => Ok(Messages::ClientParams), + + MessageTypes::ServerParams => Ok(Messages::ServerParams), + + MessageTypes::HandshakeEnd => Ok(Messages::HandshakeEnd), + } + } + + fn drain_padding(b: &mut T) -> usize { + if !b.has_remaining() { + return 0; + } + + let length = b.remaining(); + let mut count = length; + // make a shallow copy that we can work with so that we can continually + // check first byte without actually removing it (advancing the pointer + // in the Bytes object). + let mut buf = b.copy_to_bytes(b.remaining()); + for i in 0..length { + if buf[0] != 0 { + count = i; + break; + } + _ = buf.get_u8(); + } + + b.put(buf); + trace!("drained {count}B, {}B remaining", b.remaining(),); + count + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::framing::*; + use crate::test_utils::init_subscriber; + + use rand::prelude::*; + use tokio_util::bytes::BytesMut; + + #[test] + fn drain_padding() { + init_subscriber(); + let test_cases = [ + ("", 0, 0), + ("00", 1, 0), + ("0000", 2, 0), + ("0000000000000000", 8, 0), + ("000000000000000001", 8, 1), + ("0000010000000000", 2, 6), + ("0102030000000000", 0, 8), + ]; + + for case in test_cases { + let buf = hex::decode(case.0).expect("failed to decode hex"); + let mut b = BytesMut::from(&buf as &[u8]); + let cnt = Messages::drain_padding(&mut b); + assert_eq!(cnt, case.1); + assert_eq!(b.remaining(), case.2); + } + } + + #[test] + fn prngseed() -> Result<(), FrameError> { + init_subscriber(); + + let mut buf = BytesMut::new(); + let mut rng = rand::thread_rng(); + let pad_len = rng.gen_range(0..100); + let mut seed = [0_u8; SEED_LENGTH]; + rng.fill_bytes(&mut seed); + + build_and_marshall(&mut buf, MessageTypes::PrngSeed.into(), seed, pad_len)?; + + let pkt = Messages::try_parse(&mut buf)?; + assert_eq!(Messages::PrngSeed(seed), pkt); + + Ok(()) + } + + #[test] + fn payload() -> Result<(), FrameError> { + init_subscriber(); + + let mut buf = BytesMut::new(); + let mut rng = rand::thread_rng(); + let pad_len = rng.gen_range(0..100); + let mut payload = [0_u8; 1000]; + rng.fill_bytes(&mut payload); + + build_and_marshall(&mut buf, MessageTypes::Payload.into(), payload, pad_len)?; + + let pkt = Messages::try_parse(&mut buf)?; + assert_eq!(Messages::Payload(payload.to_vec()), pkt); + + Ok(()) + } +} diff --git a/crates/o5/src/framing/mod.rs b/crates/o5/src/framing/mod.rs index c1fbecf..047bade 100644 --- a/crates/o5/src/framing/mod.rs +++ b/crates/o5/src/framing/mod.rs @@ -1 +1,189 @@ -mod packet; +/// Package framing implements the obfs4 link framing and cryptography. +/// +/// The Encoder/Decoder shared secret format is: +/// +/// ```txt +/// NaCl_secretbox_key [u8; 32]; +/// NaCl_Nonce_prefix [u8; 16]; +/// SipHash_24_key [u8; 16]; // (used to obfsucate length) +/// SipHash_24_IV [u8; 8]; +/// ``` +/// +/// The frame format is: +/// +/// ```txt +/// length u16; // (obfsucated, big endian) +/// // NaCl secretbox (Poly1305/XChaCha20) containing: +/// tag [u8; 16]; // (Part of the secretbox construct) +/// payload [u8]; +/// ``` +/// +/// The length field is length of the NaCl secretbox XORed with the truncated +/// SipHash-2-4 digest ran in OFB mode. +/// +/// ```txt +/// // Initialize K, IV[0] with values from the shared secret. +/// // On each packet, IV[n] = H(K, IV[n - 1]) +/// // mask_n = IV[n][0:2] +/// // obfs_len = length ^ mask[n] +/// ``` +/// +/// The NaCl secretbox (Poly1305/XChaCha20) nonce format is: +/// +/// ```txt +/// prefix [u8; 24]; //(Fixed) +/// counter u64; // (Big endian) +/// ``` +/// +/// The counter is initialized to 1, and is incremented on each frame. Since +/// the protocol is designed to be used over a reliable medium, the nonce is not +/// transmitted over the wire as both sides of the conversation know the prefix +/// and the initial counter value. It is imperative that the counter does not +/// wrap, and sessions MUST terminate before 2^64 frames are sent. +use crate::common::drbg; +use bytes::{Buf, BufMut}; + +mod messages_base; +pub use messages_base::*; + +mod messages_v1; +pub use messages_v1::{MessageTypes, Messages}; + +mod codecs; +pub use codecs::EncryptingCodec as O5Codec; + +pub(crate) mod handshake; +pub use handshake::*; + +/// MaximumSegmentLength is the length of the largest possible segment +/// including overhead. +pub(crate) const MAX_SEGMENT_LENGTH: usize = 1500 - (40 + 12); + +/// secret box overhead is fixed length prefix and counter +const SECRET_BOX_OVERHEAD: usize = TAG_SIZE; + +/// FrameOverhead is the length of the framing overhead. +pub(crate) const FRAME_OVERHEAD: usize = LENGTH_LENGTH + SECRET_BOX_OVERHEAD; + +/// MaximumFramePayloadLength is the length of the maximum allowed payload +/// per frame. +pub(crate) const MAX_FRAME_PAYLOAD_LENGTH: usize = MAX_SEGMENT_LENGTH - FRAME_OVERHEAD; + +// pub(crate) const MAX_FRAME_LENGTH: usize = MAX_SEGMENT_LENGTH - LENGTH_LENGTH; +// pub(crate) const MIN_FRAME_LENGTH: usize = FRAME_OVERHEAD - LENGTH_LENGTH; + +pub(crate) const NONCE_PREFIX_LENGTH: usize = 16; +// pub(crate) const NONCE_COUNTER_LENGTH: usize = 8; +// pub(crate) const NONCE_LENGTH: usize = NONCE_PREFIX_LENGTH + NONCE_COUNTER_LENGTH; + +/// length in bytes of the `Length` field at the front of a Frame. Converted to +/// big-endian u16 when decoding. +pub(crate) const LENGTH_LENGTH: usize = 2; + +/// KEY_LENGTH is the length of the Encoder/Decoder secret key. +pub(crate) const KEY_LENGTH: usize = 32; + +/// Size of the HMAC tag used for the frame security. +pub(crate) const TAG_SIZE: usize = 16; + +/// This is the expected length of the Key material that is used to seed the +/// encrypting / decryptong codec, i.e. in framing/codec and handshake/ +pub(crate) const KEY_MATERIAL_LENGTH: usize = KEY_LENGTH + NONCE_PREFIX_LENGTH + drbg::SEED_LENGTH; + +pub trait Marshall { + fn marshall(&mut self, buf: &mut impl BufMut) -> Result<(), FrameError>; +} + +pub trait TryParse { + type Output; + fn try_parse(&mut self, buf: &mut impl Buf) -> Result + where + Self: Sized; +} + +impl std::error::Error for FrameError {} + +#[derive(Debug, PartialEq, Eq)] +pub enum FrameError { + /// is the error returned when [`encode`] rejects the payload length. + InvalidPayloadLength(usize), + + /// A cryptographic error occured. + Crypto(crypto_secretbox::Error), + + /// An error occured with the I/O processing + IO(String), + + /// Returned when [`decode`] requires more data to continue. + EAgain, + + /// Returned when [`decode`] failes to authenticate a frame. + TagMismatch, + + /// Returned when the NaCl secretbox nonce's counter wraps (FATAL). + NonceCounterWrapped, + + /// Returned when the buffer provided for writing a frame is too small. + ShortBuffer, + + /// Error indicating that a message decoded, or a message provided for + /// encoding is of an innapropriate type for the context. + InvalidMessage, + + /// Failed while trying to parse a handshake message + InvalidHandshake, + + /// Received either a REALLY unfortunate random, or a replayed handshake message + ReplayedHandshake, + + /// An unknown packet type was received in a non-handshake packet frame. + UnknownMessageType(u8), +} + +impl std::fmt::Display for FrameError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FrameError::InvalidPayloadLength(s) => { + write!(f, "framing: Invalid payload length: {s}") + } + FrameError::Crypto(e) => write!(f, "framing: Secretbox encrypt/decrypt error: {e}"), + FrameError::IO(e) => { + write!(f, "framing: i/o error occured while processing frame: {e}") + } + FrameError::EAgain => write!(f, "framing: more data needed to decode"), + FrameError::TagMismatch => write!(f, "framing: Poly1305 tag mismatch"), + FrameError::NonceCounterWrapped => write!(f, "framing: Nonce counter wrapped"), + FrameError::ShortBuffer => write!( + f, + "framing: provided bytes buffer was too short for payload" + ), + FrameError::InvalidMessage => write!(f, "framing: incorrect message for context"), + FrameError::InvalidHandshake => write!(f, "framing: failed to parse handshake message"), + FrameError::ReplayedHandshake => write!(f, "framing: handshake replayed within TTL"), + FrameError::UnknownMessageType(pt) => write!(f, "framing: unknown packet type ({pt})"), + } + } +} + +impl From for FrameError { + fn from(value: crypto_secretbox::Error) -> Self { + FrameError::Crypto(value) + } +} + +impl From for FrameError { + fn from(value: std::io::Error) -> Self { + FrameError::IO(value.to_string()) + } +} + +impl From for std::io::Error { + fn from(value: FrameError) -> Self { + std::io::Error::new(std::io::ErrorKind::Other, format!("{}", value)) + } +} + +#[cfg(test)] +mod generic_test; +#[cfg(test)] +mod testing; diff --git a/crates/o5/src/framing/testing.rs b/crates/o5/src/framing/testing.rs new file mode 100644 index 0000000..3fe2dd0 --- /dev/null +++ b/crates/o5/src/framing/testing.rs @@ -0,0 +1,211 @@ +/// testing out tokio_util::codec for building transports. +/// +/// useful links: +/// - https://dev.to/jtenner/creating-a-tokio-codec-1f0l +/// - example telnet implementation using codecs +/// - https://github.com/jtenner/telnet_codec +/// +/// - https://docs.rs/tokio-util/latest/tokio_util/codec/index.html +/// - tokio_util codec docs +/// +use super::*; +use crate::test_utils::init_subscriber; +use crate::Result; + +use bytes::{Bytes, BytesMut}; +use futures::{SinkExt, StreamExt}; +use rand::prelude::*; +use tokio_util::codec::{Decoder, Encoder}; +use tracing::{debug, trace}; + +fn random_key_material() -> [u8; KEY_MATERIAL_LENGTH] { + let mut r = [0_u8; KEY_MATERIAL_LENGTH]; + getrandom::getrandom(&mut r).unwrap(); + r +} + +#[test] +fn encode_decode() -> Result<()> { + init_subscriber(); + let message = b"Hello there".to_vec(); + let mut key_material = [0_u8; KEY_MATERIAL_LENGTH]; + rand::thread_rng().fill(&mut key_material[..]); + + let mut codec = O5Codec::new(key_material, key_material); + + let mut b = bytes::BytesMut::with_capacity(LENGTH_LENGTH + MESSAGE_OVERHEAD + message.len()); + let mut input = BytesMut::new(); + build_and_marshall(&mut input, MessageTypes::Payload.into(), message.clone(), 0)?; + codec.encode(&mut input, &mut b)?; + + let Messages::Payload(plaintext) = codec.decode(&mut b)?.expect("failed to decode") else { + panic!("f") + }; + assert_eq!(plaintext, message); + + Ok(()) +} + +#[tokio::test] +async fn basic_flow() -> Result<()> { + init_subscriber(); + let message = b"Hello there"; + let key_material = [0_u8; KEY_MATERIAL_LENGTH]; + + try_flow(key_material, message.to_vec()).await +} + +#[tokio::test] +async fn oversized_flow() -> Result<()> { + init_subscriber(); + let frame_len = MAX_FRAME_PAYLOAD_LENGTH + 1; + let oversized_messsage = vec![65_u8; frame_len]; + let key_material = [0_u8; KEY_MATERIAL_LENGTH]; + + let mut b = bytes::BytesMut::with_capacity(2_usize.pow(13)); + let mut codec = O5Codec::new(key_material, key_material); + let mut src = Bytes::from(oversized_messsage); + let res = codec.encode(&mut src, &mut b); + + assert_eq!( + res.unwrap_err(), + FrameError::InvalidPayloadLength(frame_len) + ); + Ok(()) +} + +#[tokio::test] +async fn many_sizes_flow() -> Result<()> { + init_subscriber(); + for l in MAX_FRAME_PAYLOAD_LENGTH - 6..(MAX_FRAME_PAYLOAD_LENGTH - MESSAGE_OVERHEAD) { + let key_material = random_key_material(); + let message = vec![65_u8; l]; + debug!("\n\n{l}, {}", message.len()); + tokio::select! { + res = try_flow(key_material, message) => { + res?; + }, + _ = tokio::time::sleep(tokio::time::Duration::from_secs(3)) => { + panic!("timed out for {l}"); + }, + } + } + Ok(()) +} + +async fn try_flow(key_material: [u8; KEY_MATERIAL_LENGTH], msg: Vec) -> Result<()> { + let (c, s) = tokio::io::duplex(16 * 1024); + + let msg_s = msg.clone(); + + tokio::spawn(async move { + let codec = O5Codec::new(key_material, key_material); + let message = &msg_s; + + let (mut sink, mut input) = codec.framed(s).split(); + + while let Some(Ok(event)) = input.next().await { + if let Messages::Payload(m) = event { + assert_eq!(&m, &message.clone()); + trace!("Event {:?}", String::from_utf8(m.clone()).unwrap()); + + let mut b = BytesMut::new(); + build_and_marshall(&mut b, MessageTypes::Payload.into(), &m, 0).unwrap(); + sink.send(b).await.expect("server response failed"); + } else { + panic!("failed while reading from codec"); + } + } + }); + + let mut message = BytesMut::new(); + build_and_marshall(&mut message, MessageTypes::Payload.into(), &msg, 0)?; + + let client_codec = O5Codec::new(key_material, key_material); + let (mut c_sink, mut c_stream) = client_codec.framed(c).split(); + + c_sink.send(&mut message).await.expect("client send failed"); + trace!("client write success"); + + if let Messages::Payload(m) = c_stream + .next() + .await + .unwrap_or_else(|| { + panic!( + "you were supposed to call me back!, {} (max={})", + message.len(), + MAX_FRAME_PAYLOAD_LENGTH + ) + }) + .expect("an error occured when you called back") + { + // skip over length field in the Payload message + assert_eq!(&m, &msg); + trace!("client read success"); + } else { + panic!("failed while reading from codec"); + } + + Ok(()) +} + +#[tokio::test] +async fn double_encode_decode() -> Result<()> { + // println!(); + init_subscriber(); + let (c, s) = tokio::io::duplex(16 * 1024); + let msg = b"j dkja ;ae ;awena woea;wfel rfawe"; + let plain_msg = Messages::Payload(msg.to_vec()); + let mut pkt1 = BytesMut::new(); + plain_msg.marshall(&mut pkt1)?; + let mut pkt2 = pkt1.clone(); + + let key_material = random_key_material(); + let client_codec = O5Codec::new(key_material, key_material); + let (mut c_sink, mut c_stream) = client_codec.framed(c).split(); + let server_codec = O5Codec::new(key_material, key_material); + let (mut s_sink, mut s_stream) = server_codec.framed(s).split::(); + + c_sink.send(&mut pkt1).await.expect("client send failed"); + c_sink.send(&mut pkt2).await.expect("client send failed"); + + for i in 0..2 { + let Some(Ok(event)) = s_stream.next().await else { + panic!("read none!!!") + }; + if let Messages::Payload(m) = event { + assert_eq!(&m, &msg.clone()); + trace!("Event-{i} {:?}", String::from_utf8(m.clone()).unwrap()); + + let mut msg = BytesMut::new(); + Messages::Payload(m).marshall(&mut msg)?; + + s_sink.send(msg.freeze()).await?; + } else { + panic!("failed while reading from codec"); + } + } + + for i in 0..2 { + if let Messages::Payload(m) = c_stream + .next() + .await + .unwrap_or_else(|| { + panic!( + "you were supposed to call me back!, {} (max={})", + msg.len(), + MAX_FRAME_PAYLOAD_LENGTH + ) + }) + .expect("an error occured when you called back") + { + // skip over length field in the Payload message + assert_eq!(&m, &msg); + trace!("client read {i} success"); + } else { + panic!("failed while reading from codec"); + } + } + + Ok(()) +} diff --git a/crates/o5/src/handshake.rs b/crates/o5/src/handshake.rs new file mode 100644 index 0000000..09c2ee0 --- /dev/null +++ b/crates/o5/src/handshake.rs @@ -0,0 +1,489 @@ +//! Implements the ntor v3 key exchange, as described in proposal 332. +//! +//! The main difference between the ntor v3r handshake and the +//! original ntor handshake is that this this one allows each party to +//! encrypt data (without forward secrecy) after it sends the first +//! message. + +use crate::sessions::SessionPublicKey; + +use cipher::{KeyIvInit as _, StreamCipher as _}; +use digest::{Digest, ExtendableOutput, XofReader}; +use tor_bytes::{EncodeResult, Writeable, Writer}; +use tor_llcrypto::cipher::aes::Aes256Ctr; +use tor_llcrypto::d::{Sha3_256, Shake256}; +use zeroize::Zeroizing; + +mod keys; +use keys::NtorV3XofReader; +pub(crate) use keys::{Authcode, AUTHCODE_LENGTH}; +pub use keys::{IdentityPublicKey, IdentitySecretKey, NtorV3KeyGen}; + +/// Super trait to be used where we require a distinction between client and server roles. +trait Role { + fn is_client() -> bool; +} + +struct ClientRole {} +impl Role for ClientRole { + fn is_client() -> bool { + true + } +} + +struct ServerRole {} +impl Role for ServerRole { + fn is_client() -> bool { + false + } +} + +mod client; +pub(crate) use client::{HandshakeMaterials as CHSMaterials, NtorV3Client}; + +mod server; +pub(crate) use server::HandshakeMaterials as SHSMaterials; + +use crate::common::mlkem1024_x25519; + +/// The verification string to be used for circuit extension. +pub const NTOR3_CIRC_VERIFICATION: &[u8] = b"circuit extend"; + +/// The size of an encryption key in bytes. +pub const ENC_KEY_LEN: usize = 32; +/// The size of a MAC key in bytes. +pub const MAC_KEY_LEN: usize = 32; +/// The size of a digest output in bytes. +pub const DIGEST_LEN: usize = 32; +/// The length of a MAC output in bytes. +pub const MAC_LEN: usize = 32; +/// The length of a node identity in bytes. +pub const ID_LEN: usize = 32; + +/// The output of the digest, as an array. +type DigestVal = [u8; DIGEST_LEN]; +/// The output of the MAC. +type MessageMac = [u8; MAC_LEN]; +/// A key for symmetric encryption or decryption. +type EncKey = Zeroizing<[u8; ENC_KEY_LEN]>; +/// A key for message authentication codes. +type MacKey = [u8; MAC_KEY_LEN]; + +/// An encapsulated value for passing as input to a MAC, digest, or +/// KDF algorithm. +/// +/// This corresponds to the ENCAP() function in proposal 332. +struct Encap<'a>(&'a [u8]); + +impl<'a> Writeable for Encap<'a> { + fn write_onto(&self, b: &mut B) -> EncodeResult<()> { + b.write_u64(self.0.len() as u64); + b.write(self.0) + } +} + +impl<'a> Encap<'a> { + /// Return the length of the underlying data in bytes. + fn len(&self) -> usize { + self.0.len() + } + /// Return the underlying data + fn data(&self) -> &'a [u8] { + self.0 + } +} + +/// Helper to define a set of tweak values as instances of `Encap`. +macro_rules! define_tweaks { + { + $(#[$pid_meta:meta])* + PROTOID = $protoid:expr; + $( $(#[$meta:meta])* $name:ident <= $suffix:expr ; )* + } => { + $(#[$pid_meta])* + const PROTOID: &'static [u8] = $protoid.as_bytes(); + $( + $(#[$meta])* + const $name : Encap<'static> = + Encap(concat!($protoid, ":", $suffix).as_bytes()); + )* + } +} + +pub(crate) const T_KEY: &[u8; 36] = b"ntor-curve25519-sha256-1:key_extract"; + +define_tweaks! { + /// Protocol ID: concatenated with other things in the protocol to + /// prevent hash confusion. + PROTOID = "ntor3-curve25519-sha3_256-1"; + + /// Message MAC tweak: used to compute the MAC of an encrypted client + /// message. + // in obfs4 -> b"ntor-curve25519-sha256-1:mac" + T_MSGMAC <= "msg_mac"; + /// Message KDF tweak: used when deriving keys for encrypting and MACing + /// client message. + T_MSGKDF <= "kdf_phase1"; + /// Key seeding tweak: used to derive final KDF input from secret_input. + T_KEY_SEED <= "key_seed"; + /// Verifying tweak: used to derive 'verify' value from secret_input. + // in obfs4 -> b"ntor-curve25519-sha256-1:key_verify" + T_VERIFY <= "verify"; + /// Final KDF tweak: used to derive keys for encrypting relay message + /// and for the actual tor circuit. + T_FINAL <= "kdf_final"; + /// Authentication tweak: used to derive the final authentication + /// value for the handshake. + T_AUTH <= "auth_final"; + /// Key Expansion Tweak: obfs4 tweak used for expanding seed into key + M_EXPAND <= "key_expand"; +} + +/// Compute a tweaked hash. +fn hash(t: &Encap<'_>, data: &[u8]) -> DigestVal { + let mut d = Sha3_256::new(); + d.update((t.len() as u64).to_be_bytes()); + d.update(t.data()); + d.update(data); + d.finalize().into() +} + +/// Perform a symmetric encryption operation and return the encrypted data. +/// +/// (This isn't safe to do more than once with the same key, but we never +/// do that in this protocol.) +fn encrypt(key: &EncKey, m: &[u8]) -> Vec { + let mut d = m.to_vec(); + let zero_iv = Default::default(); + let k: &[u8; 32] = key; + let mut cipher = Aes256Ctr::new(k.into(), &zero_iv); + cipher.apply_keystream(&mut d); + d +} +/// Perform a symmetric decryption operation and return the encrypted data. +fn decrypt(key: &EncKey, m: &[u8]) -> Vec { + encrypt(key, m) +} + +/// Wrapper around a Digest or ExtendedOutput object that lets us use it +/// as a tor_bytes::Writer. +struct DigestWriter(U); +impl tor_bytes::Writer for DigestWriter { + fn write_all(&mut self, bytes: &[u8]) { + self.0.update(bytes); + } +} +impl DigestWriter { + /// Consume this wrapper and return the underlying object. + fn take(self) -> U { + self.0 + } +} + +/// Hash tweaked with T_KEY_SEED +fn h_key_seed(d: &[u8]) -> DigestVal { + hash(&T_KEY_SEED, d) +} +/// Hash tweaked with T_VERIFY +fn h_verify(d: &[u8]) -> DigestVal { + hash(&T_VERIFY, d) +} + +/// Helper: compute the encryption key and mac_key for the client's +/// encrypted message. +/// +/// Takes as inputs `xb` (the shared secret derived from +/// diffie-hellman as Bx or Xb), the relay's public key information, +/// the client's public key (B), and the shared verification string. +fn kdf_msgkdf( + xb: &mlkem1024_x25519::SharedSecret, + relay_public: &IdentityPublicKey, + client_public: &SessionPublicKey, + verification: &[u8], +) -> EncodeResult<(EncKey, DigestWriter)> { + // secret_input_phase1 = Bx | ID | X | B | PROTOID | ENCAP(VER) + // phase1_keys = KDF_msgkdf(secret_input_phase1) + // (ENC_K1, MAC_K1) = PARTITION(phase1_keys, ENC_KEY_LEN, MAC_KEY_LEN + let mut msg_kdf = DigestWriter(Shake256::default()); + msg_kdf.write(&T_MSGKDF)?; + msg_kdf.write(&xb.as_bytes())?; + msg_kdf.write(&relay_public.id)?; + msg_kdf.write(&client_public.as_bytes())?; + msg_kdf.write(&relay_public.pk.as_bytes())?; + msg_kdf.write(PROTOID)?; + msg_kdf.write(&Encap(verification))?; + let mut r = msg_kdf.take().finalize_xof(); + let mut enc_key = Zeroizing::new([0; ENC_KEY_LEN]); + let mut mac_key = Zeroizing::new([0; MAC_KEY_LEN]); + + r.read(&mut enc_key[..]); + r.read(&mut mac_key[..]); + let mut mac = DigestWriter(Sha3_256::default()); + { + mac.write(&T_MSGMAC)?; + mac.write(&Encap(&mac_key[..]))?; + mac.write(&relay_public.id)?; + mac.write(&relay_public.pk.as_bytes())?; + mac.write(&client_public.as_bytes())?; + } + + Ok((enc_key, mac)) +} + +/// Trait for an object that handle and incoming client message and +/// return a server's reply. +/// +/// This is implemented for `FnMut(&[u8]) -> Option>` automatically. +pub(crate) trait MsgReply { + /// Given a message received from a client, parse it and decide + /// how (and whether) to reply. + /// + /// Return None if the handshake should fail. + fn reply(&mut self, msg: &[u8]) -> Option>; +} + +impl MsgReply for F +where + F: FnMut(&[u8]) -> Option>, +{ + fn reply(&mut self, msg: &[u8]) -> Option> { + self(msg) + } +} + +#[cfg(test)] +#[allow(non_snake_case)] // to enable variable names matching the spec. +#[allow(clippy::many_single_char_names)] // ibid +mod test { + // @@ begin test lint list maintained by maint/add_warning @@ + #![allow(clippy::bool_assert_comparison)] + #![allow(clippy::clone_on_copy)] + #![allow(clippy::dbg_macro)] + #![allow(clippy::mixed_attributes_style)] + #![allow(clippy::print_stderr)] + #![allow(clippy::print_stdout)] + #![allow(clippy::single_char_pattern)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::unchecked_duration_subtraction)] + #![allow(clippy::useless_vec)] + #![allow(clippy::needless_pass_by_value)] + //! + use crate::common::mlkem1024_x25519::{PublicKey, StaticSecret}; + use crate::common::ntor_arti::{ClientHandshake, KeyGenerator, ServerHandshake}; + use crate::constants::{NODE_ID_LENGTH, SEED_LENGTH}; + use crate::Server; + + use super::*; + use crate::{handshake::IdentitySecretKey, sessions::SessionSecretKey}; + + use hex::FromHex; + use hex_literal::hex; + use rand::thread_rng; + use tor_basic_utils::test_rng::testing_rng; + use tor_cell::relaycell::extend::NtorV3Extension; + + #[test] + fn test_ntor3_roundtrip() { + let mut rng = rand::thread_rng(); + let relay_private = IdentitySecretKey::random_from_rng(&mut testing_rng()); + + let verification = &b"shared secret"[..]; + let client_message = &b"Hello. I am a client. Let's be friends!"[..]; + let relay_message = &b"Greetings, client. I am a robot. Beep boop."[..]; + let materials = CHSMaterials::new(&relay_private.pk, "fake_session_id-1".into()); + + let (c_state, c_handshake) = + client::client_handshake_ntor_v3(&mut rng, materials, verification).unwrap(); + + struct Rep(Vec, Vec); + impl MsgReply for Rep { + fn reply(&mut self, msg: &[u8]) -> Option> { + self.0 = msg.to_vec(); + Some(self.1.clone()) + } + } + let mut rep = Rep(Vec::new(), relay_message.to_vec()); + + let (s_handshake, mut s_keygen) = server::server_handshake_ntor_v3( + &mut rng, + &mut rep, + &c_handshake, + &[relay_private], + verification, + ) + .unwrap(); + + let (s_msg, mut c_keygen) = + client::client_handshake_ntor_v3_part2(&c_state, &s_handshake, verification).unwrap(); + + assert_eq!(rep.0[..], client_message[..]); + assert_eq!(s_msg[..], relay_message[..]); + let mut s_keys = [0_u8; 100]; + let mut c_keys = [0_u8; 1000]; + s_keygen.read(&mut s_keys); + c_keygen.read(&mut c_keys); + assert_eq!(s_keys[..], c_keys[..100]); + } + + // Same as previous test, but use the higher-level APIs instead. + #[test] + fn test_ntor3_roundtrip_highlevel() { + let relay_private = IdentitySecretKey::random_from_rng(&mut testing_rng()); + + let materials = CHSMaterials::new(&relay_private.pk, "fake_session_id-1".into()); + let (c_state, c_handshake) = NtorV3Client::client1(materials).unwrap(); + + let mut rep = |_: &[NtorV3Extension]| Some(vec![]); + + let server = Server::new_from_random(&mut thread_rng()); + let shs_materials = SHSMaterials { + len_seed: [0u8; SEED_LENGTH], + session_id: "roundtrip_test_serverside".into(), + }; + let (s_keygen, s_handshake) = server + .server(&mut rep, &shs_materials, &c_handshake) + .unwrap(); + + let (extensions, keygen) = NtorV3Client::client2(c_state, s_handshake).unwrap(); + + assert!(extensions.is_empty()); + let c_keys = keygen.expand(1000).unwrap(); + let s_keys = s_keygen.expand(100).unwrap(); + assert_eq!(s_keys[..], c_keys[..100]); + } + + // Same as previous test, but encode some congestion control extensions. + #[test] + fn test_ntor3_roundtrip_highlevel_cc() { + let relay_private = IdentitySecretKey::random_from_rng(&mut testing_rng()); + + let client_exts = vec![NtorV3Extension::RequestCongestionControl]; + let reply_exts = vec![NtorV3Extension::AckCongestionControl { sendme_inc: 42 }]; + let materials = CHSMaterials::new(&relay_private.pk, "client_session_1".into()) + .with_aux_data([NtorV3Extension::RequestCongestionControl]); + + let (c_state, c_handshake) = NtorV3Client::client1(materials).unwrap(); + + let mut rep = |msg: &[NtorV3Extension]| -> Option> { + assert_eq!(msg, client_exts); + Some(reply_exts.clone()) + }; + + let shs_materials = SHSMaterials { + len_seed: [0u8; SEED_LENGTH], + session_id: "roundtrip_test_serverside".into(), + }; + let server = Server::new_from_random(&mut thread_rng()); + let (s_keygen, s_handshake) = server + .server(&mut rep, &shs_materials, &c_handshake) + .unwrap(); + + let (extensions, keygen) = NtorV3Client::client2(c_state, s_handshake).unwrap(); + + assert_eq!(extensions, reply_exts); + let c_keys = keygen.expand(1000).unwrap(); + let s_keys = s_keygen.expand(100).unwrap(); + assert_eq!(s_keys[..], c_keys[..100]); + } + + #[test] + fn test_ntor3_testvec() { + let mut rng = rand::thread_rng(); + let b = hex!("4051daa5921cfa2a1c27b08451324919538e79e788a81b38cbed097a5dff454a"); + let id = <[u8; NODE_ID_LENGTH]>::from_hex( + "aaaaaaaaaaaaaaaaaaaaaaaa9fad2af287ef942632833d21f946c6260c33fae6", + ) + .unwrap(); + let x = hex!("b825a3719147bcbe5fb1d0b0fcb9c09e51948048e2e3283d2ab7b45b5ef38b49"); + let y = hex!("4865a5b7689dafd978f529291c7171bc159be076b92186405d13220b80e2a053"); + let b: StaticSecret = b.into(); + let B: PublicKey = (&b).into(); + let x: SessionSecretKey = x.into(); + //let X = (&x).into(); + let y: StaticSecret = y.into(); + + let client_message = hex!("68656c6c6f20776f726c64"); + let verification = hex!("78797a7a79"); + let server_message = hex!("486f6c61204d756e646f"); + + let relay_private = IdentitySecretKey::new(b, id.into()); + let relay_public = relay_private.pk; // { pk: B, id }; + + let mut chs_materials = CHSMaterials::new(&relay_public, "".into()); + let (state, client_handshake) = + client::client_handshake_ntor_v3_no_keygen(x, chs_materials, &verification).unwrap(); + + assert_eq!(client_handshake[..], hex!("9fad2af287ef942632833d21f946c6260c33fae6172b60006e86e4a6911753a2f8307a2bc1870b00b828bb74dbb8fd88e632a6375ab3bcd1ae706aaa8b6cdd1d252fe9ae91264c91d4ecb8501f79d0387e34ad8ca0f7c995184f7d11d5da4f463bebd9151fd3b47c180abc9e044d53565f04d82bbb3bebed3d06cea65db8be9c72b68cd461942088502f67")[..]); + + struct Replier(Vec, Vec, bool); + impl MsgReply for Replier { + fn reply(&mut self, msg: &[u8]) -> Option> { + assert_eq!(msg, &self.0); + self.2 = true; + Some(self.1.clone()) + } + } + let mut rep = Replier(client_message.to_vec(), server_message.to_vec(), false); + + let (server_handshake, mut server_keygen) = server::server_handshake_ntor_v3_no_keygen( + &mut rng, + &mut rep, + &y, + &client_handshake, + &relay_private, + &verification, + ) + .unwrap(); + assert!(rep.2); + + assert_eq!(server_handshake[..], hex!("4bf4814326fdab45ad5184f5518bd7fae25dc59374062698201a50a22954246d2fc5f8773ca824542bc6cf6f57c7c29bbf4e5476461ab130c5b18ab0a91276651202c3e1e87c0d32054c")[..]); + + let (server_msg_received, mut client_keygen) = + client::client_handshake_ntor_v3_part2(&state, &server_handshake, &verification) + .unwrap(); + assert_eq!(&server_msg_received, &server_message); + + let (c_keys, s_keys) = { + let mut c = [0_u8; 256]; + let mut s = [0_u8; 256]; + client_keygen.read(&mut c); + server_keygen.read(&mut s); + (c, s) + }; + assert_eq!(c_keys, s_keys); + assert_eq!(c_keys[..], hex!("9c19b631fd94ed86a817e01f6c80b0743a43f5faebd39cfaa8b00fa8bcc65c3bfeaa403d91acbd68a821bf6ee8504602b094a254392a07737d5662768c7a9fb1b2814bb34780eaee6e867c773e28c212ead563e98a1cd5d5b4576f5ee61c59bde025ff2851bb19b721421694f263818e3531e43a9e4e3e2c661e2ad547d8984caa28ebecd3e4525452299be26b9185a20a90ce1eac20a91f2832d731b54502b09749b5a2a2949292f8cfcbeffb790c7790ed935a9d251e7e336148ea83b063a5618fcff674a44581585fd22077ca0e52c59a24347a38d1a1ceebddbf238541f226b8f88d0fb9c07a1bcd2ea764bbbb5dacdaf5312a14c0b9e4f06309b0333b4a")[..]); + } + + #[test] + fn mlkem1024_x25519_3way_handshake_flow() { + let mut rng = rand::thread_rng(); + // long-term server id and keys + let server_id_keys = StaticSecret::random_from_rng(&mut rng); + let _server_id_pub = PublicKey::from(&server_id_keys); + // let server_id = ID::new(); + + // client open session, generating the associated ephemeral keys + let client_session = StaticSecret::random_from_rng(&mut rng); + + // client sends mlkem1024_x25519 session pubkey(s) + let _cpk = PublicKey::from(&client_session); + + // server computes mlkem1024_x25519 combined shared secret + let _server_session = StaticSecret::random_from_rng(&mut rng); + // let server_hs_res = server_handshake(&server_session, &cpk, &server_id_keys, &server_id); + + // server sends mlkemx25519 session pubkey(s) + let _spk = PublicKey::from(&client_session); + + // // client computes mlkem1024_x25519 combined shared secret + // let client_hs_res = client_handshake(&client_session, &spk, &server_id_pub, &server_id); + + // assert_ne!(client_hs_res.is_some().unwrap_u8(), 0); + // assert_ne!(server_hs_res.is_some().unwrap_u8(), 0); + + // let chsres = client_hs_res.unwrap(); + // let shsres = server_hs_res.unwrap(); + // assert_eq!(chsres.key_seed, shsres.key_seed); + // assert_eq!(&chsres.auth, &shsres.auth); + } +} diff --git a/crates/o5/src/handshake/README.md b/crates/o5/src/handshake/README.md new file mode 100644 index 0000000..8fc23cf --- /dev/null +++ b/crates/o5/src/handshake/README.md @@ -0,0 +1,9 @@ + +# PQ Obfs Handshake + + + +## Differences from Ntorv3 + + +## Differences from Obfs4 diff --git a/crates/o5/src/handshake/client.rs b/crates/o5/src/handshake/client.rs new file mode 100644 index 0000000..4d6673a --- /dev/null +++ b/crates/o5/src/handshake/client.rs @@ -0,0 +1,292 @@ +use crate::{ + common::{ + ct, + mlkem1024_x25519::SharedSecret, + ntor_arti::{ClientHandshake, ClientHandshakeMaterials}, + }, + constants::*, + framing::handshake::ClientHandshakeMessage, + handshake::*, + sessions::{SessionPublicKey, SessionSecretKey}, + Error, Result, +}; + +use bytes::BytesMut; +use hmac::Hmac; +use keys::NtorV3KeyGenerator; +// use cipher::KeyIvInit; +use rand::{CryptoRng, Rng, RngCore}; +use subtle::ConstantTimeEq; +use tor_bytes::{EncodeResult, Reader, SecretBuf, Writer}; +use tor_cell::relaycell::extend::NtorV3Extension; +use tor_error::into_internal; +use tor_llcrypto::{ + d::{Sha3_256, Shake256}, + pk::ed25519::Ed25519Identity, +}; +use zeroize::Zeroizing; + + +/// Client state for the o5 (ntor v3) handshake. +/// +/// The client needs to hold this state between when it sends its part +/// of the handshake and when it receives the relay's reply. +pub(crate) struct HandshakeState { + /// The temporary curve25519 secret (x) that we've generated for + /// this handshake. + // We'd like to EphemeralSecret here, but we can't since we need + // to use it twice. + my_sk: SessionSecretKey, + + /// handshake materials + materials: HandshakeMaterials, + + /// the computed hour at which the initial portion of the handshake was sent. + epoch_hr: String, + + /// The shared secret generated as Bx or Xb. + shared_secret: SharedSecret, // Bx + + /// The MAC of our original encrypted message. + msg_mac: MessageMac, // msg_mac +} + +impl HandshakeState { + fn node_pubkey(&self) -> &mlkem1024_x25519::PublicKey { + &self.materials.node_pubkey.pk + } + + fn node_id(&self) -> Ed25519Identity { + self.materials.node_pubkey.id + } +} + +/// Materials required to initiate a handshake from the client role. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HandshakeMaterials { + pub(crate) node_pubkey: IdentityPublicKey, + pub(crate) pad_len: usize, + pub(crate) session_id: String, + aux_data: Vec, +} + +impl HandshakeMaterials { + pub(crate) fn new(node_pubkey: &IdentityPublicKey, session_id: String) -> Self { + HandshakeMaterials { + node_pubkey: node_pubkey.clone(), + session_id, + pad_len: rand::thread_rng().gen_range(CLIENT_MIN_PAD_LENGTH..CLIENT_MAX_PAD_LENGTH), + aux_data: vec![], + } + } + + pub fn with_aux_data(mut self, data: impl AsRef<[NtorV3Extension]>) -> Self { + self.aux_data = data.as_ref().to_vec(); + self + } +} + +impl ClientHandshakeMaterials for HandshakeMaterials { + type IdentityKeyType = IdentityPublicKey; + type ClientAuxData = Vec; + + fn node_pubkey(&self) -> &Self::IdentityKeyType { + &self.node_pubkey + } + + fn aux_data(&self) -> Option<&Self::ClientAuxData> { + Some(&self.aux_data) + } +} + +/// Client side of the ntor v3 handshake. +pub(crate) struct NtorV3Client; + +impl ClientHandshake for NtorV3Client { + type StateType = HandshakeState; + type KeyGen = NtorV3KeyGenerator; + type ServerAuxData = Vec; + type HandshakeMaterials = HandshakeMaterials; + + /// Generate a new client onionskin for a relay with a given onion key. + /// If any `extensions` are provided, encode them into to the onionskin. + /// + /// On success, return a state object that will be used to complete the handshake, along + /// with the message to send. + fn client1(hs_materials: Self::HandshakeMaterials) -> Result<(Self::StateType, Vec)> { + let mut rng = rand::thread_rng(); + + Ok( + client_handshake_ntor_v3(&mut rng, hs_materials, NTOR3_CIRC_VERIFICATION) + .map_err(into_internal!("Can't encode ntor3 client handshake."))?, + ) + } + + /// Handle an onionskin from a relay, and produce a key generator. + /// + /// The state object must match the one that was used to make the + /// client onionskin that the server is replying to. + fn client2>( + state: Self::StateType, + msg: T, + ) -> Result<(Vec, Self::KeyGen)> { + let (message, xofreader) = + client_handshake_ntor_v3_part2(&state, msg.as_ref(), NTOR3_CIRC_VERIFICATION)?; + let extensions = NtorV3Extension::decode(&message).map_err(|err| Error::CellDecodeErr { + object: "ntor v3 extensions", + err, + })?; + let keygen = NtorV3KeyGenerator::new::(xofreader); + + Ok((extensions, keygen)) + } +} + +/// Client-side Ntor version 3 handshake, part one. +/// +/// Given a secure `rng`, a relay's public key, a secret message to send, +/// and a shared verification string, generate a new handshake state +/// and a message to send to the relay. +pub(crate) fn client_handshake_ntor_v3( + rng: &mut R, + materials: HandshakeMaterials, + verification: &[u8], +) -> EncodeResult<(HandshakeState, Vec)> { + let my_sk = SessionSecretKey::random_from_rng(rng); + client_handshake_ntor_v3_no_keygen(my_sk, materials, verification) +} + +/// As `client_handshake_ntor_v3`, but don't generate an ephemeral DH +/// key: instead take that key an arguments `my_sk`. +pub(crate) fn client_handshake_ntor_v3_no_keygen( + my_sk: SessionSecretKey, + materials: HandshakeMaterials, + verification: &[u8], +) -> EncodeResult<(HandshakeState, Vec)> { + let my_public = SessionPublicKey::from(&my_sk); + let client_msg = ClientHandshakeMessage::new(my_public.clone(), &materials); + + // -------- + let node_pubkey = materials.node_pubkey(); + // let bx = my_sk.diffie_hellman(&node_pubkey); + let mut rng = rand::thread_rng(); + let (ct, bx) = my_sk.hpke(&mut rng, materials.node_pubkey.pk)?; + // .map_err(|e| Error::Crypto(e.to_string())); + + let (enc_key, mut mac) = kdf_msgkdf(&bx, node_pubkey, &my_public, verification)?; + + // encrypted_msg = ENC(ENC_K1, CM) + // msg_mac = MAC_msgmac(MAC_K1, ID | B | X | encrypted_msg) + let encrypted_msg = encrypt(&enc_key, client_msg); + let msg_mac: DigestVal = { + use digest::Digest; + mac.write(&encrypted_msg)?; + mac.take().finalize().into() + }; + + let mut message = Vec::new(); + message.write(&node_pubkey.id)?; + message.write(&node_pubkey.pk.as_bytes())?; + message.write(&my_public.as_bytes())?; + message.write(&encrypted_msg)?; + message.write(&msg_mac)?; + // -------- + + let mut buf = BytesMut::with_capacity(MAX_HANDSHAKE_LENGTH); + let mut hmac_key = materials.node_pubkey.pk.as_bytes().to_vec(); + hmac_key.append(&mut materials.node_pubkey.id.as_bytes().to_vec()); + client_msg.marshall(&mut buf, &hmac_key[..]); + let message = buf.to_vec(); + + let state = HandshakeState { + materials, + my_sk, + shared_secret: bx, + msg_mac, + epoch_hr: client_msg.get_epoch_hr(), + }; + + Ok((state, message)) +} + +/// Finalize the handshake on the client side. +/// +/// Called after we've received a message from the relay: try to +/// complete the handshake and verify its correctness. +/// +/// On success, return the server's reply to our original encrypted message, +/// and an `XofReader` to use in generating circuit keys. +pub(crate) fn client_handshake_ntor_v3_part2( + state: &HandshakeState, + relay_handshake: &[u8], + verification: &[u8], +) -> Result<(Vec, NtorV3XofReader)> { + let mut reader = Reader::from_slice(relay_handshake); + let y_pk: SessionPublicKey = reader + .extract() + .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; + let auth: DigestVal = reader + .extract() + .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; + let encrypted_msg = reader.into_rest(); + let my_public = SessionPublicKey::from(&state.my_sk); + + // TODO: Some of this code is duplicated from the server handshake code! It + // would be better to factor it out. + let yx = state.my_sk.diffie_hellman(&y_pk); + let secret_input = { + let mut si = SecretBuf::new(); + si.write(&yx) + .and_then(|_| si.write(&state.shared_secret.as_bytes())) + .and_then(|_| si.write(&state.node_id())) + .and_then(|_| si.write(&state.node_pubkey().as_bytes())) + .and_then(|_| si.write(&my_public.as_bytes())) + .and_then(|_| si.write(&y_pk.as_bytes())) + .and_then(|_| si.write(PROTOID)) + .and_then(|_| si.write(&Encap(verification))) + .map_err(into_internal!("error encoding ntor3 secret_input"))?; + si + }; + let ntor_key_seed = h_key_seed(&secret_input); + let verify = h_verify(&secret_input); + + let computed_auth: DigestVal = { + use digest::Digest; + let mut auth = DigestWriter(Sha3_256::default()); + auth.write(&T_AUTH) + .and_then(|_| auth.write(&verify)) + .and_then(|_| auth.write(&state.node_id())) + .and_then(|_| auth.write(&state.node_pubkey().as_bytes())) + .and_then(|_| auth.write(&y_pk.as_bytes())) + .and_then(|_| auth.write(&my_public.as_bytes())) + .and_then(|_| auth.write(&state.msg_mac)) + .and_then(|_| auth.write(&Encap(encrypted_msg))) + .and_then(|_| auth.write(PROTOID)) + .and_then(|_| auth.write(&b"Server"[..])) + .map_err(into_internal!("error encoding ntor3 authentication input"))?; + auth.take().finalize().into() + }; + + let okay = computed_auth.ct_eq(&auth) + & ct::bool_to_choice(yx.was_contributory()) + & ct::bool_to_choice(state.shared_secret.was_contributory()); + + let (enc_key, keystream) = { + use digest::{ExtendableOutput, XofReader}; + let mut xof = DigestWriter(Shake256::default()); + xof.write(&T_FINAL) + .and_then(|_| xof.write(&ntor_key_seed)) + .map_err(into_internal!("error encoding ntor3 xof input"))?; + let mut r = xof.take().finalize_xof(); + let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); + r.read(&mut enc_key[..]); + (enc_key, r) + }; + let server_reply = decrypt(&enc_key, encrypted_msg); + + if okay.into() { + Ok((server_reply, NtorV3XofReader::new(keystream))) + } else { + Err(Error::BadCircHandshakeAuth) + } +} diff --git a/crates/o5/src/handshake/integration.rs b/crates/o5/src/handshake/integration.rs index 18bf109..6605eac 100644 --- a/crates/o5/src/handshake/integration.rs +++ b/crates/o5/src/handshake/integration.rs @@ -15,175 +15,248 @@ //! use super::*; -use crate::common::ntor_arti::{ClientHandshake, ServerHandshake}; +use crate::{ + common::{colorize, curve25519}, + test_utils::{init_subscriber, FakePRNG}, +}; use hex_literal::hex; use tor_basic_utils::test_rng::testing_rng; -use digest::XofReader; - +fn make_fake_ephem_key(bytes: &[u8]) -> curve25519::EphemeralSecret { + assert_eq!(bytes.len(), 32); + let rng = FakePRNG::new(bytes); + curve25519::EphemeralSecret::random_from_rng(rng) +} #[test] -fn test_obfs4_roundtrip() { +fn test_o5_roundtrip() -> Result<()> { let mut rng = rand::thread_rng(); - let relay_private = Obfs4NtorSecretKey::generate_for_test(&mut testing_rng()); - let verification = &b"shared secret"[..]; - let client_message = &b"Hello. I am a client. Let's be friends!"[..]; - let relay_message = &b"Greetings, client. I am a robot. Beep boop."[..]; + let relay_private = O5NtorSecretKey::generate_for_test(&mut rng); + let x = O5NtorSecretKey::generate_for_test(&mut rng); + let y = O5NtorSecretKey::generate_for_test(&mut rng); - let (c_state, c_handshake) = - client_handshake_obfs4(&mut rng, &relay_private.pk, client_message, verification) - .unwrap(); + let mut sid = [0u8; SESSION_ID_LEN]; + rand::thread_rng().fill_bytes(&mut sid); - struct Rep(Vec, Vec); - impl MsgReply for Rep { - fn reply(&mut self, msg: &[u8]) -> Option> { - self.0 = msg.to_vec(); - Some(self.1.clone()) - } - } - let mut rep = Rep(Vec::new(), relay_message.to_vec()); - - let (s_handshake, mut s_keygen) = server_handshake_obfs4( - &mut rng, - &mut rep, - &c_handshake, - &[relay_private], - verification, - ) - .unwrap(); + let chs_materials = CHSMaterials::new(relay_private.pk, colorize(sid)); + + let server = Server::new_from_key(relay_private); + + let shs_materials = SHSMaterials { + identity_keys: server.identity_keys.clone(), + len_seed: [0u8; SEED_LENGTH], + session_id: "s-yyy".into(), + }; + + let (state, create_msg) = client_handshake_o5_no_keygen(x.sk, chs_materials).unwrap(); + + let ephem = make_fake_ephem_key(&y.sk.as_bytes()[..]); + let (s_keygen, created_msg) = server + .server_handshake_o5_no_keygen(ephem, &create_msg[..], shs_materials) + .unwrap(); - let (s_msg, mut c_keygen) = - client_handshake_obfs4_part2(&c_state, &s_handshake, verification).unwrap(); + let (c_keygen, _) = client_handshake2_o5(created_msg, &state)?; - assert_eq!(rep.0[..], client_message[..]); - assert_eq!(s_msg[..], relay_message[..]); - let mut s_keys = [0_u8; 100]; - let mut c_keys = [0_u8; 1000]; - s_keygen.read(&mut s_keys); - c_keygen.read(&mut c_keys); - assert_eq!(s_keys[..], c_keys[..100]); + let c_keys = c_keygen.expand(72)?; + let s_keys = s_keygen.expand(72)?; + assert_eq!(c_keys, s_keys); + + Ok(()) } // Same as previous test, but use the higher-level APIs instead. #[test] -fn test_obfs4_roundtrip_highlevel() { - let mut rng = rand::thread_rng(); - let relay_private = Obfs4NtorSecretKey::generate_for_test(&mut testing_rng()); +fn test_o5_roundtrip_highlevel() -> Result<()> { + let rng = testing_rng(); + let relay_secret = StaticSecret::random_from_rng(rng); + let relay_public = PublicKey::from(&relay_secret); + let relay_identity = RsaIdentity::from_bytes(&[12; 20]).unwrap(); + let relay_ntpk = O5NtorPublicKey { + id: relay_identity, + pk: relay_public, + }; + let hs_materials = CHSMaterials::new(relay_ntpk, "c-xxx".into()); + let (state, cmsg) = O5NtorHandshake::client1(&hs_materials, &())?; - let (c_state, c_handshake) = - Obfs4NtorClient::client1(&mut rng, &relay_private.pk, &[]).unwrap(); + let relay_ntsk = O5NtorSecretKey { + pk: relay_ntpk, + sk: relay_secret, + }; + let server = Server::new_from_key(relay_ntsk.clone()); + let shs_materials = [SHSMaterials::new( + &relay_ntsk, + "s-yyy".into(), + [0u8; SEED_LENGTH], + )]; - let mut rep = |_: &[NtorV3Extension]| Some(vec![]); + let (skeygen, smsg) = server + .server(&mut |_: &()| Some(()), &shs_materials, &cmsg) + .unwrap(); - let (s_keygen, s_handshake) = - Obfs4NtorServer::server(&mut rng, &mut rep, &[relay_private], &c_handshake).unwrap(); + let (_extensions, ckeygen) = O5NtorHandshake::client2(state, smsg)?; - let (extensions, keygen) = Obfs4NtorClient::client2(c_state, s_handshake).unwrap(); + let skeys = skeygen.expand(55)?; + let ckeys = ckeygen.expand(55)?; - assert!(extensions.is_empty()); - let c_keys = keygen.expand(1000).unwrap(); - let s_keys = s_keygen.expand(100).unwrap(); - assert_eq!(s_keys[..], c_keys[..100]); + assert_eq!(skeys, ckeys); + + Ok(()) } -// Same as previous test, but encode some congestion control extensions. #[test] -fn test_obfs4_roundtrip_highlevel_cc() { - let mut rng = rand::thread_rng(); - let relay_private = Obfs4NtorSecretKey::generate_for_test(&mut testing_rng()); +fn test_o5_testvec_compat() -> Result<()> { + init_subscriber(); + let b_sk = hex!("a83fdd04eb9ed77a2b38d86092a09a1cecfb93a7bdec0da35e542775b2e7af6e"); + let x_sk = hex!("308ff4f3a0ebe8c1a93bcd40d67e3eec6b856aa5c07ef6d5a3d3cedf13dcf150"); + let y_sk = hex!("881f9ad60e0833a627f0c47f5aafbdcb0b5471800eaeaa1e678291b947e4295c"); + let id = hex!("000102030405060708090a0b0c0d0e0f10111213"); + let expected_seed = "05b858d18df21a01566c74d39a5b091b4415f103c05851e77e79b274132dc5b5"; + let expected_auth = "dc71f8ded2e56f829f1b944c1e94357fa8b7987f10211a017e2d1f2455092917"; + + let sk: StaticSecret = b_sk.into(); + let pk = O5NtorPublicKey { + id: RsaIdentity::from_bytes(&id).unwrap(), + pk: (&sk).into(), + }; + let relay_sk = O5NtorSecretKey { pk, sk }; + let server = Server::new_from_key(relay_sk.clone()); - let client_exts = vec![NtorV3Extension::RequestCongestionControl]; - let reply_exts = vec![NtorV3Extension::AckCongestionControl { sendme_inc: 42 }]; + let x: StaticSecret = x_sk.into(); - let (c_state, c_handshake) = Obfs4NtorClient::client1( - &mut rng, - &relay_private.pk, - &[NtorV3Extension::RequestCongestionControl], - ) - .unwrap(); + let chs_materials = CHSMaterials::new(pk, "c-xxx".into()); - let mut rep = |msg: &[NtorV3Extension]| -> Option> { - assert_eq!(msg, client_exts); - Some(reply_exts.clone()) - }; + let shs_materials = + SHSMaterials::new(&server.identity_keys, "s-yyy".into(), [0u8; SEED_LENGTH]); + + let (state, create_msg) = client_handshake_o5_no_keygen(x, chs_materials).unwrap(); - let (s_keygen, s_handshake) = - Obfs4NtorServer::server(&mut rng, &mut rep, &[relay_private], &c_handshake).unwrap(); + let (s_keygen, created_msg) = server + .server_handshake_o5_no_keygen( + make_fake_ephem_key(&y_sk[..]), // convert the StaticSecret to an EphemeralSecret for api to allow from hex + &create_msg[..], + shs_materials, + ) + .unwrap(); - let (extensions, keygen) = Obfs4NtorClient::client2(c_state, s_handshake).unwrap(); + let (c_keygen, auth) = client_handshake2_no_auth_check_o5(created_msg, &state)?; + let seed = c_keygen.seed.clone(); - assert_eq!(extensions, reply_exts); - let c_keys = keygen.expand(1000).unwrap(); - let s_keys = s_keygen.expand(100).unwrap(); - assert_eq!(s_keys[..], c_keys[..100]); + let c_keys = c_keygen.expand(72)?; + let s_keys = s_keygen.expand(72)?; + + assert_eq!(&s_keys[..], &c_keys[..]); + assert_eq!(hex::encode(auth), expected_auth); + assert_eq!(hex::encode(&seed[..]), expected_seed); + + Ok(()) } +#[cfg(target_feature = "disabled")] #[test] -fn test_obfs4_testvec() { - let id = hex!("9fad2af287ef942632833d21f946c6260c33fae6172b60006e86e4a6911753a2"); - let b = hex!("a8649010896d3c05ab0e2ab75e3e9368695c8f72652e8aa73016756007054e59"); // identity key - let x = hex!("f886d140047a115b228a8f7f63cbc5a3ad74e9c970ad3cb4a7f7fd8067f0dd68"); // client session key - let y = hex!("403ec0927a8852bdff12129244fdb03cb3c94b8b8d15c863462fcda52b510d73"); // server session key - let b: curve25519::StaticSecret = b.into(); - let B: curve25519::PublicKey = (&b).into(); - let id: Ed25519Identity = id.into(); - let x: curve25519::StaticSecret = x.into(); - let y: curve25519::StaticSecret = y.into(); - - let client_message = hex!("68656c6c6f20776f726c64"); - let verification = hex!("78797a7a79"); - let server_message = hex!("486f6c61204d756e646f"); - - let identity_public = Obfs4NtorPublicKey { pk: B, id, rp: None }; - let identity_private = Obfs4NtorSecretKey { - sk: b, - pk: identity_public.clone(), +fn test_ntor_v1_testvec() -> Result<()> { + let b_sk = hex!("4820544f4c4420594f5520444f474954204b454550532048415050454e494e47"); + let x_sk = hex!("706f6461792069207075742e2e2e2e2e2e2e2e4a454c4c59206f6e2074686973"); + let y_sk = hex!("70686520737175697272656c2e2e2e2e2e2e2e2e686173206869732067616d65"); + let id = hex!("69546f6c64596f7541626f75745374616972732e"); + let client_handshake = hex!("69546f6c64596f7541626f75745374616972732eccbc8541904d18af08753eae967874749e6149f873de937f57f8fd903a21c471e65dfdbef8b2635837fe2cebc086a8096eae3213e6830dc407516083d412b078"); + let server_handshake = hex!("390480a14362761d6aec1fea840f6e9e928fb2adb7b25c670be1045e35133a371cbdf68b89923e1f85e8e18ee6e805ea333fe4849c790ffd2670bd80fec95cc8"); + let keys = hex!("0c62dee7f48893370d0ef896758d35729867beef1a5121df80e00f79ed349af39b51cae125719182f19d932a667dae1afbf2e336e6910e7822223e763afad0a13342157969dc6b79"); + + let sk: StaticSecret = b_sk.into(); + let pk = O5NtorPublicKey { + id: RsaIdentity::from_bytes(&id).unwrap(), + pk: (&sk).into(), + rp: (&sk).into(), }; + let relay_sk = O5NtorSecretKey { pk, sk }; - let (state, client_handshake) = - client_handshake_obfs4_no_keygen(&identity_public, &client_message, &verification, x) - .unwrap(); + let x: StaticSecret = x_sk.into(); + let y: StaticSecret = y_sk.into(); - // assert_eq!(client_handshake[..], hex!("9fad2af287ef942632833d21f946c6260c33fae6172b60006e86e4a6911753a2f8307a2bc1870b00b828bb74dbb8fd88e632a6375ab3bcd1ae706aaa8b6cdd1d252fe9ae91264c91d4ecb8501f79d0387e34ad8ca0f7c995184f7d11d5da4f463bebd9151fd3b47c180abc9e044d53565f04d82bbb3bebed3d06cea65db8be9c72b68cd461942088502f67")[..]); + let (state, create_msg) = + client_handshake_o5_no_keygen((&x).into(), x, &relay_sk.pk).unwrap(); + assert_eq!(&create_msg[..], &client_handshake[..]); - struct Replier(Vec, Vec, bool); - impl MsgReply for Replier { - fn reply(&mut self, msg: &[u8]) -> Option> { - assert_eq!(msg, &self.0); - self.2 = true; - Some(self.1.clone()) - } - } - let mut rep = Replier(client_message.to_vec(), server_message.to_vec(), false); - - let (server_handshake, mut server_keygen) = server_handshake_obfs4_no_keygen( - &mut rep, - &y, - &client_handshake, - &[identity_private], - &verification, + let (s_keygen, created_msg) = server_handshake_o5_no_keygen( + (&y).into(), + make_fake_ephem_key(&y_sk[..]), // convert the StaticSecret to an EphemeralSecret for api to allow from hex + &create_msg[..], + &[relay_sk], ) .unwrap(); - assert!(rep.2); - // assert_eq!(server_handshake[..], hex!("4bf4814326fdab45ad5184f5518bd7fae25dc59374062698201a50a22954246d2fc5f8773ca824542bc6cf6f57c7c29bbf4e5476461ab130c5b18ab0a91276651202c3e1e87c0d32054c")[..]); + assert_eq!(&created_msg[..], &server_handshake[..]); + + let c_keygen = client_handshake2_o5(created_msg, &state)?; + + let c_keys = c_keygen.expand(keys.len())?; + let s_keys = s_keygen.expand(keys.len())?; + assert_eq!(&c_keys[..], &keys[..]); + assert_eq!(&s_keys[..], &keys[..]); + + Ok(()) +} - let (server_msg_received, mut client_keygen) = - client_handshake_obfs4_part2(&state, &server_handshake, &verification).unwrap(); - assert_eq!(&server_msg_received, &server_message); +#[test] +fn failing_handshakes() { + let mut rng = testing_rng(); + + // Set up keys. + let relay_secret = StaticSecret::random_from_rng(&mut rng); + let relay_public = PublicKey::from(&relay_secret); + let wrong_public = PublicKey::from([16_u8; 32]); + let relay_identity = RsaIdentity::from_bytes(&[12; 20]).unwrap(); + let wrong_identity = RsaIdentity::from_bytes(&[13; 20]).unwrap(); + let relay_ntpk = O5NtorPublicKey { + id: relay_identity, + pk: relay_public, + }; + let relay_ntsk = O5NtorSecretKey { + pk: relay_ntpk.clone(), + sk: relay_secret, + }; - let (c_keys, s_keys) = { - let mut c = [0_u8; 256]; - let mut s = [0_u8; 256]; - client_keygen.read(&mut c); - server_keygen.read(&mut s); - (c, s) + let wrong_ntpk1 = O5NtorPublicKey { + id: wrong_identity, + pk: relay_public, }; - assert_eq!(c_keys, s_keys); - assert_eq!(c_keys[..], hex!("05b858d18df21a01566c74d39a5b091b4415f103c05851e77e79b274132dc5b5")[..]); - // assert_eq!(c_keys[..], hex!("9c19b631fd94ed86a817e01f6c80b0743a43f5faebd39cfaa8b00fa8bcc65c3bfeaa403d91acbd68a821bf6ee8504602b094a254392a07737d5662768c7a9fb1b2814bb34780eaee6e867c773e28c212ead563e98a1cd5d5b4576f5ee61c59bde025ff2851bb19b721421694f263818e3531e43a9e4e3e2c661e2ad547d8984caa28ebecd3e4525452299be26b9185a20a90ce1eac20a91f2832d731b54502b09749b5a2a2949292f8cfcbeffb790c7790ed935a9d251e7e336148ea83b063a5618fcff674a44581585fd22077ca0e52c59a24347a38d1a1ceebddbf238541f226b8f88d0fb9c07a1bcd2ea764bbbb5dacdaf5312a14c0b9e4f06309b0333b4a")[..]); + let wrong_ntpk2 = O5NtorPublicKey { + id: relay_identity, + pk: wrong_public, + }; + + let resources = &Server::new_from_random(&mut rng); + + // If the client uses the wrong keys, the relay should reject the + // handshake. + let mut hs_materials = CHSMaterials::new(wrong_ntpk1.clone(), "c-xxx".into()); + let (_, handshake1) = O5NtorHandshake::client1(&hs_materials, &()).unwrap(); + hs_materials.node_pubkey = wrong_ntpk2; + let (_, handshake2) = O5NtorHandshake::client1(&hs_materials, &()).unwrap(); + hs_materials.node_pubkey = relay_ntpk; + let (st3, handshake3) = O5NtorHandshake::client1(&hs_materials, &()).unwrap(); + + let shs_materials = [SHSMaterials::new( + &relay_ntsk, + "s-yyy".into(), + [0u8; SEED_LENGTH], + )]; + let ans1 = resources.server(&mut |_: &()| Some(()), &shs_materials, &handshake1); + let ans2 = resources.server(&mut |_: &()| Some(()), &shs_materials, &handshake2); + + assert!(ans1.is_err()); + assert!(ans2.is_err()); + + // If the relay's message is tampered with, the client will + // reject the handshake. + let (_, mut smsg) = resources + .server(&mut |_: &()| Some(()), &shs_materials, &handshake3) + .unwrap(); + smsg[60] ^= 7; + let ans3 = O5NtorHandshake::client2(st3, smsg); + assert!(ans3.is_err()); } #[test] @@ -194,9 +267,8 @@ fn about_half() -> Result<()> { let mut not_found = 0; let mut not_match = 0; for _ in 0..1_000 { - let sk = curve25519::StaticSecret::random_from_rng(&mut rng); - let rp: Option= (&sk).into(); + let rp: Option = (&sk).into(); let repres = match rp { Some(r) => r, None => { @@ -207,7 +279,6 @@ fn about_half() -> Result<()> { let pk = curve25519::PublicKey::from(&sk); - let decoded_pk = curve25519::PublicKey::from(&repres); if hex::encode(pk) != hex::encode(decoded_pk) { not_match += 1; @@ -229,10 +300,10 @@ fn about_half() -> Result<()> { fn keypair() -> Result<()> { let mut rng = rand::thread_rng(); for _ in 0..1_000 { - let kp = Obfs4NtorSecretKey::generate_for_test(&mut rng); + let kp = O5NtorSecretKey::generate_for_test(&mut rng); let pk = kp.pk.pk.to_bytes(); - let repres = kp.pk.rp; + let repres: Option = (&kp.sk).into(); let pubkey = curve25519::PublicKey::from(&repres.unwrap()); assert_eq!(hex::encode(pk), hex::encode(pubkey.to_bytes())); @@ -240,7 +311,6 @@ fn keypair() -> Result<()> { Ok(()) } - /* // Benchmark Client/Server handshake. The actual time taken that will be // observed on either the Client or Server is half the reported time per diff --git a/crates/o5/src/handshake/keys.rs b/crates/o5/src/handshake/keys.rs new file mode 100644 index 0000000..5d71f9e --- /dev/null +++ b/crates/o5/src/handshake/keys.rs @@ -0,0 +1,321 @@ +use super::*; +use crate::{ + common::{ + // kdf::{Kdf, Ntor1Kdf}, + mlkem1024_x25519::{self, Ciphertext, PublicKey, SharedSecret, StaticSecret}, + ntor_arti::{KeyGenerator, SessionID, SessionIdentifier}, + }, + constants::*, + framing::{O5Codec, KEY_MATERIAL_LENGTH}, + Error, Result, +}; + +use base64::{ + engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, + Engine, +}; +use kem::Encapsulate; +use subtle::{Choice, ConstantTimeEq}; +use tor_bytes::{Readable, SecretBuf}; +use tor_llcrypto::{d::Shake256Reader, pk::ed25519::Ed25519Identity}; + +use rand::{CryptoRng, RngCore}; + +/// Key information about a relay used for the ntor v3 handshake. +/// +/// Contains a single curve25519 ntor onion key, and the relay's ed25519 +/// identity. +#[derive(Clone, Debug, PartialEq)] +pub struct IdentityPublicKey { + /// The relay's identity. + pub(crate) id: Ed25519Identity, + /// The relay's onion key. + pub(crate) pk: PublicKey, +} + +impl From<&IdentitySecretKey> for IdentityPublicKey { + fn from(value: &IdentitySecretKey) -> Self { + value.pk.clone() + } +} + +impl IdentityPublicKey { + const CERT_LENGTH: usize = mlkem1024_x25519::PUBKEY_LEN; + const CERT_SUFFIX: &'static str = "=="; + /// Construct a new IdentityPublicKey from its components. + #[allow(unused)] + pub(crate) fn new( + pk: [u8; mlkem1024_x25519::PUBKEY_LEN], + id: [u8; NODE_ID_LENGTH], + ) -> Result { + Ok(Self { + pk: pk.try_into()?, + id: id.into(), + }) + } +} + +impl std::str::FromStr for IdentityPublicKey { + type Err = Error; + fn from_str(s: &str) -> std::prelude::v1::Result { + let mut cert = String::from(s); + cert.push_str(Self::CERT_SUFFIX); + let decoded = STANDARD + .decode(cert.as_bytes()) + .map_err(|e| format!("failed to decode cert: {e}"))?; + if decoded.len() != Self::CERT_LENGTH { + return Err(format!("cert length {} is invalid", decoded.len()).into()); + } + let id: [u8; NODE_ID_LENGTH] = decoded[..NODE_ID_LENGTH].try_into()?; + let pk: [u8; NODE_PUBKEY_LENGTH] = decoded[NODE_ID_LENGTH..].try_into()?; + IdentityPublicKey::new(pk, id) + } +} + +#[allow(clippy::to_string_trait_impl)] +impl std::string::ToString for IdentityPublicKey { + fn to_string(&self) -> String { + let mut s = Vec::from(self.id.as_bytes()); + s.extend(self.pk.as_bytes()); + STANDARD_NO_PAD.encode(s) + } +} + +impl Readable for IdentityPublicKey { + fn take_from(_b: &mut tor_bytes::Reader<'_>) -> tor_bytes::Result { + todo!("IdentityPublicKey Reader needs implemented"); + } +} + +/// Secret key information used by a relay for the ntor v3 handshake. +pub struct IdentitySecretKey { + /// The relay's public key information + pub(crate) pk: IdentityPublicKey, + /// The secret onion key. + pub(super) sk: StaticSecret, +} + +impl IdentitySecretKey { + /// Construct a new IdentitySecretKey from its components. + #[allow(unused)] + pub(crate) fn new(sk: StaticSecret, id: Ed25519Identity) -> Self { + Self { + pk: IdentityPublicKey { + id, + pk: PublicKey::from(&sk), + }, + sk, + } + } + + pub fn try_from_bytes(bytes: impl AsRef<[u8]>) -> Result { + let buf = bytes.as_ref(); + if buf.len() < mlkem1024_x25519::PRIVKEY_LEN + NODE_ID_LENGTH { + return Err(Error::new("bad station identity cert provided")); + } + + let mut id = [0u8; NODE_ID_LENGTH]; + id.copy_from_slice(&buf[..NODE_ID_LENGTH]); + let sk = StaticSecret::try_from_bytes(&buf[NODE_ID_LENGTH..])?; + Ok(Self::new(sk, id.into())) + } + + /// Generate a key using the given `rng`, suitable for testing. + pub(crate) fn random_from_rng(rng: &mut R) -> Self { + let mut id = [0_u8; NODE_ID_LENGTH]; + // Random bytes will work for testing, but aren't necessarily actually a valid id. + rng.fill_bytes(&mut id); + let sk = StaticSecret::random_from_rng(rng); + Self::new(sk, id.into()) + } + + /// Checks whether `id` and `pk` match this secret key. + /// + /// Used to perform a constant-time secret key lookup. + pub(crate) fn matches(&self, id: Ed25519Identity, pk: PublicKey) -> Choice { + id.as_bytes().ct_eq(self.pk.id.as_bytes()) & pk.as_bytes().ct_eq(self.pk.pk.as_bytes()) + } + + /// Hybrid Public Key Encryption (HPKE) handshake for ML-KEM1024 + X25519 + /// + /// This is a custom interface for now as there isn't an example interface that I am aware of. + /// (Read - this will likely change in the future) + pub fn hpke( + &self, + rng: &mut R, + pubkey: &IdentityPublicKey, + ) -> Result<(Ciphertext, SharedSecret)> { + self.sk + .with_pub(&pubkey.pk) + .encapsulate(rng) + .map_err(|e| Error::Crypto(e.to_string())) + } +} + +impl TryFrom<&[u8]> for IdentitySecretKey { + type Error = Error; + fn try_from(value: &[u8]) -> std::result::Result { + Self::try_from_bytes(value) + } +} + +pub trait NtorV3KeyGen: KeyGenerator + SessionIdentifier + Into {} + +/// Opaque wrapper type for NtorV3's hash reader. +pub(crate) struct NtorV3XofReader(Shake256Reader); + +impl NtorV3XofReader { + pub(crate) fn new(reader: Shake256Reader) -> Self { + Self(reader) + } +} + +impl digest::XofReader for NtorV3XofReader { + fn read(&mut self, buffer: &mut [u8]) { + self.0.read(buffer); + } +} + +/// A key generator returned from an ntor v3 handshake. +pub(crate) struct NtorV3KeyGenerator { + /// The underlying `digest::XofReader`. + reader: NtorV3XofReader, + session_id: SessionID, + codec: O5Codec, +} + +impl KeyGenerator for NtorV3KeyGenerator { + fn expand(mut self, keylen: usize) -> Result { + let mut ret: SecretBuf = vec![0; keylen].into(); + self.reader.read(ret.as_mut()); + Ok(ret) + } +} + +impl NtorV3KeyGen for NtorV3KeyGenerator {} + +impl NtorV3KeyGenerator { + pub(crate) fn new(mut reader: NtorV3XofReader) -> Self { + // let okm = Self::kdf(&seed[..], KEY_MATERIAL_LENGTH * 2 + SESSION_ID_LEN) + // .expect("bug: failed to derive key material from seed"); + + // use the seed value to bootstrap Read / Write crypto codec and session ID. + let mut ekm = [0u8; KEY_MATERIAL_LENGTH]; + reader.read(&mut ekm); + let mut dkm = [0u8; KEY_MATERIAL_LENGTH]; + reader.read(&mut dkm); + + let mut id = [0u8; SESSION_ID_LEN]; + reader.read(&mut id); + + // server ekm == client dkm and vice-versa + let codec = match R::is_client() { + false => O5Codec::new(ekm, dkm), + true => O5Codec::new(dkm, ekm), + }; + + Self { + reader, + codec, + session_id: id.into(), + } + } +} + +impl SessionIdentifier for NtorV3KeyGenerator { + type ID = SessionID; + fn session_id(&mut self) -> Self::ID { + self.session_id + } +} + +impl KeyGenerator for NtorV3XofReader { + fn expand(mut self, keylen: usize) -> Result { + // let ntor1_key = &T_KEY_SEED[..]; + // let ntor1_expand = &M_EXPAND[..]; + // Ntor1Kdf::new(ntor1_key, ntor1_expand).derive(&self.seed[..], keylen) + let mut ret: SecretBuf = vec![0; keylen].into(); + self.0.read(ret.as_mut()); + Ok(ret) + } +} + +impl From for O5Codec { + fn from(keygen: NtorV3KeyGenerator) -> Self { + keygen.codec + } +} + +/// Alias for an HMAC output, used to validate correctness of a handshake. +pub(crate) type Authcode = [u8; 32]; +pub(crate) const AUTHCODE_LENGTH: usize = 32; + +// /// helper: compute a key generator and an authentication code from a set +// /// of ntor parameters. +// /// +// /// These parameter names are as described in tor-spec.txt +// fn ntor_derive( +// xy: &SharedSecret, +// xb: &SharedSecret, +// server_pk: &IdentityPublicKey, +// x: &PublicKey, +// y: &PublicKey, +// ) -> EncodeResult<(SecretBuf, Authcode)> { +// // ) -> EncodeResult<(NtorHkdfKeyGenerator, Authcode)> { +// let server_string = &b"Server"[..]; +// +// // obfs4 uses a different order than Ntor V1 and accidentally writes the +// // server's identity public key bytes twice. +// let mut suffix = SecretBuf::new(); +// suffix.write(&server_pk.pk.as_bytes())?; // b +// suffix.write(&server_pk.pk.as_bytes())?; // b +// suffix.write(x.as_bytes())?; // x +// suffix.write(y.as_bytes())?; // y +// suffix.write(PROTOID)?; // PROTOID +// suffix.write(&server_pk.id)?; // ID +// +// // secret_input = EXP(X,y) | EXP(X,b) OR = EXP(Y,x) | EXP(B,x) +// // ^ these are the equivalent x25519 shared secrets concatenated +// // +// // message = (secret_input) | b | b | x | y | PROTOID | ID +// let mut message = SecretBuf::new(); +// message.write(xy.as_bytes())?; // EXP(X,y) +// message.write(xb.as_bytes())?; // EXP(X,b) +// message.write(&suffix[..])?; // b | b | x | y | PROTOID | ID +// +// // verify = HMAC_SHA256(msg, T_VERIFY) +// let verify = { +// let mut m = Hmac::::new_from_slice(T_VERIFY).expect("Hmac allows keys of any size"); +// m.update(&message[..]); +// m.finalize() +// }; +// +// // auth_input = verify | (suffix) | "Server" +// // auth_input = verify | b | b | y | x | PROTOID | ID | "Server" +// // +// // Again obfs4 uses all of the same fields (with the servers identity public +// // key duplicated), but in a different order than Ntor V1. +// let mut auth_input = Vec::new(); +// auth_input.write_and_consume(verify)?; // verify +// auth_input.write(&suffix[..])?; // b | b | x | y | PROTOID | ID +// auth_input.write(server_string)?; // "Server" +// +// // auth = HMAC_SHA256(auth_input, T_MAC) +// let auth_mac = { +// let mut m = Hmac::::new_from_slice(T_MAC).expect("Hmac allows keys of any size"); +// m.update(&auth_input[..]); +// m.finalize() +// }; +// let auth: [u8; 32] = auth_mac.into_bytes()[..].try_into().unwrap(); +// +// // key_seed = HMAC_SHA256(message, T_KEY) +// let key_seed_bytes = { +// let mut m = Hmac::::new_from_slice(T_KEY).expect("Hmac allows keys of any size"); +// m.update(&message[..]); +// m.finalize() +// }; +// let mut key_seed = SecretBuf::new(); +// key_seed.write_and_consume(key_seed_bytes)?; +// +// Ok((key_seed, auth)) +// } diff --git a/crates/o5/src/handshake/mod.rs b/crates/o5/src/handshake/mod.rs deleted file mode 100644 index 316b7b4..0000000 --- a/crates/o5/src/handshake/mod.rs +++ /dev/null @@ -1,774 +0,0 @@ -//! Implements the ntor v3 key exchange, as described in proposal 332. -//! -//! The main difference between the ntor v3r handshake and the -//! original ntor handshake is that this this one allows each party to -//! encrypt data (without forward secrecy) after it sends the first -//! message. - -// TODO: -// Remove the "allow" item for dead_code. -// Make terminology and variable names consistent with spec. - -// This module is still unused: so allow some dead code for now. -#![allow(dead_code)] - -use std::borrow::Borrow; - -use crate::common::ntor_arti::{KeyGenerator, RelayHandshakeError, RelayHandshakeResult}; -use crate::common::ct; -use crate::common::curve25519; -use crate::{Error, Result}; -use tor_bytes::{EncodeResult, Reader, SecretBuf, Writeable, Writer}; -use tor_error::into_internal; -use tor_llcrypto::d::{Sha256, Shake256, Shake256Reader}; -use tor_llcrypto::pk::ed25519::Ed25519Identity; -use tor_llcrypto::util::ct::ct_lookup; - -use cipher::{KeyIvInit, StreamCipher}; - -use rand_core::{CryptoRng, RngCore}; -use subtle::{Choice, ConstantTimeEq}; -use tor_cell::relaycell::extend::NtorV3Extension; -use tor_llcrypto::cipher::aes::Aes256Ctr; -use zeroize::Zeroizing; - -/// The verification string to be used for circuit extension. -const OBFS4_CIRC_VERIFICATION: &[u8] = b"circuit extend"; - -/// The size of an encryption key in bytes. -const ENC_KEY_LEN: usize = 32; -/// The size of a MAC key in bytes. -const MAC_KEY_LEN: usize = 32; -/// The size of a curve25519 public key in bytes. -const PUB_KEY_LEN: usize = 32; -/// The size of a digest output in bytes. -const DIGEST_LEN: usize = 32; -/// The length of a MAC output in bytes. -const MAC_LEN: usize = 32; -/// The length of a node identity in bytes. -const ID_LEN: usize = 32; - -/// The output of the digest, as an array. -type DigestVal = [u8; DIGEST_LEN]; -/// The output of the MAC. -type MacVal = [u8; MAC_LEN]; -/// A key for symmetric encryption or decryption. -// -// TODO (nickm): Any move operations applied to this key could subvert the zeroizing. -type EncKey = Zeroizing<[u8; ENC_KEY_LEN]>; -/// A key for message authentication codes. -type MacKey = [u8; MAC_KEY_LEN]; - -/// Opaque wrapper type for Obfs4Ntor's hash reader. -struct Obfs4NtorXofReader(Shake256Reader); - -impl digest::XofReader for Obfs4NtorXofReader { - fn read(&mut self, buffer: &mut [u8]) { - self.0.read(buffer); - } -} - -/// An encapsulated value for passing as input to a MAC, digest, or -/// KDF algorithm. -/// -/// This corresponds to the ENCAP() function in proposal 332. -struct Encap<'a>(&'a [u8]); - -impl<'a> Writeable for Encap<'a> { - fn write_onto(&self, b: &mut B) -> EncodeResult<()> { - b.write_u64(self.0.len() as u64); - b.write(self.0) - } -} - -impl<'a> Encap<'a> { - /// Return the length of the underlying data in bytes. - fn len(&self) -> usize { - self.0.len() - } - /// Return the underlying data - fn data(&self) -> &'a [u8] { - self.0 - } -} - -/// Helper to define a set of tweak values as instances of `Encap`. -macro_rules! define_tweaks { - { - $(#[$pid_meta:meta])* - PROTOID = $protoid:expr; - $( $(#[$meta:meta])* $name:ident <= $suffix:expr ; )* - } => { - $(#[$pid_meta])* - const PROTOID: &'static [u8] = $protoid.as_bytes(); - $( - $(#[$meta])* - const $name : Encap<'static> = - Encap(concat!($protoid, ":", $suffix).as_bytes()); - )* - } -} - -define_tweaks! { - - pub(crate) const PROTO_ID: &[u8; 24] = b"ntor-curve25519-sha256-1"; - - pub(crate) const T_MAC: &[u8; 28] = b"ntor-curve25519-sha256-1:mac"; - - pub(crate) const T_VERIFY: &[u8; 35] = b"ntor-curve25519-sha256-1:key_verify"; - - pub(crate) const T_KEY: &[u8; 36] = b"ntor-curve25519-sha256-1:key_extract"; - - - /// Protocol ID: concatenated with other things in the protocol to - /// prevent hash confusion. - PROTOID = "ntor3-curve25519-sha3_256-1"; - - /// Message MAC tweak: used to compute the MAC of an encrypted client - /// message. - T_MSGMAC <= "msg_mac"; - /// Message KDF tweak: used when deriving keys for encrypting and MACing - /// client message. - T_MSGKDF <= "kdf_phase1"; - /// Key seeding tweak: used to derive final KDF input from secret_input. - T_KEY_SEED <= "key_seed"; - /// Verifying tweak: used to derive 'verify' value from secret_input. - T_VERIFY <= "verify"; - /// Final KDF tweak: used to derive keys for encrypting relay message - /// and for the actual tor circuit. - T_FINAL <= "kdf_final"; - /// Authentication tweak: used to derive the final authentication - /// value for the handshake. - T_AUTH <= "auth_final"; -} - -/// Compute a tweaked hash. -fn hash(t: &Encap<'_>, data: &[u8]) -> DigestVal { - use digest::Digest; - let mut d = Sha256::new(); - d.update((t.len() as u64).to_be_bytes()); - d.update(t.data()); - d.update(data); - d.finalize().into() -} - -/// Perform a symmetric encryption operation and return the encrypted data. -/// -/// (This isn't safe to do more than once with the same key, but we never -/// do that in this protocol.) -fn encrypt(key: &EncKey, m: &[u8]) -> Vec { - let mut d = m.to_vec(); - let zero_iv = Default::default(); - let mut cipher = Aes256Ctr::new(key.as_ref().into(), &zero_iv); - cipher.apply_keystream(&mut d); - d -} -/// Perform a symmetric decryption operation and return the encrypted data. -fn decrypt(key: &EncKey, m: &[u8]) -> Vec { - encrypt(key, m) -} - -/// Wrapper around a Digest or ExtendedOutput object that lets us use it -/// as a tor_bytes::Writer. -struct DigestWriter(U); -impl tor_bytes::Writer for DigestWriter { - fn write_all(&mut self, bytes: &[u8]) { - self.0.update(bytes); - } -} -impl DigestWriter { - /// Consume this wrapper and return the underlying object. - fn take(self) -> U { - self.0 - } -} - -/// Hash tweaked with T_KEY_SEED -fn h_key_seed(d: &[u8]) -> DigestVal { - hash(&T_KEY_SEED, d) -} -/// Hash tweaked with T_VERIFY -fn h_verify(d: &[u8]) -> DigestVal { - hash(&T_VERIFY, d) -} - -/// Helper: compute the encryption key and mac_key for the client's -/// encrypted message. -/// -/// Takes as inputs `xb` (the shared secret derived from -/// diffie-hellman as Bx or Xb), the relay's public key information, -/// the client's public key (B), and the shared verification string. -fn kdf_msgkdf( - xb: &curve25519::SharedSecret, - relay_public: &Obfs4NtorPublicKey, - client_public: &curve25519::PublicKey, - verification: &[u8], -) -> EncodeResult<(EncKey, DigestWriter)> { - // secret_input_phase1 = Bx | ID | X | B | PROTOID | ENCAP(VER) - // phase1_keys = KDF_msgkdf(secret_input_phase1) - // (ENC_K1, MAC_K1) = PARTITION(phase1_keys, ENC_KEY_LEN, MAC_KEY_LEN - use digest::{ExtendableOutput, XofReader}; - let mut msg_kdf = DigestWriter(Shake256::default()); - msg_kdf.write(&T_MSGKDF)?; - msg_kdf.write(xb.as_bytes())?; - msg_kdf.write(&relay_public.id)?; - msg_kdf.write(client_public.as_bytes())?; - msg_kdf.write(&relay_public.pk.as_bytes())?; - msg_kdf.write(PROTOID)?; - msg_kdf.write(&Encap(verification))?; - let mut r = msg_kdf.take().finalize_xof(); - let mut enc_key = Zeroizing::new([0; ENC_KEY_LEN]); - let mut mac_key = Zeroizing::new([0; MAC_KEY_LEN]); - - r.read(&mut enc_key[..]); - r.read(&mut mac_key[..]); - let mut mac = DigestWriter(Sha256::default()); - { - mac.write(&T_MSGMAC)?; - mac.write(&Encap(&mac_key[..]))?; - mac.write(&relay_public.id)?; - mac.write(&relay_public.pk.as_bytes())?; - mac.write(client_public.as_bytes())?; - } - - Ok((enc_key, mac)) -} - -/// Client side of the ntor v3 handshake. -pub(crate) struct Obfs4NtorClient; - -impl crate::common::ntor_arti::ClientHandshake for Obfs4NtorClient { - type KeyType = Obfs4NtorPublicKey; - type StateType = Obfs4NtorHandshakeState; - type KeyGen = Obfs4NtorKeyGenerator; - type ClientAuxData = [NtorV3Extension]; - type ServerAuxData = Vec; - - /// Generate a new client onionskin for a relay with a given onion key. - /// If any `extensions` are provided, encode them into to the onionskin. - /// - /// On success, return a state object that will be used to complete the handshake, along - /// with the message to send. - fn client1>( - rng: &mut R, - key: &Obfs4NtorPublicKey, - extensions: &M, - ) -> Result<(Self::StateType, Vec)> { - let mut message = Vec::new(); - NtorV3Extension::write_many_onto(extensions.borrow(), &mut message) - .map_err(|e| Error::from_bytes_enc(e, "ntor3 handshake extensions"))?; - Ok( - client_handshake_obfs4(rng, key, &message, OBFS4_CIRC_VERIFICATION) - .map_err(into_internal!("Can't encode obfs4 client handshake."))?, - ) - } - - /// Handle an onionskin from a relay, and produce a key generator. - /// - /// The state object must match the one that was used to make the - /// client onionskin that the server is replying to. - fn client2>( - state: Self::StateType, - msg: T, - ) -> Result<(Vec, Self::KeyGen)> { - let (message, xofreader) = - client_handshake_obfs4_part2(&state, msg.as_ref(), OBFS4_CIRC_VERIFICATION)?; - let extensions = NtorV3Extension::decode(&message).map_err(|err| Error::CellDecodeErr { - object: "ntor v3 extensions", - err, - })?; - let keygen = Obfs4NtorKeyGenerator { reader: xofreader }; - - Ok((extensions, keygen)) - } -} - -/// Server side of the ntor v3 handshake. -pub(crate) struct Obfs4NtorServer; - -impl crate::common::ntor_arti::ServerHandshake for Obfs4NtorServer { - type KeyType = Obfs4NtorSecretKey; - type KeyGen = Obfs4NtorKeyGenerator; - type ClientAuxData = [NtorV3Extension]; - type ServerAuxData = Vec; - - fn server, T: AsRef<[u8]>>( - rng: &mut R, - reply_fn: &mut REPLY, - key: &[Self::KeyType], - msg: T, - ) -> RelayHandshakeResult<(Self::KeyGen, Vec)> { - let mut bytes_reply_fn = |bytes: &[u8]| -> Option> { - let client_exts = NtorV3Extension::decode(bytes).ok()?; - let reply_exts = reply_fn.reply(&client_exts)?; - let mut out = vec![]; - NtorV3Extension::write_many_onto(&reply_exts, &mut out).ok()?; - Some(out) - }; - - let (res, reader) = server_handshake_obfs4( - rng, - &mut bytes_reply_fn, - msg.as_ref(), - key, - OBFS4_CIRC_VERIFICATION, - )?; - Ok((Obfs4NtorKeyGenerator { reader }, res)) - } -} - -/// Key information about a relay used for the ntor v3 handshake. -/// -/// Contains a single curve25519 ntor onion key, and the relay's ed25519 -/// identity. -#[derive(Clone, Debug)] -pub(crate) struct Obfs4NtorPublicKey { - /// The relay's identity. - pub(crate) id: Ed25519Identity, - /// The Bridge's identity key. - pub(crate) pk: curve25519::PublicKey, - /// The Elligator2 representative for the public key - pub(crate) rp: Option, -} - -/// Secret key information used by a relay for the ntor v3 handshake. -pub(crate) struct Obfs4NtorSecretKey { - /// The relay's public key information - pk: Obfs4NtorPublicKey, - /// The secret onion key. - sk: curve25519::StaticSecret, -} - -impl Obfs4NtorSecretKey { - /// Construct a new Obfs4NtorSecretKey from its components. - #[allow(unused)] - pub(crate) fn new( - sk: curve25519::StaticSecret, - pk: curve25519::PublicKey, - rp: Option, - id: Ed25519Identity, - ) -> Self { - Self { - pk: Obfs4NtorPublicKey { id, pk, rp }, - sk, - } - } - - /// Generate a key using the given `rng`, suitable for testing. - #[cfg(test)] - pub(crate) fn generate_for_test(mut rng: R) -> Self { - let mut id = [0_u8; 32]; - // Random bytes will work for testing, but aren't necessarily actually a valid id. - rng.fill_bytes(&mut id); - - let mut sk: curve25519::StaticSecret = [0u8;32].into(); - let mut pk1: curve25519::PublicKey = [0u8;32].into(); - let mut rp: Option = None; - - for _ in 0..64 { // artificial ceil of 64 so this can't infinite loop - - // approx 50% of keys do not have valid representatives so we just - // iterate until we find a key where it is valid. This should take - // a low number of iteratations and always succeed eventually. - sk = curve25519::StaticSecret::random_from_rng(&mut rng); - rp = (&sk).into(); - if rp.is_none() { - continue - } - pk1 = (&sk).into(); - break - } - - let pk = Obfs4NtorPublicKey { - pk: pk1, - id: id.into(), - rp, - }; - Self { pk, sk } - } - - /// Checks whether `id` and `pk` match this secret key. - /// - /// Used to perform a constant-time secret key lookup. - fn matches(&self, id: Ed25519Identity, pk: curve25519::PublicKey) -> Choice { - // TODO: use similar pattern in ntor_v1! - id.as_bytes().ct_eq(self.pk.id.as_bytes()) & pk.as_bytes().ct_eq(self.pk.pk.as_bytes()) - } -} - -/// Client state for the ntor v3 handshake. -/// -/// The client needs to hold this state between when it sends its part -/// of the handshake and when it receives the relay's reply. -pub(crate) struct Obfs4NtorHandshakeState { - /// The public key of the relay we're communicating with. - relay_public: Obfs4NtorPublicKey, // B, ID. - /// Our ephemeral secret key for this handshake. - my_sk: curve25519::StaticSecret, // x - /// Our ephemeral public key for this handshake. - my_public: curve25519::PublicKey, // X - - /// The shared secret generated as Bx or Xb. - shared_secret: curve25519::SharedSecret, // Bx - /// The MAC of our original encrypted message. - msg_mac: MacVal, // msg_mac -} - -/// A key generator returned from an ntor v3 handshake. -pub(crate) struct Obfs4NtorKeyGenerator { - /// The underlying `digest::XofReader`. - reader: Obfs4NtorXofReader, -} - -impl KeyGenerator for Obfs4NtorKeyGenerator { - fn expand(mut self, keylen: usize) -> Result { - use digest::XofReader; - let mut ret: SecretBuf = vec![0; keylen].into(); - self.reader.read(ret.as_mut()); - Ok(ret) - } -} - -/// Client-side Ntor version 3 handshake, part one. -/// -/// Given a secure `rng`, a relay's public key, a secret message to send, -/// and a shared verification string, generate a new handshake state -/// and a message to send to the relay. -fn client_handshake_obfs4( - rng: &mut R, - relay_public: &Obfs4NtorPublicKey, - client_msg: &[u8], - verification: &[u8], -) -> EncodeResult<(Obfs4NtorHandshakeState, Vec)> { - let my_sk = curve25519::StaticSecret::random_from_rng(rng); - client_handshake_obfs4_no_keygen(relay_public, client_msg, verification, my_sk) -} - -/// As `client_handshake_obfs4`, but don't generate an ephemeral DH -/// key: instead take that key an arguments `my_sk`. -fn client_handshake_obfs4_no_keygen( - relay_public: &Obfs4NtorPublicKey, - client_msg: &[u8], - verification: &[u8], - my_sk: curve25519::StaticSecret, -) -> EncodeResult<(Obfs4NtorHandshakeState, Vec)> { - let my_public = curve25519::PublicKey::from(&my_sk); - let bx = my_sk.diffie_hellman(&relay_public.pk); - - let (enc_key, mut mac) = kdf_msgkdf(&bx, relay_public, &my_public, verification)?; - - //encrypted_msg = ENC(ENC_K1, CM) - // msg_mac = MAC_msgmac(MAC_K1, ID | B | X | encrypted_msg) - let encrypted_msg = encrypt(&enc_key, client_msg); - let msg_mac: DigestVal = { - use digest::Digest; - mac.write(&encrypted_msg)?; - mac.take().finalize().into() - }; - - let mut message = Vec::new(); - message.write(&relay_public.id)?; - message.write(&relay_public.pk.as_bytes())?; - message.write(&my_public.as_bytes())?; - message.write(&encrypted_msg)?; - message.write(&msg_mac)?; - - let state = Obfs4NtorHandshakeState { - relay_public: relay_public.clone(), - my_sk, - my_public, - shared_secret: bx, - msg_mac, - }; - - Ok((state, message)) -} - -/// Trait for an object that handle and incoming client message and -/// return a server's reply. -/// -/// This is implemented for `FnMut(&[u8]) -> Option>` automatically. -pub(crate) trait MsgReply { - /// Given a message received from a client, parse it and decide - /// how (and whether) to reply. - /// - /// Return None if the handshake should fail. - fn reply(&mut self, msg: &[u8]) -> Option>; -} - -impl MsgReply for F -where - F: FnMut(&[u8]) -> Option>, -{ - fn reply(&mut self, msg: &[u8]) -> Option> { - self(msg) - } -} - -/// Complete an ntor v3 handshake as a server. -/// -/// Use the provided `rng` to generate keys; use the provided -/// `reply_fn` to handle incoming client secret message and decide how -/// to reply. The client's handshake is in `message`. Our private -/// key(s) are in `keys`. The `verification` string must match the -/// string provided by the client. -/// -/// On success, return the server handshake message to send, and an XofReader -/// to use in generating circuit keys. -fn server_handshake_obfs4( - rng: &mut RNG, - reply_fn: &mut REPLY, - message: &[u8], - keys: &[Obfs4NtorSecretKey], - verification: &[u8], -) -> RelayHandshakeResult<(Vec, Obfs4NtorXofReader)> { - let secret_key_y = curve25519::StaticSecret::random_from_rng(rng); - server_handshake_obfs4_no_keygen(reply_fn, &secret_key_y, message, keys, verification) -} - -/// As `server_handshake_obfs4`, but take a secret key instead of an RNG. -fn server_handshake_obfs4_no_keygen( - reply_fn: &mut REPLY, - secret_key_y: &curve25519::StaticSecret, - message: &[u8], - keys: &[Obfs4NtorSecretKey], - verification: &[u8], -) -> RelayHandshakeResult<(Vec, Obfs4NtorXofReader)> { - // Decode the message. - let mut r = Reader::from_slice(message); - let id: Ed25519Identity = r.extract()?; - - let pk_buf: [u8;32] = r.extract()?; - let requested_pk = curve25519::PublicKey::from(pk_buf); - let pk_buf: [u8;32] = r.extract()?; - let client_pk = curve25519::PublicKey::from(pk_buf); - let client_msg = if let Some(msg_len) = r.remaining().checked_sub(MAC_LEN) { - r.take(msg_len)? - } else { - return Err(tor_bytes::Error::Truncated.into()); - }; - let msg_mac: MacVal = r.extract()?; - r.should_be_exhausted()?; - - // See if we recognize the provided (id,requested_pk) pair. - let keypair = ct_lookup(keys, |key| key.matches(id, requested_pk)); - let keypair = match keypair { - Some(k) => k, - None => return Err(RelayHandshakeError::MissingKey), - }; - - let xb = keypair.sk.diffie_hellman(&client_pk); - let (enc_key, mut mac) = kdf_msgkdf(&xb, &keypair.pk, &client_pk, verification) - .map_err(into_internal!("Can't apply obfs4 kdf."))?; - // Verify the message we received. - let computed_mac: DigestVal = { - use digest::Digest; - mac.write(client_msg) - .map_err(into_internal!("Can't compute MAC input."))?; - mac.take().finalize().into() - }; - let y_pk: curve25519::PublicKey = (secret_key_y).into(); - let xy = secret_key_y.diffie_hellman(&client_pk); - - let mut okay = computed_mac.ct_eq(&msg_mac) - & ct::bool_to_choice(xy.was_contributory()) - & ct::bool_to_choice(xb.was_contributory()); - - let plaintext_msg = decrypt(&enc_key, client_msg); - - // Handle the message and decide how to reply. - let reply = reply_fn.reply(&plaintext_msg); - - // It's not exactly constant time to use is_some() and - // unwrap_or_else() here, but that should be somewhat - // hidden by the rest of the computation. - okay &= ct::bool_to_choice(reply.is_some()); - let reply = reply.unwrap_or_default(); - - // If we reach this point, we are actually replying, or pretending - // that we're going to reply. - - let secret_input = { - let mut si = SecretBuf::new(); - si.write(&xy.as_bytes()) - .and_then(|_| si.write(&xb.as_bytes())) - .and_then(|_| si.write(&keypair.pk.id)) - .and_then(|_| si.write(&keypair.pk.pk.as_bytes())) - .and_then(|_| si.write(&client_pk.as_bytes())) - .and_then(|_| si.write(&y_pk.as_bytes())) - .and_then(|_| si.write(PROTOID)) - .and_then(|_| si.write(&Encap(verification))) - .map_err(into_internal!("can't derive obfs4 secret_input"))?; - si - }; - let ntor_key_seed = h_key_seed(&secret_input); - let verify = h_verify(&secret_input); - - let (enc_key, keystream) = { - use digest::{ExtendableOutput, XofReader}; - let mut xof = DigestWriter(Shake256::default()); - xof.write(&T_FINAL) - .and_then(|_| xof.write(&ntor_key_seed)) - .map_err(into_internal!("can't generate obfs4 xof."))?; - let mut r = xof.take().finalize_xof(); - let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); - r.read(&mut enc_key[..]); - (enc_key, r) - }; - let encrypted_reply = encrypt(&enc_key, &reply); - let auth: DigestVal = { - use digest::Digest; - let mut auth = DigestWriter(Sha256::default()); - auth.write(&T_AUTH) - .and_then(|_| auth.write(&verify)) - .and_then(|_| auth.write(&keypair.pk.id)) - .and_then(|_| auth.write(&keypair.pk.pk.as_bytes())) - .and_then(|_| auth.write(&y_pk.as_bytes())) - .and_then(|_| auth.write(&client_pk.as_bytes())) - .and_then(|_| auth.write(&msg_mac)) - .and_then(|_| auth.write(&Encap(&encrypted_reply))) - .and_then(|_| auth.write(PROTOID)) - .and_then(|_| auth.write(&b"Server"[..])) - .map_err(into_internal!("can't derive obfs4 authentication"))?; - auth.take().finalize().into() - }; - - let reply = { - let mut reply = Vec::new(); - reply - .write(&y_pk.as_bytes()) - .and_then(|_| reply.write(&auth)) - .and_then(|_| reply.write(&encrypted_reply)) - .map_err(into_internal!("can't encode obfs4 reply."))?; - reply - }; - - if okay.into() { - Ok((reply, Obfs4NtorXofReader(keystream))) - } else { - Err(RelayHandshakeError::BadClientHandshake) - } -} - -/// Finalize the handshake on the client side. -/// -/// Called after we've received a message from the relay: try to -/// complete the handshake and verify its correctness. -/// -/// On success, return the server's reply to our original encrypted message, -/// and an `XofReader` to use in generating circuit keys. -fn client_handshake_obfs4_part2( - state: &Obfs4NtorHandshakeState, - relay_handshake: &[u8], - verification: &[u8], -) -> Result<(Vec, Obfs4NtorXofReader)> { - let mut reader = Reader::from_slice(relay_handshake); - let pk_buf: [u8;32] = reader - .extract() - .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; - let y_pk = curve25519::PublicKey::from(pk_buf); - let auth: DigestVal = reader - .extract() - .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; - let encrypted_msg = reader.into_rest(); - - // TODO: Some of this code is duplicated from the server handshake code! It - // would be better to factor it out. - let yx = state.my_sk.diffie_hellman(&y_pk); - let secret_input = { - let mut si = SecretBuf::new(); - si.write(&yx.as_bytes()) - .and_then(|_| si.write(&state.shared_secret.as_bytes())) - .and_then(|_| si.write(&state.relay_public.id)) - .and_then(|_| si.write(&state.relay_public.pk.as_bytes())) - .and_then(|_| si.write(&state.my_public.as_bytes())) - .and_then(|_| si.write(&y_pk.as_bytes())) - .and_then(|_| si.write(PROTOID)) - .and_then(|_| si.write(&Encap(verification))) - .map_err(into_internal!("error encoding obfs4 secret_input"))?; - si - }; - let ntor_key_seed = h_key_seed(&secret_input); - let verify = h_verify(&secret_input); - - let computed_auth: DigestVal = { - use digest::Digest; - let mut auth = DigestWriter(Sha256::default()); - auth.write(&T_AUTH) - .and_then(|_| auth.write(&verify)) - .and_then(|_| auth.write(&state.relay_public.id)) - .and_then(|_| auth.write(&state.relay_public.pk.as_bytes())) - .and_then(|_| auth.write(&y_pk.as_bytes())) - .and_then(|_| auth.write(&state.my_public.as_bytes())) - .and_then(|_| auth.write(&state.msg_mac)) - .and_then(|_| auth.write(&Encap(encrypted_msg))) - .and_then(|_| auth.write(PROTOID)) - .and_then(|_| auth.write(&b"Server"[..])) - .map_err(into_internal!("error encoding obfs4 authentication input"))?; - auth.take().finalize().into() - }; - - let okay = computed_auth.ct_eq(&auth) - & ct::bool_to_choice(yx.was_contributory()) - & ct::bool_to_choice(state.shared_secret.was_contributory()); - - let (enc_key, keystream) = { - use digest::{ExtendableOutput, XofReader}; - let mut xof = DigestWriter(Shake256::default()); - xof.write(&T_FINAL) - .and_then(|_| xof.write(&ntor_key_seed)) - .map_err(into_internal!("error encoding obfs4 xof input"))?; - let mut r = xof.take().finalize_xof(); - let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); - r.read(&mut enc_key[..]); - println!("{}", hex::encode(&enc_key)); - (enc_key, r) - }; - let server_reply = decrypt(&enc_key, encrypted_msg); - - if okay.into() { - Ok((server_reply, Obfs4NtorXofReader(keystream))) - } else { - Err(Error::BadCircHandshakeAuth) - } -} - -fn derive_ntor_shared( - secret_input: impl AsRef<[u8]>, - id: &ID, - b: &PublicKey, - x: &PublicKey, - y: &PublicKey, -) -> (KeySeed, Auth) { - let mut key_seed = KeySeed::default(); - let mut auth = Auth::default(); - - let mut message = secret_input.as_ref().to_vec(); - message.append(&mut b.to_bytes().to_vec()); - message.append(&mut x.to_bytes().to_vec()); - message.append(&mut y.to_bytes().to_vec()); - message.append(&mut PROTO_ID.to_vec()); - message.append(&mut id.to_bytes().to_vec()); - - let mut h = HmacSha256::new_from_slice(&T_KEY[..]).unwrap(); - h.update(message.as_ref()); - let tmp: &[u8] = &h.finalize().into_bytes()[..]; - key_seed.0 = tmp.try_into().expect("unable to write key_seed"); - - let mut h = HmacSha256::new_from_slice(&T_VERIFY[..]).unwrap(); - h.update(message.as_ref()); - let mut verify = h.finalize().into_bytes().to_vec(); - - // auth_input = verify | ID | B | Y | X | PROTOID | "Server" - verify.append(&mut message.to_vec()); - verify.append(&mut b"Server".to_vec()); - let mut h = HmacSha256::new_from_slice(&T_MAC[..]).unwrap(); - h.update(verify.as_ref()); - let mut tmp = &h.finalize().into_bytes()[..]; - auth.0 = tmp.try_into().expect("unable to write auth"); - - (key_seed, auth) -} - -#[cfg(test)] -mod integration; diff --git a/crates/o5/src/handshake/server.rs b/crates/o5/src/handshake/server.rs new file mode 100644 index 0000000..e5a6cf3 --- /dev/null +++ b/crates/o5/src/handshake/server.rs @@ -0,0 +1,225 @@ +use crate::{ + common::{ + ct, + drbg::SEED_LENGTH, + ntor_arti::{AuxDataReply, RelayHandshakeError, RelayHandshakeResult, ServerHandshake}, + }, + handshake::*, + sessions::SessionSecretKey, + Error, Server, +}; + +// use cipher::KeyIvInit; +use digest::{Digest, ExtendableOutput, XofReader}; +use hmac::{Hmac, Mac}; +use keys::NtorV3KeyGenerator; +use rand_core::{CryptoRng, RngCore}; +use sha2::Sha256; +use subtle::ConstantTimeEq; +use tor_bytes::{Reader, SecretBuf, Writer}; +use tor_cell::relaycell::extend::NtorV3Extension; +use tor_error::into_internal; +use tor_llcrypto::d::{Sha3_256, Shake256}; +use tor_llcrypto::pk::ed25519::Ed25519Identity; +use zeroize::Zeroizing; + +/// Server Materials needed for completing a handshake +pub(crate) struct HandshakeMaterials { + pub(crate) session_id: String, + pub(crate) len_seed: [u8; SEED_LENGTH], +} + +impl<'a> HandshakeMaterials { + pub fn get_hmac<'b>(&self, identity_keys: &'b IdentitySecretKey) -> Hmac { + let mut key = identity_keys.pk.pk.as_bytes().to_vec(); + key.append(&mut identity_keys.pk.id.as_bytes().to_vec()); + Hmac::::new_from_slice(&key[..]).unwrap() + } + + pub fn new<'b>(session_id: String, len_seed: [u8; SEED_LENGTH]) -> Self + where + 'b: 'a, + { + HandshakeMaterials { + session_id, + len_seed, + } + } +} + +impl ServerHandshake for Server { + type HandshakeParams = SHSMaterials; + type KeyGen = NtorV3KeyGenerator; + type ClientAuxData = [NtorV3Extension]; + type ServerAuxData = Vec; + + fn server, T: AsRef<[u8]>>( + &self, + reply_fn: &mut REPLY, + _materials: &Self::HandshakeParams, // TODO: do we need materials during server handshake? + msg: T, + ) -> RelayHandshakeResult<(Self::KeyGen, Vec)> { + let mut bytes_reply_fn = |bytes: &[u8]| -> Option> { + let client_exts = NtorV3Extension::decode(bytes).ok()?; + let reply_exts = reply_fn.reply(&client_exts)?; + let mut out = vec![]; + NtorV3Extension::write_many_onto(&reply_exts, &mut out).ok()?; + Some(out) + }; + let mut rng = rand::thread_rng(); + + let (res, reader) = server_handshake_ntor_v3( + &mut rng, + &mut bytes_reply_fn, + msg.as_ref(), + &self.identity_keys, + NTOR3_CIRC_VERIFICATION, + )?; + Ok((NtorV3KeyGenerator::new::(reader), res)) + } +} + +/// Complete an ntor v3 handshake as a server. +/// +/// Use the provided `rng` to generate keys; use the provided +/// `reply_fn` to handle incoming client secret message and decide how +/// to reply. The client's handshake is in `message`. Our private +/// key(s) are in `keys`. The `verification` string must match the +/// string provided by the client. +/// +/// On success, return the server handshake message to send, and an XofReader +/// to use in generating circuit keys. +pub(crate) fn server_handshake_ntor_v3( + rng: &mut R, + reply_fn: &mut REPLY, + message: &[u8], + keys: &IdentitySecretKey, + verification: &[u8], +) -> RelayHandshakeResult<(Vec, NtorV3XofReader)> { + let secret_key_y = SessionSecretKey::random_from_rng(rng); + server_handshake_ntor_v3_no_keygen(rng, reply_fn, &secret_key_y, message, keys, verification) +} + +/// As `server_handshake_ntor_v3`, but take a secret key instead of an RNG. +pub(crate) fn server_handshake_ntor_v3_no_keygen( + rng: &mut R, + reply_fn: &mut REPLY, + secret_key_y: &SessionSecretKey, + message: &[u8], + keys: &IdentitySecretKey, + verification: &[u8], +) -> RelayHandshakeResult<(Vec, NtorV3XofReader)> { + // Decode the message. + let mut r = Reader::from_slice(message); + let id: Ed25519Identity = r.extract()?; + let requested_pk: IdentityPublicKey = r.extract()?; + let client_pk: SessionPublicKey = r.extract()?; + let client_msg = if let Some(msg_len) = r.remaining().checked_sub(MAC_LEN) { + r.take(msg_len)? + } else { + let deficit = (MAC_LEN - r.remaining()) + .try_into() + .expect("miscalculated!"); + return Err(Error::incomplete_error(deficit).into()); + }; + + let msg_mac: MessageMac = r.extract()?; + r.should_be_exhausted()?; + + // See if we recognize the provided (id,requested_pk) pair. + let keypair = match keys.matches(id, requested_pk.pk).into() { + Some(k) => keys, + None => return Err(RelayHandshakeError::MissingKey), + }; + + let xb = keypair + .hpke(rng, &client_pk) + .map_err(|e| Error::Crypto(e.into()))?; + let (enc_key, mut mac) = kdf_msgkdf(&xb, &keypair.pk, &client_pk, verification) + .map_err(into_internal!("Can't apply ntor3 kdf."))?; + // Verify the message we received. + let computed_mac: DigestVal = { + mac.write(client_msg) + .map_err(into_internal!("Can't compute MAC input."))?; + mac.take().finalize().into() + }; + let y_pk = SessionPublicKey::from(secret_key_y); + let xy = secret_key_y.hpke(rng, &client_pk)?; + + let mut okay = computed_mac.ct_eq(&msg_mac) + & ct::bool_to_choice(xy.was_contributory()) + & ct::bool_to_choice(xb.was_contributory()); + + let plaintext_msg = decrypt(&enc_key, client_msg); + + // Handle the message and decide how to reply. + let reply = reply_fn.reply(&plaintext_msg); + + // It's not exactly constant time to use is_some() and + // unwrap_or_else() here, but that should be somewhat + // hidden by the rest of the computation. + okay &= ct::bool_to_choice(reply.is_some()); + let reply = reply.unwrap_or_default(); + + // If we reach this point, we are actually replying, or pretending + // that we're going to reply. + + let secret_input = { + let mut si = SecretBuf::new(); + si.write(&xy.as_bytes()) + .and_then(|_| si.write(&xb.as_bytes())) + .and_then(|_| si.write(&keypair.pk.id)) + .and_then(|_| si.write(&keypair.pk.pk.as_bytes())) + .and_then(|_| si.write(&client_pk.as_bytes())) + .and_then(|_| si.write(&y_pk.as_bytes())) + .and_then(|_| si.write(PROTOID)) + .and_then(|_| si.write(&Encap(verification))) + .map_err(into_internal!("can't derive ntor3 secret_input"))?; + si + }; + let ntor_key_seed = h_key_seed(&secret_input); + let verify = h_verify(&secret_input); + + let (enc_key, keystream) = { + let mut xof = DigestWriter(Shake256::default()); + xof.write(&T_FINAL) + .and_then(|_| xof.write(&ntor_key_seed)) + .map_err(into_internal!("can't generate ntor3 xof."))?; + let mut r = xof.take().finalize_xof(); + let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); + r.read(&mut enc_key[..]); + (enc_key, r) + }; + let encrypted_reply = encrypt(&enc_key, &reply); + let auth: DigestVal = { + let mut auth = DigestWriter(Sha3_256::default()); + auth.write(&T_AUTH) + .and_then(|_| auth.write(&verify)) + .and_then(|_| auth.write(&keypair.pk.id)) + .and_then(|_| auth.write(&keypair.pk.pk.as_bytes())) + .and_then(|_| auth.write(&y_pk.as_bytes())) + .and_then(|_| auth.write(&client_pk.as_bytes())) + .and_then(|_| auth.write(&msg_mac)) + .and_then(|_| auth.write(&Encap(&encrypted_reply))) + .and_then(|_| auth.write(PROTOID)) + .and_then(|_| auth.write(&b"Server"[..])) + .map_err(into_internal!("can't derive ntor3 authentication"))?; + auth.take().finalize().into() + }; + + let reply = { + let mut reply = Vec::new(); + reply + .write(&y_pk.as_bytes()) + .and_then(|_| reply.write(&auth)) + .and_then(|_| reply.write(&encrypted_reply)) + .map_err(into_internal!("can't encode ntor3 reply."))?; + reply + }; + + if okay.into() { + Ok((reply, NtorV3XofReader::new(keystream))) + } else { + Err(RelayHandshakeError::BadClientHandshake) + } +} diff --git a/crates/o5/src/lib.rs b/crates/o5/src/lib.rs index f14f12c..c584223 100644 --- a/crates/o5/src/lib.rs +++ b/crates/o5/src/lib.rs @@ -1,10 +1,45 @@ #![doc = include_str!("../README.md")] +pub mod client; +pub mod common; +pub mod framing; +pub mod proto; +pub mod server; +pub use client::{Client, ClientBuilder}; +pub use server::{Server, ServerBuilder}; + +pub(crate) mod constants; +pub(crate) mod handshake; +pub(crate) mod sessions; -mod framing; -// mod handshake; -// mod transport; +#[cfg(test)] +mod testing; + +mod pt; +pub use pt::{Transport, O5PT}; + +mod error; +pub use error::{Error, Result}; + +pub const TRANSPORT_NAME: &str = "o5"; +#[cfg(test)] +pub(crate) mod test_utils; + +#[cfg(debug_assertions)] +pub mod dev { + /// Pre-generated / shared key for use while running in debug mode. + pub const DEV_PRIV_KEY: &[u8; 32] = b"0123456789abcdeffedcba9876543210"; + + /// Client obfs4 arguments based on pre-generated dev key `DEV_PRIV_KEY`. + pub const CLIENT_ARGS: &str = + "cert=AAAAAAAAAAAAAAAAAAAAAAAAAADTSFvsGKxNFPBcGdOCBSgpEtJInG9zCYZezBPVBuBWag"; + + /// Server obfs4 arguments based on pre-generated dev key `DEV_PRIV_KEY`. + pub const SERVER_ARGS: &str = "drbg-seed=0a0b0c0d0e0f0a0b0c0d0e0f0a0b0c0d0e0f0a0b0c0d0e0f;node-id=0000000000000000000000000000000000000000;private-key=3031323334353637383961626364656666656463626139383736353433323130"; +} + +/* #[cfg(test)] #[allow(unused)] mod ml_kem_tests { @@ -66,71 +101,4 @@ mod ml_kem_tests { Ok(()) } } - -/* -#[cfg(test)] -#[allow(unused)] -mod pqc_kyber_tests { - use pqc_kyber::*; - use x25519_dalek::{EphemeralSecret, PublicKey}; - - type Result = std::result::Result; - - #[derive(Debug)] - enum Error { - PQCError(pqc_kyber::KyberError), - Other(Box), - } - - impl From for Error { - fn from(e: pqc_kyber::KyberError) -> Self { - Error::PQCError(e) - } - } - - // impl From<&dyn std::error::Error> for Error { - // fn from(e: &dyn std::error::Error) -> Self { - // Error::Other(Box::new(e)) - // } - // } - - struct Kyber1024XKeypair {} - - impl Kyber1024XKeypair { - fn new() -> Result { - todo!() - } - } - - #[test] - fn it_works() -> Result<()> { - let mut rng = rand::thread_rng(); - - // Generate Keypair - let alice_secret = EphemeralSecret::random_from_rng(&mut rng); - let alice_public = PublicKey::from(&alice_secret); - let keys_alice = keypair(&mut rng)?; - // alice -> bob public keys - let mut kyber1024x_pubkey = alice_public.as_bytes().to_vec(); - kyber1024x_pubkey.extend_from_slice(&keys_alice.public); - - assert_eq!(kyber1024x_pubkey.len(), 1600); - - let bob_secret = EphemeralSecret::random_from_rng(&mut rng); - let bob_public = PublicKey::from(&bob_secret); - - // Bob encapsulates a shared secret using Alice's public key - let (ciphertext, shared_secret_bob) = encapsulate(&keys_alice.public, &mut rng)?; - let bob_shared_secret = bob_secret.diffie_hellman(&alice_public); - - // // Alice decapsulates a shared secret using the ciphertext sent by Bob - let shared_secret_alice = decapsulate(&ciphertext, &keys_alice.secret)?; - let alice_shared_secret = alice_secret.diffie_hellman(&bob_public); - - assert_eq!(alice_shared_secret.as_bytes(), bob_shared_secret.as_bytes()); - assert_eq!(shared_secret_bob, shared_secret_alice); - - Ok(()) - } -} */ diff --git a/crates/o5/src/proto.rs b/crates/o5/src/proto.rs new file mode 100644 index 0000000..86f02ec --- /dev/null +++ b/crates/o5/src/proto.rs @@ -0,0 +1,347 @@ +use crate::{ + common::{ + drbg, + probdist::{self, WeightedDist}, + }, + constants::*, + framing, + sessions::Session, + Result, +}; + +use bytes::{Buf, BytesMut}; +use futures::{Sink, Stream}; +use pin_project::pin_project; +use ptrs::trace; +use sha2::{Digest, Sha256}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::time::{Duration, Instant}; +use tokio_util::codec::Framed; + +use std::{ + io::Error as IoError, + pin::Pin, + result::Result as StdResult, + task::{Context, Poll}, +}; + +use super::framing::{FrameError, Messages}; + +#[derive(Debug, Clone)] +pub(crate) enum MaybeTimeout { + Default_, + Fixed(Instant), + Length(Duration), + Unset, +} + +impl MaybeTimeout { + pub(crate) fn duration(&self) -> Option { + match self { + MaybeTimeout::Default_ => Some(CLIENT_HANDSHAKE_TIMEOUT), + MaybeTimeout::Fixed(i) => { + if *i < Instant::now() { + None + } else { + Some(*i - Instant::now()) + } + } + MaybeTimeout::Length(d) => Some(*d), + MaybeTimeout::Unset => None, + } + } +} + +#[pin_project] +/// AsyncReadable and AsyncWritable Obfuscated stream +/// +/// Writing in plaintext gets turned into obfuscated bytes and reading obfuscated +/// ciphertext results in decrypted planitext. +/// +/// TODO: this needs significantly more documentation +pub struct O5Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + // s: Arc>>, + #[pin] + s: ObfuscatedStream, +} + +impl O5Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + pub(crate) fn from_o4(os: ObfuscatedStream) -> Self { + O5Stream { + // s: Arc::new(Mutex::new(o4)), + s: os, + } + } +} + +#[pin_project] +pub(crate) struct ObfuscatedStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + #[pin] + pub stream: Framed, + + pub length_dist: probdist::WeightedDist, + pub ipt_dist: probdist::WeightedDist, + + pub session: Session, +} + +impl ObfuscatedStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + pub(crate) fn new( + // inner: &'a mut dyn Stream<'a>, + inner: T, + codec: framing::O5Codec, + session: Session, + ) -> Self { + let stream = Framed::new(inner, codec); + let len_seed = session.len_seed(); + + let mut hasher = Sha256::new(); + hasher.update(len_seed.as_bytes()); + // the result of a sha256 haash is 32 bytes (256 bits) so we will + // always have enough for a seed here. + let ipt_seed = drbg::Seed::try_from(&hasher.finalize()[..SEED_LENGTH]).unwrap(); + + let length_dist = WeightedDist::new( + len_seed, + 0, + framing::MAX_SEGMENT_LENGTH as i32, + session.biased(), + ); + let ipt_dist = WeightedDist::new( + ipt_seed, + 0, + framing::MAX_SEGMENT_LENGTH as i32, + session.biased(), + ); + + Self { + stream, + session, + length_dist, + ipt_dist, + } + } + + pub(crate) fn try_handle_non_payload_message(&mut self, msg: framing::Messages) -> Result<()> { + match msg { + Messages::Payload(_) => Err(FrameError::InvalidMessage.into()), + Messages::Padding(_) => Ok(()), + + // TODO: Handle other Messages + _ => Ok(()), + } + } + + /*// TODO Apply pad_burst logic and Inter-packet timing policy to packet assembly (probably as part of AsyncRead / AsyncWrite impl) + /// Attempts to pad a burst of data so that the last packet is of the length + /// `to_pad_to`. This can involve creating multiple packets, making this + /// slightly complex. + /// + /// TODO: document logic more clearly + pub(crate) fn pad_burst(&self, buf: &mut BytesMut, to_pad_to: usize) -> Result<()> { + let tail_len = buf.len() % framing::MAX_SEGMENT_LENGTH; + + let pad_len: usize = if to_pad_to >= tail_len { + to_pad_to - tail_len + } else { + (framing::MAX_SEGMENT_LENGTH - tail_len) + to_pad_to + }; + + if pad_len > HEADER_LENGTH { + // pad_len > 19 + Ok(framing::build_and_marshall( + buf, + MessageTypes::Payload.into(), + vec![], + pad_len - HEADER_LENGTH, + )?) + } else if pad_len > 0 { + framing::build_and_marshall( + buf, + MessageTypes::Payload.into(), + vec![], + framing::MAX_MESSAGE_PAYLOAD_LENGTH, + )?; + // } else { + Ok(framing::build_and_marshall( + buf, + MessageTypes::Payload.into(), + vec![], + pad_len, + )?) + } else { + Ok(()) + } + } */ +} + +impl AsyncWrite for ObfuscatedStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let msg_len = buf.remaining(); + let mut this = self.as_mut().project(); + + // determine if the stream is ready to send an event? + if futures::Sink::<&[u8]>::poll_ready(this.stream.as_mut(), cx) == Poll::Pending { + return Poll::Pending; + } + + // while we have bytes in the buffer write MAX_MESSAGE_PAYLOAD_LENGTH + // chunks until we have less than that amount left. + // TODO: asyncwrite - apply length_dist instead of just full payloads + let mut len_sent: usize = 0; + let mut out_buf = BytesMut::with_capacity(framing::MAX_MESSAGE_PAYLOAD_LENGTH); + while msg_len - len_sent > framing::MAX_MESSAGE_PAYLOAD_LENGTH { + // package one chunk of the mesage as a payload + let payload = framing::Messages::Payload( + buf[len_sent..len_sent + framing::MAX_MESSAGE_PAYLOAD_LENGTH].to_vec(), + ); + + // send the marshalled payload + payload.marshall(&mut out_buf)?; + this.stream.as_mut().start_send(&mut out_buf)?; + + len_sent += framing::MAX_MESSAGE_PAYLOAD_LENGTH; + out_buf.clear(); + + // determine if the stream is ready to send more data. if not back off + if futures::Sink::<&[u8]>::poll_ready(this.stream.as_mut(), cx) == Poll::Pending { + return Poll::Ready(Ok(len_sent)); + } + } + + let payload = framing::Messages::Payload(buf[len_sent..].to_vec()); + + let mut out_buf = BytesMut::new(); + payload.marshall(&mut out_buf)?; + this.stream.as_mut().start_send(out_buf)?; + + Poll::Ready(Ok(msg_len)) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + trace!("{} flushing", self.session.id()); + let mut this = self.project(); + match futures::Sink::<&[u8]>::poll_flush(this.stream.as_mut(), cx) { + Poll::Ready(Ok(_)) => Poll::Ready(Ok(())), + Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())), + Poll::Pending => Poll::Pending, + } + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + trace!("{} shutting down", self.session.id()); + let mut this = self.project(); + match futures::Sink::<&[u8]>::poll_close(this.stream.as_mut(), cx) { + Poll::Ready(Ok(_)) => Poll::Ready(Ok(())), + Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())), + Poll::Pending => Poll::Pending, + } + } +} + +impl AsyncRead for ObfuscatedStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + // If there is no payload from the previous Read() calls, consume data off + // the network. Not all data received is guaranteed to be usable payload, + // so do this in a loop until we would block on a read or an error occurs. + loop { + let msg = { + // mutable borrow of self is dropped at the end of this block + let mut this = self.as_mut().project(); + match this.stream.as_mut().poll_next(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(res) => { + // TODO: when would this be None? + // It seems like this maybe happens when reading an EOF + // or reading from a closed connection + if res.is_none() { + return Poll::Ready(Ok(())); + } + + match res.unwrap() { + Ok(m) => m, + Err(e) => Err(e)?, + } + } + } + }; + + if let framing::Messages::Payload(message) = msg { + buf.put_slice(&message); + return Poll::Ready(Ok(())); + } + if let Messages::Padding(_) = msg { + continue; + } + + match self.as_mut().try_handle_non_payload_message(msg) { + Ok(_) => continue, + Err(e) => return Poll::Ready(Err(e.into())), + } + } + } +} + +impl AsyncWrite for O5Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + this.s.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.s.poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.s.poll_shutdown(cx) + } +} + +impl AsyncRead for O5Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + this.s.poll_read(cx, buf) + } +} diff --git a/crates/o5/src/pt.rs b/crates/o5/src/pt.rs new file mode 100644 index 0000000..15e0e33 --- /dev/null +++ b/crates/o5/src/pt.rs @@ -0,0 +1,253 @@ +use crate::{constants::*, handshake::IdentityPublicKey, proto::O5Stream, Error, TRANSPORT_NAME}; +use ptrs::{args::Args, FutureResult as F}; + +use std::{ + marker::PhantomData, + net::{SocketAddrV4, SocketAddrV6}, + pin::Pin, + time::Duration, +}; + +use hex::FromHex; +use ptrs::trace; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; + +pub type O5PT = Transport; + +#[derive(Debug, Default)] +pub struct Transport { + _p: PhantomData, +} +impl Transport { + pub const NAME: &'static str = TRANSPORT_NAME; +} + +impl ptrs::PluggableTransport for Transport +where + T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, +{ + type ClientBuilder = crate::ClientBuilder; + type ServerBuilder = crate::ServerBuilder; + + fn name() -> String { + TRANSPORT_NAME.into() + } + + fn client_builder() -> >::ClientBuilder { + crate::ClientBuilder::default() + } + + fn server_builder() -> >::ServerBuilder { + crate::ServerBuilder::default() + } +} + +impl ptrs::ServerBuilder for crate::ServerBuilder +where + T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, +{ + type ServerPT = crate::Server; + type Error = Error; + type Transport = Transport; + + fn build(&self) -> Self::ServerPT { + crate::ServerBuilder::build(self) + } + + fn method_name() -> String { + TRANSPORT_NAME.into() + } + + fn options(&mut self, opts: &Args) -> Result<&mut Self, Self::Error> { + // TODO: pass on opts + + let state = Self::parse_state(None::<&str>, opts)?; + self.identity_keys = state.private_key; + // self.drbg = state.drbg_seed; // TODO apply seed from args to server + + trace!( + "node_pubkey: {}, node_id: {}", + hex::encode(self.identity_keys.pk.pk.as_bytes()), + hex::encode(self.identity_keys.pk.id.as_bytes()), + ); + Ok(self) + } + + fn get_client_params(&self) -> String { + self.client_params() + } + + fn statefile_location(&mut self, _path: &str) -> Result<&mut Self, Self::Error> { + Ok(self) + } + + fn timeout(&mut self, _timeout: Option) -> Result<&mut Self, Self::Error> { + Ok(self) + } + + fn v4_bind_addr(&mut self, _addr: SocketAddrV4) -> Result<&mut Self, Self::Error> { + Ok(self) + } + + fn v6_bind_addr(&mut self, _addr: SocketAddrV6) -> Result<&mut Self, Self::Error> { + Ok(self) + } +} + +impl ptrs::ClientBuilder for crate::ClientBuilder +where + T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, +{ + type ClientPT = crate::Client; + type Error = Error; + type Transport = Transport; + + fn method_name() -> String { + TRANSPORT_NAME.into() + } + + /// Builds a new PtCommonParameters. + /// + /// **Errors** + /// If a required field has not been initialized. + fn build(&self) -> Self::ClientPT { + crate::ClientBuilder::build(self) + } + + /// Pluggable transport attempts to parse and validate options from a string, + /// typically using ['parse_smethod_args']. + fn options(&mut self, opts: &Args) -> Result<&mut Self, Self::Error> { + let server_materials = match opts.retrieve(CERT_ARG) { + Some(cert_strs) => { + // The "new" (version >= 0.0.3) bridge lines use a unified "cert" argument + // for the Node ID and Public Key. + if cert_strs.is_empty() { + return Err(format!("missing argument '{NODE_ID_ARG}'").into()); + } + trace!("cert string: {}", &cert_strs); + IdentityPublicKey::from_str(&cert_strs)? + } + None => { + // The "old" style (version <= 0.0.2) bridge lines use separate Node ID + // and Public Key arguments in Base16 encoding and are a UX disaster. + let node_id_strs = opts + .retrieve(NODE_ID_ARG) + .ok_or(format!("missing argument '{NODE_ID_ARG}'"))?; + let id = <[u8; NODE_ID_LENGTH]>::from_hex(node_id_strs) + .map_err(|e| format!("malformed node id: {e}"))?; + + let public_key_strs = opts + .retrieve(PUBLIC_KEY_ARG) + .ok_or(format!("missing argument '{PUBLIC_KEY_ARG}'"))?; + + IdentityPublicKey::from_str(&public_key_strs) + .map_err(|e| format!("malformed public key: {e}"))? + } + }; + + self.with_node_pubkey(server_materials.pk.to_bytes()) + .with_node_id(server_materials.id.into()); + trace!( + "node_pubkey: {}, node_id: {}", + hex::encode(self.station_pubkey), + hex::encode(self.station_id), + ); + + Ok(self) + } + + /// A path where the launched PT can store state. + fn statefile_location(&mut self, _path: &str) -> Result<&mut Self, Self::Error> { + Ok(self) + } + + /// The maximum time we should wait for a pluggable transport binary to + /// report successful initialization. If `None`, a default value is used. + fn timeout(&mut self, _timeout: Option) -> Result<&mut Self, Self::Error> { + Ok(self) + } + + /// An IPv4 address to bind outgoing connections to (if specified). + /// + /// Leaving this out will mean the PT uses a sane default. + fn v4_bind_addr(&mut self, _addr: SocketAddrV4) -> Result<&mut Self, Self::Error> { + Ok(self) + } + + /// An IPv6 address to bind outgoing connections to (if specified). + /// + /// Leaving this out will mean the PT uses a sane default. + fn v6_bind_addr(&mut self, _addr: SocketAddrV6) -> Result<&mut Self, Self::Error> { + Ok(self) + } +} + +/// Example wrapping transport that just passes the incoming connection future through +/// unmodified as a proof of concept. +impl ptrs::ClientTransport for crate::Client +where + InRW: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, + InErr: std::error::Error + Send + Sync + 'static, +{ + type OutRW = O5Stream; + type OutErr = Error; + type Builder = crate::ClientBuilder; + + fn establish(self, input: Pin>) -> Pin> { + Box::pin(crate::Client::establish(self, input)) + } + + fn wrap(self, io: InRW) -> Pin> { + Box::pin(crate::Client::wrap(self, io)) + } + + fn method_name() -> String { + TRANSPORT_NAME.into() + } +} + +impl ptrs::ServerTransport for crate::Server +where + InRW: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, +{ + type OutRW = O5Stream; + type OutErr = Error; + type Builder = crate::ServerBuilder; + + /// Use something that can be accessed reference (Arc, Rc, etc.) + fn reveal(self, io: InRW) -> Pin> { + Box::pin(crate::Server::wrap(self, io)) + } + + fn method_name() -> String { + TRANSPORT_NAME.into() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn check_name() { + let pt_name = >::name(); + assert_eq!(pt_name, O5PT::NAME); + + let cb_name = >::method_name(); + assert_eq!(cb_name, O5PT::NAME); + + let sb_name = + as ptrs::ServerBuilder>::method_name(); + assert_eq!(sb_name, O5PT::NAME); + + let ct_name = + >::method_name(); + assert_eq!(ct_name, O5PT::NAME); + + let st_name = >::method_name(); + assert_eq!(st_name, O5PT::NAME); + } +} diff --git a/crates/o5/src/server.rs b/crates/o5/src/server.rs new file mode 100644 index 0000000..1058d7d --- /dev/null +++ b/crates/o5/src/server.rs @@ -0,0 +1,340 @@ +#![allow(unused)] + +use super::*; +use crate::{ + client::ClientBuilder, + common::{ + colorize, drbg, + replay_filter::{self, ReplayFilter}, + HmacSha256, + }, + constants::*, + framing::{FrameError, Marshall, O5Codec, TryParse, KEY_LENGTH}, + handshake::{IdentityPublicKey, IdentitySecretKey}, + proto::{MaybeTimeout, O5Stream}, + sessions::Session, + Error, Result, +}; +use ptrs::args::Args; +use tor_cell::relaycell::extend::NtorV3Extension; + +use std::{ + borrow::BorrowMut, marker::PhantomData, ops::Deref, str::FromStr, string::ToString, sync::Arc, +}; + +use bytes::{Buf, BufMut, Bytes}; +use hex::FromHex; +use hmac::{Hmac, Mac}; +use ptrs::{debug, info}; +use rand::prelude::*; +use subtle::ConstantTimeEq; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::time::{Duration, Instant}; +use tokio_util::codec::Encoder; + +const STATE_FILENAME: &str = "obfs4_state.json"; + +pub struct ServerBuilder { + pub statefile_path: Option, + pub(crate) identity_keys: IdentitySecretKey, + pub(crate) handshake_timeout: MaybeTimeout, + // pub(crate) drbg: Drbg, // TODO: build in DRBG + _stream_type: PhantomData, +} + +impl Default for ServerBuilder { + fn default() -> Self { + let identity_keys = IdentitySecretKey::random_from_rng(&mut rand::thread_rng()); + Self { + statefile_path: None, + identity_keys, + handshake_timeout: MaybeTimeout::Default_, + _stream_type: PhantomData, + } + } +} + +impl ServerBuilder { + /// 64 byte combined representation of an x25519 public key, private key + /// combination. + pub fn node_keys(&mut self, keys: impl AsRef<[u8]>) -> Result<&Self> { + let sk = IdentitySecretKey::try_from(keys.as_ref())?; + self.identity_keys = sk; + Ok(self) + } + + pub fn statefile_path(&mut self, path: &str) -> &Self { + self.statefile_path = Some(path.into()); + self + } + + pub fn node_id(&mut self, id: [u8; NODE_ID_LENGTH]) -> &Self { + self.identity_keys.pk.id = id.into(); + self + } + + pub fn with_handshake_timeout(&mut self, d: Duration) -> &Self { + self.handshake_timeout = MaybeTimeout::Length(d); + self + } + + pub fn with_handshake_deadline(&mut self, deadline: Instant) -> &Self { + self.handshake_timeout = MaybeTimeout::Fixed(deadline); + self + } + + pub fn fail_fast(&mut self) -> &Self { + self.handshake_timeout = MaybeTimeout::Unset; + self + } + + pub fn client_params(&self) -> String { + let mut params = Args::new(); + params.insert(CERT_ARG.into(), vec![self.identity_keys.pk.to_string()]); + params.encode_smethod_args() + } + + pub fn build(self) -> Server { + Server(Arc::new(ServerInner { + identity_keys: self.identity_keys, + biased: false, + handshake_timeout: self.handshake_timeout.duration(), + + // metrics: Arc::new(std::sync::Mutex::new(ServerMetrics {})), + replay_filter: ReplayFilter::new(REPLAY_TTL), + })) + } + + pub fn validate_args(args: &Args) -> Result<()> { + let _ = RequiredServerState::try_from(args)?; + + Ok(()) + } + + pub(crate) fn parse_state( + statedir: Option>, + args: &Args, + ) -> Result { + if statedir.is_none() { + return RequiredServerState::try_from(args); + } + + // if the provided arguments do not satisfy all required arguments, we + // attempt to parse the server state from json IFF a statedir path was + // provided. Otherwise this method just fails. + let mut required_args = args.clone(); + match RequiredServerState::try_from(args) { + Ok(state) => Ok(state), + Err(e) => { + Self::server_state_from_file(statedir.unwrap(), &mut required_args)?; + RequiredServerState::try_from(&required_args) + } + } + } + + fn server_state_from_file(statedir: impl AsRef, args: &mut Args) -> Result<()> { + let mut file_path = String::from(statedir.as_ref()); + file_path.push_str(STATE_FILENAME); + + let state_str = std::fs::read(file_path)?; + + Self::server_state_from_json(&state_str[..], args) + } + + fn server_state_from_json(state_rdr: impl std::io::Read, args: &mut Args) -> Result<()> { + let state: JsonServerState = + serde_json::from_reader(state_rdr).map_err(|e| Error::Other(Box::new(e)))?; + + state.extend_args(args); + Ok(()) + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct JsonServerState { + #[serde(rename = "node-id")] + node_id: Option, + #[serde(rename = "private-key")] + private_key: Option, + #[serde(rename = "public-key")] + public_key: Option, + #[serde(rename = "drbg-seed")] + drbg_seed: Option, +} + +impl JsonServerState { + fn extend_args(self, args: &mut Args) { + if let Some(id) = self.node_id { + args.add(NODE_ID_ARG, &id); + } + if let Some(sk) = self.private_key { + args.add(PRIVATE_KEY_ARG, &sk); + } + if let Some(pubkey) = self.public_key { + args.add(PUBLIC_KEY_ARG, &pubkey); + } + if let Some(seed) = self.drbg_seed { + args.add(SEED_ARG, &seed); + } + } +} + +pub(crate) struct RequiredServerState { + pub(crate) private_key: IdentitySecretKey, + pub(crate) drbg_seed: drbg::Drbg, +} + +impl TryFrom<&Args> for RequiredServerState { + type Error = Error; + fn try_from(value: &Args) -> std::prelude::v1::Result { + let privkey_str = value + .retrieve(PRIVATE_KEY_ARG) + .ok_or("missing argument {PRIVATE_KEY_ARG}")?; + let sk = <[u8; KEY_LENGTH]>::from_hex(privkey_str)?; + + let drbg_seed_str = value + .retrieve(SEED_ARG) + .ok_or("missing argument {SEED_ARG}")?; + let drbg_seed = drbg::Seed::from_hex(drbg_seed_str)?; + + let node_id_str = value + .retrieve(NODE_ID_ARG) + .ok_or("missing argument {NODE_ID_ARG}")?; + let node_id = <[u8; NODE_ID_LENGTH]>::from_hex(node_id_str)?; + + let private_key = IdentitySecretKey::try_from_bytes(sk)?; + + Ok(RequiredServerState { + private_key, + drbg_seed: drbg::Drbg::new(Some(drbg_seed))?, + }) + } +} + +#[derive(Clone)] +pub struct Server(Arc); + +pub struct ServerInner { + pub(crate) handshake_timeout: Option, + pub(crate) biased: bool, + pub(crate) identity_keys: IdentitySecretKey, + + pub(crate) replay_filter: ReplayFilter, + // pub(crate) metrics: Metrics, +} + +impl Deref for Server { + type Target = ServerInner; + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl Server { + pub fn new(identity: IdentitySecretKey) -> Self { + Self::new_from_key(identity) + } + + pub(crate) fn new_from_key(identity_keys: IdentitySecretKey) -> Self { + Self(Arc::new(ServerInner { + handshake_timeout: Some(SERVER_HANDSHAKE_TIMEOUT), + identity_keys, + biased: false, + + // metrics: Arc::new(std::sync::Mutex::new(ServerMetrics {})), + replay_filter: ReplayFilter::new(REPLAY_TTL), + })) + } + + pub fn new_from_random(rng: &mut R) -> Self { + let mut id = [0_u8; 20]; + + // Generated identity secret key does not need to be elligator2 representable + // so we can use the regular dalek_x25519 key generation. + let identity_keys = IdentitySecretKey::random_from_rng(rng); + + let pk = IdentityPublicKey::from(&identity_keys); + + Self::new_from_key(identity_keys) + } + + pub async fn wrap(self, stream: T) -> Result> + where + T: AsyncRead + AsyncWrite + Unpin, + { + let session = self.new_server_session()?; + let deadline = self.handshake_timeout.map(|d| Instant::now() + d); + let mut null_extension_handler = |_: &[NtorV3Extension]| None; + + session + .handshake(&self, stream, &mut null_extension_handler, deadline) + .await + } + + pub fn set_args(&mut self, args: &dyn std::any::Any) -> Result<&Self> { + Ok(self) + } + + pub fn new_from_statefile() -> Result { + Err(Error::NotImplemented) + } + + pub fn write_statefile(f: std::fs::File) -> Result<()> { + Err(Error::NotImplemented) + } + + pub fn client_params(&self) -> ClientBuilder { + ClientBuilder { + // these unwraps should be safe as we are sure of the size of the source + station_pubkey: self.identity_keys.pk.pk.as_bytes().try_into().unwrap(), + station_id: self.identity_keys.pk.id.as_bytes().try_into().unwrap(), + + statefile_path: None, + handshake_timeout: MaybeTimeout::Default_, + } + } + + pub(crate) fn new_server_session( + &self, + ) -> Result> { + let mut session_id = [0u8; SESSION_ID_LEN]; + rand::thread_rng().fill_bytes(&mut session_id); + Ok(sessions::ServerSession { + // fixed by server + biased: self.biased, + + // generated per session + session_id: session_id.into(), + len_seed: drbg::Seed::new().unwrap(), + ipt_seed: drbg::Seed::new().unwrap(), + + _state: sessions::Initialized {}, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::dev; + + use super::*; + + use ptrs::trace; + use tokio::net::TcpStream; + + use crate::test_utils::init_subscriber; + + #[test] + fn parse_json_state() -> Result<()> { + init_subscriber(); + + let mut args = Args::new(); + let test_state = format!( + r#"{{"{NODE_ID_ARG}": "00112233445566778899", "{PRIVATE_KEY_ARG}":"0123456789abcdeffedcba9876543210", "{SEED_ARG}": "abcdefabcdefabcdefabcdef"}}"# + ); + ServerBuilder::::server_state_from_json(test_state.as_bytes(), &mut args)?; + debug!("{:?}\n{}", args.encode_smethod_args(), test_state); + + Ok(()) + } +} diff --git a/crates/o5/src/sessions.rs b/crates/o5/src/sessions.rs new file mode 100644 index 0000000..b0af194 --- /dev/null +++ b/crates/o5/src/sessions.rs @@ -0,0 +1,65 @@ +//! obfs4 session details and construction +//! +/// Session state management as a way to organize session establishment and +/// steady state transfer. +use crate::common::{drbg, mlkem1024_x25519}; + +use tor_bytes::Readable; + +mod client; +pub(crate) use client::{new_client_session, ClientSession}; + +mod server; +pub(crate) use server::ServerSession; + +/// Ephermeral single use session secret key type +pub type SessionSecretKey = mlkem1024_x25519::StaticSecret; + +/// Public key type associated with SessionSecretKey. +pub type SessionPublicKey = mlkem1024_x25519::PublicKey; + + +impl Readable for SessionPublicKey { + fn take_from(_b: &mut tor_bytes::Reader<'_>) -> tor_bytes::Result { + todo!("SessionPublicKey Reader needs implemented"); + } +} + + +/// Initial state for a Session, created with any params. +pub(crate) struct Initialized; + +/// A session has completed the handshake and made it to steady state transfer. +pub(crate) struct Established; + +/// The session broke due to something like a timeout, reset, lost connection, etc. +trait Fault {} + +pub enum Session { + Client(ClientSession), + Server(ServerSession), +} + +impl Session { + #[allow(unused)] + pub fn id(&self) -> String { + match self { + Session::Client(cs) => format!("c{}", cs.session_id()), + Session::Server(ss) => format!("s{}", ss.session_id()), + } + } + + pub fn biased(&self) -> bool { + match self { + Session::Client(cs) => cs.biased(), + Session::Server(ss) => ss.biased, //biased, + } + } + + pub fn len_seed(&self) -> drbg::Seed { + match self { + Session::Client(cs) => cs.len_seed(), + Session::Server(ss) => ss.len_seed(), + } + } +} diff --git a/crates/o5/src/sessions/client.rs b/crates/o5/src/sessions/client.rs new file mode 100644 index 0000000..aa613cc --- /dev/null +++ b/crates/o5/src/sessions/client.rs @@ -0,0 +1,266 @@ +use crate::{ + common::{ + discard, drbg, + ntor_arti::{ClientHandshake, RelayHandshakeError, SessionID, SessionIdentifier}, + }, + constants::*, + framing, + handshake::{CHSMaterials, IdentityPublicKey, NtorV3Client, NtorV3KeyGen}, + proto::{O5Stream, ObfuscatedStream}, + sessions::{Established, Fault, Initialized, Session}, + Error, Result, +}; + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use bytes::BytesMut; +use ptrs::{debug, info}; +use rand_core::RngCore; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::time::Instant; +use tokio_util::codec::Decoder; + +// ================================================================ // +// Client States // +// ================================================================ // + +pub(crate) struct ClientSession { + node_pubkey: IdentityPublicKey, + session_id: SessionID, + epoch_hour: String, + + biased: bool, + + len_seed: drbg::Seed, + + _state: S, +} + +#[allow(unused)] +struct ClientHandshakeFailed { + details: String, +} + +struct ClientHandshaking {} + +pub(crate) trait ClientSessionState {} +impl ClientSessionState for Initialized {} +impl ClientSessionState for ClientHandshaking {} +impl ClientSessionState for Established {} + +impl ClientSessionState for ClientHandshakeFailed {} +impl Fault for ClientHandshakeFailed {} + +impl ClientSession { + pub fn session_id(&self) -> String { + String::from("c-") + &self.session_id.to_string() + } + + pub(crate) fn set_session_id(&mut self, id: SessionID) { + debug!("{} -> {} client updating session id", self.session_id, id); + self.session_id = id; + } + + pub(crate) fn biased(&self) -> bool { + self.biased + } + + pub fn len_seed(&self) -> drbg::Seed { + self.len_seed.clone() + } + + /// Helper function to perform state transitions. + fn transition(self, t: T) -> ClientSession { + ClientSession { + node_pubkey: self.node_pubkey, + session_id: self.session_id, + epoch_hour: self.epoch_hour, + biased: self.biased, + + len_seed: self.len_seed, + _state: t, + } + } + + /// Helper function to perform state transitions. + fn fault(self, f: F) -> ClientSession { + ClientSession { + node_pubkey: self.node_pubkey, + session_id: self.session_id, + epoch_hour: self.epoch_hour, + biased: self.biased, + + len_seed: self.len_seed, + _state: f, + } + } +} + +pub fn new_client_session(station_pubkey: IdentityPublicKey) -> ClientSession { + let mut session_id = [0u8; SESSION_ID_LEN]; + rand::thread_rng().fill_bytes(&mut session_id); + ClientSession { + node_pubkey: station_pubkey, + session_id: session_id.into(), + epoch_hour: "".into(), + biased: false, + + len_seed: drbg::Seed::new().unwrap(), + _state: Initialized, + } +} + +impl ClientSession { + /// Perform a Handshake over the provided stream. + /// + /// Completes the client handshake including sending the initial hello message + /// and processing the response (or lack thereof). On success this returns: + /// 1) The remaining bytes included in the server response not part of the + /// handshake packet. + /// TODO: should 1 &2 be combined? + /// 2) Any sever extensions sent as part of the handshake response + /// 3) An NtorV3-Like key generator used to bootstrap the codec that will + /// be used to obfuscate the stream data. + /// + /// Errors can be cause by: + /// - failing to connect to remote host (timeout) + /// - failing to write the handshake + /// - timeout / cancel while waiting for response + /// - failing to read response + /// - crypto error in response + /// - response fails server auth check + /// + /// TODO: make sure failure modes are understood (FIN/RST w/ and w/out buffered data, etc.) + pub async fn handshake(self, mut stream: T, deadline: Option) -> Result> + where + T: AsyncRead + AsyncWrite + Unpin, + { + // set up for handshake + let mut session = self.transition(ClientHandshaking {}); + + let materials = CHSMaterials::new(&session.node_pubkey, session.session_id()); + + // default deadline + let d_def = Instant::now() + CLIENT_HANDSHAKE_TIMEOUT; + let handshake_fut = Self::complete_handshake(&mut stream, materials, deadline); + let (mut remainder, mut keygen) = + match tokio::time::timeout_at(deadline.unwrap_or(d_def), handshake_fut).await { + Ok(result) => match result { + Ok(handshake) => handshake, + Err(e) => { + // non-timeout error, + let id = session.session_id(); + let _ = session.fault(ClientHandshakeFailed { + details: format!("{id} handshake failed {e}"), + }); + return Err(e); + } + }, + Err(_) => { + let id = session.session_id(); + let _ = session.fault(ClientHandshakeFailed { + details: format!("{id} timed out"), + }); + return Err(Error::HandshakeTimeout); + } + }; + + // post-handshake state updates + session.set_session_id(keygen.session_id()); + let mut codec: framing::O5Codec = keygen.into(); + + let res = codec.decode(&mut remainder); + if let Ok(Some(framing::Messages::PrngSeed(seed))) = res { + // try to parse the remainder of the server hello packet as a + // PrngSeed since it should be there. + let len_seed = drbg::Seed::from(seed); + session.set_len_seed(len_seed); + } else { + debug!("NOPE {res:?}"); + } + + // mark session as Established + let session_state: ClientSession = session.transition(Established {}); + info!("{} handshake complete", session_state.session_id()); + + codec.handshake_complete(); + let o4 = ObfuscatedStream::new(stream, codec, Session::Client(session_state)); + + Ok(O5Stream::from_o4(o4)) + } + + async fn complete_handshake( + mut stream: T, + materials: CHSMaterials, + deadline: Option, + ) -> Result<(BytesMut, impl NtorV3KeyGen)> + where + T: AsyncRead + AsyncWrite + Unpin, + { + // let session_id = materials.session_id; + let (state, chs_message) = NtorV3Client::client1(materials)?; + // let mut file = tokio::fs::File::create("message.hex").await?; + // file.write_all(&chs_message).await?; + stream.write_all(&chs_message).await?; + + debug!( + "{} handshake sent {}B, waiting for sever response", + materials.session_id, + chs_message.len() + ); + + let mut buf = [0u8; MAX_HANDSHAKE_LENGTH]; + loop { + let n = stream.read(&mut buf).await?; + if n == 0 { + Err(Error::IOError(IoError::new( + IoErrorKind::UnexpectedEof, + "read 0B in client handshake", + )))? + } + debug!( + "{} read {n}/{}B of server handshake", + materials.session_id, + buf.len() + ); + + match NtorV3Client::client2(state, &buf[..n]) { + Ok(r) => return Ok(r), + Err(Error::HandshakeErr(RelayHandshakeError::EAgain)) => continue, + Err(e) => { + // if a deadline was set and has not passed already, discard + // from the stream until the deadline, then close. + if deadline.is_some_and(|d| d > Instant::now()) { + debug!("{} discarding due to: {e}", materials.session_id); + discard(&mut stream, deadline.unwrap() - Instant::now()).await?; + } + stream.shutdown().await?; + return Err(e); + } + } + } + } +} + +impl ClientSession { + pub(crate) fn set_len_seed(&mut self, seed: drbg::Seed) { + debug!( + "{} setting length seed {}", + self.session_id(), + hex::encode(seed.as_bytes()) + ); + self.len_seed = seed; + } +} + +impl std::fmt::Debug for ClientSession { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[ id:{}, ident_pk:{}, epoch_hr:{} ]", + hex::encode(self.node_pubkey.id.as_bytes()), + hex::encode(self.node_pubkey.pk.as_bytes()), + self.epoch_hour, + ) + } +} diff --git a/crates/o5/src/sessions/server.rs b/crates/o5/src/sessions/server.rs new file mode 100644 index 0000000..ee76e73 --- /dev/null +++ b/crates/o5/src/sessions/server.rs @@ -0,0 +1,224 @@ +use crate::{ + common::{ + discard, drbg, + ntor_arti::{ + AuxDataReply, RelayHandshakeError, ServerHandshake, SessionID, SessionIdentifier, + }, + }, + constants::*, + framing, + handshake::{NtorV3KeyGen, SHSMaterials}, + proto::{O5Stream, ObfuscatedStream}, + server::Server, + sessions::{Established, Fault, Initialized, Session}, + Error, Result, +}; + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use ptrs::{debug, info, trace}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::time::Instant; + +// ================================================================ // +// Server Sessions States // +// ================================================================ // + +pub(crate) struct ServerSession { + // -------- fixed by server -------- + pub(crate) biased: bool, + + // pub(crate) server: &'a Server, + + // -------- generated per session -------- + /// Session Identifier + /// + /// Generated randomly to begin with then deterministically derived from the + /// shared secret once session is established such that the client and server + /// session_id values match. + pub(crate) session_id: SessionID, + /// Packet (padding) length seed + /// + /// Used when selecting lengths to add onto packets to obscure client data length. + pub(crate) len_seed: drbg::Seed, + /// Inter-packet timing seed + /// + /// Used when generating delays between packets. + pub(crate) ipt_seed: drbg::Seed, + + pub(crate) _state: S, +} + +pub(crate) struct ServerHandshaking {} + +#[allow(unused)] +pub(crate) struct ServerHandshakeFailed { + details: String, +} + +pub(crate) trait ServerSessionState {} +impl ServerSessionState for Initialized {} +impl ServerSessionState for ServerHandshaking {} +impl ServerSessionState for Established {} + +impl ServerSessionState for ServerHandshakeFailed {} +impl Fault for ServerHandshakeFailed {} + +impl ServerSession { + pub fn session_id(&self) -> String { + String::from("s-") + &self.session_id.to_string() + } + + pub(crate) fn biased(&self) -> bool { + self.biased + } + + pub fn len_seed(&self) -> drbg::Seed { + self.len_seed.clone() + } + + pub(crate) fn set_session_id(&mut self, id: SessionID) { + debug!("{} -> {} server updating session id", self.session_id, id); + self.session_id = id; + } + + /// Helper function to perform state transitions. + fn transition(self, _state: T) -> ServerSession { + ServerSession { + // fixed by server + biased: self.biased, + + // generated per session + session_id: self.session_id, + len_seed: self.len_seed, + ipt_seed: self.ipt_seed, + + _state, + } + } + + /// Helper function to perform state transition on error. + fn fault(self, f: F) -> ServerSession { + ServerSession { + // fixed by server + biased: self.biased, + + // generated per session + session_id: self.session_id, + len_seed: self.len_seed, + ipt_seed: self.ipt_seed, + + _state: f, + } + } +} + +impl ServerSession { + /// Attempt to complete the handshake with a new client connection. + pub async fn handshake, T>( + self, + server: &Server, + mut stream: T, + extensions_handler: &mut REPLY, + deadline: Option, + ) -> Result> + where + T: AsyncRead + AsyncWrite + Unpin, + { + // set up for handshake + let mut session = self.transition(ServerHandshaking {}); + + let materials = SHSMaterials::new(session.session_id(), session.len_seed.to_bytes()); + + // default deadline + let d_def = Instant::now() + SERVER_HANDSHAKE_TIMEOUT; + let handshake_fut = + server.complete_handshake(&mut stream, extensions_handler, materials, deadline); + + let mut keygen = + match tokio::time::timeout_at(deadline.unwrap_or(d_def), handshake_fut).await { + Ok(result) => match result { + Ok(handshake) => handshake, + Err(e) => { + // non-timeout error, + let id = session.session_id(); + let _ = session.fault(ServerHandshakeFailed { + details: format!("{id} handshake failed {e}"), + }); + return Err(e); + } + }, + Err(_) => { + let id = session.session_id(); + let _ = session.fault(ServerHandshakeFailed { + details: format!("{id} timed out"), + }); + return Err(Error::HandshakeTimeout); + } + }; + + // post handshake state updates + session.set_session_id(keygen.session_id()); + let mut codec: framing::O5Codec = keygen.into(); + + // mark session as Established + let session_state: ServerSession = session.transition(Established {}); + + codec.handshake_complete(); + let o4 = ObfuscatedStream::new(stream, codec, Session::Server(session_state)); + + Ok(O5Stream::from_o4(o4)) + } +} + +impl Server { + /// Complete the handshake with the client. This function assumes that the + /// client has already sent a message and that we do not know yet if the + /// message is valid. + async fn complete_handshake, T>( + &self, + mut stream: T, + reply_fn: &mut REPLY, + materials: SHSMaterials, + deadline: Option, + ) -> Result> + where + T: AsyncRead + AsyncWrite + Unpin, + { + let session_id = materials.session_id.clone(); + + // wait for and attempt to consume the client hello message + let mut buf = [0_u8; MAX_HANDSHAKE_LENGTH]; + loop { + let n = stream.read(&mut buf).await?; + if n == 0 { + stream.shutdown().await?; + return Err(IoError::from(IoErrorKind::UnexpectedEof).into()); + } + trace!("{} successful read {n}B", session_id); + + match self.server(reply_fn, &materials, &buf[..n]) { + Ok((keygen, response)) => { + stream.write_all(&response).await?; + info!("{} handshake complete", session_id); + return Ok(keygen); + } + Err(RelayHandshakeError::EAgain) => { + trace!("{} reading more", session_id); + continue; + } + Err(e) => { + trace!("{} failed to parse client handshake: {e}", session_id); + // if a deadline was set and has not passed already, discard + // from the stream until the deadline, then close. + if deadline.is_some_and(|d| d > Instant::now()) { + debug!("{} discarding due to: {e}", session_id); + discard(&mut stream, deadline.unwrap() - Instant::now()).await? + } + stream.shutdown().await?; + return Err(e.into()); + } + }; + } + } +} diff --git a/crates/o5/src/test_utils/fake_prng.rs b/crates/o5/src/test_utils/fake_prng.rs new file mode 100644 index 0000000..28279ef --- /dev/null +++ b/crates/o5/src/test_utils/fake_prng.rs @@ -0,0 +1,27 @@ +pub(crate) struct FakePRNG<'a> { + bytes: &'a [u8], +} +impl<'a> FakePRNG<'a> { + pub(crate) fn new(bytes: &'a [u8]) -> Self { + Self { bytes } + } +} +impl<'a> rand_core::RngCore for FakePRNG<'a> { + fn next_u32(&mut self) -> u32 { + rand_core::impls::next_u32_via_fill(self) + } + fn next_u64(&mut self) -> u64 { + rand_core::impls::next_u64_via_fill(self) + } + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> std::result::Result<(), rand_core::Error> { + self.fill_bytes(dest); + Ok(()) + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + assert!(dest.len() <= self.bytes.len()); + + dest.copy_from_slice(&self.bytes[0..dest.len()]); + self.bytes = &self.bytes[dest.len()..]; + } +} +impl rand_core::CryptoRng for FakePRNG<'_> {} diff --git a/crates/o5/src/test_utils/mod.rs b/crates/o5/src/test_utils/mod.rs new file mode 100644 index 0000000..f88fb88 --- /dev/null +++ b/crates/o5/src/test_utils/mod.rs @@ -0,0 +1,185 @@ +#![cfg(test)] +#![allow(dead_code)] + +mod fake_prng; +pub mod tests; +pub(crate) use fake_prng::*; + +use std::env; +use std::io::{Read, Result, Write}; +use std::os::unix::net::UnixStream; +use std::str::FromStr; +use std::sync::Once; + +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::UnixStream as AsyncUnixStream; +use tracing_subscriber::filter::LevelFilter; + +static SUBSCRIBER_INIT: Once = Once::new(); + +pub fn init_subscriber() { + SUBSCRIBER_INIT.call_once(|| { + let level = env::var("RUST_LOG_LEVEL").unwrap_or("error".into()); + let lf = LevelFilter::from_str(&level).unwrap(); + + tracing_subscriber::fmt().with_max_level(lf).init(); + }); +} + +#[cfg(unix)] +pub fn pipe_set() -> Result<( + (impl Read + Write + Sized, impl Read + Write + Sized), + (impl Read + Write + Sized, impl Read + Write + Sized), +)> { + Ok((UnixStream::pair()?, UnixStream::pair()?)) +} + +#[cfg(unix)] +pub fn pipes() -> Result<(impl Read + Write + Sized, impl Read + Write + Sized)> { + UnixStream::pair() +} + +#[cfg(unix)] +pub fn pipe_set_async() -> Result<( + ( + impl AsyncRead + AsyncWrite + Sized, + impl AsyncRead + AsyncWrite + Sized, + ), + ( + impl AsyncRead + AsyncWrite + Sized, + impl AsyncRead + AsyncWrite + Sized, + ), +)> { + Ok((AsyncUnixStream::pair()?, AsyncUnixStream::pair()?)) +} + +#[cfg(unix)] +pub fn pipe_set_async_unixstream() -> Result<( + (AsyncUnixStream, AsyncUnixStream), + (AsyncUnixStream, AsyncUnixStream), +)> { + Ok((AsyncUnixStream::pair()?, AsyncUnixStream::pair()?)) +} + +#[cfg(unix)] +pub fn pipes_async() -> Result<( + impl AsyncRead + AsyncWrite + Sized, + impl AsyncRead + AsyncWrite + Sized, +)> { + AsyncUnixStream::pair() +} + +// // TODO: implement with something like named_pipes for windows +// #[cfg(windows)] +// pub fn pipe_set() -> ((RW,RW), (RW,RW)) +// where +// RW: Read + Write + ?Sized +// { +// // Ok((UnixStream::pair()?, UnixStream::pair()?)) +// } + +#[cfg(test)] +mod test { + + use super::*; + use std::{io, thread}; + + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + #[cfg(unix)] + #[tokio::test] + async fn build_async_pipes() { + let (mut client_host, mut client_wasm) = pipes_async().unwrap(); + let (mut wasm_remote, mut remote) = AsyncUnixStream::pair().unwrap(); + + let buf = b"hello world"; + + tokio::spawn(async move { + let transport_result = { + client_host.write_all(buf).await.unwrap(); + tokio::io::copy(&mut client_wasm, &mut wasm_remote).await + }; + assert!(transport_result.is_ok()); + let n = transport_result.unwrap() as usize; + assert_eq!(n, buf.len()); + }); + + let mut out = vec![0_u8; 1024]; + let nr = remote.read(&mut out).await.unwrap(); + assert_eq!(nr, buf.len()); + assert_eq!(std::str::from_utf8(&out[..nr]).unwrap(), "hello world"); + } + + #[cfg(unix)] + #[tokio::test] + async fn build_async_pipe_set() { + let ((mut client_host, mut client_wasm), (mut wasm_remote, mut remote)) = + pipe_set_async().unwrap(); + + let buf = b"hello world"; + + tokio::spawn(async move { + let transport_result = { + client_host.write_all(buf).await.unwrap(); + tokio::io::copy(&mut client_wasm, &mut wasm_remote).await + }; + assert!(transport_result.is_ok()); + let n = transport_result.unwrap() as usize; + assert_eq!(n, buf.len()); + }); + + let mut out = vec![0_u8; 1024]; + let nr = remote.read(&mut out).await.unwrap(); + assert_eq!(nr, buf.len()); + assert_eq!(std::str::from_utf8(&out[..nr]).unwrap(), "hello world"); + } + + #[cfg(unix)] + #[test] + fn build_pipes() -> Result<()> { + let (mut client_host, mut client_wasm) = pipes()?; + let (mut wasm_remote, mut remote) = UnixStream::pair()?; + + let buf = b"hello world"; + + thread::spawn(move || { + let transport_result = { + client_host.write_all(buf).unwrap(); + io::copy(&mut client_wasm, &mut wasm_remote) + }; + assert!(transport_result.is_ok()); + let n = transport_result.unwrap() as usize; + assert_eq!(n, buf.len()); + }); + + let mut out = vec![0_u8; 1024]; + let nr = remote.read(&mut out)?; + assert_eq!(nr, buf.len()); + assert_eq!(std::str::from_utf8(&out[..nr]).unwrap(), "hello world"); + Ok(()) + } + + #[cfg(unix)] + #[test] + fn build_pipe_set() -> Result<()> { + let ((mut client_host, mut client_wasm), (mut wasm_remote, mut remote)) = pipe_set()?; + + let buf = b"hello world"; + + thread::spawn(move || { + let transport_result = { + client_host.write_all(buf).unwrap(); + io::copy(&mut client_wasm, &mut wasm_remote) + }; + assert!(transport_result.is_ok()); + let n = transport_result.unwrap() as usize; + assert_eq!(n, buf.len()); + }); + + let mut out = vec![0_u8; 1024]; + let nr = remote.read(&mut out)?; + assert_eq!(nr, buf.len()); + assert_eq!(std::str::from_utf8(&out[..nr]).unwrap(), "hello world"); + Ok(()) + } +} diff --git a/crates/o5/src/test_utils/tests.rs b/crates/o5/src/test_utils/tests.rs new file mode 100644 index 0000000..ce81e5b --- /dev/null +++ b/crates/o5/src/test_utils/tests.rs @@ -0,0 +1,101 @@ +// use crate::{stream::Stream, Error, Result}; +use crate::Result; + +// use futures::join; + +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf}; + +use ptrs::debug; +/* +/// +/// write ===================> encode ===================> >| +/// read <=================== decode <=================== <| echo +/// +/// [ loop Buffer ] -> | source | -> | plaintext | -> | ciphertext | -> | echo | +/// pipe pipe +/// +#[allow(non_snake_case)] +pub async fn duplex_end_to_end_1_MB<'a, A, B>( + source: A, + mut plaintext: A, + mut ciphertext: B, + echo: B, + duplex: impl DuplexTransform + 'a, +) -> Result<(u64, u64)> +where + A: Stream<'a> + 'a, + B: Stream<'a> + 'a, +{ + let proxy_task = async { + let r = duplex + .copy_bidirectional(&mut plaintext, &mut ciphertext) + .await; + plaintext.flush().await?; + plaintext.shutdown().await?; + std::thread::sleep(std::time::Duration::from_millis(100)); + ciphertext.flush().await?; + ciphertext.shutdown().await?; + debug!("proxy finished"); + r + }; + + let (echo_r, echo_w) = tokio::io::split(echo); + let echo_task = echo_fn(echo_r, echo_w); + + let (source_r, source_w) = tokio::io::split(source); + let trash_task = trash(source_r); + + let client_write = write_and_close(source_w); + + let (trash_result, proxy_result, echo_result, client_result) = + join!(trash_task, proxy_task, echo_task, client_write); + echo_result.unwrap(); // ensure result is Ok - otherwise result is useless. + assert_eq!(client_result?, 1024 * 1024); + assert_eq!(trash_result?, 1024 * 1024); + + debug!("test_complete"); + let out = proxy_result.map_err(Error::IOError); + debug!("returning"); + out +} +*/ + +async fn echo_fn<'a, A, B>(mut r: ReadHalf, mut w: WriteHalf) -> std::io::Result<()> +where + A: AsyncRead + Unpin + 'a, + B: AsyncWrite + Unpin + 'a, +{ + let _n = tokio::io::copy(&mut r, &mut w).await?; + _ = w.write(&[]).await?; + w.flush().await?; + w.shutdown().await?; + debug!("echo_fn finished"); + Ok(()) +} + +async fn write_and_close<'a, A: AsyncWrite + Unpin + 'a>( + mut w: WriteHalf, +) -> std::io::Result { + let write_me = vec![0_u8; 1024]; + let mut n = 0; + for _ in 0..1024 { + n += w.write(&write_me).await?; + } + n += w.write(&[]).await?; + w.flush().await?; + assert_eq!(n, 1024 * 1024); + + debug!("finished writing... sleeping 1s"); + std::thread::sleep(std::time::Duration::from_millis(100)); + w.shutdown().await?; + debug!("writer closed"); + Ok(n) +} + +async fn trash<'a, A: AsyncRead + Unpin + 'a>(mut r: ReadHalf) -> Result { + let out_file = tokio::fs::File::create("/dev/null").await.unwrap(); + let mut out_file = tokio::io::BufWriter::new(out_file); + let n = tokio::io::copy(&mut r, &mut out_file).await.unwrap(); + debug!("trash finished"); + Ok(n) +} diff --git a/crates/o5/src/testing.rs b/crates/o5/src/testing.rs new file mode 100644 index 0000000..71c9241 --- /dev/null +++ b/crates/o5/src/testing.rs @@ -0,0 +1,355 @@ +use crate::{test_utils::init_subscriber, Result, Server}; + +use ptrs::{debug, trace}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use std::cmp::Ordering; +use std::time::Duration; + +#[tokio::test] +async fn public_handshake() -> Result<()> { + init_subscriber(); + let (mut c, mut s) = tokio::io::duplex(65_536); + let mut rng = rand::thread_rng(); + + let o4_server = Server::new_from_random(&mut rng); + let client_config = o4_server.client_params(); + + tokio::spawn(async move { + let o4s_stream = o4_server.wrap(&mut s).await.unwrap(); + let _ = tokio::io::split(o4s_stream); + }); + + let o4_client = client_config.build(); + let _o4c_stream = o4_client.wrap(&mut c).await?; + + Ok(()) +} + +#[tokio::test] +async fn public_iface() -> Result<()> { + init_subscriber(); + let message = b"awoewaeojawenwaefaw lfawn;awe da;wfenalw fawf aw"; + let (mut c, mut s) = tokio::io::duplex(65_536); + let mut rng = rand::thread_rng(); + + let o4_server = Server::new_from_random(&mut rng); + let client_config = o4_server.client_params(); + + tokio::spawn(async move { + let mut o4s_stream = o4_server.wrap(&mut s).await.unwrap(); + // let (mut r, mut w) = tokio::io::split(o4s_stream); + // tokio::io::copy(&mut r, &mut w).await.unwrap(); + + let mut buf = [0_u8; 50]; + let n = o4s_stream.read(&mut buf).await.unwrap(); + o4s_stream.write_all(&buf[..n]).await.unwrap(); + o4s_stream.flush().await.unwrap(); + + if n != 48 { + debug!("echo lengths don't match {n} != 48"); + } + }); + + let o4_client = client_config.build(); + let mut o4c_stream = o4_client.wrap(&mut c).await?; + + o4c_stream.write_all(&message[..]).await?; + o4c_stream.flush().await?; + + let mut buf = vec![0_u8; message.len()]; + let n = o4c_stream.read(&mut buf).await?; + assert_eq!(n, message.len()); + assert_eq!( + &message[..], + &buf, + "\"{}\" != \"{}\"", + String::from_utf8(message.to_vec())?, + String::from_utf8(buf.clone())?, + ); + + Ok(()) +} + +#[allow(non_snake_case)] +#[tokio::test] +async fn transfer_10k_x1() -> Result<()> { + init_subscriber(); + + let (c, mut s) = tokio::io::duplex(1024 * 1000); + let mut rng = rand::thread_rng(); + + let o4_server = Server::new_from_random(&mut rng); + let client_config = o4_server.client_params(); + + tokio::spawn(async move { + let o4s_stream = o4_server.wrap(&mut s).await.unwrap(); + let (mut r, mut w) = tokio::io::split(o4s_stream); + tokio::io::copy(&mut r, &mut w).await.unwrap(); + }); + + let o4_client = client_config.build(); + let o4c_stream = o4_client.wrap(c).await?; + + let (mut r, mut w) = tokio::io::split(o4c_stream); + + tokio::spawn(async move { + let msg = [0_u8; 10240]; + w.write_all(&msg) + .await + .unwrap_or_else(|e| panic!("failed on write {e}")); + w.flush().await.unwrap(); + }); + + let expected_total = 10240; + let mut buf = vec![0_u8; 1024 * 11]; + let mut received: usize = 0; + for i in 0..8 { + debug!("client read: {i}"); + tokio::select! { + res = r.read(&mut buf) => { + let n = res?; + received += n; + trace!("received: {n}: total:{received}"); + } + _ = tokio::time::sleep(std::time::Duration::from_millis(1000)) => { + panic!("client failed to read after {i} iterations: timeout"); + } + } + } + + if received != expected_total { + panic!("incorrect amount received {received} != {expected_total}"); + } + Ok(()) +} + +#[allow(non_snake_case)] +#[tokio::test] +async fn transfer_10k_x3() -> Result<()> { + init_subscriber(); + + let (c, mut s) = tokio::io::duplex(1024 * 1000); + + let mut rng = rand::thread_rng(); + let o4_server = Server::new_from_random(&mut rng); + let client_config = o4_server.client_params(); + + tokio::spawn(async move { + let o4s_stream = o4_server.wrap(&mut s).await.unwrap(); + let (mut r, mut w) = tokio::io::split(o4s_stream); + tokio::io::copy(&mut r, &mut w).await.unwrap(); + }); + + let o4_client = client_config.build(); + let o4c_stream = o4_client.wrap(c).await?; + + let (mut r, mut w) = tokio::io::split(o4c_stream); + + tokio::spawn(async move { + for _ in 0..3 { + let msg = [0_u8; 10240]; + w.write_all(&msg) + .await + .unwrap_or_else(|e| panic!("failed on write {e}")); + w.flush().await.unwrap(); + } + }); + + let expected_total = 10240 * 3; + let mut buf = vec![0_u8; 1024 * 32]; + let mut received: usize = 0; + for i in 0..24 { + // debug!("client read: {i}"); + tokio::select! { + res = r.read(&mut buf) => { + let n = res?; + received += n; + trace!("received: {n}: total:{received}"); + } + _ = tokio::time::sleep(std::time::Duration::from_millis(1000)) => { + panic!("client failed to read after {i} iterations: timeout"); + } + } + } + + if received != expected_total { + panic!("incorrect amount received {received} != {expected_total}"); + } + Ok(()) +} + +#[allow(non_snake_case)] +#[tokio::test] +async fn transfer_1M_1024x1024() -> Result<()> { + init_subscriber(); + + let (c, mut s) = tokio::io::duplex(1024 * 1000); + let mut rng = rand::thread_rng(); + + let o4_server = Server::new_from_random(&mut rng); + let client_config = o4_server.client_params(); + + tokio::spawn(async move { + let o4s_stream = o4_server.wrap(&mut s).await.unwrap(); + let (mut r, mut w) = tokio::io::split(o4s_stream); + tokio::io::copy(&mut r, &mut w).await.unwrap(); + }); + + let o4_client = client_config.build(); + let o4c_stream = o4_client.wrap(c).await?; + + let (mut r, mut w) = tokio::io::split(o4c_stream); + + tokio::spawn(async move { + let msg = [0_u8; 1024]; + for i in 0..1024 { + w.write_all(&msg) + .await + .unwrap_or_else(|e| panic!("failed on write #{i}: {e}")); + w.flush().await.unwrap(); + } + }); + + let expected_total = 1024 * 1024; + let mut buf = vec![0_u8; 1024 * 1024]; + let mut received: usize = 0; + for i in 0..1024 { + // debug!("client read: {i}"); + tokio::select! { + res = r.read(&mut buf) => { + received += res?; + } + _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => { + panic!("client failed to read after {i} iterations: timeout"); + } + } + } + + if received != expected_total { + panic!("incorrect amount received {received} != {expected_total}"); + } + Ok(()) +} + +#[allow(non_snake_case)] +#[tokio::test] +async fn transfer_512k_x1() -> Result<()> { + init_subscriber(); + + let (c, mut s) = tokio::io::duplex(1024 * 512); + let mut rng = rand::thread_rng(); + + let o4_server = Server::new_from_random(&mut rng); + let client_config = o4_server.client_params(); + + tokio::spawn(async move { + let o4s_stream = o4_server.wrap(&mut s).await.unwrap(); + let (mut r, mut w) = tokio::io::split(o4s_stream); + tokio::io::copy(&mut r, &mut w).await.unwrap(); + }); + + let o4_client = client_config.build(); + let o4c_stream = o4_client.wrap(c).await?; + + let (mut r, mut w) = tokio::io::split(o4c_stream); + + tokio::spawn(async move { + let msg = [0_u8; 1024 * 512]; + w.write_all(&msg) + .await + .unwrap_or_else(|_| panic!("failed on write")); + w.flush().await.unwrap(); + }); + + let expected_total = 1024 * 512; + let mut buf = vec![0_u8; 1024 * 1024]; + let mut received: usize = 0; + let mut i = 0; + while received < expected_total { + debug!("client read: {i} / {received}"); + tokio::select! { + res = r.read(&mut buf) => { + received += res?; + } + _ = tokio::time::sleep(std::time::Duration::from_millis(2000)) => { + panic!("client failed to read after {i} iterations: timeout"); + } + } + i += 1; + } + + assert_eq!( + received, expected_total, + "incorrect amount received {received} != {expected_total}" + ); + Ok(()) +} + +#[tokio::test] +async fn transfer_2_x() -> Result<()> { + init_subscriber(); + + let (c, mut s) = tokio::io::duplex(1024 * 1000); + let mut rng = rand::thread_rng(); + + let o4_server = Server::new_from_random(&mut rng); + let client_config = o4_server.client_params(); + + tokio::spawn(async move { + let o4s_stream = o4_server.wrap(&mut s).await.unwrap(); + let (mut r, mut w) = tokio::io::split(o4s_stream); + tokio::io::copy(&mut r, &mut w).await.unwrap(); + }); + + let o4_client = client_config.build(); + let o4c_stream = o4_client.wrap(c).await?; + + let (mut r, mut w) = tokio::io::split(o4c_stream); + + let base: usize = 2; + tokio::spawn(async move { + for i in (0..20).step_by(2) { + let msg = vec![0_u8; base.pow(i)]; + w.write_all(&msg) + .await + .unwrap_or_else(|_| panic!("failed on write #{i}")); + debug!("wrote 2^{i}"); + w.flush().await.unwrap(); + } + }); + + let mut buf = vec![0_u8; 1024 * 1024 * 100]; + let expected_total: usize = (0..20).step_by(2).map(|i| base.pow(i)).sum(); + let mut received = 0; + + let mut i = 0; + loop { + let res_timeout = + tokio::time::timeout(Duration::from_millis(10000), r.read(&mut buf)).await; + + let res = res_timeout.unwrap(); + let n = res?; + received += n; + if n == 0 { + debug!("read 0?"); + break; + } else { + debug!("({i}) read {n}B - {received}"); + } + + match received.cmp(&expected_total) { + Ordering::Less => {} + Ordering::Equal => break, + Ordering::Greater => { + panic!("received more than expected {received} > {expected_total}") + } + } + i += 1; + } + + if received != expected_total { + panic!("incorrect amount received {received} != {expected_total}"); + } + Ok(()) +} diff --git a/crates/o5/src/transport.rs b/crates/o5/src/transport.rs deleted file mode 100644 index a17c878..0000000 --- a/crates/o5/src/transport.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::traits::*; -use std::error::Error; - - -const NAME: &'static str = "o7"; - -#[allow(non_camel_case_types)] -pub struct o7 {} - -impl Named for o7 { - fn name() -> &'static str { - NAME - } -} - -#[derive(Default, Clone, Copy, Debug)] -pub struct Client {} -impl Named for Client { - fn name() -> &'static str { - NAME+"_client" - } -} - - -#[derive(Default, Clone, Copy, Debug)] -pub struct Server {} -impl Named for Server { - fn name() -> &'static str { - NAME+"_server" - } -} - -struct Cfg { - s: String -} - -impl ToConfig for Cfg {} -impl From<&'static str> for Cfg { - fn from(s: &'static str) -> Self { - Cfg { s: s.to_string() } - } -} - -impl From> for Cfg { - fn from(s: dyn Iterator) -> Self { - Cfg { s: s.try_collect().unwrap()} - } -} - - -impl Ingress for Client {} -impl Configurable for Client { - fn with_config(mut self, cfg: Cfg)-> Result { - Ok(self) - } -} - -impl Egress for Server {} -impl Configurable for Server { - fn with_config(mut self, cfg: Cfg)-> Result { - Ok(self) - } -} - -impl IngressBuilder for o7 { - fn client() -> Result { - Ok(Client::default()) - } -} - -impl EgressBuilder for o7 { - fn server(args: Option<()>) -> Result { - Ok(()) - } -} diff --git a/crates/obfs4/Cargo.toml b/crates/obfs4/Cargo.toml index ef59b93..f599277 100644 --- a/crates/obfs4/Cargo.toml +++ b/crates/obfs4/Cargo.toml @@ -54,10 +54,10 @@ tokio-util = { version = "0.7.10", features = ["codec", "io"]} bytes = "1.5.0" ## ntor_arti -tor-cell = "0.22.0" -tor-llcrypto = "0.22.0" -tor-error = "0.22.0" -tor-bytes = "0.22.0" +tor-cell = "0.23.0" +tor-llcrypto = "0.23.0" +tor-error = "0.23.0" +tor-bytes = "0.23.0" cipher = "0.4.4" zeroize = "1.7.0" thiserror = "1.0.56" @@ -81,7 +81,7 @@ filetime = {version="0.2.25", optional=true} [dev-dependencies] tracing-subscriber = "0.3.18" hex-literal = "0.4.1" -tor-basic-utils = "0.22.0" +tor-basic-utils = "0.23.0" # benches # criterion = "0.5" diff --git a/crates/obfs4/src/handshake/handshake_client.rs b/crates/obfs4/src/handshake/handshake_client.rs index 0ac5345..d0ff22a 100644 --- a/crates/obfs4/src/handshake/handshake_client.rs +++ b/crates/obfs4/src/handshake/handshake_client.rs @@ -53,7 +53,7 @@ pub(super) fn client_handshake_obfs4( client_handshake_obfs4_no_keygen(my_sk, materials.clone()) } -/// Helper: client handshake _without_ generating new keys. +/// Helper: client handshake _without_ generating new keys. pub(crate) fn client_handshake_obfs4_no_keygen( ephem: EphemeralSecret, materials: HandshakeMaterials, diff --git a/doc/pq_obfs/obfs4.png b/doc/pq_obfs/obfs4.png new file mode 100644 index 0000000..ee19b7c Binary files /dev/null and b/doc/pq_obfs/obfs4.png differ diff --git a/doc/pq_obfs/pq_obfs_full.png b/doc/pq_obfs/pq_obfs_full.png new file mode 100644 index 0000000..bca4312 Binary files /dev/null and b/doc/pq_obfs/pq_obfs_full.png differ diff --git a/doc/pq_obfs/pq_obfs_step-1.png b/doc/pq_obfs/pq_obfs_step-1.png new file mode 100644 index 0000000..7cbd091 Binary files /dev/null and b/doc/pq_obfs/pq_obfs_step-1.png differ diff --git a/doc/pq_obfs/pq_obfs_step-2.png b/doc/pq_obfs/pq_obfs_step-2.png new file mode 100644 index 0000000..d2fe287 Binary files /dev/null and b/doc/pq_obfs/pq_obfs_step-2.png differ diff --git a/doc/pq_obfs/pq_obfs_step-3.png b/doc/pq_obfs/pq_obfs_step-3.png new file mode 100644 index 0000000..452dd9e Binary files /dev/null and b/doc/pq_obfs/pq_obfs_step-3.png differ diff --git a/doc/pq_obfs/st_obfs4.png b/doc/pq_obfs/st_obfs4.png new file mode 100644 index 0000000..53c69e8 Binary files /dev/null and b/doc/pq_obfs/st_obfs4.png differ