Skip to content

crypto: update signing_key on device upsert - #566

Closed
IIIIbntttt wants to merge 1 commit into
mautrix:mainfrom
IIIIbntttt:fix/crypto-device-signing-key-upsert
Closed

IIIIbntttt wants to merge 1 commit into
mautrix:mainfrom
IIIIbntttt:fix/crypto-device-signing-key-upsert

Conversation

@IIIIbntttt

Copy link
Copy Markdown

Problem

A client that logs out and logs back in under the same device ID gets a fresh Olm
account. After that round trip crypto_device holds a mixed row: the new
identity_key next to the previous signing_key. Every Olm message from that
device is then dropped with

sender_device_keys: received update for device with different signing key (expected …, got …)

so m.room_key never lands and the megolm session cannot be decrypted. Seen in
production on a bridge: the user silently stopped being able to send anything.

How the row gets mixed

  1. Logging out does not delete the row — PutDevices only sets deleted=true.
  2. The client comes back with the same device ID and a new Olm account.
  3. validateDevice would reject the new keys on a signing key mismatch, but the
    existing-device lookup goes through GetDevices, which filters deleted=false.
    The soft-deleted row is invisible there, existing == nil, nothing to compare
    against, so the new keys pass.
  4. PutDevices runs INSERT … ON CONFLICT (user_id, device_id) DO UPDATE. The SET
    list covers identity_key, deleted, trust and name — but not
    signing_key, so the old one stays in the row.
  5. The Olm path resolves the device with FindDeviceByKey, which — unlike
    GetDevices — does not filter deleted. It finds the mixed row,
    validateDevice compares the ed25519 key from the envelope against the stale
    signing_key, and the event is discarded.

The change

signing_key=excluded.signing_key added to the ON CONFLICT SET list.

Outside tests PutDevice is called from crypto/machine.go (own device on start)
and from verificationhelper (sas.go, reciprocate.go); in the latter three the
device comes straight out of GetOrFetchDevice, so the signing key only
round-trips through the store and the change is a no-op there. PutDevices is
called from crypto/devicelist.go for /keys/query results, which have already
passed validateDevice.

Test

crypto/signing_key_repro_test.go reproduces the round trip on the existing
getCryptoStores helper. Without the change it fails with
expected "signing-2", actual "signing-1"; with it ./crypto/... is green
(-tags goolm; crypto/libolm needs libolm headers and was not built).

Not fixed here

The asymmetry underneath is left alone: GetDevices filters deleted=false,
FindDeviceByKey does not. The validation path cannot see a soft-deleted row and
lets new keys through, while the Olm path sees that same row and rejects
everything against it. Whether to align the two is your call.

A client that logs out and logs back in under the same device ID gets a fresh
Olm account. Logging out only sets deleted=true, so the row survives, and the
ON CONFLICT SET list did not carry signing_key: the row came back with the new
identity_key next to the previous signing_key.

FindDeviceByKey does not filter deleted, so the Olm path resolves that mixed row
and validateDevice rejects every to-device event from the device with "received
update for device with different signing key". m.room_key never lands and the
megolm session cannot be decrypted.
@tulir tulir closed this Sep 10, 2026
@IIIIbntttt

Copy link
Copy Markdown
Author

@tulir Why did you close my PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants