Skip to main content

sentinel_dbms/collection/
verification.rs

1use tracing::{error, trace, warn};
2
3use crate::{Result, SentinelError};
4use super::coll::Collection;
5
6#[allow(
7    clippy::multiple_inherent_impl,
8    reason = "multiple impl blocks for Collection are intentional for organization"
9)]
10impl Collection {
11    /// Verifies document hash according to the specified verification options.
12    ///
13    /// # Arguments
14    ///
15    /// * `doc` - The document to verify
16    /// * `options` - The verification options
17    ///
18    /// # Returns
19    ///
20    /// Returns `Ok(())` if verification passes or is handled according to the mode,
21    /// or `Err(SentinelError::HashVerificationFailed)` if verification fails in Strict mode.
22    pub async fn verify_hash(&self, doc: &crate::Document, options: crate::VerificationOptions) -> Result<()> {
23        if options.hash_verification_mode == crate::VerificationMode::Silent {
24            return Ok(());
25        }
26
27        trace!("Verifying hash for document: {}", doc.id());
28        let computed_hash = sentinel_crypto::hash_data(doc.data()).await?;
29
30        if computed_hash != doc.hash() {
31            let reason = format!(
32                "Expected hash: {}, Computed hash: {}",
33                doc.hash(),
34                computed_hash
35            );
36
37            match options.hash_verification_mode {
38                crate::VerificationMode::Strict => {
39                    error!("Document {} hash verification failed: {}", doc.id(), reason);
40                    return Err(SentinelError::HashVerificationFailed {
41                        id: doc.id().to_owned(),
42                        reason,
43                    });
44                },
45                crate::VerificationMode::Warn => {
46                    warn!("Document {} hash verification failed: {}", doc.id(), reason);
47                },
48                crate::VerificationMode::Silent => {},
49            }
50        }
51        else {
52            trace!("Document {} hash verified successfully", doc.id());
53        }
54
55        Ok(())
56    }
57
58    /// Verifies document signature according to the specified verification options.
59    ///
60    /// # Arguments
61    ///
62    /// * `doc` - The document to verify
63    /// * `options` - The verification options containing modes for different scenarios
64    ///
65    /// # Returns
66    ///
67    /// Returns `Ok(())` if verification passes or is handled according to the mode,
68    /// or `Err(SentinelError::SignatureVerificationFailed)` if verification fails in Strict mode.
69    pub async fn verify_signature(&self, doc: &crate::Document, options: crate::VerificationOptions) -> Result<()> {
70        if options.signature_verification_mode == crate::VerificationMode::Silent &&
71            options.empty_signature_mode == crate::VerificationMode::Silent
72        {
73            return Ok(());
74        }
75
76        trace!("Verifying signature for document: {}", doc.id());
77
78        if doc.signature().is_empty() {
79            let reason = "Document has no signature".to_owned();
80
81            match options.empty_signature_mode {
82                crate::VerificationMode::Strict => {
83                    error!("Document {} has no signature: {}", doc.id(), reason);
84                    return Err(SentinelError::SignatureVerificationFailed {
85                        id: doc.id().to_owned(),
86                        reason,
87                    });
88                },
89                crate::VerificationMode::Warn => {
90                    warn!("Document {} has no signature: {}", doc.id(), reason);
91                },
92                crate::VerificationMode::Silent => {},
93            }
94            return Ok(());
95        }
96
97        if !options.verify_signature {
98            trace!("Signature verification disabled for document: {}", doc.id());
99            return Ok(());
100        }
101
102        if let Some(ref signing_key) = self.signing_key {
103            let public_key = signing_key.verifying_key();
104            let is_valid = sentinel_crypto::verify_signature(doc.hash(), doc.signature(), &public_key).await?;
105
106            if !is_valid {
107                let reason = "Signature verification using public key failed".to_owned();
108
109                match options.signature_verification_mode {
110                    crate::VerificationMode::Strict => {
111                        error!(
112                            "Document {} signature verification failed: {}",
113                            doc.id(),
114                            reason
115                        );
116                        return Err(SentinelError::SignatureVerificationFailed {
117                            id: doc.id().to_owned(),
118                            reason,
119                        });
120                    },
121                    crate::VerificationMode::Warn => {
122                        warn!(
123                            "Document {} signature verification failed: {}",
124                            doc.id(),
125                            reason
126                        );
127                    },
128                    crate::VerificationMode::Silent => {},
129                }
130            }
131            else {
132                trace!("Document {} signature verified successfully", doc.id());
133            }
134        }
135        else {
136            trace!("No signing key available for verification, skipping signature check");
137        }
138
139        Ok(())
140    }
141
142    /// Verifies both hash and signature of a document according to the specified options.
143    ///
144    /// # Arguments
145    ///
146    /// * `doc` - The document to verify
147    /// * `options` - The verification options
148    ///
149    /// # Returns
150    ///
151    /// Returns `Ok(())` if verifications pass or are handled according to the modes,
152    /// or an error if verification fails in Strict mode.
153    pub async fn verify_document(&self, doc: &crate::Document, options: &crate::VerificationOptions) -> Result<()> {
154        if options.verify_hash {
155            self.verify_hash(doc, *options).await?;
156        }
157
158        // Check for empty signature regardless of verify_signature option
159        if doc.signature().is_empty() {
160            let reason = "Document has no signature".to_owned();
161
162            match options.empty_signature_mode {
163                crate::VerificationMode::Strict => {
164                    error!("Document {} has no signature: {}", doc.id(), reason);
165                    return Err(SentinelError::SignatureVerificationFailed {
166                        id: doc.id().to_owned(),
167                        reason,
168                    });
169                },
170                crate::VerificationMode::Warn => {
171                    warn!("Document {} has no signature: {}", doc.id(), reason);
172                },
173                crate::VerificationMode::Silent => {},
174            }
175        }
176        else if options.verify_signature {
177            self.verify_signature(doc, *options).await?;
178        }
179
180        Ok(())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use serde_json::json;
187
188    use super::*;
189    use crate::{Document, Store, VerificationMode, VerificationOptions};
190
191    async fn setup_collection_with_signing_key() -> (crate::Collection, tempfile::TempDir) {
192        let temp_dir = tempfile::tempdir().unwrap();
193        let store = Store::new_with_config(
194            temp_dir.path(),
195            Some("test_passphrase"),
196            sentinel_wal::StoreWalConfig::default(),
197        )
198        .await
199        .unwrap();
200        let collection = store.collection_with_config("test", None).await.unwrap();
201        (collection, temp_dir)
202    }
203
204    async fn setup_collection() -> (crate::Collection, tempfile::TempDir) {
205        let temp_dir = tempfile::tempdir().unwrap();
206        let store = Store::new_with_config(
207            temp_dir.path(),
208            None,
209            sentinel_wal::StoreWalConfig::default(),
210        )
211        .await
212        .unwrap();
213        let collection = store.collection_with_config("test", None).await.unwrap();
214        (collection, temp_dir)
215    }
216
217    #[tokio::test]
218    async fn test_verify_hash_silent_mode() {
219        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
220        collection
221            .insert("doc1", json!({"name": "test"}))
222            .await
223            .unwrap();
224        let doc = collection.get("doc1").await.unwrap().unwrap();
225
226        let options = VerificationOptions {
227            hash_verification_mode: VerificationMode::Silent,
228            ..Default::default()
229        };
230
231        let result = collection.verify_hash(&doc, options).await;
232        assert!(result.is_ok());
233    }
234
235    #[tokio::test]
236    async fn test_verify_hash_warn_mode() {
237        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
238        collection
239            .insert("doc1", json!({"name": "test"}))
240            .await
241            .unwrap();
242        let doc = collection.get("doc1").await.unwrap().unwrap();
243
244        let options = VerificationOptions {
245            hash_verification_mode: VerificationMode::Warn,
246            ..Default::default()
247        };
248
249        let result = collection.verify_hash(&doc, options).await;
250        assert!(result.is_ok());
251    }
252
253    #[tokio::test]
254    async fn test_verify_hash_strict_mode_valid() {
255        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
256        collection
257            .insert("doc1", json!({"name": "test"}))
258            .await
259            .unwrap();
260        let doc = collection.get("doc1").await.unwrap().unwrap();
261
262        let options = VerificationOptions {
263            hash_verification_mode: VerificationMode::Strict,
264            ..Default::default()
265        };
266
267        let result = collection.verify_hash(&doc, options).await;
268        assert!(result.is_ok());
269    }
270
271    #[tokio::test]
272    async fn test_verify_hash_strict_mode_corrupted() {
273        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
274        collection
275            .insert("doc1", json!({"name": "test"}))
276            .await
277            .unwrap();
278        let mut doc = collection.get("doc1").await.unwrap().unwrap();
279
280        // Corrupt the hash field
281        doc = Document {
282            id:         doc.id().to_string(),
283            version:    doc.version(),
284            created_at: doc.created_at(),
285            updated_at: doc.updated_at(),
286            hash:       "corrupted_hash".to_string(),
287            signature:  doc.signature().to_string(),
288            data:       doc.data().clone(),
289        };
290
291        let options = VerificationOptions {
292            hash_verification_mode: VerificationMode::Strict,
293            ..Default::default()
294        };
295
296        let result = collection.verify_hash(&doc, options).await;
297        assert!(result.is_err());
298    }
299
300    #[tokio::test]
301    async fn test_verify_signature_silent_mode() {
302        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
303        collection
304            .insert("doc1", json!({"name": "test"}))
305            .await
306            .unwrap();
307        let doc = collection.get("doc1").await.unwrap().unwrap();
308
309        let options = VerificationOptions {
310            signature_verification_mode: VerificationMode::Silent,
311            empty_signature_mode: VerificationMode::Silent,
312            ..Default::default()
313        };
314
315        let result = collection.verify_signature(&doc, options).await;
316        assert!(result.is_ok());
317    }
318
319    #[tokio::test]
320    async fn test_verify_signature_empty_signature_strict() {
321        let (collection, _temp_dir) = setup_collection().await;
322        // Insert without signature
323        collection
324            .insert("doc1", json!({"name": "test"}))
325            .await
326            .unwrap();
327        let doc = collection.get("doc1").await.unwrap().unwrap();
328
329        let options = VerificationOptions {
330            empty_signature_mode: VerificationMode::Strict,
331            ..Default::default()
332        };
333
334        let result = collection.verify_signature(&doc, options).await;
335        assert!(result.is_err());
336        if let Err(SentinelError::SignatureVerificationFailed {
337            reason,
338            ..
339        }) = result
340        {
341            assert!(reason.contains("no signature"));
342        }
343    }
344
345    #[tokio::test]
346    async fn test_verify_signature_empty_signature_warn() {
347        let (collection, _temp_dir) = setup_collection().await;
348        collection
349            .insert("doc1", json!({"name": "test"}))
350            .await
351            .unwrap();
352        let doc = collection.get("doc1").await.unwrap().unwrap();
353
354        let options = VerificationOptions {
355            empty_signature_mode: VerificationMode::Warn,
356            ..Default::default()
357        };
358
359        let result = collection.verify_signature(&doc, options).await;
360        assert!(result.is_ok());
361    }
362
363    #[tokio::test]
364    async fn test_verify_signature_disabled() {
365        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
366        collection
367            .insert("doc1", json!({"name": "test"}))
368            .await
369            .unwrap();
370        let doc = collection.get("doc1").await.unwrap().unwrap();
371
372        let options = VerificationOptions {
373            verify_signature: false,
374            ..Default::default()
375        };
376
377        let result = collection.verify_signature(&doc, options).await;
378        assert!(result.is_ok());
379    }
380
381    #[tokio::test]
382    async fn test_verify_signature_no_signing_key() {
383        let (collection, _temp_dir) = setup_collection().await;
384        collection
385            .insert("doc1", json!({"name": "test"}))
386            .await
387            .unwrap();
388        let doc = collection.get("doc1").await.unwrap().unwrap();
389
390        let options = VerificationOptions {
391            signature_verification_mode: VerificationMode::Strict,
392            empty_signature_mode: VerificationMode::Silent,
393            verify_signature: true,
394            ..Default::default()
395        };
396
397        // Should skip verification if collection has no signing key
398        let result = collection.verify_signature(&doc, options).await;
399        assert!(result.is_ok());
400    }
401
402    #[tokio::test]
403    async fn test_verify_document_both_enabled() {
404        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
405        collection
406            .insert("doc1", json!({"name": "test"}))
407            .await
408            .unwrap();
409        let doc = collection.get("doc1").await.unwrap().unwrap();
410
411        let options = VerificationOptions {
412            verify_hash: true,
413            verify_signature: false,
414            empty_signature_mode: VerificationMode::Silent,
415            hash_verification_mode: VerificationMode::Strict,
416            ..Default::default()
417        };
418
419        let result = collection.verify_document(&doc, &options).await;
420        assert!(result.is_ok());
421    }
422
423    #[tokio::test]
424    async fn test_verify_document_neither_enabled() {
425        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
426        collection
427            .insert("doc1", json!({"name": "test"}))
428            .await
429            .unwrap();
430        let doc = collection.get("doc1").await.unwrap().unwrap();
431
432        let options = VerificationOptions {
433            verify_hash: false,
434            verify_signature: false,
435            ..Default::default()
436        };
437
438        let result = collection.verify_document(&doc, &options).await;
439        assert!(result.is_ok());
440    }
441
442    #[tokio::test]
443    async fn test_verify_document_hash_only() {
444        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
445        collection
446            .insert("doc1", json!({"test": "data"}))
447            .await
448            .unwrap();
449        let doc = collection.get("doc1").await.unwrap().unwrap();
450
451        let options = VerificationOptions {
452            verify_hash: true,
453            verify_signature: false,
454            hash_verification_mode: VerificationMode::Strict,
455            ..Default::default()
456        };
457
458        let result = collection.verify_document(&doc, &options).await;
459        assert!(result.is_ok());
460    }
461
462    #[tokio::test]
463    async fn test_verify_signature_strict_mode_corrupted() {
464        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
465        collection
466            .insert("doc1", json!({"name": "test"}))
467            .await
468            .unwrap();
469        let mut doc = collection.get("doc1").await.unwrap().unwrap();
470
471        // Corrupt the signature field
472        doc = Document {
473            id:         doc.id().to_string(),
474            version:    doc.version(),
475            created_at: doc.created_at(),
476            updated_at: doc.updated_at(),
477            hash:       doc.hash().to_string(),
478            signature:  "corrupted_signature".to_string(),
479            data:       doc.data().clone(),
480        };
481
482        let options = VerificationOptions {
483            signature_verification_mode: VerificationMode::Strict,
484            empty_signature_mode: VerificationMode::Silent,
485            ..Default::default()
486        };
487
488        let result = collection.verify_signature(&doc, options).await;
489        assert!(result.is_err());
490        if let Err(SentinelError::SignatureVerificationFailed {
491            reason,
492            ..
493        }) = result
494        {
495            assert!(reason.contains("Signature verification using public key failed"));
496        }
497    }
498
499    #[tokio::test]
500    async fn test_verify_signature_warn_mode_corrupted() {
501        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
502        collection
503            .insert("doc1", json!({"name": "test"}))
504            .await
505            .unwrap();
506        let mut doc = collection.get("doc1").await.unwrap().unwrap();
507
508        // Corrupt the hash field so signature doesn't match
509        doc = Document {
510            id:         doc.id().to_string(),
511            version:    doc.version(),
512            created_at: doc.created_at(),
513            updated_at: doc.updated_at(),
514            hash:       "corrupted_hash".to_string(),
515            signature:  doc.signature().to_string(),
516            data:       doc.data().clone(),
517        };
518
519        let options = VerificationOptions {
520            signature_verification_mode: VerificationMode::Warn,
521            empty_signature_mode: VerificationMode::Silent,
522            ..Default::default()
523        };
524
525        let result = collection.verify_signature(&doc, options).await;
526        assert!(result.is_ok()); // Warn mode should not fail
527    }
528
529    #[tokio::test]
530    async fn test_verify_signature_silent_mode_corrupted() {
531        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
532        collection
533            .insert("doc1", json!({"name": "test"}))
534            .await
535            .unwrap();
536        let mut doc = collection.get("doc1").await.unwrap().unwrap();
537
538        // Corrupt the hash field so signature doesn't match
539        doc = Document {
540            id:         doc.id().to_string(),
541            version:    doc.version(),
542            created_at: doc.created_at(),
543            updated_at: doc.updated_at(),
544            hash:       "corrupted_hash".to_string(),
545            signature:  doc.signature().to_string(),
546            data:       doc.data().clone(),
547        };
548
549        let options = VerificationOptions {
550            signature_verification_mode: VerificationMode::Silent,
551            empty_signature_mode: VerificationMode::Silent,
552            ..Default::default()
553        };
554
555        let result = collection.verify_signature(&doc, options).await;
556        assert!(result.is_ok()); // Silent mode should not fail
557    }
558
559    #[tokio::test]
560    async fn test_verify_document_signature_strict_mode_corrupted() {
561        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
562        collection
563            .insert("doc1", json!({"name": "test"}))
564            .await
565            .unwrap();
566        let mut doc = collection.get("doc1").await.unwrap().unwrap();
567
568        // Corrupt the signature field
569        doc = Document {
570            id:         doc.id().to_string(),
571            version:    doc.version(),
572            created_at: doc.created_at(),
573            updated_at: doc.updated_at(),
574            hash:       doc.hash().to_string(),
575            signature:  "corrupted_signature".to_string(),
576            data:       doc.data().clone(),
577        };
578
579        let options = VerificationOptions {
580            verify_hash: false,
581            verify_signature: true,
582            signature_verification_mode: VerificationMode::Strict,
583            empty_signature_mode: VerificationMode::Silent,
584            ..Default::default()
585        };
586
587        let result = collection.verify_document(&doc, &options).await;
588        assert!(result.is_err());
589        if let Err(SentinelError::SignatureVerificationFailed {
590            reason,
591            ..
592        }) = result
593        {
594            assert!(reason.contains("Signature verification using public key failed"));
595        }
596    }
597
598    #[tokio::test]
599    async fn test_verify_document_signature_warn_mode_corrupted() {
600        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
601        collection
602            .insert("doc1", json!({"name": "test"}))
603            .await
604            .unwrap();
605        let mut doc = collection.get("doc1").await.unwrap().unwrap();
606
607        // Corrupt the hash field so signature doesn't match
608        doc = Document {
609            id:         doc.id().to_string(),
610            version:    doc.version(),
611            created_at: doc.created_at(),
612            updated_at: doc.updated_at(),
613            hash:       "corrupted_hash".to_string(),
614            signature:  doc.signature().to_string(),
615            data:       doc.data().clone(),
616        };
617
618        let options = VerificationOptions {
619            verify_hash: false,
620            verify_signature: true,
621            signature_verification_mode: VerificationMode::Warn,
622            empty_signature_mode: VerificationMode::Silent,
623            ..Default::default()
624        };
625
626        let result = collection.verify_document(&doc, &options).await;
627        assert!(result.is_ok()); // Warn mode should not fail
628    }
629
630    #[tokio::test]
631    async fn test_verify_signature_with_signing_key_success() {
632        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
633        collection
634            .insert("doc1", json!({"name": "test"}))
635            .await
636            .unwrap();
637        let doc = collection.get("doc1").await.unwrap().unwrap();
638
639        let options = VerificationOptions {
640            signature_verification_mode: VerificationMode::Strict,
641            empty_signature_mode: VerificationMode::Silent,
642            verify_signature: true,
643            ..Default::default()
644        };
645
646        let result = collection.verify_signature(&doc, options).await;
647        assert!(result.is_ok()); // Should succeed with valid signature
648    }
649
650    #[tokio::test]
651    async fn test_verify_signature_no_signing_key_with_signature() {
652        let (collection_with_key, _temp_dir1) = setup_collection_with_signing_key().await;
653        collection_with_key
654            .insert("doc1", json!({"name": "test"}))
655            .await
656            .unwrap();
657        let doc_with_sig = collection_with_key.get("doc1").await.unwrap().unwrap();
658
659        // Now verify against a collection without signing key
660        let (collection_no_key, _temp_dir2) = setup_collection().await;
661
662        let options = VerificationOptions {
663            signature_verification_mode: VerificationMode::Strict,
664            empty_signature_mode: VerificationMode::Silent,
665            verify_signature: true,
666            ..Default::default()
667        };
668
669        let result = collection_no_key
670            .verify_signature(&doc_with_sig, options)
671            .await;
672        assert!(result.is_ok()); // Should skip verification when no signing key available
673    }
674}