Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ Cargo.lock
*.perf
*.data
*.data.old

**/.vscode
out.log
16 changes: 7 additions & 9 deletions crates/lyrebird/src/fwd/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,16 +442,14 @@ where
let mut listeners = Vec::new();

let mut builder = Obfs4PT::server_builder();
let server = if params.is_some() {
builder.options(&params.unwrap())?.build()
} else {
builder.build()
};
if params.is_some() {
builder.options(&params.unwrap())?;
}

info!(
"({obfs4_name}) client params: \"{}\"",
builder.get_client_params()
);
let client_options = builder.get_client_params();
let server = builder.build();

info!("({obfs4_name}) client params: \"{}\"", client_options);

let listener = tokio::net::TcpListener::bind(listen_addrs).await?;
listeners.push(server_listen_loop::<TcpStream, _, _>(
Expand Down
6 changes: 3 additions & 3 deletions crates/lyrebird/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,10 @@ async fn server_setup(
}

let mut builder = Obfs4PT::server_builder();
let server = builder
builder
.statefile_location(statedir)?
.options(&bind_addr.options)?
.build();
.options(&bind_addr.options)?;
let server = builder.build();

let listener = tokio::net::TcpListener::bind(bind_addr.addr).await?;
listeners.push(server_listen_loop::<TcpStream, _>(
Expand Down
24 changes: 16 additions & 8 deletions crates/o5/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ keywords = ["tor", "censorship", "pluggable", "transports"]
categories = ["network-programming", "cryptography"]
repository = "https://github.com/jmwample/ptrs"

[features]
default = ["debug"] # TODO: make debug non-default
debug = ["ptrs/debug"]

[lib]
name = "o5"
Expand All @@ -26,15 +29,19 @@ rand_core = "0.6.4"

## Crypto
digest = { version = "0.10.7", features=["mac", "core-api"]}
generic-array = { version = "0.14.7", features=["zeroize"]}
typenum = "1.17.0"
block-buffer = "0.10.4"
siphasher = "1.0.0"
sha2 = "0.10.8"
sha3 = "0.10.8"
hmac = { version="0.12.1", features=["reset"]}
hkdf = "0.12.3"
hybrid-array = "0.2.1"
crypto_secretbox = { version="0.1.1", features=["chacha20"]}
subtle = "2.5.0"
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets"]}
x-wing = "0.0.1-alpha"

## Utils
pin-project = "1.1.3"
Expand All @@ -52,27 +59,28 @@ 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"
tor-cell = "0.24.0"
tor-llcrypto = "0.24.0"
tor-error = "0.24.0"
tor-bytes = "0.24.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"
# kemeleon / OKemCore
ml-kem = { git="https://github.com/jmwample/KEMs", branch="params" }
kem = "0.3.0-pre.0"
# kemeleon = { version="0.1.0-rc.1" }
# 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"}
kemeleon = { git="https://github.com/jmwample/kemeleon", branch="main"}

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


[lints.rust]
Expand Down
27 changes: 13 additions & 14 deletions crates/o5/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,33 @@ date elements without worrying about being backward compatible.
## 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:
- 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
- padding is given an explicit message type different than that of a payload message 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
- x25519 key-exchange -> [X-Wing](https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem#name-with-hpke-x25519kyber768dra) (ML-KEM + X25519) Hybrid Public 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.
- [Kemeleon Encoding](https://docs.rs/kemeleon/latest/kemeleon/) for obfuscating ML-KEM public keys and ciphertext on the wire.
- [Elligator2](https://docs.rs/curve25519-elligator2/latest/curve25519_elligator2/) for obfuscating X25519 public keys.
- [PQ-Obfs handshake](https://eprint.iacr.org/2024/1086.pdf)
- Adapted from the [NTor V3 handshake](https://spec.torproject.org/proposals/332-ntor-v3-with-extra-data.html)
- Change mark and MAC from sha256-128 to sha3-256
- Allow messages extra data to be sent with the handshake, 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.
- This takes space out of the padding already used in the client handshake.
- (Maybe) session tickets and resumption
- (Maybe) handshake complete frame type

### Goals
* Post Quantum Forward Secrecy - traffic captured today cannot be decrypted by a quantum computer tomorrow.
* 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
Expand Down
88 changes: 56 additions & 32 deletions crates/o5/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
#![allow(unused)]

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

use bytes::{Buf, BufMut, BytesMut};
use hmac::{Hmac, Mac};
use kemeleon::Encode;
use ptrs::{debug, info, trace, warn};
use rand::prelude::*;
use subtle::ConstantTimeEq;
Expand All @@ -20,52 +23,64 @@ use tokio::time::{Duration, Instant};
use std::{
fmt,
io::{Error as IoError, ErrorKind as IoErrorKind},
marker::PhantomData,
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 struct ClientBuilder<K: OKemCore, D: Digest> {
pub node_details: Option<IdentityPublicKey<K>>,
pub statefile_path: Option<String>,
pub(crate) handshake_timeout: MaybeTimeout,

pub(crate) _digest: PhantomData<D>,
}

impl Default for ClientBuilder {
impl<K: OKemCore, D: Digest> Default for ClientBuilder<K, D> {
fn default() -> Self {
Self {
station_pubkey: [0u8; PUBLIC_KEY_LEN],
station_id: [0_u8; NODE_ID_LENGTH],
node_details: None,
statefile_path: None,
handshake_timeout: MaybeTimeout::Default_,
_digest: PhantomData,
}
}
}

impl ClientBuilder {
impl<K: OKemCore, D: Digest> ClientBuilder<K, D> {
pub fn new(pubkey: impl AsRef<[u8]>) -> Result<Self> {
Ok(Self {
node_details: Some(IdentityPublicKey::<K>::try_from_bytes(pubkey)?),
statefile_path: None,
handshake_timeout: MaybeTimeout::Default_,
_digest: PhantomData,
})
}

/// TODO: implement client builder from statefile
pub fn from_statefile(location: &str) -> Result<Self> {
todo!("this is not implemented");
Ok(Self {
station_pubkey: [0_u8; PUBLIC_KEY_LEN],
station_id: [0_u8; NODE_ID_LENGTH],
node_details: None,
statefile_path: Some(location.into()),
handshake_timeout: MaybeTimeout::Default_,
_digest: PhantomData,
})
}

/// 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_,
})
todo!("this is not implemented");
}

pub fn with_node_pubkey(&mut self, pubkey: impl AsRef<[u8]>) -> Result<&mut Self> {
self.node_details = Some(IdentityPublicKey::<K>::try_from_bytes(pubkey)?);
Ok(self)
}

pub fn with_node_pubkey(&mut self, pubkey: [u8; PUBLIC_KEY_LEN]) -> &mut Self {
self.station_pubkey = pubkey;
pub(crate) fn with_node(&mut self, pubkey: IdentityPublicKey<K>) -> &mut Self {
self.node_details = Some(pubkey);
self
}

Expand All @@ -75,7 +90,9 @@ impl ClientBuilder {
}

pub fn with_node_id(&mut self, id: [u8; NODE_ID_LENGTH]) -> &mut Self {
self.station_id = id;
if let Some(d) = self.node_details.as_mut() {
d.id = id.into();
}
self
}

Expand All @@ -94,11 +111,14 @@ impl ClientBuilder {
self
}

pub fn build(&self) -> Client {
pub fn build(&self) -> Client<K, D> {
if self.node_details.is_none() {
panic!("tried to build client from details missing server identity");
}
Client {
station_pubkey: IdentityPublicKey::new(self.station_pubkey, self.station_id)
.expect("failed to build client - bad options."),
station_pubkey: self.node_details.clone().unwrap(),
handshake_timeout: self.handshake_timeout.duration(),
_digest: PhantomData,
}
}

Expand All @@ -108,42 +128,43 @@ impl ClientBuilder {
}
}

impl fmt::Display for ClientBuilder {
impl<K: OKemCore, D: Digest> fmt::Display for ClientBuilder<K, D> {
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,
pub struct Client<K: OKemCore, D: Digest> {
station_pubkey: IdentityPublicKey<K>,
handshake_timeout: Option<tokio::time::Duration>,
_digest: PhantomData<D>,
}

impl Client {
impl<K: OKemCore, D: Digest> Client<K, D> {
/// 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>>
pub async fn wrap<'a, T>(self, mut stream: T) -> Result<O5Stream<T, K>>
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
session.handshake::<T, D>(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>>
) -> Result<O5Stream<T, K>>
where
T: AsyncRead + AsyncWrite + Unpin + 'a,
E: std::error::Error + Send + Sync + 'static,
Expand All @@ -154,7 +175,7 @@ impl Client {

let deadline = self.handshake_timeout.map(|d| Instant::now() + d);

session.handshake(stream, deadline).await
session.handshake::<T, D>(stream, deadline).await
}
}

Expand All @@ -163,12 +184,15 @@ mod test {
use super::*;
use crate::Result;

use kemeleon::MlKem768;
use sha3::Sha3_256;

#[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())?;
let cb = ClientBuilder::<MlKem768, Sha3_256>::from_params(test_case.to_vec())?;
}
Ok(())
}
Expand Down
Loading