security: E2EE and transport correctness (issue #147) #173

Open
jabber.developer2 wants to merge 1 commits from fix/e2ee-transport-147 into master
Collaborator

Implements the four task blocks of #147; the two requirements already satisfied on master are verified and left unchanged.

T04 — security events visible (REQ-LOG-01, REQ-LOG-02, REQ-AUTH-03): SASL auth failure, TLS handshake failure and see-other-host redirects are logged as warnings; the certificate-failure prompt is preceded by a warning with subject and fingerprint; DISABLE_TLS, TRUST_TLS and LEGACY_AUTH produce a log warning plus a console notice, so a weakened transport is never silent.

T05 — transport policy (REQ-CRY-03, REQ-CFG-01): a session that ends up unencrypted without the user having asked for it triggers a loud warning (an explicit tls disable is reported at settings time and not repeated); in-band registration is refused on an unencrypted stream, since the account password would otherwise be sent in the clear; a shared http_warn_insecure_transfer() warns on each HTTP transfer performed with certificate verification disabled, covering upload, download and aesgcm transfers.

T06 — update check (REQ-VUL-02): the update check is pinned to https with peer/host verification and no redirects, failures are reported; the sscanf version parse is replaced with a strict N.N.N parser that rejects signs, trailing garbage and out-of-range components — the fetched version is untrusted network input.

T09 — crypto correctness (REQ-CRY-01, REQ-CRY-02, REQ-CRY-07, REQ-MEM-05): a failed MUC OMEMO send no longer writes plaintext to log/history — the message is only recorded when a stanza id was returned; the OTR opportunistic first message is plaintext on the wire and now passes allow_unencrypted_message() like any other cleartext send; get_random_string() draws from a CSPRNG (gcry_create_nonce, falling back to the OS random device) with rejection sampling to remove modulo bias, and libgcrypt is detected independently of OMEMO; the identity-key length decrement is guarded against unsigned underflow on an empty buffer.

Not changed, verified instead: REQ-VUL-03 is already satisfied on master (PREF_REVEAL_OS defaults off and the version string is empty since c5dd0cf9d); REQ-CRY-09 is already correct — trust mode defaults to manual, only blind/firstusage persist trust, both with a visible console notice.

Resolve #147

Implements the four task blocks of #147; the two requirements already satisfied on master are verified and left unchanged. **T04 — security events visible** (REQ-LOG-01, REQ-LOG-02, REQ-AUTH-03): SASL auth failure, TLS handshake failure and see-other-host redirects are logged as warnings; the certificate-failure prompt is preceded by a warning with subject and fingerprint; `DISABLE_TLS`, `TRUST_TLS` and `LEGACY_AUTH` produce a log warning plus a console notice, so a weakened transport is never silent. **T05 — transport policy** (REQ-CRY-03, REQ-CFG-01): a session that ends up unencrypted without the user having asked for it triggers a loud warning (an explicit `tls disable` is reported at settings time and not repeated); in-band registration is refused on an unencrypted stream, since the account password would otherwise be sent in the clear; a shared `http_warn_insecure_transfer()` warns on each HTTP transfer performed with certificate verification disabled, covering upload, download and aesgcm transfers. **T06 — update check** (REQ-VUL-02): the update check is pinned to https with peer/host verification and no redirects, failures are reported; the `sscanf` version parse is replaced with a strict N.N.N parser that rejects signs, trailing garbage and out-of-range components — the fetched version is untrusted network input. **T09 — crypto correctness** (REQ-CRY-01, REQ-CRY-02, REQ-CRY-07, REQ-MEM-05): a failed MUC OMEMO send no longer writes plaintext to log/history — the message is only recorded when a stanza id was returned; the OTR opportunistic first message is plaintext on the wire and now passes `allow_unencrypted_message()` like any other cleartext send; `get_random_string()` draws from a CSPRNG (`gcry_create_nonce`, falling back to the OS random device) with rejection sampling to remove modulo bias, and libgcrypt is detected independently of OMEMO; the identity-key length decrement is guarded against unsigned underflow on an empty buffer. **Not changed, verified instead:** REQ-VUL-03 is already satisfied on master (`PREF_REVEAL_OS` defaults off and the version string is empty since c5dd0cf9d); REQ-CRY-09 is already correct — trust mode defaults to manual, only blind/firstusage persist trust, both with a visible console notice. Resolve #147
jabber.developer2 added 1 commit 2026-07-30 06:26:39 +00:00
security: E2EE and transport correctness (issue #147)
Some checks failed
CI Code / Check spelling (pull_request) Successful in 13s
CI Code / Check coding style (pull_request) Successful in 23s
CI Code / Code Coverage (pull_request) Failing after 3m23s
CI Code / Linux (debian) (pull_request) Failing after 5m32s
CI Code / Linux (arch) (pull_request) Failing after 6m54s
CI Code / Linux (ubuntu) (pull_request) Failing after 8m20s
6dd1610e47
T04: promote security events to warnings — SASL auth failure, TLS
handshake failure, cert-failure details, see-other-host redirect;
DISABLE_TLS, TRUST_TLS and LEGACY_AUTH get a log warning plus a
console notice (REQ-LOG-01, REQ-LOG-02, REQ-AUTH-03)

T05: warn when a session ends up unencrypted without the user having
asked for it; refuse in-band registration on an unencrypted stream;
warn on each HTTP transfer with certificate verification disabled
(REQ-CRY-03, REQ-CFG-01)

T06: pin the update check to https with peer/host verification and no
redirects; strict N.N.N parser for the fetched version, which is
untrusted network input (REQ-VUL-02)

T09: no plaintext logging on failed MUC OMEMO sends; OTR opportunistic
first message passes allow_unencrypted_message(); get_random_string()
draws from a CSPRNG without modulo bias; guard the identity-key length
decrement against unsigned underflow (REQ-CRY-01, REQ-CRY-02,
REQ-CRY-07, REQ-MEM-05)

REQ-VUL-03 and REQ-CRY-09 are already satisfied on master and are
left unchanged.
jabber.developer2 force-pushed fix/e2ee-transport-147 from 6dd1610e47 to 214f860d2c 2026-07-30 07:44:44 +00:00 Compare
jabber.developer requested changes 2026-07-31 15:06:27 +00:00
jabber.developer left a comment
Owner

Generally a good update, which requires further polish

Generally a good update, which requires further polish
@@ -473,1 +476,3 @@
curl_easy_perform(handle);
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 0L);

We just overwrite defaults here.

We just overwrite defaults here.
@@ -474,0 +482,4 @@
#else
curl_easy_setopt(handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
curl_easy_setopt(handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
#endif

Can curl realistically fallback to HTTP? If not, then this code provides no benefit. The string is defined in const variable url which we do not touch later, so the protocol always remains the same.

Can curl realistically fallback to HTTP? If not, then this code provides no benefit. The string is defined in const variable `url` which we do not touch later, so the protocol always remains the same.
@@ -484,0 +527,4 @@
gint64 value = 0;
while (g_ascii_isdigit(*curr)) {
value = value * 10 + (*curr - '0');
if (value > G_MAXINT) {

Can we actually detect this overflow postfactum?

Can we actually detect this overflow postfactum?
@@ -702,0 +765,4 @@
}
#endif
FILE* urandom = fopen("/dev/urandom", "rb");

Why such complicated setup? Earlier setup with g_rand_new() and g_rand_int_range() utilized /dev/urandom as well. It even had a fallback mechanism.

https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/grand.c#L172

get_random_string() is used only to create stanza ids, download ids, and unique profanity identifier. There is no need for actual strong randomness. But I see the potential of strong random in future applications, such as password generation.

Why such complicated setup? Earlier setup with `g_rand_new()` and `g_rand_int_range()` utilized `/dev/urandom` as well. It even had a fallback mechanism. https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/grand.c#L172 `get_random_string()` is used only to create stanza ids, download ids, and unique profanity identifier. There is no need for actual strong randomness. But I see the potential of strong random in future applications, such as password generation.
@@ -201,3 +201,1 @@
groupchat_log_omemo_msg_out(mucwin->roomjid, message);
log_database_add_outgoing_muc(id, mucwin->roomjid, message, replace_id, PROF_MSG_ENC_OMEMO);
mucwin_outgoing_msg(mucwin, message, id, PROF_MSG_ENC_OMEMO, replace_id);
if (id != NULL) { // nothing was sent, so don't persist or echo the plaintext

TODO: probably it's a bug with diff, but the only change I see here is adding this condtion. However, for me, the lines below are highlighted as well.

TODO: probably it's a bug with diff, but the only change I see here is adding this condtion. However, for me, the lines below are highlighted as well.
@@ -206,3 +206,3 @@
{
cons_show_error("Login failed.");
log_info("Login failed");
log_warning("[SECURITY] Authentication failed for %s", STR_MAYBE_NULL(session_get_account_name()));

Do we even need to guard NULL-strings with STR_MAYBE_NULL? Typically all %s for us show (null) if they are null.

Do we even need to guard NULL-strings with `STR_MAYBE_NULL`? Typically all `%s` for us show `(null)` if they are null.
@@ -1434,2 +1434,4 @@
unsigned char* identity_public_key_data = signal_buffer_data(identity_public_key);
if (identity_public_key_len == 0) {
log_error("[OMEMO] cannot fingerprint an empty identity key"); // the decrement below would wrap

The comment is nice, but I feel that it can be placed slightly higher, since inline comment for log message looks less readable.

The comment is nice, but I feel that it can be placed slightly higher, since inline comment for log message looks less readable.
@@ -331,6 +332,10 @@ otr_on_message_send(ProfChatWin* chatwin, const char* const message, gboolean re
// tag and send for policy opportunistic
if (policy == PROF_OTRPOLICY_OPPORTUNISTIC) {
// the tagged first message is still plaintext on the wire

Nice catch! I really dislike message_send_chat_otr, as it's really misleading: unlike other message_send_chat_ ENCRYPTION_NAME methods, the method sends plaintext message and just marks the message as "otr".

Nice catch! I really dislike `message_send_chat_otr`, as it's really misleading: unlike other `message_send_chat_` `ENCRYPTION_NAME` methods, the method sends plaintext message and just marks the message as "otr".
@@ -54,0 +57,4 @@
http_warn_insecure_transfer(ProfWin* window, char* id, const char* const url)
{
log_warning("[SECURITY] TLS certificate verification disabled for transfer: %s", url);
http_print_transfer_update(window, id, THEME_ERROR, 0,

We need theme_warning

We need `theme_warning`
@@ -215,0 +217,4 @@
if (flags & XMPP_CONN_FLAG_DISABLE_TLS) {
log_warning("[SECURITY] TLS is disabled for this connection: traffic and credentials are sent in the clear");
cons_show_error("Security warning: TLS is disabled, this connection is unencrypted.");

We need cons_show_warning

We need `cons_show_warning`
@@ -960,0 +985,4 @@
// tls.policy=allow negotiates opportunistically, so an unrequested downgrade must not pass silently
if (!connection_is_secured() && !conn.tls_disabled_by_user) {
log_warning("[SECURITY] Logged in over an unencrypted connection to %s: the server offered no usable TLS", conn.domain);
cons_show_error("Security warning: this session is NOT encrypted, the server did not provide TLS. Use '/account set <account> tls force' to require it.");

Nice actionable warning.

Nice actionable warning.
@@ -1126,0 +1157,4 @@
g_key_file_set_string(keyfile.keyfile, "identifier", "random_bytes", profanity_instance_id);
save_keyfile(&keyfile);
} else {
log_error("Could not generate an instance identifier: no random source available");

Here and below shouldn't happen. We don't need true or strong randomness for simple things like that. Falling back on current time, as GLib does, is perfectly acceptable in these scenarios.

Here and below shouldn't happen. We don't need true or strong randomness for simple things like that. Falling back on current time, as GLib does, is perfectly acceptable in these scenarios.
@@ -1091,1 +1091,4 @@
assert_false(release_is_new("1.0.0", NULL));
// the found version is untrusted network input: nothing but strictly N.N.N may parse
assert_false(release_is_new("0.16.0", " 1.0.0")); // leading whitespace

What about ..., ., ..?

What about `...`, `.`, `..`?
jabber.developer added a new dependency 2026-07-31 15:08:16 +00:00
jabber.developer added the
Status
Blocked
1
label 2026-07-31 15:08:38 +00:00

Blocked by #87.

Blocked by #87.
All checks were successful
CI Code / Check spelling (pull_request) Successful in 14s
Required
Details
CI Code / Check coding style (pull_request) Successful in 25s
Required
Details
CI Code / Code Coverage (pull_request) Successful in 2m58s
Required
Details
CI Code / Linux (debian) (pull_request) Successful in 4m54s
Required
Details
CI Code / Linux (ubuntu) (pull_request) Successful in 5m1s
Required
Details
CI Code / Linux (arch) (pull_request) Successful in 13m5s
Required
Details
This pull request has changes requested by an official reviewer.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/e2ee-transport-147:fix/e2ee-transport-147
git checkout fix/e2ee-transport-147
Sign in to join this conversation.
No description provided.