From ec652a6fe89d6f0cd15a449107bb80fec1f7f056 Mon Sep 17 00:00:00 2001 From: Dave Date: Sat, 14 Mar 2026 19:56:38 +0000 Subject: [PATCH] story-kit: enforce cryptographic identity verification for Matrix commands (story 246) Remove the require_verified_devices config toggle. The bot now always requires encrypted rooms and cross-signing-verified devices before executing any command. Messages from unencrypted rooms or unverified devices are rejected. Co-Authored-By: Claude Opus 4.6 --- server/src/matrix/bot.rs | 75 ++++++++++++++++++------------------- server/src/matrix/config.rs | 75 ++++++++++++++----------------------- 2 files changed, 65 insertions(+), 85 deletions(-) diff --git a/server/src/matrix/bot.rs b/server/src/matrix/bot.rs index 37cd7a9..14af2d8 100644 --- a/server/src/matrix/bot.rs +++ b/server/src/matrix/bot.rs @@ -77,9 +77,6 @@ pub struct BotContext { /// bot so it can continue a conversation thread without requiring an /// explicit `@mention` on every follow-up. pub bot_sent_event_ids: Arc>>, - /// When `true`, the bot rejects messages from users whose devices have not - /// been verified via cross-signing in encrypted rooms. - pub require_verified_devices: bool, } // --------------------------------------------------------------------------- @@ -192,12 +189,9 @@ pub async fn run_bot(config: BotConfig, project_root: PathBuf) -> Result<(), Str history: Arc::new(TokioMutex::new(HashMap::new())), history_size: config.history_size, bot_sent_event_ids: Arc::new(TokioMutex::new(HashSet::new())), - require_verified_devices: config.require_verified_devices, }; - if config.require_verified_devices { - slog!("[matrix-bot] require_verified_devices is ON — messages from unverified devices in encrypted rooms will be rejected"); - } + slog!("[matrix-bot] Cryptographic identity verification is always ON — commands from unencrypted rooms or unverified devices are rejected"); // Register event handlers and inject shared context. client.add_event_handler_context(ctx); @@ -475,28 +469,37 @@ async fn on_room_message( return; } - // When require_verified_devices is enabled and the room is encrypted, - // reject messages from users whose devices have not been verified. - if ctx.require_verified_devices && room.encryption_state().is_encrypted() { - match check_sender_verified(&client, &ev.sender).await { - Ok(true) => { /* sender has at least one verified device — proceed */ } - Ok(false) => { - slog!( - "[matrix-bot] WARNING: Rejecting message from {} — \ - unverified device(s) in encrypted room {}", - ev.sender, - incoming_room_id - ); - return; - } - Err(e) => { - slog!( - "[matrix-bot] Error checking verification for {}: {e} — \ - rejecting message (fail-closed)", - ev.sender - ); - return; - } + // Reject commands from unencrypted rooms — E2EE is mandatory. + if !room.encryption_state().is_encrypted() { + slog!( + "[matrix-bot] Rejecting message from {} — room {} is not encrypted. \ + Commands are only accepted from encrypted rooms.", + ev.sender, + incoming_room_id + ); + return; + } + + // Always verify that the sender has at least one cross-signing-verified + // device. This check is unconditional and cannot be disabled via config. + match check_sender_verified(&client, &ev.sender).await { + Ok(true) => { /* sender has at least one verified device — proceed */ } + Ok(false) => { + slog!( + "[matrix-bot] Rejecting message from {} — no cross-signing-verified \ + device found in encrypted room {}", + ev.sender, + incoming_room_id + ); + return; + } + Err(e) => { + slog!( + "[matrix-bot] Error checking verification for {}: {e} — \ + rejecting message (fail-closed)", + ev.sender + ); + return; } } @@ -930,7 +933,9 @@ mod tests { } #[test] - fn bot_context_require_verified_devices_field() { + fn bot_context_has_no_require_verified_devices_field() { + // Verification is always on — BotContext no longer has a toggle field. + // This test verifies the struct can be constructed and cloned without it. let ctx = BotContext { bot_user_id: make_user_id("@bot:example.com"), target_room_ids: vec![], @@ -939,15 +944,9 @@ mod tests { history: Arc::new(TokioMutex::new(HashMap::new())), history_size: 20, bot_sent_event_ids: Arc::new(TokioMutex::new(HashSet::new())), - require_verified_devices: true, }; - assert!(ctx.require_verified_devices); - - let ctx_off = BotContext { - require_verified_devices: false, - ..ctx - }; - assert!(!ctx_off.require_verified_devices); + // Clone must work (required by Matrix SDK event handler injection). + let _cloned = ctx.clone(); } // -- drain_complete_paragraphs ------------------------------------------ diff --git a/server/src/matrix/config.rs b/server/src/matrix/config.rs index 2fa64cc..1fcb5aa 100644 --- a/server/src/matrix/config.rs +++ b/server/src/matrix/config.rs @@ -35,12 +35,6 @@ pub struct BotConfig { /// dropped. Defaults to 20. #[serde(default = "default_history_size")] pub history_size: usize, - /// When `true`, the bot rejects messages from users whose devices have not - /// been verified via cross-signing in encrypted rooms. When `false` - /// (default), messages are accepted regardless of device verification - /// status, preserving existing plaintext-room behaviour. - #[serde(default)] - pub require_verified_devices: bool, /// Previously used to select an Anthropic model. Now ignored — the bot /// uses Claude Code which manages its own model selection. Kept for /// backwards compatibility so existing bot.toml files still parse. @@ -241,47 +235,6 @@ enabled = true assert_eq!(config.history_size, 20); } - #[test] - fn load_defaults_require_verified_devices_to_false() { - let tmp = tempfile::tempdir().unwrap(); - let sk = tmp.path().join(".story_kit"); - fs::create_dir_all(&sk).unwrap(); - fs::write( - sk.join("bot.toml"), - r#" -homeserver = "https://matrix.example.com" -username = "@bot:example.com" -password = "secret" -room_ids = ["!abc:example.com"] -enabled = true -"#, - ) - .unwrap(); - let config = BotConfig::load(tmp.path()).unwrap(); - assert!(!config.require_verified_devices); - } - - #[test] - fn load_respects_require_verified_devices_true() { - let tmp = tempfile::tempdir().unwrap(); - let sk = tmp.path().join(".story_kit"); - fs::create_dir_all(&sk).unwrap(); - fs::write( - sk.join("bot.toml"), - r#" -homeserver = "https://matrix.example.com" -username = "@bot:example.com" -password = "secret" -room_ids = ["!abc:example.com"] -enabled = true -require_verified_devices = true -"#, - ) - .unwrap(); - let config = BotConfig::load(tmp.path()).unwrap(); - assert!(config.require_verified_devices); - } - #[test] fn load_respects_custom_history_size() { let tmp = tempfile::tempdir().unwrap(); @@ -302,4 +255,32 @@ history_size = 50 let config = BotConfig::load(tmp.path()).unwrap(); assert_eq!(config.history_size, 50); } + + #[test] + fn load_ignores_legacy_require_verified_devices_key() { + // Old bot.toml files that still have `require_verified_devices = true` + // must parse successfully — the field is simply ignored now that + // verification is always enforced unconditionally. + let tmp = tempfile::tempdir().unwrap(); + let sk = tmp.path().join(".story_kit"); + fs::create_dir_all(&sk).unwrap(); + fs::write( + sk.join("bot.toml"), + r#" +homeserver = "https://matrix.example.com" +username = "@bot:example.com" +password = "secret" +room_ids = ["!abc:example.com"] +enabled = true +require_verified_devices = true +"#, + ) + .unwrap(); + // Should still load successfully despite the unknown field. + let config = BotConfig::load(tmp.path()); + assert!( + config.is_some(), + "bot.toml with legacy require_verified_devices key must still load" + ); + } }