Skip to main content

optirs_core/privacy/enhanced_audit/
proofs.rs

1//! Cryptographic proofs over released values.
2//!
3//! # What these proofs are
4//!
5//! Every algorithm registered here is a **keyed or unkeyed integrity
6//! commitment** over the canonical encoding of the released vector: SHA-256
7//! for the public variant, HMAC-SHA256 (RFC 2104) for the keyed variant. They
8//! are real, verifiable, and implemented in pure Rust on top of `sha2`.
9//!
10//! # What they are not
11//!
12//! Zero-knowledge proofs, asymmetric digital signatures and confidentiality
13//! proofs need a proving system or a signature scheme that this crate does not
14//! carry, and adding one is not something an integrity digest can fake. Those
15//! requirements are therefore *rejected* by
16//! [`CryptographicProofGenerator::check_requirements`] with
17//! [`OptimError::UnsupportedOperation`], instead of being answered with a
18//! digest dressed up as a signature.
19//!
20//! The previous implementation registered **no** proof types at all, so
21//! `generate_proof` could only ever fail, and the keys were empty maps.
22
23use crate::error::{OptimError, Result};
24use scirs2_core::ndarray::Array1;
25use scirs2_core::numeric::Float;
26use std::collections::HashMap;
27use std::fmt::Debug;
28use std::time::{SystemTime, UNIX_EPOCH};
29
30use super::hashing::{
31    canonical_array_bytes, digests_equal, hmac_sha256, random_key, sha256, Digest32,
32};
33use super::types::{
34    CryptographicKeys, CryptographicProof, CryptographicProofType, ProofAlgorithm,
35    ProofRequirements,
36};
37
38/// Name of the unkeyed integrity proof algorithm.
39pub const SHA256_INTEGRITY: &str = "sha256-integrity";
40/// Name of the keyed integrity proof algorithm.
41pub const HMAC_SHA256_INTEGRITY: &str = "hmac-sha256-integrity";
42
43/// Seconds since the Unix epoch, or an error if the clock is before it.
44pub(super) fn unix_timestamp() -> Result<u64> {
45    SystemTime::now()
46        .duration_since(UNIX_EPOCH)
47        .map(|elapsed| elapsed.as_secs())
48        .map_err(|err| {
49            OptimError::InvalidState(format!(
50                "the system clock is set before the Unix epoch, so audit records cannot be \
51                 timestamped: {err}"
52            ))
53        })
54}
55
56impl CryptographicKeys {
57    /// Create an empty key store.
58    pub fn new() -> Self {
59        Self {
60            signing_keys: HashMap::new(),
61            verification_keys: HashMap::new(),
62            encryption_keys: HashMap::new(),
63        }
64    }
65
66    /// Create a key store with a freshly generated symmetric MAC key.
67    ///
68    /// The key is used for HMAC-SHA256 integrity proofs. It is recorded under
69    /// `signing_keys` *and* `verification_keys` because HMAC is symmetric:
70    /// verification requires the same secret. That is exactly why HMAC cannot
71    /// provide non-repudiation, and why
72    /// [`CryptographicProofGenerator::check_requirements`] rejects that
73    /// requirement rather than pretending otherwise.
74    pub fn generate() -> Self {
75        let key = random_key();
76        let mut signing_keys = HashMap::new();
77        signing_keys.insert(HMAC_SHA256_INTEGRITY.to_string(), key.to_vec());
78        let mut verification_keys = HashMap::new();
79        verification_keys.insert(HMAC_SHA256_INTEGRITY.to_string(), key.to_vec());
80        Self {
81            signing_keys,
82            verification_keys,
83            encryption_keys: HashMap::new(),
84        }
85    }
86
87    /// The MAC key for `name`, if present.
88    pub fn mac_key(&self, name: &str) -> Option<&[u8]> {
89        self.signing_keys.get(name).map(|key| key.as_slice())
90    }
91}
92
93impl Default for CryptographicKeys {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99/// Cryptographic proof generator.
100pub struct CryptographicProofGenerator<T: Float + Debug + Send + Sync + 'static> {
101    /// Registered proof types.
102    proof_types: HashMap<String, CryptographicProofType<T>>,
103    /// Key material.
104    keys: CryptographicKeys,
105}
106
107impl<T: Float + Debug + Send + Sync + 'static> CryptographicProofGenerator<T> {
108    /// Create a generator with the two integrity proof types registered and a
109    /// freshly generated MAC key.
110    pub fn new() -> Self {
111        let keys = CryptographicKeys::generate();
112        let mut generator = Self {
113            proof_types: HashMap::new(),
114            keys,
115        };
116        generator.register_integrity_proof_types();
117        generator
118    }
119
120    /// Create a generator with no proof types registered.
121    ///
122    /// `generate_proof` then fails for every input, which is the honest
123    /// behaviour for a generator that has been given nothing to do.
124    pub fn empty() -> Self {
125        Self {
126            proof_types: HashMap::new(),
127            keys: CryptographicKeys::new(),
128        }
129    }
130
131    /// Register the built-in integrity proof types.
132    fn register_integrity_proof_types(&mut self) {
133        self.proof_types.insert(
134            SHA256_INTEGRITY.to_string(),
135            CryptographicProofType {
136                name: SHA256_INTEGRITY.to_string(),
137                generate_fn: Box::new(|data: &Array1<T>, _keys: &CryptographicKeys| {
138                    let bytes = canonical_array_bytes(data)?;
139                    let digest = sha256(&[&bytes]);
140                    let mut metadata = HashMap::new();
141                    metadata.insert("algorithm".to_string(), "SHA-256".to_string());
142                    metadata.insert("element_count".to_string(), data.len().to_string());
143                    Ok(CryptographicProof {
144                        prooftype: SHA256_INTEGRITY.to_string(),
145                        proof_data: digest.to_vec(),
146                        public_params: (data.len() as u64).to_le_bytes().to_vec(),
147                        timestamp: unix_timestamp()?,
148                        metadata,
149                    })
150                }),
151                verify_fn: Box::new(
152                    |proof: &CryptographicProof, data: &Array1<T>, _keys: &CryptographicKeys| {
153                        let Ok(bytes) = canonical_array_bytes(data) else {
154                            return false;
155                        };
156                        let digest = sha256(&[&bytes]);
157                        proof.proof_data.len() == digest.len()
158                            && digest_matches(&proof.proof_data, &digest)
159                    },
160                ),
161            },
162        );
163
164        self.proof_types.insert(
165            HMAC_SHA256_INTEGRITY.to_string(),
166            CryptographicProofType {
167                name: HMAC_SHA256_INTEGRITY.to_string(),
168                generate_fn: Box::new(|data: &Array1<T>, keys: &CryptographicKeys| {
169                    let key = keys.mac_key(HMAC_SHA256_INTEGRITY).ok_or_else(|| {
170                        OptimError::InvalidState(format!(
171                            "no MAC key is registered under `{HMAC_SHA256_INTEGRITY}`"
172                        ))
173                    })?;
174                    let bytes = canonical_array_bytes(data)?;
175                    let tag = hmac_sha256(key, &bytes);
176                    let mut metadata = HashMap::new();
177                    metadata.insert("algorithm".to_string(), "HMAC-SHA-256".to_string());
178                    metadata.insert("element_count".to_string(), data.len().to_string());
179                    metadata.insert("non_repudiation".to_string(), "false".to_string());
180                    Ok(CryptographicProof {
181                        prooftype: HMAC_SHA256_INTEGRITY.to_string(),
182                        proof_data: tag.to_vec(),
183                        public_params: (data.len() as u64).to_le_bytes().to_vec(),
184                        timestamp: unix_timestamp()?,
185                        metadata,
186                    })
187                }),
188                verify_fn: Box::new(
189                    |proof: &CryptographicProof, data: &Array1<T>, keys: &CryptographicKeys| {
190                        let Some(key) = keys.mac_key(HMAC_SHA256_INTEGRITY) else {
191                            return false;
192                        };
193                        let Ok(bytes) = canonical_array_bytes(data) else {
194                            return false;
195                        };
196                        let tag = hmac_sha256(key, &bytes);
197                        proof.proof_data.len() == tag.len()
198                            && digest_matches(&proof.proof_data, &tag)
199                    },
200                ),
201            },
202        );
203    }
204
205    /// Names of the registered proof types.
206    pub fn registered_proof_types(&self) -> Vec<String> {
207        let mut names: Vec<String> = self.proof_types.keys().cloned().collect();
208        names.sort();
209        names
210    }
211
212    /// Register a caller-supplied proof type.
213    pub fn register_proof_type(&mut self, proof_type: CryptographicProofType<T>) {
214        self.proof_types.insert(proof_type.name.clone(), proof_type);
215    }
216
217    /// Generate a proof of the named type over `data`.
218    pub fn generate_proof(&self, prooftype: &str, data: &Array1<T>) -> Result<CryptographicProof> {
219        if self.proof_types.is_empty() {
220            return Err(OptimError::InvalidState(
221                "no cryptographic proof types are registered, so no proof can be produced"
222                    .to_string(),
223            ));
224        }
225        let generator = self.proof_types.get(prooftype).ok_or_else(|| {
226            OptimError::InvalidConfig(format!(
227                "unknown proof type `{prooftype}`; registered types: {:?}",
228                self.registered_proof_types()
229            ))
230        })?;
231        (generator.generate_fn)(data, &self.keys)
232    }
233
234    /// Verify a proof against `data`.
235    pub fn verify_proof(&self, proof: &CryptographicProof, data: &Array1<T>) -> Result<bool> {
236        let verifier = self.proof_types.get(&proof.prooftype).ok_or_else(|| {
237            OptimError::InvalidConfig(format!(
238                "unknown proof type `{}`; registered types: {:?}",
239                proof.prooftype,
240                self.registered_proof_types()
241            ))
242        })?;
243        Ok((verifier.verify_fn)(proof, data, &self.keys))
244    }
245
246    /// Reject proof requirements that cannot be met.
247    ///
248    /// Integrity and completeness are covered by the registered commitments.
249    /// Zero-knowledge, non-repudiation and confidentiality are not, and no
250    /// digest can stand in for them.
251    pub fn check_requirements(&self, requirements: &ProofRequirements) -> Result<()> {
252        let mut missing = Vec::new();
253        if requirements.zero_knowledge_proofs {
254            missing.push("zero_knowledge_proofs (needs a zero-knowledge proving system)");
255        }
256        if requirements.non_repudiation {
257            missing.push(
258                "non_repudiation (needs an asymmetric signature scheme; HMAC is symmetric and \
259                 cannot bind a single signer)",
260            );
261        }
262        if requirements.confidentiality_proofs {
263            missing.push("confidentiality_proofs (needs an authenticated encryption scheme)");
264        }
265        if missing.is_empty() {
266            Ok(())
267        } else {
268            Err(OptimError::UnsupportedOperation(format!(
269                "the audit configuration requests proof guarantees this build cannot provide: {}",
270                missing.join("; ")
271            )))
272        }
273    }
274}
275
276impl<T: Float + Debug + Send + Sync + 'static> Default for CryptographicProofGenerator<T> {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282/// Constant-time comparison of a variable-length proof against a digest.
283fn digest_matches(proof: &[u8], digest: &Digest32) -> bool {
284    if proof.len() != digest.len() {
285        return false;
286    }
287    let mut fixed = [0u8; 32];
288    fixed.copy_from_slice(proof);
289    digests_equal(&fixed, digest)
290}
291
292/// Proof system for formal verification.
293pub struct ProofSystem<T: Float + Debug + Send + Sync + 'static> {
294    /// Registered algorithms.
295    algorithms: HashMap<String, ProofAlgorithm<T>>,
296    /// Verification key material, by algorithm name.
297    verification_keys: HashMap<String, Vec<u8>>,
298}
299
300impl<T: Float + Debug + Send + Sync + 'static> ProofSystem<T> {
301    /// Create a proof system with the built-in integrity algorithms.
302    pub fn new() -> Self {
303        let key = random_key();
304        let mut verification_keys = HashMap::new();
305        verification_keys.insert(HMAC_SHA256_INTEGRITY.to_string(), key.to_vec());
306
307        let mut algorithms = HashMap::new();
308        algorithms.insert(
309            SHA256_INTEGRITY.to_string(),
310            ProofAlgorithm {
311                name: SHA256_INTEGRITY.to_string(),
312                generate_fn: Box::new(|data: &Array1<T>| {
313                    let bytes = canonical_array_bytes(data)?;
314                    Ok(sha256(&[&bytes]).to_vec())
315                }),
316                verify_fn: Box::new(|proof: &[u8], data: &Array1<T>| {
317                    let Ok(bytes) = canonical_array_bytes(data) else {
318                        return false;
319                    };
320                    digest_matches(proof, &sha256(&[&bytes]))
321                }),
322            },
323        );
324        let mac_key = key;
325        algorithms.insert(
326            HMAC_SHA256_INTEGRITY.to_string(),
327            ProofAlgorithm {
328                name: HMAC_SHA256_INTEGRITY.to_string(),
329                generate_fn: Box::new(move |data: &Array1<T>| {
330                    let bytes = canonical_array_bytes(data)?;
331                    Ok(hmac_sha256(&mac_key, &bytes).to_vec())
332                }),
333                verify_fn: Box::new(move |proof: &[u8], data: &Array1<T>| {
334                    let Ok(bytes) = canonical_array_bytes(data) else {
335                        return false;
336                    };
337                    digest_matches(proof, &hmac_sha256(&mac_key, &bytes))
338                }),
339            },
340        );
341
342        Self {
343            algorithms,
344            verification_keys,
345        }
346    }
347
348    /// Create a proof system with no algorithms registered.
349    pub fn empty() -> Self {
350        Self {
351            algorithms: HashMap::new(),
352            verification_keys: HashMap::new(),
353        }
354    }
355
356    /// Names of the registered algorithms.
357    pub fn registered_algorithms(&self) -> Vec<String> {
358        let mut names: Vec<String> = self.algorithms.keys().cloned().collect();
359        names.sort();
360        names
361    }
362
363    /// Register an algorithm.
364    pub fn register_algorithm(&mut self, algorithm: ProofAlgorithm<T>) {
365        self.algorithms.insert(algorithm.name.clone(), algorithm);
366    }
367
368    /// Verification key material for an algorithm, if any.
369    pub fn verification_key(&self, name: &str) -> Option<&[u8]> {
370        self.verification_keys.get(name).map(|key| key.as_slice())
371    }
372
373    /// Generate a proof with the named algorithm.
374    pub fn generate(&self, algorithm: &str, data: &Array1<T>) -> Result<Vec<u8>> {
375        if self.algorithms.is_empty() {
376            return Err(OptimError::InvalidState(
377                "no proof algorithms are registered with the proof system".to_string(),
378            ));
379        }
380        let entry = self.algorithms.get(algorithm).ok_or_else(|| {
381            OptimError::InvalidConfig(format!(
382                "unknown proof algorithm `{algorithm}`; registered: {:?}",
383                self.registered_algorithms()
384            ))
385        })?;
386        (entry.generate_fn)(data)
387    }
388
389    /// Verify a proof with the named algorithm.
390    pub fn verify(&self, algorithm: &str, proof: &[u8], data: &Array1<T>) -> Result<bool> {
391        let entry = self.algorithms.get(algorithm).ok_or_else(|| {
392            OptimError::InvalidConfig(format!(
393                "unknown proof algorithm `{algorithm}`; registered: {:?}",
394                self.registered_algorithms()
395            ))
396        })?;
397        Ok((entry.verify_fn)(proof, data))
398    }
399}
400
401impl<T: Float + Debug + Send + Sync + 'static> Default for ProofSystem<T> {
402    fn default() -> Self {
403        Self::new()
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    fn data() -> Array1<f64> {
412        Array1::from(vec![1.0, -2.5, 3.75, 0.0])
413    }
414
415    #[test]
416    fn a_generated_integrity_proof_verifies_and_a_tampered_value_does_not() {
417        let generator: CryptographicProofGenerator<f64> = CryptographicProofGenerator::new();
418        for prooftype in [SHA256_INTEGRITY, HMAC_SHA256_INTEGRITY] {
419            let proof = match generator.generate_proof(prooftype, &data()) {
420                Ok(proof) => proof,
421                Err(err) => panic!("{prooftype} generation failed: {err}"),
422            };
423            assert_eq!(proof.proof_data.len(), 32);
424            match generator.verify_proof(&proof, &data()) {
425                Ok(true) => {}
426                Ok(false) => panic!("{prooftype} must verify against its own input"),
427                Err(err) => panic!("{prooftype} verification failed: {err}"),
428            }
429
430            // Flip one bit of one element.
431            let mut tampered = data();
432            tampered[2] = f64::from_bits(tampered[2].to_bits() ^ 1);
433            match generator.verify_proof(&proof, &tampered) {
434                Ok(false) => {}
435                Ok(true) => panic!("{prooftype} must not verify against modified data"),
436                Err(err) => panic!("{prooftype} verification failed: {err}"),
437            }
438
439            // Truncating the vector must also fail (length is committed).
440            let shorter = Array1::from(vec![1.0, -2.5, 3.75]);
441            match generator.verify_proof(&proof, &shorter) {
442                Ok(false) => {}
443                Ok(true) => panic!("{prooftype} must commit to the length"),
444                Err(err) => panic!("{prooftype} verification failed: {err}"),
445            }
446        }
447    }
448
449    #[test]
450    fn a_generator_with_no_proof_types_fails_instead_of_succeeding() {
451        let generator: CryptographicProofGenerator<f64> = CryptographicProofGenerator::empty();
452        assert!(generator.generate_proof(SHA256_INTEGRITY, &data()).is_err());
453        assert!(generator.registered_proof_types().is_empty());
454    }
455
456    #[test]
457    fn an_unknown_proof_type_is_an_error() {
458        let generator: CryptographicProofGenerator<f64> = CryptographicProofGenerator::new();
459        assert!(generator.generate_proof("zk-snark", &data()).is_err());
460    }
461
462    #[test]
463    fn two_generators_use_independent_mac_keys() {
464        let left: CryptographicProofGenerator<f64> = CryptographicProofGenerator::new();
465        let right: CryptographicProofGenerator<f64> = CryptographicProofGenerator::new();
466        let left_proof = match left.generate_proof(HMAC_SHA256_INTEGRITY, &data()) {
467            Ok(proof) => proof,
468            Err(err) => panic!("generation failed: {err}"),
469        };
470        // The keyed tag must not verify under a different key.
471        match right.verify_proof(&left_proof, &data()) {
472            Ok(false) => {}
473            Ok(true) => panic!("the MAC key must not be a shared constant"),
474            Err(err) => panic!("verification failed: {err}"),
475        }
476    }
477
478    #[test]
479    fn the_unkeyed_digest_is_reproducible_across_generators() {
480        let left: CryptographicProofGenerator<f64> = CryptographicProofGenerator::new();
481        let right: CryptographicProofGenerator<f64> = CryptographicProofGenerator::new();
482        let proof = match left.generate_proof(SHA256_INTEGRITY, &data()) {
483            Ok(proof) => proof,
484            Err(err) => panic!("generation failed: {err}"),
485        };
486        match right.verify_proof(&proof, &data()) {
487            Ok(true) => {}
488            Ok(false) => panic!("an unkeyed digest must be publicly verifiable"),
489            Err(err) => panic!("verification failed: {err}"),
490        }
491    }
492
493    #[test]
494    fn unmeetable_proof_requirements_are_refused() {
495        let generator: CryptographicProofGenerator<f64> = CryptographicProofGenerator::new();
496        let supported = ProofRequirements {
497            zero_knowledge_proofs: false,
498            non_repudiation: false,
499            integrity_proofs: true,
500            confidentiality_proofs: false,
501            completeness_proofs: true,
502        };
503        assert!(generator.check_requirements(&supported).is_ok());
504
505        for requirements in [
506            ProofRequirements {
507                zero_knowledge_proofs: true,
508                non_repudiation: false,
509                integrity_proofs: true,
510                confidentiality_proofs: false,
511                completeness_proofs: false,
512            },
513            ProofRequirements {
514                zero_knowledge_proofs: false,
515                non_repudiation: true,
516                integrity_proofs: true,
517                confidentiality_proofs: false,
518                completeness_proofs: false,
519            },
520            ProofRequirements {
521                zero_knowledge_proofs: false,
522                non_repudiation: false,
523                integrity_proofs: true,
524                confidentiality_proofs: true,
525                completeness_proofs: false,
526            },
527        ] {
528            assert!(
529                generator.check_requirements(&requirements).is_err(),
530                "an unimplementable guarantee must be refused, not silently granted"
531            );
532        }
533    }
534
535    #[test]
536    fn the_proof_system_generates_and_verifies_both_algorithms() {
537        let system: ProofSystem<f64> = ProofSystem::new();
538        for algorithm in [SHA256_INTEGRITY, HMAC_SHA256_INTEGRITY] {
539            let proof = match system.generate(algorithm, &data()) {
540                Ok(proof) => proof,
541                Err(err) => panic!("{algorithm} failed: {err}"),
542            };
543            match system.verify(algorithm, &proof, &data()) {
544                Ok(true) => {}
545                Ok(false) => panic!("{algorithm} must verify its own proof"),
546                Err(err) => panic!("{algorithm} verification failed: {err}"),
547            }
548            let mut tampered = data();
549            tampered[0] = 1.5;
550            match system.verify(algorithm, &proof, &tampered) {
551                Ok(false) => {}
552                Ok(true) => panic!("{algorithm} must reject modified data"),
553                Err(err) => panic!("{algorithm} verification failed: {err}"),
554            }
555        }
556    }
557
558    #[test]
559    fn an_empty_proof_system_reports_that_it_has_nothing_registered() {
560        let system: ProofSystem<f64> = ProofSystem::empty();
561        assert!(system.generate(SHA256_INTEGRITY, &data()).is_err());
562        assert!(system.registered_algorithms().is_empty());
563    }
564
565    #[test]
566    fn non_finite_values_cannot_be_committed_to() {
567        let system: ProofSystem<f64> = ProofSystem::new();
568        // NaN is representable as f64, so it *can* be committed; the digest
569        // just has to be stable. What must fail is a value that cannot be
570        // converted at all, which f64 always can -- so assert the NaN case is
571        // handled deterministically rather than erroring.
572        let with_nan = Array1::from(vec![f64::NAN, 1.0]);
573        let proof = match system.generate(SHA256_INTEGRITY, &with_nan) {
574            Ok(proof) => proof,
575            Err(err) => panic!("generation failed: {err}"),
576        };
577        match system.verify(SHA256_INTEGRITY, &proof, &with_nan) {
578            Ok(true) => {}
579            Ok(false) => panic!("a NaN commitment must be reproducible"),
580            Err(err) => panic!("verification failed: {err}"),
581        }
582    }
583}