From 0d40583991570e827ee2c00cafb80b5820a22efd Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 15 Jul 2026 18:12:13 +0200 Subject: [PATCH] feat(#2482): bootstrap cross-signing so agent matrix devices show verified --- hive-matrix-mcp/src/client.rs | 57 ++++++++++++++++++++++++++++++++--- hive-matrix-mcp/src/main.rs | 4 +++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/hive-matrix-mcp/src/client.rs b/hive-matrix-mcp/src/client.rs index ca24e1c3..867ced5d 100644 --- a/hive-matrix-mcp/src/client.rs +++ b/hive-matrix-mcp/src/client.rs @@ -12,10 +12,15 @@ //! //! E2EE is enabled via `with_encryption_settings(EncryptionSettings::default())`. //! Crypto keys are persisted in the sqlite store under `state_dir` -//! (survives container restarts, lost on `--purge`). Cross-signing and -//! automatic key backup are deliberately left at their defaults (disabled) -//! for the first pass: bot accounts authenticated with a static bearer token -//! can't bootstrap cross-signing without MSC3967 on the server side. +//! (survives container restarts, lost on `--purge`). +//! +//! Cross-signing is bootstrapped best-effort after session restore +//! (`ensure_cross_signing`) so the agent's device stops showing as +//! "unverified" in other users' Matrix clients: the in-hive tuwunel accepts +//! the initial device-signing-key upload on the bearer token alone (no UIAA +//! re-auth). A homeserver that DID require UIAA just fails the call, which is +//! swallowed — the session stays exactly as unverified as before. Automatic +//! key backup is still left at its default (disabled). use std::path::Path; @@ -204,6 +209,50 @@ async fn whoami(homeserver: &str, token: &str) -> Result<(OwnedUserId, OwnedDevi Ok((user_id, device_id)) } +/// Best-effort: bootstrap this account's cross-signing identity so its +/// device stops showing as "unverified" in other users' Matrix clients. +/// +/// `bootstrap_cross_signing_if_needed(None)` is idempotent — it queries the +/// account's existing keys and only uploads the master / self-signing / +/// user-signing key set when no cross-signing identity exists yet. That +/// upload also self-signs the current device, which is what actually clears +/// the unverified warning. On a restart with the identity already present +/// it's a no-op. +/// +/// The `None` is the UIAA auth: the in-hive tuwunel accepts the initial +/// `device_signing/upload` on the bearer token alone (no interactive +/// re-auth). If a homeserver required UIAA the call returns a `UiaaResponse` +/// error we can't satisfy with a bare bearer token — so this is best-effort: +/// the failure is logged and swallowed, leaving the session no worse off than +/// before. Never breaks account bring-up. +/// +/// Known gap (self-verify only): this signs the *current* device against a +/// freshly-bootstrapped identity. A device re-provisioned under an already +/// existing identity (new `device_id`, e.g. after a token reissue) would need +/// the cross-signing secret keys via secret storage to re-sign itself — +/// tracked as the longer-term mutual-verification follow-up. +pub async fn ensure_cross_signing(client: &Client, account: &str) { + match client + .encryption() + .bootstrap_cross_signing_if_needed(None) + .await + { + Ok(()) => { + tracing::info!( + account, + "matrix cross-signing ensured (device self-verified)" + ); + } + Err(e) => { + tracing::warn!( + account, + error = %format!("{e:#}"), + "matrix cross-signing bootstrap failed; session stays unverified (non-fatal)" + ); + } + } +} + /// Best-effort: set this account's matrix avatar from the rasterized icon /// PNG. The path comes from `HIVE_ICON_PNG` (a nix-built derivation the /// harness forwards into the daemon env; absent when no icon resolves). diff --git a/hive-matrix-mcp/src/main.rs b/hive-matrix-mcp/src/main.rs index a6af4a57..9d9a0b61 100644 --- a/hive-matrix-mcp/src/main.rs +++ b/hive-matrix-mcp/src/main.rs @@ -286,6 +286,10 @@ async fn bring_up_account( // the live (authenticated, correct-homeserver) Client. Replaces the old // curl oneshot; failures are swallowed inside sync_avatar. client::sync_avatar(&client, &cfg.state_dir, &cfg.name).await; + // Best-effort: bootstrap cross-signing so the account's device isn't + // flagged "unverified" in other users' clients. Idempotent + swallows + // failures (a UIAA-requiring homeserver) inside ensure_cross_signing. + client::ensure_cross_signing(&client, &cfg.name).await; timeline::install_message_handler(&client, hyperhive_socket.to_path_buf(), tag.clone()); let sync_client = client.clone();