Skip to main content

confium_privacy/
secure_aggregation.rs

1//! Secure aggregation protocol.
2//!
3//! Google-style secure aggregation: N parties each hold a private
4//! value, and want to compute the SUM without revealing any individual
5//! value. Uses pairwise masking (each pair shares a mask that cancels
6//! when summed).
7
8use getrandom::SysRng;
9use p256::elliptic_curve::rand_core::Rng;
10use p256::elliptic_curve::rand_core::UnwrapErr;
11use std::collections::HashMap;
12
13/// A party in the secure aggregation.
14#[derive(Debug, Clone)]
15pub struct AggregationParty {
16    pub id: u32,
17    pub value: i64,
18    /// Masks shared with other parties: mask[i][j] = random value
19    /// added by i, subtracted by j.
20    pub masks_to_apply: HashMap<u32, i64>,
21}
22
23/// The secure aggregation session.
24#[derive(Debug)]
25pub struct SecureAggregation {
26    pub parties: Vec<AggregationParty>,
27}
28
29impl SecureAggregation {
30    pub fn new(party_count: u32) -> Self {
31        let parties = (0..party_count)
32            .map(|id| AggregationParty {
33                id,
34                value: 0,
35                masks_to_apply: HashMap::new(),
36            })
37            .collect();
38        Self { parties }
39    }
40
41    /// Set each party's private value.
42    pub fn set_value(&mut self, party_id: u32, value: i64) {
43        if let Some(party) = self.parties.iter_mut().find(|p| p.id == party_id) {
44            party.value = value;
45        }
46    }
47
48    /// Establish pairwise masks between all parties. Each pair (i, j)
49    /// generates a shared random mask: i adds it, j subtracts it.
50    /// When summed, masks cancel.
51    pub fn establish_masks(&mut self) {
52        let n = self.parties.len();
53        let mut rng = UnwrapErr(SysRng);
54        for i in 0..n {
55            for j in (i + 1)..n {
56                // Use small masks to avoid i64 overflow
57                let mask = (rng.next_u32() as i64) % 1000;
58                let id_i = self.parties[i].id;
59                let id_j = self.parties[j].id;
60                self.parties[i].masks_to_apply.insert(id_j, mask);
61                self.parties[j].masks_to_apply.insert(id_i, -mask);
62            }
63        }
64    }
65
66    /// Each party computes their masked value: v_i + sum(masks_to_apply).
67    pub fn masked_values(&self) -> Vec<i64> {
68        self.parties
69            .iter()
70            .map(|p| {
71                let mask_sum: i64 = p.masks_to_apply.values().sum();
72                p.value + mask_sum
73            })
74            .collect()
75    }
76
77    /// Aggregate masked values. The result equals the sum of all
78    /// original values (masks cancel).
79    pub fn aggregate(&self) -> i64 {
80        self.masked_values().iter().sum()
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn sum_preserved_with_masks() {
90        let mut agg = SecureAggregation::new(3);
91        agg.set_value(0, 10);
92        agg.set_value(1, 20);
93        agg.set_value(2, 30);
94        agg.establish_masks();
95        assert_eq!(agg.aggregate(), 60);
96    }
97
98    #[test]
99    fn single_party() {
100        let mut agg = SecureAggregation::new(1);
101        agg.set_value(0, 42);
102        agg.establish_masks();
103        assert_eq!(agg.aggregate(), 42);
104    }
105
106    #[test]
107    fn negative_values() {
108        let mut agg = SecureAggregation::new(3);
109        agg.set_value(0, -10);
110        agg.set_value(1, 20);
111        agg.set_value(2, -5);
112        agg.establish_masks();
113        assert_eq!(agg.aggregate(), 5);
114    }
115
116    #[test]
117    fn many_parties() {
118        let n = 10;
119        let mut agg = SecureAggregation::new(n);
120        let total: i64 = (1..=n as i64).sum();
121        for i in 0..n {
122            agg.set_value(i, (i + 1) as i64);
123        }
124        agg.establish_masks();
125        assert_eq!(agg.aggregate(), total);
126    }
127
128    #[test]
129    fn masked_values_hide_individuals() {
130        let mut agg = SecureAggregation::new(2);
131        agg.set_value(0, 100);
132        agg.set_value(1, 200);
133        agg.establish_masks();
134        let masked = agg.masked_values();
135        // Neither masked value should equal the original
136        assert_ne!(masked[0], 100);
137        assert_ne!(masked[1], 200);
138    }
139
140    #[test]
141    fn zero_values() {
142        let mut agg = SecureAggregation::new(3);
143        agg.establish_masks();
144        assert_eq!(agg.aggregate(), 0);
145    }
146
147    #[test]
148    fn masks_symmetric() {
149        let mut agg = SecureAggregation::new(2);
150        agg.establish_masks();
151        // mask from party 0 to 1 should be negation of 1 to 0
152        let m01 = agg.parties[0].masks_to_apply.get(&1).copied().unwrap_or(0);
153        let m10 = agg.parties[1].masks_to_apply.get(&0).copied().unwrap_or(0);
154        assert_eq!(m01, -m10);
155    }
156}