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}