1use crate::models::field_names;
28use anyhow::{Context, Result};
29use rusqlite::{Connection, OptionalExtension, params};
30use serde::{Deserialize, Serialize};
31
32pub(crate) const ATTEST_OPERATOR_SIGNED: &str = "operator_signed";
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct Rule {
40 pub id: String,
41 pub kind: String,
42 pub matcher: String,
43 pub severity: String,
44 pub reason: String,
45 pub namespace: String,
46 pub created_by: String,
47 pub created_at: i64,
48 pub enabled: bool,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub signature: Option<Vec<u8>>,
51 pub attest_level: String,
52}
53
54pub fn insert(conn: &Connection, rule: &Rule) -> Result<()> {
63 conn.execute(
64 "INSERT INTO governance_rules (
65 id, kind, matcher, severity, reason, namespace,
66 created_by, created_at, enabled, signature, attest_level
67 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
68 params![
69 rule.id,
70 rule.kind,
71 rule.matcher,
72 rule.severity,
73 rule.reason,
74 rule.namespace,
75 rule.created_by,
76 rule.created_at,
77 i64::from(rule.enabled),
78 rule.signature,
79 rule.attest_level,
80 ],
81 )
82 .with_context(|| format!("rules_store::insert: id={}", rule.id))?;
83 Ok(())
84}
85
86pub fn get(conn: &Connection, id: &str) -> Result<Option<Rule>> {
95 let row = conn
96 .query_row(
97 "SELECT id, kind, matcher, severity, reason, namespace,
98 created_by, created_at, enabled, signature, attest_level
99 FROM governance_rules WHERE id = ?1",
100 params![id],
101 row_to_rule,
102 )
103 .optional()
104 .with_context(|| format!("rules_store::get: id={id}"))?;
105 Ok(row)
106}
107
108pub fn list(conn: &Connection) -> Result<Vec<Rule>> {
115 let mut stmt = conn
116 .prepare(
117 "SELECT id, kind, matcher, severity, reason, namespace,
118 created_by, created_at, enabled, signature, attest_level
119 FROM governance_rules ORDER BY id ASC",
120 )
121 .context("rules_store::list: prepare")?;
122 let rows = stmt
123 .query_map([], row_to_rule)
124 .context("rules_store::list: query_map")?;
125 let mut out = Vec::new();
126 for r in rows {
127 out.push(r.context("rules_store::list: row")?);
128 }
129 Ok(out)
130}
131
132pub fn list_enabled_by_kind(conn: &Connection, kind: &str) -> Result<Vec<Rule>> {
175 let mut stmt = conn
176 .prepare(
177 "SELECT id, kind, matcher, severity, reason, namespace,
178 created_by, created_at, enabled, signature, attest_level
179 FROM governance_rules
180 WHERE kind = ?1 AND enabled = 1
181 ORDER BY id ASC",
182 )
183 .context("rules_store::list_enabled_by_kind: prepare")?;
184 let rows = stmt
185 .query_map(params![kind], row_to_rule)
186 .context("rules_store::list_enabled_by_kind: query_map")?;
187 let operator_pubkey = resolve_operator_pubkey();
188 let mut out = Vec::new();
189 for r in rows {
190 let rule = r.context("rules_store::list_enabled_by_kind: row")?;
191 if enforced_rule_passes(&rule, operator_pubkey.as_ref()) {
192 out.push(rule);
193 }
194 }
195 Ok(out)
196}
197
198#[must_use]
205pub fn enforced_rule_passes(
206 rule: &Rule,
207 operator_pubkey: Option<&ed25519_dalek::VerifyingKey>,
208) -> bool {
209 match (operator_pubkey, rule.attest_level.as_str()) {
210 (Some(pk), ATTEST_OPERATOR_SIGNED) => match verify_rule_signature(rule, pk) {
211 Ok(()) => true,
212 Err(_) => {
213 tracing::error!(
214 rule_id = %rule.id,
215 "L1-6: operator_signed rule failed signature verification — \
216 skipping. Tampered row OR rule was directly modified after \
217 signing (e.g. `UPDATE governance_rules SET enabled = 1`). \
218 Re-sign with `ai-memory rules sign-seed` after audit."
219 );
220 false
221 }
222 },
223 (Some(_), _) => {
224 tracing::warn!(
229 rule_id = %rule.id,
230 attest_level = %rule.attest_level,
231 "L1-6: enabled rule is not operator_signed — skipping. Run \
232 `ai-memory rules sign-seed` to commit the operator signature."
233 );
234 false
235 }
236 (None, _) => {
237 true
245 }
246 }
247}
248
249const OPERATOR_PUBKEY_KEYGEN_FILE: &str = "operator.key.pub";
253const OPERATOR_PUBKEY_LEGACY_FILE: &str = "operator.pub";
256
257pub const OPERATOR_SIGNED_ATTEST_LEVEL: &str = "operator_signed";
266
267#[must_use]
306pub fn resolve_operator_pubkey() -> Option<ed25519_dalek::VerifyingKey> {
307 #[cfg(test)]
322 if force_no_operator_pubkey_active() {
323 return None;
324 }
325
326 use base64::Engine;
327 let from_bytes = |bytes: &[u8]| -> Option<ed25519_dalek::VerifyingKey> {
328 if bytes.len() != ed25519_dalek::PUBLIC_KEY_LENGTH {
329 return None;
330 }
331 let mut arr = [0u8; ed25519_dalek::PUBLIC_KEY_LENGTH];
332 arr.copy_from_slice(bytes);
333 ed25519_dalek::VerifyingKey::from_bytes(&arr).ok()
334 };
335 let try_decode = |s: &str| -> Option<ed25519_dalek::VerifyingKey> {
336 let trimmed = s.trim();
337 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
338 .decode(trimmed)
339 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(trimmed))
340 .ok()?;
341 from_bytes(&bytes)
342 };
343 let try_pub_file = |path: &std::path::Path| -> Option<ed25519_dalek::VerifyingKey> {
347 let raw = std::fs::read(path).ok()?;
348 if let Ok(text) = std::str::from_utf8(&raw)
349 && let Some(pk) = try_decode(text)
350 {
351 return Some(pk);
352 }
353 from_bytes(&raw)
354 };
355
356 if let Ok(v) = std::env::var("AI_MEMORY_OPERATOR_PUBKEY")
357 && !v.is_empty()
358 && let Some(pk) = try_decode(&v)
359 {
360 return Some(pk);
361 }
362
363 let key_dir = crate::identity::keypair::default_key_dir().ok()?;
367 let mut candidates: Vec<std::path::PathBuf> = vec![
368 key_dir.join(OPERATOR_PUBKEY_KEYGEN_FILE),
369 key_dir.join(OPERATOR_PUBKEY_LEGACY_FILE),
370 ];
371 if let Some(parent) = key_dir.parent() {
372 candidates.push(parent.join(OPERATOR_PUBKEY_KEYGEN_FILE));
373 candidates.push(parent.join(OPERATOR_PUBKEY_LEGACY_FILE));
374 }
375 candidates.iter().find_map(|p| try_pub_file(p))
376}
377
378#[cfg(test)]
385fn force_no_operator_pubkey_active() -> bool {
386 FORCE_NO_OPERATOR_PUBKEY.with(std::cell::Cell::get)
387}
388
389#[cfg(test)]
390thread_local! {
391 static FORCE_NO_OPERATOR_PUBKEY: std::cell::Cell<bool> =
392 const { std::cell::Cell::new(false) };
393}
394
395#[cfg(test)]
411#[must_use = "the guard must be held for its scope to suppress pubkey resolution"]
412pub fn force_no_operator_pubkey_for_test() -> ForceNoOperatorPubkeyGuard {
413 let prior = FORCE_NO_OPERATOR_PUBKEY.with(|c| c.replace(true));
414 ForceNoOperatorPubkeyGuard { prior }
415}
416
417#[cfg(test)]
420pub struct ForceNoOperatorPubkeyGuard {
421 prior: bool,
422}
423
424#[cfg(test)]
425impl Drop for ForceNoOperatorPubkeyGuard {
426 fn drop(&mut self) {
427 FORCE_NO_OPERATOR_PUBKEY.with(|c| c.set(self.prior));
428 }
429}
430
431pub fn count_enabled_rules(conn: &Connection) -> Result<i64> {
448 let result = conn.query_row(
449 "SELECT COUNT(*) FROM governance_rules WHERE enabled = 1",
450 [],
451 |row| row.get::<_, i64>(0),
452 );
453 match result {
454 Ok(n) => Ok(n),
455 Err(rusqlite::Error::SqliteFailure(_, Some(msg)))
456 if msg.contains("no such table") || msg.contains("does not exist") =>
457 {
458 Ok(0)
459 }
460 Err(rusqlite::Error::SqliteFailure(_, None)) => Ok(0),
461 Err(e) => Err(anyhow::Error::new(e).context("rules_store::count_enabled_rules")),
462 }
463}
464
465#[must_use]
470pub fn l1_6_attest_active() -> bool {
471 resolve_operator_pubkey().is_some()
472}
473
474pub fn log_missing_operator_pubkey_once(enabled_rule_count: i64) {
481 use std::sync::OnceLock;
482 static LOGGED: OnceLock<()> = OnceLock::new();
483 if LOGGED.set(()).is_err() {
484 return;
485 }
486 tracing::error!(
487 enabled_rule_count,
488 "SEC-2: governance_rules contains {enabled_rule_count} enabled row(s) but no operator \
489 pubkey is resolved (AI_MEMORY_OPERATOR_PUBKEY unset AND \
490 ~/.config/ai-memory/operator.key.pub absent). Substrate is in FAIL-OPEN posture: every \
491 enabled rule passes through without signature verification, so a SQL-write gadget that \
492 can mutate `governance_rules` can install or flip rules without operator consent. \
493 Activate L1-6 by either (a) running `ai-memory rules keygen` + `ai-memory rules \
494 sign-seed` to place an operator key + sign the existing rows, or (b) setting `[governance] \
495 require_operator_pubkey = true` in config.toml to refuse boot until the pubkey is in \
496 place."
497 );
498}
499
500pub fn remove(conn: &Connection, id: &str) -> Result<bool> {
507 let affected = conn
508 .execute("DELETE FROM governance_rules WHERE id = ?1", params![id])
509 .with_context(|| format!("rules_store::remove: id={id}"))?;
510 Ok(affected > 0)
511}
512
513pub fn remove_signed(
541 conn: &Connection,
542 id: &str,
543 signing_key: &ed25519_dalek::SigningKey,
544 operator_agent_id: &str,
545) -> Result<bool> {
546 use ed25519_dalek::Signer;
547
548 let tx = rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate)
549 .context("rules_store::remove_signed: begin IMMEDIATE tx")?;
550
551 let Some(rule) = get(&tx, id)? else {
552 tx.commit()
555 .context("rules_store::remove_signed: commit empty tx")?;
556 return Ok(false);
557 };
558
559 let canonical = canonical_bytes_for_signing(&rule)?;
560 let payload_hash = crate::signed_events::payload_hash(&canonical);
561 let signature = signing_key.sign(&payload_hash);
562
563 let event = crate::signed_events::SignedEvent {
564 id: uuid::Uuid::new_v4().to_string(),
565 agent_id: operator_agent_id.to_string(),
566 event_type: crate::signed_events::event_types::GOVERNANCE_RULE_REMOVED.to_string(),
567 payload_hash,
568 signature: Some(signature.to_bytes().to_vec()),
569 attest_level: OPERATOR_SIGNED_ATTEST_LEVEL.to_string(),
570 timestamp: chrono::Utc::now().to_rfc3339(),
571 ..crate::signed_events::SignedEvent::default()
572 };
573 crate::signed_events::append_signed_event_no_tx(&tx, &event)?;
574
575 let affected = tx
576 .execute("DELETE FROM governance_rules WHERE id = ?1", params![id])
577 .with_context(|| format!("rules_store::remove_signed: delete id={id}"))?;
578
579 tx.commit()
580 .context("rules_store::remove_signed: commit tx")?;
581 Ok(affected > 0)
582}
583
584pub fn set_enabled(conn: &Connection, id: &str, enabled: bool) -> Result<bool> {
594 let affected = conn
595 .execute(
596 "UPDATE governance_rules SET enabled = ?1 WHERE id = ?2",
597 params![i64::from(enabled), id],
598 )
599 .with_context(|| format!("rules_store::set_enabled: id={id} enabled={enabled}"))?;
600 Ok(affected > 0)
601}
602
603pub fn update_signature(
612 conn: &Connection,
613 id: &str,
614 signature: &[u8],
615 attest_level: &str,
616) -> Result<bool> {
617 let affected = conn
618 .execute(
619 "UPDATE governance_rules
620 SET signature = ?1, attest_level = ?2
621 WHERE id = ?3",
622 params![signature, attest_level, id],
623 )
624 .with_context(|| format!("rules_store::update_signature: id={id}"))?;
625 Ok(affected > 0)
626}
627
628pub fn canonical_bytes(rule: &Rule) -> Result<Vec<u8>> {
648 let canonical = serde_json::json!({
649 "id": rule.id,
650 "kind": rule.kind,
651 "matcher": rule.matcher,
652 "severity": rule.severity,
653 "reason": rule.reason,
654 "namespace": rule.namespace,
655 (field_names::CREATED_BY): rule.created_by,
656 (field_names::CREATED_AT): rule.created_at,
657 });
658 serde_json::to_vec(&canonical).context("rules_store::canonical_bytes: serialize")
659}
660
661pub fn canonical_bytes_for_signing(rule: &Rule) -> Result<Vec<u8>> {
695 let canonical = serde_json::json!({
696 "id": rule.id,
697 "kind": rule.kind,
698 "matcher": rule.matcher,
699 "severity": rule.severity,
700 "reason": rule.reason,
701 "namespace": rule.namespace,
702 (field_names::CREATED_BY): rule.created_by,
703 "enabled": rule.enabled,
704 });
705 serde_json::to_vec(&canonical).context("rules_store::canonical_bytes_for_signing: serialize")
706}
707
708pub fn verify_rule_signature(
721 rule: &Rule,
722 operator_pubkey: &ed25519_dalek::VerifyingKey,
723) -> Result<(), ed25519_dalek::SignatureError> {
724 use ed25519_dalek::{Signature, Verifier};
725
726 let Some(sig_bytes) = rule.signature.as_ref() else {
727 return Err(ed25519_dalek::SignatureError::new());
728 };
729 if sig_bytes.len() != ed25519_dalek::SIGNATURE_LENGTH {
730 return Err(ed25519_dalek::SignatureError::new());
731 }
732 let mut sig_arr = [0u8; ed25519_dalek::SIGNATURE_LENGTH];
733 sig_arr.copy_from_slice(sig_bytes);
734 let signature = Signature::from_bytes(&sig_arr);
735 let canonical =
741 canonical_bytes_for_signing(rule).map_err(|_| ed25519_dalek::SignatureError::new())?;
742 operator_pubkey.verify(&canonical, &signature)
743}
744
745fn row_to_rule(row: &rusqlite::Row<'_>) -> rusqlite::Result<Rule> {
746 Ok(Rule {
747 id: row.get(0)?,
748 kind: row.get(1)?,
749 matcher: row.get(2)?,
750 severity: row.get(3)?,
751 reason: row.get(4)?,
752 namespace: row.get(5)?,
753 created_by: row.get(6)?,
754 created_at: row.get(7)?,
755 enabled: row.get::<_, i64>(8)? != 0,
756 signature: row.get(9)?,
757 attest_level: row.get(10)?,
758 })
759}
760
761#[cfg(test)]
766mod tests {
767 use super::*;
768
769 fn fresh_conn() -> Connection {
770 let conn = Connection::open_in_memory().unwrap();
771 conn.execute_batch(
772 "CREATE TABLE governance_rules (
773 id TEXT PRIMARY KEY,
774 kind TEXT NOT NULL,
775 matcher TEXT NOT NULL,
776 severity TEXT NOT NULL CHECK (severity IN ('refuse','warn','log')),
777 reason TEXT NOT NULL,
778 namespace TEXT NOT NULL DEFAULT '_global',
779 created_by TEXT NOT NULL,
780 created_at INTEGER NOT NULL,
781 enabled INTEGER NOT NULL DEFAULT 1,
782 signature BLOB,
783 attest_level TEXT NOT NULL DEFAULT 'unsigned'
784 );",
785 )
786 .unwrap();
787 conn
788 }
789
790 fn make_rule(id: &str, kind: &str, enabled: bool) -> Rule {
791 Rule {
792 id: id.to_string(),
793 kind: kind.to_string(),
794 matcher: r#"{"k":"v"}"#.to_string(),
795 severity: "refuse".to_string(),
796 reason: "test".to_string(),
797 namespace: "_global".to_string(),
798 created_by: "test".to_string(),
799 created_at: 12345,
800 enabled,
801 signature: None,
802 attest_level: "unsigned".to_string(),
803 }
804 }
805
806 #[test]
807 fn insert_then_get_roundtrip() {
808 let conn = fresh_conn();
809 let rule = make_rule("R1", "bash", true);
810 insert(&conn, &rule).unwrap();
811 let got = get(&conn, "R1").unwrap();
812 assert_eq!(got.as_ref(), Some(&rule));
813 }
814
815 #[test]
816 fn get_returns_none_when_missing() {
817 let conn = fresh_conn();
818 assert_eq!(get(&conn, "nope").unwrap(), None);
819 }
820
821 #[test]
822 fn insert_duplicate_id_errors() {
823 let conn = fresh_conn();
824 let rule = make_rule("R1", "bash", true);
825 insert(&conn, &rule).unwrap();
826 assert!(insert(&conn, &rule).is_err());
827 }
828
829 #[test]
830 fn list_orders_by_id_ascending() {
831 let conn = fresh_conn();
832 insert(&conn, &make_rule("R3", "bash", true)).unwrap();
833 insert(&conn, &make_rule("R1", "bash", true)).unwrap();
834 insert(&conn, &make_rule("R2", "bash", true)).unwrap();
835 let all = list(&conn).unwrap();
836 let ids: Vec<&str> = all.iter().map(|r| r.id.as_str()).collect();
837 assert_eq!(ids, vec!["R1", "R2", "R3"]);
838 }
839
840 #[test]
841 fn list_enabled_by_kind_filters_correctly() {
842 let _no_pubkey = force_no_operator_pubkey_for_test();
849 let conn = fresh_conn();
850 insert(&conn, &make_rule("R1", "bash", true)).unwrap();
851 insert(&conn, &make_rule("R2", "bash", false)).unwrap();
852 insert(&conn, &make_rule("R3", "filesystem_write", true)).unwrap();
853 let bash_rules = list_enabled_by_kind(&conn, "bash").unwrap();
854 assert_eq!(bash_rules.len(), 1);
855 assert_eq!(bash_rules[0].id, "R1");
856 let fs_rules = list_enabled_by_kind(&conn, "filesystem_write").unwrap();
857 assert_eq!(fs_rules.len(), 1);
858 assert_eq!(fs_rules[0].id, "R3");
859 let other = list_enabled_by_kind(&conn, "network_request").unwrap();
860 assert!(other.is_empty());
861 }
862
863 #[test]
864 fn remove_returns_true_on_hit_false_on_miss() {
865 let conn = fresh_conn();
866 insert(&conn, &make_rule("R1", "bash", true)).unwrap();
867 assert!(remove(&conn, "R1").unwrap());
868 assert!(!remove(&conn, "R1").unwrap());
869 assert_eq!(get(&conn, "R1").unwrap(), None);
870 }
871
872 fn fresh_conn_with_audit() -> Connection {
876 let conn = fresh_conn();
877 conn.execute_batch(
878 "CREATE TABLE signed_events (
879 id TEXT PRIMARY KEY,
880 agent_id TEXT NOT NULL,
881 event_type TEXT NOT NULL,
882 payload_hash BLOB NOT NULL,
883 signature BLOB,
884 attest_level TEXT NOT NULL DEFAULT 'unsigned',
885 timestamp TEXT NOT NULL,
886 prev_hash BLOB,
887 sequence INTEGER
888 );
889 CREATE UNIQUE INDEX idx_signed_events_sequence ON signed_events(sequence);",
890 )
891 .unwrap();
892 conn
893 }
894
895 #[test]
902 fn remove_signed_emits_operator_signed_audit_event_and_deletes() {
903 use ed25519_dalek::Verifier;
904 let conn = fresh_conn_with_audit();
905 let rule = make_rule("R1", "bash", true);
906 insert(&conn, &rule).unwrap();
907
908 let mut csprng = rand_core::OsRng;
909 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
910 let operator_pubkey = signing.verifying_key();
911
912 let removed = remove_signed(&conn, "R1", &signing, "operator").unwrap();
913 assert!(removed, "remove_signed must report the row was deleted");
914 assert_eq!(get(&conn, "R1").unwrap(), None, "rule row must be gone");
915
916 let (event_type, attest_level, payload_hash, sig): (String, String, Vec<u8>, Vec<u8>) =
918 conn.query_row(
919 "SELECT event_type, attest_level, payload_hash, signature FROM signed_events",
920 [],
921 |row| {
922 Ok((
923 row.get::<_, String>(0)?,
924 row.get::<_, String>(1)?,
925 row.get::<_, Vec<u8>>(2)?,
926 row.get::<_, Vec<u8>>(3)?,
927 ))
928 },
929 )
930 .expect("exactly one signed_events row must exist");
931 assert_eq!(
932 event_type,
933 crate::signed_events::event_types::GOVERNANCE_RULE_REMOVED
934 );
935 assert_eq!(attest_level, OPERATOR_SIGNED_ATTEST_LEVEL);
936
937 let expected_hash =
940 crate::signed_events::payload_hash(&canonical_bytes_for_signing(&rule).unwrap());
941 assert_eq!(payload_hash, expected_hash);
942 assert_eq!(
943 sig.len(),
944 ed25519_dalek::SIGNATURE_LENGTH,
945 "signature must be 64 bytes"
946 );
947 let mut sig_arr = [0u8; ed25519_dalek::SIGNATURE_LENGTH];
948 sig_arr.copy_from_slice(&sig);
949 let signature = ed25519_dalek::Signature::from_bytes(&sig_arr);
950 operator_pubkey
951 .verify(&payload_hash, &signature)
952 .expect("operator signature over payload_hash must verify");
953 }
954
955 #[test]
958 fn remove_signed_missing_id_returns_false_and_emits_nothing() {
959 let conn = fresh_conn_with_audit();
960 let mut csprng = rand_core::OsRng;
961 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
962
963 let removed = remove_signed(&conn, "nope", &signing, "operator").unwrap();
964 assert!(!removed);
965 let count: i64 = conn
966 .query_row("SELECT COUNT(*) FROM signed_events", [], |row| row.get(0))
967 .unwrap();
968 assert_eq!(count, 0, "no audit row may be emitted for a no-op removal");
969 }
970
971 #[test]
972 fn set_enabled_toggles() {
973 let conn = fresh_conn();
974 insert(&conn, &make_rule("R1", "bash", false)).unwrap();
975 assert!(set_enabled(&conn, "R1", true).unwrap());
976 assert!(get(&conn, "R1").unwrap().unwrap().enabled);
977 assert!(set_enabled(&conn, "R1", false).unwrap());
978 assert!(!get(&conn, "R1").unwrap().unwrap().enabled);
979 }
980
981 #[test]
982 fn set_enabled_missing_returns_false() {
983 let conn = fresh_conn();
984 assert!(!set_enabled(&conn, "nope", true).unwrap());
985 }
986
987 #[test]
988 fn update_signature_persists_blob_and_attest_level() {
989 let conn = fresh_conn();
990 insert(&conn, &make_rule("R1", "bash", true)).unwrap();
991 let sig = vec![1u8, 2, 3, 4];
992 assert!(update_signature(&conn, "R1", &sig, "operator_signed").unwrap());
993 let got = get(&conn, "R1").unwrap().unwrap();
994 assert_eq!(got.signature, Some(sig));
995 assert_eq!(got.attest_level, "operator_signed");
996 }
997
998 #[test]
999 fn update_signature_missing_returns_false() {
1000 let conn = fresh_conn();
1001 assert!(!update_signature(&conn, "nope", &[1, 2, 3], "operator_signed").unwrap());
1002 }
1003
1004 #[test]
1005 fn canonical_bytes_excludes_signature_fields() {
1006 let mut rule = make_rule("R1", "bash", true);
1007 rule.signature = Some(vec![9, 9, 9]);
1008 rule.attest_level = "operator_signed".to_string();
1009 let bytes = canonical_bytes(&rule).unwrap();
1010 let s = std::str::from_utf8(&bytes).unwrap();
1011 assert!(!s.contains("signature"));
1014 assert!(!s.contains("attest_level"));
1015 assert!(s.contains("\"id\":\"R1\""));
1016 assert!(s.contains("\"kind\":\"bash\""));
1017 }
1018
1019 #[test]
1020 fn severity_check_constraint_rejects_unknown() {
1021 let conn = fresh_conn();
1022 let mut rule = make_rule("R1", "bash", true);
1023 rule.severity = "unknown".to_string();
1024 assert!(insert(&conn, &rule).is_err());
1025 }
1026
1027 #[test]
1028 fn rule_serde_roundtrip() {
1029 let rule = make_rule("R1", "bash", true);
1030 let v = serde_json::to_value(&rule).unwrap();
1031 let back: Rule = serde_json::from_value(v).unwrap();
1032 assert_eq!(back, rule);
1033 }
1034
1035 #[test]
1040 fn canonical_bytes_for_signing_includes_enabled() {
1041 let mut rule = make_rule("R1", "bash", true);
1042 let bytes_enabled = canonical_bytes_for_signing(&rule).unwrap();
1043 rule.enabled = false;
1044 let bytes_disabled = canonical_bytes_for_signing(&rule).unwrap();
1045 assert_ne!(
1046 bytes_enabled, bytes_disabled,
1047 "flipping `enabled` must change canonical bytes"
1048 );
1049 for b in [&bytes_enabled, &bytes_disabled] {
1051 let s = std::str::from_utf8(b).unwrap();
1052 assert!(s.contains("\"enabled\""), "missing enabled in: {s}");
1053 }
1054 }
1055
1056 #[test]
1057 fn canonical_bytes_for_signing_excludes_signature_and_attest_level() {
1058 let mut rule = make_rule("R1", "bash", true);
1059 rule.signature = Some(vec![1, 2, 3, 4]);
1060 rule.attest_level = "operator_signed".to_string();
1061 let bytes = canonical_bytes_for_signing(&rule).unwrap();
1062 let s = std::str::from_utf8(&bytes).unwrap();
1063 assert!(!s.contains("signature"), "got: {s}");
1064 assert!(!s.contains("attest_level"), "got: {s}");
1065 assert!(!s.contains("created_at"), "got: {s}");
1069 }
1070
1071 #[test]
1072 fn verify_rule_signature_round_trips_under_correct_key() {
1073 use ed25519_dalek::Signer;
1074 let mut rule = make_rule("R1", "bash", false);
1075 let mut csprng = rand_core::OsRng;
1076 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1077 let verifying = signing.verifying_key();
1078 let canonical = canonical_bytes_for_signing(&rule).unwrap();
1079 let sig = signing.sign(&canonical);
1080 rule.signature = Some(sig.to_bytes().to_vec());
1081 assert!(verify_rule_signature(&rule, &verifying).is_ok());
1082 }
1083
1084 #[test]
1085 fn verify_rule_signature_fails_on_enabled_flip() {
1086 use ed25519_dalek::Signer;
1087 let mut rule = make_rule("R1", "bash", false);
1088 let mut csprng = rand_core::OsRng;
1089 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1090 let verifying = signing.verifying_key();
1091 let canonical = canonical_bytes_for_signing(&rule).unwrap();
1092 let sig = signing.sign(&canonical);
1093 rule.signature = Some(sig.to_bytes().to_vec());
1094 rule.enabled = true;
1096 assert!(
1097 verify_rule_signature(&rule, &verifying).is_err(),
1098 "signature must not verify after `enabled` flip"
1099 );
1100 }
1101
1102 #[test]
1103 fn verify_rule_signature_fails_on_matcher_tamper() {
1104 use ed25519_dalek::Signer;
1105 let mut rule = make_rule("R1", "bash", false);
1106 let mut csprng = rand_core::OsRng;
1107 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1108 let verifying = signing.verifying_key();
1109 let canonical = canonical_bytes_for_signing(&rule).unwrap();
1110 let sig = signing.sign(&canonical);
1111 rule.signature = Some(sig.to_bytes().to_vec());
1112 rule.matcher = r#"{"k":"tampered"}"#.to_string();
1114 assert!(verify_rule_signature(&rule, &verifying).is_err());
1115 }
1116
1117 #[test]
1118 fn verify_rule_signature_fails_under_wrong_key() {
1119 use ed25519_dalek::Signer;
1120 let mut rule = make_rule("R1", "bash", false);
1121 let mut csprng = rand_core::OsRng;
1122 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1123 let other = ed25519_dalek::SigningKey::generate(&mut csprng);
1124 let canonical = canonical_bytes_for_signing(&rule).unwrap();
1125 let sig = signing.sign(&canonical);
1126 rule.signature = Some(sig.to_bytes().to_vec());
1127 assert!(verify_rule_signature(&rule, &other.verifying_key()).is_err());
1129 }
1130
1131 #[test]
1132 fn verify_rule_signature_fails_on_missing_signature() {
1133 let mut csprng = rand_core::OsRng;
1134 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1135 let rule = make_rule("R1", "bash", false);
1136 assert!(rule.signature.is_none());
1137 assert!(verify_rule_signature(&rule, &signing.verifying_key()).is_err());
1138 }
1139
1140 #[test]
1141 fn verify_rule_signature_fails_on_wrong_length_signature() {
1142 let mut csprng = rand_core::OsRng;
1143 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1144 let mut rule = make_rule("R1", "bash", false);
1145 rule.signature = Some(vec![0u8; 8]); assert!(verify_rule_signature(&rule, &signing.verifying_key()).is_err());
1147 }
1148
1149 fn signed_rule(id: &str, enabled: bool, signing: &ed25519_dalek::SigningKey) -> Rule {
1154 use ed25519_dalek::Signer;
1155 let mut rule = make_rule(id, "bash", enabled);
1156 rule.attest_level = "operator_signed".to_string();
1157 let canonical = canonical_bytes_for_signing(&rule).unwrap();
1158 let sig = signing.sign(&canonical);
1159 rule.signature = Some(sig.to_bytes().to_vec());
1160 rule
1161 }
1162
1163 #[test]
1164 fn enforced_rule_passes_when_no_pubkey_configured() {
1165 let rule = make_rule("R1", "bash", true);
1168 assert!(enforced_rule_passes(&rule, None));
1169 let mut signed_ish = make_rule("R2", "bash", true);
1173 signed_ish.attest_level = "operator_signed".to_string();
1174 assert!(enforced_rule_passes(&signed_ish, None));
1175 }
1176
1177 #[test]
1178 fn enforced_rule_passes_signed_under_correct_key() {
1179 let mut csprng = rand_core::OsRng;
1180 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1181 let pk = signing.verifying_key();
1182 let rule = signed_rule("R1", false, &signing);
1183 assert!(enforced_rule_passes(&rule, Some(&pk)));
1184 }
1185
1186 #[test]
1187 fn enforced_rule_passes_rejects_tampered_signed_row() {
1188 let mut csprng = rand_core::OsRng;
1189 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1190 let pk = signing.verifying_key();
1191 let mut rule = signed_rule("R1", false, &signing);
1192 rule.enabled = true;
1194 assert!(!enforced_rule_passes(&rule, Some(&pk)));
1195 }
1196
1197 #[test]
1198 fn enforced_rule_passes_rejects_unsigned_with_pubkey_configured() {
1199 let mut csprng = rand_core::OsRng;
1200 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1201 let pk = signing.verifying_key();
1202 let rule = make_rule("R1", "bash", true); assert!(!enforced_rule_passes(&rule, Some(&pk)));
1204 }
1205
1206 #[test]
1207 fn enforced_rule_passes_rejects_signed_under_wrong_key() {
1208 let mut csprng = rand_core::OsRng;
1209 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1210 let other = ed25519_dalek::SigningKey::generate(&mut csprng);
1211 let rule = signed_rule("R1", false, &signing);
1212 assert!(!enforced_rule_passes(&rule, Some(&other.verifying_key())));
1213 }
1214
1215 #[test]
1221 fn count_enabled_rules_returns_zero_when_table_empty() {
1222 let conn = fresh_conn();
1223 assert_eq!(count_enabled_rules(&conn).unwrap(), 0);
1224 }
1225
1226 #[test]
1227 fn count_enabled_rules_returns_zero_when_table_missing() {
1228 let conn = Connection::open_in_memory().unwrap();
1231 assert_eq!(count_enabled_rules(&conn).unwrap(), 0);
1232 }
1233
1234 #[test]
1235 fn count_enabled_rules_counts_only_enabled_rows() {
1236 let conn = fresh_conn();
1237 insert(&conn, &make_rule("R1", "bash", true)).unwrap();
1238 insert(&conn, &make_rule("R2", "bash", false)).unwrap();
1239 insert(&conn, &make_rule("R3", "filesystem_write", true)).unwrap();
1240 assert_eq!(count_enabled_rules(&conn).unwrap(), 2);
1242 }
1243
1244 #[test]
1245 fn count_enabled_rules_single_enabled_row() {
1246 let conn = fresh_conn();
1247 insert(&conn, &make_rule("R1", "bash", true)).unwrap();
1248 assert_eq!(count_enabled_rules(&conn).unwrap(), 1);
1249 }
1250
1251 #[test]
1252 fn log_missing_operator_pubkey_once_is_idempotent() {
1253 log_missing_operator_pubkey_once(7);
1259 log_missing_operator_pubkey_once(99);
1260 }
1264
1265 #[test]
1266 fn resolve_operator_pubkey_returns_none_when_env_and_file_absent() {
1267 let prior = std::env::var("AI_MEMORY_OPERATOR_PUBKEY").ok();
1278 unsafe { std::env::remove_var("AI_MEMORY_OPERATOR_PUBKEY") };
1282 let _ = resolve_operator_pubkey();
1283 let _ = l1_6_attest_active();
1285 if let Some(v) = prior {
1286 unsafe { std::env::set_var("AI_MEMORY_OPERATOR_PUBKEY", v) };
1287 }
1288 }
1289
1290 #[test]
1291 fn resolve_operator_pubkey_accepts_url_safe_no_pad_base64() {
1292 use base64::Engine;
1293 let mut csprng = rand_core::OsRng;
1295 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1296 let vk = signing.verifying_key();
1297 let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(vk.as_bytes());
1298
1299 let prior = std::env::var("AI_MEMORY_OPERATOR_PUBKEY").ok();
1300 unsafe { std::env::set_var("AI_MEMORY_OPERATOR_PUBKEY", &encoded) };
1301 let got = resolve_operator_pubkey();
1302 assert!(got.is_some(), "expected to decode URL_SAFE_NO_PAD pubkey");
1303 assert_eq!(got.unwrap().as_bytes(), vk.as_bytes());
1304 match prior {
1306 Some(v) => unsafe { std::env::set_var("AI_MEMORY_OPERATOR_PUBKEY", v) },
1307 None => unsafe { std::env::remove_var("AI_MEMORY_OPERATOR_PUBKEY") },
1308 }
1309 }
1310
1311 #[test]
1318 fn resolve_operator_pubkey_reads_keygen_layout_from_key_dir() {
1319 use base64::Engine;
1320 let _lock = crate::identity::keypair::key_dir_env_lock()
1321 .lock()
1322 .unwrap_or_else(std::sync::PoisonError::into_inner);
1323
1324 let mut csprng = rand_core::OsRng;
1325 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1326 let vk = signing.verifying_key();
1327 let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(vk.as_bytes());
1328
1329 let dir = tempfile::TempDir::new().expect("tempdir");
1330 std::fs::write(dir.path().join(OPERATOR_PUBKEY_KEYGEN_FILE), encoded).unwrap();
1331
1332 let prior_pubkey = std::env::var("AI_MEMORY_OPERATOR_PUBKEY").ok();
1335 let prior_key_dir = std::env::var("AI_MEMORY_KEY_DIR").ok();
1336 unsafe {
1337 std::env::remove_var("AI_MEMORY_OPERATOR_PUBKEY");
1338 std::env::set_var("AI_MEMORY_KEY_DIR", dir.path());
1339 }
1340
1341 let got = resolve_operator_pubkey();
1342
1343 unsafe {
1346 match prior_pubkey {
1347 Some(v) => std::env::set_var("AI_MEMORY_OPERATOR_PUBKEY", v),
1348 None => std::env::remove_var("AI_MEMORY_OPERATOR_PUBKEY"),
1349 }
1350 match prior_key_dir {
1351 Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
1352 None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
1353 }
1354 }
1355
1356 assert!(
1357 got.is_some(),
1358 "verifier must resolve operator.key.pub from AI_MEMORY_KEY_DIR"
1359 );
1360 assert_eq!(got.unwrap().as_bytes(), vk.as_bytes());
1361 }
1362
1363 #[test]
1367 fn resolve_operator_pubkey_reads_legacy_raw_layout_from_key_dir() {
1368 let _lock = crate::identity::keypair::key_dir_env_lock()
1369 .lock()
1370 .unwrap_or_else(std::sync::PoisonError::into_inner);
1371
1372 let mut csprng = rand_core::OsRng;
1373 let signing = ed25519_dalek::SigningKey::generate(&mut csprng);
1374 let vk = signing.verifying_key();
1375
1376 let dir = tempfile::TempDir::new().expect("tempdir");
1377 std::fs::write(dir.path().join(OPERATOR_PUBKEY_LEGACY_FILE), vk.as_bytes()).unwrap();
1379
1380 let prior_pubkey = std::env::var("AI_MEMORY_OPERATOR_PUBKEY").ok();
1381 let prior_key_dir = std::env::var("AI_MEMORY_KEY_DIR").ok();
1382 unsafe {
1383 std::env::remove_var("AI_MEMORY_OPERATOR_PUBKEY");
1384 std::env::set_var("AI_MEMORY_KEY_DIR", dir.path());
1385 }
1386
1387 let got = resolve_operator_pubkey();
1388
1389 unsafe {
1390 match prior_pubkey {
1391 Some(v) => std::env::set_var("AI_MEMORY_OPERATOR_PUBKEY", v),
1392 None => std::env::remove_var("AI_MEMORY_OPERATOR_PUBKEY"),
1393 }
1394 match prior_key_dir {
1395 Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
1396 None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
1397 }
1398 }
1399
1400 assert!(
1401 got.is_some(),
1402 "verifier must resolve legacy operator.pub from AI_MEMORY_KEY_DIR"
1403 );
1404 assert_eq!(got.unwrap().as_bytes(), vk.as_bytes());
1405 }
1406}