feat(cli): add bootstrap-cross-signing subcommand #4

Merged
jmz merged 2 commits from kitty/sync-wedge-resilience-fixes into main 2026-05-02 12:37:36 +00:00 AGit
Contributor

Bridge users created via matrix-bridge setup (or any login flow that
just calls matrix_auth().login_username()) end up with device-level
crypto but no cross-signing identity published on the homeserver.
Symptom from a peer client: Element X shows "user verification
unavailable" when trying to verify any of this user's devices, because
there's no master key to anchor a verification chain against.

Without cross-signing, Element X (and any other matrix-rust-sdk client
that requires it) refuses to forward Megolm session keys to the bridge's
device on m.room_key_request. So even with
automatic-room-key-forwarding enabled on both ends, historic UTDs stay
UTD because trusted peers refuse to share.

This subcommand calls Encryption::bootstrap_cross_signing() on the
existing session, handles the standard UIA round-trip (first call
returns a UiaaResponse describing the required auth flow; we prompt
for the password and retry with AuthData::Password(...)). Output:

Restoring session for @claudius-felinus:matrix.ziefle.org ...
Attempting bootstrap_cross_signing(None) — expecting UIA challenge ...
Password for @claudius-felinus:matrix.ziefle.org (UIA): ********
Retrying with password auth ...
Cross-signing bootstrapped ✓
  master / self-signing / user-signing keys uploaded for @claudius-felinus:matrix.ziefle.org.
  Other clients (e.g. Element X) can now verify this user's devices.

After running this once per bot user, Element X (or another verifying
client) can do the standard emoji/QR device-verification flow against
the bridge's device, which cross-signs that device against the user's
master key. Peers that trust the user identity then accept Megolm key
forwarding requests from that device.

One-time setup. Idempotent on the homeserver side: subsequent calls
either no-op (matrix-sdk skips if keys already exist) or fail safely
without altering published state.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Bridge users created via `matrix-bridge setup` (or any login flow that just calls `matrix_auth().login_username()`) end up with device-level crypto but no cross-signing identity published on the homeserver. Symptom from a peer client: Element X shows "user verification unavailable" when trying to verify any of this user's devices, because there's no master key to anchor a verification chain against. Without cross-signing, Element X (and any other matrix-rust-sdk client that requires it) refuses to forward Megolm session keys to the bridge's device on `m.room_key_request`. So even with `automatic-room-key-forwarding` enabled on both ends, historic UTDs stay UTD because trusted peers refuse to share. This subcommand calls `Encryption::bootstrap_cross_signing()` on the existing session, handles the standard UIA round-trip (first call returns a UiaaResponse describing the required auth flow; we prompt for the password and retry with `AuthData::Password(...)`). Output: ``` Restoring session for @claudius-felinus:matrix.ziefle.org ... Attempting bootstrap_cross_signing(None) — expecting UIA challenge ... Password for @claudius-felinus:matrix.ziefle.org (UIA): ******** Retrying with password auth ... Cross-signing bootstrapped ✓ master / self-signing / user-signing keys uploaded for @claudius-felinus:matrix.ziefle.org. Other clients (e.g. Element X) can now verify this user's devices. ``` After running this once per bot user, Element X (or another verifying client) can do the standard emoji/QR device-verification flow against the bridge's device, which cross-signs that device against the user's master key. Peers that trust the user identity then accept Megolm key forwarding requests from that device. One-time setup. Idempotent on the homeserver side: subsequent calls either no-op (matrix-sdk skips if keys already exist) or fail safely without altering published state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
matrix-sdk-crypto's automatic-room-key-forwarding feature only fires
key-request to-device events when a UTD is encountered during live
`/sync` processing — NOT when UTDs are returned via `room.messages()`
backfill or initial-sync timeline. Verified empirically: after enabling
the feature flag, 0 outstanding key_requests appeared in the crypto
store after a fresh init sync that delivered known UTDs.

Fix: in `read_messages`, after the `room.messages()` chunk is fetched,
iterate UTDs explicitly and call `OlmMachine::request_room_key()` per
event. matrix-sdk-crypto dedupes by Megolm session_id, so multiple UTD
events sharing one session collapse into one outgoing request. Then
flush via `sync_once(timeout=0)` to push the queued m.room_key_request
to-device events to the homeserver.

Implementation:
* Added matrix-sdk-crypto = "=0.14.0" as a direct dep (transitive type
  access for `Raw<EncryptedEvent>` and `OlmMachine`).
* Added "testing" to the matrix-sdk feature list. matrix-sdk's only
  public path to the OlmMachine in 0.14 is
  `Client::olm_machine_for_testing()`, gated on this feature. Despite
  the misleading suffix, the feature does not pull in test runners or
  affect runtime behaviour beyond exposing accessors.
* `Raw::cast_unchecked` is used to coerce the timeline event's raw
  envelope into `Raw<EncryptedEvent>` — for UTD events the payload IS
  an encrypted-event envelope by construction, so the cast is correct
  by definition.
* Best-effort: any per-event error (or flush sync timeout) is logged
  at debug level and skipped so a single malformed event can't break
  the whole batch.

Caveat: a successful request only results in a decrypted message if a
peer device of the sender has the Megolm session in its store AND
chooses to forward it (cross-signed-trusted requesting device, etc.).
For sessions that have rotated out of all peer stores, no amount of
request will recover them — those events are cryptographically
permanent UTDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bridge users created via `matrix-bridge setup` (or any login flow that
just calls `matrix_auth().login_username()`) end up with device-level
crypto but no cross-signing identity published on the homeserver.
Symptom from a peer client: Element X shows "user verification
unavailable" when trying to verify any of this user's devices, because
there's no master key to anchor a verification chain against.

Without cross-signing, Element X (and any other matrix-rust-sdk client
that requires it) refuses to forward Megolm session keys to the bridge's
device on `m.room_key_request`. So even with
`automatic-room-key-forwarding` enabled on both ends, historic UTDs stay
UTD because trusted peers refuse to share.

This subcommand calls `Encryption::bootstrap_cross_signing()` on the
existing session, handles the standard UIA round-trip (first call
returns a UiaaResponse describing the required auth flow; we prompt
for the password and retry with `AuthData::Password(...)`). Output:

```
Restoring session for @claudius-felinus:matrix.ziefle.org ...
Attempting bootstrap_cross_signing(None) — expecting UIA challenge ...
Password for @claudius-felinus:matrix.ziefle.org (UIA): ********
Retrying with password auth ...
Cross-signing bootstrapped ✓
  master / self-signing / user-signing keys uploaded for @claudius-felinus:matrix.ziefle.org.
  Other clients (e.g. Element X) can now verify this user's devices.
```

After running this once per bot user, Element X (or another verifying
client) can do the standard emoji/QR device-verification flow against
the bridge's device, which cross-signs that device against the user's
master key. Peers that trust the user identity then accept Megolm key
forwarding requests from that device.

One-time setup. Idempotent on the homeserver side: subsequent calls
either no-op (matrix-sdk skips if keys already exist) or fail safely
without altering published state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jmz merged commit 7f4a5791a7 into main 2026-05-02 12:37:36 +00:00
Sign in to join this conversation.
No description provided.