feat(cli): verify-own-devices — cross-sign sibling bridge devices #18

Merged
jmz merged 1 commit from feat/own-device-cross-signing into main 2026-06-11 09:16:06 +00:00
Owner

What

Adds a verify-own-devices [--device-id ID] CLI subcommand that signs this user's own other devices with our self-signing key.

Why

bootstrap-cross-signing only signs the device it runs on, and trust.rs deliberately skips our own user (member.user_id() == user_id → continue). So sibling bridge devices — the same @claudius-maximus bot logged in from mba / rpi5 / nas — never receive a cross-signing signature. Peers then refuse to forward Megolm keys to them, and inbound stays [unable to decrypt]. This is the bridge-code gap maximus@nas handed back in jmz/pa#20 (Jun 11).

How

Run from the device that holds the self-signing secret (currently the NAS):

  1. Acquire the single-instance store lock (don't race a running bridge on the Olm store).
  2. Restore + sync_once so the device list and cross-signing state are current.
  3. cross_signing_status() → bail with guidance if has_self_signing is false.
  4. get_user_devices(own_user) → for each unverified, non-current device (or just --device-id), call Device::verify() which uploads a self-signing signature.

Idempotent (already-verified devices are skipped); the selection logic is a pure should_sign_device() with unit tests.

Verification

  • Not built locally (disk constraints on the dev host) — relying on CI cargo check --all to confirm it compiles against matrix-sdk 0.14. Every SDK method used (get_user_devices/.devices()/is_verified()/verify()/device_id()) is already exercised in trust.rs; the only new surface (cross_signing_status/CrossSigningStatus) is confirmed against docs.rs.
  • CI runs cargo check --all, which does not compile the #[cfg(test)] module, so the unit tests are not executed in CI.
  • Live verification pending: run on the NAS against mba-726dadit + rpi5 once built.

Operational note

The command supersedes any running bridge on the same store for its duration (single-instance invariant) — reconnect/restart the bridge MCP after running it.

This is the immediate unblock. The durable fix (SSSS recovery-key bootstrap so every bridge instance self-signs on startup) follows as a separate PR.

Refs jmz/pa#20

## What Adds a `verify-own-devices [--device-id ID]` CLI subcommand that signs **this user's own other devices** with our self-signing key. ## Why `bootstrap-cross-signing` only signs the device it runs on, and `trust.rs` deliberately skips our own user (`member.user_id() == user_id → continue`). So sibling bridge devices — the same `@claudius-maximus` bot logged in from mba / rpi5 / nas — never receive a cross-signing signature. Peers then refuse to forward Megolm keys to them, and inbound stays `[unable to decrypt]`. This is the bridge-code gap maximus@nas handed back in jmz/pa#20 (Jun 11). ## How Run from the device that **holds the self-signing secret** (currently the NAS): 1. Acquire the single-instance store lock (don't race a running bridge on the Olm store). 2. Restore + `sync_once` so the device list and cross-signing state are current. 3. `cross_signing_status()` → bail with guidance if `has_self_signing` is false. 4. `get_user_devices(own_user)` → for each unverified, non-current device (or just `--device-id`), call `Device::verify()` which uploads a self-signing signature. Idempotent (already-verified devices are skipped); the selection logic is a pure `should_sign_device()` with unit tests. ## Verification - **Not built locally** (disk constraints on the dev host) — relying on CI `cargo check --all` to confirm it compiles against matrix-sdk 0.14. Every SDK method used (`get_user_devices`/`.devices()`/`is_verified()`/`verify()`/`device_id()`) is already exercised in `trust.rs`; the only new surface (`cross_signing_status`/`CrossSigningStatus`) is confirmed against docs.rs. - CI runs `cargo check --all`, which does **not** compile the `#[cfg(test)]` module, so the unit tests are not executed in CI. - **Live verification pending**: run on the NAS against `mba-726dadit` + `rpi5` once built. ## Operational note The command supersedes any running bridge on the same store for its duration (single-instance invariant) — reconnect/restart the bridge MCP after running it. This is the immediate unblock. The durable fix (SSSS recovery-key bootstrap so every bridge instance self-signs on startup) follows as a separate PR. Refs jmz/pa#20
feat(cli): add verify-own-devices to cross-sign sibling bridge devices
Some checks failed
check / check (pull_request) Successful in 3m20s
check / check (push) Has been cancelled
484afdddf3
bootstrap-cross-signing only signs the device it runs on, and the trust
policy deliberately skips our own user — so sibling bridge devices (the
same bot logged in from several hosts) never receive a self-signing
signature, and peers refuse to forward Megolm keys to them, leaving
inbound undecryptable.

Add `verify-own-devices [--device-id ID]`: run from the device that
holds the self-signing secret, it syncs, verifies the secret is present
(clear bail otherwise), then signs each unverified own device via
Device::verify(). Holds the single-instance store lock so it can't race
a running bridge on the Olm store. The signing decision is factored into
a pure should_sign_device() with unit tests (skip current device, skip
already-verified, honor --device-id).

Immediate unblock for inbound decrypt on mba-726dadit / rpi5; the durable
SSSS recovery-key fix follows separately.

Refs jmz/pa#20

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jmz merged commit 484afdddf3 into main 2026-06-11 09:16:06 +00:00
Sign in to join this conversation.
No description provided.