Skip to content
Merged
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
32 changes: 22 additions & 10 deletions ooniauth-core/src/submit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ pub fn digest_point(point: RistrettoPoint) -> [u8; 32] {
out
}

fn submit_domain_generator(probe_cc: &str, probe_asn: &str) -> G {
debug_assert!(
probe_cc.len() == 2 && probe_cc.bytes().all(|b| b.is_ascii_uppercase()),
"probe_cc must be a two-letter uppercase ASCII country code"
);
debug_assert!(
(3..=12).contains(&probe_asn.len())
&& probe_asn.strip_prefix("AS").is_some_and(|asn| {
asn.bytes().all(|b| b.is_ascii_digit()) && asn.parse::<u32>().is_ok()
}),
"probe_asn must be AS-prefixed ASCII digits with total length 3..=12"
);

let domain_str = format!("ooni.org/{}/{}", probe_cc, probe_asn);
G::hash_from_bytes::<Sha512>(domain_str.as_bytes())
}

fn inclusive_upper_bound(range: &std::ops::Range<u32>) -> u32 {
range.end.saturating_sub(1)
}
Expand Down Expand Up @@ -113,9 +130,8 @@ impl UserState {
))?;

// Domain-specific generator and NYM computation
let domain_str = format!("ooni.org/{}/{}", probe_cc, probe_asn);
trace!("Computing DOMAIN for submit request");
let DOMAIN = G::hash_from_bytes::<Sha512>(domain_str.as_bytes());
let DOMAIN = submit_domain_generator(&probe_cc, &probe_asn);
let NYM = Old.nym_id.unwrap() * DOMAIN;
debug!("NYM computed successfully");

Expand Down Expand Up @@ -252,8 +268,7 @@ impl ServerState {
nym_point,
} = req;

let domain_str = format!("ooni.org/{}/{}", probe_cc, probe_asn);
let DOMAIN = G::hash_from_bytes::<Sha512>(domain_str.as_bytes());
let DOMAIN = submit_domain_generator(probe_cc, probe_asn);

// The probe id must be the salted hash of the nym point.
// Otherwise, return an error.
Expand Down Expand Up @@ -310,8 +325,7 @@ impl ServerState {
#[cfg(test)]
mod tests {
use super::*;
use crate::{Scalar, ServerState, UserState, G};
use sha2::Sha512;
use crate::{Scalar, ServerState, UserState};

#[test]
fn test_domain_nym_computation() {
Expand All @@ -320,16 +334,14 @@ mod tests {

let probe_cc = "US";
let probe_asn = "AS1234";
let domain_str = format!("ooni.org/{}/{}", probe_cc, probe_asn);
let domain = G::hash_from_bytes::<Sha512>(domain_str.as_bytes());
let domain = submit_domain_generator(probe_cc, probe_asn);

// Test with a known nym_id
let nym_id = Scalar::from(42u32);
let nym = nym_id * domain;

// Different domain should produce different NYM
let different_domain_str = format!("ooni.org/{}/{}", "UK", "AS5678");
let different_domain = G::hash_from_bytes::<Sha512>(different_domain_str.as_bytes());
let different_domain = submit_domain_generator("UK", "AS5678");
let different_nym = nym_id * different_domain;

assert_ne!(
Expand Down
38 changes: 35 additions & 3 deletions ooniauth-py/ooniauth_py.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,23 @@ class ServerState:
measurement_hash: str,
age_range: tuple[builtins.int, builtins.int],
min_measurement_count: builtins.int,
) -> str: ...
) -> str:
r"""
Handle a submit request from the client.

Both arguments `probe_cc` and `probe_asn` MUST have the following format:

- probe_cc = two uppercase ASCII letters
- probe_asn = starts with "AS", 3 <= len(probe_asn) <= 12, and the int
part after AS does not have leading 0s

This validation is done in backend server validating measurements.

# Panics

The arguments `probe_cc` and `probe_asn` will cause a panic (in debug mode) if not valid.
"""

def handle_submit_request_with_hash(
self,
nym: str,
Expand All @@ -65,7 +81,7 @@ class ServerState:
r"""
Performs a submission request computing the hash from the input
measurement. Computes the hash internally using the
[submit_measurement_hash] function
[submit_measurement_hash] function.
"""

def handle_update_request(
Expand Down Expand Up @@ -98,7 +114,23 @@ class UserState:
measurement_hash: str,
age_range: tuple[builtins.int, builtins.int],
min_measurement_count: builtins.int,
) -> SubmitRequest: ...
) -> SubmitRequest:
r"""
Make a submission request, to send a measurement to the server

The arguments `probe_cc` and `probe_asn` MUST have the following format:

- probe_cc = two uppercase ASCII letters
- probe_asn = starts with "AS", 3 <= len(probe_asn) <= 12, and the int
part after AS does not have leading 0s

This validation is done in backend server validating measurements.

# Panics

The arguments `probe_cc` and `probe_asn` will cause a panic (in debug mode) if not valid.
"""

def make_submit_request_with_hash(
self,
probe_cc: str,
Expand Down
47 changes: 38 additions & 9 deletions ooniauth-py/src/protocol.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use base64::prelude::*;
use ooniauth_core::registration::open_registration;
use ooniauth_core::submit::submit;
use ooniauth_core::submit::submit_measurement_hash as core_submit_measurement_hash;
use ooniauth_core::update::*;
use ooniauth_core::{self as ooni, PublicParameters, SecretKey};
use ooniauth_core::submit::submit_measurement_hash as core_submit_measurement_hash;

use pyo3::{prelude::*, types::PyString};
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyfunction, gen_stub_pymethods};
Expand Down Expand Up @@ -115,6 +115,19 @@ impl ServerState {
ooni::ServerState::today()
}

/// Handle a submit request from the client.
///
/// Both arguments `probe_cc` and `probe_asn` MUST have the following format:
///
/// - probe_cc = two uppercase ASCII letters
/// - probe_asn = starts with "AS", 3 <= len(probe_asn) <= 12, and the int
/// part after AS does not have leading 0s
///
/// This validation is done in backend server validating measurements.
///
/// # Panics
///
/// The arguments `probe_cc` and `probe_asn` will cause a panic (in debug mode) if not valid.
#[allow(clippy::too_many_arguments)]
fn handle_submit_request(
&self,
Expand Down Expand Up @@ -144,7 +157,7 @@ impl ServerState {

/// Performs a submission request computing the hash from the input
/// measurement. Computes the hash internally using the
/// [submit_measurement_hash] function
/// [submit_measurement_hash] function.
#[allow(clippy::too_many_arguments)]
fn handle_submit_request_with_hash(
&self,
Expand Down Expand Up @@ -301,6 +314,19 @@ impl UserState {
Ok(())
}

/// Make a submission request, to send a measurement to the server
///
/// The arguments `probe_cc` and `probe_asn` MUST have the following format:
///
/// - probe_cc = two uppercase ASCII letters
/// - probe_asn = starts with "AS", 3 <= len(probe_asn) <= 12, and the int
/// part after AS does not have leading 0s
///
/// This validation is done in backend server validating measurements.
///
/// # Panics
///
/// The arguments `probe_cc` and `probe_asn` will cause a panic (in debug mode) if not valid.
pub fn make_submit_request(
&mut self,
py: Python<'_>,
Expand Down Expand Up @@ -454,10 +480,12 @@ mod tests {
#[test]
fn test_submit_measurement_hash_wrapper() {
let measurement = b"measurement:US:AS1234";
let expected = BASE64_STANDARD.encode(ooniauth_core::submit::submit_measurement_hash(
measurement,
));
assert_eq!(crate::submit_measurement_hash(std::str::from_utf8(measurement).unwrap()), expected);
let expected =
BASE64_STANDARD.encode(ooniauth_core::submit::submit_measurement_hash(measurement));
assert_eq!(
crate::submit_measurement_hash(std::str::from_utf8(measurement).unwrap()),
expected
);
}

#[test]
Expand Down Expand Up @@ -530,12 +558,13 @@ mod tests {
pyo3::Python::initialize();
Python::attach(|py| {
let server = crate::ServerState::new();
let mut client =
crate::UserState::new(py, server.get_public_parameters(py)).unwrap();
let mut client = crate::UserState::new(py, server.get_public_parameters(py)).unwrap();

let req = client.make_registration_request(py).unwrap();
let reg_response = server.handle_registration_request(py, req).unwrap();
client.handle_registration_response(py, reg_response).unwrap();
client
.handle_registration_response(py, reg_response)
.unwrap();

let cc: Py<PyString> = PyString::new(py, "VE").into();
let asn: Py<PyString> = PyString::new(py, "AS1234").into();
Expand Down