Matrix: cross-signing, so a verified device is verified everywhere #94

Closed
opened 2026-09-02 23:25:38 -04:00 by Salastil · 2 comments
Owner

moho verifies your own sessions against each other and stops there: crypto.rs:217 skips SignatureUpload and verification.rs:164 discards the upload from Sas::confirm(). The UI now says so (#60), which was the honest half of the fix.

The consequence stands: a device verified in moho shows unverified in Element, you cannot verify other people, and none of the green-shield trust Element users rely on works.

Done looks like: cross-signing keys are bootstrapped and uploaded, other users can be verified, and their devices' trust is shown on messages.

moho verifies your own sessions against each other and stops there: `crypto.rs:217` skips `SignatureUpload` and `verification.rs:164` discards the upload from `Sas::confirm()`. The UI now says so (#60), which was the honest half of the fix. The consequence stands: a device verified in moho shows unverified in Element, you cannot verify *other people*, and none of the green-shield trust Element users rely on works. **Done looks like:** cross-signing keys are bootstrapped and uploaded, other users can be verified, and their devices' trust is shown on messages.
Salastil added the criticalfeature labels 2026-09-03 13:11:29 -04:00
Author
Owner

What landed. The half that was actually broken: a verification's signature is now published.

send_one skipped SignatureUpload outright and confirm_sas dropped the upload it was handed, so the emoji matched, both sides agreed, and nobody ever told the homeserver. Both now go to /keys/signatures/upload, and per-key refusals arriving inside a 200 are logged rather than swallowed - a signature the server rejected is a device that keeps showing unverified, and silence there is the same bug wearing a different hat.

Also in:

  • Cross-signing is created and published where an account has none, over the homeserver's user-interactive auth - the same password gate removing a device goes through.
  • After a verification completes, the other session is asked for the cross-signing keys this one lacks; the answer arrives as a to-device secret through the sync loop already running.
  • Verification takes a user id, so it is no longer restricted to your own devices. Which key signs the result follows from whose device it is: self-signing for your own, user-signing for anybody else's.
  • The sessions panel reads the account's real state instead of the flat "moho does no cross-signing" disclaimer.

The careful part, recorded because it nearly went the other way. bootstrap_cross_signing(false) decides whether to mint keys by looking at what this client holds. An identity created in Element sits on the homeserver with its private half on Element's machine, so the crate would happily mint a second one - and publishing that replaces the first, un-verifying every device on the account, everywhere, for everyone. Bootstrapping now refuses when the server already has an identity this client cannot sign with, and says what to do instead.

What is left, and why this stays open.

  1. Verifying another user from the UI. The daemon accepts it; there is no way to start one. The emoji dialog is rendered inside the account panel, so a flow begun from a profile card would have no visible dialog - it needs lifting out to a global surface first, the way the profile card and the call banner already are.
  2. Trust shown on messages. Nothing per-message yet. The sender device's trust is known at decrypt time and would need carrying through record_message, storage and the wire before a shield could be drawn.

Measured: both Matrix accounts on this machine report hasIdentity, hasMaster, canSignDevices, canSignOthers and thisDeviceSigned all true - so the keys were already here and the missing upload was the entire gap. The next verification should show through to Element.

**What landed.** The half that was actually broken: a verification's signature is now published. `send_one` skipped `SignatureUpload` outright and `confirm_sas` dropped the upload it was handed, so the emoji matched, both sides agreed, and nobody ever told the homeserver. Both now go to `/keys/signatures/upload`, and per-key refusals arriving inside a 200 are logged rather than swallowed - a signature the server rejected is a device that keeps showing unverified, and silence there is the same bug wearing a different hat. Also in: - Cross-signing is created and published where an account has none, over the homeserver's user-interactive auth - the same password gate removing a device goes through. - After a verification completes, the other session is asked for the cross-signing keys this one lacks; the answer arrives as a to-device secret through the sync loop already running. - Verification takes a user id, so it is no longer restricted to your own devices. Which key signs the result follows from whose device it is: self-signing for your own, user-signing for anybody else's. - The sessions panel reads the account's real state instead of the flat "moho does no cross-signing" disclaimer. **The careful part, recorded because it nearly went the other way.** `bootstrap_cross_signing(false)` decides whether to mint keys by looking at what *this* client holds. An identity created in Element sits on the homeserver with its private half on Element's machine, so the crate would happily mint a second one - and publishing that replaces the first, un-verifying every device on the account, everywhere, for everyone. Bootstrapping now refuses when the server already has an identity this client cannot sign with, and says what to do instead. **What is left, and why this stays open.** 1. *Verifying another user from the UI.* The daemon accepts it; there is no way to start one. The emoji dialog is rendered inside the account panel, so a flow begun from a profile card would have no visible dialog - it needs lifting out to a global surface first, the way the profile card and the call banner already are. 2. *Trust shown on messages.* Nothing per-message yet. The sender device's trust is known at decrypt time and would need carrying through record_message, storage and the wire before a shield could be drawn. **Measured:** both Matrix accounts on this machine report `hasIdentity`, `hasMaster`, `canSignDevices`, `canSignOthers` and `thisDeviceSigned` all true - so the keys were already here and the missing upload was the entire gap. The next verification should show through to Element.
Author
Owner

Finished — https://git.salastil.com/Salastil/nobilis/commit/e552bbc, https://git.salastil.com/Salastil/nobilis/commit/e0748ea, https://git.salastil.com/Salastil/moho/commit/612eae7 and https://git.salastil.com/Salastil/moho/commit/6a81c50

Both halves this was left open for are in.

Verifying another person. tick asked get_verification_requests(own_user_id) — the machine is asked one user at a time, so a request from anybody else was never looked for. It would not have arrived anyway: cross-user verification travels through the room you share, because until you have agreed which devices you are talking about there is no device to address it to. Room verification events now reach the machine, decrypted where the room is encrypted, and the outgoing half sends the room message it used to skip on purpose. Starting one is offered from a profile card, goes into a direct message made if there is not one, and is refused early in words that say which case it is — your own account is a different gesture, and somebody with no cross-signing identity has nothing to vouch for.

Trust on messages. The verification state the machine works out while decrypting was being discarded. A message from a device this account has never verified now carries a warning beside the sender's name. Only where there was something to check — an encrypted message from somebody else — and only when the answer is no: marking the doubtful ones is what makes the mark mean anything, and a tick on every ordinary message would say nothing while taking the room.

Two fixes fell out of chasing a real report of Element→moho verification doing nothing: the invitation Element opens to carry it was never shown (the invite list was rebuilt from every sync, and only an initial sync carries the whole set), and an incoming verification request was handled nowhere in the client at all — the daemon recorded it and no case ever read it. The emoji dialog also moved out of the account panel, since a request can arrive while that panel is closed.

Not verified end to end from here: completing a real cross-user flow needs somebody to answer in Element.

Finished — https://git.salastil.com/Salastil/nobilis/commit/e552bbc, https://git.salastil.com/Salastil/nobilis/commit/e0748ea, https://git.salastil.com/Salastil/moho/commit/612eae7 and https://git.salastil.com/Salastil/moho/commit/6a81c50 Both halves this was left open for are in. **Verifying another person.** `tick` asked `get_verification_requests(own_user_id)` — the machine is asked one user at a time, so a request from anybody else was never looked for. It would not have arrived anyway: cross-user verification travels through the room you share, because until you have agreed which devices you are talking about there is no device to address it to. Room verification events now reach the machine, decrypted where the room is encrypted, and the outgoing half sends the room message it used to skip on purpose. Starting one is offered from a profile card, goes into a direct message made if there is not one, and is refused early in words that say which case it is — your own account is a different gesture, and somebody with no cross-signing identity has nothing to vouch for. **Trust on messages.** The verification state the machine works out while decrypting was being discarded. A message from a device this account has never verified now carries a warning beside the sender's name. Only where there was something to check — an encrypted message from somebody else — and only when the answer is no: marking the doubtful ones is what makes the mark mean anything, and a tick on every ordinary message would say nothing while taking the room. Two fixes fell out of chasing a real report of Element→moho verification doing nothing: the invitation Element opens to carry it was never shown (the invite list was rebuilt from every sync, and only an initial sync carries the whole set), and an incoming verification request was handled nowhere in the client at all — the daemon recorded it and no case ever read it. The emoji dialog also moved out of the account panel, since a request can arrive while that panel is closed. Not verified end to end from here: completing a real cross-user flow needs somebody to answer in Element.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#94