OMEMO cannot bootstrap against prosody-class servers #169

Open
opened 2026-07-16 14:32:18 +00:00 by jabber.developer2 · 0 comments
Collaborator

Summary

OMEMO does not work at all between two fresh cproof accounts on a prosody server: /omemo gen never publishes the device list or the key bundle, so /omemo start on the peer ends with Can't find a OMEMO device id for <jid> / This message cannot be encrypted for any recipient. Both root causes are ejabberd-isms in the client (the OMEMO guide examples use ejabberd.local, which is presumably why this was never hit). Found by live-testing two client instances against prosody 13 in Docker; both fixes below were validated in that setup (encrypted exchange works both ways with them).

Bug 1: "node does not exist" is only recognized via the legacy code="404" error attribute

_omemo_receive_devicelist() (src/xmpp/omemo.c, around line 449) handles an error reply to the device-list fetch like this:

const char* code = xmpp_stanza_get_attribute(error, "code");
if (g_strcmp0(code, "404") == 0) {
    omemo_set_device_list(from, NULL);
    return 1;
}

ejabberd attaches the legacy numeric code attribute, prosody sends the plain RFC 6120 defined condition:

<iq type="error" ...><error type="cancel"><item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error></iq>

so the empty-list bootstrap (omemo_set_device_list(from, NULL)) never runs, the own device id is never merged, and /omemo gen never publishes the device list.

Validated fix: also accept the defined condition:

if (g_strcmp0(code, "404") == 0 || xmpp_stanza_get_child_by_name(error, "item-not-found")) {

(omemo_set_device_list() is NULL-from-safe — it falls back to connection_get_fulljid() — so the reply arriving without a from attribute is fine here.)

Bug 2: PEP features are discovered only on the server domain, so the bundle is never published

omemo_bundle_publish() (src/xmpp/omemo.c, around line 91) silently returns when connection_supports(XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS) is false. connection_request_features() (src/xmpp/connection.c, around line 655) only ever sends disco#info to conn.domain, but per XEP-0163 §4 the PEP service — including http://jabber.org/protocol/pubsub#publish-options — is announced on the account's own bare JID. prosody announces it only there; ejabberd happens to also expose pubsub features on the domain. Result on prosody: the feature is never seen, the bundle is never published, and even after fixing Bug 1 the peer fails with item-not-found on the bundle node.

Validated fix: disco the own bare JID as well (mirroring how the domain entry is registered):

void
connection_request_features(void)
{
    iq_disco_info_request_onconnect(conn.domain);

    /* XEP-0163: PEP features (e.g. pubsub#publish-options, needed to publish
     * OMEMO material) are announced on the account's own bare JID */
    const char* barejid = connection_get_barejid();
    if (barejid && !g_hash_table_lookup(conn.features_by_jid, barejid)) {
        g_hash_table_insert(conn.features_by_jid, strdup(barejid),
                            g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL));
        iq_disco_info_request_onconnect(barejid);
    }
}

The bare JID is deliberately not added to requested_features, matching the existing comment about not firing sv_ev_connection_features_received early.

Reproduction

Two fresh accounts with a mutual presence subscription on any prosody (a stock prosody with mod_pep on localhost is enough), client started with -l DEBUG:

  1. /omemo gen on both accounts.
  2. /omemo start on either side → Can't find a OMEMO device id for <peer>, and after the fallback hint (/omemo end + /omemo start) → This message cannot be encrypted for any recipient.
  3. The debug log shows [OMEMO] can't get OMEMO device list, the device-list fetch answered with <item-not-found/> (no legacy code attribute), and no pubsub publish is ever sent (no SENT: <iq ...><pubsub ...><publish lines).
  4. With Bug 1 fixed, the device list is published but the peer's bundle fetch still fails (item-not-found on the bundle node) because the bundle publish is skipped — the sender's log shows no [OMEMO] publish own OMEMO bundle line (Bug 2).

With both fixes applied the full flow works: device lists and bundles are published, sessions build on both sides, and messages travel only inside <encrypted> payloads (verified against the raw SENT: stanzas in the debug log).

  • OMEMO device-list/bundle PEP nodes are presence-scoped by default, so a mutual roster subscription is a hard prerequisite for a first-time session — worth a line in the OMEMO guide (the encryption guide currently does not mention it).
  • OMEMO bootstrap logs several expected conditions at ERR level (can't get OMEMO device list for a not-yet-created node, [OMEMO][STORE] SG_ERR_INVALID_KEY_ID on the very first session build, item-not-found IQ errors). They are normal-flow noise and would be less alarming at INF/DBG.
  • OX (XEP-0373/0374), tested in the same two-client setup, works — but p_ox_gpg_signcrypt() encrypts with flags = 0 (no GPGME_ENCRYPT_ALWAYS_TRUST, unlike the XEP-0027 PGP path), so a key imported via /ox request is unusable until the user sets ownertrust manually with gpg. That matches man profanity-ox-setup, but the UX is rough: the client imports the key itself and then refuses to encrypt to it, with an error (Unable to send OX message...) that does not mention trust. Suggestion: mention ownertrust in the error text, or offer an /ox trust command. OTR and classic PGP were exercised the same way with no findings.
## Summary OMEMO does not work at all between two fresh cproof accounts on a prosody server: `/omemo gen` never publishes the device list or the key bundle, so `/omemo start` on the peer ends with `Can't find a OMEMO device id for <jid>` / `This message cannot be encrypted for any recipient`. Both root causes are ejabberd-isms in the client (the OMEMO guide examples use `ejabberd.local`, which is presumably why this was never hit). Found by live-testing two client instances against prosody 13 in Docker; both fixes below were validated in that setup (encrypted exchange works both ways with them). ## Bug 1: "node does not exist" is only recognized via the legacy `code="404"` error attribute `_omemo_receive_devicelist()` (`src/xmpp/omemo.c`, around line 449) handles an error reply to the device-list fetch like this: ```c const char* code = xmpp_stanza_get_attribute(error, "code"); if (g_strcmp0(code, "404") == 0) { omemo_set_device_list(from, NULL); return 1; } ``` ejabberd attaches the legacy numeric `code` attribute, prosody sends the plain RFC 6120 defined condition: ```xml <iq type="error" ...><error type="cancel"><item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error></iq> ``` so the empty-list bootstrap (`omemo_set_device_list(from, NULL)`) never runs, the own device id is never merged, and `/omemo gen` never publishes the device list. **Validated fix:** also accept the defined condition: ```c if (g_strcmp0(code, "404") == 0 || xmpp_stanza_get_child_by_name(error, "item-not-found")) { ``` (`omemo_set_device_list()` is NULL-`from`-safe — it falls back to `connection_get_fulljid()` — so the reply arriving without a `from` attribute is fine here.) ## Bug 2: PEP features are discovered only on the server domain, so the bundle is never published `omemo_bundle_publish()` (`src/xmpp/omemo.c`, around line 91) silently returns when `connection_supports(XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS)` is false. `connection_request_features()` (`src/xmpp/connection.c`, around line 655) only ever sends disco#info to `conn.domain`, but per XEP-0163 §4 the PEP service — including `http://jabber.org/protocol/pubsub#publish-options` — is announced on the **account's own bare JID**. prosody announces it only there; ejabberd happens to also expose pubsub features on the domain. Result on prosody: the feature is never seen, the bundle is never published, and even after fixing Bug 1 the peer fails with `item-not-found` on the bundle node. **Validated fix:** disco the own bare JID as well (mirroring how the domain entry is registered): ```c void connection_request_features(void) { iq_disco_info_request_onconnect(conn.domain); /* XEP-0163: PEP features (e.g. pubsub#publish-options, needed to publish * OMEMO material) are announced on the account's own bare JID */ const char* barejid = connection_get_barejid(); if (barejid && !g_hash_table_lookup(conn.features_by_jid, barejid)) { g_hash_table_insert(conn.features_by_jid, strdup(barejid), g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL)); iq_disco_info_request_onconnect(barejid); } } ``` The bare JID is deliberately not added to `requested_features`, matching the existing comment about not firing `sv_ev_connection_features_received` early. ## Reproduction Two fresh accounts with a mutual presence subscription on any prosody (a stock prosody with `mod_pep` on localhost is enough), client started with `-l DEBUG`: 1. `/omemo gen` on both accounts. 2. `/omemo start` on either side → `Can't find a OMEMO device id for <peer>`, and after the fallback hint (`/omemo end` + `/omemo start`) → `This message cannot be encrypted for any recipient`. 3. The debug log shows `[OMEMO] can't get OMEMO device list`, the device-list fetch answered with `<item-not-found/>` (no legacy `code` attribute), and **no** pubsub publish is ever sent (no `SENT: <iq ...><pubsub ...><publish` lines). 4. With Bug 1 fixed, the device list is published but the peer's bundle fetch still fails (`item-not-found` on the bundle node) because the bundle publish is skipped — the sender's log shows no `[OMEMO] publish own OMEMO bundle` line (Bug 2). With both fixes applied the full flow works: device lists and bundles are published, sessions build on both sides, and messages travel only inside `<encrypted>` payloads (verified against the raw `SENT:` stanzas in the debug log). ## Notes / related observations from the same test session - OMEMO device-list/bundle PEP nodes are presence-scoped by default, so a mutual roster subscription is a hard prerequisite for a first-time session — worth a line in the OMEMO guide (the encryption guide currently does not mention it). - OMEMO bootstrap logs several expected conditions at ERR level (`can't get OMEMO device list` for a not-yet-created node, `[OMEMO][STORE] SG_ERR_INVALID_KEY_ID` on the very first session build, item-not-found IQ errors). They are normal-flow noise and would be less alarming at INF/DBG. - OX (XEP-0373/0374), tested in the same two-client setup, works — but `p_ox_gpg_signcrypt()` encrypts with `flags = 0` (no `GPGME_ENCRYPT_ALWAYS_TRUST`, unlike the XEP-0027 PGP path), so a key imported via `/ox request` is unusable until the user sets ownertrust manually with gpg. That matches `man profanity-ox-setup`, but the UX is rough: the client imports the key itself and then refuses to encrypt to it, with an error (`Unable to send OX message...`) that does not mention trust. Suggestion: mention ownertrust in the error text, or offer an `/ox trust` command. OTR and classic PGP were exercised the same way with no findings.
jabber.developer2 added the
Kind/Bug
Priority
Low
4
labels 2026-07-16 14:32:18 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: devs/cproof#169
No description provided.