Skip to main content

confium_privacy/
distributed_prf.rs

1//! Distributed pseudorandom function (PRF).
2//!
3//! Each party contributes a PRF share; the combined output is the
4//! XOR of partial evaluations. Used for threshold key derivation
5//! without reconstructing the underlying secret.
6
7use hmac::{Hmac, KeyInit, Mac};
8use serde::{Deserialize, Serialize};
9use sha2::Sha256;
10
11type HmacSha256 = Hmac<Sha256>;
12
13/// A PRF share: a key that can evaluate the PRF on inputs.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PrfShare {
16    pub key: Vec<u8>,
17}
18
19impl PrfShare {
20    /// Evaluate the PRF on a given input. Returns a 32-byte output.
21    pub fn evaluate(&self, input: &[u8]) -> [u8; 32] {
22        let mut mac = HmacSha256::new_from_slice(&self.key).expect("HMAC key");
23        mac.update(input);
24        let result = mac.finalize().into_bytes();
25        let mut out = [0u8; 32];
26        let len = result.len().min(32);
27        out[..len].copy_from_slice(&result[..len]);
28        out
29    }
30}
31
32/// Result of a distributed PRF computation.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DistributedPrfResult {
35    pub output: [u8; 32],
36}
37
38/// Generate PRF shares by splitting a secret into T-of-N shares.
39/// Each share is independently usable to compute partial PRF output.
40pub fn distribute(secret: &[u8], threshold: usize, party_count: usize) -> Vec<PrfShare> {
41    // XOR-based secret sharing: generate N-1 random shares, then
42    // derive the last share so that XOR of all shares = secret.
43    let mut shares = Vec::with_capacity(party_count);
44    let mut xor_accum = vec![0u8; secret.len()];
45
46    // Generate party_count - 1 random shares
47    for _ in 0..(party_count.saturating_sub(1)) {
48        let mut rand_share = vec![0u8; secret.len()];
49        for (i, byte) in rand_share.iter_mut().enumerate() {
50            *byte = rand_byte();
51            xor_accum[i] ^= *byte;
52        }
53        shares.push(PrfShare { key: rand_share });
54    }
55
56    // Last share: ensures XOR of all shares equals secret
57    let last: Vec<u8> = secret
58        .iter()
59        .zip(xor_accum.iter())
60        .map(|(s, x)| s ^ x)
61        .collect();
62    shares.push(PrfShare { key: last });
63
64    let _ = threshold; // threshold is used at evaluation time
65    shares
66}
67
68/// Combine partial PRF outputs (XOR) to get the final output.
69pub fn combine(partials: &[[u8; 32]]) -> [u8; 32] {
70    let mut result = [0u8; 32];
71    for partial in partials {
72        for (i, &b) in partial.iter().enumerate() {
73            result[i] ^= b;
74        }
75    }
76    result
77}
78
79/// Compute the distributed PRF: each party evaluates, then combine.
80pub fn evaluate(
81    shares: &[PrfShare],
82    input: &[u8],
83    threshold: usize,
84) -> Result<DistributedPrfResult, String> {
85    if shares.len() < threshold {
86        return Err(format!("need {threshold} shares, got {}", shares.len()));
87    }
88    let partials: Vec<[u8; 32]> = shares[..threshold]
89        .iter()
90        .map(|s| s.evaluate(input))
91        .collect();
92    let output = combine(&partials);
93    Ok(DistributedPrfResult { output })
94}
95
96fn rand_byte() -> u8 {
97    use rand_core::{OsRng, RngCore};
98    let mut b = [0u8; 1];
99    OsRng.fill_bytes(&mut b);
100    b[0]
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn share_and_evaluate() {
109        let secret = b"my secret key";
110        let shares = distribute(secret, 3, 5);
111        let result = evaluate(&shares, b"input", 3).unwrap();
112        assert_eq!(result.output.len(), 32);
113    }
114
115    #[test]
116    fn insufficient_shares_rejected() {
117        let secret = b"key";
118        let shares = distribute(secret, 3, 5);
119        assert!(evaluate(&shares[0..2], b"input", 3).is_err());
120    }
121
122    #[test]
123    fn deterministic_evaluation() {
124        let secret = b"key";
125        let shares = distribute(secret, 2, 3);
126        let r1 = evaluate(&shares, b"in", 2).unwrap();
127        let r2 = evaluate(&shares, b"in", 2).unwrap();
128        assert_eq!(r1, r2);
129    }
130
131    #[test]
132    fn different_inputs_different_outputs() {
133        let secret = b"key";
134        let shares = distribute(secret, 2, 3);
135        let r1 = evaluate(&shares, b"input1", 2).unwrap();
136        let r2 = evaluate(&shares, b"input2", 2).unwrap();
137        assert_ne!(r1, r2);
138    }
139
140    #[test]
141    fn combine_xors_correctly() {
142        let a = [1u8; 32];
143        let b = [2u8; 32];
144        let result = combine(&[a, b]);
145        for r in &result {
146            assert_eq!(*r, 1 ^ 2);
147        }
148    }
149}