//! Battery 10: Advanced Signature Tests. //! //! Tests for v2 signatures, multi-sig, and unknown signature versions. //! //! # Test Coverage //! //! | Test | Scenario | Validates | //! |------|----------|-----------| //! | `test_v2_valid_signature_accepted` | Valid v2 sig | Accepted and stored | //! | `test_unknown_signature_version_rejected` | Unknown version | Rejected | //! | `test_multi_sig_all_valid_accepted` | Multi-sig valid | Accepted | //! | `test_multi_sig_one_invalid_rejected` | Multi-sig partial | Rejected | #![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages use super::helpers::*; /// Test 10.1: Valid v2 signature is accepted. /// /// Agent A signs an assertion using v2 (content hash) format. /// Assert: assertion is stored, index entries exist. #[tokio::test] async fn test_v2_valid_signature_accepted() { let dir = tempdir().expect("create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let base_ts: u64 = 1_000_000; // Create a properly signed v2 assertion using the helper let assertion = create_signed_assertion_v2( "Subject_V2_Valid", "predicate_test", ObjectValue::Text("v2_value".to_string()), SourceClass::Clinical, 0.8, base_ts, ); // Verify the assertion has v2 signature assert_eq!(assertion.signatures[0].version, 2, "should be v2 signature"); // Write to WAL let mut journal = Journal::open(&wal_dir).expect("open journal"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); // Ingest via IngestWorker let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(HybridStore::open(&db_dir).expect("open store")); let mut worker = IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker"); let bytes = worker.step().await.expect("v2 ingest step should succeed"); assert!(bytes > 0, "v2: should process data from WAL"); // Verify assertion is stored (H: key exists) let h_prefix = key_codec::assertion_key("Subject_V2_Valid", ""); let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:"); assert_eq!(h_entries.len(), 1, "v2: should have 1 assertion stored"); // Verify SP: index exists let sp_prefix = key_codec::subject_predicate_scan_prefix("Subject_V2_Valid"); let sp_entries = store.scan_prefix(&sp_prefix).await.expect("scan SP:"); assert_eq!(sp_entries.len(), 1, "v2: should have 1 SP: index entry"); } /// Test 10.2: Unknown signature version is rejected. /// /// Agent A signs an assertion but uses an unknown version (e.g., v99). /// Assert: ingestion fails with unknown version error. #[tokio::test] async fn test_unknown_signature_version_rejected() { let dir = tempdir().expect("create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let base_ts: u64 = 1_000_000; // Create a signed assertion let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); // Sign for "Subject_Unknown:predicate_test" let message = format!("{}:{}", "Subject_Unknown", "predicate_test"); let signature = signing_key.sign(message.as_bytes()); // Create assertion with unknown version let assertion = AssertionBuilder::new() .subject("Subject_Unknown") .predicate("predicate_test") .object_text("value") .source_class(SourceClass::Clinical) .confidence(0.8) .lifecycle(LifecycleStage::Proposed) .timestamp(base_ts) .signatures(vec![SignatureEntry { agent_id: verifying_key.to_bytes(), signature: signature.to_bytes(), timestamp: base_ts, version: 99, }]) .build(); // Write to WAL let mut journal = Journal::open(&wal_dir).expect("open journal"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); // Ingest via IngestWorker let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(HybridStore::open(&db_dir).expect("open store")); let mut worker = IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker"); // Attempt to ingest - should fail due to unknown version let result = worker.step().await; assert!(result.is_err(), "unknown signature version should be rejected"); // Verify the error mentions the unknown version let err = result.unwrap_err(); let err_str = err.to_string(); assert!( err_str.contains("Unknown signature version") || err_str.contains("99"), "error should mention unknown version, got: {}", err_str ); // Verify no assertion was stored let h_prefix = key_codec::assertion_key("Subject_Unknown", ""); let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:"); assert_eq!(h_entries.len(), 0, "assertion with unknown signature version should NOT be stored"); } /// Test 10.3: Multi-sig with all valid signatures is accepted. /// /// Agent A and Agent B both sign the same assertion (two valid SignatureEntries). /// Assert: ingestion succeeds. #[tokio::test] async fn test_multi_sig_all_valid_accepted() { let dir = tempdir().expect("create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let base_ts: u64 = 1_000_000; // Create Agent A's key pair let mut csprng = OsRng; let signing_key_a = SigningKey::generate(&mut csprng); let verifying_key_a = signing_key_a.verifying_key(); // Create Agent B's key pair let signing_key_b = SigningKey::generate(&mut csprng); let verifying_key_b = signing_key_b.verifying_key(); // Both agents sign the same message "Subject_F:predicate_test" let message = format!("{}:{}", "Subject_F", "predicate_test"); let signature_a = signing_key_a.sign(message.as_bytes()); let signature_b = signing_key_b.sign(message.as_bytes()); // Create assertion with two valid signatures let assertion = AssertionBuilder::new() .subject("Subject_F") .predicate("predicate_test") .object_text("value") .source_class(SourceClass::Clinical) .confidence(0.8) .lifecycle(LifecycleStage::Proposed) .timestamp(base_ts) .signatures(vec![ SignatureEntry { agent_id: verifying_key_a.to_bytes(), signature: signature_a.to_bytes(), timestamp: base_ts, version: 1, }, SignatureEntry { agent_id: verifying_key_b.to_bytes(), signature: signature_b.to_bytes(), timestamp: base_ts, version: 1, }, ]) .build(); // Write to WAL let mut journal = Journal::open(&wal_dir).expect("open journal"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); // Ingest via IngestWorker let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(HybridStore::open(&db_dir).expect("open store")); let mut worker = IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker"); let bytes = worker.step().await.expect("multi-sig should be accepted"); assert!(bytes > 0, "should process data from WAL"); // Verify assertion is stored let h_prefix = key_codec::assertion_key("Subject_F", ""); let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:"); assert_eq!(h_entries.len(), 1, "multi-sig assertion should be stored"); // Verify the stored assertion has both signatures let (_key, value) = &h_entries[0]; let stored: Assertion = stemedb_core::serde::deserialize(value).expect("deserialize"); assert_eq!(stored.signatures.len(), 2, "stored assertion should have 2 signatures"); } /// Test 10.4: Multi-sig with one invalid signature is rejected. /// /// Agent A signs validly, Agent B's signature is invalid (tampered). /// Assert: ingestion fails. ALL signatures must be valid. #[tokio::test] async fn test_multi_sig_one_invalid_rejected() { let dir = tempdir().expect("create temp dir"); let wal_dir = dir.path().join("wal"); let db_dir = dir.path().join("db"); let base_ts: u64 = 1_000_000; // Create Agent A's key pair let mut csprng = OsRng; let signing_key_a = SigningKey::generate(&mut csprng); let verifying_key_a = signing_key_a.verifying_key(); // Create Agent B's key pair let signing_key_b = SigningKey::generate(&mut csprng); let verifying_key_b = signing_key_b.verifying_key(); // Agent A signs correctly for "Subject_G:predicate_test" let message = format!("{}:{}", "Subject_G", "predicate_test"); let signature_a = signing_key_a.sign(message.as_bytes()); // Agent B signs a DIFFERENT message (tampered) let wrong_message = format!("{}:{}", "Wrong_Subject", "predicate_test"); let signature_b_wrong = signing_key_b.sign(wrong_message.as_bytes()); // Create assertion with one valid and one invalid signature let assertion = AssertionBuilder::new() .subject("Subject_G") .predicate("predicate_test") .object_text("value") .source_class(SourceClass::Clinical) .confidence(0.8) .lifecycle(LifecycleStage::Proposed) .timestamp(base_ts) .signatures(vec![ SignatureEntry { agent_id: verifying_key_a.to_bytes(), signature: signature_a.to_bytes(), timestamp: base_ts, version: 1, }, SignatureEntry { agent_id: verifying_key_b.to_bytes(), signature: signature_b_wrong.to_bytes(), timestamp: base_ts, version: 1, }, ]) .build(); // Write to WAL let mut journal = Journal::open(&wal_dir).expect("open journal"); journal.append(serialize_assertion(&assertion).expect("ser")).expect("append"); // Ingest via IngestWorker let journal = Arc::new(Mutex::new(journal)); let store = Arc::new(HybridStore::open(&db_dir).expect("open store")); let mut worker = IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker"); // Attempt to ingest - should fail because one signature is invalid let result = worker.step().await; assert!( result.is_err(), "multi-sig with one invalid signature should fail (ALL signatures must be valid)" ); // Verify the error is an invalid signature error let err = result.unwrap_err(); let err_str = err.to_string(); assert!( err_str.contains("Signature") || err_str.contains("verification"), "error should be related to signature verification, got: {}", err_str ); // Verify no assertion was stored let h_prefix = key_codec::assertion_key("Subject_G", ""); let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:"); assert_eq!(h_entries.len(), 0, "multi-sig with invalid signature should NOT be stored"); }