mgmt: first message to a COLD outreach DM is undecryptable for the recipient (megolm not shared to invited members) #104

Open
opened 2026-07-12 06:40:19 +00:00 by robocub · 3 comments
Collaborator

Split from #97 (mode 2 of the #100 residuals): an operator msg/announce to a user with NO existing DM creates the room and sends immediately; in the (default-encrypted) fresh DM, the megolm session is shared before/without the invited recipient — they accept and find the first message permanently undecryptable.

Not fixed by member-list loading (tried in 5c6cb63: room.get_member server-backed fetch + retry before the send — instance-2's mgmt_dm_pending_reap drill still reproduces the UTD). Working theory: the SDK does not share megolm to INVITED members (and/or MSC3061 shared-history semantics) — the recipient can only ever decrypt messages sent after their join lands in the bot's view.

Candidate fixes to evaluate:

  1. dm_user waits (bounded) for the recipient to JOIN before the first send — correct-by-construction, adds delivery latency; needs a queue for announce so N cold recipients don't serialize a handler.
  2. Discard + re-share the outbound session once the join is seen, resending the message.
  3. Deep: verify matrix-sdk's shared-history/invited-member share behavior and use it properly.

Scope: operator outreach to cold recipients only. Self-serve users (who initiate the DM themselves) are unaffected — live-proven E2EE DM with a real user 2026-07-12. The e2e scenario mgmt_dm_pending_reap stays KnownFail pointing HERE, so the suite tracks it honestly; #97 itself (invite races + reap, mode 1) is fully fixed and closes with the next release.

Split from #97 (mode 2 of the #100 residuals): an operator `msg`/`announce` to a user with NO existing DM creates the room and sends immediately; in the (default-encrypted) fresh DM, the megolm session is shared before/without the invited recipient — they accept and find the first message permanently undecryptable. **Not fixed by member-list loading** (tried in `5c6cb63`: `room.get_member` server-backed fetch + retry before the send — instance-2's `mgmt_dm_pending_reap` drill still reproduces the UTD). Working theory: the SDK does not share megolm to INVITED members (and/or MSC3061 shared-history semantics) — the recipient can only ever decrypt messages sent after their join lands in the bot's view. Candidate fixes to evaluate: 1. `dm_user` waits (bounded) for the recipient to JOIN before the first send — correct-by-construction, adds delivery latency; needs a queue for `announce` so N cold recipients don't serialize a handler. 2. Discard + re-share the outbound session once the join is seen, resending the message. 3. Deep: verify matrix-sdk's shared-history/invited-member share behavior and use it properly. Scope: operator outreach to cold recipients only. Self-serve users (who initiate the DM themselves) are unaffected — live-proven E2EE DM with a real user 2026-07-12. The e2e scenario `mgmt_dm_pending_reap` stays `KnownFail` pointing HERE, so the suite tracks it honestly; #97 itself (invite races + reap, mode 1) is fully fixed and closes with the next release.
Author
Collaborator

Root-cause update (2026-07-13): this is NOT "megolm not shared to invited members." The bridge shares the key correctly — the failure is federated to-device (EDU) delivery.

The proposed fix was built (server-authoritative latest_encryption_state + Room::members() fetch, plus request_user_identity to force a synchronous federated /keys/query, before the send — covering both msg and announce) and live-tested against the nether.autos fixture. fed_cold_outreach_utd still failed deterministically. Crypto-level tracing (matrix_sdk_crypto=debug) shows why the fix was aimed at a non-problem:

  • Bridge side — the key IS shared to the invited remote recipient. share_room_key for the DM room lists the target among the recipients ("Trying to encrypt a room key"{@…:nether.autos: {<device>}}), a to-device batch is created for that device, and the outbound session is "marked as shared". The recipient's device is discovered and included even while they are merely invited — device discovery was never broken.
  • Target side — the key never arrives. Capturing the receiving crypto client's log across the full 180 s wait (the actor runs in the nvb-e2e process, so RUST_LOG=nvb_e2e=info,matrix_sdk_crypto=debug captures it with no bridge rebuild), it never logs "Received a new megolm room key" for the DM room. The to-device event carrying the session is not delivered to it.
  • Same-server works. mgmt_dm_e2ee — identical client and send path, same homeserver — passes. The only differing variable in fed_cold_outreach_utd is the federation hop.

⇒ The federated failure is to-device delivery dropping the room-key EDU to the recipient's homeserver, i.e. a federation/infrastructure gap (same family as the known Tuwunel federation issues), not a bridge defect. The attempted bridge-side fix has been reverted; fed_cold_outreach_utd stays KnownFail{#104}.

Note: request_user_identity does exist in matrix-sdk 0.18 and performs an up-to-date federated /keys/query, but it's irrelevant here since discovery already succeeds.

Next: confirm which hop drops the EDU (sender outbound vs fixture inbound) from server logs, and retarget the scenario as a fixture/federation-delivery check rather than a bridge-behaviour assertion. Suggest re-scoping the title away from "megolm not shared to invited members."

**Root-cause update (2026-07-13): this is NOT "megolm not shared to invited members." The bridge shares the key correctly — the failure is federated to-device (EDU) delivery.** The proposed fix was built (server-authoritative `latest_encryption_state` + `Room::members()` fetch, plus `request_user_identity` to force a synchronous federated `/keys/query`, before the send — covering both `msg` and `announce`) and live-tested against the `nether.autos` fixture. `fed_cold_outreach_utd` still failed deterministically. Crypto-level tracing (`matrix_sdk_crypto=debug`) shows why the fix was aimed at a non-problem: - **Bridge side — the key IS shared to the invited remote recipient.** `share_room_key` for the DM room lists the target among the recipients (`"Trying to encrypt a room key"` → `{@…:nether.autos: {<device>}}`), a to-device batch is created for that device, and the outbound session is `"marked as shared"`. The recipient's device is discovered and included even while they are merely *invited* — device discovery was never broken. - **Target side — the key never arrives.** Capturing the *receiving* crypto client's log across the full 180 s wait (the actor runs in the `nvb-e2e` process, so `RUST_LOG=nvb_e2e=info,matrix_sdk_crypto=debug` captures it with no bridge rebuild), it **never** logs `"Received a new megolm room key"` for the DM room. The to-device event carrying the session is not delivered to it. - **Same-server works.** `mgmt_dm_e2ee` — identical client and send path, same homeserver — passes. The only differing variable in `fed_cold_outreach_utd` is the federation hop. ⇒ The federated failure is **to-device delivery dropping the room-key EDU to the recipient's homeserver**, i.e. a federation/infrastructure gap (same family as the known Tuwunel federation issues), **not a bridge defect**. The attempted bridge-side fix has been reverted; `fed_cold_outreach_utd` stays `KnownFail{#104}`. Note: `request_user_identity` does exist in matrix-sdk 0.18 and performs an up-to-date federated `/keys/query`, but it's irrelevant here since discovery already succeeds. Next: confirm which hop drops the EDU (sender outbound vs fixture inbound) from server logs, and retarget the scenario as a fixture/federation-delivery check rather than a bridge-behaviour assertion. Suggest re-scoping the title away from "megolm not shared to invited members."
Author
Collaborator

Possible upstream fix in Tuwunel v1.8.2 (2026-07-17). The 07-13 diagnosis of the federated variant (fed_cold_outreach_utd) was that the bridge shares + dispatches the megolm key correctly, but the to-device EDU carrying it never arrives at the remote recipient — i.e. a federated to-device delivery drop, infrastructure rather than bridge code (same-server mgmt_dm_e2ee passes; only the federation hop differs). v1.8.2 carries two changes squarely in that area:

  • "Federation delivery no longer runs hot against a peer that has come back — the backoff gate now consults full failure history instead of just the current time bucket, preventing re-authorization at timeout boundaries."
  • "A proxy or CDN answering a federation request with non-JSON is treated as transient rather than evicting the route outright," plus profile queries now return 404 instead of an empty 200 (empty-200 hardening).

Prod nether.im is already on 1.8.2; the fixture nether.autos is being upgraded 1.8.1 → 1.8.2 now. Next: re-run fed_cold_outreach_utd against the upgraded fixture and record the result here — if it flips to Pass, #104's federated failure was this upstream federation-delivery bug, not anything in the bridge.

Release: https://github.com/matrix-construct/tuwunel/releases/tag/v1.8.2

**Possible upstream fix in Tuwunel v1.8.2** (2026-07-17). The 07-13 diagnosis of the *federated* variant (`fed_cold_outreach_utd`) was that the bridge shares + dispatches the megolm key correctly, but the **to-device EDU carrying it never arrives** at the remote recipient — i.e. a federated to-device *delivery* drop, infrastructure rather than bridge code (same-server `mgmt_dm_e2ee` passes; only the federation hop differs). v1.8.2 carries two changes squarely in that area: - *"Federation delivery no longer runs hot against a peer that has come back — the backoff gate now consults full failure history instead of just the current time bucket, preventing re-authorization at timeout boundaries."* - *"A proxy or CDN answering a federation request with non-JSON is treated as transient rather than evicting the route outright,"* plus profile queries now return 404 instead of an empty 200 (empty-200 hardening). Prod `nether.im` is already on 1.8.2; the fixture `nether.autos` is being upgraded 1.8.1 → 1.8.2 now. **Next: re-run `fed_cold_outreach_utd` against the upgraded fixture and record the result here** — if it flips to Pass, #104's federated failure was this upstream federation-delivery bug, not anything in the bridge. Release: https://github.com/matrix-construct/tuwunel/releases/tag/v1.8.2
Author
Collaborator

v1.8.2 retest — NEGATIVE (not fixed by the version bump). Upgraded the fixture nether.autos 1.8.1 → 1.8.2 (both prod nether.im and the fixture now on 1.8.2, signing key preserved) and re-ran fed_cold_outreach_utd: still known_fail (operator message not decryptable by the remote target within the window). So the v1.8.2 federation-delivery/empty-200 changes did NOT resolve #104s federated to-device drop on their own. Note the same-server twin mgmt_dm_pending_reap now unexpected-passes — worth confirming whether that improvement is 1.8.2-related and, if stable, flipping its expectation. #104 federated variant stays open; next angle is a fresh crypto-trace on the upgraded fixture to see whether the to-device EDU now reaches the target server at all.

**v1.8.2 retest — NEGATIVE (not fixed by the version bump).** Upgraded the fixture `nether.autos` 1.8.1 → 1.8.2 (both prod `nether.im` and the fixture now on 1.8.2, signing key preserved) and re-ran `fed_cold_outreach_utd`: still `known_fail` (operator message not decryptable by the remote target within the window). So the v1.8.2 federation-delivery/empty-200 changes did NOT resolve #104s federated to-device drop on their own. Note the same-server twin `mgmt_dm_pending_reap` now *unexpected-passes* — worth confirming whether that improvement is 1.8.2-related and, if stable, flipping its expectation. #104 federated variant stays open; next angle is a fresh crypto-trace on the upgraded fixture to see whether the to-device EDU now reaches the target server at all.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dark/nether-voicebridge#104
No description provided.