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 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-03-14 19:56:38 +00:00
parent 568207687d
commit ec652a6fe8
2 changed files with 65 additions and 85 deletions

View File

@@ -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<TokioMutex<HashSet<OwnedEventId>>>,
/// 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 ------------------------------------------

View File

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