Skip to content

Commit

Permalink
getting moving on o5 again
Browse files Browse the repository at this point in the history
  • Loading branch information
jmwample committed Sep 18, 2024
1 parent dbd9460 commit c969f4f
Show file tree
Hide file tree
Showing 43 changed files with 9,196 additions and 549 deletions.
38 changes: 36 additions & 2 deletions crates/o5/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
Expand Down
50 changes: 29 additions & 21 deletions crates/o5/README.md
Original file line number Diff line number Diff line change
@@ -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 🚧 ⚠️
Expand All @@ -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

188 changes: 188 additions & 0 deletions crates/o5/src/client.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
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<Self> {
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<impl AsRef<[u8]>>) -> Result<Self> {
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<tokio::time::Duration>,
}

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<O5Stream<T>>
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<ptrs::FutureResult<T, E>>,
) -> Result<O5Stream<T>>
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(())
}
}
10 changes: 10 additions & 0 deletions crates/o5/src/common/README.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions crates/o5/src/common/ct.rs
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit c969f4f

Please sign in to comment.