stemedb/crates/stemedb-ingest/src/worker/tests/signatures.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

172 lines
5.8 KiB
Rust

//! Signature verification tests.
//!
//! Tests for Ed25519 signature verification, rejection of invalid
//! signatures, and multi-signature validation.
use super::*;
use crate::error::IngestError;
use stemedb_storage::key_codec;
/// Test: Assertions with invalid signatures are rejected.
#[tokio::test]
async fn test_rejects_invalid_signature() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
// Create an assertion with an invalid signature (all zeros)
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "invalid_sig".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![SignatureEntry {
version: 1,
agent_id: [1u8; 32], // Invalid: not a valid Ed25519 public key
signature: [2u8; 64], // Invalid: not a valid signature
timestamp: 1000,
}],
confidence: 0.95,
timestamp: 1000,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
// Step should fail with InvalidSignature error
let result = worker.step().await;
assert!(result.is_err(), "Should reject invalid signature");
let err = result.unwrap_err();
assert!(
matches!(err, IngestError::InvalidSignature(_)),
"Error should be InvalidSignature, got: {:?}",
err
);
// Verify no assertion was stored
let count_key = key_codec::assertion_count_key();
let count_entry = store.get(&count_key).await.expect("get");
assert!(count_entry.is_none(), "No assertion should be stored");
}
/// Test: Assertions with no signatures are rejected.
#[tokio::test]
async fn test_rejects_unsigned_assertion() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
// Create an assertion with no signatures
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "unsigned".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![], // No signatures!
confidence: 0.95,
timestamp: 1000,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_err(), "Should reject unsigned assertion");
let err = result.unwrap_err();
assert!(
matches!(err, IngestError::InvalidSignature(_)),
"Error should be InvalidSignature, got: {:?}",
err
);
}
/// Test: Multi-signature assertions require all signatures to be valid.
#[tokio::test]
async fn test_multisig_all_must_be_valid() {
let dir = tempdir().expect("Failed to create temp dir");
let wal_dir = dir.path().join("wal");
let db_dir = dir.path().join("db");
// Create an assertion with one valid and one invalid signature
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let message = "Test:multisig";
let valid_signature = signing_key.sign(message.as_bytes());
let assertion = Assertion {
subject: "Test".to_string(),
predicate: "multisig".to_string(),
object: ObjectValue::Text("test".to_string()),
parent_hash: None,
source_hash: [0u8; 32],
source_class: SourceClass::Expert,
visual_hash: None,
epoch: None,
source_metadata: None,
lifecycle: LifecycleStage::Proposed,
signatures: vec![
// Valid signature
SignatureEntry {
version: 1,
agent_id: verifying_key.to_bytes(),
signature: valid_signature.to_bytes(),
timestamp: 1000,
},
// Invalid signature
SignatureEntry {
version: 1,
agent_id: [1u8; 32],
signature: [2u8; 64],
timestamp: 1001,
},
],
confidence: 0.95,
timestamp: 1000,
vector: None,
};
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
let store = HybridStore::open(&db_dir).expect("Failed to open store");
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
let journal = Arc::new(Mutex::new(journal));
let store = Arc::new(store);
let mut worker =
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
let result = worker.step().await;
assert!(result.is_err(), "Should reject when any signature is invalid");
}