Skip to main content

securedrop_protocol_minimal/server/
challenge.rs

1use crate::challenge::{derive_challenge_key, encrypt_message_id};
2use crate::keys::NR_ID;
3use crate::primitives::provider::constants::{LEN_KMID, LEN_MESSAGE_ID};
4use crate::primitives::ristretto255::{
5    dh_shared_secret, generate_random_scalar, random_dh_public_key,
6};
7use crate::{DHPublicKey, Envelope, FetchResponse};
8use alloc::vec::Vec;
9use rand_core::{CryptoRng, RngCore};
10
11/// Given a set of ciphertext bundles (C, X, Z) and their associated uuid,
12/// compute a fixed-length set of "challenges" >= the number of SeverMessageStore entries.
13/// A challenge is returned as a tuple of DH agreement outputs (or random data tuples of the same length).
14/// For benchmarking purposes, supply the rng as a separable parameter, and allow the total number of expected responses to be specified as a paremeter (worst case performance
15/// when the number of items in the server store approaches num total_responses.)
16///
17/// Note this is marked lax temporaily due to the `push` panic freedom requirement
18#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
19pub fn compute_fetch_challenges<R: RngCore + CryptoRng>(
20    rng: &mut R,
21    entries: &[([u8; LEN_MESSAGE_ID], Envelope)],
22    total_responses: usize,
23) -> Vec<FetchResponse> {
24    let mut responses = Vec::with_capacity(total_responses);
25
26    // Generate two random DH public keys to use to generate the padded responses. Sampling two independently at random mimics the distribution of those sent by the client.
27    let decoy_mgdh = random_dh_public_key(rng);
28    let decoy_mgdh_pubkey = random_dh_public_key(rng);
29
30    for (message_id, envelope) in entries.iter() {
31        if responses.len() < total_responses {
32            responses.push(make_response(
33                rng,
34                message_id,
35                &envelope.mgdh,
36                &envelope.mgdh_pubkey,
37                NR_ID,
38            ));
39        }
40    }
41
42    // Pad if needed to return fixed length of responses
43    while responses.len() < total_responses {
44        responses.push(make_response(
45            rng,
46            &[0u8; LEN_MESSAGE_ID],
47            &decoy_mgdh,
48            &decoy_mgdh_pubkey,
49            NR_ID,
50        ));
51    }
52    responses
53}
54
55/// Note this is marked lax temporaily due to the `.expect()` panic freedom requirement
56#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
57fn make_response<R: RngCore + CryptoRng>(
58    rng: &mut R,
59    message_id: &[u8; LEN_MESSAGE_ID],
60    mgdh: &DHPublicKey,
61    mgdh_pubkey: &DHPublicKey,
62    nr_id: &[u8],
63) -> FetchResponse {
64    // Generate ephemeral scalar r, fresh per message
65    let eph_sk = generate_random_scalar(&mut *rng);
66
67    // 3-party DH yields shared_secret used to encrypt message_id
68    let shared_secret = dh_shared_secret(mgdh, &eph_sk);
69    let key = derive_challenge_key(&shared_secret, nr_id).unwrap();
70    let enc_mid = encrypt_message_id(&key, message_id).unwrap();
71
72    // `copy_from_slice` rather than `try_into()`: Core_models has no
73    // `TryInto<Vec<u8>, [u8; N]>` instance, and this is the codebase's
74    // Vec->array idiom. (Lengths must match; covered by `lax`.)
75    let mut kmid = [0u8; LEN_KMID];
76    kmid.copy_from_slice(&enc_mid);
77
78    // 2-party DH yields per-request clue (pmgdh) used by intended recipient
79    // to compute shared_secret
80    let pmgdh = dh_shared_secret(mgdh_pubkey, &eph_sk);
81
82    FetchResponse {
83        enc_id: kmid,
84        pmgdh,
85    }
86}