chie_crypto/
commitment.rs

1//! Cryptographic commitments for zero-knowledge proofs and challenge-response.
2//!
3//! This module provides commitment schemes used in the bandwidth proof protocol:
4//! - Hash-based commitments (Pedersen-style)
5//! - Challenge-response protocols
6//! - Proof-of-possession for chunk data
7
8use crate::hash::{Hash, hash, keyed_hash};
9use crate::{PublicKey, SecretKey, SignatureBytes};
10use rand::RngCore;
11use thiserror::Error;
12
13/// Commitment value (256 bits).
14pub type Commitment = Hash;
15
16/// Opening value for a commitment.
17#[derive(Debug, Clone)]
18pub struct CommitmentOpening {
19    /// The committed value.
20    pub value: Vec<u8>,
21    /// Random blinding factor.
22    pub blinding: [u8; 32],
23}
24
25/// Commitment error types.
26#[derive(Debug, Error)]
27pub enum CommitmentError {
28    #[error("Invalid commitment opening")]
29    InvalidOpening,
30
31    #[error("Commitment verification failed")]
32    VerificationFailed,
33
34    #[error("Invalid proof")]
35    InvalidProof,
36}
37
38/// Create a cryptographic commitment to a value.
39///
40/// Uses a hash-based commitment scheme: C = H(value || blinding)
41pub fn commit(value: &[u8]) -> (Commitment, CommitmentOpening) {
42    let mut blinding = [0u8; 32];
43    rand::thread_rng().fill_bytes(&mut blinding);
44
45    let mut data = Vec::with_capacity(value.len() + 32);
46    data.extend_from_slice(value);
47    data.extend_from_slice(&blinding);
48
49    let commitment = hash(&data);
50
51    let opening = CommitmentOpening {
52        value: value.to_vec(),
53        blinding,
54    };
55
56    (commitment, opening)
57}
58
59/// Verify a commitment opening.
60pub fn verify_commitment(
61    commitment: &Commitment,
62    opening: &CommitmentOpening,
63) -> Result<(), CommitmentError> {
64    let mut data = Vec::with_capacity(opening.value.len() + 32);
65    data.extend_from_slice(&opening.value);
66    data.extend_from_slice(&opening.blinding);
67
68    let computed = hash(&data);
69
70    if &computed == commitment {
71        Ok(())
72    } else {
73        Err(CommitmentError::VerificationFailed)
74    }
75}
76
77/// Proof of possession for chunk data.
78///
79/// This proves that a node has access to the actual chunk data
80/// without revealing the data itself.
81#[derive(Debug, Clone)]
82pub struct ChunkPossessionProof {
83    /// Challenge nonce used in the proof.
84    pub challenge: [u8; 32],
85    /// Response: HMAC(chunk_data, challenge).
86    pub response: Hash,
87}
88
89impl ChunkPossessionProof {
90    /// Generate a proof of possession for chunk data.
91    ///
92    /// # Arguments
93    /// * `chunk_data` - The actual chunk data to prove possession of
94    /// * `challenge` - Challenge nonce from the requester
95    pub fn generate(chunk_data: &[u8], challenge: &[u8; 32]) -> Self {
96        // Response is keyed hash: H_k(chunk_data) where k = challenge
97        let response = keyed_hash(challenge, chunk_data);
98
99        Self {
100            challenge: *challenge,
101            response,
102        }
103    }
104
105    /// Verify the proof of possession.
106    ///
107    /// # Arguments
108    /// * `chunk_data` - The chunk data to verify against
109    pub fn verify(&self, chunk_data: &[u8]) -> Result<(), CommitmentError> {
110        let expected = keyed_hash(&self.challenge, chunk_data);
111
112        if expected == self.response {
113            Ok(())
114        } else {
115            Err(CommitmentError::InvalidProof)
116        }
117    }
118}
119
120/// Challenge for requesting proof of chunk possession.
121#[derive(Debug, Clone)]
122pub struct ChunkChallenge {
123    /// Random challenge nonce.
124    pub nonce: [u8; 32],
125    /// Chunk index being challenged.
126    pub chunk_index: u64,
127    /// Expected chunk hash (for verification).
128    pub expected_hash: Hash,
129}
130
131impl ChunkChallenge {
132    /// Create a new chunk challenge.
133    pub fn new(chunk_index: u64, expected_hash: Hash) -> Self {
134        let mut nonce = [0u8; 32];
135        rand::thread_rng().fill_bytes(&mut nonce);
136
137        Self {
138            nonce,
139            chunk_index,
140            expected_hash,
141        }
142    }
143
144    /// Verify a chunk possession proof against this challenge.
145    pub fn verify_proof(
146        &self,
147        chunk_data: &[u8],
148        proof: &ChunkPossessionProof,
149    ) -> Result<(), CommitmentError> {
150        // Check the chunk hash matches
151        let chunk_hash = hash(chunk_data);
152        if chunk_hash != self.expected_hash {
153            return Err(CommitmentError::InvalidProof);
154        }
155
156        // Check the nonce matches
157        if proof.challenge != self.nonce {
158            return Err(CommitmentError::InvalidProof);
159        }
160
161        // Verify the proof
162        proof.verify(chunk_data)
163    }
164}
165
166/// Bandwidth proof commitment for challenge-response protocol.
167///
168/// Used in the CHIE bandwidth proof protocol to prevent cheating.
169#[derive(Debug, Clone)]
170pub struct BandwidthProofCommitment {
171    /// Commitment to the chunk data.
172    pub chunk_commitment: Commitment,
173    /// Timestamp of commitment.
174    pub timestamp: i64,
175    /// Chunk index.
176    pub chunk_index: u64,
177}
178
179impl BandwidthProofCommitment {
180    /// Create a commitment to chunk data before transfer.
181    pub fn new(chunk_data: &[u8], chunk_index: u64, timestamp: i64) -> (Self, CommitmentOpening) {
182        let (commitment, opening) = commit(chunk_data);
183
184        let bw_commitment = Self {
185            chunk_commitment: commitment,
186            timestamp,
187            chunk_index,
188        };
189
190        (bw_commitment, opening)
191    }
192
193    /// Verify the commitment against the actual chunk data.
194    pub fn verify(
195        &self,
196        opening: &CommitmentOpening,
197        expected_chunk_data: &[u8],
198    ) -> Result<(), CommitmentError> {
199        // Verify the commitment opening
200        verify_commitment(&self.chunk_commitment, opening)?;
201
202        // Verify the opened value matches expected chunk data
203        if opening.value != expected_chunk_data {
204            return Err(CommitmentError::VerificationFailed);
205        }
206
207        Ok(())
208    }
209}
210
211/// Proof-of-possession for a signing key (proves knowledge of secret key).
212#[derive(Debug, Clone)]
213pub struct KeyPossessionProof {
214    /// Public key being proven.
215    pub public_key: PublicKey,
216    /// Challenge nonce.
217    pub challenge: [u8; 32],
218    /// Signature of challenge.
219    pub signature: SignatureBytes,
220}
221
222impl KeyPossessionProof {
223    /// Generate a proof of possession for a signing key.
224    pub fn generate(
225        secret_key: &SecretKey,
226        challenge: &[u8; 32],
227    ) -> Result<Self, crate::SigningError> {
228        use crate::signing::KeyPair;
229
230        let keypair = KeyPair::from_secret_key(secret_key)?;
231        let public_key = keypair.public_key();
232        let signature = keypair.sign(challenge);
233
234        Ok(Self {
235            public_key,
236            challenge: *challenge,
237            signature,
238        })
239    }
240
241    /// Verify the proof of possession.
242    pub fn verify(&self) -> Result<(), CommitmentError> {
243        use crate::signing::verify;
244
245        verify(&self.public_key, &self.challenge, &self.signature)
246            .map_err(|_| CommitmentError::InvalidProof)
247    }
248}
249
250/// Generate a random challenge nonce.
251pub fn generate_challenge() -> [u8; 32] {
252    let mut challenge = [0u8; 32];
253    rand::thread_rng().fill_bytes(&mut challenge);
254    challenge
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_commit_verify() {
263        let value = b"test data to commit";
264        let (commitment, opening) = commit(value);
265
266        assert!(verify_commitment(&commitment, &opening).is_ok());
267
268        // Wrong value should fail
269        let mut wrong_opening = opening.clone();
270        wrong_opening.value = b"wrong data".to_vec();
271        assert!(verify_commitment(&commitment, &wrong_opening).is_err());
272    }
273
274    #[test]
275    fn test_chunk_possession_proof() {
276        let chunk_data = b"chunk data that needs to be proven";
277        let challenge = generate_challenge();
278
279        let proof = ChunkPossessionProof::generate(chunk_data, &challenge);
280        assert!(proof.verify(chunk_data).is_ok());
281
282        // Wrong data should fail
283        assert!(proof.verify(b"wrong data").is_err());
284    }
285
286    #[test]
287    fn test_chunk_challenge() {
288        let chunk_data = b"test chunk data";
289        let chunk_hash = hash(chunk_data);
290        let challenge = ChunkChallenge::new(0, chunk_hash);
291
292        let proof = ChunkPossessionProof::generate(chunk_data, &challenge.nonce);
293        assert!(challenge.verify_proof(chunk_data, &proof).is_ok());
294
295        // Wrong chunk should fail
296        assert!(challenge.verify_proof(b"wrong chunk", &proof).is_err());
297    }
298
299    #[test]
300    fn test_bandwidth_proof_commitment() {
301        let chunk_data = b"bandwidth proof chunk";
302        let (commitment, opening) = BandwidthProofCommitment::new(chunk_data, 0, 1234567890);
303
304        assert!(commitment.verify(&opening, chunk_data).is_ok());
305        assert!(commitment.verify(&opening, b"wrong data").is_err());
306    }
307
308    #[test]
309    fn test_key_possession_proof() {
310        use crate::signing::KeyPair;
311
312        let keypair = KeyPair::generate();
313        let secret = keypair.secret_key();
314        let challenge = generate_challenge();
315
316        let proof = KeyPossessionProof::generate(&secret, &challenge).unwrap();
317        assert!(proof.verify().is_ok());
318
319        // Tampered signature should fail
320        let mut bad_proof = proof.clone();
321        bad_proof.signature[0] ^= 1;
322        assert!(bad_proof.verify().is_err());
323    }
324
325    #[test]
326    fn test_different_blindings() {
327        let value = b"same value";
328
329        let (c1, _) = commit(value);
330        let (c2, _) = commit(value);
331
332        // Same value with different blindings should produce different commitments
333        assert_ne!(c1, c2);
334    }
335}