stemedb/crates/stemedb-query/tests/battery/battery10_signature_advanced.rs
jordan 137a588ed0 feat: Concept hierarchy (Phase 5D) - ConceptPath, source schemes, AliasStore
Implements hierarchical subject identifiers with scheme-based source tier inference:

- ConceptPath type with parse/wire_format, leaf/parent, prefix matching
- SourceScheme registry mapping schemes to default SourceClass tiers:
  - rfc://, fda://, ietf:// → Regulatory (Tier 0)
  - peer://, pubmed:// → PeerReviewed (Tier 1)
  - code://, wiki:// → Expert (Tier 3)
  - blog://, anon:// → Anecdotal (Tier 5)
- AliasStore for cross-scheme entity resolution (bidirectional indexing)
- API endpoints for concept operations
- Battery tests 8, 9 & 10 for concepts, aliases, and advanced signatures
- Go SDK updates for concept types and signing

Completes Phase 5, advancing to Phase 6 (Distributed Writes).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 17:44:54 -07:00

298 lines
11 KiB
Rust

//! 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");
}