diff --git a/crates/o5/Cargo.toml b/crates/o5/Cargo.toml index 82aada2..ff78b12 100644 --- a/crates/o5/Cargo.toml +++ b/crates/o5/Cargo.toml @@ -8,19 +8,53 @@ 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"]} +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=["salsa20"]} 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"} -# ntor_arti +## 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" + +## 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.16.0" +tor-llcrypto = "0.7.0" +tor-error = "0.6.1" +tor-bytes = "0.10.0" +cipher = "0.4.4" zeroize = "1.7.0" +thiserror = "1.0.56" [dev-dependencies] -hex = "0.4.3" anyhow = "1.0" +tracing-subscriber = "0.3.18" +hex-literal = "0.4.1" +tor-basic-utils = "0.8.0" # o5 pqc test # pqc_kyber = {version="0.7.1", features=["kyber1024", "std"]} 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..eae4470 --- /dev/null +++ b/crates/o5/src/client.rs @@ -0,0 +1,188 @@ +#![allow(unused)] + +use crate::{ + common::{colorize, HmacSha256}, + constants::*, + framing::{FrameError, Marshall, O5Codec, TryParse, KEY_LENGTH, KEY_MATERIAL_LENGTH}, + handshake::O5NtorPublicKey, + proto::{MaybeTimeout, O5Stream, IAT}, + 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 iat_mode: IAT, + pub station_pubkey: [u8; KEY_LENGTH], + pub station_id: [u8; NODE_ID_LENGTH], + pub statefile_path: Option, + pub(crate) handshake_timeout: MaybeTimeout, +} + +impl Default for ClientBuilder { + fn default() -> Self { + Self { + iat_mode: IAT::Off, + station_pubkey: [0u8; KEY_LENGTH], + 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 { + iat_mode: IAT::Off, + station_pubkey: [0_u8; KEY_LENGTH], + 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 { + iat_mode: IAT::Off, + station_pubkey: [0_u8; KEY_LENGTH], + station_id: [0_u8; NODE_ID_LENGTH], + statefile_path: None, + handshake_timeout: MaybeTimeout::Default_, + }) + } + + pub fn with_node_pubkey(&mut self, pubkey: [u8; KEY_LENGTH]) -> &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_iat_mode(&mut self, iat: IAT) -> &mut Self { + self.iat_mode = iat; + 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 { + iat_mode: self.iat_mode, + station_pubkey: O5NtorPublicKey { + id: self.station_id.into(), + pk: self.station_pubkey.into(), + }, + 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 { + iat_mode: IAT, + station_pubkey: O5NtorPublicKey, + 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, self.iat_mode); + + 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, self.iat_mode); + + 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/curve25519.rs b/crates/o5/src/common/curve25519.rs new file mode 100644 index 0000000..5bdd713 --- /dev/null +++ b/crates/o5/src/common/curve25519.rs @@ -0,0 +1,157 @@ +//! Re-exporting Curve25519 implementations. +//! +//! *TODO*: Eventually we should probably recommend using this code via some +//! key-agreement trait, but for now we are just re-using the APIs from +//! [`x25519_dalek`]. + +// TODO: We may want eventually want to expose ReusableSecret instead of +// StaticSecret, for use in places where we need to use a single secret +// twice in one handshake, but we do not need that secret to be persistent. +// +// The trouble here is that if we use ReusableSecret in these cases, we +// cannot easily construct it for testing purposes. We could in theory +// kludge something together using a fake Rng, but that might be more +// trouble than we want to go looking for. +#[allow(unused)] +pub use x25519_dalek::{ + EphemeralSecret, PublicKey, PublicRepresentative, ReusableSecret, SharedSecret, StaticSecret, +}; + +use rand_core::{CryptoRng, RngCore}; + +pub const REPRESENTATIVE_LENGTH: usize = 32; + +/// Curve25519 keys that are guaranteed to have a valid Elligator2 representative. +/// As only certain Curve25519 keys can be obfuscated with Elligator2, the +/// representative must be checked when generating the secret key. +/// +/// The probablility that a key does not have a representable elligator2 encoding +/// is ~50%, so we are (statistiscally) guaranteed to find a representable key +/// in relatively few iterations. +pub struct Representable; + +const RETRY_LIMIT: usize = 128; + +#[allow(unused)] +impl Representable { + /// Generate a new Elligator2 representable ['StaticSecret'] with the supplied RNG. + pub fn static_from_rng(mut rng: R) -> StaticSecret { + let mut private = StaticSecret::random_from_rng(&mut rng); + let mut repres: Option = (&private).into(); + + for _ in 0..RETRY_LIMIT { + if repres.is_some() { + return private; + } + private = StaticSecret::random_from_rng(&mut rng); + repres = (&private).into(); + } + + panic!("failed to generate representable secret, bad RNG provided"); + } + + /// Generate a new Elligator2 representable ['ReusableSecret'] with the supplied RNG. + pub fn reusable_from_rng(mut rng: R) -> ReusableSecret { + let mut private = ReusableSecret::random_from_rng(&mut rng); + let mut repres: Option = (&private).into(); + + for _ in 0..RETRY_LIMIT { + if repres.is_some() { + return private; + } + private = ReusableSecret::random_from_rng(&mut rng); + repres = (&private).into(); + } + + panic!("failed to generate representable secret, bad RNG provided"); + } + + /// Generate a new Elligator2 representable ['EphemeralSecret'] with the supplied RNG. + pub fn ephemeral_from_rng(mut rng: R) -> EphemeralSecret { + let mut private = EphemeralSecret::random_from_rng(&mut rng); + let mut repres: Option = (&private).into(); + + for _ in 0..RETRY_LIMIT { + if repres.is_some() { + return private; + } + private = EphemeralSecret::random_from_rng(&mut rng); + repres = (&private).into(); + } + + panic!("failed to generate representable secret, bad RNG provided"); + } + + /// Generate a new Elligator2 representable ['StaticSecret']. + pub fn random_static() -> StaticSecret { + let mut private = StaticSecret::random(); + let mut repres: Option = (&private).into(); + + for _ in 0..RETRY_LIMIT { + if repres.is_some() { + return private; + } + private = StaticSecret::random(); + repres = (&private).into(); + } + + panic!("failed to generate representable secret, getrandom failed"); + } + + /// Generate a new Elligator2 representable ['ReusableSecret']. + pub fn random_reusable() -> ReusableSecret { + let mut private = ReusableSecret::random(); + let mut repres: Option = (&private).into(); + + for _ in 0..RETRY_LIMIT { + if repres.is_some() { + return private; + } + private = ReusableSecret::random(); + repres = (&private).into(); + } + + panic!("failed to generate representable secret, getrandom failed"); + } + + /// Generate a new Elligator2 representable ['EphemeralSecret']. + pub fn random_ephemeral() -> EphemeralSecret { + let mut private = EphemeralSecret::random(); + let mut repres: Option = (&private).into(); + + for _ in 0..RETRY_LIMIT { + if repres.is_some() { + return private; + } + private = EphemeralSecret::random(); + repres = (&private).into(); + } + + panic!("failed to generate representable secret, getrandom failed"); + } +} + +#[cfg(test)] +mod test { + use super::*; + use hex::FromHex; + + #[test] + fn representative_match() { + let mut repres = <[u8; 32]>::from_hex( + "8781b04fefa49473ca5943ab23a14689dad56f8118d5869ad378c079fd2f4079", + ) + .unwrap(); + let incorrect = "1af2d7ac95b5dd1ab2b5926c9019fa86f211e77dd796f178f3fe66137b0d5d15"; + let expected = "a946c3dd16d99b8c38972584ca599da53e32e8b13c1e9a408ff22fdb985c2d79"; + + // we are not clearing the high order bits before translating the representative to a + // public key. + repres[31] &= 0x3f; + + 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())); + } +} 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/mod.rs b/crates/o5/src/common/mod.rs new file mode 100644 index 0000000..3ea0ed6 --- /dev/null +++ b/crates/o5/src/common/mod.rs @@ -0,0 +1,37 @@ +use crate::Result; + +use colored::Colorize; +use hmac::Hmac; +use sha2::Sha256; + +pub(crate) mod ct; +pub(crate) mod curve25519; +pub(crate) mod kdf; + +mod skip; +pub use skip::discard; + +pub mod drbg; +// pub mod ntor; +pub mod ntor_arti; +pub mod probdist; +pub mod replay_filter; + +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/mod.rs b/crates/o5/src/common/ntor_arti/mod.rs new file mode 100644 index 0000000..34ba8dc --- /dev/null +++ b/crates/o5/src/common/ntor_arti/mod.rs @@ -0,0 +1,180 @@ +//! 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::borrow::Borrow; + +use crate::Result; +//use zeroize::Zeroizing; +use tor_bytes::SecretBuf; + +/// A ClientHandshake is used to generate a client onionskin and +/// handle a relay onionskin. +pub(crate) trait ClientHandshake { + /// The type for the onion key. + type KeyType; + /// 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 sent from client (without forward secrecy). + type ClientAuxData: ?Sized; + /// 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>( + key: &Self::KeyType, + client_aux_data: &M, + ) -> 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. +pub(crate) trait ServerHandshake { + /// The type for the onion key. This is a private key type. + type KeyType; + /// 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, + key: &[Self::KeyType], + 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; +} + +/// 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(Clone, 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), + + /// 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..a2538ea --- /dev/null +++ b/crates/o5/src/common/skip.rs @@ -0,0 +1,64 @@ +use crate::Result; + +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; + +// use std::pin::Pin; +// use std::task::{Context, Poll}; +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/constants.rs b/crates/o5/src/constants.rs new file mode 100644 index 0000000..db8a870 --- /dev/null +++ b/crates/o5/src/constants.rs @@ -0,0 +1,79 @@ +#![allow(unused)] + +use tor_llcrypto::pk::rsa::RSA_ID_LEN; + +use crate::{ + common::{curve25519::REPRESENTATIVE_LENGTH, drbg}, + framing, + handshake::AUTHCODE_LENGTH, +}; + +use std::time::Duration; + +//=========================[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 IAT_ARG: &str = "iat-mode"; +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_IAT_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 SESSION_ID_LEN: usize = 8; + +pub const NODE_ID_LENGTH: usize = RSA_ID_LEN; +pub const NODE_PUBKEY_LENGTH: usize = 32; diff --git a/crates/o5/src/error.rs b/crates/o5/src/error.rs new file mode 100644 index 0000000..0f50b3d --- /dev/null +++ b/crates/o5/src/error.rs @@ -0,0 +1,245 @@ +use crate::framing::FrameError; + +use std::array::TryFromSliceError; +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, + + // 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::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, + } + } +} + +#[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..f37e9c5 --- /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}, + XSalsa20Poly1305, +}; +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 = XSalsa20Poly1305::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 = XSalsa20Poly1305::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..fdab8fd --- /dev/null +++ b/crates/o5/src/framing/handshake.rs @@ -0,0 +1,176 @@ +use crate::{ + common::{ + curve25519::{PublicKey, PublicRepresentative}, + HmacSha256, + }, + constants::*, + handshake::{get_epoch_hour, make_hs_pad, Authcode, AUTHCODE_LENGTH}, + Result, +}; + +use bytes::BufMut; +use hmac::Mac; +use ptrs::trace; +use rand::Rng; + +// -----------------------------[ Server ]----------------------------- + +pub struct ServerHandshakeMessage { + server_auth: [u8; AUTHCODE_LENGTH], + pad_len: usize, + repres: PublicRepresentative, + pubkey: Option, + epoch_hour: String, +} + +impl ServerHandshakeMessage { + pub fn new( + repres: PublicRepresentative, + server_auth: [u8; AUTHCODE_LENGTH], + epoch_hr: String, + ) -> Self { + Self { + server_auth, + pad_len: rand::thread_rng().gen_range(SERVER_MIN_PAD_LENGTH..SERVER_MAX_PAD_LENGTH), + repres, + pubkey: None, + epoch_hour: epoch_hr, + } + } + + pub fn server_pubkey(&mut self) -> PublicKey { + match self.pubkey { + Some(pk) => pk, + None => { + let pk = PublicKey::from(&self.repres); + self.pubkey = Some(pk); + pk + } + } + } + + 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"); + + h.reset(); + h.update(self.repres.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.repres.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 { + pub(crate) pad_len: usize, + pub(crate) repres: PublicRepresentative, + pub(crate) pubkey: Option, + + // only used when parsing (i.e. on the server side) + pub(crate) epoch_hour: String, +} + +impl ClientHandshakeMessage { + pub fn new(repres: PublicRepresentative, pad_len: usize, epoch_hour: String) -> Self { + Self { + pad_len, + repres, + pubkey: None, + + // only used when parsing (i.e. on the server side) + epoch_hour, + } + } + + pub fn get_public(&mut self) -> PublicKey { + trace!("repr: {}", hex::encode(self.repres)); + match self.pubkey { + Some(pk) => pk, + None => { + let pk = PublicKey::from(&self.repres); + self.pubkey = Some(pk); + pk + } + } + } + + #[allow(unused)] + /// Return the elligator2 representative of the public key value. + pub fn get_representative(&self) -> PublicRepresentative { + self.repres + } + + /// 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, mut h: HmacSha256) -> Result<()> { + trace!("serializing client handshake"); + + 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..729dd53 --- /dev/null +++ b/crates/o5/src/framing/messages_v1/mod.rs @@ -0,0 +1,283 @@ +//! Version 1 of the Protocol Messagess to be included in constructed frames. +//! +//! ## Compatability concerns: +//! +//! Server - when operating as a server we may want to support clients using v0 +//! as well as clients using v1. In order to accomplish this the server can +//! look for the presence of the [`ClientParams`] message. If it is included as +//! a part of the clients handshake we can affirmatively assign protocol message +//! set v1 to the clients session. If we complete the handshake without +//! receiving a [`ClientParams`] messsage then we default to v0 (if the server +//! enables support). +//! +//! Client - When operating as a client we want to support the option to connect +//! with either v0 or v1 servers. when running as a v1 client the server will +//! ignore the unknown frames including [`ClientParams`] and [`CryptoOffer`]. +//! This means that the `SevrerHandshake` will not include [`ServerParams`] or +//! [`CryptoAccept`] frames which indicates to a v1 client that it is speaking +//! with a server unwilling or incapable of speaking v1. This should allow +//! cross compatibility. + +mod crypto; +use crypto::CryptoExtension; + +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, + + ClientParams, + ServerParams, + CryptoOffer, + CryptoAccept, + + 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 CLIENT_PARAMS: u8 = 0x10; + const SERVER_PARAMS: u8 = 0x11; + const CRYPTO_OFFER: u8 = 0x12; + const CRYPTO_ACCEPT: u8 = 0x13; + //... + 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::ClientParams => MessageTypes::CLIENT_PARAMS, + MessageTypes::ServerParams => MessageTypes::SERVER_PARAMS, + MessageTypes::CryptoOffer => MessageTypes::CRYPTO_OFFER, + MessageTypes::CryptoAccept => MessageTypes::CRYPTO_ACCEPT, + 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, + CryptoOffer(CryptoExtension), + CryptoAccept(CryptoExtension), + + 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::ClientParams => MessageTypes::ClientParams, + Messages::ServerParams => MessageTypes::ServerParams, + Messages::CryptoOffer(_) => MessageTypes::CryptoOffer, + Messages::CryptoAccept(_) => MessageTypes::CryptoAccept, + 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::ClientParams => Ok(Messages::ClientParams), + + MessageTypes::ServerParams => Ok(Messages::ServerParams), + + MessageTypes::CryptoOffer => Ok(Messages::CryptoOffer(CryptoExtension::Kyber)), + + MessageTypes::CryptoAccept => Ok(Messages::CryptoAccept(CryptoExtension::Kyber)), + + 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..7cdccbc 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/XSalsa20) 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/XSalsa20) 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/README.md b/crates/o5/src/handshake/README.md new file mode 100644 index 0000000..7dda9c2 --- /dev/null +++ b/crates/o5/src/handshake/README.md @@ -0,0 +1,129 @@ + +# Obfs4 Ntor Handshake + +While exchanging messages during the handshake the client and +server use (a modified version of) the Ntor handshake to +compute a shared seed as well as an authentication value. + +The original implementation of the Obfs4 Ntor Handshake has some +small variation from the actual Ntor V1 handshake and as such +requires an alternative implementation to be compatible with +the existing [golang implementation](https://gitlab.com/yawning/obfs4) + + +## Difference from Ntor V1 + +* message value used for key seed is different: obfs4 uses a different order and +accidentally writes the server's identity public key bytes twice. + - Ntor V1 - uses `message = (secret_input) | ID | b | x | y | PROTOID` + - Obfs4 - uses `message = (secret_input) | b | b | x | y | PROTOID | ID` + +* seed for key generator + * Ntor V1 - uses raw bytes from `message` + * Obfs4 - uses `HMAC_SHA256(message, T_KEY)` where `T_KEY = "ntor-curve25519-sha256-1:key_extract"` + +* The constant string for `T_VERIFY` + * Ntor V1 - `T_VERIFY = b"ntor-curve25519-sha256-1:verify";` + * Obfs4 - `T_VERIFY = b"ntor-curve25519-sha256-1:key_verify";` + +* message value used for auth is different -- these hash over the same fields, +but result in different hash values. Obfs4 reuses part of the `message` value +so the duplicated server identity public key is included. + * Ntor V1 - uses input `verify | ID | b | y | x | PROTOID | "Server"` + * Obfs4 - uses input `verify | b | b | y | x | PROTOID | ID | "Server"` + +The rust implementation of the Obfs4 Ntor derivation with diff markup. + +```diff + +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; 31] = b"ntor-curve25519-sha256-1:verify"; ++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"; +pub(crate) const T_EXPAND: &[u8; 35] = b"ntor-curve25519-sha256-1:key_expand"; + + +/// 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: &O5NtorPublicKey, + x: &PublicKey, + y: &PublicKey, +) -> EncodeResult<(NtorHkdfKeyGenerator, Authcode)> { + let server_string = &b"Server"[..]; + + // shared_secret_input = EXP(X,y) | EXP(X,b) OR = EXP(Y,x) | EXP(B,x) +- // message = (shared_secret_input) | ID | X | Y | PROTOID +- let mut message = SecretBuf::new(); +- message.write(xy.as_bytes())?; // EXP(X,y) +- message.write(xb.as_bytes())?; // EXP(X,b) +- message.write(&server_pk.id)?; // ID +- message.write(&server_pk.pk.as_bytes())?; // b +- message.write(x.as_bytes())?; // x +- message.write(y.as_bytes())?; // y +- message.write(PROTO_ID)?; // PROTOID ++ // 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(PROTO_ID)?; // PROTOID ++ suffix.write(&server_pk.id)?; // ID ++ ++ // 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(message, 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 | ID | b | y | x | PROTOID | "Server" ++ // auth_input = verify | (suffix) | "Server" ++ // auth_input = verify | b | b | y | x | PROTOID | ID | "Server" + let mut auth_input = Vec::new(); + auth_input.write_and_consume(verify)?; // verify +- auth_input.write(&server_pk.id)?; // ID +- auth_input.write(&server_pk.pk.as_bytes())?; // B +- auth_input.write(y.as_bytes())?; // Y +- auth_input.write(x.as_bytes())?; // X +- auth_input.write(PROTO_ID)?; // PROTOID ++ 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 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)?; ++ ++ let keygen = NtorHkdfKeyGenerator::new(key_seed); +- let keygen = NtorHkdfKeyGenerator::new(message); + Ok((keygen, auth_mac)) +} + +``` diff --git a/crates/o5/src/handshake/handshake_client.rs b/crates/o5/src/handshake/handshake_client.rs new file mode 100644 index 0000000..316bef6 --- /dev/null +++ b/crates/o5/src/handshake/handshake_client.rs @@ -0,0 +1,335 @@ +use super::*; +use crate::{ + common::{ + curve25519::{PublicKey, PublicRepresentative, REPRESENTATIVE_LENGTH}, + HmacSha256, + }, + framing::handshake::{ClientHandshakeMessage, ServerHandshakeMessage}, +}; + +use ptrs::trace; +use rand::Rng; + +/// materials required to initiate a handshake from the client role. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HandshakeMaterials { + pub(crate) node_pubkey: O5NtorPublicKey, + pub(crate) pad_len: usize, + pub(crate) session_id: String, +} + +impl HandshakeMaterials { + pub(crate) fn new(node_pubkey: O5NtorPublicKey, session_id: String) -> Self { + HandshakeMaterials { + node_pubkey, + session_id, + pad_len: rand::thread_rng().gen_range(CLIENT_MIN_PAD_LENGTH..CLIENT_MAX_PAD_LENGTH), + } + } +} + + +/// 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. +#[derive(Clone)] +pub(crate) struct O5NtorHandshakeState { + /// 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: curve25519::StaticSecret, + + /// 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: curve25519::SharedSecret, // Bx + + /// The MAC of our original encrypted message. + msg_mac: MacVal, // msg_mac +} + +/// 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(super) fn client_handshake_o5( + materials: &HandshakeMaterials, +) -> Result<(O5NtorHandshakeState, Vec)> { + let rng = rand::thread_rng(); + let my_sk = Representable::static_from_rng(rng); + client_handshake_o5_no_keygen(my_sk, materials.clone()) +} +// fn client_handshake_o5( +// relay_public: &NtorV3PublicKey, +// client_msg: &[u8], +// verification: &[u8], +// ) -> EncodeResult<(NtorV3HandshakeState, Vec)> { +// let mut rng = rand::thread_rng(); +// let my_sk = curve25519::StaticSecret::random_from_rng(rng); +// client_handshake_ntor_v3_no_keygen(relay_public, client_msg, verification, my_sk) +// } + +/// As `client_handshake_ntor_v3`, but don't generate an ephemeral DH +/// key: instead take that key an arguments `my_sk`. +// fn client_handshake_ntor_o5_keygen( +// relay_public: &NtorV3PublicKey, +// client_msg: &[u8], +// verification: &[u8], +// my_sk: curve25519::StaticSecret, +// ) -> EncodeResult<(NtorV3HandshakeState, Vec)> { +pub(super) fn client_handshake_o5_no_keygen( + my_sk: curve25519::StaticSecret, + materials: HandshakeMaterials, +) -> Result<(O5NtorHandshakeState, Vec)> { + let my_public = curve25519::PublicKey::from(&my_sk); + let bx = my_sk.diffie_hellman(&materials.node_pubkey.pk); + + let (enc_key, mut mac) = kdf_msgkdf(&bx, materials.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(&relay_public.id)?; + message.write(&relay_public.pk)?; + message.write(&my_public)?; + message.write(&encrypted_msg)?; + message.write(&msg_mac)?; + + let state = O5NtorHandshakeState { + relay_public: relay_public.clone(), + my_sk, + my_public, + shared_secret: bx, + msg_mac, + }; + + Ok((state, message)) +} + +/// Helper: client handshake _without_ generating new keys. +pub(crate) fn client_handshake_obfs4_no_keygen( + ephem: StaticSecret, + materials: HandshakeMaterials, +) -> Result<(O5NtorHandshakeState, Vec)> { + let repres: Option = (&ephem).into(); + + // build client handshake message + let mut ch_msg = ClientHandshakeMessage::new( + repres.unwrap(), + materials.pad_len, + materials.session_id.clone(), + ); + + let mut buf = BytesMut::with_capacity(MAX_HANDSHAKE_LENGTH); + let mut key = materials.node_pubkey.pk.as_bytes().to_vec(); + key.append(&mut materials.node_pubkey.id.as_bytes().to_vec()); + let h = HmacSha256::new_from_slice(&key[..]).unwrap(); + ch_msg.marshall(&mut buf, h)?; + + let state = O5NtorHandshakeState { + my_sk: ephem, + materials, + epoch_hr: ch_msg.epoch_hour, + }; + + Ok((state, buf.to_vec())) +} + +/// Complete a client handshake, returning a key generator on success. +/// +/// 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_ntor_v3_part2( +// state: &NtorV3HandshakeState, +// relay_handshake: &[u8], +// verification: &[u8], +// ) -> Result<(Vec, NtorV3XofReader)> { +pub(super) fn client_handshake_o5_part2( + msg: T, + state: &O5NtorHandshakeState, +) -> Result<(O5NtorKeyGenerator, Vec)> +where + T: AsRef<[u8]>, +{ + let mut reader = Reader::from_slice(relay_handshake); + let y_pk: curve25519::PublicKey = 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(); + + // 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)) + .and_then(|_| si.write(&state.relay_public.id)) + .and_then(|_| si.write(&state.relay_public.pk)) + .and_then(|_| si.write(&state.my_public)) + .and_then(|_| si.write(&y_pk)) + .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.relay_public.id)) + .and_then(|_| auth.write(&state.relay_public.pk)) + .and_then(|_| auth.write(&y_pk)) + .and_then(|_| auth.write(&state.my_public)) + .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(keystream))) + } else { + Err(Error::BadCircHandshakeAuth) + } +} + +#[cfg(test)] +pub(crate) fn client_handshake2_no_auth_check_obfs4( + msg: T, + state: &O5NtorHandshakeState, +) -> Result<(O5NtorKeyGenerator, Authcode)> +where + T: AsRef<[u8]>, +{ + // try to parse the message as an incoming server handshake. + let (mut shs_msg, _) = try_parse(&msg, state)?; + + let their_pk = shs_msg.server_pubkey(); + // let auth: Authcode = shs_msg.server_auth(); + + let node_pubkey = &state.materials.node_pubkey; + let my_public: PublicKey = (&state.my_sk).into(); + + let xy = state.my_sk.diffie_hellman(&their_pk); + let xb = state.my_sk.diffie_hellman(&node_pubkey.pk); + + let (key_seed, authcode) = ntor_derive(&xy, &xb, node_pubkey, &my_public, &their_pk) + .map_err(into_internal!("Error deriving keys"))?; + + let keygen = O5NtorKeyGenerator::new(key_seed, true); + + Ok((keygen, authcode)) +} + +fn try_parse( + buf: impl AsRef<[u8]>, + state: &O5NtorHandshakeState, +) -> Result<(ServerHandshakeMessage, usize)> { + let buf = buf.as_ref(); + trace!( + "{} parsing server handshake {}", + state.materials.session_id, + buf.len() + ); + + if buf.len() < SERVER_MIN_HANDSHAKE_LENGTH { + Err(RelayHandshakeError::EAgain)? + } + + // derive the server mark + let mut key = state.materials.node_pubkey.pk.as_bytes().to_vec(); + key.append(&mut state.materials.node_pubkey.id.as_bytes().to_vec()); + let mut h = HmacSha256::new_from_slice(&key[..]).unwrap(); + h.reset(); // disambiguate reset() implementations Mac v digest + + let mut r_bytes: [u8; 32] = buf[0..REPRESENTATIVE_LENGTH].try_into().unwrap(); + h.update(&r_bytes); + + // clear the inconsistent elligator2 bits of the representative after + // using the wire format for deriving the mark + r_bytes[31] &= 0x3f; + let server_repres = PublicRepresentative::from(r_bytes); + let server_auth: [u8; AUTHCODE_LENGTH] = + buf[REPRESENTATIVE_LENGTH..REPRESENTATIVE_LENGTH + AUTHCODE_LENGTH].try_into()?; + + let server_mark = h.finalize_reset().into_bytes()[..MARK_LENGTH].try_into()?; + + //attempt to find the mark + MAC + let start_pos = REPRESENTATIVE_LENGTH + AUTHCODE_LENGTH + SERVER_MIN_PAD_LENGTH; + let pos = match find_mac_mark(server_mark, buf, start_pos, MAX_HANDSHAKE_LENGTH, false) { + Some(p) => p, + None => { + if buf.len() > MAX_HANDSHAKE_LENGTH { + Err(RelayHandshakeError::BadServerHandshake)? + } + Err(RelayHandshakeError::EAgain)? + } + }; + + // validate the MAC + h.reset(); // disambiguate `reset()` implementations Mac v digest + h.update(&buf[..pos + MARK_LENGTH]); + h.update(state.epoch_hr.as_bytes()); + let mac_calculated = &h.finalize_reset().into_bytes()[..MAC_LENGTH]; + let mac_received = &buf[pos + MARK_LENGTH..pos + MARK_LENGTH + MAC_LENGTH]; + trace!( + "client mac check {}-{}", + hex::encode(mac_calculated), + hex::encode(mac_received) + ); + if mac_calculated.ct_eq(mac_received).into() { + let mut r_bytes = server_repres.to_bytes(); + r_bytes[31] &= 0x3f; + return Ok(( + ServerHandshakeMessage::new(server_repres, server_auth, state.epoch_hr.clone()), + pos + MARK_LENGTH + MAC_LENGTH, + )); + } + + // received the incorrect mac + Err(RelayHandshakeError::BadServerHandshake.into()) +} diff --git a/crates/o5/src/handshake/handshake_server.rs b/crates/o5/src/handshake/handshake_server.rs new file mode 100644 index 0000000..3cdfb06 --- /dev/null +++ b/crates/o5/src/handshake/handshake_server.rs @@ -0,0 +1,284 @@ +use super::*; +use crate::{ + common::{ + curve25519::{PublicRepresentative, REPRESENTATIVE_LENGTH}, + HmacSha256, + }, + framing::{build_and_marshall, ClientHandshakeMessage, MessageTypes, ServerHandshakeMessage}, +}; + +use ptrs::{debug, trace}; +use rand::thread_rng; +use tokio_util::codec::Encoder; + +use std::time::Instant; + +#[derive(Clone)] +pub(crate) struct HandshakeMaterials { + pub(crate) identity_keys: O5NtorSecretKey, + pub(crate) session_id: String, + pub(crate) len_seed: [u8; SEED_LENGTH], +} + +impl<'a> HandshakeMaterials { + pub fn get_hmac(&self) -> HmacSha256 { + let mut key = self.identity_keys.pk.pk.as_bytes().to_vec(); + key.append(&mut self.identity_keys.pk.id.as_bytes().to_vec()); + HmacSha256::new_from_slice(&key[..]).unwrap() + } + + pub fn new<'b>( + identity_keys: &'b O5NtorSecretKey, + session_id: String, + len_seed: [u8; SEED_LENGTH], + ) -> Self + where + 'b: 'a, + { + HandshakeMaterials { + identity_keys: identity_keys.clone(), + session_id, + len_seed, + } + } +} + +impl Server { + /// Perform a server-side ntor handshake. + /// + /// On success returns a key generator and a server onionskin. + pub(super) fn server_handshake_obfs4( + &self, + msg: T, + materials: HandshakeMaterials, + ) -> RelayHandshakeResult<(NtorHkdfKeyGenerator, Vec)> + where + T: AsRef<[u8]>, + { + let rng = thread_rng(); + let session_sk = Representable::ephemeral_from_rng(rng); + + self.server_handshake_obfs4_no_keygen(session_sk, msg, materials) + } + + /// Helper: perform a server handshake without generating any new keys. + pub(crate) fn server_handshake_obfs4_no_keygen( + &self, + session_sk: EphemeralSecret, + msg: T, + mut materials: HandshakeMaterials, + ) -> RelayHandshakeResult<(NtorHkdfKeyGenerator, Vec)> + where + T: AsRef<[u8]>, + { + if CLIENT_MIN_HANDSHAKE_LENGTH > msg.as_ref().len() { + Err(RelayHandshakeError::EAgain)?; + } + + let mut client_hs = match self.try_parse_client_handshake(msg, &mut materials) { + Ok(chs) => chs, + Err(Error::HandshakeErr(RelayHandshakeError::EAgain)) => { + return Err(RelayHandshakeError::EAgain); + } + Err(_e) => { + debug!( + "{} failed to parse client handshake: {_e}", + materials.session_id + ); + return Err(RelayHandshakeError::BadClientHandshake); + } + }; + + debug!( + "{} successfully parsed client handshake", + materials.session_id + ); + let their_pk = client_hs.get_public(); + let ephem_pub = PublicKey::from(&session_sk); + let session_repres: Option = (&session_sk).into(); + + let xy = session_sk.diffie_hellman(&their_pk); + let xb = materials.identity_keys.sk.diffie_hellman(&their_pk); + + // Ensure that none of the keys are broken (i.e. equal to zero). + let okay = + ct::bool_to_choice(xy.was_contributory()) & ct::bool_to_choice(xb.was_contributory()); + trace!("x {} y {}", hex::encode(their_pk), hex::encode(ephem_pub)); + + let (key_seed, authcode) = + ntor_derive(&xy, &xb, &materials.identity_keys.pk, &their_pk, &ephem_pub) + .map_err(into_internal!("Error deriving keys"))?; + trace!( + "seed: {} auth: {}", + hex::encode(key_seed.as_slice()), + hex::encode(authcode) + ); + + let mut keygen = NtorHkdfKeyGenerator::new(key_seed, false); + + let reply = self.complete_server_hs( + &client_hs, + materials, + session_repres.unwrap(), + &mut keygen, + authcode, + )?; + + if okay.into() { + Ok((keygen, reply)) + } else { + Err(RelayHandshakeError::BadClientHandshake) + } + } + + pub(crate) fn complete_server_hs( + &self, + client_hs: &ClientHandshakeMessage, + materials: HandshakeMaterials, + session_repres: PublicRepresentative, + keygen: &mut NtorHkdfKeyGenerator, + authcode: Authcode, + ) -> RelayHandshakeResult> { + let epoch_hr = client_hs.get_epoch_hr(); + + // Since the current and only implementation always sends a PRNG seed for + // the length obfuscation, this makes the amount of data received from the + // server inconsistent with the length sent from the client. + // + // Re-balance this by tweaking the client minimum padding/server maximum + // padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed + // as part of the server response). See inlineSeedFrameLength in + // handshake_ntor.go. + + // Generate/send the response. + let mut sh_msg = ServerHandshakeMessage::new(session_repres, authcode, epoch_hr); + + let h = materials.get_hmac(); + let mut buf = BytesMut::with_capacity(MAX_HANDSHAKE_LENGTH); + sh_msg + .marshall(&mut buf, h) + .map_err(|e| RelayHandshakeError::FrameError(format!("{e}")))?; + trace!("adding encoded prng seed"); + + // Send the PRNG seed as part of the first packet. + let mut prng_pkt_buf = BytesMut::new(); + build_and_marshall( + &mut prng_pkt_buf, + MessageTypes::PrngSeed.into(), + materials.len_seed, + 0, + ) + .map_err(|e| RelayHandshakeError::FrameError(format!("{e}")))?; + + let codec = &mut keygen.codec; + codec + .encode(prng_pkt_buf.clone(), &mut buf) + .map_err(|e| RelayHandshakeError::FrameError(format!("{e}")))?; + + debug!( + "{} writing server handshake {}B ...{}", + materials.session_id, + buf.len(), + hex::encode(&buf[buf.len() - 10..]), + ); + + Ok(buf.to_vec()) + } + + fn try_parse_client_handshake( + &self, + buf: impl AsRef<[u8]>, + materials: &mut HandshakeMaterials, + ) -> Result { + let buf = buf.as_ref(); + let mut h = materials.get_hmac(); + + if CLIENT_MIN_HANDSHAKE_LENGTH > buf.len() { + Err(Error::HandshakeErr(RelayHandshakeError::EAgain))?; + } + + let mut r_bytes: [u8; 32] = buf[0..REPRESENTATIVE_LENGTH].try_into().unwrap(); + + // derive the mark based on the literal bytes on the wire + h.update(&r_bytes[..]); + + // clear the bits that are unreliable (and randomized) for elligator2 + r_bytes[31] &= 0x3f; + let repres = PublicRepresentative::from(&r_bytes); + + let m = h.finalize_reset().into_bytes(); + let mark: [u8; MARK_LENGTH] = m[..MARK_LENGTH].try_into()?; + + trace!("{} mark?:{}", materials.session_id, hex::encode(mark)); + + // find mark + mac position + let pos = match find_mac_mark( + mark, + buf, + REPRESENTATIVE_LENGTH + CLIENT_MIN_PAD_LENGTH, + MAX_HANDSHAKE_LENGTH, + true, + ) { + Some(p) => p, + None => { + trace!("{} didn't find mark", materials.session_id); + if buf.len() > MAX_HANDSHAKE_LENGTH { + Err(Error::HandshakeErr(RelayHandshakeError::BadClientHandshake))? + } + Err(Error::HandshakeErr(RelayHandshakeError::EAgain))? + } + }; + + // validate he MAC + let mut mac_found = false; + let mut epoch_hr = String::new(); + for offset in [0_i64, -1, 1] { + // Allow the epoch to be off by up to one hour in either direction + trace!("server trying offset: {offset}"); + let eh = format!("{}", offset + get_epoch_hour() as i64); + + h.reset(); + h.update(&buf[..pos + MARK_LENGTH]); + h.update(eh.as_bytes()); + let mac_calculated = &h.finalize_reset().into_bytes()[..MAC_LENGTH]; + let mac_received = &buf[pos + MARK_LENGTH..pos + MARK_LENGTH + MAC_LENGTH]; + trace!( + "server {}-{}", + hex::encode(mac_calculated), + hex::encode(mac_received) + ); + if mac_calculated.ct_eq(mac_received).into() { + trace!("correct mac"); + // Ensure that this handshake has not been seen previously. + if self + .replay_filter + .test_and_set(Instant::now(), mac_received) + { + // The client either happened to generate exactly the same + // session key and padding, or someone is replaying a previous + // handshake. In either case, fuck them. + Err(Error::HandshakeErr(RelayHandshakeError::ReplayedHandshake))? + } + + epoch_hr = eh; + mac_found = true; + // we could break here, but in the name of reducing timing + // variance, we just evaluate all three MACs. + } + } + if !mac_found { + // This could be a [`RelayHandshakeError::TagMismatch`] :shrug: + Err(Error::HandshakeErr(RelayHandshakeError::BadClientHandshake))? + } + + // client should never send any appended padding at the end. + if buf.len() != pos + MARK_LENGTH + MAC_LENGTH { + Err(Error::HandshakeErr(RelayHandshakeError::BadClientHandshake))? + } + + Ok(ClientHandshakeMessage::new( + repres, 0, // pad_len doesn't matter when we are reading client handshake msg + epoch_hr, + )) + } +} 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/kyber.rs b/crates/o5/src/handshake/kyber.rs new file mode 100644 index 0000000..080dfbd --- /dev/null +++ b/crates/o5/src/handshake/kyber.rs @@ -0,0 +1,355 @@ +//! ## KyberX25519 Ntor Handshake +//! +//! ### As Described in draft-tls-westerbaan-xyber768d00-03 +//! +//! ``` +//! 3. Construction +//! +//! We instantiate draft-ietf-tls-hybrid-design-06 with X25519 [rfc7748] +//! and Kyber768Draft00 [kyber]. The latter is Kyber as submitted to +//! round 3 of the NIST PQC process [KyberV302]. +//! +//! 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. +//! +//! 4. Security Considerations +//! +//! For TLS 1.3, this concatenation approach provides a secure key +//! exchange if either component key exchange methods (X25519 or +//! Kyber768Draft00) are secure [hybrid]. +//! ``` + +use crate::{ + common::ntor::{ + derive_ntor_shared, Auth, HandshakeResult, IdentityKeyPair, KeySeed, NtorError, PublicKey, + SessionKeyPair, ID, + }, + Error, Result, +}; + +use bytes::BytesMut; +use pqc_kyber::*; +use subtle::{Choice, ConstantTimeEq, CtOption}; + +use super::{AUTH_LENGTH, KEY_SEED_LENGTH}; + +const _ZERO_EXP: [u8; 32] = [0_u8; 32]; +const X25519_PUBKEY_LEN: usize = 32; +pub const KYBERX_PUBKEY_LEN: usize = KYBER_PUBLICKEYBYTES + X25519_PUBKEY_LEN; + +pub struct KyberXPublicKey { + pub kyber: pqc_kyber::PublicKey, + pub x25519: PublicKey, + contiguous: [u8; KYBERX_PUBKEY_LEN], +} + +impl KyberXPublicKey { + pub fn from_parts(x25519: PublicKey, kyber: pqc_kyber::PublicKey) -> Self { + let mut contiguous = [0_u8; KYBERX_PUBKEY_LEN]; + contiguous[..X25519_PUBKEY_LEN].copy_from_slice(&x25519.to_bytes()); + contiguous[X25519_PUBKEY_LEN..].copy_from_slice(&kyber); + + KyberXPublicKey { + kyber, + x25519, + contiguous, + } + } + + pub fn from_bytes(bytes: impl AsRef<[u8]>) -> std::result::Result { + let value = bytes.as_ref(); + if value.len() != KYBERX_PUBKEY_LEN { + return Err(NtorError::ParseError(String::from( + "failed to parse kyberx25519 public key, incorrect length", + ))); + } + + let mut x25519 = [0_u8; X25519_PUBKEY_LEN]; + x25519[..].copy_from_slice(&value[..X25519_PUBKEY_LEN]); + + let mut kyber = [0_u8; KYBER_PUBLICKEYBYTES]; + kyber[..].copy_from_slice(&value[X25519_PUBKEY_LEN..]); + + let mut contiguous = [0_u8; KYBERX_PUBKEY_LEN]; + contiguous[..].copy_from_slice(&value); + + Ok(KyberXPublicKey { + x25519: PublicKey::from(x25519), + kyber, + contiguous, + }) + } +} + +impl From<&KyberXSessionKeys> for KyberXPublicKey { + fn from(value: &KyberXSessionKeys) -> Self { + value.get_public() + } +} + +impl From<&KyberXIdentityKeys> for KyberXPublicKey { + fn from(value: &KyberXIdentityKeys) -> Self { + value.get_public() + } +} + +impl AsRef<[u8]> for KyberXPublicKey { + fn as_ref(&self) -> &[u8] { + &self.contiguous + } +} + +pub struct KyberXSessionKeys { + pub kyber: pqc_kyber::Keypair, + pub x25519: SessionKeyPair, +} + +impl KyberXSessionKeys { + fn new() -> Self { + let mut rng = rand::thread_rng(); + + KyberXSessionKeys { + x25519: SessionKeyPair::new(true), + kyber: pqc_kyber::keypair(&mut rng).expect("kyber key generation failed"), + } + } + + pub fn from_random(rng: &mut R) -> Self { + KyberXSessionKeys { + x25519: SessionKeyPair::new(true), + kyber: pqc_kyber::keypair(rng).expect("kyber key generation failed"), + } + } + + /// Allow downgrade of key pair to x25519 only. + pub fn to_x25519(self) -> SessionKeyPair { + self.x25519 + } + + pub fn get_public(&self) -> KyberXPublicKey { + let mut contiguous = [0_u8; KYBERX_PUBKEY_LEN]; + contiguous[..X25519_PUBKEY_LEN].copy_from_slice(&self.x25519.public.to_bytes()); + contiguous[X25519_PUBKEY_LEN..].copy_from_slice(&self.kyber.public); + + KyberXPublicKey { + kyber: self.kyber.public, + x25519: self.x25519.public, + contiguous, + } + } +} + +pub struct KyberXIdentityKeys { + pub kyber: pqc_kyber::Keypair, + pub x25519: IdentityKeyPair, +} + +impl KyberXIdentityKeys { + fn new() -> Self { + let mut rng = rand::thread_rng(); + + KyberXIdentityKeys { + x25519: IdentityKeyPair::new(), + kyber: pqc_kyber::keypair(&mut rng).expect("kyber key generation failed"), + } + } + + fn from_random(rng: &mut R) -> Self { + KyberXIdentityKeys { + x25519: IdentityKeyPair::new(), + kyber: pqc_kyber::keypair(rng).expect("kyber key generation failed"), + } + } + + /// Allow downgrade of key pair to x25519 only. + pub fn to_x25519(self) -> IdentityKeyPair { + self.x25519 + } + + pub fn get_public(&self) -> KyberXPublicKey { + let mut contiguous = [0_u8; KYBERX_PUBKEY_LEN]; + contiguous[..X25519_PUBKEY_LEN].copy_from_slice(&self.x25519.public.to_bytes()); + contiguous[X25519_PUBKEY_LEN..].copy_from_slice(&self.kyber.public); + + KyberXPublicKey { + kyber: self.kyber.public, + x25519: self.x25519.public, + contiguous, + } + } +} + +/// The client side uses the ntor derived shared secret based on the secret +/// input created by appending the shared secret derived between the client's +/// session keys and the server's sessions keys with the shared secret derived +/// between the clients session keys and the server's identity keys. +/// +/// secret input = X25519(Y, x) | Kyber(Y, x) | X25519(B, x) | Kyber(B, x) +pub fn client_handshake( + client_keys: &KyberXSessionKeys, + server_public: &KyberXPublicKey, + id_public: &KyberXPublicKey, + id: &ID, +) -> subtle::CtOption { + let mut not_ok = 0; + let mut secret_input: Vec = vec![]; + + // EXP(Y,x) + let exp = client_keys + .x25519 + .private + .diffie_hellman(&server_public.x25519); + + not_ok |= _ZERO_EXP[..].ct_eq(exp.as_bytes()).unwrap_u8(); + secret_input.append(&mut exp.as_bytes().to_vec()); + + // EXP(B,x) + let exp = client_keys.x25519.private.diffie_hellman(&id_public.x25519); + not_ok |= _ZERO_EXP[..].ct_eq(exp.as_bytes()).unwrap_u8(); + secret_input.append(&mut exp.as_bytes().to_vec()); + + let (key_seed, auth) = derive_ntor_shared( + secret_input, + id, + id_public, + &client_keys.get_public(), + server_public, + ); + + // failed if not_ok != 0 + // if not_ok != 0 then scalar operations failed + subtle::CtOption::new(HandshakeResult { key_seed, auth }, not_ok.ct_eq(&0_u8)) +} + +/// The server side uses the ntor derived shared secret based on the secret +/// input created by appending the shared secret derived between the server's +/// session keys and the client's sessions keys with the shared secret derived +/// between the server's identity keys and the clients session keys. +/// +/// secret input = X25519(X, y) | Kyber(X, y) | X25519(X, b) | Kyber(X, b) +pub fn server_handshake( + server_keys: &KyberXSessionKeys, + client_public: &KyberXPublicKey, + id_keys: &KyberXIdentityKeys, + id: &ID, +) -> subtle::CtOption { + let mut not_ok = 0; + let mut secret_input: Vec = vec![]; + + // EXP(X,y) + let exp = server_keys + .x25519 + .private + .diffie_hellman(&client_public.x25519); + not_ok |= _ZERO_EXP[..].ct_eq(exp.as_bytes()).unwrap_u8(); + secret_input.append(&mut exp.as_bytes().to_vec()); + + // EXP(X,b) + let exp = id_keys.x25519.private.diffie_hellman(&client_public.x25519); + not_ok |= _ZERO_EXP[..].ct_eq(exp.as_bytes()).unwrap_u8(); + secret_input.append(&mut exp.as_bytes().to_vec()); + + let (key_seed, auth) = derive_ntor_shared( + secret_input, + id, + &id_keys.get_public(), + client_public, + &server_keys.get_public(), + ); + + // failed if not_ok != 0 + // if not_ok != 0 then scalar operations failed + subtle::CtOption::new(HandshakeResult { key_seed, auth }, not_ok.ct_eq(&0_u8)) +} + +#[cfg(test)] +#[allow(unused)] +mod tests { + use crate::common::ntor::compare_auth; + + use super::*; + use x25519_dalek::EphemeralSecret; + + #[test] + fn kyberx25519_handshake_flow() { + // long-term server id and keys + let server_id_keys = KyberXIdentityKeys::new(); + let server_id_pub = server_id_keys.get_public(); + let server_id = ID::new(); + + // client open session, generating the associated ephemeral keys + let client_session = KyberXSessionKeys::new(); + + // client sends kyber25519 session pubkey(s) + let cpk = client_session.get_public(); + + // server computes kyberx25519 combined shared secret + let server_session = KyberXSessionKeys::new(); + let server_hs_res = server_handshake(&server_session, &cpk, &server_id_keys, &server_id); + + // server sends kyberx25519 session pubkey(s) + let spk = client_session.get_public(); + + // client computes kyberx25519 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); + } + + #[test] + fn kyber_handshake_supplement_flow() { + // long-term server id and keys + let server_id_keys = KyberXIdentityKeys::new(); + let server_id_pub = server_id_keys.get_public(); + let server_id = ID::new(); + + // client open session, generating the associated ephemeral keys + let client_session = KyberXSessionKeys::new(); + + // client sends ed25519 session pubkey elligator2 encoded and includes + // session Kyber1024Supplement CryptoOffer. + let c_ed_pk = client_session.x25519.public; + let c_ky_pk = client_session.kyber.public; + let cpk = KyberXPublicKey::from_parts(c_ed_pk, c_ky_pk); + + // server computes KyberX25519 combined shared secret + let server_session = KyberXSessionKeys::new(); + let server_hs_res = server_handshake(&server_session, &cpk, &server_id_keys, &server_id); + + // server sends ed25519 session pubkey elligator2 encoded and includes + // session Kyber1024Supplement CryptoAccept. + let s_ed_pk = client_session.x25519.public; + let s_ky_pk = client_session.kyber.public; + let spk = KyberXPublicKey::from_parts(c_ed_pk, c_ky_pk); + + // client computes KyberX25519 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/mod.rs b/crates/o5/src/handshake/mod.rs index 316b7b4..3eddb59 100644 --- a/crates/o5/src/handshake/mod.rs +++ b/crates/o5/src/handshake/mod.rs @@ -5,35 +5,57 @@ //! 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. +use crate::{ + common::{ + ct, + curve25519::{Representable, StaticSecret}, + ntor_arti::{ + AuxDataReply, ClientHandshake, KeyGenerator, RelayHandshakeError, RelayHandshakeResult, + ServerHandshake, + }, + }, + constants::*, + framing::{O5Codec, KEY_MATERIAL_LENGTH}, + Error, Result, Server, +}; -// 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::d::{Sha3_256, Shake256, Shake256Reader}; +use tor_llcrypto::pk::{curve25519, ed25519::Ed25519Identity}; use tor_llcrypto::util::ct::ct_lookup; use cipher::{KeyIvInit, StreamCipher}; +#[cfg(test)] use rand_core::{CryptoRng, RngCore}; + use subtle::{Choice, ConstantTimeEq}; use tor_cell::relaycell::extend::NtorV3Extension; use tor_llcrypto::cipher::aes::Aes256Ctr; use zeroize::Zeroizing; +use std::borrow::Borrow; + +mod handshake_client; +// mod handshake_server; +mod utils; + +pub(crate) mod kyber; + +pub(crate) use utils::*; + +pub(crate) use handshake_client::HandshakeMaterials as CHSMaterials; +#[cfg(test)] +pub(crate) use handshake_client::{ + client_handshake2_no_auth_check_o5, client_handshake_o5_no_keygen, +}; + +use handshake_client::{client_handshake2_o5, client_handshake_o5, O5NtorHandshakeState}; +pub(crate) use handshake_server::HandshakeMaterials as SHSMaterials; + /// The verification string to be used for circuit extension. -const OBFS4_CIRC_VERIFICATION: &[u8] = b"circuit extend"; +const NTOR3_CIRC_VERIFICATION: &[u8] = b"circuit extend"; /// The size of an encryption key in bytes. const ENC_KEY_LEN: usize = 32; @@ -48,21 +70,25 @@ const MAC_LEN: usize = 32; /// The length of a node identity in bytes. const ID_LEN: usize = 32; +/// 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; + /// 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. +// TODO: 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); +/// Opaque wrapper type for NtorV3's hash reader. +struct NtorV3XofReader(Shake256Reader); -impl digest::XofReader for Obfs4NtorXofReader { +impl digest::XofReader for NtorV3XofReader { fn read(&mut self, buffer: &mut [u8]) { self.0.read(buffer); } @@ -88,7 +114,6 @@ impl<'a> Encap<'a> { } /// Return the underlying data fn data(&self) -> &'a [u8] { - self.0 } } @@ -110,16 +135,6 @@ macro_rules! define_tweaks { } 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"; @@ -145,7 +160,7 @@ define_tweaks! { /// Compute a tweaked hash. fn hash(t: &Encap<'_>, data: &[u8]) -> DigestVal { use digest::Digest; - let mut d = Sha256::new(); + let mut d = Sha3_256::new(); d.update((t.len() as u64).to_be_bytes()); d.update(t.data()); d.update(data); @@ -200,20 +215,20 @@ fn h_verify(d: &[u8]) -> DigestVal { /// the client's public key (B), and the shared verification string. fn kdf_msgkdf( xb: &curve25519::SharedSecret, - relay_public: &Obfs4NtorPublicKey, + relay_public: &O5NtorPublicKey, client_public: &curve25519::PublicKey, verification: &[u8], -) -> EncodeResult<(EncKey, DigestWriter)> { +) -> 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(xb)?; + msg_kdf.write(&materials.node_pubkey.id)?; + msg_kdf.write(client_public)?; + msg_kdf.write(&materials.node_pubkey.pk)?; msg_kdf.write(PROTOID)?; msg_kdf.write(&Encap(verification))?; let mut r = msg_kdf.take().finalize_xof(); @@ -222,25 +237,25 @@ fn kdf_msgkdf( r.read(&mut enc_key[..]); r.read(&mut mac_key[..]); - let mut mac = DigestWriter(Sha256::default()); + 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())?; + mac.write(&materials.node_pubkey.id)?; + mac.write(&materials.node_pubkey.pk)?; + mac.write(client_public)?; } Ok((enc_key, mac)) } /// Client side of the ntor v3 handshake. -pub(crate) struct Obfs4NtorClient; +pub(crate) struct O5Client; -impl crate::common::ntor_arti::ClientHandshake for Obfs4NtorClient { - type KeyType = Obfs4NtorPublicKey; - type StateType = Obfs4NtorHandshakeState; - type KeyGen = Obfs4NtorKeyGenerator; +impl ClientHandshake for O5Client { + type KeyType = O5NtorPublicKey; + type StateType = O5NtorHandshakeState; + type KeyGen = O5NtorKeyGenerator; type ClientAuxData = [NtorV3Extension]; type ServerAuxData = Vec; @@ -249,17 +264,17 @@ impl crate::common::ntor_arti::ClientHandshake for Obfs4NtorClient { /// /// 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, + fn client1>( + key: &Self::KeyType, + client_aux_data: &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"))?; + // // TODO: Add extensions back in + // 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."))?, + client_handshake_o5(key, &message, NTOR3_CIRC_VERIFICATION) + .map_err(into_internal!("Can't encode ntor3 client handshake."))?, ) } @@ -270,30 +285,30 @@ impl crate::common::ntor_arti::ClientHandshake for Obfs4NtorClient { fn client2>( state: Self::StateType, msg: T, - ) -> Result<(Vec, Self::KeyGen)> { + ) -> Result<(Self::ServerAuxData, Self::KeyGen)> { let (message, xofreader) = - client_handshake_obfs4_part2(&state, msg.as_ref(), OBFS4_CIRC_VERIFICATION)?; + client_handshake_o5_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 = Obfs4NtorKeyGenerator { reader: xofreader }; + let keygen = O5NtorKeyGenerator { reader: xofreader }; Ok((extensions, keygen)) } } /// Server side of the ntor v3 handshake. -pub(crate) struct Obfs4NtorServer; +pub(crate) struct O5Server; -impl crate::common::ntor_arti::ServerHandshake for Obfs4NtorServer { - type KeyType = Obfs4NtorSecretKey; - type KeyGen = Obfs4NtorKeyGenerator; +impl ServerHandshake for O5Server { + type KeyType = O5NtorSecretKey; + type KeyGen = O5NtorKeyGenerator; type ClientAuxData = [NtorV3Extension]; type ServerAuxData = Vec; - fn server, T: AsRef<[u8]>>( - rng: &mut R, + fn server, T: AsRef<[u8]>>( + &self, reply_fn: &mut REPLY, key: &[Self::KeyType], msg: T, @@ -306,14 +321,13 @@ impl crate::common::ntor_arti::ServerHandshake for Obfs4NtorServer { Some(out) }; - let (res, reader) = server_handshake_obfs4( - rng, + let (res, reader) = server_handshake_o5( &mut bytes_reply_fn, msg.as_ref(), key, - OBFS4_CIRC_VERIFICATION, + NTOR3_CIRC_VERIFICATION, )?; - Ok((Obfs4NtorKeyGenerator { reader }, res)) + Ok((O5NtorKeyGenerator { reader }, res)) } } @@ -322,67 +336,47 @@ impl crate::common::ntor_arti::ServerHandshake for Obfs4NtorServer { /// Contains a single curve25519 ntor onion key, and the relay's ed25519 /// identity. #[derive(Clone, Debug)] -pub(crate) struct Obfs4NtorPublicKey { +pub(crate) struct O5NtorPublicKey { /// The relay's identity. pub(crate) id: Ed25519Identity, - /// The Bridge's identity key. + /// The relay's onion 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 { +pub(crate) struct O5NtorSecretKey { /// The relay's public key information - pk: Obfs4NtorPublicKey, + pk: O5NtorPublicKey, /// The secret onion key. sk: curve25519::StaticSecret, } -impl Obfs4NtorSecretKey { - /// Construct a new Obfs4NtorSecretKey from its components. +impl O5NtorSecretKey { + /// Construct a new O5NtorSecretKey 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 }, + pk: O5NtorPublicKey { id, pk }, sk, } } /// Generate a key using the given `rng`, suitable for testing. #[cfg(test)] - pub(crate) fn generate_for_test(mut rng: R) -> Self { + pub(crate) fn generate_for_test(rng: &mut 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; + let sk = curve25519::StaticSecret::random_from_rng(rng); - 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, + let pk = O5NtorPublicKey { + pk: (&sk).into(), id: id.into(), - rp, }; Self { pk, sk } } @@ -396,31 +390,14 @@ impl Obfs4NtorSecretKey { } } -/// 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 { +pub(crate) struct O5NtorKeyGenerator { /// The underlying `digest::XofReader`. - reader: Obfs4NtorXofReader, + reader: NtorV3XofReader, } -impl KeyGenerator for Obfs4NtorKeyGenerator { + +impl KeyGenerator for O5NtorKeyGenerator { fn expand(mut self, keylen: usize) -> Result { use digest::XofReader; let mut ret: SecretBuf = vec![0; keylen].into(); @@ -429,60 +406,6 @@ impl KeyGenerator for Obfs4NtorKeyGenerator { } } -/// 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. @@ -515,33 +438,30 @@ where /// /// 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, +fn server_handshake_o5( reply_fn: &mut REPLY, message: &[u8], - keys: &[Obfs4NtorSecretKey], + keys: &[O5NtorSecretKey], verification: &[u8], -) -> RelayHandshakeResult<(Vec, Obfs4NtorXofReader)> { +) -> RelayHandshakeResult<(Vec, NtorV3XofReader)> { + let mut rng = rand::thread_rng(); let secret_key_y = curve25519::StaticSecret::random_from_rng(rng); - server_handshake_obfs4_no_keygen(reply_fn, &secret_key_y, message, keys, verification) + server_handshake_o5_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( +/// As `server_handshake_o5`, but take a secret key instead of an RNG. +fn server_handshake_o5_no_keygen( reply_fn: &mut REPLY, secret_key_y: &curve25519::StaticSecret, message: &[u8], - keys: &[Obfs4NtorSecretKey], + keys: &[O5NtorSecretKey], verification: &[u8], -) -> RelayHandshakeResult<(Vec, Obfs4NtorXofReader)> { +) -> RelayHandshakeResult<(Vec, NtorV3XofReader)> { // 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 requested_pk: curve25519::PublicKey = r.extract()?; + let client_pk: curve25519::PublicKey = r.extract()?; let client_msg = if let Some(msg_len) = r.remaining().checked_sub(MAC_LEN) { r.take(msg_len)? } else { @@ -559,7 +479,7 @@ fn server_handshake_obfs4_no_keygen( 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."))?; + .map_err(into_internal!("Can't apply ntor3 kdf."))?; // Verify the message we received. let computed_mac: DigestVal = { use digest::Digest; @@ -590,15 +510,15 @@ fn server_handshake_obfs4_no_keygen( let secret_input = { let mut si = SecretBuf::new(); - si.write(&xy.as_bytes()) - .and_then(|_| si.write(&xb.as_bytes())) + si.write(&xy) + .and_then(|_| si.write(&xb)) .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(&keypair.pk.pk)) + .and_then(|_| si.write(&client_pk)) + .and_then(|_| si.write(&y_pk)) .and_then(|_| si.write(PROTOID)) .and_then(|_| si.write(&Encap(verification))) - .map_err(into_internal!("can't derive obfs4 secret_input"))?; + .map_err(into_internal!("can't derive ntor3 secret_input"))?; si }; let ntor_key_seed = h_key_seed(&secret_input); @@ -609,7 +529,7 @@ fn server_handshake_obfs4_no_keygen( 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."))?; + .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[..]); @@ -618,156 +538,219 @@ fn server_handshake_obfs4_no_keygen( let encrypted_reply = encrypt(&enc_key, &reply); let auth: DigestVal = { use digest::Digest; - let mut auth = DigestWriter(Sha256::default()); + 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(&keypair.pk.pk)) + .and_then(|_| auth.write(&y_pk)) + .and_then(|_| auth.write(&client_pk)) .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"))?; + .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()) + .write(&y_pk) .and_then(|_| reply.write(&auth)) .and_then(|_| reply.write(&encrypted_reply)) - .map_err(into_internal!("can't encode obfs4 reply."))?; + .map_err(into_internal!("can't encode ntor3 reply."))?; reply }; if okay.into() { - Ok((reply, Obfs4NtorXofReader(keystream))) + Ok((reply, NtorV3XofReader(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()); +#[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 super::*; + use hex_literal::hex; + use tor_basic_utils::test_rng::testing_rng; + + #[test] + fn test_ntor3_roundtrip() { + let mut rng = rand::thread_rng(); + let relay_private = O5NtorSecretKey::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 (c_state, c_handshake) = + client_handshake_o5(&relay_private.pk, client_message, 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 (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); + let (s_handshake, mut s_keygen) = + server_handshake_o5(&mut rep, &c_handshake, &[relay_private], verification) + .unwrap(); - if okay.into() { - Ok((server_reply, Obfs4NtorXofReader(keystream))) - } else { - Err(Error::BadCircHandshakeAuth) + let (s_msg, mut c_keygen) = + client_handshake_o5_part2(&c_state, &s_handshake, verification).unwrap(); + + assert_eq!(rep.0[..], client_message[..]); + assert_eq!(s_msg[..], relay_message[..]); + use digest::XofReader; + 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]); } -} -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) + // Same as previous test, but use the higher-level APIs instead. + #[test] + fn test_ntor3_roundtrip_highlevel() { + let mut rng = rand::thread_rng(); + let relay_private = O5NtorSecretKey::generate_for_test(&mut testing_rng()); + + let (c_state, c_handshake) = O5Client::client1(&relay_private.pk, &[]).unwrap(); + + let mut rep = |_: &[NtorV3Extension]| Some(vec![]); + + let mut s = O5Server {}; + let (s_keygen, s_handshake) = s.server(&mut rep, &[relay_private], &c_handshake).unwrap(); + + let (extensions, keygen) = O5Client::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 mut rng = rand::thread_rng(); + let relay_private = O5NtorSecretKey::generate_for_test(&mut testing_rng()); + + let client_exts = vec![NtorV3Extension::RequestCongestionControl]; + let reply_exts = vec![NtorV3Extension::AckCongestionControl { sendme_inc: 42 }]; + + let (c_state, c_handshake) = O5Client::client1( + &relay_private.pk, + &[NtorV3Extension::RequestCongestionControl], + ) + .unwrap(); + + let mut rep = |msg: &[NtorV3Extension]| -> Option> { + assert_eq!(msg, client_exts); + Some(reply_exts.clone()) + }; + + let (s_keygen, s_handshake) = + O5Server::server(&mut rng, &mut rep, &[relay_private], &c_handshake).unwrap(); + + let (extensions, keygen) = O5Client::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 b = hex!("4051daa5921cfa2a1c27b08451324919538e79e788a81b38cbed097a5dff454a"); + let id = hex!("9fad2af287ef942632833d21f946c6260c33fae6172b60006e86e4a6911753a2"); + let x = hex!("b825a3719147bcbe5fb1d0b0fcb9c09e51948048e2e3283d2ab7b45b5ef38b49"); + let y = hex!("4865a5b7689dafd978f529291c7171bc159be076b92186405d13220b80e2a053"); + let b: curve25519::StaticSecret = b.into(); + let B: curve25519::PublicKey = (&b).into(); + let id: Ed25519Identity = id.into(); + let x: curve25519::StaticSecret = x.into(); + //let X = (&x).into(); + let y: curve25519::StaticSecret = y.into(); + + let client_message = hex!("68656c6c6f20776f726c64"); + let verification = hex!("78797a7a79"); + let server_message = hex!("486f6c61204d756e646f"); + + let materials = CHSMaterials{ + node_pubkey: O5NtorPublicKey { pk: B, id }, + }; + let relay_private = O5NtorSecretKey { + sk: b, + pk: materials.node_pubkey.clone(), + }; + + let (state, client_handshake) = + client_handshake_o5_no_keygen(&materials.node_pubkey, &client_message, &verification, x) + .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_handshake_o5_no_keygen( + &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_handshake_o5_part2(&state, &server_handshake, &verification).unwrap(); + assert_eq!(&server_msg_received, &server_message); + + let (c_keys, s_keys) = { + use digest::XofReader; + 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")[..]); + } } #[cfg(test)] diff --git a/crates/o5/src/handshake/ntor_obfs4.rs b/crates/o5/src/handshake/ntor_obfs4.rs new file mode 100644 index 0000000..4feeb56 --- /dev/null +++ b/crates/o5/src/handshake/ntor_obfs4.rs @@ -0,0 +1,352 @@ +//! Implements the ntor handshake, as used in modern Tor. + +use crate::{ + common::{ + ct, + curve25519::{EphemeralSecret, PublicKey, Representable, SharedSecret, StaticSecret}, + kdf::{Kdf, Ntor1Kdf}, + ntor_arti::{ + AuxDataReply, ClientHandshake, KeyGenerator, RelayHandshakeError, RelayHandshakeResult, + ServerHandshake, + }, + }, + constants::*, + framing::{O5Codec, KEY_MATERIAL_LENGTH}, + Error, Result, Server, +}; + +use std::borrow::Borrow; + +use base64::{ + engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, + Engine as _, +}; +use bytes::BytesMut; +use digest::Mac; +use hmac::Hmac; +use ptrs::warn; +use subtle::ConstantTimeEq; +use tor_bytes::{EncodeResult, SecretBuf, Writer}; +use tor_error::into_internal; +use tor_llcrypto::d::Sha256; +use tor_llcrypto::pk::rsa::RsaIdentity; + +#[cfg(test)] +use rand::{CryptoRng, RngCore}; + +mod handshake_client; +mod handshake_server; +mod utils; + +pub(crate) use utils::*; + +pub(crate) use handshake_client::HandshakeMaterials as CHSMaterials; +#[cfg(test)] +pub(crate) use handshake_client::{ + client_handshake2_no_auth_check_obfs4, client_handshake_obfs4_no_keygen, +}; +use handshake_client::{client_handshake2_obfs4, client_handshake_obfs4, NtorHandshakeState}; +pub(crate) use handshake_server::HandshakeMaterials as SHSMaterials; + +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"; +pub(crate) const M_EXPAND: &[u8; 35] = b"ntor-curve25519-sha256-1:key_expand"; + +/// Struct containing associated function for the obfs4 Ntor handshake. +pub(crate) struct O5NtorHandshake; + +impl ClientHandshake for O5NtorHandshake { + type KeyType = CHSMaterials; + type StateType = NtorHandshakeState; + type KeyGen = NtorHkdfKeyGenerator; + type ClientAuxData = (); + type ServerAuxData = BytesMut; + + fn client1>( + key: &Self::KeyType, + _client_aux_data: &M, + ) -> Result<(Self::StateType, Vec)> { + client_handshake_obfs4(key) + } + + fn client2>( + state: Self::StateType, + msg: T, + ) -> Result<(Self::ServerAuxData, Self::KeyGen)> { + let (keygen, remainder) = client_handshake2_obfs4(msg, &state)?; + Ok((BytesMut::from(&remainder[..]), keygen)) + } +} + +impl ServerHandshake for Server { + type KeyType = SHSMaterials; + type KeyGen = NtorHkdfKeyGenerator; + type ClientAuxData = (); + type ServerAuxData = (); + + fn server, T: AsRef<[u8]>>( + &self, + reply_fn: &mut REPLY, + key: &[Self::KeyType], + msg: T, + ) -> RelayHandshakeResult<(Self::KeyGen, Vec)> { + reply_fn + .reply(&()) + .ok_or(RelayHandshakeError::BadClientHandshake)?; + + if key.is_empty() { + return Err(RelayHandshakeError::MissingKey); + } + + if key.len() > 1 { + warn!("Multiple keys provided, but only the first key will be used"); + } + + let shs_materials = key[0].clone(); + + self.server_handshake_obfs4(msg, shs_materials) + } +} + +/// 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, Copy, PartialEq, Eq, Hash)] +pub(crate) struct O5NtorPublicKey { + /// Public RSA identity fingerprint for the relay; used in authentication + /// calculation. + pub(crate) id: RsaIdentity, + /// The Bridge's identity key. + pub(crate) pk: PublicKey, +} + +impl O5NtorPublicKey { + const CERT_LENGTH: usize = NODE_ID_LENGTH + NODE_PUBKEY_LENGTH; + const CERT_SUFFIX: &'static str = "=="; + /// Construct a new O5NtorPublicKey from its components. + #[allow(unused)] + pub(crate) fn new(pk: [u8; NODE_PUBKEY_LENGTH], id: [u8; NODE_ID_LENGTH]) -> Self { + Self { + pk: pk.into(), + id: id.into(), + } + } +} + +impl std::str::FromStr for O5NtorPublicKey { + 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().unwrap(); + let pk: [u8; NODE_PUBKEY_LENGTH] = decoded[NODE_ID_LENGTH..].try_into().unwrap(); + Ok(O5NtorPublicKey::new(pk, id)) + } +} + +#[allow(clippy::to_string_trait_impl)] +impl std::string::ToString for O5NtorPublicKey { + 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) + } +} + +/// Secret key information used by a relay for the ntor v3 handshake. +#[derive(Clone)] +pub(crate) struct O5NtorSecretKey { + /// The relay's public key information + pub(crate) pk: O5NtorPublicKey, + /// The secret onion key. + pub(crate) sk: StaticSecret, +} + +impl O5NtorSecretKey { + /// Construct a new O5NtorSecretKey from its components. + #[allow(unused)] + pub(crate) fn new(sk: StaticSecret, id: RsaIdentity) -> Self { + let pk = PublicKey::from(&sk); + Self { + pk: O5NtorPublicKey { id, pk }, + sk, + } + } + + /// Construct a new ['O5NtorSecretKey'] from a CSPRNG. + pub(crate) fn getrandom() -> Self { + let sk = Representable::random_static(); + let mut id = [0_u8; NODE_ID_LENGTH]; + getrandom::getrandom(&mut id).expect("internal randomness error"); + Self::new(sk, RsaIdentity::from(id)) + } + + /// Generate a key using the given `rng`, suitable for testing. + #[cfg(test)] + pub(crate) fn generate_for_test(rng: &mut R) -> Self { + let mut id = [0_u8; 20]; + // Random bytes will work for testing, but aren't necessarily actually a valid id. + rng.fill_bytes(&mut id); + + let sk = Representable::static_from_rng(rng); + + let pk = O5NtorPublicKey { + pk: (&sk).into(), + id: id.into(), + }; + Self { pk, sk } + } +} + +/// KeyGenerator for use with ntor circuit handshake. +pub(crate) struct NtorHkdfKeyGenerator { + /// Secret key information derived from the handshake, used as input + /// to HKDF + seed: SecretBuf, + codec: O5Codec, + session_id: [u8; SESSION_ID_LEN], +} + +impl NtorHkdfKeyGenerator { + /// Create a new key generator to expand a given seed + pub(crate) fn new(seed: SecretBuf, is_client: bool) -> Self { + // use the seed value to bootstrap Read / Write crypto codec. + let okm = Self::kdf(&seed[..], KEY_MATERIAL_LENGTH * 2 + SESSION_ID_LEN) + .expect("bug: failed to derive key material from seed"); + + let ekm: [u8; KEY_MATERIAL_LENGTH] = okm[KEY_MATERIAL_LENGTH..KEY_MATERIAL_LENGTH * 2] + .try_into() + .unwrap(); + let dkm: [u8; KEY_MATERIAL_LENGTH] = okm[..KEY_MATERIAL_LENGTH].try_into().unwrap(); + + let session_id = okm[KEY_MATERIAL_LENGTH * 2..].try_into().unwrap(); + + // server ekm == client dkm and vice-versa + let codec = match is_client { + false => O5Codec::new(ekm, dkm), + true => O5Codec::new(dkm, ekm), + }; + + NtorHkdfKeyGenerator { + seed, + codec, + session_id, + } + } + + fn kdf(seed: impl AsRef<[u8]>, keylen: usize) -> Result { + Ntor1Kdf::new(&T_KEY[..], &M_EXPAND[..]).derive(seed.as_ref(), keylen) + } +} + +impl KeyGenerator for NtorHkdfKeyGenerator { + fn expand(self, keylen: usize) -> Result { + let ntor1_key = &T_KEY[..]; + let ntor1_expand = &M_EXPAND[..]; + Ntor1Kdf::new(ntor1_key, ntor1_expand).derive(&self.seed[..], keylen) + } +} + +/// 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: &O5NtorPublicKey, + 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(PROTO_ID)?; // 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)) +} + +/// O5 helper trait to ensure that a returned key generator can be used +/// to create a usable codec and retrieve a session id. +pub trait O5Keygen: KeyGenerator + Into { + fn session_id(&mut self) -> [u8; SESSION_ID_LEN]; +} + +impl O5Keygen for NtorHkdfKeyGenerator { + fn session_id(&mut self) -> [u8; SESSION_ID_LEN] { + self.session_id + } +} + +impl From for O5Codec { + fn from(keygen: NtorHkdfKeyGenerator) -> Self { + keygen.codec + } +} + +#[cfg(test)] +mod integration; diff --git a/crates/o5/src/handshake/utils.rs b/crates/o5/src/handshake/utils.rs new file mode 100644 index 0000000..fc89649 --- /dev/null +++ b/crates/o5/src/handshake/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/handshake_old/integration.rs b/crates/o5/src/handshake_old/integration.rs new file mode 100644 index 0000000..6dc00f5 --- /dev/null +++ b/crates/o5/src/handshake_old/integration.rs @@ -0,0 +1,297 @@ +#![allow(non_snake_case)] // to enable variable names matching the spec. +#![allow(clippy::many_single_char_names)] // ibid + +// @@ 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)] +//! + +use super::*; +use crate::common::ntor_arti::{ClientHandshake, ServerHandshake}; + +use hex_literal::hex; +use tor_basic_utils::test_rng::testing_rng; +use digest::XofReader; + + + +#[test] +fn test_obfs4_roundtrip() { + let mut rng = rand::thread_rng(); + let relay_private = O5NtorSecretKey::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 (c_state, c_handshake) = + client_handshake_obfs4(&mut rng, &relay_private.pk, client_message, 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_handshake_obfs4( + &mut rng, + &mut rep, + &c_handshake, + &[relay_private], + verification, + ) + .unwrap(); + + let (s_msg, mut c_keygen) = + client_handshake_obfs4_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_obfs4_roundtrip_highlevel() { + let mut rng = rand::thread_rng(); + let relay_private = O5NtorSecretKey::generate_for_test(&mut testing_rng()); + + let (c_state, c_handshake) = + Obfs4NtorClient::client1(&mut rng, &relay_private.pk, &[]).unwrap(); + + let mut rep = |_: &[NtorV3Extension]| Some(vec![]); + + let (s_keygen, s_handshake) = + Obfs4NtorServer::server(&mut rng, &mut rep, &[relay_private], &c_handshake).unwrap(); + + let (extensions, keygen) = Obfs4NtorClient::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_obfs4_roundtrip_highlevel_cc() { + let mut rng = rand::thread_rng(); + let relay_private = O5NtorSecretKey::generate_for_test(&mut testing_rng()); + + let client_exts = vec![NtorV3Extension::RequestCongestionControl]; + let reply_exts = vec![NtorV3Extension::AckCongestionControl { sendme_inc: 42 }]; + + let (c_state, c_handshake) = Obfs4NtorClient::client1( + &mut rng, + &relay_private.pk, + &[NtorV3Extension::RequestCongestionControl], + ) + .unwrap(); + + let mut rep = |msg: &[NtorV3Extension]| -> Option> { + assert_eq!(msg, client_exts); + Some(reply_exts.clone()) + }; + + let (s_keygen, s_handshake) = + Obfs4NtorServer::server(&mut rng, &mut rep, &[relay_private], &c_handshake).unwrap(); + + let (extensions, keygen) = Obfs4NtorClient::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_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 = O5NtorPublicKey { pk: B, id, rp: None }; + let identity_private = O5NtorSecretKey { + sk: b, + pk: identity_public.clone(), + }; + + let (state, client_handshake) = + client_handshake_obfs4_no_keygen(&identity_public, &client_message, &verification, x) + .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_handshake_obfs4_no_keygen( + &mut rep, + &y, + &client_handshake, + &[identity_private], + &verification, + ) + .unwrap(); + assert!(rep.2); + + // assert_eq!(server_handshake[..], hex!("4bf4814326fdab45ad5184f5518bd7fae25dc59374062698201a50a22954246d2fc5f8773ca824542bc6cf6f57c7c29bbf4e5476461ab130c5b18ab0a91276651202c3e1e87c0d32054c")[..]); + + let (server_msg_received, mut client_keygen) = + client_handshake_obfs4_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!("05b858d18df21a01566c74d39a5b091b4415f103c05851e77e79b274132dc5b5")[..]); + // assert_eq!(c_keys[..], hex!("9c19b631fd94ed86a817e01f6c80b0743a43f5faebd39cfaa8b00fa8bcc65c3bfeaa403d91acbd68a821bf6ee8504602b094a254392a07737d5662768c7a9fb1b2814bb34780eaee6e867c773e28c212ead563e98a1cd5d5b4576f5ee61c59bde025ff2851bb19b721421694f263818e3531e43a9e4e3e2c661e2ad547d8984caa28ebecd3e4525452299be26b9185a20a90ce1eac20a91f2832d731b54502b09749b5a2a2949292f8cfcbeffb790c7790ed935a9d251e7e336148ea83b063a5618fcff674a44581585fd22077ca0e52c59a24347a38d1a1ceebddbf238541f226b8f88d0fb9c07a1bcd2ea764bbbb5dacdaf5312a14c0b9e4f06309b0333b4a")[..]); +} + +#[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 = curve25519::StaticSecret::random_from_rng(&mut rng); + let rp: Option= (&sk).into(); + let repres = match rp { + Some(r) => r, + None => { + not_found += 1; + continue; + } + }; + + let pk = curve25519::PublicKey::from(&sk); + + + let decoded_pk = curve25519::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 keypair() -> Result<()> { + let mut rng = rand::thread_rng(); + for _ in 0..1_000 { + let kp = O5NtorSecretKey::generate_for_test(&mut rng); + + let pk = kp.pk.pk.to_bytes(); + let repres = kp.pk.rp; + + let pubkey = curve25519::PublicKey::from(&repres.unwrap()); + assert_eq!(hex::encode(pk), hex::encode(pubkey.to_bytes())); + } + 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 +// operation since the benchmark does both sides. +func BenchmarkHandshake(b *testing.B) { + // Generate the "long lasting" identity key and NodeId. + idKeypair, err := NewKeypair(false) + if err != nil || idKeypair == nil { + b.Fatal("Failed to generate identity keypair") + } + nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13")) + if err != nil { + b.Fatal("Failed to load NodeId:", err) + } + b.ResetTimer() + + // Start the actual benchmark. + for i := 0; i < b.N; i++ { + // Generate the keypairs. + serverKeypair, err := NewKeypair(true) + if err != nil || serverKeypair == nil { + b.Fatal("Failed to generate server keypair") + } + + clientKeypair, err := NewKeypair(true) + if err != nil || clientKeypair == nil { + b.Fatal("Failed to generate client keypair") + } + + // Server handshake. + clientPublic := clientKeypair.Representative().ToPublic() + ok, serverSeed, serverAuth := ServerHandshake(clientPublic, + serverKeypair, idKeypair, nodeID) + if !ok || serverSeed == nil || serverAuth == nil { + b.Fatal("ServerHandshake failed") + } + + // Client handshake. + serverPublic := serverKeypair.Representative().ToPublic() + ok, clientSeed, clientAuth := ClientHandshake(clientKeypair, + serverPublic, idKeypair.Public(), nodeID) + if !ok || clientSeed == nil || clientAuth == nil { + b.Fatal("ClientHandshake failed") + } + + // Validate the authenticator. Real code would pass the AUTH read off + // the network as a slice to CompareAuth here. + if !CompareAuth(clientAuth, serverAuth.Bytes()[:]) || + !CompareAuth(serverAuth, clientAuth.Bytes()[:]) { + b.Fatal("AUTH mismatched between client/server") + } + } +} +*/ diff --git a/crates/o5/src/handshake_old/mod.rs b/crates/o5/src/handshake_old/mod.rs new file mode 100644 index 0000000..30b2330 --- /dev/null +++ b/crates/o5/src/handshake_old/mod.rs @@ -0,0 +1,774 @@ +//! 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: &O5NtorPublicKey, + 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 = O5NtorPublicKey; + type StateType = O5NtorHandshakeState; + 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: &O5NtorPublicKey, + 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 = O5NtorSecretKey; + 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 O5NtorPublicKey { + /// 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 O5NtorSecretKey { + /// The relay's public key information + pk: O5NtorPublicKey, + /// The secret onion key. + sk: curve25519::StaticSecret, +} + +impl O5NtorSecretKey { + /// Construct a new O5NtorSecretKey from its components. + #[allow(unused)] + pub(crate) fn new( + sk: curve25519::StaticSecret, + pk: curve25519::PublicKey, + rp: Option, + id: Ed25519Identity, + ) -> Self { + Self { + pk: O5NtorPublicKey { 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 = O5NtorPublicKey { + 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 O5NtorHandshakeState { + /// The public key of the relay we're communicating with. + relay_public: O5NtorPublicKey, // 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: &O5NtorPublicKey, + client_msg: &[u8], + verification: &[u8], +) -> EncodeResult<(O5NtorHandshakeState, 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: &O5NtorPublicKey, + client_msg: &[u8], + verification: &[u8], + my_sk: curve25519::StaticSecret, +) -> EncodeResult<(O5NtorHandshakeState, 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 = O5NtorHandshakeState { + 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: &[O5NtorSecretKey], + 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: &[O5NtorSecretKey], + 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: &O5NtorHandshakeState, + 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/lib.rs b/crates/o5/src/lib.rs index f14f12c..ab7f616 100644 --- a/crates/o5/src/lib.rs +++ b/crates/o5/src/lib.rs @@ -1,9 +1,44 @@ #![doc = include_str!("../README.md")] +pub mod client; +pub mod common; +pub mod server; -mod framing; -// mod handshake; -// mod transport; +pub mod framing; +pub mod proto; +pub use client::{Client, ClientBuilder}; +pub use server::{Server, ServerBuilder}; + +pub(crate) mod constants; +pub(crate) mod handshake; +pub(crate) mod sessions; + +#[cfg(test)] +mod testing; + +mod pt; +pub use pt::{O5PT, Transport}; + +mod error; +pub use error::{Error, Result}; + +pub const OBFS4_NAME: &str = "obfs4"; + +#[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;iat-mode=0"; + + /// 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;iat-mode=0"; +} #[cfg(test)] #[allow(unused)] diff --git a/crates/o5/src/proto.rs b/crates/o5/src/proto.rs new file mode 100644 index 0000000..ae1a2f5 --- /dev/null +++ b/crates/o5/src/proto.rs @@ -0,0 +1,373 @@ +use crate::{ + common::{ + drbg, + probdist::{self, WeightedDist}, + }, + constants::*, + framing, + sessions::Session, + Error, 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}; + +#[allow(dead_code, unused)] +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub enum IAT { + #[default] + Off, + Enabled, + Paranoid, +} + +#[derive(Debug, Clone)] +pub(crate) enum MaybeTimeout { + Default_, + Fixed(Instant), + Length(Duration), + Unset, +} + +impl std::str::FromStr for IAT { + type Err = Error; + fn from_str(s: &str) -> StdResult { + match s { + "0" => Ok(IAT::Off), + "1" => Ok(IAT::Enabled), + "2" => Ok(IAT::Paranoid), + _ => Err(format!("invalid iat-mode '{s}'").into()), + } + } +} + +impl std::fmt::Display for IAT { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IAT::Off => write!(f, "0")?, + IAT::Enabled => write!(f, "1")?, + IAT::Paranoid => write!(f, "2")?, + } + Ok(()) + } +} + +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] +pub struct O5Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + // s: Arc>>, + #[pin] + s: O4Stream, +} + +impl O5Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + pub(crate) fn from_o4(o4: O4Stream) -> Self { + O5Stream { + // s: Arc::new(Mutex::new(o4)), + s: o4, + } + } +} + +#[pin_project] +pub(crate) struct O4Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + #[pin] + pub stream: Framed, + + pub length_dist: probdist::WeightedDist, + pub iat_dist: probdist::WeightedDist, + + pub session: Session, +} + +impl O4Stream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + pub(crate) fn new( + // inner: &'a mut dyn Stream<'a>, + inner: T, + codec: framing::O5Codec, + session: Session, + ) -> O4Stream { + 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 iat_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 iat_dist = WeightedDist::new( + iat_seed, + 0, + framing::MAX_SEGMENT_LENGTH as i32, + session.biased(), + ); + + Self { + stream, + session, + length_dist, + iat_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 IAT 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 O4Stream +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 O4Stream +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..9988807 --- /dev/null +++ b/crates/o5/src/pt.rs @@ -0,0 +1,274 @@ +use crate::{ + constants::*, + handshake::O5NtorPublicKey, + proto::{O5Stream, IAT}, + Error, OBFS4_NAME, +}; +use ptrs::{args::Args, FutureResult as F}; + +use std::{ + marker::PhantomData, + net::{SocketAddrV4, SocketAddrV6}, + pin::Pin, + str::FromStr, + 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 = OBFS4_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 { + OBFS4_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 { + OBFS4_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.iat_mode(state.iat_mode); + // self.drbg = state.drbg_seed; // TODO apply seed from args to server + + trace!( + "node_pubkey: {}, node_id: {}, iat: {}", + hex::encode(self.identity_keys.pk.pk.as_bytes()), + hex::encode(self.identity_keys.pk.id.as_bytes()), + self.iat_mode, + ); + 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 { + OBFS4_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); + let ntor_pk = O5NtorPublicKey::from_str(&cert_strs)?; + let pk: [u8; NODE_PUBKEY_LENGTH] = *ntor_pk.pk.as_bytes(); + let id: [u8; NODE_ID_LENGTH] = ntor_pk.id.as_bytes().try_into().unwrap(); + (pk, id) + } + 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}'"))?; + + let pk = <[u8; 32]>::from_hex(public_key_strs) + .map_err(|e| format!("malformed public key: {e}"))?; + // O5NtorPublicKey::new(pk, node_id) + (pk, id) + } + }; + + // IAT config is common across the two bridge line formats. + let iat_strs = opts + .retrieve(IAT_ARG) + .ok_or(format!("missing argument '{IAT_ARG}'"))?; + let iat_mode = IAT::from_str(&iat_strs)?; + + self.with_node_pubkey(server_materials.0) + .with_node_id(server_materials.1) + .with_iat_mode(iat_mode); + trace!( + "node_pubkey: {}, node_id: {}, iat: {}", + hex::encode(self.station_pubkey), + hex::encode(self.station_id), + iat_mode + ); + + 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 { + OBFS4_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 { + OBFS4_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..74287c3 --- /dev/null +++ b/crates/o5/src/server.rs @@ -0,0 +1,386 @@ +#![allow(unused)] + +use super::*; +use crate::{ + client::ClientBuilder, + common::{ + colorize, + curve25519::{PublicKey, StaticSecret}, + drbg, + replay_filter::{self, ReplayFilter}, + HmacSha256, + }, + constants::*, + framing::{FrameError, Marshall, O5Codec, TryParse, KEY_LENGTH}, + handshake::{O5NtorPublicKey, O5NtorSecretKey}, + proto::{MaybeTimeout, O5Stream, IAT}, + sessions::Session, + Error, Result, +}; +use ptrs::args::Args; + +use std::{borrow::BorrowMut, marker::PhantomData, ops::Deref, str::FromStr, 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; +use tor_llcrypto::pk::rsa::RsaIdentity; + +const STATE_FILENAME: &str = "obfs4_state.json"; + +pub struct ServerBuilder { + pub iat_mode: IAT, + pub statefile_path: Option, + pub(crate) identity_keys: O5NtorSecretKey, + 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 = O5NtorSecretKey::getrandom(); + Self { + iat_mode: IAT::Off, + 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: [u8; KEY_LENGTH * 2]) -> &Self { + let sk: [u8; KEY_LENGTH] = keys[..KEY_LENGTH].try_into().unwrap(); + let pk: [u8; KEY_LENGTH] = keys[KEY_LENGTH..].try_into().unwrap(); + self.identity_keys.sk = sk.into(); + self.identity_keys.pk.pk = (&self.identity_keys.sk).into(); + 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 iat_mode(&mut self, iat: IAT) -> &Self { + self.iat_mode = iat; + 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.insert(IAT_ARG.into(), vec![self.iat_mode.to_string()]); + params.encode_smethod_args() + } + + pub fn build(&self) -> Server { + Server(Arc::new(ServerInner { + identity_keys: self.identity_keys.clone(), + iat_mode: self.iat_mode, + 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, + #[serde(rename = "iat-mode")] + iat_mode: 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); + } + if let Some(mode) = self.iat_mode { + args.add(IAT_ARG, &mode); + } + } +} + +pub(crate) struct RequiredServerState { + pub(crate) private_key: O5NtorSecretKey, + pub(crate) drbg_seed: drbg::Drbg, + pub(crate) iat_mode: IAT, +} + +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 iat_mode = match value.retrieve(IAT_ARG) { + Some(s) => IAT::from_str(&s)?, + None => IAT::default(), + }; + + let secret_key = StaticSecret::from(sk); + let private_key = O5NtorSecretKey::new(secret_key, RsaIdentity::from(node_id)); + + Ok(RequiredServerState { + private_key, + drbg_seed: drbg::Drbg::new(Some(drbg_seed))?, + iat_mode, + }) + } +} + +#[derive(Clone)] +pub struct Server(Arc); + +pub struct ServerInner { + pub(crate) handshake_timeout: Option, + pub(crate) iat_mode: IAT, + pub(crate) biased: bool, + pub(crate) identity_keys: O5NtorSecretKey, + + 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(sec: [u8; KEY_LENGTH], id: [u8; NODE_ID_LENGTH]) -> Self { + let sk = StaticSecret::from(sec); + let pk = O5NtorPublicKey { + pk: PublicKey::from(&sk), + id: id.into(), + }; + + let identity_keys = O5NtorSecretKey { pk, sk }; + + Self::new_from_key(identity_keys) + } + + pub(crate) fn new_from_key(identity_keys: O5NtorSecretKey) -> Self { + Self(Arc::new(ServerInner { + handshake_timeout: Some(SERVER_HANDSHAKE_TIMEOUT), + identity_keys, + iat_mode: IAT::Off, + biased: false, + + // metrics: Arc::new(std::sync::Mutex::new(ServerMetrics {})), + replay_filter: ReplayFilter::new(REPLAY_TTL), + })) + } + + pub fn new_from_random(mut rng: R) -> Self { + let mut id = [0_u8; 20]; + // Random bytes will work for testing, but aren't necessarily actually a valid id. + rng.fill_bytes(&mut id); + + // Generated identity secret key does not need to be elligator2 representable + // so we can use the regular dalek_x25519 key generation. + let sk = StaticSecret::random_from_rng(rng); + + let pk = O5NtorPublicKey { + pk: PublicKey::from(&sk), + id: id.into(), + }; + + let identity_keys = O5NtorSecretKey { pk, sk }; + + Self::new_from_key(identity_keys) + } + + pub fn getrandom() -> Self { + let identity_keys = O5NtorSecretKey::getrandom(); + 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); + + session.handshake(&self, stream, deadline).await + } + + // pub fn set_iat_mode(&mut self, mode: IAT) -> &Self { + // self.iat_mode = mode; + // self + // } + + 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 { + station_pubkey: *self.identity_keys.pk.pk.as_bytes(), + station_id: self.identity_keys.pk.id.as_bytes().try_into().unwrap(), + iat_mode: self.iat_mode, + 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 + identity_keys: self.identity_keys.clone(), + biased: self.biased, + + // generated per session + session_id, + len_seed: drbg::Seed::new().unwrap(), + iat_seed: drbg::Seed::new().unwrap(), + + _state: sessions::Initialized {}, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::dev; + + use super::*; + + use ptrs::trace; + use tokio::net::TcpStream; + + #[test] + fn parse_json_state() -> Result<()> { + crate::test_utils::init_subscriber(); + + let mut args = Args::new(); + let test_state = format!( + r#"{{"{NODE_ID_ARG}": "00112233445566778899", "{PRIVATE_KEY_ARG}":"0123456789abcdeffedcba9876543210", "{IAT_ARG}": "0", "{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..694f944 --- /dev/null +++ b/crates/o5/src/sessions.rs @@ -0,0 +1,497 @@ +//! obfs4 session details and construction +//! +/// Session state management as a way to organize session establishment and +/// steady state transfer. +use crate::{ + common::{ + colorize, discard, drbg, + ntor_arti::{ClientHandshake, RelayHandshakeError, ServerHandshake}, + }, + constants::*, + framing, + handshake::{ + CHSMaterials, O5Keygen, O5NtorHandshake, O5NtorPublicKey, O5NtorSecretKey, + SHSMaterials, + }, + proto::{O4Stream, O5Stream, IAT}, + server::Server, + Error, Result, +}; + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use bytes::BytesMut; +use ptrs::{debug, info, trace}; +use rand_core::RngCore; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::time::Instant; +use tokio_util::codec::Decoder; + +/// 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.clone(), + Session::Server(ss) => ss.len_seed.clone(), + } + } +} + +// ================================================================ // +// Client States // +// ================================================================ // + +pub(crate) struct ClientSession { + node_pubkey: O5NtorPublicKey, + session_id: [u8; SESSION_ID_LEN], + iat_mode: IAT, // TODO: add IAT normal / paranoid writing modes + 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-") + &colorize(self.session_id) + } + + pub(crate) fn set_session_id(&mut self, id: [u8; SESSION_ID_LEN]) { + debug!( + "{} -> {} client updating session id", + colorize(self.session_id), + colorize(id) + ); + self.session_id = id; + } + + /// Helper function to perform state transitions. + fn transition(self, t: T) -> ClientSession { + ClientSession { + node_pubkey: self.node_pubkey, + session_id: self.session_id, + iat_mode: self.iat_mode, + 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, + iat_mode: self.iat_mode, + epoch_hour: self.epoch_hour, + biased: self.biased, + + len_seed: self.len_seed, + _state: f, + } + } +} + +pub fn new_client_session( + station_pubkey: O5NtorPublicKey, + iat_mode: IAT, +) -> ClientSession { + let mut session_id = [0u8; SESSION_ID_LEN]; + rand::thread_rng().fill_bytes(&mut session_id); + ClientSession { + node_pubkey: station_pubkey, + session_id, + iat_mode, + epoch_hour: "".into(), + biased: false, + + len_seed: drbg::Seed::new().unwrap(), + _state: Initialized, + } +} + +impl ClientSession { + /// Perform a Handshake over the provided stream. + /// ``` + /// + /// ``` + /// + /// TODO: make sure failure modes align with golang obfs4 + /// - FIN/RST based on 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 = O4Stream::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 O5Keygen)> + where + T: AsyncRead + AsyncWrite + Unpin, + { + let (state, chs_message) = O5NtorHandshake::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 O5NtorHandshake::client2(state.clone(), &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:{}, iat:{:?}, epoch_hr:{} ]", + hex::encode(self.node_pubkey.id.as_bytes()), + hex::encode(self.node_pubkey.pk.as_bytes()), + self.iat_mode, + self.epoch_hour, + ) + } +} + +// ================================================================ // +// Server Sessions States // +// ================================================================ // + +pub(crate) struct ServerSession { + // fixed by server + pub(crate) identity_keys: O5NtorSecretKey, + pub(crate) biased: bool, + // pub(crate) server: &'a Server, + + // generated per session + pub(crate) session_id: [u8; SESSION_ID_LEN], + pub(crate) len_seed: drbg::Seed, + pub(crate) iat_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-") + &colorize(self.session_id) + } + + pub(crate) fn set_session_id(&mut self, id: [u8; SESSION_ID_LEN]) { + debug!( + "{} -> {} server updating session id", + colorize(self.session_id), + colorize(id) + ); + self.session_id = id; + } + + /// Helper function to perform state transitions. + fn transition(self, _state: T) -> ServerSession { + ServerSession { + // fixed by server + identity_keys: self.identity_keys, + biased: self.biased, + + // generated per session + session_id: self.session_id, + len_seed: self.len_seed, + iat_seed: self.iat_seed, + + _state, + } + } + + /// Helper function to perform state transition on error. + fn fault(self, f: F) -> ServerSession { + ServerSession { + // fixed by server + identity_keys: self.identity_keys, + biased: self.biased, + + // generated per session + session_id: self.session_id, + len_seed: self.len_seed, + iat_seed: self.iat_seed, + + _state: f, + } + } +} + +impl ServerSession { + /// Attempt to complete the handshake with a new client connection. + pub async fn handshake( + self, + server: &Server, + mut stream: T, + deadline: Option, + ) -> Result> + where + T: AsyncRead + AsyncWrite + Unpin, + { + // set up for handshake + let mut session = self.transition(ServerHandshaking {}); + + let materials = SHSMaterials::new( + &session.identity_keys, + 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, 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 = O4Stream::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( + &self, + mut stream: T, + 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(&mut |_: &()| Some(()), &[materials.clone()], &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..185aab1 --- /dev/null +++ b/crates/o5/src/testing.rs @@ -0,0 +1,354 @@ +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 o4_server = Server::getrandom(); + 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(()) - } -}