feat(#2482): bootstrap cross-signing so agent matrix devices show verified

This commit is contained in:
damocles 2026-07-15 18:12:13 +02:00 committed by mara
commit 0d40583991
2 changed files with 57 additions and 4 deletions

View file

@ -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).

View file

@ -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();