#隐私 #分析 #协议 #数据隐私 #数据分析

star-constellation

基于 STAR 协议构建的嵌套阈值聚合

3 个版本

0.2.3 2023 年 11 月 27 日
0.2.2 2023 年 9 月 14 日
0.2.1 2023 年 8 月 3 日

#1099 in 密码学

MPL-2.0 许可证

71KB
1.5K SLoC

星宿星座

实现星宿星座阈值聚合机制的 Rust 库。它允许客户端以可能最高的分辨率提交有序、细粒度数据,同时保持基于群体的匿名性。接收服务器只能解码那些内容也由至少一定数量其他客户端提交的消息,阻止对独特行为的识别。

星宿星座是 嵌套 版本的 STAR 协议,并且这个库使用了 sta-rs Rust 实现。

免责声明

警告:此库尚未经过审计,使用风险自负!此代码正在积极开发中,未来版本可能会有重大变化。

快速入门

构建 & 测试

cargo build
cargo test

lib.rs:

star-constellation 包实现了星宿星座聚合机制:对原始 STAR 协议的修改,允许客户端以可能最高的分辨率提交有序、细粒度数据,同时保持基于群体的匿名性。

星宿星座不仅比 STAR 提供更高的数据聚合效用(在可能的情况下揭示部分度量),而且为细粒度客户端数据提供了更好的隐私。

背景

具体来说,星宿星座将有序的度量向量“嵌套”或“分层”到相关的 STAR 消息中,使得只有当上一层的 STAR 消息成功恢复时才能访问每个消息。未揭示的层的隐私是通过对称加密提供的,该加密只能使用封装在上一 STAR 消息中的密钥进行解密。

示例 API 使用

客户端

客户端使用星宿星座格式生成一个用于阈值聚合的消息。

#
let threshold = 10;
let epoch = 0u8;
let random_fetcher = RandomnessFetcher::new();

// setup randomness server information
let example_aux = vec![1u8; 3];

let measurements = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()];
let rrs = client::prepare_measurement(&measurements, epoch).unwrap();
let req = client::construct_randomness_request(&rrs);

let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();

let points_slice_vec: Vec<&[u8]> =
  resp.serialized_points.iter().map(|v| v.as_slice()).collect();
let proofs_slice_vec: Vec<&[u8]> =
  resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
client::construct_message(
  &points_slice_vec,
  Some(&proofs_slice_vec),
  &rrs,
  &Some(random_fetcher.get_server().get_public_key()),
  &example_aux,
  threshold
).unwrap();

服务器

服务器聚合接收多个客户端消息作为输入,并输出那些至少来自 threshold 个客户端的度量。它还揭示了来自超过 threshold 个客户端的完整度量的前缀。

完整恢复

在接收到至少 threshold 个相同的完整测量值后,服务器可以执行聚合并揭示客户端测量值。

#
let threshold = 10;
let epoch = 0u8;
let random_fetcher = RandomnessFetcher::new();

// construct at least `threshold` client messages with the same measurement
let measurements_1 = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()];
let client_messages_to_reveal: Vec<Vec<u8>> = (0..threshold).into_iter().map(|i| {
  let example_aux = vec![i as u8; 3];
  let rrs = client::prepare_measurement(&measurements_1, epoch).unwrap();
  let req = client::construct_randomness_request(&rrs);

  let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
  let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();

  let points_slice_vec: Vec<&[u8]> =
    resp.serialized_points.iter().map(|v| v.as_slice()).collect();
  let proofs_slice_vec: Vec<&[u8]> =
    resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
  client::construct_message(
    &points_slice_vec,
    Some(&proofs_slice_vec),
    &rrs,
    &Some(random_fetcher.get_server().get_public_key()),
    &example_aux,
    threshold
  ).unwrap()
}).collect();

// construct a low number client messages with a different measurement
let measurements_2 = vec!["something".as_bytes().to_vec(), "else".as_bytes().to_vec()];
let client_messages_to_hide: Vec<Vec<u8>> = (0..2).into_iter().map(|i| {
  let example_aux = vec![i as u8; 3];
  let rrs = client::prepare_measurement(&measurements_2, epoch).unwrap();
  let req = client::construct_randomness_request(&rrs);

  let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
  let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();

  let points_slice_vec: Vec<&[u8]> =
    resp.serialized_points.iter().map(|v| v.as_slice()).collect();
  let proofs_slice_vec: Vec<&[u8]> =
    resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
  client::construct_message(
    &points_slice_vec,
    Some(&proofs_slice_vec),
    &rrs,
    &Some(random_fetcher.get_server().get_public_key()),
    &example_aux,
    threshold
  ).unwrap()
}).collect();

// aggregation reveals the client measurement that reaches the
// threshold, the other measurement stays hidden
let agg_res = server::aggregate(
  &[client_messages_to_reveal, client_messages_to_hide].concat(),
  threshold,
  epoch,
  measurements_1.len()
);
let output = agg_res.outputs();
assert_eq!(output.len(), 1);
let revealed_output = output.iter().find(|v| v.value() == vec!["world"]).unwrap();
assert_eq!(revealed_output.value(), vec!["world"]);
assert_eq!(revealed_output.occurrences(), 10);
(0..10).into_iter().for_each(|i| {
  assert_eq!(revealed_output.auxiliary_data()[i], vec![i as u8; 3]);
});

部分恢复

部分恢复允许在完整测量值本身保持隐藏的情况下,揭示足够客户端接收到的完整测量的前缀。

#
let threshold = 10;
let epoch = 0u8;
let random_fetcher = RandomnessFetcher::new();

// construct a low number client messages with the same measurement
let measurements_1 = vec!["hello".as_bytes().to_vec(), "world".as_bytes().to_vec()];
let client_messages_1: Vec<Vec<u8>> = (0..5).into_iter().map(|i| {
  let example_aux = vec![i as u8; 3];
  let rrs = client::prepare_measurement(&measurements_1, epoch).unwrap();
  let req = client::construct_randomness_request(&rrs);

  let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
  let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();

  let points_slice_vec: Vec<&[u8]> =
    resp.serialized_points.iter().map(|v| v.as_slice()).collect();
  let proofs_slice_vec: Vec<&[u8]> =
    resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
  client::construct_message(
    &points_slice_vec,
    Some(&proofs_slice_vec),
    &rrs,
    &Some(random_fetcher.get_server().get_public_key()),
    &example_aux,
    threshold
  ).unwrap()
}).collect();

// construct a low number of measurements that also share a prefix
let measurements_2 = vec!["hello".as_bytes().to_vec(), "goodbye".as_bytes().to_vec()];
let client_messages_2: Vec<Vec<u8>> = (0..5).into_iter().map(|i| {
  let example_aux = vec![i as u8; 3];
  let rrs = client::prepare_measurement(&measurements_2, epoch).unwrap();
  let req = client::construct_randomness_request(&rrs);

  let req_slice_vec: Vec<&[u8]> = req.iter().map(|v| v.as_slice()).collect();
  let resp = random_fetcher.eval(&req_slice_vec, epoch).unwrap();

  let points_slice_vec: Vec<&[u8]> =
    resp.serialized_points.iter().map(|v| v.as_slice()).collect();
  let proofs_slice_vec: Vec<&[u8]> =
    resp.serialized_proofs.iter().map(|v| v.as_slice()).collect();
  client::construct_message(
    &points_slice_vec,
    Some(&proofs_slice_vec),
    &rrs,
    &Some(random_fetcher.get_server().get_public_key()),
    &example_aux,
    threshold
  ).unwrap()
}).collect();

// aggregation reveals the partial client measurement `vec!["hello"]`,
// but the full measurements stay hidden
let agg_res = server::aggregate(
  &[client_messages_1, client_messages_2].concat(),
  threshold,
  epoch,
  measurements_1.len()
);
let output = agg_res.outputs();
assert_eq!(output.len(), 1);
assert_eq!(output[0].value(), vec!["hello"]);
assert_eq!(output[0].occurrences(), 10);
(0..10).into_iter().for_each(|i| {
  let val = i % 5;
  assert_eq!(output[0].auxiliary_data()[i], vec![val as u8; 3]);
});

依赖项

~4.5–6MB
~132K SLoC