Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

o5 Transport #26

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

members = [
"crates/lyrebird",
# "crates/o5",
"crates/o5",
"crates/o7",
"crates/obfs4",
"crates/ptrs",
Expand Down
2 changes: 1 addition & 1 deletion crates/lyrebird/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ anyhow = "1.0"
clap = { version = "4.4", features = ["derive"]}
fast-socks5 = "0.9.1"
futures = "0.3.29"
safelog = "0.3.5"
safelog = "0.4.0"
thiserror = "1.0.56"
tokio = { version = "1.34", features = ["io-util", "net", "macros", "sync", "signal"] }
tokio-util = "0.7.10"
Expand Down
63 changes: 56 additions & 7 deletions crates/o5/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,79 @@
[package]
name = "o5"
version = "0.1.0"
version = "0.1.0-alpha.1"
edition = "2021"
authors = ["Jack Wampler <[email protected]>"]
rust-version = "1.81"
license = "MIT OR Apache-2.0"
description = "Pure rust implementation of the o5 pluggable transport"
keywords = ["tor", "censorship", "pluggable", "transports"]
categories = ["network-programming", "cryptography"]
repository = "https://github.com/jmwample/ptrs"


[lib]
name = "o5"
crate-type = ["cdylib", "rlib"]

[dependencies]
## Local
ptrs = { path="../ptrs", version="0.1.0" }

## PRNG
getrandom = "0.2.11"
rand = { version="0.8.5", features=["getrandom"]}
rand_core = "0.6.4"

## Crypto
digest = { version = "0.10.7", features=["mac", "core-api"]}
typenum = "1.17.0"
block-buffer = "0.10.4"
siphasher = "1.0.0"
sha2 = "0.10.8"
hmac = { version="0.12.1", features=["reset"]}
hkdf = "0.12.3"
crypto_secretbox = { version="0.1.1", features=["chacha20"]}
subtle = "2.5.0"
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets", "elligator2"], git = "https://github.com/jmwample/curve25519-dalek.git", branch = "elligator2-ntor"}
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets"]}

## Utils
pin-project = "1.1.3"
hex = "0.4.3"
futures = "0.3.29"
tracing = "0.1.40"
colored = "2.0.4"
serde_json = "1.0.114"
serde = "1.0.197"
base64 = "0.22.0"

# ntor_arti
## Networking tools
tokio = { version = "1.33", features = ["io-util", "rt-multi-thread", "net", "rt", "macros", "sync", "signal", "time", "fs"] }
tokio-util = { version = "0.7.10", features = ["codec", "io"]}
bytes = "1.5.0"

## ntor_arti
tor-cell = "0.23.0"
tor-llcrypto = "0.23.0"
tor-error = "0.23.0"
tor-bytes = "0.23.0"
cipher = "0.4.4"
zeroize = "1.7.0"
thiserror = "1.0.56"

curve25519-elligator2 = { version="0.1.0-alpha.1", features=["elligator2"] }

# o5 pqc
ml-kem = "0.2.1"
kem = "0.3.0-pre.0"
# kemeleon = { version="0.1.0-rc.1", path="../../../../elligantt/kemeleon"}
kemeleon = { version="0.1.0-rc.1", git="https://github.com/jmwample/kemeleon", branch="cleanup"}

[dev-dependencies]
hex = "0.4.3"
anyhow = "1.0"
tracing-subscriber = "0.3.18"
hex-literal = "0.4.1"
tor-basic-utils = "0.22.0"

# o5 pqc test
# pqc_kyber = {version="0.7.1", features=["kyber1024", "std"]}
ml-kem = "0.1.0"

[lints.rust]
# unexpected_cfgs are used to disable incomplete / WIP features and tests. This is
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

175 changes: 175 additions & 0 deletions crates/o5/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#![allow(unused)]

use crate::{
common::{colorize, mlkem1024_x25519, HmacSha256},
constants::*,
framing::{FrameError, Marshall, O5Codec, TryParse, KEY_LENGTH, KEY_MATERIAL_LENGTH},
handshake::IdentityPublicKey,
proto::{MaybeTimeout, O5Stream},
sessions, Error, Result,
};

use bytes::{Buf, BufMut, BytesMut};
use hmac::{Hmac, Mac};
use ptrs::{debug, info, trace, warn};
use rand::prelude::*;
use subtle::ConstantTimeEq;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::time::{Duration, Instant};

use std::{
fmt,
io::{Error as IoError, ErrorKind as IoErrorKind},
pin::Pin,
sync::{Arc, Mutex},
};

#[derive(Clone, Debug)]
pub struct ClientBuilder {
pub station_pubkey: [u8; PUBLIC_KEY_LEN],
pub station_id: [u8; NODE_ID_LENGTH],
pub statefile_path: Option<String>,
pub(crate) handshake_timeout: MaybeTimeout,
}

impl Default for ClientBuilder {
fn default() -> Self {
Self {
station_pubkey: [0u8; PUBLIC_KEY_LEN],
station_id: [0_u8; NODE_ID_LENGTH],
statefile_path: None,
handshake_timeout: MaybeTimeout::Default_,
}
}
}

impl ClientBuilder {
/// TODO: implement client builder from statefile
pub fn from_statefile(location: &str) -> Result<Self> {
Ok(Self {
station_pubkey: [0_u8; PUBLIC_KEY_LEN],
station_id: [0_u8; NODE_ID_LENGTH],
statefile_path: Some(location.into()),
handshake_timeout: MaybeTimeout::Default_,
})
}

/// TODO: implement client builder from string args
pub fn from_params(param_strs: Vec<impl AsRef<[u8]>>) -> Result<Self> {
Ok(Self {
station_pubkey: [0_u8; PUBLIC_KEY_LEN],
station_id: [0_u8; NODE_ID_LENGTH],
statefile_path: None,
handshake_timeout: MaybeTimeout::Default_,
})
}

pub fn with_node_pubkey(&mut self, pubkey: [u8; PUBLIC_KEY_LEN]) -> &mut Self {
self.station_pubkey = pubkey;
self
}

pub fn with_statefile_path(&mut self, path: &str) -> &mut Self {
self.statefile_path = Some(path.into());
self
}

pub fn with_node_id(&mut self, id: [u8; NODE_ID_LENGTH]) -> &mut Self {
self.station_id = id;
self
}

pub fn with_handshake_timeout(&mut self, d: Duration) -> &mut Self {
self.handshake_timeout = MaybeTimeout::Length(d);
self
}

pub fn with_handshake_deadline(&mut self, deadline: Instant) -> &mut Self {
self.handshake_timeout = MaybeTimeout::Fixed(deadline);
self
}

pub fn fail_fast(&mut self) -> &mut Self {
self.handshake_timeout = MaybeTimeout::Unset;
self
}

pub fn build(&self) -> Client {
Client {
station_pubkey: IdentityPublicKey::new(self.station_pubkey, self.station_id)
.expect("failed to build client - bad options."),
handshake_timeout: self.handshake_timeout.duration(),
}
}

pub fn as_opts(&self) -> String {
//TODO: String self as command line options
"".into()
}
}

impl fmt::Display for ClientBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
//TODO: string self
write!(f, "")
}
}

/// Client implementing the obfs4 protocol.
pub struct Client {
station_pubkey: IdentityPublicKey,
handshake_timeout: Option<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);

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);

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
Loading