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
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
members = [
"ooniauth-core",
"ooniauth-py",
"ooniauth-ffi"
"ooniauth-ffi",
"fuzz"
]
default-members = ["ooniauth-core"]
resolver = "2"
Expand Down
4 changes: 4 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
37 changes: 37 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "ooniauth-core-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
bincode = {workspace=true}
rand = "0.8"

[dependencies.ooniauth-core]
path = "../ooniauth-core"

[[bin]]
name = "submit_handle"
path = "fuzz_targets/submit_handle.rs"
test = false
doc = false
bench = false

[[bin]]
name = "registration_open"
path = "fuzz_targets/registration_open.rs"
test = false
doc = false
bench = false

[[bin]]
name = "update_handle"
path = "fuzz_targets/update_handle.rs"
test = false
doc = false
bench = false
10 changes: 10 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Fuzzers
===

This crate contains a collection of basic fuzzing harness that can be used with cargo fuzz.

## Example

```
cargo +nightly fuzz run update_handle
```
24 changes: 24 additions & 0 deletions fuzz/fuzz_targets/registration_open.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#![no_main]
//! Server-facing registration ingress.
//!
//! Mirrors `ServerState::handle_registration_request` in `ooniauth-py`:
//! `base64 -> bincode::deserialize::<open_registration::Request> ->
//! ServerState::open_registration`. Exercises the macro-generated wire decoder
//! and the `try_from(&bytes).unwrap()` re-decode inside the handler.
use libfuzzer_sys::fuzz_target;
use ooniauth_core::registration::open_registration;
use ooniauth_core::ServerState;
use std::sync::OnceLock;

static SERVER: OnceLock<ServerState> = OnceLock::new();

fn server() -> &'static ServerState {
SERVER.get_or_init(|| ServerState::new(&mut rand::thread_rng()))
}

fuzz_target!(|data: &[u8]| {
let Ok(req) = bincode::deserialize::<open_registration::Request>(data) else {
return;
};
let _ = server().open_registration(req);
});
50 changes: 50 additions & 0 deletions fuzz/fuzz_targets/submit_handle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#![no_main]
//! Lead target: most remote-facing surface.
//!
//! Mirrors `ServerState::handle_submit_request` in `ooniauth-py`, which is just
//! `base64 -> bincode::deserialize::<SubmitRequest> -> ServerState::handle_submit`.
//! We feed raw bytes (== post-base64 input) straight into the bincode decoder
//! and the server submit handler.
//!
//! Phase 1 maximises coverage of the deserialization / wire-decode surface
//! (the primary remote DoS / panic vector). The server short-circuits when the
//! request's nym digest does not match `probe_id`; reaching the cryptographic
//! verify path with attacker-chosen nyms is the job of the later structured
//! harness, since `nym_point` is a private field here.
use libfuzzer_sys::fuzz_target;
use ooniauth_core::submit::SubmitRequest;
use ooniauth_core::ServerState;
use std::sync::OnceLock;

static SERVER: OnceLock<ServerState> = OnceLock::new();

fn server() -> &'static ServerState {
// `ServerState::new` also runs `cmz_group_init`, which must happen before
// any protocol handler is called.
SERVER.get_or_init(|| ServerState::new(&mut rand::thread_rng()))
}

fuzz_target!(|data: &[u8]| {
let Ok(req) = bincode::deserialize::<SubmitRequest>(data) else {
return;
};

let server = server();
let probe_id = [0u8; 32];
let today = ServerState::today();
let age_range = today.saturating_sub(30)..today.saturating_add(1);
let measurement_count_range = 0u32..100u32;
let measurement_hash = [1u8; 32];

let mut rng = rand::thread_rng();
let _ = server.handle_submit(
&mut rng,
req,
&probe_id,
"US",
"AS1234",
&measurement_hash,
age_range,
measurement_count_range,
);
});
31 changes: 31 additions & 0 deletions fuzz/fuzz_targets/update_handle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#![no_main]
//! Server-facing credential-update ingress.
//!
//! Mirrors `ServerState::handle_update_request` in `ooniauth-py`:
//! `base64 -> bincode::deserialize::<update::Request> ->
//! ServerState::handle_update`. The old-key material is held fixed; the fuzzer
//! controls only the request bytes (the remote-attacker-controlled input).
use libfuzzer_sys::fuzz_target;
use ooniauth_core::update::update;
use ooniauth_core::ServerState;
use std::sync::OnceLock;

static NEW_SERVER: OnceLock<ServerState> = OnceLock::new();
static OLD_SERVER: OnceLock<ServerState> = OnceLock::new();

fuzz_target!(|data: &[u8]| {
let Ok(req) = bincode::deserialize::<update::Request>(data) else {
return;
};

let new_server = NEW_SERVER.get_or_init(|| ServerState::new(&mut rand::thread_rng()));
let old_server = OLD_SERVER.get_or_init(|| ServerState::new(&mut rand::thread_rng()));

let mut rng = rand::thread_rng();
let _ = new_server.handle_update(
&mut rng,
req,
old_server.secret_key_ref(),
old_server.public_parameters_ref(),
);
});