From d497387cec31f4329a25db6abb1e5b934f2c5567 Mon Sep 17 00:00:00 2001 From: Andrew Miller Date: Thu, 23 Apr 2026 22:52:46 -0400 Subject: [PATCH] matrix: auto-bootstrap cross-signing on first startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, every Matrix bot started under hermes-agent shows the "Encrypted by a device not verified by its owner" badge in Element indefinitely, because the cross-signing chain (master → SSK → device) was never published. Operators currently have to write their own bootstrap script and remember to run it once per bot — and it's easy to get wrong (the obvious base64.b64encode().decode() produces padded keyids that matrix-rust-sdk silently rejects in /keys/query, so even correctly-signed keys fail to load identity in Element). mautrix already has the right primitive: generate_recovery_key() does the full flow — generate seeds, upload privates to SSSS, publish publics to the homeserver, sign the current device with the new SSK, and return the human-readable recovery key. We invoke it once on startup if the bot has no existing cross-signing identity, and log the recovery key with a clear instruction to save it for future restarts via MATRIX_RECOVERY_KEY (which the existing recovery-key path already consumes). Skipped when MATRIX_RECOVERY_KEY is set (existing path takes over) or when the bot already has cross-signing keys on the homeserver (get_own_cross_signing_public_keys returns non-None). Bootstrap failure is non-fatal — logged with hint about UIA; the bot continues without cross-signing and Element will show the warning that prompted this PR. That matches the existing soft-fail pattern for verify_with_recovery_key. Tested against Continuwuity 0.5.7 (no UIA required). Synapse with UIA enabled will need a follow-up PR to thread MATRIX_PASSWORD through to /keys/device_signing/upload. --- gateway/platforms/matrix.py | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index 8c07f6fc6..c5685fddb 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -698,6 +698,44 @@ class MatrixAdapter(BasePlatformAdapter): logger.warning( "Matrix: recovery key verification failed: %s", exc ) + else: + # No recovery key — bootstrap cross-signing if the bot + # has none yet. Without this, Element shows "Encrypted + # by a device not verified by its owner" on every + # message from this bot, indefinitely. mautrix's + # generate_recovery_key does the full flow: generates + # MSK/SSK/USK, uploads private keys to SSSS, publishes + # public keys to the homeserver, and signs the current + # device with the new SSK. Some homeservers require UIA + # for /keys/device_signing/upload — those will need an + # alternate path; Continuwuity and Synapse-with-shared- + # secret accept the unauthenticated upload. + try: + own_xsign = await olm.get_own_cross_signing_public_keys() + except Exception as exc: + own_xsign = None + logger.warning( + "Matrix: cross-signing key lookup failed: %s", exc + ) + if own_xsign is None: + try: + new_recovery_key = await olm.generate_recovery_key() + logger.warning( + "Matrix: bootstrapped cross-signing for %s. " + "SAVE THIS RECOVERY KEY — set " + "MATRIX_RECOVERY_KEY for future restarts so " + "the bot can re-sign its device after key " + "rotation: %s", + client.mxid, + new_recovery_key, + ) + except Exception as exc: + logger.warning( + "Matrix: cross-signing bootstrap failed " + "(non-fatal — Element will show 'not " + "verified by its owner'): %s", + exc, + ) client.crypto = olm logger.info(