sta_rs/
lib.rs

1//! This module provides the implementation of the STAR (distributed
2//! Secret-sharing for Threshold AggRegation of data) protocol. The STAR
3//! protocol provides the ability for clients to report secret
4//! measurements to servers, whilst maintaining k-anonymity-like
5//! guarantees.
6//!
7//! In essence, such measurements are only revealed if a `threshold`
8//! number of clients all send the same message. Clients are permitted
9//! to also send relevant, arbitrary associated data that can also be
10//! revealed.
11//!
12//! In STAR, clients derive randomness from a separate server that
13//! implements a puncturable partially oblivious pseudorandom function
14//! (PPOPRF) protocol. In STARLite, clients derive randomness used for
15//! hiding their measurements locally from the measurement itself. The
16//! PPOPRF protocol takes in the client measurement, a server secret
17//! key, and the current epoch metadata tag as input, and outputs a
18//! random (deterministic) value.
19//!
20//! In the case of STARLite, the design is simpler than in STAR, but
21//! security is ONLY maintained in the case where client measurements
22//! are sampled from a high-entropy domain. In the case of STAR, client
23//! security guarantees hold even for low-entropy inputs, as long as the
24//! randomness is only revealed after the epoch metadata tag has been
25//! punctured from the randomness server's secret key.
26//!
27//! See the [full paper](https://arxiv.org/abs/2109.10074) for more
28//! details.
29//!
30//! # Example (client)
31//!
32//! The following example shows how to generate a message triple of `(ciphertext,
33//! share, tag)`. This message can then be sent to the aggregation server.
34//!
35//! ```
36//! # use sta_rs::*;
37//! # let threshold = 2;
38//! # let epoch = "t";
39//! let measurement = SingleMeasurement::new("hello world".as_bytes());
40//! let mg = MessageGenerator::new(measurement, threshold, epoch.as_bytes());
41//! let mut rnd = [0u8; 32];
42//! // NOTE: this is for STARLite. Randomness must be sampled from a
43//! // randomness server in order to implement the full STAR protocol.
44//! mg.sample_local_randomness(&mut rnd);
45//!
46//! let Message {
47//!   ciphertext,
48//!   share,
49//!   tag,
50//! } = Message::generate(&mg, &mut rnd, None)
51//!     .expect("Could not generate message triplet");
52//! ```
53//! # Example (WASM client)
54//!
55//! The following example shows how to generate a triple of `(key,
56//! share, tag)` for each client in the STARLite protocol, which is used
57//! in the existing WASM integration. The STAR protocol is not yet
58//! supported.
59//!
60//! In the WASM integration the `key` MUST then be used to encrypt the
61//! measurement and associated data into a `ciphertext` in the
62//! higher-level application. The message triple `(ciphertext, share,
63//! tag)` is then sent to the server.
64//!
65//! ```
66//! # use sta_rs::*;
67//! # let threshold = 2;
68//! # let epoch = "t";
69//! let measurement = SingleMeasurement::new("hello world".as_bytes());
70//! let mg = MessageGenerator::new(measurement, threshold, epoch.as_bytes());
71//! let mut rnd = [0u8; 32];
72//! // NOTE: this is for STARLite. Randomness must be sampled from a
73//! // randomness server in order to implement the full STAR protocol.
74//! mg.sample_local_randomness(&mut rnd);
75//! let WASMSharingMaterial {
76//!   key,
77//!   share,
78//!   tag,
79//! } = mg.share_with_local_randomness().unwrap();
80//! ```
81//!
82//! # Example (server)
83//!
84//! Once over `threshold` shares are recovered from clients, it is
85//! possible to recover the randomness encoded in each of the shares
86//!
87//! ```
88//! # use sta_rs::*;
89//! # use star_test_utils::*;
90//! # let mut messages = Vec::new();
91//! # let threshold = 2;
92//! # let epoch = "t";
93//! # let measurement = SingleMeasurement::new("hello world".as_bytes());
94//!
95//! # let mg = MessageGenerator::new(measurement, threshold, epoch.as_bytes());
96//! # for i in 0..3 {
97//! #     let mut rnd = [0u8; 32];
98//! #     mg.sample_local_randomness(&mut rnd);
99//! #     messages.push(Message::generate(&mg, &mut rnd, None).unwrap());
100//! # }
101//! # let shares: Vec<Share> = messages.iter().map(|triple| triple.share.clone()).collect();
102//! let value = share_recover(&shares).unwrap().get_message();
103//!
104//! // derive key for decrypting payload data in client message
105//! let mut enc_key = vec![0u8; 16];
106//! derive_ske_key(&value, epoch.as_bytes(), &mut enc_key);
107//! ```
108use std::error::Error;
109use std::str;
110
111use rand::Rng;
112mod strobe_rng;
113use strobe_rng::StrobeRng;
114use strobe_rs::{SecParam, Strobe};
115use zeroize::{Zeroize, ZeroizeOnDrop};
116
117use adss::{recover, Commune};
118pub use {adss::load_bytes, adss::store_bytes, adss::Share as InternalShare};
119
120#[cfg(feature = "star2")]
121use ppoprf::ppoprf::{end_to_end_evaluation, Server as PPOPRFServer};
122
123pub const AES_BLOCK_LEN: usize = 24;
124pub const DIGEST_LEN: usize = 32;
125
126// A `Measurement` provides the wrapper for a client-generated value in
127// the STAR protocol that is later aggregated and processed at the
128// server-side. Measurements are only revealed on the server-side if the
129// `threshold` is met, in terms of clients that send the same
130// `Measurement` value.
131#[derive(Clone, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
132pub struct SingleMeasurement(Vec<u8>);
133impl SingleMeasurement {
134  pub fn new(x: &[u8]) -> Self {
135    Self(x.to_vec())
136  }
137
138  pub fn as_slice(&self) -> &[u8] {
139    self.0.as_slice()
140  }
141
142  pub fn as_vec(&self) -> Vec<u8> {
143    self.0.clone()
144  }
145
146  pub fn byte_len(&self) -> usize {
147    self.0.len()
148  }
149
150  pub fn is_empty(&self) -> bool {
151    self.0.is_empty()
152  }
153}
154
155impl From<&str> for SingleMeasurement {
156  fn from(s: &str) -> Self {
157    SingleMeasurement::new(s.as_bytes())
158  }
159}
160
161// The `AssociatedData` struct wraps the arbitrary data that a client
162// can encode in its message to the `Server`. Such data is also only
163// revealed in the case that the `threshold` is met.
164#[derive(Debug)]
165pub struct AssociatedData(Vec<u8>);
166impl AssociatedData {
167  pub fn new(buf: &[u8]) -> Self {
168    Self(buf.to_vec())
169  }
170
171  pub fn as_slice(&self) -> &[u8] {
172    self.0.as_slice()
173  }
174
175  pub fn as_vec(&self) -> Vec<u8> {
176    self.0.clone()
177  }
178}
179impl From<&str> for AssociatedData {
180  fn from(s: &str) -> Self {
181    AssociatedData::from(s.as_bytes())
182  }
183}
184impl From<&[u8]> for AssociatedData {
185  fn from(buf: &[u8]) -> Self {
186    AssociatedData::new(buf)
187  }
188}
189
190// Wrapper type for `adss::Share` to implement `ZeroizeOnDrop`properly.
191#[derive(Clone, Debug, PartialEq, Eq, Zeroize)]
192pub struct Share(InternalShare);
193impl Share {
194  pub fn to_bytes(&self) -> Vec<u8> {
195    self.0.to_bytes()
196  }
197
198  pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
199    Some(Self(InternalShare::from_bytes(bytes)?))
200  }
201}
202impl Drop for Share {
203  fn drop(&mut self) {
204    self.0.zeroize();
205  }
206}
207
208// The `Ciphertext` struct holds the symmetrically encrypted data that
209// corresponds to the concatenation of `Measurement` and any optional
210// `AssociatedData`.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct Ciphertext {
213  bytes: Vec<u8>,
214}
215impl Ciphertext {
216  pub fn new(enc_key_buf: &[u8], data: &[u8], label: &str) -> Self {
217    let mut s = Strobe::new(label.as_bytes(), SecParam::B128);
218    s.key(enc_key_buf, false);
219    let mut x = vec![0u8; data.len()];
220    x.copy_from_slice(data);
221    s.send_enc(&mut x, false);
222
223    Self { bytes: x.to_vec() }
224  }
225
226  pub fn decrypt(&self, enc_key_buf: &[u8], label: &str) -> Vec<u8> {
227    let mut s = Strobe::new(label.as_bytes(), SecParam::B128);
228    s.key(enc_key_buf, false);
229    let mut m = vec![0u8; self.bytes.len()];
230    m.copy_from_slice(&self.bytes);
231    s.recv_enc(&mut m, false);
232    m
233  }
234
235  pub fn to_bytes(&self) -> Vec<u8> {
236    self.bytes.clone()
237  }
238
239  pub fn from_bytes(bytes: &[u8]) -> Ciphertext {
240    Self {
241      bytes: bytes.to_vec(),
242    }
243  }
244}
245impl From<Vec<u8>> for Ciphertext {
246  fn from(bytes: Vec<u8>) -> Self {
247    Self { bytes }
248  }
249}
250
251// A `Message` is the message that a client sends to the server during
252// the STAR protocol. Consisting of a `Ciphertext`, a `Share`, and a
253// `tag`. The `Ciphertext`can only be decrypted if a `threshold` number
254// of clients possess the same measurement.
255//
256// This struct should only be used by applications that do not perform
257// encryption at the higher application-levels.
258#[derive(Clone, Debug, PartialEq, Eq)]
259pub struct Message {
260  pub ciphertext: Ciphertext,
261  pub share: Share,
262  pub tag: Vec<u8>,
263}
264impl Message {
265  fn new(c: Ciphertext, share: Share, tag: &[u8]) -> Self {
266    Self {
267      ciphertext: c,
268      share,
269      tag: tag.to_vec(),
270    }
271  }
272
273  // Generates a message that is used in the aggregation phase
274  pub fn generate(
275    mg: &MessageGenerator,
276    rnd: &[u8; 32],
277    aux: Option<AssociatedData>,
278  ) -> Result<Self, Box<dyn Error>> {
279    let r = mg.derive_random_values(rnd);
280
281    // key is then used for encrypting measurement and associated
282    // data
283    let key = mg.derive_key(&r[0]);
284    let share = mg.share(&r[0], &r[1])?;
285    let tag = r[2];
286
287    let mut data: Vec<u8> = Vec::new();
288    store_bytes(mg.x.as_slice(), &mut data);
289    if let Some(ad) = aux {
290      store_bytes(ad.as_slice(), &mut data);
291    }
292    let ciphertext = Ciphertext::new(&key, &data, "star_encrypt");
293
294    Ok(Message::new(ciphertext, share, &tag))
295  }
296
297  pub fn to_bytes(&self) -> Vec<u8> {
298    let mut out: Vec<u8> = Vec::new();
299
300    // ciphertext: Ciphertext
301    store_bytes(&self.ciphertext.to_bytes(), &mut out);
302
303    // share: Share
304    store_bytes(&self.share.to_bytes(), &mut out);
305
306    // tag: Vec<u8>
307    store_bytes(&self.tag, &mut out);
308
309    out
310  }
311
312  pub fn from_bytes(bytes: &[u8]) -> Option<Message> {
313    let mut slice = bytes;
314
315    // ciphertext: Ciphertext
316    let cb = load_bytes(slice)?;
317    let ciphertext = Ciphertext::from_bytes(cb);
318    slice = &slice[4 + cb.len()..];
319
320    // share: Share
321    let sb = load_bytes(slice)?;
322    let share = Share::from_bytes(sb)?;
323    slice = &slice[4 + sb.len()..];
324
325    // tag: Vec<u8>
326    let tag = load_bytes(slice)?;
327
328    Some(Message {
329      ciphertext,
330      share,
331      tag: tag.to_vec(),
332    })
333  }
334}
335
336// The `WASMSharingMaterial` consists of all data that is passed to
337// higher-level applications using the star-wasm API. This allows
338// encrypting and sending the client measurements in higher-level
339// implementations of the STAR protocol.
340#[derive(Zeroize)]
341pub struct WASMSharingMaterial {
342  /// 16-byte AES encryption key
343  pub key: [u8; 16],
344  /// Secret share of key derivation randomness
345  pub share: Share,
346  /// 32-byte random tag associated with client measurement
347  pub tag: [u8; 32],
348}
349
350// In the STAR protocol, the `MessageGenerator` is the entity which
351// samples and sends `Measurement` to the `AggregationServer`. The
352// measurements will only be revealed if a `threshold` number of
353// MessageGenerators send the same encoded `Measurement` value.
354//
355// Note that the `MessageGenerator` struct holds all of the public
356// protocol parameters, the secret `Measurement` and `AssociatedData`
357// objects, and where randomness should be sampled from.
358//
359// In the STARLite protocol, the `MessageGenerator` samples randomness
360// locally: derived straight from the `Measurement` itself. In the STAR
361// protocol, the `MessageGenerator` derives its randomness from an
362// exchange with a specifically-defined server that runs a POPRF.
363#[derive(Zeroize, ZeroizeOnDrop)]
364pub struct MessageGenerator {
365  pub x: SingleMeasurement,
366  threshold: u32,
367  epoch: Vec<u8>,
368}
369impl MessageGenerator {
370  pub fn new(x: SingleMeasurement, threshold: u32, epoch: &[u8]) -> Self {
371    Self {
372      x,
373      threshold,
374      epoch: epoch.into(),
375    }
376  }
377
378  // Share with OPRF randomness (STARLite)
379  pub fn share_with_local_randomness(
380    &self,
381  ) -> Result<WASMSharingMaterial, Box<dyn Error>> {
382    let mut rnd = vec![0u8; 32];
383    self.sample_local_randomness(&mut rnd);
384    let r = self.derive_random_values(&rnd);
385
386    // key is then used for encrypting measurement and associated
387    // data
388    let key = self.derive_key(&r[0]);
389    let share = self.share(&r[0], &r[1])?;
390    let tag = r[2];
391    Ok(WASMSharingMaterial { key, share, tag })
392  }
393
394  #[cfg(feature = "star2")]
395  // Share with OPRF randomness (STAR)
396  pub fn share_with_oprf_randomness(
397    &self,
398    oprf_server: &PPOPRFServer,
399  ) -> WASMSharingMaterial {
400    let mut rnd = vec![0u8; 32];
401    self.sample_oprf_randomness(oprf_server, &mut rnd);
402    let r = self.derive_random_values(&rnd);
403
404    // key is then used for encrypting measurement and associated
405    // data
406    let key = self.derive_key(&r[0]);
407    let share = self.share(&r[0], &r[1]);
408    let tag = r[2].clone();
409    WASMSharingMaterial { key, share, tag }
410  }
411
412  fn derive_random_values(&self, randomness: &[u8]) -> Vec<[u8; 32]> {
413    let mut output = Vec::new();
414    for i in 0..3 {
415      let mut to_fill = [0u8; 32];
416      strobe_digest(
417        randomness,
418        &[&[i as u8]],
419        "star_derive_randoms",
420        &mut to_fill,
421      );
422      output.push(to_fill);
423    }
424    output
425  }
426
427  fn derive_key(&self, r1: &[u8]) -> [u8; 16] {
428    let mut enc_key = [0u8; 16];
429    derive_ske_key(r1, &self.epoch, &mut enc_key);
430    enc_key
431  }
432
433  fn share(&self, r1: &[u8], r2: &[u8]) -> Result<Share, Box<dyn Error>> {
434    let c = Commune::new(self.threshold, r1.to_vec(), r2.to_vec(), None);
435    Ok(Share(c.share()?))
436  }
437
438  pub fn sample_local_randomness(&self, out: &mut [u8]) {
439    if out.len() != DIGEST_LEN {
440      panic!(
441        "Output buffer length ({}) does not match randomness length ({})",
442        out.len(),
443        DIGEST_LEN
444      );
445    }
446    strobe_digest(
447      self.x.as_slice(),
448      &[&self.epoch, &self.threshold.to_le_bytes()],
449      "star_sample_local",
450      out,
451    );
452  }
453
454  #[cfg(feature = "star2")]
455  pub fn sample_oprf_randomness(
456    &self,
457    oprf_server: &PPOPRFServer,
458    out: &mut [u8],
459  ) {
460    let mds = oprf_server.get_valid_metadata_tags();
461    let index = mds.iter().position(|r| r == &self.epoch).unwrap();
462    end_to_end_evaluation(oprf_server, self.x.as_slice(), index, true, out);
463  }
464}
465
466// FIXME can we implement collect trait?
467pub fn share_recover(shares: &[Share]) -> Result<Commune, Box<dyn Error>> {
468  recover(
469    &shares
470      .iter()
471      .map(|share| share.0.clone())
472      .collect::<Vec<InternalShare>>(),
473  )
474}
475
476// The `derive_ske_key` helper function derives symmetric encryption
477// keys that are used for encrypting/decrypting `Ciphertext` objects
478// during the STAR protocol.
479pub fn derive_ske_key(r1: &[u8], epoch: &[u8], key_out: &mut [u8]) {
480  let mut to_fill = vec![0u8; 32];
481  strobe_digest(r1, &[epoch], "star_derive_ske_key", &mut to_fill);
482  key_out.copy_from_slice(&to_fill[..16]);
483}
484
485pub fn strobe_digest(key: &[u8], ad: &[&[u8]], label: &str, out: &mut [u8]) {
486  if out.len() != DIGEST_LEN {
487    panic!(
488      "Output buffer length ({}) does not match intended output length ({})",
489      out.len(),
490      DIGEST_LEN
491    );
492  } else if ad.is_empty() {
493    panic!("No additional data provided");
494  }
495  let mut t = Strobe::new(label.as_bytes(), SecParam::B128);
496  t.key(key, false);
497  for x in ad.iter() {
498    t.ad(x, false);
499  }
500  let mut rng: StrobeRng = t.into();
501  rng.fill(out);
502}