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 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 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 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 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 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 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 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 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 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()); }
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 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()); }
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 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 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()); }
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()); }
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 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()); }
674}