omemo: trust given before a device's key is in is recorded as verified, kept in memory only, and not reported #5

Closed
opened 2026-10-16 06:22:40 +00:00 by asxpi · 1 comment

omemo trust for a device whose key hasn't arrived yet (listed, but no trust row) goes into PendingTrust (tacky/xmpp/modules/omemo.tcl, rc5 d1cca8e5): the bundle is fetched and the choice applied when it lands. Found from anotherim-js 0.5.2, where an OMEMO 2 device of a room member with no fetched bundle offered Trust (another-im/anotherim-js#12 now offers no choice for such a row).

1. A Trust given ahead of the key is recorded as verified. ApplyPendingTrust (l. 6033) calls RecordDecision for a non-blind pending Trust once the bundle is in. That pins the fetched key as the one the user verified, but the user was never shown it, and the device then reads "Verified". A pending Trust should be refused (OMEMO TRUST_NO_KEY) or at most become blind trust.

2. It is in memory only. variable PendingTrust {} (l. 970) isn't stored, so a restart forgets it. A Don't trust given in advance silently stops applying.

3. trust_list doesn't report it. The row keeps showing the stored trust (undecided), so a host can't show that a choice is pending. In the app, Don't trust looked like it did nothing.

4. It applies only from a successful FetchBundle. If the fetch fails, the choice waits for good, with no retry and nothing reported. I haven't checked whether a key arriving another way (an incoming message's key exchange) takes it up.

Suggested:

  • Refuse trust -state trusted with no known key.
  • Keep a pending refusal with the device (store) and report it in trust_list, e.g. pending: "untrusted".
  • Apply it whenever the device's key first arrives, by whatever path.

With blind trust on, a refusal ahead of the key is what stops a device someone added to a contact's list from being encrypted to as soon as its key appears.

`omemo trust` for a device whose key hasn't arrived yet (listed, but no trust row) goes into `PendingTrust` (`tacky/xmpp/modules/omemo.tcl`, rc5 `d1cca8e5`): the bundle is fetched and the choice applied when it lands. Found from anotherim-js 0.5.2, where an OMEMO 2 device of a room member with no fetched bundle offered Trust (another-im/anotherim-js#12 now offers no choice for such a row). **1. A Trust given ahead of the key is recorded as verified.** `ApplyPendingTrust` (l. 6033) calls `RecordDecision` for a non-blind pending Trust once the bundle is in. That pins the fetched key as the one the user verified, but the user was never shown it, and the device then reads "Verified". A pending Trust should be refused (`OMEMO TRUST_NO_KEY`) or at most become blind trust. **2. It is in memory only.** `variable PendingTrust {}` (l. 970) isn't stored, so a restart forgets it. A Don't trust given in advance silently stops applying. **3. `trust_list` doesn't report it.** The row keeps showing the stored trust (`undecided`), so a host can't show that a choice is pending. In the app, Don't trust looked like it did nothing. **4. It applies only from a successful `FetchBundle`.** If the fetch fails, the choice waits for good, with no retry and nothing reported. I haven't checked whether a key arriving another way (an incoming message's key exchange) takes it up. **Suggested:** - Refuse `trust -state trusted` with no known key. - Keep a pending refusal with the device (store) and report it in `trust_list`, e.g. `pending: "untrusted"`. - Apply it whenever the device's key first arrives, by whatever path. With blind trust on, a refusal ahead of the key is what stops a device someone added to a contact's list from being encrypted to as soon as its key appears.
ann referenced this issue from a commit 2026-10-16 11:16:47 +00:00
A choice made in `omemo trust` for a listed device whose key is not in yet is kept and shown, and a trust is refused (#5): `-state trusted`, blind or not, raises {OMEMO TRUST_NO_KEY}, since the key that lands would be pinned as one the user verified without ever being shown it; `untrusted` is stored as the row pending/<jid>/<dev> of module omemo (it was the in-memory PendingTrust alone, which a restart forgot), loaded with the device rows, kept by a new identity and carried by a backup as they are, and reported by trust_list as OmemoTrust's new `pending` field while `trust` still reads undecided; `undecided` takes it back. Every path that first records a device's key (SetBundle for a bundle, Unwrap for a key exchange) makes the row with it through NewRowTrust, and Save drops the pending row in the commit that writes the device row, so a failed fetch leaves it pending and a later fetch or an incoming key exchange applies it; ApplyPendingTrust and PendingBlind are gone. With blind trust on, such a refusal is what keeps a device someone added to a contact's list from being encrypted to once its key appears. The schema's trust doc and the new field, docs/features.md and ROBOTS.md say so. test_omemo.tcl's two tests of a trust applied ahead of the key and test_omemo_prompt.tcl's two of a blind one are replaced by tests of the refusal, the pending report through a failed fetch, a restart, a key exchange and undecided, each failing without the change, and test_backup_format.tcl keeps a pending row. In the container, after rebuilding the stale OMEMO binding: check-api clean; the 15 suites matching *omemo*.tcl 456/456, test_storage*.tcl 99/99, test_backup_format.tcl 22/22 and test_wire*.tcl 54/54 pass.
Owner

Fixed on develop as 77050f5f (fix/omemo-pending-trust, merged 8a7db33c, pushed 2026-10-16), for the next tag (rc7). As suggested: trust -state trusted for a listed device whose key is not in yet is refused with {OMEMO TRUST_NO_KEY}, blind or not (the key that landed would be pinned as one the user verified without ever being shown it; ApplyPendingTrust and PendingBlind are gone). An untrusted given ahead of the key is stored as the row pending// of module omemo (a restart keeps it, a backup carries it like the device/ rows), reported by trust_list as OmemoTrust's new pending field while trust still reads undecided, and undecided takes it back. Every path that first records the device's key makes its row with that trust (SetBundle for a bundle, Unwrap for an incoming key exchange) and the row's write retires the pending row in the same commit, so a failed fetch leaves it pending and reported, with no retry of its own. Not synced to the account's other devices (not asked). Tests: the refusal, the pending report through a failed fetch, a restart, a key exchange, undecided, the backup fixture; omemo suites 456/456.

Fixed on develop as 77050f5f (fix/omemo-pending-trust, merged 8a7db33c, pushed 2026-10-16), for the next tag (rc7). As suggested: `trust -state trusted` for a listed device whose key is not in yet is refused with {OMEMO TRUST_NO_KEY}, blind or not (the key that landed would be pinned as one the user verified without ever being shown it; ApplyPendingTrust and PendingBlind are gone). An `untrusted` given ahead of the key is stored as the row pending/<jid>/<dev> of module omemo (a restart keeps it, a backup carries it like the device/ rows), reported by trust_list as OmemoTrust's new `pending` field while `trust` still reads undecided, and `undecided` takes it back. Every path that first records the device's key makes its row with that trust (SetBundle for a bundle, Unwrap for an incoming key exchange) and the row's write retires the pending row in the same commit, so a failed fetch leaves it pending and reported, with no retry of its own. Not synced to the account's other devices (not asked). Tests: the refusal, the pending report through a failed fetch, a restart, a key exchange, undecided, the backup fixture; *omemo* suites 456/456.
ann closed this issue 2026-10-16 11:18:35 +00:00
ann referenced this issue from a commit 2026-10-27 00:26:42 +00:00
schedule: the scheduler is our own server or a gateway we are registered with, never any domain that says it schedules, and resubmits, releases and cancels lose nothing, from the audit of 2026-10-26 (/data/another-im/audit-2026-10-26-libtacky.md #1 Critical, #3, #4, #5; S5 of the schedule sub-report): a chat's domain is its scheduler only when the account is registered with it (in the roster at to or both, roster transportUser's rule) and its disco#info names a gateway identity and the feature (Registered, Trusted); any other domain, a plain server running mod_schedule included, is never asked, and a <changed/> from anyone but our server or a registered gateway about a chat at itself is ignored before anything is asked or read, so a federated domain can no longer hold, drop or plant our scheduled messages or make us send disco#info to domains it names; a redirect is followed only to a registered gateway; a listed row is read with omemo decryptListed, side-effect free. Resubmit writes the msg column (it wrote row, which the table has not, so send_now of a refused submit and an edit of one never taken always threw). A submit left waiting by a lost answer goes again at the next sign-in or resumption only after its chat's list answered (SubmitWaiting, RecheckThenSubmit), which takes the item back by origin-id, and an edit made meanwhile is put to that item by <replace/> (PutChange) instead of a second submit. An item gone from a list, or a cancel answered item-not-found, keeps its row hidden as released for its copy (30 days) instead of deleting it when its time was ahead, which lost a send_now from another device or a release racing a cancel from this device's history; a <changed what='cancelled'/> naming the item drops it at once. docs/protocol/schedule-0.md §3, §4.4, §5, §6, §7 and §8 say so. Unit tests in test_schedule.tcl, each failing without the change but the cancelled-headline guard; test_schedule 37 of 37 in the container. Nothing changes for tackygram, which is a registered gateway with identity gateway/telegram; anotherim-server mod_schedule could also answer a submit repeating one of the user's origin-ids with that item.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
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
another-im/libtacky#5
No description provided.