Security compliance: assessment, planning #144

Open
opened 2026-06-29 17:11:39 +00:00 by jabber.developer · 3 comments

Security Compliance — NIST CSF 2.0 & ISO/IEC 27001:2022 (cproof)

This document combines two artifacts for the cproof project: a software security requirements catalog (stable REQ-* IDs traced to NIST CSF 2.0 and ISO/IEC 27001:2022 Annex A controls) and a static source-code compliance audit assessed against that catalog. The audit target is the master branch at HEAD 622054fc6, which includes the merged PGP plaintext-fallback fix. The method was static source analysis performed by 12 domain auditors, with every consequential finding adversarially re-verified against the working tree. Part 1 presents the audit; Part 2 presents the full catalog. Every REQ-XXX-NN cited in Part 1 links to its catalog entry in Part 2.

References

Table of contents

Part 1 — Compliance audit

Scope: the audit target is cproof master, HEAD 622054fc6 (includes the merged PGP plaintext-fallback fix), assessed against the catalog in Part 2. Method: static source analysis by 12 domain auditors, with every consequential finding adversarially re-verified against the working tree. The two critical-path findings were confirmed verbatim against the tree: the MUC OMEMO caller (client_events.c:200-203) unconditionally logs/echoes on a NULL id, and the aesgcm download path runs the external-command branch even after a failed decrypt with no removal of the plaintext output file.

Executive summary

cproof's core E2EE wire guarantees are sound: after commit 622054fc6 no cleartext <body> reaches the network on any PGP, OX, or OMEMO encryption failure (REQ-CRY-01), TLS is mandatory by default and certificate validation fails closed with explicit, persisted TOFU trust (REQ-CRY-04, REQ-CRY-10), and the auto-cleanup/secure-coding conventions are real and CI-checked (REQ-MEM-06). The audit nevertheless surfaced several serious gaps that an E2EE chat client cannot treat as cosmetic. The single most dangerous is REQ-EXT-01 (Gap, critical residual): the AI/LLM client never sets CURLOPT_SSL_VERIFYPEER/VERIFYHOST and accepts http:// provider URLs, so the API key and full conversation can be sent in cleartext or to an unverified peer. On the inbound side, REQ-INP-01 (Gap, high) is a remotely-triggerable NULL-deref DoS: four receive-path handlers dereference jid_create() results without a NULL guard on a malformed from (e.g. a@@b), and the exact fix the requirement names was never applied. REQ-DAR-03 / REQ-LOG-03 (Gap, high) leak decrypted plaintext bodies into profanity.log in the default dblog=on mode via four log_* sites in database_sqlite.c. REQ-AUTH-01 (Partial, high) leaves the OTR private key (keys.txt) and the plaintext history DB (chatlog.db) world/group-readable (no chmod after creation). REQ-CRY-06 (Partial) persists unauthenticated decrypted OMEMO media to the user's chosen path before the GCM tag is verified. Supply-chain posture is weak across the board: the libstrophe Meson wrap is pinned to revision = HEAD (REQ-SUP-01, Gap critical), CI base images and bootstrapped deps float (REQ-SUP-02), and there is no dependency-CVE scanning, no secret-scanning, no fuzzing, and no CODEOWNERS (REQ-SUP-04, REQ-SDLC-01/REQ-SDLC-03/REQ-SDLC-05). Build hardening is partial — autotools omits PIE/noexecstack and the Meson release path ships with zero hardening (REQ-MEM-01), and the ASan/UBSan suite is never run in CI (REQ-MEM-02). The recurring theme: the cryptographic primitives and wire paths are well-built, but the local data-at-rest, operational-logging, build-hardening, and supply-chain/process controls lag, and several requirement-named defects remain literally unfixed at HEAD.

Compliance scorecard

Domain Met Partial Gap N/A Unknown
CRY (crypto/TLS) 3 7 0 0 0
AUTH (auth/secrets) 2 4 0 0 0
INP (input validation) 4 4 2 0 0
MEM (memory safety) 1 5 3 0 0
DAR (data at rest) 6 0 1 0 0
LOG (logging) 3 2 1 0 0
CFG (config) 1 1 0 0 0
EXT (external/AI/plugins) 1 2 1 0 0
SUP (supply chain) 0 1 5 0 0
VUL (vuln mgmt) 0 3 1 0 0
RES (resilience) 3 1 0 0 0
SDLC 1 3 2 0 0
Overall 25 33 16 0 0

How to read this: Met = the requirement's normative SHALL clauses are satisfied in code on master (residual risk usually low). Partial = the core control exists but at least one mandated clause/sub-path/test is unmet. Gap = the control is materially absent or a requirement-named defect is unfixed. Residual risk is judged by exploitability and blast radius, not by status alone — e.g. several Partial items (REQ-AUTH-01, REQ-CRY-03) carry high residual risk, while some Gap items (REQ-SUP-05) are low.

Note: the overall tally counts 74 verified requirements; the findings the verifier left as null (REQ-AUTH-04, REQ-INP-10, REQ-MEM-06, REQ-DAR-04/REQ-DAR-05/REQ-DAR-06/REQ-DAR-07, REQ-CFG-02, REQ-LOG-04/REQ-LOG-05, REQ-RES-04, and remaining SDLC items) retain their auditor status, all Met.

Critical & high-priority gaps (ranked)

1 — REQ-EXT-01 — Enforce TLS verification & https on AI/LLM outbound — Gap (critical residual)

References: REQ-EXT-01.

  • Evidence: src/ai/ai_client.c:811-815 and :1596-1601 set only URL/HTTPHEADER/WRITEFUNCTION/WRITEDATA/POSTFIELDS/TIMEOUT. The provider scheme check (src/command/cmd_funcs.c:7019) explicitly accepts both http and https (g_strcmp0(scheme,"http")==0 || …"https"…), and ai_provider_new (ai_client.c:388) stores api_url verbatim. _build_curl_headers (ai_client.c:795) attaches Authorization: Bearer <key>; request_url is concatenated from provider->api_url (ai_client.c:1590). tests/functionaltests/test_ai.c:85,95 codifies acceptance of http://127.0.0.1:1/.
  • Correction: An earlier characterization ("no CURLOPT_SSL_VERIFYPEER/VERIFYHOST ⇒ unverified HTTPS") is overstated: libcurl defaults VERIFYPEER=1/VERIFYHOST=2, so HTTPS provider URLs are certificate-verified by default. A scheme check also exists. The real, confirmed defect is narrower but still serious: http:// provider URLs are accepted, so /ai set provider … http://… ships the API key and the entire conversation in cleartext over plain HTTP. Residual risk: high (revised down from "critical").

  • Why it matters: NIST PR.DS-02 / ISO A.8.24, A.5.14 (data-in-transit). Cleartext egress of an API key plus full message content to (or via) any on-path observer.
  • Fix: Reject non-https schemes in the provider scheme check (cmd_funcs.c:7019) and at request time before curl_easy_perform; flip test_ai.c to assert http:// is rejected. Additionally set CURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2L explicitly at both curl sites as defense-in-depth (guards against a non-default global curl config or a libcurl built with relaxed defaults).

2 — REQ-SUP-01 — Pin libstrophe Meson wrap to an immutable revision — Gap (high residual, critical priority)

References: REQ-SUP-01.

  • Evidence: subprojects/libstrophe.wrap line 3 revision = HEAD; no 40-hex SHA, tag, or source_hash. No CI lint catches it.
  • Why it matters: NIST ID.RA / GV.SC, ISO A.5.23/A.8.30. libstrophe is the XMPP parser on the untrusted-network path; any clone can silently incorporate an attacker-modified/regressed parser, and builds are non-reproducible.
  • Fix: Replace revision = HEAD with the vetted 40-hex commit SHA (or tag + source_hash); add a CI lint failing on revision = HEAD/bare-branch wraps.

3 — REQ-INP-01 / REQ-INP-09 — NULL-guard peer JIDs on receive — Gap (high)

References: REQ-INP-01, REQ-INP-09.

  • Evidence: jid_create() returns NULL for NULL/empty/over-3071-char/invalid-UTF-8/double-@ (src/xmpp/jid.c:35-38,82-153). Four unguarded derefs: src/xmpp/message.c:1749-1750 (_should_ignore_based_on_silence, the exact function the requirement names), src/xmpp/presence.c:397-402 (_presence_error_handler, from not even NULL-checked), presence.c:453-454 (_unsubscribed_handler), presence.c:468-470 (_subscribed_handler). The sibling _subscribe_handler (presence.c:482-484) does guard, proving these are bugs.
  • Why it matters: NIST PR.DS / ISO A.8.28. A hostile peer sending a stanza with missing/malformed from (e.g. a@@b) causes a remote NULL-deref DoS — a crash on every connection attempt from that peer.
  • Fix: After every receive-path jid_create(from) add if (!from_jid) { return; } before field access. Add a regression test driving these handlers with absent/a@@b from under ASan.

4 — REQ-DAR-03 / REQ-LOG-03 — Plaintext bodies in operational logs — Gap (high)

References: REQ-DAR-03, REQ-LOG-03.

  • Evidence: Default PREF_DBLOG="on" (src/config/preferences.c:2815) does not redact. src/database_sqlite.c:694 (LMC mismatch, message->plain), :717 (duplicate stanza-id, content: %s), :748 (log_debug("Writing to DB. Query: %s") — the INSERT embeds message->plain at :735), :760 (insert-failure, the requirement-named defect) all write decrypted bodies to profanity.log. Triggers are remotely influenceable (XEP-0308 LMC, server-assigned stanza-id). The libstrophe debug bridge _xmpp_file_logger (src/xmpp/connection.c:1088-1107) forwards raw SASL <auth>/<challenge>/<response> and XEP-0077 registration credentials with no redaction (gated by PREF_STROPHE_VERBOSITY, default off).
  • Why it matters: NIST PR.DS-01 / ISO A.8.10, A.8.12. Decrypted E2EE content lands in a plaintext log in the shipped default config, surviving all log-level filters — defeating the point of E2EE for anyone who shares logs for debugging.
  • Fix: Drop message->plain/content:/Message: from the format args at database_sqlite.c:694,717,760; do not log the full INSERT at :748 (log row id only). Redact <auth>/<challenge>/<response>/jabber:iq:register payloads in _xmpp_file_logger.

5 — REQ-AUTH-01 — Owner-only (0600) on every secret/history file — Partial (high)

References: REQ-AUTH-01.

  • Evidence: Met for keyfiles/accounts/OMEMO/tlscerts (prof_keyfile g_chmod 0600, src/common.c:186/202), main log (src/log.c:177), export (src/database_export.c:329). Unprotected: src/database_sqlite.c:144 sqlite3_open(filename,…) has no following chmod (plaintext history DB inherits umask, typically 0644); OTR keys.txt (src/otr/otr.c:372) and fingerprints.txt (otr.c:113,386) written by libotr with no chmod. No process-wide umask anywhere in the tree.
  • Why it matters: NIST PR.AC / ISO A.8.3, A.5.15. The OTR private signing key and the decrypted-history DB are readable by other local users on a default umask.
  • Fix: g_chmod(filename, S_IRUSR|S_IWUSR) after sqlite3_open; g_chmod after each otrl_privkey_generate/otrl_privkey_write_fingerprints.

6 — REQ-CRY-06 — Persisted unauthenticated OMEMO media plaintext — Partial (medium)

References: REQ-CRY-06.

  • Evidence: aes256gcm_crypt_file writes all decrypted plaintext to the output FILE* (src/omemo/crypto.c:445) before gcry_cipher_checktag (:462). The caller opens outfh directly on the user's final destination (src/tools/aesgcm_download.c:66) and on tag failure only prints an error (:114-118) — it never removes the plaintext output (remove(tmpname) at :112 deletes only the ciphertext temp), and the external-command branch (:126-140) still runs on a failed decrypt. (Confirmed verbatim against the tree.)
  • Why it matters: NIST PR.DS-08 / ISO A.8.24. Violates the AEAD "no plaintext output on tag mismatch" mandate — a tampered/truncated media file yields a fully-persisted, attacker-controlled file at the user's path, possibly fed to an external opener.
  • Fix: Decrypt to a temp file, verify the tag, then atomically rename on success; on failure remove() the output and skip the cmd_template branch.

7 — REQ-CRY-03 — No silent STARTTLS downgrade — Partial (high)

References: REQ-CRY-03.

  • Evidence: Default (NULL/force) correctly sets MANDATORY_TLS (src/xmpp/connection.c:185-186). But the allowlisted allow policy (src/common.c:406) sets neither MANDATORY_TLS nor DISABLE_TLS (connection.c:185-194) → opportunistic STARTTLS, plaintext if the server omits it, with no log (flags==0 so the :201 diagnostic never fires). In-band registration uses xmpp_connect_raw (connection.c:496-515); _register_handle_features secures only "if possible" (:397-431), so even tls.policy=force sends new-account credentials in cleartext when STARTTLS is absent.
  • Why it matters: NIST PR.DS-02 / ISO A.8.24. SASL/registration credentials can traverse plaintext under a documented, autocompleted policy.
  • Fix: Map allow to a hard "TLS-required-or-fail" floor or warn loudly; enforce TLS before in-band registration sends credentials.

8 — REQ-MEM-01 — Full hardening baseline, build-system parity — Partial (high)

References: REQ-MEM-01.

  • Evidence: autotools has stack-protector/FORTIFY/RELRO/-Wformat=2 but no -fPIE/-pie and no -Wl,-z,noexecstack (configure.ac). meson.build:64-71 gates stack-protector + FORTIFY behind if is_debug; the release path (:33, executable() at :536) gets none, and Meson has zero RELRO/now/noexecstack/PIE link args. No checksec/readelf CI gate.
  • Why it matters: NIST PR.PS / ISO A.8.31. Meson release binaries ship with no exploit mitigations; the two build systems are not at parity.
  • Fix: Add -fPIE -pie -Wl,-z,noexecstack (probed) to autotools; move Meson hardening out of if is_debug and add b_pie=true + relro/now/noexecstack link args; add a checksec CI assertion.

9 — REQ-MEM-02 — ASan/UBSan gate the test suite in CI — Partial (high)

References: REQ-MEM-02.

  • Evidence: --enable-sanitizers exists (configure.ac:74-75,441-452) but no CI job passes it (ci-build.sh:193-205 defines 4 Linux configs, none with sanitizers; tree-wide grep finds no use). Valgrind is wired and gating (ci-build.sh:295).
  • Why it matters: NIST PR.PS / ISO A.8.28. The deliberate-heap-overflow / unsigned-overflow verification would not fail CI; memory-safety regressions can land.
  • Fix: Add a non-continue-on-error job running ./configure --enable-sanitizers && make check.

10 — REQ-EXT-04 — Plugin install: https + integrity verification — Partial (high)

References: REQ-EXT-04.

  • Evidence: cmd_plugins_install's _http_based_uri_scheme accepts both http and https (src/command/cmd_funcs.c:7019); plugin download reuses http_file_get, which sets CURLOPT_SSL_VERIFY*=0L when account tls_policy=="trust" (src/tools/http_download.c:120-156); the downloaded .so/.py is plugins_install'd then dlopen'd at full privilege with no signature/checksum check (src/tools/plugin_download.c, src/plugins/plugins.c:198-218). copy_file (src/common.c:234-243) doesn't strip group/world-write. Trust model undocumented.
  • Why it matters: NIST GV.SC / ISO A.5.23, A.8.25. A network MitM can substitute a malicious plugin that runs with full process privilege and access to all secret stores — RCE via plugin update.
  • Fix: Require https for plugin install; force VERIFY*=1 regardless of XMPP tls_policy; verify a signature/checksum before install; chmod 0600 installed files; document the full-privilege trust model.

11 — Other high-priority Gaps (concise)

  • REQ-SUP-02 (Gap): CI base images use mutable tags (ci/Dockerfile.*), deps cloned at HEAD with no git checkout <sha>, Arch AUR snapshot fetched + makepkg'd unverified (Dockerfile.arch:61-65). → Digest-pin FROMs, commit-pin checkouts, sha256sum -c downloads.
  • REQ-SUP-03 (Partial, high): gpgme uses unversioned AC_CHECK_LIB([gpgme],[main]) (configure.ac:309); libgcrypt has no floor (:351); sqlite3/gcrypt/gpgme floors diverge from meson.build. → Versioned PKG_CHECK_MODULES, align floors, add a parity lint.
  • REQ-MEM-03 (Gap): strcpy/strcat on peer barejids (src/pgp/ox.c:153-156), sprintf on key/nonce (src/omemo/crypto.c:479,483) — forbidden primitives, no CI grep. (Buffers are correctly sized, so residual is medium.) → Replace with g_strdup_printf/snprintf; add a CI grep.
  • REQ-MEM-04 (Gap): No overflow-checked allocation anywhere; omemo.c:1440 malloc(len*2+1), http_upload.c:101-103 unchecked size*nmemb/realloc-overwrite, three omemo.c:455/475/492 malloc(*length) deref'd without NULL check. → __builtin_mul_overflow/g_malloc_n, NULL-check, save realloc to temp.
  • REQ-MEM-09 (Gap): alloca sized from peer barejid length (src/pgp/ox.c:150-151), no -Wvla. → Replace with g_strdup_printf; add -Werror=vla.
  • REQ-INP-08 (Gap): No control-char/bidi neutralization or length cap on display/log/DB sinks (src/ui/window.c:2024,2120); str_xml_sanitize is outgoing-only and skips U+202E. → Single sanitizer at display + log/DB write boundary.

Partial / medium findings (grouped)

CRY

  • REQ-CRY-01 — MUC OMEMO caller logs/echoes plaintext on NULL id (client_events.c:200-203, confirmed). → Guard if (id != NULL) like the 1:1 path.
  • REQ-CRY-02 — OTR-opportunistic first message bypasses allow_unencrypted_message (src/otr/otr.c:332-339,425-428). → Consult the gate before the tagged cleartext send.
  • REQ-CRY-07get_random_string uses g_rand_* (Mersenne-Twister) for stanza IDs and the profanity_instance_id HMAC key (src/common.c:712-717, connection.c:808,1123). → Switch identifiers/HMAC seed to gcry_create_nonce.
  • REQ-CRY-09blind/firstusage modes persist trust from inbound handlers (src/omemo/omemo.c:608,698). → Confine trust transitions to user commands.

AUTH

  • REQ-AUTH-03 — Insecure flags logged at log_debug, not WARN (connection.c:201-213). → WARN + cons_show on DISABLE_TLS|LEGACY_*|TRUST_TLS.
  • REQ-AUTH-05eval_password spawned with G_SPAWN_SEARCH_PATH (src/config/account.c:149). → Require absolute path / drop the flag.
  • REQ-AUTH-06 — Account/eval/PGP passphrases freed without wipe (account.c:184-185, session.c:581/591, gpg.c:79/104/108). → explicit_bzero before free.

INP

  • REQ-INP-02 — Validation correct; the prescribed boundary/forbidden/@@/UTF-8 test table is absent (tests/.../test_jid.c). → Add the table-driven suite.
  • REQ-INP-05 — MAM <result> id not disco-gated (message.c:1536-1571 → stored at :1409-1410). → Apply _stanza_id_by_trusted on the MAM path.
  • REQ-INP-06/url open|save accept any scheme (cmd_funcs.c:9633-9655,9678-9698). → Whitelist http/https/aesgcm.
  • REQ-INP-07 — OSX notifier uses system() with hand-rolled escaping (src/ui/notifier.c:160). → g_spawn argv / g_shell_quote.

MEM

  • REQ-MEM-05omemo.c:1437 identity_public_key_len-- lacks a >0 guard. → Reject zero-length before decrement.
  • REQ-MEM-07identity_key_store_destroy doesn't NULL freed fields (store.c:51-53); undocumented out-param ownership. (Double-free unreachable — memset at omemo.c:307.) → Document ownership + add =NULL.
  • REQ-MEM-08 — Good NULL-guarding by convention but -Wnull-dereference only under non-CI --enable-hardening. → Enable in one CI config.

LOG / DAR

  • REQ-LOG-01 — SASL auth failure at INFO (server_events.c:207); TLS cert-fail logs nothing (server_events.c:1135-1200). → Promote both to WARN.
  • REQ-LOG-02 — TLS-disabled / handshake-fail / see-other-host / insecure-login all DEBUG-only. → WARN on all four.
  • REQ-LOG-06 — stderr→log bridge applies no redaction (src/log.c:289,297). → Redact secret markers before log_msg.

CFG / VUL / SDLC

  • REQ-CFG-01tls.policy=trust disables curl verify silently on transfers, no user warning (http_upload.c:242-244, http_download.c:153-155). → Surface the insecure state per transfer.
  • REQ-VUL-02release_get_latest never sets VERIFY*/https-only (src/common.c:459-482). → Set them explicitly.
  • REQ-VUL-03PREF_REVEAL_OS defaults TRUE (preferences.c:2692,2706); no <version>-omit pref. → Default OFF; add version toggle.
  • REQ-VUL-04 — No per-release Security/CVE changelog convention; RELEASE_GUIDE.md has no security step. → Add convention + review step.
  • REQ-EXT-03 — No CURLOPT_CONNECTTIMEOUT; no cap/stall test (ai_client.c). → Add connect-timeout + tests.
  • REQ-RES-02PRAGMA integrity_check only on /history verify, never at DB open (database_sqlite.c). → Run quick_check in _sqlite_init.
  • REQ-SDLC-03 — No CODEOWNERS; commitlint config dormant (.commitlintrc.json unreferenced). → Add CODEOWNERS + commitlint CI job.
  • REQ-SDLC-04 — Style/auto-type/CWE-134/spell checks marked continue-on-error (ci-code.yml:40,94). → Remove continue-on-error.
  • REQ-SDLC-06 — No invariant→test REQ-ID traceability matrix. → Add compliance/REQ-TO-TEST.md.

What cproof does well (notable Met items)

  • REQ-CRY-01 — Fail-closed E2EE on the wire. Every PGP failure branch returns NULL before _send_message_stanza (message.c:491-517); OX returns NULL before send (:564-571); OMEMO keys==NULL does goto out without reaching message_send_chat_omemo (omemo.c:928-966). The 622054fc6 fix is real and the 1:1 caller guards if (id != NULL) (client_events.c:138/148/157).
  • REQ-CRY-04 — TLS cert validation fails closed. Certfail handler always registered (connection.c:226); blind trust only on explicit tls.policy=trust; sv_ev_certfail accepts only via prior-trust or explicit /tls allow|always, else return 0 (server_events.c:1134-1191).
  • REQ-CRY-10 — Fail-safe OMEMO trust. Default manual mode; untrusted devices get no session (omemo.c:702-705) and are skipped; keys==NULL aborts with a remediation listing and no stanza.
  • REQ-AUTH-02 — 0700 secret dirs. All dir creation routes through create_dirg_mkdir_with_parents(name, S_IRWXU) (common.c:224); no 0755 bypass anywhere.
  • REQ-INP-03 — No CWE-134 format-string sinks. Every variadic wrapper carries G_GNUC_PRINTF; all untrusted classes (URLs, plugin, AI response) are %s-wrapped; check-cwe134.sh exits 0.
  • REQ-MEM-06 — Auto-cleanup macros enforced. Nine auto_* macros, 1105 uses, CI asserts the set and fails on uninitialized auto_* locals (ci-code.yml:48,51).
  • REQ-DAR-01/REQ-DAR-02off/redact modes are airtight. Both backends early-return / placeholder-substitute before any persistence (database_sqlite.c:640-647, database_flatfile.c:363-373).
  • REQ-EXT-02 — AI consent is opt-in. Content reaches a provider only from a user-opened AI window; no inbound hook auto-forwards; default providers carry no key so no network call occurs; history is volatile-only.
  • REQ-RES-01/REQ-RES-03 — Migration preflight + state-preserving reconnect. Disk-space gate + single-transaction migrations with ROLLBACK (database_sqlite.c:252-258,783-918); XEP-0198 SM state captured/re-applied with no E2EE-flag downgrade on reconnect; OTR torn down fail-safe.

Verifier disagreements

Several auditor "Met" verdicts were overturned to Partial by the adversarial verifier. These corrections are material — they prevent over-stating compliance:

  • REQ-CRY-01 — The auditor verdict held the caller logs/echoes only when id != NULL. The verifier found the MUC OMEMO caller (client_events.c:200-203) unconditionally logs the plaintext to the SQLite history DB and echoes it on screen even when omemo_on_message_send returns NULL. This was confirmed verbatim against the tree. Net: no network plaintext, but a local DB + UX leak on the MUC failure path.
  • REQ-CRY-02 — Auditor: the gate covers all cleartext sends. Verifier: the OTR-opportunistic first message returns TRUE from otr_on_message_send, short-circuiting && allow_unencrypted_message(), so the force-encryption gate is never consulted for that cleartext send.
  • REQ-CRY-06 — Auditor flagged only a benign short-tag nuance. Verifier found the decisive defect: file plaintext is persisted to the user's destination before tag verification, and the post-download external command still runs on decrypt failure.
  • REQ-CRY-07 — Auditor (grep scoped to omemo/otr/pgp) declared CSPRNG "Met". Verifier found g_rand_* (named as forbidden in the requirement) used for stanza IDs and an HMAC key seed in src/common.c/src/xmpp.
  • REQ-CRY-09 / REQ-INP-05 — Auditor "Met"; verifier found inbound-handler trust transitions (blind/firstusage) and an un-disco-gated MAM <result> id, respectively.
  • REQ-MEM-07 — Verifier lowered residual risk (medium→low): the "latent double-free" is unreachable because signal_protocol_store_context_destroy is guarded and immediately followed by memset(&omemo_ctx, 0, …) (omemo.c:302-307).

In every overturned case the verifier's corrected status is authoritative; the disagreements arose because the original auditors' greps were scoped too narrowly (missing src/common.c, src/xmpp, the MUC caller, the MAM path) rather than from misreading the cited code.

Methodology & limitations

  • Scope: Static source analysis of master at HEAD 622054fc6, read directly from the working tree, requirement-driven against the catalog in Part 2. Each consequential finding was produced by a domain auditor and independently re-verified by an adversarial verifier; where they disagreed, the corrected status is reported.
  • Not a pentest: No dynamic exploitation, fuzzing, or runtime instrumentation was performed. DoS/overflow reachability is argued from code paths, not demonstrated with live inputs; exploitability assessments (e.g. the correctly-sized alloca/sprintf buffers in REQ-MEM-03/REQ-MEM-09) reflect code review, not runtime proof.
  • Server-side items are Unknown by nature: GitHub branch-protection / required-status-check enforcement (REQ-SDLC-03) cannot be verified from the tree; only the in-repo prerequisites (absent CODEOWNERS, dormant commitlint) are confirmed. The PGP fingerprint currency in SECURITY.md (REQ-VUL-01) is likewise unverifiable here.
  • Build-config caveats: Findings about Meson vs autotools parity (REQ-MEM-01, REQ-SUP-03) are read from build files, not from inspecting produced binaries; a checksec/readelf pass on actual release artifacts would confirm the shipped mitigation set.
  • No status was inflated: Several requirement-named defects (REQ-INP-01, REQ-DAR-03, REQ-MEM-03/REQ-MEM-09) were confirmed still present and unfixed at HEAD; conversely, null-verdict items retain their auditor "Met" status and were not independently re-derived here.

Part 2 — Requirements catalog

Scope and introduction

This catalog defines the software security requirements for cproof, a fork of the Profanity console XMPP/Jabber chat client (~90k LOC, C, autotools build). Requirements are derived from and traced to NIST CSF 2.0 (Govern/Identify/Protect/Detect/Respond/Recover Categories and Subcategories) and ISO/IEC 27001:2022 Annex A controls. The target system is a desktop, single-user end-to-end-encrypted (E2EE) chat client offering OMEMO, OTR, OpenPGP (XEP-0027) and OX (XEP-0373), transported over XMPP/TLS via libstrophe, with a local SQLite/flat-file history store, plaintext GKeyFile account config, C/Python plugin extensibility, and an outbound AI/LLM client module (src/ai). The assumed threat model is: an active or passive network/server adversary (MitM, downgrade, key injection, stanza spoofing, server-side archive); a hostile peer sending attacker-controlled XML/stanzas, message bodies, MUC fields, URLs and file transfers; a malicious or compromised LLM provider or update endpoint; other local users on a shared host reading at-rest secrets; and a compromised supply chain (dependencies, CI, wrapped subprojects, plugins). In scope are the client's crypto correctness/enforcement, transport security, input validation, memory safety, data-at-rest protection, logging/detection, secure defaults, supply-chain and vulnerability management, extension/external-interface security, resilience, and the secure-development lifecycle of the repository itself. Explicitly out of scope are purely physical, HR, and datacenter/organizational-estate controls (there is no central server or IT estate); these are enumerated at the end.

How to read this

  • IDs are stable and domain-prefixed: REQ-<DOMAIN>-NN (CRY cryptography, AUTH authentication/access/secrets, INP input validation, MEM memory safety, DAR data-at-rest/privacy, LOG logging/detection, CFG secure config/defaults, SUP supply chain, VUL vulnerability/patch/change-control, EXT extension/external interfaces, RES resilience/fail-safe, SDLC secure-development lifecycle/governance). IDs are intended to be cited from source-code audit findings and never renumbered.
  • Priority is one of critical / high / medium / low, reflecting blast radius for an E2EE client (a plaintext leak or MitM bypass is critical; a hardening or documentation gap is medium/low).
  • Each requirement carries an italic traceability line: NIST | ISO | Priority | Verify. The verification text is written to be concretely checkable against the cproof source tree.
  • Where one requirement satisfies a control raised by several domains, the duplicates are merged and the merge noted in the statement or applicability. The first appearance of a hardening/permissions/no-plaintext-log control is canonical; later domains reference it.
  • "SHALL" denotes a mandatory requirement; "SHOULD" a strong recommendation that may be risk-accepted with justification.

REQ-CRY — Cryptography & Confidentiality

REQ-CRY-01

Fail-closed E2EE: no plaintext fallback on encryption failure

When a chat or MUC window has an active encryption mode (OMEMO, OTR, PGP XEP-0027, OX XEP-0373), cproof SHALL NOT transmit the message body in cleartext if encryption fails, the recipient key/session is missing, the sender key is missing, the crypto context cannot be created, or the crypto backend is unavailable (HAVE_LIBGPGME/HAVE_OMEMO undefined); every such path SHALL abort the send, surface a specific visible error in the relevant window, and return NULL/failure so the caller skips network send, history logging and on-screen echo. (Merges REQ-RES-01; satisfies the same E2EE-integrity control from the Cryptography and Resilience domains.)

NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.8.26 | Priority: critical | Verify: regression/integration tests forcing each failure path (deleted recipient key, unref'd sender key, induced gpgme error, empty OMEMO device_list, backend compiled out) assert no <body> cleartext stanza is emitted and the send returns NULL; grep that p_gpg_encrypt/OMEMO/OX encrypt failure branches never reach message_send_chat/_send_message_stanza; confirm cl_ev_send_msg_correct echoes/logs only when the returned id is non-NULL (guards the 622054fc6 fix).

REQ-CRY-02

Mandatory force-encryption policy as last-line plaintext gate

For conversations without an active E2EE session, cproof SHALL consult allow_unencrypted_message() before any cleartext send; under PREF_FORCE_ENCRYPTION mode block it SHALL refuse with no per-message opt-out, under resend-to-confirm it SHALL require an explicit second user action, and any unrecognized/empty mode SHALL fail closed (block) rather than fail open. (Merges REQ-RES-02.)

NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.5.10 | Priority: critical | Verify: unit-test allow_unencrypted_message() returns FALSE for block; for resend-to-confirm first Enter blocks and an identical re-send proceeds; an invalid/empty mode returns FALSE; confirm the final fallthrough returns FALSE.

REQ-CRY-03

TLS mandatory by default; no silent STARTTLS downgrade

For every account, cproof SHALL map an absent/empty/force tls.policy to XMPP_CONN_FLAG_MANDATORY_TLS, SHALL set XMPP_CONN_FLAG_DISABLE_TLS only when the user explicitly sets tls.policy=disable, and SHALL NOT proceed with SASL authentication or message traffic over a connection that failed to upgrade to TLS under any non-disabled policy. (Merges REQ-AUTH-03 transport half, REQ-CFG-01.)

NIST: PR.DS-02, PR.PS-01 | ISO: A.8.20, A.8.9, A.8.24 | Priority: critical | Verify: unit-test _conn_apply_settings flag computation in src/xmpp/connection.c: NULL/force/trust set MANDATORY_TLS and exclude DISABLE_TLS/LEGACY_SSL; disable sets DISABLE_TLS only on explicit config; integration test against a server advertising no STARTTLS aborts under default policy; confirm no auth/send occurs before TLS established.

REQ-CRY-04

TLS certificate validation enforced; trust-bypass is explicit and fails closed

cproof SHALL perform full X.509 chain and hostname validation for XMPP TLS by default, SHALL set XMPP_CONN_FLAG_TRUST_TLS (blind trust) only when tls.policy=trust is explicitly chosen, SHALL always register the certfail handler, and on validation failure SHALL default to abort — accepting a cert only via explicit interactive /tls allow (session) or /tls always (persist by SHA-256 fingerprint via tlscerts/cafile), and rejecting on /tls deny or EOF. (Merges REQ-CFG-02, REQ-CFG-03.)

NIST: PR.AA-03, DE.CM-09 | ISO: A.8.21, A.8.9, A.8.24 | Priority: critical | Verify: test only literal policy trust yields TRUST_TLS and no default/auto-fallback does; test _connection_certfail_cb/sv_ev_certfail returns accept only for explicit allow/always and abort (0) for deny/EOF; assert tlscerts_exists/fingerprint-equality gate silent re-acceptance; negative test: untrusted self-signed cert under default policy must not connect.

REQ-CRY-05

Per-account tls.policy validated against an allowlist on load

cproof SHALL validate each stored tls.policy against the known allowlist (force, trust, disable, direct/legacy) via valid_tls_policy_option on load, and SHALL discard an unrecognized value by falling back to the secure default (force/MANDATORY_TLS) rather than passing an unknown string to the connection layer.

NIST: PR.PS-01, PR.DS-02 | ISO: A.8.9 | Priority: high | Verify: test that loading an account with tls.policy="bogus" yields tls_policy=NULL (interpreted as force) and that valid_tls_policy_option rejects out-of-allowlist values.

REQ-CRY-06

Approved cipher/key-length and mandatory AEAD tag verification for OMEMO

OMEMO message and file encryption SHALL use only AES in an authenticated/standardized mode at ≥128-bit strength (AES-256-GCM for payload/file; AES-128-GCM and AES-CBC-PKCS5 only where the OMEMO/Signal spec requires), SHALL reject unknown cipher/key-length selectors (switch default returns error, no silent weaker substitution), and SHALL call gcry_cipher_checktag on every decrypt and abort with no plaintext output on tag mismatch.

NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: review the key_len switch in src/omemo/crypto.c handles only sanctioned lengths with an erroring default (no fall-through); confirm gcry_cipher_checktag failure aborts decrypt; add a tampered-tag test asserting decrypt fails and emits no plaintext.

REQ-CRY-07

CSPRNG for all key/nonce/IV/identifier generation

All key material, IVs/nonces, and identifiers SHALL be drawn from libgcrypt at GCRY_VERY_STRONG_RANDOM (gcry_randomize/gcry_random_bytes_secure); libgcrypt secure-memory init and GCRYCTL_INITIALIZATION_FINISHED SHALL complete (and be checked via GCRYCTL_INITIALIZATION_FINISHED_P) before any keygen; no predictable source (rand()/random()/g_random_*) SHALL be used for crypto material.

NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: confirm omemo_random_func uses VERY_STRONG_RANDOM; verify init-finished is set before keygen and checked; grep crypto/omemo/otr/pgp sources for rand()/random()/g_random_ used as key/IV/nonce and assert none.

REQ-CRY-08

Forward-secret ratcheting enforced; one-time prekeys consumed

OMEMO sessions SHALL use the libsignal/libomemo-c double ratchet and OTR the libotr AKE/ratchet; cproof SHALL NOT implement its own session KDF/ratchet, and consumed one-time prekeys SHALL be removed from the prekey store (remove_pre_key) after use so that long-term identity-key compromise does not expose past messages.

NIST: PR.DS-02 | ISO: A.8.24 | Priority: high | Verify: confirm session crypto is delegated to the libraries (no custom KDF/ratchet); verify remove_pre_key is wired and invoked on session establishment; review signed-prekey rotation; integration test that a replayed prekey bundle does not reproduce a usable session key.

REQ-CRY-09

TOFU trust model: explicit, persisted fingerprint verification before trusted send

cproof SHALL bind peer identity to cryptographic fingerprints (OMEMO identity-key fingerprints, OTR fingerprints, PGP key IDs), distinguish untrusted/blind/verified states, require an explicit user action (/omemo trust, /otr fingerprint/SMP, /pgp trust) to mark a fingerprint verified, persist trust decisions, and treat a changed peer identity key as untrusted rather than auto-accepting it. Trust transitions SHALL be driven only by user commands, never by inbound-stanza handlers. (Merges REQ-AUTH-07.)

NIST: PR.AA-03, PR.AA-04 | ISO: A.5.31, A.5.17 | Priority: high | Verify: unit-test is_trusted_identity returns untrusted for unknown/changed keys; verify trust-store round-trips and a new identity key for a known JID does not auto-trust; simulate an inbound message from an unverified peer and assert identity stays untrusted until a verify command runs.

REQ-CRY-10

Fail-safe OMEMO trust on send and receive

On send, cproof SHALL omit untrusted OMEMO recipient devices and SHALL abort the send (with an explanatory untrusted-fingerprint listing) when no trusted device remains, never silently encrypting to an unverified device; on receive, a message from an untrusted identity SHALL be flagged untrusted to the user rather than displayed as trusted. The default PREF_OMEMO_TRUST_MODE SHALL be manual (not blind); blind/firstusage SHALL surface a visible auto-trust notice. (Merges REQ-CFG-07, REQ-RES-06.)

NIST: PR.AA-03, PR.DS-02 | ISO: A.8.24, A.8.9, A.5.17 | Priority: critical | Verify: test prefs_get_string(PREF_OMEMO_TRUST_MODE) default == manual; test that with all recipient devices untrusted no stanza is sent and the untrusted-fingerprint guidance shows (omemo_on_message_send keys==NULL branch); test receiving from an untrusted identity sets the trusted flag FALSE and marks the UI; confirm blind/firstusage emit a cons_show notice.

REQ-AUTH — Authentication, Access Control & Secret Management

REQ-AUTH-01

Owner-only (0600) permissions for every secret/credential/trust/history file

cproof SHALL create and re-assert mode 0600 (S_IRUSR|S_IWUSR) on every file holding secrets or security-critical state — accounts keyfile (passwords/eval_password), OMEMO identity.txt/trust.txt/sessions.txt/known_devices.txt, OTR keys.txt/fingerprints.txt, PGP material, TLS pinned certs (tlscerts), the SQLite history DB and flat-file backend, history export files, and the main log — both on creation and on every save, regardless of pre-existing mode. Files written by third-party libraries (e.g. libotr otrl_privkey_write_fingerprints) SHALL get an explicit chmod afterward. (Canonical permissions requirement; merges REQ-CRY perms, REQ-DAR-01, REQ-DAR-02, REQ-CFG-09, REQ-LOG-03.)

NIST: PR.DS-01, PR.AA-01, PR.AA-05 | ISO: A.8.3, A.8.10, A.5.17 | Priority: critical | Verify: static — every fopen/g_creat/g_key_file_save_to_file/sqlite3_open for a secret/history path is followed by g_chmod/fchmod(...,S_IRUSR|S_IWUSR) with no S_IRGRP/S_IROTH; the SQLite sqlite3_open in _sqlite_init (src/database_sqlite.c) currently lacks this — close the gap. Runtime — under umask 0, init account/OMEMO/OTR/DB/export, stat() each and assert (mode & 0777)==0600. Template: database_flatfile_verify.c / database_export.c fchmod.

REQ-AUTH-02

Owner-only (0700) permissions for all secret-bearing directories

cproof SHALL create every directory under the data/config root that contains secret material or loadable code (per-account OTR/OMEMO/PGP subdirs, plugins dir) with mode 0700 (S_IRWXU) via the central create_dir() helper and SHALL NOT widen them, so secret files are not enumerable or subject to TOCTOU by other local users.

NIST: PR.AA-05, PR.DS-01 | ISO: A.8.3, A.8.2 | Priority: high | Verify: confirm create_dir() uses S_IRWXU and all account/OTR/OMEMO/PGP/plugins dir creation routes through it (no raw mkdir/g_mkdir with 0755); runtime stat() asserts (mode & 0777)==0700.

REQ-AUTH-03

Insecure transport/auth policies are explicit opt-in and logged

cproof SHALL default to MANDATORY_TLS for SASL credential exchange and SHALL enable DISABLE_TLS, TRUST_TLS, LEGACY_SSL, or LEGACY_AUTH only from explicit per-account tls.policy/auth.policy strings, logging a WARN-level diagnostic whenever a credential-exposing flag is active; none SHALL be on by default. (Complements REQ-CRY-03/04; merges REQ-CFG-12.)

NIST: PR.AA-03, PR.DS-02 | ISO: A.5.17, A.8.5, A.8.9, A.8.20 | Priority: critical | Verify: confirm in _conn_apply_settings the NULL/force branch sets MANDATORY_TLS and that DISABLE_TLS/TRUST_TLS/LEGACY_ are reachable only via explicit policy strings; test that default flags contain neither LEGACY_SSL nor LEGACY_AUTH; drive with disable/legacy and assert a warning is logged (LOG_FLAG_IF_SET path).*

REQ-AUTH-04

Account passwords are opt-in at rest; eval_password retrieval is the recommended path

cproof SHALL NOT write the account password to the accounts keyfile unless the user explicitly stored it, SHALL support non-persistent retrieval at connect time via eval_password (e.g. a system keyring), and SHALL remove the persisted password from the keyfile (g_key_file_remove_key + save) on /account clear ... password. (Merges REQ-DAR-08 clear-on-disk half.)

NIST: PR.DS-01, PR.AA-01 | ISO: A.5.17, A.8.3 | Priority: medium | Verify: configure an account with only eval_password set and assert no password key is written yet login works; after /account clear password assert the key is absent from the keyfile.

REQ-AUTH-05

eval_password runs without a shell and without ambiguous PATH resolution

cproof SHALL execute the eval_password command via direct argv spawn (g_shell_parse_argv + g_spawn_sync, never system()/popen()//bin/sh -c), SHALL treat a non-zero exit or empty output as authentication failure (never falling back to an empty password), and SHOULD avoid G_SPAWN_SEARCH_PATH so the helper binary is unambiguous and not hijackable via a poisoned PATH.

NIST: PR.AA-01, PR.AA-05 | ISO: A.8.2, A.5.17 | Priority: high | Verify: confirm no shell is used in account_eval_password and empty/failed output returns FALSE (no empty password to xmpp_conn_set_pass); review G_SPAWN_SEARCH_PATH; stub a helper returning non-zero/empty and assert FALSE and account->password stays NULL.

REQ-AUTH-06

Secrets zeroized with a non-elidable wipe before free; key buffers protected in memory

cproof SHALL overwrite, with a compiler-non-elidable routine (explicit_bzero/gcry_free of secure memory/signal_buffer_bzero_free), the buffers holding account passwords, eval_password output, reconnect-saved password copies, PGP passphrases, and OMEMO/OTR/PGP private-key/plaintext-key material immediately before release, and SHALL use libgcrypt secure memory for OMEMO key operations; ordinary free()/g_free() of a secret without a preceding wipe is non-conformant. (Merges REQ-MEM-12, REQ-DAR-08 in-memory half; OMEMO signal_buffer_bzero_free/gcry_free is the reference pattern.)

NIST: PR.DS-01, PR.AA-01, PR.PS-01 | ISO: A.5.17, A.8.3, A.8.24 | Priority: high | Verify: grep that every free/g_free of a password/passphrase/private-key buffer (src/config/account.c, src/xmpp/session.c saved_account.passwd/saved_details.passwd, src/pgp/gpg.c, src/omemo/*) is preceded by a non-elidable wipe over its length; teardown unit test asserts key buffers are zero after *_destroy; optional heap-scan-after-logout test asserts the password byte pattern is absent.

REQ-INP — Input Validation & Untrusted-Data Handling

REQ-INP-01

Validate every peer-supplied JID and fail safe on parse failure

cproof SHALL parse every JID from an untrusted source (stanza from/to, MUC item jids, roster pushes, carbons/MAM by, vCard/avatar refs) via jid_create()/jid_is_valid() and SHALL treat a NULL return as a hard parse failure — abandoning the stanza — rather than dereferencing Jid* or any part. Confirmed defect to fix: _should_ignore_based_on_silence() in src/xmpp/message.c reads from_jid->barejid after jid_create(from) with no NULL guard (remote NULL-deref DoS).

NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: grep all jid_create( call sites and assert each guards NULL before field access; unit-test test_jid.c with NULL/empty/a@@b/non-UTF-8/over-3071-char input asserting NULL; regression test driving _should_ignore_based_on_silence with a stanza lacking from asserts no crash; run under ASan.

REQ-INP-02

Enforce RFC 6122 length and character bounds in JID validation

jid_is_valid() SHALL reject any JID exceeding JID_MAX_TOTAL_LEN (3071) total or JID_MAX_PART_LEN (1023) per part, any localpart containing RFC 6122 forbidden chars (space " & ' / : < > @), more than one @ in the bare portion, empty localpart when @ is present, empty domainpart, or input failing g_utf8_validate().

NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: table-driven test_jid.c covering boundary lengths (1023/1024 per part, 3071/3072 total), each forbidden char, double-@, missing localpart/domain, invalid UTF-8; CI fails on any mis-classification.

REQ-INP-03

Never pass untrusted data as a printf/format-string argument (CWE-134)

cproof SHALL NOT pass any peer- or user-controlled string (message body/subject, nickname, JID, MUC status, URL, filename, presence status, library error text) as the format-string parameter of cons_show*, win_print*/win_println*, log_*, g_strdup_printf, or any printf-family/varargs function; such data SHALL appear only as a %s argument. Every variadic printf-like wrapper SHALL carry G_GNUC_PRINTF/__attribute__((format(printf,N,M))). (Merges REQ-MEM-09; data crossing the plugin/AI boundary (REQ-EXT-04) is covered here too.)

NIST: PR.PS-06, PR.PS-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: build with -Wformat=2 -Werror=format-security -Werror=format-nonliteral; run ./check-cwe134.sh src and require exit 0; grep cons_show(/win_print( whose first arg is a bare variable; fuzz/unit test feeding %-bearing stanza/plugin/AI content through a display sink.

REQ-INP-04

Build promotes format-security diagnostics to errors

The build SHALL set -Wformat=2 and promote format diagnostics to errors (-Werror=format-security, and -Werror=format-nonliteral where supported) for cproof translation units so any CWE-134 regression fails the build; the check-cwe134.sh audit SHALL run in CI as a blocking (not continue-on-error) step.

NIST: PR.PS-06, PR.PS-01 | ISO: A.8.28, A.8.25, A.8.29 | Priority: high | Verify: inspect configure.ac AM_CFLAGS for -Werror=format-security; confirm the CWE-134 CI step is blocking; introduce a deliberate non-literal format and confirm the build/CI fails.

REQ-INP-05

Gate XEP-0359 stanza-id/origin-id trust on the asserting entity's disco support

When consuming a <stanza-id by='…'> or MAM <result> id, cproof SHALL honour it only when by equals the user's own bare JID or the message's authoritative archive (MUC bare JID or account domain) AND that entity advertises urn:xmpp:sid:0 via disco; ids from any other by SHALL be ignored. Ownership of an outgoing message SHALL be established by verifying the HMAC in origin-id (message_is_sent_by_us), not by id-string match.

NIST: PR.DS-02, PR.AA-03 | ISO: A.8.26, A.8.28 | Priority: high | Verify: test a <stanza-id by='attacker@evil'> from a different sender is not trusted (_stanza_id_by_trusted); toggle urn:xmpp:sid:0 in features and assert the gate; assert message_is_sent_by_us rejects a valid-length prefix with a wrong HMAC.

REQ-INP-06

Validate URL scheme and pass URLs/filenames as argv, never to a shell

Before acting on a peer/user-supplied URL (/url open, /url save, OOB/file-transfer refs), cproof SHALL whitelist the scheme via g_uri_parse_scheme() (only http, https, aesgcm) and SHALL invoke any external open/download helper by building an explicit argv vector (format_call_external_argv + call_external/g_spawn, no shell), so URL/filename content can never be interpreted as shell metacharacters or extra args. (Merges REQ-EXT-05 URL half.)

NIST: PR.PS-06, PR.PS-05, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: grep system(/popen(/sh -c/g_spawn_*sync over src/ and confirm none operate on URL input; unit-test scheme handling rejects file:/javascript:/data:; test a URL with spaces/quotes/; reaches the helper as one argv element.

REQ-INP-07

No shell-string desktop notification command injection

Desktop notification invocation SHALL NOT construct a shell command string from message-derived content for system(); it SHALL use a libnotify/g_spawn argv API or rigorously g_shell_quote every interpolated value, treating message/sender content as untrusted data. Defect to remediate: src/ui/notifier.c builds a notify string and runs it via system().

NIST: PR.PS-05 | ISO: A.8.26, A.8.28 | Priority: high | Verify: grep notifier.c for system(/popen(; confirm migration to argv g_spawn or full quoting; test a sender/message with quotes, $(), backticks, ; spawns no extra process.

REQ-INP-08

Bound and sanitize peer-controlled text before display, logging and storage

cproof SHALL treat decrypted bodies, subjects, MUC topics, presence status, nicknames and resource parts as untrusted and SHALL neutralize terminal-control and Unicode bidi-override sequences (e.g. ESC/CSI, U+202E) and enforce a sane maximum rendered length before writing to the ncurses UI, log files, or the SQLite/flat-file backend, so peer content cannot inject terminal escapes, spoof UI via bidi, or corrupt log/DB output.

NIST: PR.DS-01, PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test a body containing raw ESC[2J, a NUL, and U+202E asserting the stored/displayed form is sanitized/escaped; review the display sink for a control-char filter and a length cap on rendered peer strings.

REQ-INP-09

Strict structural validation of incoming stanzas; cap MAM result sets

cproof SHALL guard every stanza consumer so that missing/malformed mandatory children/attributes (absent body/from, non-JID from, unexpected namespaces) cause the stanza to be skipped rather than NULL-dereferencing the result of xmpp_stanza_get_child_by_name/get_attribute, SHALL not call abort()/assert() on attacker-influenced conditions, and SHALL cap MAM results requested/processed per query (MESSAGES_TO_RETRIEVE). (Merges REQ-MEM-08, REQ-RES-05.)

NIST: PR.PS-06, PR.DS-02, PR.IR-01 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: fuzz/replay malformed stanzas (missing body/from/jid, deeply nested, huge MAM batches, oversized base64 OMEMO key) into message.c/iq.c/presence.c/muc.c/omemo.c handlers under ASan/UBSan asserting clean rejection and no crash; code-review NULL checks after each xmpp_stanza_get_* on the receive path; confirm assert() in receive paths is debug-only and unreachable from peer data.

REQ-INP-10

Validate and escape data crossing into AI/LLM outbound requests and responses

Where src/ai forwards user/peer content into an outbound LLM request, it SHALL JSON-escape all such content via ai_json_escape() before embedding it, and SHALL parse provider responses with bounds-checked unescaping (_json_unescape_substring/_extract_json_string) so an embedded quote or control byte cannot break the JSON string context.

NIST: PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test ai_json_escape() with ", \, \n and control bytes asserting valid escaped output; test _extract_json_string() against a response value embedding an escaped quote; confirm every request-body assembly routes content through ai_json_escape().

REQ-MEM — Memory Safety & Secure Coding

REQ-MEM-01

Full compiler/linker hardening baseline in release builds, at parity across build systems

Every released cproof binary and plugin SHALL be compiled/linked with -fstack-protector-strong, -fno-common, -D_FORTIFY_SOURCE=2 (or 3 where supported), -Wformat=2, full RELRO and immediate binding (-Wl,-z,relro -Wl,-z,now), non-executable stack (-Wl,-z,noexecstack), and a position-independent executable (-fPIE -pie); the autotools and meson definitions SHALL stay at flag parity, and the build SHALL fail if any flag is silently dropped. Known gaps to close: configure.ac omits -fPIE/-pie; meson.build omits the RELRO/-z,now linker hardening present in autotools. (Canonical hardening requirement; merges REQ-CRY-12, REQ-AUTH-09, REQ-CFG-10, REQ-SUP-07, REQ-EXT-10, REQ-RES-10, REQ-SDLC-01, REQ-VUL-09.)

NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.27, A.8.28 | Priority: critical | Verify: grep configure.ac and meson.build for each flag and assert parity; on the built ./profanity run checksec/readelf -d/readelf -h and assert Full RELRO, BIND_NOW, NX, PIE (Type DYN), stack canary, FORTIFY symbols present (and CET/-fstack-clash-protection where the toolchain supports them); add a CI step that fails when any mitigation is missing on the produced binary and loaded .so plugins.

REQ-MEM-02

ASan + UBSan + unsigned-overflow build gates the test suite in CI

The project SHALL provide an --enable-sanitizers build (ASan + UBSan + -fsanitize=unsigned-integer-overflow, -fno-sanitize-recover=all) and SHALL run the full cmocka unit/functional suite under it in CI on every pull request, AND additionally run the suite under Valgrind with --error-exitcode=1 --leak-check=full; any sanitizer/Valgrind error or definite leak SHALL fail the build. (Merges REQ-SDLC-02, REQ-RES-10 sanitizer half.)

NIST: PR.PS-06, DE.CM-01, ID.RA-01 | ISO: A.8.28, A.8.29, A.8.25 | Priority: critical | Verify: confirm a non-continue-on-error CI job runs ./configure --enable-sanitizers && make && make check and that ci-build.sh invokes Valgrind with --error-exitcode=1; intentionally introduce a heap overflow in a test and confirm CI fails.

REQ-MEM-03

No unbounded/unsafe C string or format primitives on untrusted data

cproof SHALL NOT use strcpy, strcat, sprintf, gets, or unbounded scanf on data derived from stanzas, peer content, MUC, file-transfer/URL input, or remote JIDs; such code SHALL use length-bounded equivalents (g_strdup_printf, g_strlcpy, snprintf with checked return). Defects to remediate: strcpy/strcat on peer barejids in src/pgp/ox.c:153-156; sprintf in src/omemo/crypto.c:479/483 and src/xmpp/iq.c:1787.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: grep -rnE '\b(strcpy|strcat|sprintf|gets)\(' src/ and confirm each remaining hit operates only on fixed compile-time-constant strings; add a CI grep that fails on these calls in stanza/crypto-handling files.

REQ-MEM-04

Overflow-checked allocation sizes

Every malloc/realloc/g_malloc size that multiplies or adds attacker-influenced lengths SHALL be computed with overflow-checked arithmetic (__builtin_mul_overflow/__builtin_add_overflow or g_malloc_n-style helpers) and SHALL error/abort rather than allocate a truncated buffer; realloc results SHALL be checked before the old pointer is overwritten.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: review multiplicative/additive malloc/realloc args in src/tools/http_upload.c/http_download.c, src/common.c, src/xmpp/connection.c, src/omemo/omemo.c (identity_public_key_len*2+1, malloc(*length)) for a preceding overflow check; UBSan unsigned-overflow tests feeding near-SIZE_MAX lengths.

REQ-MEM-05

Underflow-guarded unsigned subtraction on sizes/offsets (per project unsigned policy)

cproof SHALL guard every subtraction of unsigned size/length/offset values that could go negative against wrap to ~SIZE_MAX, using pre-checks or builtins rather than casting an operand to signed, consistent with the project's unsigned-arithmetic policy.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: build with -fsanitize=unsigned-integer-overflow and run tests/fuzz over UI scroll/pad math (src/ui/inputwin.c documented SIZE_MAX guard) and parser/crypto buffer-slicing; code-review subtractions of size_t/guint operands for a lower-bound check; confirm no size_t is cast to int to mask a wrap.

REQ-MEM-06

Mandatory auto-cleanup macros for heap/FILE/fd ownership

Local owning pointers and resources SHALL use the project auto-cleanup attributes (auto_gchar, auto_char, auto_gcharv, auto_guchar, auto_FILE, auto_gfd, auto_gerror, auto_jid, auto_sqlite) instead of manual free/g_free/fclose/close on early-return paths, so ownership is released exactly once on every exit path.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: medium | Verify: code-review that new owning locals use auto_* rather than trailing frees; run make check under ASan/LeakSanitizer asserting no leaks/double-frees; grep changed handlers for free()/g_free() paired with early returns that should use auto_*.

REQ-MEM-07

Null freed pointers; document out-parameter ownership; no UAF on crypto buffers

After freeing a heap pointer that remains in scope or in a struct field, cproof SHALL set it to NULL; functions returning allocated memory via out-parameters SHALL define ownership; freed crypto/session/key buffers (gcry_free/free paths in src/omemo, src/otr, src/pgp) SHALL NOT be referenced after release.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: ASan/UBSan make check over crypto and session teardown; code-review each free of a still-live pointer is followed by =NULL; targeted tests double-invoking teardown to confirm no double-free.

REQ-MEM-08

NULL-checks on every peer/remote-derived parse/lookup before dereference

cproof SHALL validate for NULL the result of every parse/lookup that can fail (XMPP attribute/child lookups, JID parsing, base64/hex decode, GKeyFile reads, library returns) before dereferencing and SHALL handle absence as a recoverable error. (Closely related to REQ-INP-09; this is the codebase-wide secure-coding form.)

NIST: PR.PS-01, DE.CM-01 | ISO: A.8.28 | Priority: high | Verify: enable -Wnull-dereference and review warnings; run crafted-stanza/malformed-input tests under ASan asserting no NULL-deref crash; code-review each new xmpp_stanza_get_*/g_key_file_get_*/decode result for a NULL guard (cf. the autoping NULL-domain guard 15dfc2bd).

REQ-MEM-09

No alloca/VLA sized from untrusted lengths

cproof SHALL NOT size stack allocations (alloca or VLAs) from attacker-influenced lengths; such buffers SHALL be heap-allocated with checked sizes or bounded by a validated compile-time maximum. Defects to remediate: alloca((strlen(barejid)+6)) in src/pgp/ox.c:150-156; unsigned char out[mac_len] VLA in src/omemo/crypto.c:107.

NIST: PR.PS-01 | ISO: A.8.28, A.8.27 | Priority: medium | Verify: grep -rnE '\balloca\(' src/ and review for non-constant sizes; compile with -Wvla and treat as error to flag VLAs; replace flagged sites with checked heap allocation + auto_* cleanup.

REQ-DAR — Data Protection at Rest & Privacy

File/directory permission and in-memory/at-rest secret-zeroization requirements for at-rest material are consolidated in REQ-AUTH-01 (0600 files, including the SQLite DB gap), REQ-AUTH-02 (0700 dirs), and REQ-AUTH-06 (zeroization). The requirements below cover history-content minimization, redaction, and at-rest integrity.

REQ-DAR-01

Functional no-storage (off) mode writes no message content

When PREF_DBLOG is off, cproof SHALL NOT write any message body, sender/recipient JID-pair, or per-message timestamp derived from chat traffic to any history backend (SQLite or flat-file), for incoming, outgoing, MUC and MUC-PM messages alike.

NIST: PR.DS-01 | ISO: A.8.10 | Priority: high | Verify: set PREF_DBLOG=off, drive add_incoming + all add_outgoing_*, assert zero rows in SQLite ChatLogs and zero content lines in the flat-file; confirm get_previous_chat returns nothing; pin the early-return in both backends (database_sqlite.c, database_flatfile.c).

REQ-DAR-02

Redaction (redact) mode never leaks plaintext into storage

When PREF_DBLOG is redact, cproof SHALL substitute the message body with a fixed placeholder ([REDACTED]) before it reaches any persistence call, so no original body character is passed to sqlite3_bind or written to the flat-file; non-content metadata may be retained.

NIST: PR.DS-01 | ISO: A.8.11 | Priority: high | Verify: with redact, store a message containing a unique sentinel, then grep the raw chatlog.db bytes and flat-file and assert the sentinel is absent while a [REDACTED] row/line exists; confirm redaction precedes the bind/write.

REQ-DAR-03

No plaintext chat content or secrets in operational/debug logs

cproof SHALL NOT write decrypted message bodies, account passwords, eval_password output, SASL credentials, OMEMO/OTR/PGP private keys, or AI provider API keys/Bearer tokens into profanity.log at any level; statements involving message content SHALL log only metadata (JIDs, ids, sizes, encryption type, library error code). Defect to remediate: src/database_sqlite.c:760 log_error('content: %s', message->plain) on insert failure (bypasses no-storage/redact). The libstrophe debug bridge _xmpp_file_logger SHALL redact SASL <auth>/<challenge>/<response> and XEP-0077 registration credentials before they reach _log_msg. (Merges REQ-LOG-01, REQ-LOG-02, REQ-EXT-03 log half; SDLC secret-scanning is REQ-SDLC-04.)

NIST: PR.PS-04, PR.DS-01 | ISO: A.8.12, A.8.15 | Priority: critical | Verify: grep log_* for interpolation of message->plain/message->body/password/api_key/token/Bearer/key bytes and assert none print the value; drive login+decrypt+AI-request with a sentinel secret and assert it never appears in the log; fix database_sqlite.c:760; confirm src/ai/ai_client.c never logs the api_key and never sets CURLOPT_VERBOSE; connect with XMPP debug logging and a sentinel password and assert it (and its base64) is absent.

REQ-DAR-04

Atomic, permission-preserving writes for history export and keyfile rewrites

cproof SHALL write history exports and rewritten secret/history files atomically via a private (0600) temp file created with mkstemp/O_EXCL followed by rename, never via a predictable fixed temp path, so a concurrent or hostile local process cannot pre-create, read, or race the file.

NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: review src/database_export.c (mkstemp+fchmod 0600+rename) and any flat-file rewrite/compaction for the same pattern with no fixed-name fallback; test pre-creating the legacy predictable tmp name as a symlink and assert the export refuses or writes through a fresh mkstemp 0600 file.

REQ-DAR-05

Encryption-state and metadata accuracy in stored records

cproof SHALL persist the correct encryption type (none/omemo/otr/pgp/ox) for every stored message row matching the actual delivery path, and SHALL NOT record a decryption-failure message as encryption='none' plaintext or store raw ciphertext as the message body.

NIST: PR.DS-01 | ISO: A.8.11 | Priority: medium | Verify: tests asserting an OMEMO/OTR/PGP/OX message round-trips with the matching encryption value (_get_message_enc_type), and a decryption-failure message is either not stored or stored with an explicit non-none marker rather than its ciphertext under none.

REQ-DAR-06

Clear-on-close history minimization honored

When PREF_CLEAR_PERSIST_HISTORY is disabled, cproof SHALL discard in-window scrollback on window close without re-persisting it beyond the configured backend, and /clear behavior SHALL be consistent with the PREF_DBLOG retention setting.

NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: toggle PREF_CLEAR_PERSIST_HISTORY and assert closing a chat window with it disabled produces no new persisted rows; confirm /clear with persist off does not write back buffer content (src/ui/window.c, /clear handler).

REQ-DAR-07

Keep plaintext per-day chatlog subsystem disabled; document retention defaults

The legacy plaintext per-day chat-log writers (chatlog.c entry points) SHALL remain no-ops and SHALL NOT be re-enabled except behind an explicit, off-by-default preference that documents it stores decrypted content unencrypted; the flat-file/SQLite backends SHALL be the only message sinks, and PREF_DBLOG values (off/redact/flatfile/store) and their privacy implications SHALL be documented. (Merges REQ-LOG-09, REQ-DAR retention.)

NIST: PR.DS-01, PR.PS-04, GV.PO-01 | ISO: A.8.12, A.5.34 | Priority: low | Verify: assert chatlog.c functions remain no-ops (no fopen/write) via test/grep; send/receive an encrypted message and assert no chatlogs/ plaintext file under XDG_DATA_HOME; confirm PREF_DBLOG documented; require any new logging pref to default false.

REQ-LOG — Logging, Error Handling & Detection

Log-file 0600 permission is in REQ-AUTH-01; no-secrets-in-logs is in REQ-DAR-03.

REQ-LOG-01

Security-relevant events logged at WARN or higher

cproof SHALL record at PROF_LEVEL_WARN/ERROR (so they survive the default WARN filter): TLS certificate validation failure, OMEMO/OTR/PGP/OX decryption or MAC failure, use/rejection of an untrusted/unknown device fingerprint, and SASL authentication failure.

NIST: DE.CM-06, DE.AE-02 | ISO: A.8.15, A.8.16 | Priority: high | Verify: grep that cert-fail (_connection_certfail_cb/sv_ev_certfail), decrypt-fail (src/omemo, src/pgp/ox.c), untrusted-device and SASL-failure paths call log_warning/log_error (not log_debug); functional test injecting a bad cert and a corrupt OMEMO ciphertext asserts a WARN/ERROR line at the default filter.

REQ-LOG-02

Detect and log TLS/security downgrade conditions

cproof SHALL log a WARN-or-higher event whenever a connection is established/negotiated below the expected posture: TLS negotiation failed, connecting without mandatory TLS, an unencrypted (DISABLE_TLS) connection, or a followed see-other-host/stream redirect — so silent transport downgrades are detectable.

NIST: DE.AE-02, DE.CM-06 | ISO: A.8.16, A.8.15 | Priority: high | Verify: inspect src/xmpp/connection.c that DISABLE_TLS/non-mandatory/TLS-failed/see-other-host paths emit log_warning; test connecting with TLS disabled asserts a WARN line.

REQ-LOG-03

Safe, non-leaking error messages on crypto/parse failures

Error/warning messages on crypto-operation failure, stanza/XML parse failure, or DB/keyfile load failure SHALL contain only a failure category and a stable library error code/string, and SHALL NOT embed raw ciphertext, key bytes, full untrusted stanza payloads, or secret-derived values. (Reinforces REQ-DAR-03.)

NIST: PR.PS-04, DE.AE-02 | ISO: A.8.15, A.8.28 | Priority: medium | Verify: review crypto-failure log_error sites (src/omemo/omemo.c, src/pgp/ox.c gpgme_strerror, stanza handlers) to confirm they pass strerror codes/identifiers, not buffers/key material.

REQ-LOG-04

Log level user-configurable, defaulting to non-debug

cproof SHALL expose a configurable filter (DEBUG/INFO/WARN/ERROR) honored by _should_log(), default to a non-DEBUG level, map unknown level strings to WARN, and require explicit user action to enable DEBUG (which may capture additional protocol detail).

NIST: PR.PS-04, ID.RA-01 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: unit-test _should_log across levels confirming below-filter records drop; confirm log_level_from_string defaults unknown to WARN and DEBUG is not the startup default.

REQ-LOG-05

Bounded log growth via size-based rotation

When rotation is enabled, the logger SHALL rotate the main log once it reaches max_log_size, SHALL bound the number of rotated files, and rotated files SHALL inherit 0600, preventing unbounded disk consumption while preserving a finite reviewable history.

NIST: PR.PS-04, PR.IR-04 | ISO: A.8.15, A.8.6 | Priority: medium | Verify: test that writing past max_log_size triggers _rotate_log_file(), produces a bounded-index profanity.NNN.log (also 0600), reopens the main file; confirm user-provided log files are excluded from auto-rotation.

REQ-LOG-06

stderr capture fails safe and respects sensitive-data constraints

The stderr-capture facility (log_stderr_init/log_stderr_handler) that redirects process stderr into the log SHALL handle init/read errors (including EINTR/partial reads) without crashing or losing the fd, and SHALL be subject to the same no-secrets constraint so crypto-library diagnostic output to stderr (gcrypt/gpgme/libsignal) does not persist key/passphrase material.

NIST: PR.PS-04, DE.CM-06 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: review error paths in log_stderr_init for correct fd cleanup and non-fatal failure; write a sentinel secret to stderr and assert redaction/handling; verify the handler tolerates EINTR/partial reads without data loss or crash.

REQ-CFG — Secure Configuration & Secure Defaults

TLS-mandatory default (REQ-CRY-03), cert-validation/TOFU defaults (REQ-CRY-04), tls.policy allowlist (REQ-CRY-05), OMEMO manual trust default (REQ-CRY-10), legacy-flag gating (REQ-AUTH-03), and toolchain hardening (REQ-MEM-01) are consolidated above. The CFG-specific defaults remain:

REQ-CFG-01

HTTP(S) file transfer must not silently disable TLS verification

cproof SHALL keep CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST enabled for XEP-0363 upload and HTTP download by default, and SHALL disable them ONLY as an explicit consequence of the operator-selected account tls.policy=trust (or an explicit PREF_TLS_CERTPATH=none), never from any other condition or default; that insecure state SHALL be surfaced to the user, and downloaded files SHALL be written canonicalized within the configured downloads directory (rejecting peer-supplied path traversal). (Merges REQ-EXT-08.)

NIST: PR.AA-03, PR.DS-02, PR.DS-10 | ISO: A.8.9, A.8.24, A.5.14 | Priority: high | Verify: confirm in src/tools/http_upload.c/http_download.c the insecure boolean derives solely from tls_policy=="trust"/explicit none, and that with force/NULL the handles retain VERIFYPEER=1/VERIFYHOST=2; grep for any unconditional SSL_VERIFY*=0; test a peer filename containing ../ stays inside the downloads dir.

REQ-CFG-02

Encryption-loss warning and TLS indicator on by default

cproof SHALL default PREF_ENC_WARN and PREF_TLS_SHOW to enabled so the user is warned before sending unencrypted and is shown transport security state out of the box.

NIST: PR.PS-01, DE.CM-09 | ISO: A.8.9 | Priority: medium | Verify: unit-test prefs_get_boolean(PREF_ENC_WARN) and prefs_get_boolean(PREF_TLS_SHOW) both return TRUE on a fresh config; review neither falls through to the FALSE branch.

REQ-EXT — Extension & External-Interface Security (Plugins, AI, File/URL/Command Surfaces)

URL/shell handling is REQ-INP-06; notification injection REQ-INP-07; AI JSON escaping REQ-INP-10; AI key-in-log/at-rest REQ-DAR-03/REQ-AUTH-01; file-transfer TLS/traversal REQ-CFG-01; hardening REQ-MEM-01.

REQ-EXT-01

Enforce TLS peer/host verification and https scheme on AI LLM outbound requests

The AI client SHALL set CURLOPT_SSL_VERIFYPEER=1 and CURLOPT_SSL_VERIFYHOST=2 on every outbound LLM request, SHALL require the provider api_url scheme to be https (rejecting http:// at provider-add and request time), and SHALL NOT expose any per-provider option to disable verification, so the API key and conversation content are never sent in cleartext or to an unverified peer. (Merges REQ-CFG-06.)

NIST: PR.DS-02, PR.PS-01 | ISO: A.5.14, A.8.24, A.8.27 | Priority: critical | Verify: grep src/ai/ai_client.c (_curl_exec_and_handle/_ai_request_thread) for VERIFYPEER/VERIFYHOST set non-zero on both curl sites and never to 0L; add a test that ai_add_provider rejects an http:// api_url.

Correction: The audit's REQ-EXT-01 finding refines this requirement's premise. libcurl defaults CURLOPT_SSL_VERIFYPEER=1/VERIFYHOST=2, so HTTPS provider URLs are certificate-verified by default, and a provider scheme check already exists (src/command/cmd_funcs.c:7019). The confirmed, narrower defect is that the scheme check accepts both http and https, so an http:// provider URL ships the API key and full conversation in cleartext. The requirement's normative remediation (reject non-https, set VERIFYPEER/VERIFYHOST explicitly as defense-in-depth) stands; only the residual-risk rating moves from "critical" to "high".

REQ-EXT-02

Explicit opt-in consent before transmitting conversation content to LLM providers

The AI client SHALL transmit message/conversation content to an external LLM provider only after the user has explicitly enabled the feature and configured a provider, SHALL be disabled by default, and SHALL NOT auto-forward received XMPP chat/MUC messages to any provider without an explicit per-action user command (no wiring to inbound-message plugin/stanza hooks). AI prompt/response content SHALL NOT be ingested into the XMPP chat history DB/chatlogs and SHALL live only in volatile per-session memory unless the user opts into persistence. (Merges REQ-EXT-09.)

NIST: PR.DS-01, GV.PO-01 | ISO: A.5.14, A.8.24, A.8.26 | Priority: critical | Verify: confirm the AI feature is gated behind a default-off preference and explicit /ai command; verify no prof_pre/post chat/room hook or stanza-receive path calls ai_send_prompt; test that with no provider configured ai_send_prompt performs no network call; confirm no code path passes AI session content into chatlog_/database_ APIs and history is freed on ai_session_unref.

REQ-EXT-03

Bounded, time-limited AI calls with response-size cap; bounded plugin/AI buffering

Every AI curl handle SHALL set a finite CURLOPT_TIMEOUT (and ideally CURLOPT_CONNECTTIMEOUT), and _write_callback SHALL cap accumulated response size (current 10 MB) and abort the transfer when exceeded, so a slow/hung/malicious endpoint cannot hang the UI thread or exhaust memory.

NIST: PR.IR-03, ID.RA-09 | ISO: A.8.6, A.8.16 | Priority: high | Verify: review every AI curl handle sets CURLOPT_TIMEOUT and that _write_callback returns short to abort at the cap; test with a mock server that streams past the cap and one that stalls, asserting bounded abort.

REQ-EXT-04

Plugins load only from the protected 0700 plugins directory; run with full privilege; downloads verified

C plugins (dlopen with RTLD_NOW|RTLD_GLOBAL) and Python plugins SHALL be loaded only from the 0700 per-user plugins directory under the data path (files_get_data_path(DIR_PLUGINS), no user-supplied absolute/CWD/relative/shared path), SHALL NOT be auto-loaded without explicit user opt-in, SHALL be stored without world/group-write bits, and any /plugins install download SHALL require an https source with TLS verification. The full-privilege trust model (plugins access all secret stores) SHALL be documented. (Merges REQ-AUTH-08, REQ-SUP-09 plugin half.)

NIST: PR.AA-05, PR.PS-01, ID.RA-09 | ISO: A.8.19, A.8.25, A.8.2 | Priority: high | Verify: confirm the load dir derives only from files_get_data_path(DIR_PLUGINS) and is created S_IRWXU; confirm no implicit auto-load of untrusted paths; verify plugin_download.c enforces https + TLS verify (no SSL_VERIFY 0L); check installed plugin files lack world/group write; document the plugin trust model in user docs.

REQ-SUP — Supply Chain & Third-Party Dependencies

REQ-SUP-01

Pin the libstrophe Meson subproject to an immutable revision/checksum

subprojects/libstrophe.wrap SHALL pin libstrophe to an exact immutable revision (40-hex commit SHA or release tag plus source_hash/wrapdb checksum) and SHALL NOT use revision = HEAD or a bare branch, so a build cannot silently incorporate an attacker-modified or regressed XMPP parser. (Merges REQ-VUL-03, REQ-SDLC-07 wrap half; libstrophe sits on the untrusted network path.)

NIST: GV.SC-05, ID.RA-09, PR.PS-01 | ISO: A.5.21, A.5.23, A.8.8, A.8.28, A.8.30 | Priority: critical | Verify: grep the .wrap: assert no revision = HEAD/bare branch and presence of a 40-hex revision or source_hash; add a CI lint failing on unpinned wrap revisions; confirm two clean builds at the pinned revision produce identical libstrophe object hashes.

REQ-SUP-02

Pin CI base images by digest and bootstrapped deps by commit; verify download provenance

All ci/Dockerfile.* base images SHALL be pinned by immutable digest (FROM image@sha256:…, not debian:testing/archlinux:latest), and all source dependencies cloned/downloaded during image build (libstrophe, stabber, AUR libstrophe-git) SHALL be checked out at a pinned commit and obtained over authenticated HTTPS with integrity verification (commit/checksum/signature), not --depth 1 HEAD or unverified wget+makepkg. (Merges REQ-SUP-08, REQ-VUL-10.)

NIST: GV.SC-06, GV.SC-07, GV.SC-05 | ISO: A.5.20, A.5.21, A.5.23, A.8.30 | Priority: critical | Verify: grep Dockerfiles for FROM .*:latest/:testing, clone --depth 1 without a following git checkout <sha>, and wget/curl downloads lacking a subsequent sha256sum/gpg check; assert each FROM contains @sha256:; confirm makepkg integrity checks are not bypassed.

REQ-SUP-03

Enforce minimum, CVE-informed versions for all security-relevant dependencies (build fails below floor)

The build SHALL fail (AC_MSG_ERROR, not notice/warn) when any security-relevant dependency is below a documented CVE-informed minimum: libstrophe, libcurl, sqlite3, glib, gpgme, libotr, libsignal-protocol-c/libomemo-c, libgcrypt. gpgme in particular SHALL be checked via versioned PKG_CHECK_MODULES([gpgme],[gpgme >= X.Y]), replacing the unversioned AC_CHECK_LIB([gpgme],[main]). The declared minimums SHALL be identical between configure.ac and meson.build (current divergences: sqlite3 3.22.0 vs 3.35.0; gpgme unversioned), each floor documented with security rationale. (Merges REQ-CRY-11, REQ-SUP-03, REQ-SUP-04, REQ-VUL-02.)

NIST: ID.RA-09, GV.SC-04, PR.PS-02 | ISO: A.5.21, A.8.8, A.8.28 | Priority: critical | Verify: grep both files to confirm a version-bearing PKG_CHECK_MODULES (no bare AC_CHECK_LIB([gpgme],[main])); a script diffs the (name,minversion) tuples and fails on mismatch; configuring against a stubbed under-floor .pc aborts configure non-zero; confirm gcry_check_version(GCRYPT_VERSION) gates init.

REQ-SUP-04

Continuous dependency-CVE scanning in CI

CI SHALL run an automated known-vulnerability scanner (OSV-Scanner/Dependabot/grype/Trivy) over the third-party set (libstrophe, gpgme, libsignal/libomemo-c, openssl/gnutls, libgcrypt, libotr, sqlite, glib, libcurl) on each push and on a recurring schedule, with results triaged and surfaced; a seeded known-vulnerable pinned version SHALL trigger an alert. (Merges REQ-VUL-04.)

NIST: ID.RA-01, GV.SC-08, ID.IM-02 | ISO: A.5.7, A.8.8 | Priority: high | Verify: confirm a scan job exists in .github/workflows (or dependabot.yml for the wrap/Docker ecosystems), runs on cron, and fails/reports above a defined severity; verify a seeded vulnerable version triggers an alert; check triage records exist.

REQ-SUP-05

Pin GitHub Actions to commit SHAs

All third-party GitHub Actions SHALL be pinned to a full commit SHA, not a mutable tag (actions/checkout@<sha> not @v4).

NIST: GV.SC-06, PR.PS-01 | ISO: A.8.30, A.8.28 | Priority: medium | Verify: grep .github/workflows/*.yml for uses:.*@v[0-9] or any non-40-hex ref and fail; optionally enforce with a pinning linter (zizmor/ratchet) in CI.

REQ-SUP-06

Govern the C/Python plugin and AI-provider dependency boundary (documentation)

The project SHALL document and enforce the runtime trust boundary: dlopen'd C plugins and Python plugins are out-of-tree third-party code that run with full process privilege and SHALL NOT auto-load without opt-in (enforced by REQ-EXT-04), and the AI client's outbound LLM endpoints/dependencies SHALL be restricted to user-configured, TLS-validated hosts (enforced by REQ-EXT-01).

NIST: GV.SC-04, ID.RA-09 | ISO: A.5.19, A.8.30 | Priority: high | Verify: confirm documentation of the plugin/AI supply-chain boundary exists and that the enforcing controls (REQ-EXT-01/04) have passing tests; confirm the AI module only contacts configured hosts with cert verification.

REQ-VUL — Vulnerability & Patch Management, Change Control

Dependency floors (REQ-SUP-03), wrap pinning (REQ-SUP-01), CVE scanning (REQ-SUP-04), reproducible CI images (REQ-SUP-02), and hardening verification (REQ-MEM-01) are consolidated above.

REQ-VUL-01

Documented vulnerability-disclosure policy with scope, supported versions and response SLA

SECURITY.md SHALL specify, beyond a contact and PGP key: supported/maintained versions, in-scope components (OMEMO/OTR/PGP/OX crypto paths, XMPP stanza parsing, TLS, plugins, AI client), out-of-scope items, a target acknowledgement and remediation timeframe, and coordinated-disclosure expectations; designated owners for security-critical subsystems SHALL be recorded. (Merges REQ-SDLC-10.)

NIST: RS.MA-01, ID.IM-01, GV.RR-02, GV.OC-02 | ISO: A.5.5, A.5.26, A.8.8, A.5.2 | Priority: high | Verify: inspect SECURITY.md for a Supported Versions section, enumerated in/out-of-scope components, an explicit ack/remediation SLA, and coordinated-disclosure terms; confirm the contact key/fingerprint is current; add a CI doc-lint failing if required headings are absent.

REQ-VUL-02

Update-availability check verifies TLS and treats the response as untrusted

release_get_latest() SHALL explicitly set CURLOPT_SSL_VERIFYPEER=1L and CURLOPT_SSL_VERIFYHOST=2L, constrain itself to HTTPS, and treat the fetched version string as untrusted (strict ^\d+\.\d+\.\d+$ validation before display), never auto-downloading or executing anything.

NIST: ID.RA-01, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: high | Verify: read src/common.c and assert the curl handle sets VERIFYPEER/VERIFYHOST and rejects non-https; assert cons_check_version/release_is_new validate against the version regex before use; unit-test feeding malformed/oversized version strings.

REQ-VUL-03

User-controllable suppression of version/OS disclosure to peers

XEP-0092 version replies and disco SHALL be gated by user preferences, default to NOT revealing OS details (PREF_REVEAL_OS off), and allow disabling the <version> element entirely, so a peer cannot fingerprint an un-patched client+OS+build to target known CVEs.

NIST: PR.AA-05, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: medium | Verify: inspect the src/xmpp/iq.c version-reply: confirm <os> is added only when PREF_REVEAL_OS is true and is_custom_client false, a pref exists to omit <version>, and PREF_REVEAL_OS defaults off; stabber/functional test asserting the reply omits <os> by default.

REQ-VUL-04

Security fixes recorded in CHANGELOG with CVE/advisory references

Each release SHALL document security-relevant fixes in the CHANGELOG, referencing the associated CVE/advisory identifier where one exists, so downstream packagers and users can assess patch urgency.

NIST: RC.RP-05, ID.IM-04 | ISO: A.8.8, A.5.26 | Priority: medium | Verify: review the CHANGELOG for a security/fix section per release with CVE/advisory ids where applicable (precedent: Fix CVE-2017-5592); extend RELEASE_GUIDE.md to mandate a security-fix changelog review before tagging.

REQ-RES — Resilience, Robustness & Fail-Safe Behavior

Fail-closed E2EE (REQ-CRY-01), force-encryption gate (REQ-CRY-02), OMEMO fail-safe trust (REQ-CRY-10), bounded stanza/OMEMO parsing (REQ-INP-09), and bounded AI calls (REQ-EXT-03) are consolidated above.

REQ-RES-01

Disk-space and atomicity preflight for SQLite schema migrations

Before any chatlog schema migration cproof SHALL verify free disk space via _check_available_space_for_db_migration() and SHALL run each migration inside a single transaction (BEGIN/COMMIT with rollback on error), so a failed/interrupted migration leaves the DB at its prior consistent schema version; on failure it SHALL surface an error and refuse to continue with a partially-migrated DB.

NIST: RC.RP-01, PR.IR-01 | ISO: A.8.13, A.8.27 | Priority: high | Verify: local (non-CI) migration tests on v1/v2 fixtures: simulate ENOSPC and assert migration refused with version unchanged; abort mid-migration and assert DbVersion reflects the pre-migration version; static review each _migrate_to_vN wraps statements in a transaction with rollback.

REQ-RES-02

Detect and gracefully handle corrupted history DB without crashing or data loss

cproof SHALL run PRAGMA integrity_check on the chatlog DB and SHALL handle a non-ok result (and sqlite3_open/prepare failures) by reporting to the user and degrading gracefully (e.g. disabling DB-backed history for the session) rather than abort()/exit() or silently writing into a corrupt store.

NIST: RC.RP-01, PR.IR-03 | ISO: A.8.27, A.8.13 | Priority: high | Verify: open a deliberately truncated/garbage DB file and assert the app reports an integrity issue and continues running (no abort/exit, history disabled); confirm _sqlite_verify_integrity results are inspected and surfaced.

REQ-RES-03

Graceful connection-loss handling with state-preserving reconnect (no message loss, no security downgrade)

On unexpected connection failure cproof SHALL transition to a defined DISCONNECTED state, preserve XEP-0198 stream-management state for resumable reconnect, and gate reconnect/auto-ping on a valid (non-NULL) connection, so connection flicker does not cause crashes, duplicate/dropped messages, or any change in the per-chat negotiated encryption posture (E2EE flags MUST survive reconnect).

NIST: PR.IR-03, RC.RP-01 | ISO: A.8.27, A.5.29 | Priority: high | Verify: drop the socket mid-session and assert status becomes DISCONNECTED, sm_state is captured and re-applied on reconnect, no crash, and chat E2EE flags (is_omemo/is_ox/pgp_send) are unchanged after resume; confirm iq_autoping_check/reconnect paths null-check the connection (cf. 15dfc2bd).

REQ-RES-04

Sanitize outgoing message content to prevent self-inflicted stanza corruption

Outgoing chat, MUC, and private message bodies SHALL pass through str_xml_sanitize() before stanza construction, ensuring user/plugin-supplied content cannot inject control characters or invalid XML that would corrupt the stream or cause a server/peer disconnect.

NIST: PR.IR-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: test that messages with control/invalid-XML characters are sanitized before stanza construction and the stanza serializes to well-formed XML; static check that every outbound path (cl_ev_send_msg_correct, cl_ev_send_muc_msg_corrected, cl_ev_send_priv_msg) calls str_xml_sanitize on plugin/user bodies.

REQ-SDLC — Secure Development Lifecycle & Governance

Hardening parity (REQ-MEM-01), sanitizer/Valgrind CI gate (REQ-MEM-02), CWE-134 attribute audit blocking in CI (REQ-INP-04), dependency pinning/floors (REQ-SUP-01/03), and the security policy/ownership doc (REQ-VUL-01) are consolidated above.

REQ-SDLC-01

No secrets, private keys, or real credentials committed to the repo; secret-scanning in CI

The repository (working tree and pre-merge feature-branch history) SHALL contain no account passwords, OMEMO/OTR/PGP private key material, or other live secrets; tests and example configs SHALL use only placeholder/mock values, and an automated secret-scanning step (gitleaks/detect-secrets) SHALL run in CI to block credential-like content.

NIST: PR.AA-01, GV.OC-03 | ISO: A.8.4, A.5.8 | Priority: critical | Verify: add a gitleaks/detect-secrets CI job over the diff and full history; grep for PEM private-key headers (BEGIN .* PRIVATE KEY) and password=; confirm profrc.example and functional-test fixtures use only @localhost/stabber mock identities and no real server/passwords.

REQ-SDLC-02

Test/dev data isolated from real user profiles and live XMPP services

cproof tests SHALL NOT read from or write to a real user's profanity profile (~/.local/share/profanity, ~/.config/profanity) nor connect to production XMPP servers; functional tests SHALL run against the stabber mock using throwaway @localhost identities and isolated temporary XDG_DATA_HOME/XDG_CONFIG_HOME dirs created/torn down by the harness.

NIST: PR.IR-01, ID.AM-08 | ISO: A.8.31, A.8.29 | Priority: high | Verify: inspect functional-test setup/teardown for an isolated tmpdir XDG_ targeting localhost only (including PROF_FLATFILE paths); grep tests for absolute $HOME profile paths and for any non-localhost host fixture.*

REQ-SDLC-03

Mandatory peer review with merge gating and CODEOWNERS for security-critical paths

Every change SHALL merge via a pull request that passed required CI checks (build matrix, unit + functional tests, Valgrind/sanitizers) and received review approval before merge to master; changes touching src/omemo, src/otr, src/pgp/OX, src/xmpp/connection.c (TLS/SASL), authentication, src/plugins, or src/ai SHALL require review by a designated owner recorded in .github/CODEOWNERS. Security-relevant changes SHALL carry a conventional-commit fix(<scope>)/feat(<scope>) subject naming the affected module. (Merges REQ-VUL change-control.)

NIST: GV.RR-02, PR.PS-06, ID.IM-03 | ISO: A.5.8, A.8.25, A.8.32 | Priority: high | Verify: confirm branch protection on master requires PR review and passing checks; confirm commitlint runs in CI and rejects non-conventional types; add/verify a CODEOWNERS mapping the security-critical dirs and that GitHub enforces owner review; spot-check git log for fix(<scope>) security subjects.

REQ-SDLC-04

Enforced secure-coding style and static safety conventions (blocking)

cproof source SHALL conform to the project's CI-enforced conventions — clang-format (.clang-format), correct use/initialization of the GLib auto_* cleanup types, and codespell cleanliness — and the style/auto-type checks SHALL be blocking (not continue-on-error) as a precondition for merge.

NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.28 | Priority: medium | Verify: run make doublecheck (format + spell) and require pass; run the auto-type count/init grep checks from the workflow and require zero violations; remove continue-on-error from the format/auto-type steps so they block merges.

REQ-SDLC-05

Fuzz harness for highest-risk untrusted-input parsers

cproof SHALL maintain at least one fuzz harness (libFuzzer/AFL++, built under sanitizers) exercising stanza/XML handling, JID parsing, and inbound encrypted-message decode (OMEMO/PGP/OX), runnable from the repo with an in-tree seed corpus and exercised periodically in a scheduled CI job that fails on new crashes.

NIST: PR.PS-06, ID.RA-01 | ISO: A.8.29, A.8.25 | Priority: medium | Verify: confirm a fuzz target exists and builds with -fsanitize=fuzzer,address over stanza and JID parse entry points; run it for a bounded time in scheduled CI and require no new crashes; track the seed corpus in-repo.

REQ-SDLC-06

Documented security requirements and threat assumptions for crypto/plugin/AI surfaces

cproof SHALL document its security requirements and trust assumptions for security-sensitive features — E2EE invariants (no plaintext fallback, fingerprint/trust handling), TLS/cert-validation policy, the privilege/trust model for C/Python plugins, and the AI module's data-egress behavior (which providers receive content, under what consent) — and SHALL map each documented invariant to an enforcing unit/functional test or code guard (this catalog plus its REQ-IDs serve as that baseline).

NIST: GV.PO-01, PR.DS-02, GV.SC-04 | ISO: A.5.8, A.8.25 | Priority: medium | Verify: confirm a documented requirements/threat-model artifact covers E2EE no-plaintext-fallback, TLS policy, plugin trust boundary, and AI egress/consent; map each invariant to a test (e.g. tests/unittests/command/test_cmd_pgp.c, test_cmd_otr.c) or code guard referencing the relevant REQ-ID.

Traceability summary

NIST CSF 2.0 → Requirements

  • GV (Govern) — GV.PO-01: REQ-DAR-07, REQ-EXT-02, REQ-SDLC-06. GV.RR-02: REQ-SDLC-03, REQ-VUL-01. GV.OC-02/03: REQ-VUL-01, REQ-SDLC-01. GV.SC-04/05/06/07/08: REQ-SUP-01, REQ-SUP-02, REQ-SUP-03, REQ-SUP-04, REQ-SUP-05, REQ-SUP-06, REQ-SDLC-06.
  • ID (Identify) — ID.RA-01/08/09: REQ-CRY-11(→SUP-03), REQ-SUP-03, REQ-SUP-04, REQ-VUL-02, REQ-EXT-03, REQ-EXT-04, REQ-SDLC-05, REQ-MEM-02. ID.IM-01/02/03/04: REQ-VUL-01, REQ-VUL-04, REQ-SUP-04, REQ-SDLC-03. ID.AM-08: REQ-SDLC-02, REQ-SUP-01.
  • PR (Protect) — PR.AA-01/03/04/05: REQ-CRY-04, REQ-CRY-09, REQ-CRY-10, REQ-AUTH-01..06, REQ-CFG-01, REQ-VUL-03, REQ-SDLC-01. PR.DS-01/02/10: REQ-CRY-01..10, REQ-AUTH-01/06, REQ-DAR-01..07, REQ-INP-01/03/05/08/10, REQ-CFG-01/02, REQ-EXT-01/02. PR.PS-01/02/04/05/06: REQ-MEM-01..09, REQ-INP-03/04/06/07, REQ-LOG-01..06, REQ-SUP-03/05, REQ-EXT-04, REQ-SDLC-03/04/05. PR.IR-01/03/04: REQ-CRY-01/02, REQ-INP-09, REQ-RES-01..04, REQ-EXT-03, REQ-LOG-05, REQ-SDLC-02.
  • DE (Detect) — DE.AE-02: REQ-LOG-01, REQ-LOG-02, REQ-LOG-03. DE.CM-01/06/09: REQ-MEM-02, REQ-MEM-08, REQ-LOG-01, REQ-LOG-02, REQ-LOG-06, REQ-CRY-04, REQ-CFG-02, REQ-INP-09.
  • RS / RC (Respond / Recover) — RS.MA-01: REQ-VUL-01. RC.RP-01/05: REQ-RES-01, REQ-RES-02, REQ-RES-03, REQ-VUL-04.

ISO/IEC 27001:2022 Annex A clusters → Requirements

  • A.5 (Organizational) — A.5.5/5.26 (incident/vuln reporting): REQ-VUL-01, REQ-VUL-04. A.5.7 (threat intel): REQ-SUP-04. A.5.8 (project security): REQ-SDLC-01/03/06, REQ-VUL-01. A.5.14 (info transfer): REQ-EXT-01/02, REQ-CFG-01. A.5.17 (auth info): REQ-AUTH-01..06, REQ-CRY-09/10. A.5.19/5.20/5.21/5.23 (supplier/ICT supply chain): REQ-SUP-01..06. A.5.29 (continuity): REQ-RES-03. A.5.31 (legal/identity binding): REQ-CRY-09. A.5.34 (PII/minimization): REQ-DAR-07. A.5.10 (acceptable use of info): REQ-CRY-02.
  • A.8 (Technological) — A.8.2/8.3 (access/privileged access): REQ-AUTH-01/02/05, REQ-EXT-04. A.8.4 (source code/secrets): REQ-SDLC-01. A.8.5 (secure auth): REQ-AUTH-03. A.8.6 (capacity): REQ-LOG-05, REQ-EXT-03. A.8.8 (technical vuln mgmt): REQ-SUP-03/04, REQ-VUL-01..04. A.8.9 (config mgmt/secure config): REQ-CRY-03/04/05/10, REQ-AUTH-03, REQ-CFG-01/02. A.8.10/8.11/8.12 (data masking/leakage/storage): REQ-DAR-01..07, REQ-AUTH-01. A.8.13 (backup/integrity): REQ-RES-01/02. A.8.15/8.16 (logging/monitoring): REQ-LOG-01..06, REQ-DAR-03. A.8.19 (software on operational systems): REQ-EXT-04. A.8.20/8.21 (network/network services security): REQ-CRY-03/04, REQ-AUTH-03. A.8.24 (cryptography): REQ-CRY-01..10, REQ-AUTH-06, REQ-EXT-01, REQ-CFG-01. A.8.25/8.27/8.28 (secure SDLC/architecture/secure coding): REQ-MEM-01..09, REQ-INP-01..10, REQ-SUP-05, REQ-SDLC-03..06, REQ-RES-01/04. A.8.26 (application security requirements): REQ-INP-01..10, REQ-EXT-02, REQ-RES-04. A.8.29 (security testing): REQ-MEM-02, REQ-INP-04, REQ-SDLC-02/04/05. A.8.30 (outsourced/third-party dev): REQ-SUP-01/02/06. A.8.31 (env separation): REQ-SDLC-02. A.8.32 (change management): REQ-SDLC-03, REQ-VUL change-control.

Out of scope

The following ISO/CSF control areas are intentionally excluded as not meaningful for a single-user desktop chat client with no central server or organizational IT estate:

  • Physical and environmental security (A.7.* — secure areas, equipment siting, cabling, physical entry, clear-desk physical aspects).
  • Human-resources security (A.6.* — screening, terms of employment, disciplinary process, awareness-training programs as an organizational HR function).
  • Datacenter / server-room operations, redundant power/cooling/utilities, and physical media handling/disposal at facility scale.
  • Organizational asset inventory of an IT estate, enterprise endpoint/MDM fleet management, network perimeter/firewall/IDS operations, and centralized IAM/directory services.
  • Business-continuity/DR planning at the organizational level, insurance, and supplier-contract/SLA legal management beyond the source-dependency pinning covered in REQ-SUP.
  • Server-side XMPP service hardening (the client connects to, but does not operate, an XMPP server).
# Security Compliance — NIST CSF 2.0 & ISO/IEC 27001:2022 (cproof) This document combines two artifacts for the cproof project: a software security **requirements catalog** (stable `REQ-*` IDs traced to NIST CSF 2.0 and ISO/IEC 27001:2022 Annex A controls) and a **static source-code compliance audit** assessed against that catalog. The audit target is the `master` branch at HEAD `622054fc6`, which includes the merged PGP plaintext-fallback fix. The method was static source analysis performed by 12 domain auditors, with every consequential finding adversarially re-verified against the working tree. Part 1 presents the audit; Part 2 presents the full catalog. Every `REQ-XXX-NN` cited in Part 1 links to its catalog entry in Part 2. ## References - NIST Cybersecurity Framework (CSF) 2.0 — overview: <https://www.nist.gov/cyberframework> - NIST CSF 2.0 (NIST CSWP 29, full text): <https://doi.org/10.6028/NIST.CSWP.29> - ISO/IEC 27001:2022 — Information security management systems: <https://www.iso.org/standard/27001> - ISO/IEC 27002:2022 — Information security controls (Annex A control set): <https://www.iso.org/standard/75652.html> ## Table of contents - [References](#references) - [Part 1 — Compliance audit](#part-1--compliance-audit) - [Executive summary](#executive-summary) - [Compliance scorecard](#compliance-scorecard) - [Critical & high-priority gaps (ranked)](#critical--high-priority-gaps-ranked) - [Partial / medium findings (grouped)](#partial--medium-findings-grouped) - [What cproof does well (notable Met items)](#what-cproof-does-well-notable-met-items) - [Verifier disagreements](#verifier-disagreements) - [Methodology & limitations](#methodology--limitations) - [Part 2 — Requirements catalog](#part-2--requirements-catalog) - [Scope and introduction](#scope-and-introduction) - [How to read this](#how-to-read-this) - [REQ-CRY — Cryptography & Confidentiality](#req-cry--cryptography--confidentiality) - [REQ-AUTH — Authentication, Access Control & Secret Management](#req-auth--authentication-access-control--secret-management) - [REQ-INP — Input Validation & Untrusted-Data Handling](#req-inp--input-validation--untrusted-data-handling) - [REQ-MEM — Memory Safety & Secure Coding](#req-mem--memory-safety--secure-coding) - [REQ-DAR — Data Protection at Rest & Privacy](#req-dar--data-protection-at-rest--privacy) - [REQ-LOG — Logging, Error Handling & Detection](#req-log--logging-error-handling--detection) - [REQ-CFG — Secure Configuration & Secure Defaults](#req-cfg--secure-configuration--secure-defaults) - [REQ-EXT — Extension & External-Interface Security (Plugins, AI, File/URL/Command Surfaces)](#req-ext--extension--external-interface-security-plugins-ai-fileurlcommand-surfaces) - [REQ-SUP — Supply Chain & Third-Party Dependencies](#req-sup--supply-chain--third-party-dependencies) - [REQ-VUL — Vulnerability & Patch Management, Change Control](#req-vul--vulnerability--patch-management-change-control) - [REQ-RES — Resilience, Robustness & Fail-Safe Behavior](#req-res--resilience-robustness--fail-safe-behavior) - [REQ-SDLC — Secure Development Lifecycle & Governance](#req-sdlc--secure-development-lifecycle--governance) - [Traceability summary](#traceability-summary) - [Out of scope](#out-of-scope) ## Part 1 — Compliance audit > Scope: the audit target is cproof `master`, HEAD `622054fc6` (includes the merged PGP plaintext-fallback fix), assessed against the catalog in Part 2. Method: static source analysis by 12 domain auditors, with every consequential finding adversarially re-verified against the working tree. The two critical-path findings were confirmed verbatim against the tree: the MUC OMEMO caller (`client_events.c:200-203`) unconditionally logs/echoes on a NULL id, and the aesgcm download path runs the external-command branch even after a failed decrypt with no removal of the plaintext output file. ### Executive summary cproof's core E2EE *wire* guarantees are sound: after commit `622054fc6` no cleartext `<body>` reaches the network on any PGP, OX, or OMEMO encryption failure ([REQ-CRY-01](#req-cry-01)), TLS is mandatory by default and certificate validation fails closed with explicit, persisted TOFU trust ([REQ-CRY-04](#req-cry-04), [REQ-CRY-10](#req-cry-10)), and the auto-cleanup/secure-coding conventions are real and CI-checked ([REQ-MEM-06](#req-mem-06)). The audit nevertheless surfaced several serious gaps that an E2EE chat client cannot treat as cosmetic. The single most dangerous is **[REQ-EXT-01](#req-ext-01) (Gap, critical residual)**: the AI/LLM client never sets `CURLOPT_SSL_VERIFYPEER/VERIFYHOST` and accepts `http://` provider URLs, so the API key and full conversation can be sent in cleartext or to an unverified peer. On the inbound side, **[REQ-INP-01](#req-inp-01) (Gap, high)** is a remotely-triggerable NULL-deref DoS: four receive-path handlers dereference `jid_create()` results without a NULL guard on a malformed `from` (e.g. `a@@b`), and the exact fix the requirement names was never applied. **[REQ-DAR-03](#req-dar-03) / [REQ-LOG-03](#req-log-03) (Gap, high)** leak decrypted plaintext bodies into `profanity.log` in the *default* `dblog=on` mode via four `log_*` sites in `database_sqlite.c`. **[REQ-AUTH-01](#req-auth-01) (Partial, high)** leaves the OTR private key (`keys.txt`) and the plaintext history DB (`chatlog.db`) world/group-readable (no `chmod` after creation). **[REQ-CRY-06](#req-cry-06) (Partial)** persists unauthenticated decrypted OMEMO media to the user's chosen path before the GCM tag is verified. Supply-chain posture is weak across the board: the libstrophe Meson wrap is pinned to `revision = HEAD` ([REQ-SUP-01](#req-sup-01), Gap critical), CI base images and bootstrapped deps float ([REQ-SUP-02](#req-sup-02)), and there is no dependency-CVE scanning, no secret-scanning, no fuzzing, and no `CODEOWNERS` ([REQ-SUP-04](#req-sup-04), [REQ-SDLC-01](#req-sdlc-01)/[REQ-SDLC-03](#req-sdlc-03)/[REQ-SDLC-05](#req-sdlc-05)). Build hardening is partial — autotools omits PIE/noexecstack and the Meson *release* path ships with zero hardening ([REQ-MEM-01](#req-mem-01)), and the ASan/UBSan suite is never run in CI ([REQ-MEM-02](#req-mem-02)). The recurring theme: the cryptographic *primitives and wire paths* are well-built, but the **local data-at-rest, operational-logging, build-hardening, and supply-chain/process controls** lag, and several requirement-named defects remain literally unfixed at HEAD. ### Compliance scorecard | Domain | Met | Partial | Gap | N/A | Unknown | |---|---|---|---|---|---| | [CRY](#req-cry--cryptography--confidentiality) (crypto/TLS) | 3 | 7 | 0 | 0 | 0 | | [AUTH](#req-auth--authentication-access-control--secret-management) (auth/secrets) | 2 | 4 | 0 | 0 | 0 | | [INP](#req-inp--input-validation--untrusted-data-handling) (input validation) | 4 | 4 | 2 | 0 | 0 | | [MEM](#req-mem--memory-safety--secure-coding) (memory safety) | 1 | 5 | 3 | 0 | 0 | | [DAR](#req-dar--data-protection-at-rest--privacy) (data at rest) | 6 | 0 | 1 | 0 | 0 | | [LOG](#req-log--logging-error-handling--detection) (logging) | 3 | 2 | 1 | 0 | 0 | | [CFG](#req-cfg--secure-configuration--secure-defaults) (config) | 1 | 1 | 0 | 0 | 0 | | [EXT](#req-ext--extension--external-interface-security-plugins-ai-fileurlcommand-surfaces) (external/AI/plugins) | 1 | 2 | 1 | 0 | 0 | | [SUP](#req-sup--supply-chain--third-party-dependencies) (supply chain) | 0 | 1 | 5 | 0 | 0 | | [VUL](#req-vul--vulnerability--patch-management-change-control) (vuln mgmt) | 0 | 3 | 1 | 0 | 0 | | [RES](#req-res--resilience-robustness--fail-safe-behavior) (resilience) | 3 | 1 | 0 | 0 | 0 | | [SDLC](#req-sdlc--secure-development-lifecycle--governance) | 1 | 3 | 2 | 0 | 0 | | **Overall** | **25** | **33** | **16** | **0** | **0** | > **How to read this:** *Met* = the requirement's normative SHALL clauses are satisfied in code on master (residual risk usually low). *Partial* = the core control exists but at least one mandated clause/sub-path/test is unmet. *Gap* = the control is materially absent or a requirement-named defect is unfixed. Residual risk is judged by exploitability and blast radius, not by status alone — e.g. several *Partial* items ([REQ-AUTH-01](#req-auth-01), [REQ-CRY-03](#req-cry-03)) carry **high** residual risk, while some *Gap* items ([REQ-SUP-05](#req-sup-05)) are **low**. > > Note: the overall tally counts 74 verified requirements; the findings the verifier left as `null` ([REQ-AUTH-04](#req-auth-04), [REQ-INP-10](#req-inp-10), [REQ-MEM-06](#req-mem-06), [REQ-DAR-04](#req-dar-04)/[REQ-DAR-05](#req-dar-05)/[REQ-DAR-06](#req-dar-06)/[REQ-DAR-07](#req-dar-07), [REQ-CFG-02](#req-cfg-02), [REQ-LOG-04](#req-log-04)/[REQ-LOG-05](#req-log-05), [REQ-RES-04](#req-res-04), and remaining SDLC items) retain their auditor status, all Met. ### Critical & high-priority gaps (ranked) #### 1 — REQ-EXT-01 — Enforce TLS verification & https on AI/LLM outbound — Gap (critical residual) References: [REQ-EXT-01](#req-ext-01). - **Evidence:** `src/ai/ai_client.c:811-815` and `:1596-1601` set only URL/HTTPHEADER/WRITEFUNCTION/WRITEDATA/POSTFIELDS/TIMEOUT. The provider scheme check (`src/command/cmd_funcs.c:7019`) explicitly **accepts both `http` and `https`** (`g_strcmp0(scheme,"http")==0 || …"https"…`), and `ai_provider_new` (`ai_client.c:388`) stores `api_url` verbatim. `_build_curl_headers` (`ai_client.c:795`) attaches `Authorization: Bearer <key>`; `request_url` is concatenated from `provider->api_url` (`ai_client.c:1590`). `tests/functionaltests/test_ai.c:85,95` codifies acceptance of `http://127.0.0.1:1/`. - > **Correction:** An earlier characterization ("no `CURLOPT_SSL_VERIFYPEER/VERIFYHOST` ⇒ unverified HTTPS") is **overstated**: libcurl defaults `VERIFYPEER=1`/`VERIFYHOST=2`, so HTTPS provider URLs *are* certificate-verified by default. A scheme check also exists. The real, confirmed defect is narrower but still serious: **`http://` provider URLs are accepted**, so `/ai set provider … http://…` ships the API key and the entire conversation in **cleartext over plain HTTP**. Residual risk: **high** (revised down from "critical"). - **Why it matters:** NIST PR.DS-02 / ISO A.8.24, A.5.14 (data-in-transit). Cleartext egress of an API key plus full message content to (or via) any on-path observer. - **Fix:** Reject non-`https` schemes in the provider scheme check (`cmd_funcs.c:7019`) and at request time before `curl_easy_perform`; flip `test_ai.c` to assert `http://` is rejected. Additionally set `CURLOPT_SSL_VERIFYPEER=1L`/`VERIFYHOST=2L` explicitly at both curl sites as defense-in-depth (guards against a non-default global curl config or a libcurl built with relaxed defaults). #### 2 — REQ-SUP-01 — Pin libstrophe Meson wrap to an immutable revision — Gap (high residual, critical priority) References: [REQ-SUP-01](#req-sup-01). - **Evidence:** `subprojects/libstrophe.wrap` line 3 `revision = HEAD`; no 40-hex SHA, tag, or `source_hash`. No CI lint catches it. - **Why it matters:** NIST ID.RA / GV.SC, ISO A.5.23/A.8.30. libstrophe is the XMPP parser on the untrusted-network path; any clone can silently incorporate an attacker-modified/regressed parser, and builds are non-reproducible. - **Fix:** Replace `revision = HEAD` with the vetted 40-hex commit SHA (or tag + `source_hash`); add a CI lint failing on `revision = HEAD`/bare-branch wraps. #### 3 — REQ-INP-01 / REQ-INP-09 — NULL-guard peer JIDs on receive — Gap (high) References: [REQ-INP-01](#req-inp-01), [REQ-INP-09](#req-inp-09). - **Evidence:** `jid_create()` returns NULL for NULL/empty/over-3071-char/invalid-UTF-8/double-`@` (`src/xmpp/jid.c:35-38,82-153`). Four unguarded derefs: `src/xmpp/message.c:1749-1750` (`_should_ignore_based_on_silence`, the exact function the requirement names), `src/xmpp/presence.c:397-402` (`_presence_error_handler`, `from` not even NULL-checked), `presence.c:453-454` (`_unsubscribed_handler`), `presence.c:468-470` (`_subscribed_handler`). The sibling `_subscribe_handler` (`presence.c:482-484`) *does* guard, proving these are bugs. - **Why it matters:** NIST PR.DS / ISO A.8.28. A hostile peer sending a stanza with missing/malformed `from` (e.g. `a@@b`) causes a remote NULL-deref DoS — a crash on every connection attempt from that peer. - **Fix:** After every receive-path `jid_create(from)` add `if (!from_jid) { return; }` before field access. Add a regression test driving these handlers with absent/`a@@b` `from` under ASan. #### 4 — REQ-DAR-03 / REQ-LOG-03 — Plaintext bodies in operational logs — Gap (high) References: [REQ-DAR-03](#req-dar-03), [REQ-LOG-03](#req-log-03). - **Evidence:** Default `PREF_DBLOG="on"` (`src/config/preferences.c:2815`) does not redact. `src/database_sqlite.c:694` (LMC mismatch, `message->plain`), `:717` (duplicate stanza-id, `content: %s`), `:748` (`log_debug("Writing to DB. Query: %s")` — the INSERT embeds `message->plain` at `:735`), `:760` (insert-failure, the requirement-named defect) all write decrypted bodies to `profanity.log`. Triggers are remotely influenceable (XEP-0308 LMC, server-assigned stanza-id). The libstrophe debug bridge `_xmpp_file_logger` (`src/xmpp/connection.c:1088-1107`) forwards raw SASL `<auth>/<challenge>/<response>` and XEP-0077 registration credentials with no redaction (gated by `PREF_STROPHE_VERBOSITY`, default off). - **Why it matters:** NIST PR.DS-01 / ISO A.8.10, A.8.12. Decrypted E2EE content lands in a plaintext log in the *shipped default config*, surviving all log-level filters — defeating the point of E2EE for anyone who shares logs for debugging. - **Fix:** Drop `message->plain`/`content:`/`Message:` from the format args at `database_sqlite.c:694,717,760`; do not log the full INSERT at `:748` (log row id only). Redact `<auth>/<challenge>/<response>`/`jabber:iq:register` payloads in `_xmpp_file_logger`. #### 5 — REQ-AUTH-01 — Owner-only (0600) on every secret/history file — Partial (high) References: [REQ-AUTH-01](#req-auth-01). - **Evidence:** Met for keyfiles/accounts/OMEMO/tlscerts (`prof_keyfile` `g_chmod 0600`, `src/common.c:186/202`), main log (`src/log.c:177`), export (`src/database_export.c:329`). **Unprotected:** `src/database_sqlite.c:144` `sqlite3_open(filename,…)` has **no** following `chmod` (plaintext history DB inherits umask, typically 0644); OTR `keys.txt` (`src/otr/otr.c:372`) and `fingerprints.txt` (`otr.c:113,386`) written by libotr with no `chmod`. No process-wide `umask` anywhere in the tree. - **Why it matters:** NIST PR.AC / ISO A.8.3, A.5.15. The OTR *private signing key* and the decrypted-history DB are readable by other local users on a default umask. - **Fix:** `g_chmod(filename, S_IRUSR|S_IWUSR)` after `sqlite3_open`; `g_chmod` after each `otrl_privkey_generate`/`otrl_privkey_write_fingerprints`. #### 6 — REQ-CRY-06 — Persisted unauthenticated OMEMO media plaintext — Partial (medium) References: [REQ-CRY-06](#req-cry-06). - **Evidence:** `aes256gcm_crypt_file` writes all decrypted plaintext to the output `FILE*` (`src/omemo/crypto.c:445`) *before* `gcry_cipher_checktag` (`:462`). The caller opens `outfh` directly on the user's final destination (`src/tools/aesgcm_download.c:66`) and on tag failure only prints an error (`:114-118`) — it never removes the plaintext output (`remove(tmpname)` at `:112` deletes only the ciphertext temp), and the external-command branch (`:126-140`) still runs on a failed decrypt. *(Confirmed verbatim against the tree.)* - **Why it matters:** NIST PR.DS-08 / ISO A.8.24. Violates the AEAD "no plaintext output on tag mismatch" mandate — a tampered/truncated media file yields a fully-persisted, attacker-controlled file at the user's path, possibly fed to an external opener. - **Fix:** Decrypt to a temp file, verify the tag, then atomically rename on success; on failure `remove()` the output and skip the `cmd_template` branch. #### 7 — REQ-CRY-03 — No silent STARTTLS downgrade — Partial (high) References: [REQ-CRY-03](#req-cry-03). - **Evidence:** Default (NULL/`force`) correctly sets `MANDATORY_TLS` (`src/xmpp/connection.c:185-186`). But the allowlisted `allow` policy (`src/common.c:406`) sets *neither* `MANDATORY_TLS` nor `DISABLE_TLS` (`connection.c:185-194`) → opportunistic STARTTLS, plaintext if the server omits it, with no log (flags==0 so the `:201` diagnostic never fires). In-band registration uses `xmpp_connect_raw` (`connection.c:496-515`); `_register_handle_features` secures only "if possible" (`:397-431`), so even `tls.policy=force` sends new-account credentials in cleartext when STARTTLS is absent. - **Why it matters:** NIST PR.DS-02 / ISO A.8.24. SASL/registration credentials can traverse plaintext under a documented, autocompleted policy. - **Fix:** Map `allow` to a hard "TLS-required-or-fail" floor or warn loudly; enforce TLS before in-band registration sends credentials. #### 8 — REQ-MEM-01 — Full hardening baseline, build-system parity — Partial (high) References: [REQ-MEM-01](#req-mem-01). - **Evidence:** autotools has stack-protector/FORTIFY/RELRO/`-Wformat=2` but **no** `-fPIE/-pie` and **no** `-Wl,-z,noexecstack` (`configure.ac`). `meson.build:64-71` gates stack-protector + FORTIFY behind `if is_debug`; the *release* path (`:33`, `executable()` at `:536`) gets none, and Meson has zero RELRO/now/noexecstack/PIE link args. No `checksec`/`readelf` CI gate. - **Why it matters:** NIST PR.PS / ISO A.8.31. Meson release binaries ship with no exploit mitigations; the two build systems are not at parity. - **Fix:** Add `-fPIE -pie -Wl,-z,noexecstack` (probed) to autotools; move Meson hardening out of `if is_debug` and add `b_pie=true` + relro/now/noexecstack link args; add a `checksec` CI assertion. #### 9 — REQ-MEM-02 — ASan/UBSan gate the test suite in CI — Partial (high) References: [REQ-MEM-02](#req-mem-02). - **Evidence:** `--enable-sanitizers` exists (`configure.ac:74-75,441-452`) but no CI job passes it (`ci-build.sh:193-205` defines 4 Linux configs, none with sanitizers; tree-wide grep finds no use). Valgrind *is* wired and gating (`ci-build.sh:295`). - **Why it matters:** NIST PR.PS / ISO A.8.28. The deliberate-heap-overflow / unsigned-overflow verification would not fail CI; memory-safety regressions can land. - **Fix:** Add a non-`continue-on-error` job running `./configure --enable-sanitizers && make check`. #### 10 — REQ-EXT-04 — Plugin install: https + integrity verification — Partial (high) References: [REQ-EXT-04](#req-ext-04). - **Evidence:** `cmd_plugins_install`'s `_http_based_uri_scheme` accepts both `http` and `https` (`src/command/cmd_funcs.c:7019`); plugin download reuses `http_file_get`, which sets `CURLOPT_SSL_VERIFY*=0L` when account `tls_policy=="trust"` (`src/tools/http_download.c:120-156`); the downloaded `.so/.py` is `plugins_install`'d then `dlopen`'d at full privilege with **no** signature/checksum check (`src/tools/plugin_download.c`, `src/plugins/plugins.c:198-218`). `copy_file` (`src/common.c:234-243`) doesn't strip group/world-write. Trust model undocumented. - **Why it matters:** NIST GV.SC / ISO A.5.23, A.8.25. A network MitM can substitute a malicious plugin that runs with full process privilege and access to all secret stores — RCE via plugin update. - **Fix:** Require `https` for plugin install; force `VERIFY*=1` regardless of XMPP `tls_policy`; verify a signature/checksum before install; `chmod 0600` installed files; document the full-privilege trust model. #### 11 — Other high-priority Gaps (concise) - **[REQ-SUP-02](#req-sup-02) (Gap):** CI base images use mutable tags (`ci/Dockerfile.*`), deps cloned at HEAD with no `git checkout <sha>`, Arch AUR snapshot fetched + `makepkg`'d unverified (`Dockerfile.arch:61-65`). → Digest-pin FROMs, commit-pin checkouts, `sha256sum -c` downloads. - **[REQ-SUP-03](#req-sup-03) (Partial, high):** gpgme uses unversioned `AC_CHECK_LIB([gpgme],[main])` (`configure.ac:309`); libgcrypt has no floor (`:351`); sqlite3/gcrypt/gpgme floors diverge from `meson.build`. → Versioned `PKG_CHECK_MODULES`, align floors, add a parity lint. - **[REQ-MEM-03](#req-mem-03) (Gap):** `strcpy/strcat` on peer barejids (`src/pgp/ox.c:153-156`), `sprintf` on key/nonce (`src/omemo/crypto.c:479,483`) — forbidden primitives, no CI grep. (Buffers are correctly sized, so residual is medium.) → Replace with `g_strdup_printf`/`snprintf`; add a CI grep. - **[REQ-MEM-04](#req-mem-04) (Gap):** No overflow-checked allocation anywhere; `omemo.c:1440 malloc(len*2+1)`, `http_upload.c:101-103` unchecked `size*nmemb`/realloc-overwrite, three `omemo.c:455/475/492 malloc(*length)` deref'd without NULL check. → `__builtin_mul_overflow`/`g_malloc_n`, NULL-check, save realloc to temp. - **[REQ-MEM-09](#req-mem-09) (Gap):** `alloca` sized from peer barejid length (`src/pgp/ox.c:150-151`), no `-Wvla`. → Replace with `g_strdup_printf`; add `-Werror=vla`. - **[REQ-INP-08](#req-inp-08) (Gap):** No control-char/bidi neutralization or length cap on display/log/DB sinks (`src/ui/window.c:2024,2120`); `str_xml_sanitize` is outgoing-only and skips U+202E. → Single sanitizer at display + log/DB write boundary. ### Partial / medium findings (grouped) **CRY** - [REQ-CRY-01](#req-cry-01) — MUC OMEMO caller logs/echoes plaintext on NULL id (`client_events.c:200-203`, confirmed). → Guard `if (id != NULL)` like the 1:1 path. - [REQ-CRY-02](#req-cry-02) — OTR-opportunistic first message bypasses `allow_unencrypted_message` (`src/otr/otr.c:332-339,425-428`). → Consult the gate before the tagged cleartext send. - [REQ-CRY-07](#req-cry-07) — `get_random_string` uses `g_rand_*` (Mersenne-Twister) for stanza IDs and the `profanity_instance_id` HMAC key (`src/common.c:712-717`, `connection.c:808,1123`). → Switch identifiers/HMAC seed to `gcry_create_nonce`. - [REQ-CRY-09](#req-cry-09) — `blind`/`firstusage` modes persist trust from inbound handlers (`src/omemo/omemo.c:608,698`). → Confine trust transitions to user commands. **AUTH** - [REQ-AUTH-03](#req-auth-03) — Insecure flags logged at `log_debug`, not WARN (`connection.c:201-213`). → WARN + `cons_show` on `DISABLE_TLS|LEGACY_*|TRUST_TLS`. - [REQ-AUTH-05](#req-auth-05) — `eval_password` spawned with `G_SPAWN_SEARCH_PATH` (`src/config/account.c:149`). → Require absolute path / drop the flag. - [REQ-AUTH-06](#req-auth-06) — Account/eval/PGP passphrases freed without wipe (`account.c:184-185`, `session.c:581/591`, `gpg.c:79/104/108`). → `explicit_bzero` before free. **INP** - [REQ-INP-02](#req-inp-02) — Validation correct; the prescribed boundary/forbidden/`@@`/UTF-8 test table is absent (`tests/.../test_jid.c`). → Add the table-driven suite. - [REQ-INP-05](#req-inp-05) — MAM `<result>` id not disco-gated (`message.c:1536-1571` → stored at `:1409-1410`). → Apply `_stanza_id_by_trusted` on the MAM path. - [REQ-INP-06](#req-inp-06) — `/url open|save` accept any scheme (`cmd_funcs.c:9633-9655,9678-9698`). → Whitelist http/https/aesgcm. - [REQ-INP-07](#req-inp-07) — OSX notifier uses `system()` with hand-rolled escaping (`src/ui/notifier.c:160`). → `g_spawn` argv / `g_shell_quote`. **MEM** - [REQ-MEM-05](#req-mem-05) — `omemo.c:1437 identity_public_key_len--` lacks a `>0` guard. → Reject zero-length before decrement. - [REQ-MEM-07](#req-mem-07) — `identity_key_store_destroy` doesn't NULL freed fields (`store.c:51-53`); undocumented out-param ownership. *(Double-free unreachable — `memset` at `omemo.c:307`.)* → Document ownership + add `=NULL`. - [REQ-MEM-08](#req-mem-08) — Good NULL-guarding by convention but `-Wnull-dereference` only under non-CI `--enable-hardening`. → Enable in one CI config. **LOG / DAR** - [REQ-LOG-01](#req-log-01) — SASL auth failure at INFO (`server_events.c:207`); TLS cert-fail logs nothing (`server_events.c:1135-1200`). → Promote both to WARN. - [REQ-LOG-02](#req-log-02) — TLS-disabled / handshake-fail / see-other-host / insecure-login all DEBUG-only. → WARN on all four. - [REQ-LOG-06](#req-log-06) — stderr→log bridge applies no redaction (`src/log.c:289,297`). → Redact secret markers before `log_msg`. **CFG / VUL / SDLC** - [REQ-CFG-01](#req-cfg-01) — `tls.policy=trust` disables curl verify silently on transfers, no user warning (`http_upload.c:242-244`, `http_download.c:153-155`). → Surface the insecure state per transfer. - [REQ-VUL-02](#req-vul-02) — `release_get_latest` never sets `VERIFY*`/https-only (`src/common.c:459-482`). → Set them explicitly. - [REQ-VUL-03](#req-vul-03) — `PREF_REVEAL_OS` defaults TRUE (`preferences.c:2692,2706`); no `<version>`-omit pref. → Default OFF; add version toggle. - [REQ-VUL-04](#req-vul-04) — No per-release Security/CVE changelog convention; `RELEASE_GUIDE.md` has no security step. → Add convention + review step. - [REQ-EXT-03](#req-ext-03) — No `CURLOPT_CONNECTTIMEOUT`; no cap/stall test (`ai_client.c`). → Add connect-timeout + tests. - [REQ-RES-02](#req-res-02) — `PRAGMA integrity_check` only on `/history verify`, never at DB open (`database_sqlite.c`). → Run quick_check in `_sqlite_init`. - [REQ-SDLC-03](#req-sdlc-03) — No `CODEOWNERS`; commitlint config dormant (`.commitlintrc.json` unreferenced). → Add CODEOWNERS + commitlint CI job. - [REQ-SDLC-04](#req-sdlc-04) — Style/auto-type/CWE-134/spell checks marked `continue-on-error` (`ci-code.yml:40,94`). → Remove `continue-on-error`. - [REQ-SDLC-06](#req-sdlc-06) — No invariant→test REQ-ID traceability matrix. → Add `compliance/REQ-TO-TEST.md`. ### What cproof does well (notable Met items) - **[REQ-CRY-01](#req-cry-01) — Fail-closed E2EE on the wire.** Every PGP failure branch returns NULL before `_send_message_stanza` (`message.c:491-517`); OX returns NULL before send (`:564-571`); OMEMO `keys==NULL` does `goto out` without reaching `message_send_chat_omemo` (`omemo.c:928-966`). The `622054fc6` fix is real and the 1:1 caller guards `if (id != NULL)` (`client_events.c:138/148/157`). - **[REQ-CRY-04](#req-cry-04) — TLS cert validation fails closed.** Certfail handler always registered (`connection.c:226`); blind trust only on explicit `tls.policy=trust`; `sv_ev_certfail` accepts only via prior-trust or explicit `/tls allow|always`, else `return 0` (`server_events.c:1134-1191`). - **[REQ-CRY-10](#req-cry-10) — Fail-safe OMEMO trust.** Default `manual` mode; untrusted devices get no session (`omemo.c:702-705`) and are skipped; `keys==NULL` aborts with a remediation listing and no stanza. - **[REQ-AUTH-02](#req-auth-02) — 0700 secret dirs.** All dir creation routes through `create_dir` → `g_mkdir_with_parents(name, S_IRWXU)` (`common.c:224`); no 0755 bypass anywhere. - **[REQ-INP-03](#req-inp-03) — No CWE-134 format-string sinks.** Every variadic wrapper carries `G_GNUC_PRINTF`; all untrusted classes (URLs, plugin, AI response) are `%s`-wrapped; `check-cwe134.sh` exits 0. - **[REQ-MEM-06](#req-mem-06) — Auto-cleanup macros enforced.** Nine `auto_*` macros, 1105 uses, CI asserts the set and fails on uninitialized `auto_*` locals (`ci-code.yml:48,51`). - **[REQ-DAR-01](#req-dar-01)/[REQ-DAR-02](#req-dar-02) — `off`/`redact` modes are airtight.** Both backends early-return / placeholder-substitute *before* any persistence (`database_sqlite.c:640-647`, `database_flatfile.c:363-373`). - **[REQ-EXT-02](#req-ext-02) — AI consent is opt-in.** Content reaches a provider only from a user-opened AI window; no inbound hook auto-forwards; default providers carry no key so no network call occurs; history is volatile-only. - **[REQ-RES-01](#req-res-01)/[REQ-RES-03](#req-res-03) — Migration preflight + state-preserving reconnect.** Disk-space gate + single-transaction migrations with ROLLBACK (`database_sqlite.c:252-258,783-918`); XEP-0198 SM state captured/re-applied with no E2EE-flag downgrade on reconnect; OTR torn down fail-safe. ### Verifier disagreements Several auditor "Met" verdicts were **overturned to Partial** by the adversarial verifier. These corrections are material — they prevent over-stating compliance: - **[REQ-CRY-01](#req-cry-01)** — The auditor verdict held the caller logs/echoes only when `id != NULL`. The verifier found the **MUC OMEMO** caller (`client_events.c:200-203`) unconditionally logs the plaintext to the SQLite history DB and echoes it on screen even when `omemo_on_message_send` returns NULL. This was confirmed verbatim against the tree. Net: no network plaintext, but a local DB + UX leak on the MUC failure path. - **[REQ-CRY-02](#req-cry-02)** — Auditor: the gate covers all cleartext sends. Verifier: the OTR-opportunistic first message returns TRUE from `otr_on_message_send`, short-circuiting `&& allow_unencrypted_message()`, so the force-encryption gate is never consulted for that cleartext send. - **[REQ-CRY-06](#req-cry-06)** — Auditor flagged only a benign short-tag nuance. Verifier found the decisive defect: file plaintext is persisted to the user's destination *before* tag verification, and the post-download external command still runs on decrypt failure. - **[REQ-CRY-07](#req-cry-07)** — Auditor (grep scoped to `omemo/otr/pgp`) declared CSPRNG "Met". Verifier found `g_rand_*` (named as forbidden in the requirement) used for stanza IDs and an HMAC key seed in `src/common.c`/`src/xmpp`. - **[REQ-CRY-09](#req-cry-09) / [REQ-INP-05](#req-inp-05)** — Auditor "Met"; verifier found inbound-handler trust transitions (blind/firstusage) and an un-disco-gated MAM `<result>` id, respectively. - **[REQ-MEM-07](#req-mem-07)** — Verifier *lowered* residual risk (medium→low): the "latent double-free" is unreachable because `signal_protocol_store_context_destroy` is guarded and immediately followed by `memset(&omemo_ctx, 0, …)` (`omemo.c:302-307`). In every overturned case the verifier's corrected status is authoritative; the disagreements arose because the original auditors' greps were scoped too narrowly (missing `src/common.c`, `src/xmpp`, the MUC caller, the MAM path) rather than from misreading the cited code. ### Methodology & limitations - **Scope:** Static source analysis of `master` at HEAD `622054fc6`, read directly from the working tree, requirement-driven against the catalog in Part 2. Each consequential finding was produced by a domain auditor and independently re-verified by an adversarial verifier; where they disagreed, the corrected status is reported. - **Not a pentest:** No dynamic exploitation, fuzzing, or runtime instrumentation was performed. DoS/overflow reachability is argued from code paths, not demonstrated with live inputs; exploitability assessments (e.g. the correctly-sized `alloca`/`sprintf` buffers in [REQ-MEM-03](#req-mem-03)/[REQ-MEM-09](#req-mem-09)) reflect code review, not runtime proof. - **Server-side items are Unknown by nature:** GitHub branch-protection / required-status-check enforcement ([REQ-SDLC-03](#req-sdlc-03)) cannot be verified from the tree; only the in-repo prerequisites (absent `CODEOWNERS`, dormant commitlint) are confirmed. The PGP fingerprint currency in `SECURITY.md` ([REQ-VUL-01](#req-vul-01)) is likewise unverifiable here. - **Build-config caveats:** Findings about Meson vs autotools parity ([REQ-MEM-01](#req-mem-01), [REQ-SUP-03](#req-sup-03)) are read from build files, not from inspecting produced binaries; a `checksec`/`readelf` pass on actual release artifacts would confirm the shipped mitigation set. - **No status was inflated:** Several requirement-named defects ([REQ-INP-01](#req-inp-01), [REQ-DAR-03](#req-dar-03), [REQ-MEM-03](#req-mem-03)/[REQ-MEM-09](#req-mem-09)) were confirmed *still present and unfixed* at HEAD; conversely, `null`-verdict items retain their auditor "Met" status and were not independently re-derived here. ## Part 2 — Requirements catalog ### Scope and introduction This catalog defines the software security requirements for **cproof**, a fork of the Profanity console XMPP/Jabber chat client (~90k LOC, C, autotools build). Requirements are derived from and traced to **NIST CSF 2.0** (Govern/Identify/Protect/Detect/Respond/Recover Categories and Subcategories) and **ISO/IEC 27001:2022 Annex A** controls. The target system is a **desktop, single-user end-to-end-encrypted (E2EE) chat client** offering OMEMO, OTR, OpenPGP (XEP-0027) and OX (XEP-0373), transported over XMPP/TLS via libstrophe, with a local SQLite/flat-file history store, plaintext GKeyFile account config, C/Python plugin extensibility, and an outbound AI/LLM client module (`src/ai`). The assumed threat model is: an active or passive network/server adversary (MitM, downgrade, key injection, stanza spoofing, server-side archive); a hostile peer sending attacker-controlled XML/stanzas, message bodies, MUC fields, URLs and file transfers; a malicious or compromised LLM provider or update endpoint; other local users on a shared host reading at-rest secrets; and a compromised supply chain (dependencies, CI, wrapped subprojects, plugins). In scope are the client's crypto correctness/enforcement, transport security, input validation, memory safety, data-at-rest protection, logging/detection, secure defaults, supply-chain and vulnerability management, extension/external-interface security, resilience, and the secure-development lifecycle of the repository itself. Explicitly out of scope are purely physical, HR, and datacenter/organizational-estate controls (there is no central server or IT estate); these are enumerated at the end. ### How to read this - **IDs** are stable and domain-prefixed: `REQ-<DOMAIN>-NN` (CRY cryptography, AUTH authentication/access/secrets, INP input validation, MEM memory safety, DAR data-at-rest/privacy, LOG logging/detection, CFG secure config/defaults, SUP supply chain, VUL vulnerability/patch/change-control, EXT extension/external interfaces, RES resilience/fail-safe, SDLC secure-development lifecycle/governance). IDs are intended to be cited from source-code audit findings and never renumbered. - **Priority** is one of `critical` / `high` / `medium` / `low`, reflecting blast radius for an E2EE client (a plaintext leak or MitM bypass is critical; a hardening or documentation gap is medium/low). - Each requirement carries an italic **traceability line**: `NIST | ISO | Priority | Verify`. The verification text is written to be concretely checkable against the cproof source tree. - Where one requirement satisfies a control raised by several domains, the duplicates are **merged** and the merge noted in the statement or applicability. The first appearance of a hardening/permissions/no-plaintext-log control is canonical; later domains reference it. - "SHALL" denotes a mandatory requirement; "SHOULD" a strong recommendation that may be risk-accepted with justification. ### REQ-CRY — Cryptography & Confidentiality #### REQ-CRY-01 **Fail-closed E2EE: no plaintext fallback on encryption failure** When a chat or MUC window has an active encryption mode (OMEMO, OTR, PGP XEP-0027, OX XEP-0373), cproof SHALL NOT transmit the message body in cleartext if encryption fails, the recipient key/session is missing, the sender key is missing, the crypto context cannot be created, or the crypto backend is unavailable (`HAVE_LIBGPGME`/`HAVE_OMEMO` undefined); every such path SHALL abort the send, surface a specific visible error in the relevant window, and return NULL/failure so the caller skips network send, history logging and on-screen echo. *(Merges REQ-RES-01; satisfies the same E2EE-integrity control from the Cryptography and Resilience domains.)* *NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.8.26 | Priority: critical | Verify: regression/integration tests forcing each failure path (deleted recipient key, unref'd sender key, induced gpgme error, empty OMEMO device_list, backend compiled out) assert no `<body>` cleartext stanza is emitted and the send returns NULL; grep that `p_gpg_encrypt`/OMEMO/OX encrypt failure branches never reach `message_send_chat`/`_send_message_stanza`; confirm `cl_ev_send_msg_correct` echoes/logs only when the returned id is non-NULL (guards the 622054fc6 fix).* #### REQ-CRY-02 **Mandatory force-encryption policy as last-line plaintext gate** For conversations without an active E2EE session, cproof SHALL consult `allow_unencrypted_message()` before any cleartext send; under `PREF_FORCE_ENCRYPTION` mode `block` it SHALL refuse with no per-message opt-out, under `resend-to-confirm` it SHALL require an explicit second user action, and any unrecognized/empty mode SHALL fail closed (block) rather than fail open. *(Merges REQ-RES-02.)* *NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.5.10 | Priority: critical | Verify: unit-test `allow_unencrypted_message()` returns FALSE for `block`; for `resend-to-confirm` first Enter blocks and an identical re-send proceeds; an invalid/empty mode returns FALSE; confirm the final fallthrough returns FALSE.* #### REQ-CRY-03 **TLS mandatory by default; no silent STARTTLS downgrade** For every account, cproof SHALL map an absent/empty/`force` `tls.policy` to `XMPP_CONN_FLAG_MANDATORY_TLS`, SHALL set `XMPP_CONN_FLAG_DISABLE_TLS` only when the user explicitly sets `tls.policy=disable`, and SHALL NOT proceed with SASL authentication or message traffic over a connection that failed to upgrade to TLS under any non-disabled policy. *(Merges REQ-AUTH-03 transport half, REQ-CFG-01.)* *NIST: PR.DS-02, PR.PS-01 | ISO: A.8.20, A.8.9, A.8.24 | Priority: critical | Verify: unit-test `_conn_apply_settings` flag computation in `src/xmpp/connection.c`: NULL/`force`/`trust` set MANDATORY_TLS and exclude DISABLE_TLS/LEGACY_SSL; `disable` sets DISABLE_TLS only on explicit config; integration test against a server advertising no STARTTLS aborts under default policy; confirm no auth/send occurs before TLS established.* #### REQ-CRY-04 **TLS certificate validation enforced; trust-bypass is explicit and fails closed** cproof SHALL perform full X.509 chain and hostname validation for XMPP TLS by default, SHALL set `XMPP_CONN_FLAG_TRUST_TLS` (blind trust) only when `tls.policy=trust` is explicitly chosen, SHALL always register the certfail handler, and on validation failure SHALL default to abort — accepting a cert only via explicit interactive `/tls allow` (session) or `/tls always` (persist by SHA-256 fingerprint via tlscerts/cafile), and rejecting on `/tls deny` or EOF. *(Merges REQ-CFG-02, REQ-CFG-03.)* *NIST: PR.AA-03, DE.CM-09 | ISO: A.8.21, A.8.9, A.8.24 | Priority: critical | Verify: test only literal policy `trust` yields TRUST_TLS and no default/auto-fallback does; test `_connection_certfail_cb`/`sv_ev_certfail` returns accept only for explicit allow/always and abort (0) for deny/EOF; assert `tlscerts_exists`/fingerprint-equality gate silent re-acceptance; negative test: untrusted self-signed cert under default policy must not connect.* #### REQ-CRY-05 **Per-account tls.policy validated against an allowlist on load** cproof SHALL validate each stored `tls.policy` against the known allowlist (`force`, `trust`, `disable`, `direct`/`legacy`) via `valid_tls_policy_option` on load, and SHALL discard an unrecognized value by falling back to the secure default (`force`/MANDATORY_TLS) rather than passing an unknown string to the connection layer. *NIST: PR.PS-01, PR.DS-02 | ISO: A.8.9 | Priority: high | Verify: test that loading an account with `tls.policy="bogus"` yields `tls_policy=NULL` (interpreted as force) and that `valid_tls_policy_option` rejects out-of-allowlist values.* #### REQ-CRY-06 **Approved cipher/key-length and mandatory AEAD tag verification for OMEMO** OMEMO message and file encryption SHALL use only AES in an authenticated/standardized mode at ≥128-bit strength (AES-256-GCM for payload/file; AES-128-GCM and AES-CBC-PKCS5 only where the OMEMO/Signal spec requires), SHALL reject unknown cipher/key-length selectors (switch default returns error, no silent weaker substitution), and SHALL call `gcry_cipher_checktag` on every decrypt and abort with no plaintext output on tag mismatch. *NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: review the `key_len` switch in `src/omemo/crypto.c` handles only sanctioned lengths with an erroring default (no fall-through); confirm `gcry_cipher_checktag` failure aborts decrypt; add a tampered-tag test asserting decrypt fails and emits no plaintext.* #### REQ-CRY-07 **CSPRNG for all key/nonce/IV/identifier generation** All key material, IVs/nonces, and identifiers SHALL be drawn from libgcrypt at `GCRY_VERY_STRONG_RANDOM` (`gcry_randomize`/`gcry_random_bytes_secure`); libgcrypt secure-memory init and `GCRYCTL_INITIALIZATION_FINISHED` SHALL complete (and be checked via `GCRYCTL_INITIALIZATION_FINISHED_P`) before any keygen; no predictable source (`rand()`/`random()`/`g_random_*`) SHALL be used for crypto material. *NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: confirm `omemo_random_func` uses VERY_STRONG_RANDOM; verify init-finished is set before keygen and checked; grep crypto/omemo/otr/pgp sources for `rand()/random()/g_random_` used as key/IV/nonce and assert none.* #### REQ-CRY-08 **Forward-secret ratcheting enforced; one-time prekeys consumed** OMEMO sessions SHALL use the libsignal/libomemo-c double ratchet and OTR the libotr AKE/ratchet; cproof SHALL NOT implement its own session KDF/ratchet, and consumed one-time prekeys SHALL be removed from the prekey store (`remove_pre_key`) after use so that long-term identity-key compromise does not expose past messages. *NIST: PR.DS-02 | ISO: A.8.24 | Priority: high | Verify: confirm session crypto is delegated to the libraries (no custom KDF/ratchet); verify `remove_pre_key` is wired and invoked on session establishment; review signed-prekey rotation; integration test that a replayed prekey bundle does not reproduce a usable session key.* #### REQ-CRY-09 **TOFU trust model: explicit, persisted fingerprint verification before trusted send** cproof SHALL bind peer identity to cryptographic fingerprints (OMEMO identity-key fingerprints, OTR fingerprints, PGP key IDs), distinguish untrusted/blind/verified states, require an explicit user action (`/omemo trust`, `/otr fingerprint`/SMP, `/pgp` trust) to mark a fingerprint verified, persist trust decisions, and treat a changed peer identity key as untrusted rather than auto-accepting it. Trust transitions SHALL be driven only by user commands, never by inbound-stanza handlers. *(Merges REQ-AUTH-07.)* *NIST: PR.AA-03, PR.AA-04 | ISO: A.5.31, A.5.17 | Priority: high | Verify: unit-test `is_trusted_identity` returns untrusted for unknown/changed keys; verify trust-store round-trips and a new identity key for a known JID does not auto-trust; simulate an inbound message from an unverified peer and assert identity stays untrusted until a verify command runs.* #### REQ-CRY-10 **Fail-safe OMEMO trust on send and receive** On send, cproof SHALL omit untrusted OMEMO recipient devices and SHALL abort the send (with an explanatory untrusted-fingerprint listing) when no trusted device remains, never silently encrypting to an unverified device; on receive, a message from an untrusted identity SHALL be flagged untrusted to the user rather than displayed as trusted. The default `PREF_OMEMO_TRUST_MODE` SHALL be `manual` (not `blind`); `blind`/`firstusage` SHALL surface a visible auto-trust notice. *(Merges REQ-CFG-07, REQ-RES-06.)* *NIST: PR.AA-03, PR.DS-02 | ISO: A.8.24, A.8.9, A.5.17 | Priority: critical | Verify: test `prefs_get_string(PREF_OMEMO_TRUST_MODE)` default == `manual`; test that with all recipient devices untrusted no stanza is sent and the untrusted-fingerprint guidance shows (`omemo_on_message_send` keys==NULL branch); test receiving from an untrusted identity sets the trusted flag FALSE and marks the UI; confirm blind/firstusage emit a `cons_show` notice.* ### REQ-AUTH — Authentication, Access Control & Secret Management #### REQ-AUTH-01 **Owner-only (0600) permissions for every secret/credential/trust/history file** cproof SHALL create and re-assert mode 0600 (`S_IRUSR|S_IWUSR`) on every file holding secrets or security-critical state — accounts keyfile (passwords/eval_password), OMEMO `identity.txt`/`trust.txt`/`sessions.txt`/`known_devices.txt`, OTR `keys.txt`/`fingerprints.txt`, PGP material, TLS pinned certs (tlscerts), the SQLite history DB and flat-file backend, history export files, and the main log — both on creation and on every save, regardless of pre-existing mode. Files written by third-party libraries (e.g. libotr `otrl_privkey_write_fingerprints`) SHALL get an explicit chmod afterward. *(Canonical permissions requirement; merges REQ-CRY perms, REQ-DAR-01, REQ-DAR-02, REQ-CFG-09, REQ-LOG-03.)* *NIST: PR.DS-01, PR.AA-01, PR.AA-05 | ISO: A.8.3, A.8.10, A.5.17 | Priority: critical | Verify: static — every `fopen`/`g_creat`/`g_key_file_save_to_file`/`sqlite3_open` for a secret/history path is followed by `g_chmod`/`fchmod(...,S_IRUSR|S_IWUSR)` with no `S_IRGRP`/`S_IROTH`; the SQLite `sqlite3_open` in `_sqlite_init` (`src/database_sqlite.c`) currently lacks this — close the gap. Runtime — under umask 0, init account/OMEMO/OTR/DB/export, `stat()` each and assert `(mode & 0777)==0600`. Template: `database_flatfile_verify.c` / `database_export.c` fchmod.* #### REQ-AUTH-02 **Owner-only (0700) permissions for all secret-bearing directories** cproof SHALL create every directory under the data/config root that contains secret material or loadable code (per-account OTR/OMEMO/PGP subdirs, plugins dir) with mode 0700 (`S_IRWXU`) via the central `create_dir()` helper and SHALL NOT widen them, so secret files are not enumerable or subject to TOCTOU by other local users. *NIST: PR.AA-05, PR.DS-01 | ISO: A.8.3, A.8.2 | Priority: high | Verify: confirm `create_dir()` uses `S_IRWXU` and all account/OTR/OMEMO/PGP/plugins dir creation routes through it (no raw `mkdir`/`g_mkdir` with 0755); runtime `stat()` asserts `(mode & 0777)==0700`.* #### REQ-AUTH-03 **Insecure transport/auth policies are explicit opt-in and logged** cproof SHALL default to MANDATORY_TLS for SASL credential exchange and SHALL enable `DISABLE_TLS`, `TRUST_TLS`, `LEGACY_SSL`, or `LEGACY_AUTH` only from explicit per-account `tls.policy`/`auth.policy` strings, logging a WARN-level diagnostic whenever a credential-exposing flag is active; none SHALL be on by default. *(Complements REQ-CRY-03/04; merges REQ-CFG-12.)* *NIST: PR.AA-03, PR.DS-02 | ISO: A.5.17, A.8.5, A.8.9, A.8.20 | Priority: critical | Verify: confirm in `_conn_apply_settings` the NULL/`force` branch sets MANDATORY_TLS and that DISABLE_TLS/TRUST_TLS/LEGACY_* are reachable only via explicit policy strings; test that default flags contain neither LEGACY_SSL nor LEGACY_AUTH; drive with `disable`/`legacy` and assert a warning is logged (LOG_FLAG_IF_SET path).* #### REQ-AUTH-04 **Account passwords are opt-in at rest; eval_password retrieval is the recommended path** cproof SHALL NOT write the account password to the accounts keyfile unless the user explicitly stored it, SHALL support non-persistent retrieval at connect time via `eval_password` (e.g. a system keyring), and SHALL remove the persisted password from the keyfile (`g_key_file_remove_key` + save) on `/account clear ... password`. *(Merges REQ-DAR-08 clear-on-disk half.)* *NIST: PR.DS-01, PR.AA-01 | ISO: A.5.17, A.8.3 | Priority: medium | Verify: configure an account with only `eval_password` set and assert no `password` key is written yet login works; after `/account clear password` assert the key is absent from the keyfile.* #### REQ-AUTH-05 **eval_password runs without a shell and without ambiguous PATH resolution** cproof SHALL execute the `eval_password` command via direct argv spawn (`g_shell_parse_argv` + `g_spawn_sync`, never `system()`/`popen()`/`/bin/sh -c`), SHALL treat a non-zero exit or empty output as authentication failure (never falling back to an empty password), and SHOULD avoid `G_SPAWN_SEARCH_PATH` so the helper binary is unambiguous and not hijackable via a poisoned PATH. *NIST: PR.AA-01, PR.AA-05 | ISO: A.8.2, A.5.17 | Priority: high | Verify: confirm no shell is used in `account_eval_password` and empty/failed output returns FALSE (no empty password to `xmpp_conn_set_pass`); review `G_SPAWN_SEARCH_PATH`; stub a helper returning non-zero/empty and assert FALSE and `account->password` stays NULL.* #### REQ-AUTH-06 **Secrets zeroized with a non-elidable wipe before free; key buffers protected in memory** cproof SHALL overwrite, with a compiler-non-elidable routine (`explicit_bzero`/`gcry_free` of secure memory/`signal_buffer_bzero_free`), the buffers holding account passwords, eval_password output, reconnect-saved password copies, PGP passphrases, and OMEMO/OTR/PGP private-key/plaintext-key material immediately before release, and SHALL use libgcrypt secure memory for OMEMO key operations; ordinary `free()`/`g_free()` of a secret without a preceding wipe is non-conformant. *(Merges REQ-MEM-12, REQ-DAR-08 in-memory half; OMEMO `signal_buffer_bzero_free`/`gcry_free` is the reference pattern.)* *NIST: PR.DS-01, PR.AA-01, PR.PS-01 | ISO: A.5.17, A.8.3, A.8.24 | Priority: high | Verify: grep that every `free`/`g_free` of a password/passphrase/private-key buffer (`src/config/account.c`, `src/xmpp/session.c` `saved_account.passwd`/`saved_details.passwd`, `src/pgp/gpg.c`, `src/omemo/*`) is preceded by a non-elidable wipe over its length; teardown unit test asserts key buffers are zero after `*_destroy`; optional heap-scan-after-logout test asserts the password byte pattern is absent.* ### REQ-INP — Input Validation & Untrusted-Data Handling #### REQ-INP-01 **Validate every peer-supplied JID and fail safe on parse failure** cproof SHALL parse every JID from an untrusted source (stanza `from`/`to`, MUC item jids, roster pushes, carbons/MAM `by`, vCard/avatar refs) via `jid_create()`/`jid_is_valid()` and SHALL treat a NULL return as a hard parse failure — abandoning the stanza — rather than dereferencing `Jid*` or any part. Confirmed defect to fix: `_should_ignore_based_on_silence()` in `src/xmpp/message.c` reads `from_jid->barejid` after `jid_create(from)` with no NULL guard (remote NULL-deref DoS). *NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: grep all `jid_create(` call sites and assert each guards NULL before field access; unit-test `test_jid.c` with NULL/empty/`a@@b`/non-UTF-8/over-3071-char input asserting NULL; regression test driving `_should_ignore_based_on_silence` with a stanza lacking `from` asserts no crash; run under ASan.* #### REQ-INP-02 **Enforce RFC 6122 length and character bounds in JID validation** `jid_is_valid()` SHALL reject any JID exceeding `JID_MAX_TOTAL_LEN` (3071) total or `JID_MAX_PART_LEN` (1023) per part, any localpart containing RFC 6122 forbidden chars (space `" & ' / : < > @`), more than one `@` in the bare portion, empty localpart when `@` is present, empty domainpart, or input failing `g_utf8_validate()`. *NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: table-driven `test_jid.c` covering boundary lengths (1023/1024 per part, 3071/3072 total), each forbidden char, double-`@`, missing localpart/domain, invalid UTF-8; CI fails on any mis-classification.* #### REQ-INP-03 **Never pass untrusted data as a printf/format-string argument (CWE-134)** cproof SHALL NOT pass any peer- or user-controlled string (message body/subject, nickname, JID, MUC status, URL, filename, presence status, library error text) as the format-string parameter of `cons_show*`, `win_print*`/`win_println*`, `log_*`, `g_strdup_printf`, or any printf-family/varargs function; such data SHALL appear only as a `%s` argument. Every variadic printf-like wrapper SHALL carry `G_GNUC_PRINTF`/`__attribute__((format(printf,N,M)))`. *(Merges REQ-MEM-09; data crossing the plugin/AI boundary (REQ-EXT-04) is covered here too.)* *NIST: PR.PS-06, PR.PS-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: build with `-Wformat=2 -Werror=format-security -Werror=format-nonliteral`; run `./check-cwe134.sh src` and require exit 0; grep `cons_show(`/`win_print(` whose first arg is a bare variable; fuzz/unit test feeding `%`-bearing stanza/plugin/AI content through a display sink.* #### REQ-INP-04 **Build promotes format-security diagnostics to errors** The build SHALL set `-Wformat=2` and promote format diagnostics to errors (`-Werror=format-security`, and `-Werror=format-nonliteral` where supported) for cproof translation units so any CWE-134 regression fails the build; the `check-cwe134.sh` audit SHALL run in CI as a blocking (not `continue-on-error`) step. *NIST: PR.PS-06, PR.PS-01 | ISO: A.8.28, A.8.25, A.8.29 | Priority: high | Verify: inspect `configure.ac` AM_CFLAGS for `-Werror=format-security`; confirm the CWE-134 CI step is blocking; introduce a deliberate non-literal format and confirm the build/CI fails.* #### REQ-INP-05 **Gate XEP-0359 stanza-id/origin-id trust on the asserting entity's disco support** When consuming a `<stanza-id by='…'>` or MAM `<result>` id, cproof SHALL honour it only when `by` equals the user's own bare JID or the message's authoritative archive (MUC bare JID or account domain) AND that entity advertises `urn:xmpp:sid:0` via disco; ids from any other `by` SHALL be ignored. Ownership of an outgoing message SHALL be established by verifying the HMAC in origin-id (`message_is_sent_by_us`), not by id-string match. *NIST: PR.DS-02, PR.AA-03 | ISO: A.8.26, A.8.28 | Priority: high | Verify: test a `<stanza-id by='attacker@evil'>` from a different sender is not trusted (`_stanza_id_by_trusted`); toggle `urn:xmpp:sid:0` in features and assert the gate; assert `message_is_sent_by_us` rejects a valid-length prefix with a wrong HMAC.* #### REQ-INP-06 **Validate URL scheme and pass URLs/filenames as argv, never to a shell** Before acting on a peer/user-supplied URL (`/url open`, `/url save`, OOB/file-transfer refs), cproof SHALL whitelist the scheme via `g_uri_parse_scheme()` (only `http`, `https`, `aesgcm`) and SHALL invoke any external open/download helper by building an explicit argv vector (`format_call_external_argv` + `call_external`/`g_spawn`, no shell), so URL/filename content can never be interpreted as shell metacharacters or extra args. *(Merges REQ-EXT-05 URL half.)* *NIST: PR.PS-06, PR.PS-05, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: grep `system(`/`popen(`/`sh -c`/`g_spawn_*sync` over `src/` and confirm none operate on URL input; unit-test scheme handling rejects `file:`/`javascript:`/`data:`; test a URL with spaces/quotes/`;` reaches the helper as one argv element.* #### REQ-INP-07 **No shell-string desktop notification command injection** Desktop notification invocation SHALL NOT construct a shell command string from message-derived content for `system()`; it SHALL use a libnotify/`g_spawn` argv API or rigorously `g_shell_quote` every interpolated value, treating message/sender content as untrusted data. Defect to remediate: `src/ui/notifier.c` builds a notify string and runs it via `system()`. *NIST: PR.PS-05 | ISO: A.8.26, A.8.28 | Priority: high | Verify: grep `notifier.c` for `system(`/`popen(`; confirm migration to argv `g_spawn` or full quoting; test a sender/message with quotes, `$()`, backticks, `;` spawns no extra process.* #### REQ-INP-08 **Bound and sanitize peer-controlled text before display, logging and storage** cproof SHALL treat decrypted bodies, subjects, MUC topics, presence status, nicknames and resource parts as untrusted and SHALL neutralize terminal-control and Unicode bidi-override sequences (e.g. ESC/CSI, U+202E) and enforce a sane maximum rendered length before writing to the ncurses UI, log files, or the SQLite/flat-file backend, so peer content cannot inject terminal escapes, spoof UI via bidi, or corrupt log/DB output. *NIST: PR.DS-01, PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test a body containing raw `ESC[2J`, a NUL, and U+202E asserting the stored/displayed form is sanitized/escaped; review the display sink for a control-char filter and a length cap on rendered peer strings.* #### REQ-INP-09 **Strict structural validation of incoming stanzas; cap MAM result sets** cproof SHALL guard every stanza consumer so that missing/malformed mandatory children/attributes (absent body/`from`, non-JID `from`, unexpected namespaces) cause the stanza to be skipped rather than NULL-dereferencing the result of `xmpp_stanza_get_child_by_name`/`get_attribute`, SHALL not call `abort()`/`assert()` on attacker-influenced conditions, and SHALL cap MAM results requested/processed per query (`MESSAGES_TO_RETRIEVE`). *(Merges REQ-MEM-08, REQ-RES-05.)* *NIST: PR.PS-06, PR.DS-02, PR.IR-01 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: fuzz/replay malformed stanzas (missing body/from/jid, deeply nested, huge MAM batches, oversized base64 OMEMO key) into `message.c`/`iq.c`/`presence.c`/`muc.c`/`omemo.c` handlers under ASan/UBSan asserting clean rejection and no crash; code-review NULL checks after each `xmpp_stanza_get_*` on the receive path; confirm `assert()` in receive paths is debug-only and unreachable from peer data.* #### REQ-INP-10 **Validate and escape data crossing into AI/LLM outbound requests and responses** Where `src/ai` forwards user/peer content into an outbound LLM request, it SHALL JSON-escape all such content via `ai_json_escape()` before embedding it, and SHALL parse provider responses with bounds-checked unescaping (`_json_unescape_substring`/`_extract_json_string`) so an embedded quote or control byte cannot break the JSON string context. *NIST: PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test `ai_json_escape()` with `"`, `\`, `\n` and control bytes asserting valid escaped output; test `_extract_json_string()` against a response value embedding an escaped quote; confirm every request-body assembly routes content through `ai_json_escape()`.* ### REQ-MEM — Memory Safety & Secure Coding #### REQ-MEM-01 **Full compiler/linker hardening baseline in release builds, at parity across build systems** Every released cproof binary and plugin SHALL be compiled/linked with `-fstack-protector-strong`, `-fno-common`, `-D_FORTIFY_SOURCE=2` (or 3 where supported), `-Wformat=2`, full RELRO and immediate binding (`-Wl,-z,relro -Wl,-z,now`), non-executable stack (`-Wl,-z,noexecstack`), and a position-independent executable (`-fPIE -pie`); the autotools and meson definitions SHALL stay at flag parity, and the build SHALL fail if any flag is silently dropped. Known gaps to close: configure.ac omits `-fPIE/-pie`; meson.build omits the RELRO/`-z,now` linker hardening present in autotools. *(Canonical hardening requirement; merges REQ-CRY-12, REQ-AUTH-09, REQ-CFG-10, REQ-SUP-07, REQ-EXT-10, REQ-RES-10, REQ-SDLC-01, REQ-VUL-09.)* *NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.27, A.8.28 | Priority: critical | Verify: grep `configure.ac` and `meson.build` for each flag and assert parity; on the built `./profanity` run `checksec`/`readelf -d`/`readelf -h` and assert Full RELRO, BIND_NOW, NX, PIE (Type DYN), stack canary, FORTIFY symbols present (and CET/`-fstack-clash-protection` where the toolchain supports them); add a CI step that fails when any mitigation is missing on the produced binary and loaded `.so` plugins.* #### REQ-MEM-02 **ASan + UBSan + unsigned-overflow build gates the test suite in CI** The project SHALL provide an `--enable-sanitizers` build (ASan + UBSan + `-fsanitize=unsigned-integer-overflow`, `-fno-sanitize-recover=all`) and SHALL run the full cmocka unit/functional suite under it in CI on every pull request, AND additionally run the suite under Valgrind with `--error-exitcode=1 --leak-check=full`; any sanitizer/Valgrind error or definite leak SHALL fail the build. *(Merges REQ-SDLC-02, REQ-RES-10 sanitizer half.)* *NIST: PR.PS-06, DE.CM-01, ID.RA-01 | ISO: A.8.28, A.8.29, A.8.25 | Priority: critical | Verify: confirm a non-`continue-on-error` CI job runs `./configure --enable-sanitizers && make && make check` and that `ci-build.sh` invokes Valgrind with `--error-exitcode=1`; intentionally introduce a heap overflow in a test and confirm CI fails.* #### REQ-MEM-03 **No unbounded/unsafe C string or format primitives on untrusted data** cproof SHALL NOT use `strcpy`, `strcat`, `sprintf`, `gets`, or unbounded `scanf` on data derived from stanzas, peer content, MUC, file-transfer/URL input, or remote JIDs; such code SHALL use length-bounded equivalents (`g_strdup_printf`, `g_strlcpy`, `snprintf` with checked return). Defects to remediate: `strcpy`/`strcat` on peer barejids in `src/pgp/ox.c:153-156`; `sprintf` in `src/omemo/crypto.c:479/483` and `src/xmpp/iq.c:1787`. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: `grep -rnE '\b(strcpy|strcat|sprintf|gets)\(' src/` and confirm each remaining hit operates only on fixed compile-time-constant strings; add a CI grep that fails on these calls in stanza/crypto-handling files.* #### REQ-MEM-04 **Overflow-checked allocation sizes** Every `malloc`/`realloc`/`g_malloc` size that multiplies or adds attacker-influenced lengths SHALL be computed with overflow-checked arithmetic (`__builtin_mul_overflow`/`__builtin_add_overflow` or `g_malloc_n`-style helpers) and SHALL error/abort rather than allocate a truncated buffer; `realloc` results SHALL be checked before the old pointer is overwritten. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: review multiplicative/additive `malloc`/`realloc` args in `src/tools/http_upload.c`/`http_download.c`, `src/common.c`, `src/xmpp/connection.c`, `src/omemo/omemo.c` (`identity_public_key_len*2+1`, `malloc(*length)`) for a preceding overflow check; UBSan unsigned-overflow tests feeding near-`SIZE_MAX` lengths.* #### REQ-MEM-05 **Underflow-guarded unsigned subtraction on sizes/offsets (per project unsigned policy)** cproof SHALL guard every subtraction of unsigned size/length/offset values that could go negative against wrap to `~SIZE_MAX`, using pre-checks or builtins rather than casting an operand to signed, consistent with the project's unsigned-arithmetic policy. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: build with `-fsanitize=unsigned-integer-overflow` and run tests/fuzz over UI scroll/pad math (`src/ui/inputwin.c` documented SIZE_MAX guard) and parser/crypto buffer-slicing; code-review subtractions of `size_t`/`guint` operands for a lower-bound check; confirm no `size_t` is cast to `int` to mask a wrap.* #### REQ-MEM-06 **Mandatory auto-cleanup macros for heap/FILE/fd ownership** Local owning pointers and resources SHALL use the project auto-cleanup attributes (`auto_gchar`, `auto_char`, `auto_gcharv`, `auto_guchar`, `auto_FILE`, `auto_gfd`, `auto_gerror`, `auto_jid`, `auto_sqlite`) instead of manual `free`/`g_free`/`fclose`/`close` on early-return paths, so ownership is released exactly once on every exit path. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: medium | Verify: code-review that new owning locals use `auto_*` rather than trailing frees; run `make check` under ASan/LeakSanitizer asserting no leaks/double-frees; grep changed handlers for `free()`/`g_free()` paired with early returns that should use `auto_*`.* #### REQ-MEM-07 **Null freed pointers; document out-parameter ownership; no UAF on crypto buffers** After freeing a heap pointer that remains in scope or in a struct field, cproof SHALL set it to NULL; functions returning allocated memory via out-parameters SHALL define ownership; freed crypto/session/key buffers (`gcry_free`/`free` paths in `src/omemo`, `src/otr`, `src/pgp`) SHALL NOT be referenced after release. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: ASan/UBSan `make check` over crypto and session teardown; code-review each free of a still-live pointer is followed by `=NULL`; targeted tests double-invoking teardown to confirm no double-free.* #### REQ-MEM-08 **NULL-checks on every peer/remote-derived parse/lookup before dereference** cproof SHALL validate for NULL the result of every parse/lookup that can fail (XMPP attribute/child lookups, JID parsing, base64/hex decode, GKeyFile reads, library returns) before dereferencing and SHALL handle absence as a recoverable error. *(Closely related to REQ-INP-09; this is the codebase-wide secure-coding form.)* *NIST: PR.PS-01, DE.CM-01 | ISO: A.8.28 | Priority: high | Verify: enable `-Wnull-dereference` and review warnings; run crafted-stanza/malformed-input tests under ASan asserting no NULL-deref crash; code-review each new `xmpp_stanza_get_*`/`g_key_file_get_*`/decode result for a NULL guard (cf. the autoping NULL-domain guard 15dfc2bd).* #### REQ-MEM-09 **No alloca/VLA sized from untrusted lengths** cproof SHALL NOT size stack allocations (`alloca` or VLAs) from attacker-influenced lengths; such buffers SHALL be heap-allocated with checked sizes or bounded by a validated compile-time maximum. Defects to remediate: `alloca((strlen(barejid)+6))` in `src/pgp/ox.c:150-156`; `unsigned char out[mac_len]` VLA in `src/omemo/crypto.c:107`. *NIST: PR.PS-01 | ISO: A.8.28, A.8.27 | Priority: medium | Verify: `grep -rnE '\balloca\(' src/` and review for non-constant sizes; compile with `-Wvla` and treat as error to flag VLAs; replace flagged sites with checked heap allocation + `auto_*` cleanup.* ### REQ-DAR — Data Protection at Rest & Privacy > File/directory permission and in-memory/at-rest secret-zeroization requirements for at-rest material are consolidated in **REQ-AUTH-01** (0600 files, including the SQLite DB gap), **REQ-AUTH-02** (0700 dirs), and **REQ-AUTH-06** (zeroization). The requirements below cover history-content minimization, redaction, and at-rest integrity. #### REQ-DAR-01 **Functional no-storage (`off`) mode writes no message content** When `PREF_DBLOG` is `off`, cproof SHALL NOT write any message body, sender/recipient JID-pair, or per-message timestamp derived from chat traffic to any history backend (SQLite or flat-file), for incoming, outgoing, MUC and MUC-PM messages alike. *NIST: PR.DS-01 | ISO: A.8.10 | Priority: high | Verify: set `PREF_DBLOG=off`, drive `add_incoming` + all `add_outgoing_*`, assert zero rows in SQLite ChatLogs and zero content lines in the flat-file; confirm `get_previous_chat` returns nothing; pin the early-return in both backends (`database_sqlite.c`, `database_flatfile.c`).* #### REQ-DAR-02 **Redaction (`redact`) mode never leaks plaintext into storage** When `PREF_DBLOG` is `redact`, cproof SHALL substitute the message body with a fixed placeholder (`[REDACTED]`) before it reaches any persistence call, so no original body character is passed to `sqlite3_bind` or written to the flat-file; non-content metadata may be retained. *NIST: PR.DS-01 | ISO: A.8.11 | Priority: high | Verify: with `redact`, store a message containing a unique sentinel, then grep the raw `chatlog.db` bytes and flat-file and assert the sentinel is absent while a `[REDACTED]` row/line exists; confirm redaction precedes the bind/write.* #### REQ-DAR-03 **No plaintext chat content or secrets in operational/debug logs** cproof SHALL NOT write decrypted message bodies, account passwords, eval_password output, SASL credentials, OMEMO/OTR/PGP private keys, or AI provider API keys/Bearer tokens into `profanity.log` at any level; statements involving message content SHALL log only metadata (JIDs, ids, sizes, encryption type, library error code). Defect to remediate: `src/database_sqlite.c:760` `log_error('content: %s', message->plain)` on insert failure (bypasses no-storage/redact). The libstrophe debug bridge `_xmpp_file_logger` SHALL redact SASL `<auth>/<challenge>/<response>` and XEP-0077 registration credentials before they reach `_log_msg`. *(Merges REQ-LOG-01, REQ-LOG-02, REQ-EXT-03 log half; SDLC secret-scanning is REQ-SDLC-04.)* *NIST: PR.PS-04, PR.DS-01 | ISO: A.8.12, A.8.15 | Priority: critical | Verify: grep `log_*` for interpolation of `message->plain`/`message->body`/`password`/`api_key`/`token`/`Bearer`/key bytes and assert none print the value; drive login+decrypt+AI-request with a sentinel secret and assert it never appears in the log; fix `database_sqlite.c:760`; confirm `src/ai/ai_client.c` never logs the api_key and never sets `CURLOPT_VERBOSE`; connect with XMPP debug logging and a sentinel password and assert it (and its base64) is absent.* #### REQ-DAR-04 **Atomic, permission-preserving writes for history export and keyfile rewrites** cproof SHALL write history exports and rewritten secret/history files atomically via a private (0600) temp file created with `mkstemp`/`O_EXCL` followed by `rename`, never via a predictable fixed temp path, so a concurrent or hostile local process cannot pre-create, read, or race the file. *NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: review `src/database_export.c` (mkstemp+fchmod 0600+rename) and any flat-file rewrite/compaction for the same pattern with no fixed-name fallback; test pre-creating the legacy predictable tmp name as a symlink and assert the export refuses or writes through a fresh mkstemp 0600 file.* #### REQ-DAR-05 **Encryption-state and metadata accuracy in stored records** cproof SHALL persist the correct encryption type (`none`/`omemo`/`otr`/`pgp`/`ox`) for every stored message row matching the actual delivery path, and SHALL NOT record a decryption-failure message as `encryption='none'` plaintext or store raw ciphertext as the `message` body. *NIST: PR.DS-01 | ISO: A.8.11 | Priority: medium | Verify: tests asserting an OMEMO/OTR/PGP/OX message round-trips with the matching `encryption` value (`_get_message_enc_type`), and a decryption-failure message is either not stored or stored with an explicit non-`none` marker rather than its ciphertext under `none`.* #### REQ-DAR-06 **Clear-on-close history minimization honored** When `PREF_CLEAR_PERSIST_HISTORY` is disabled, cproof SHALL discard in-window scrollback on window close without re-persisting it beyond the configured backend, and `/clear` behavior SHALL be consistent with the `PREF_DBLOG` retention setting. *NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: toggle `PREF_CLEAR_PERSIST_HISTORY` and assert closing a chat window with it disabled produces no new persisted rows; confirm `/clear` with persist off does not write back buffer content (`src/ui/window.c`, `/clear` handler).* #### REQ-DAR-07 **Keep plaintext per-day chatlog subsystem disabled; document retention defaults** The legacy plaintext per-day chat-log writers (`chatlog.c` entry points) SHALL remain no-ops and SHALL NOT be re-enabled except behind an explicit, off-by-default preference that documents it stores decrypted content unencrypted; the flat-file/SQLite backends SHALL be the only message sinks, and `PREF_DBLOG` values (`off`/`redact`/`flatfile`/`store`) and their privacy implications SHALL be documented. *(Merges REQ-LOG-09, REQ-DAR retention.)* *NIST: PR.DS-01, PR.PS-04, GV.PO-01 | ISO: A.8.12, A.5.34 | Priority: low | Verify: assert `chatlog.c` functions remain no-ops (no `fopen`/write) via test/grep; send/receive an encrypted message and assert no `chatlogs/` plaintext file under `XDG_DATA_HOME`; confirm `PREF_DBLOG` documented; require any new logging pref to default false.* ### REQ-LOG — Logging, Error Handling & Detection > Log-file 0600 permission is in **REQ-AUTH-01**; no-secrets-in-logs is in **REQ-DAR-03**. #### REQ-LOG-01 **Security-relevant events logged at WARN or higher** cproof SHALL record at `PROF_LEVEL_WARN`/`ERROR` (so they survive the default WARN filter): TLS certificate validation failure, OMEMO/OTR/PGP/OX decryption or MAC failure, use/rejection of an untrusted/unknown device fingerprint, and SASL authentication failure. *NIST: DE.CM-06, DE.AE-02 | ISO: A.8.15, A.8.16 | Priority: high | Verify: grep that cert-fail (`_connection_certfail_cb`/`sv_ev_certfail`), decrypt-fail (`src/omemo`, `src/pgp/ox.c`), untrusted-device and SASL-failure paths call `log_warning`/`log_error` (not `log_debug`); functional test injecting a bad cert and a corrupt OMEMO ciphertext asserts a WARN/ERROR line at the default filter.* #### REQ-LOG-02 **Detect and log TLS/security downgrade conditions** cproof SHALL log a WARN-or-higher event whenever a connection is established/negotiated below the expected posture: TLS negotiation failed, connecting without mandatory TLS, an unencrypted (`DISABLE_TLS`) connection, or a followed see-other-host/stream redirect — so silent transport downgrades are detectable. *NIST: DE.AE-02, DE.CM-06 | ISO: A.8.16, A.8.15 | Priority: high | Verify: inspect `src/xmpp/connection.c` that DISABLE_TLS/non-mandatory/TLS-failed/see-other-host paths emit `log_warning`; test connecting with TLS disabled asserts a WARN line.* #### REQ-LOG-03 **Safe, non-leaking error messages on crypto/parse failures** Error/warning messages on crypto-operation failure, stanza/XML parse failure, or DB/keyfile load failure SHALL contain only a failure category and a stable library error code/string, and SHALL NOT embed raw ciphertext, key bytes, full untrusted stanza payloads, or secret-derived values. *(Reinforces REQ-DAR-03.)* *NIST: PR.PS-04, DE.AE-02 | ISO: A.8.15, A.8.28 | Priority: medium | Verify: review crypto-failure `log_error` sites (`src/omemo/omemo.c`, `src/pgp/ox.c` gpgme_strerror, stanza handlers) to confirm they pass strerror codes/identifiers, not buffers/key material.* #### REQ-LOG-04 **Log level user-configurable, defaulting to non-debug** cproof SHALL expose a configurable filter (DEBUG/INFO/WARN/ERROR) honored by `_should_log()`, default to a non-DEBUG level, map unknown level strings to WARN, and require explicit user action to enable DEBUG (which may capture additional protocol detail). *NIST: PR.PS-04, ID.RA-01 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: unit-test `_should_log` across levels confirming below-filter records drop; confirm `log_level_from_string` defaults unknown to WARN and DEBUG is not the startup default.* #### REQ-LOG-05 **Bounded log growth via size-based rotation** When rotation is enabled, the logger SHALL rotate the main log once it reaches `max_log_size`, SHALL bound the number of rotated files, and rotated files SHALL inherit 0600, preventing unbounded disk consumption while preserving a finite reviewable history. *NIST: PR.PS-04, PR.IR-04 | ISO: A.8.15, A.8.6 | Priority: medium | Verify: test that writing past `max_log_size` triggers `_rotate_log_file()`, produces a bounded-index `profanity.NNN.log` (also 0600), reopens the main file; confirm user-provided log files are excluded from auto-rotation.* #### REQ-LOG-06 **stderr capture fails safe and respects sensitive-data constraints** The stderr-capture facility (`log_stderr_init`/`log_stderr_handler`) that redirects process stderr into the log SHALL handle init/read errors (including EINTR/partial reads) without crashing or losing the fd, and SHALL be subject to the same no-secrets constraint so crypto-library diagnostic output to stderr (gcrypt/gpgme/libsignal) does not persist key/passphrase material. *NIST: PR.PS-04, DE.CM-06 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: review error paths in `log_stderr_init` for correct fd cleanup and non-fatal failure; write a sentinel secret to stderr and assert redaction/handling; verify the handler tolerates EINTR/partial reads without data loss or crash.* ### REQ-CFG — Secure Configuration & Secure Defaults > TLS-mandatory default (REQ-CRY-03), cert-validation/TOFU defaults (REQ-CRY-04), tls.policy allowlist (REQ-CRY-05), OMEMO manual trust default (REQ-CRY-10), legacy-flag gating (REQ-AUTH-03), and toolchain hardening (REQ-MEM-01) are consolidated above. The CFG-specific defaults remain: #### REQ-CFG-01 **HTTP(S) file transfer must not silently disable TLS verification** cproof SHALL keep `CURLOPT_SSL_VERIFYPEER` and `CURLOPT_SSL_VERIFYHOST` enabled for XEP-0363 upload and HTTP download by default, and SHALL disable them ONLY as an explicit consequence of the operator-selected account `tls.policy=trust` (or an explicit `PREF_TLS_CERTPATH=none`), never from any other condition or default; that insecure state SHALL be surfaced to the user, and downloaded files SHALL be written canonicalized within the configured downloads directory (rejecting peer-supplied path traversal). *(Merges REQ-EXT-08.)* *NIST: PR.AA-03, PR.DS-02, PR.DS-10 | ISO: A.8.9, A.8.24, A.5.14 | Priority: high | Verify: confirm in `src/tools/http_upload.c`/`http_download.c` the `insecure` boolean derives solely from `tls_policy=="trust"`/explicit none, and that with `force`/NULL the handles retain VERIFYPEER=1/VERIFYHOST=2; grep for any unconditional `SSL_VERIFY*=0`; test a peer filename containing `../` stays inside the downloads dir.* #### REQ-CFG-02 **Encryption-loss warning and TLS indicator on by default** cproof SHALL default `PREF_ENC_WARN` and `PREF_TLS_SHOW` to enabled so the user is warned before sending unencrypted and is shown transport security state out of the box. *NIST: PR.PS-01, DE.CM-09 | ISO: A.8.9 | Priority: medium | Verify: unit-test `prefs_get_boolean(PREF_ENC_WARN)` and `prefs_get_boolean(PREF_TLS_SHOW)` both return TRUE on a fresh config; review neither falls through to the FALSE branch.* ### REQ-EXT — Extension & External-Interface Security (Plugins, AI, File/URL/Command Surfaces) > URL/shell handling is REQ-INP-06; notification injection REQ-INP-07; AI JSON escaping REQ-INP-10; AI key-in-log/at-rest REQ-DAR-03/REQ-AUTH-01; file-transfer TLS/traversal REQ-CFG-01; hardening REQ-MEM-01. #### REQ-EXT-01 **Enforce TLS peer/host verification and https scheme on AI LLM outbound requests** The AI client SHALL set `CURLOPT_SSL_VERIFYPEER=1` and `CURLOPT_SSL_VERIFYHOST=2` on every outbound LLM request, SHALL require the provider `api_url` scheme to be `https` (rejecting `http://` at provider-add and request time), and SHALL NOT expose any per-provider option to disable verification, so the API key and conversation content are never sent in cleartext or to an unverified peer. *(Merges REQ-CFG-06.)* *NIST: PR.DS-02, PR.PS-01 | ISO: A.5.14, A.8.24, A.8.27 | Priority: critical | Verify: grep `src/ai/ai_client.c` (`_curl_exec_and_handle`/`_ai_request_thread`) for VERIFYPEER/VERIFYHOST set non-zero on both curl sites and never to 0L; add a test that `ai_add_provider` rejects an `http://` api_url.* > **Correction:** The audit's [REQ-EXT-01](#req-ext-01) finding refines this requirement's premise. libcurl defaults `CURLOPT_SSL_VERIFYPEER=1`/`VERIFYHOST=2`, so HTTPS provider URLs are certificate-verified by default, and a provider scheme check already exists (`src/command/cmd_funcs.c:7019`). The confirmed, narrower defect is that the scheme check **accepts both `http` and `https`**, so an `http://` provider URL ships the API key and full conversation in cleartext. The requirement's normative remediation (reject non-`https`, set VERIFYPEER/VERIFYHOST explicitly as defense-in-depth) stands; only the residual-risk rating moves from "critical" to "high". #### REQ-EXT-02 **Explicit opt-in consent before transmitting conversation content to LLM providers** The AI client SHALL transmit message/conversation content to an external LLM provider only after the user has explicitly enabled the feature and configured a provider, SHALL be disabled by default, and SHALL NOT auto-forward received XMPP chat/MUC messages to any provider without an explicit per-action user command (no wiring to inbound-message plugin/stanza hooks). AI prompt/response content SHALL NOT be ingested into the XMPP chat history DB/chatlogs and SHALL live only in volatile per-session memory unless the user opts into persistence. *(Merges REQ-EXT-09.)* *NIST: PR.DS-01, GV.PO-01 | ISO: A.5.14, A.8.24, A.8.26 | Priority: critical | Verify: confirm the AI feature is gated behind a default-off preference and explicit `/ai` command; verify no `prof_pre/post` chat/room hook or stanza-receive path calls `ai_send_prompt`; test that with no provider configured `ai_send_prompt` performs no network call; confirm no code path passes AI session content into `chatlog_`/`database_` APIs and history is freed on `ai_session_unref`.* #### REQ-EXT-03 **Bounded, time-limited AI calls with response-size cap; bounded plugin/AI buffering** Every AI curl handle SHALL set a finite `CURLOPT_TIMEOUT` (and ideally `CURLOPT_CONNECTTIMEOUT`), and `_write_callback` SHALL cap accumulated response size (current 10 MB) and abort the transfer when exceeded, so a slow/hung/malicious endpoint cannot hang the UI thread or exhaust memory. *NIST: PR.IR-03, ID.RA-09 | ISO: A.8.6, A.8.16 | Priority: high | Verify: review every AI curl handle sets `CURLOPT_TIMEOUT` and that `_write_callback` returns short to abort at the cap; test with a mock server that streams past the cap and one that stalls, asserting bounded abort.* #### REQ-EXT-04 **Plugins load only from the protected 0700 plugins directory; run with full privilege; downloads verified** C plugins (`dlopen` with `RTLD_NOW|RTLD_GLOBAL`) and Python plugins SHALL be loaded only from the 0700 per-user plugins directory under the data path (`files_get_data_path(DIR_PLUGINS)`, no user-supplied absolute/CWD/relative/shared path), SHALL NOT be auto-loaded without explicit user opt-in, SHALL be stored without world/group-write bits, and any `/plugins install` download SHALL require an https source with TLS verification. The full-privilege trust model (plugins access all secret stores) SHALL be documented. *(Merges REQ-AUTH-08, REQ-SUP-09 plugin half.)* *NIST: PR.AA-05, PR.PS-01, ID.RA-09 | ISO: A.8.19, A.8.25, A.8.2 | Priority: high | Verify: confirm the load dir derives only from `files_get_data_path(DIR_PLUGINS)` and is created `S_IRWXU`; confirm no implicit auto-load of untrusted paths; verify `plugin_download.c` enforces https + TLS verify (no `SSL_VERIFY 0L`); check installed plugin files lack world/group write; document the plugin trust model in user docs.* ### REQ-SUP — Supply Chain & Third-Party Dependencies #### REQ-SUP-01 **Pin the libstrophe Meson subproject to an immutable revision/checksum** `subprojects/libstrophe.wrap` SHALL pin libstrophe to an exact immutable revision (40-hex commit SHA or release tag plus `source_hash`/wrapdb checksum) and SHALL NOT use `revision = HEAD` or a bare branch, so a build cannot silently incorporate an attacker-modified or regressed XMPP parser. *(Merges REQ-VUL-03, REQ-SDLC-07 wrap half; libstrophe sits on the untrusted network path.)* *NIST: GV.SC-05, ID.RA-09, PR.PS-01 | ISO: A.5.21, A.5.23, A.8.8, A.8.28, A.8.30 | Priority: critical | Verify: grep the `.wrap`: assert no `revision = HEAD`/bare branch and presence of a 40-hex `revision` or `source_hash`; add a CI lint failing on unpinned wrap revisions; confirm two clean builds at the pinned revision produce identical libstrophe object hashes.* #### REQ-SUP-02 **Pin CI base images by digest and bootstrapped deps by commit; verify download provenance** All `ci/Dockerfile.*` base images SHALL be pinned by immutable digest (`FROM image@sha256:…`, not `debian:testing`/`archlinux:latest`), and all source dependencies cloned/downloaded during image build (libstrophe, stabber, AUR `libstrophe-git`) SHALL be checked out at a pinned commit and obtained over authenticated HTTPS with integrity verification (commit/checksum/signature), not `--depth 1` HEAD or unverified `wget`+`makepkg`. *(Merges REQ-SUP-08, REQ-VUL-10.)* *NIST: GV.SC-06, GV.SC-07, GV.SC-05 | ISO: A.5.20, A.5.21, A.5.23, A.8.30 | Priority: critical | Verify: grep Dockerfiles for `FROM .*:latest`/`:testing`, `clone --depth 1` without a following `git checkout <sha>`, and `wget`/`curl` downloads lacking a subsequent `sha256sum`/`gpg` check; assert each `FROM` contains `@sha256:`; confirm `makepkg` integrity checks are not bypassed.* #### REQ-SUP-03 **Enforce minimum, CVE-informed versions for all security-relevant dependencies (build fails below floor)** The build SHALL fail (`AC_MSG_ERROR`, not notice/warn) when any security-relevant dependency is below a documented CVE-informed minimum: libstrophe, libcurl, sqlite3, glib, gpgme, libotr, libsignal-protocol-c/libomemo-c, libgcrypt. gpgme in particular SHALL be checked via versioned `PKG_CHECK_MODULES([gpgme],[gpgme >= X.Y])`, replacing the unversioned `AC_CHECK_LIB([gpgme],[main])`. The declared minimums SHALL be identical between `configure.ac` and `meson.build` (current divergences: sqlite3 3.22.0 vs 3.35.0; gpgme unversioned), each floor documented with security rationale. *(Merges REQ-CRY-11, REQ-SUP-03, REQ-SUP-04, REQ-VUL-02.)* *NIST: ID.RA-09, GV.SC-04, PR.PS-02 | ISO: A.5.21, A.8.8, A.8.28 | Priority: critical | Verify: grep both files to confirm a version-bearing `PKG_CHECK_MODULES` (no bare `AC_CHECK_LIB([gpgme],[main])`); a script diffs the `(name,minversion)` tuples and fails on mismatch; configuring against a stubbed under-floor `.pc` aborts configure non-zero; confirm `gcry_check_version(GCRYPT_VERSION)` gates init.* #### REQ-SUP-04 **Continuous dependency-CVE scanning in CI** CI SHALL run an automated known-vulnerability scanner (OSV-Scanner/Dependabot/grype/Trivy) over the third-party set (libstrophe, gpgme, libsignal/libomemo-c, openssl/gnutls, libgcrypt, libotr, sqlite, glib, libcurl) on each push and on a recurring schedule, with results triaged and surfaced; a seeded known-vulnerable pinned version SHALL trigger an alert. *(Merges REQ-VUL-04.)* *NIST: ID.RA-01, GV.SC-08, ID.IM-02 | ISO: A.5.7, A.8.8 | Priority: high | Verify: confirm a scan job exists in `.github/workflows` (or `dependabot.yml` for the wrap/Docker ecosystems), runs on cron, and fails/reports above a defined severity; verify a seeded vulnerable version triggers an alert; check triage records exist.* #### REQ-SUP-05 **Pin GitHub Actions to commit SHAs** All third-party GitHub Actions SHALL be pinned to a full commit SHA, not a mutable tag (`actions/checkout@<sha>` not `@v4`). *NIST: GV.SC-06, PR.PS-01 | ISO: A.8.30, A.8.28 | Priority: medium | Verify: grep `.github/workflows/*.yml` for `uses:.*@v[0-9]` or any non-40-hex ref and fail; optionally enforce with a pinning linter (zizmor/ratchet) in CI.* #### REQ-SUP-06 **Govern the C/Python plugin and AI-provider dependency boundary (documentation)** The project SHALL document and enforce the runtime trust boundary: dlopen'd C plugins and Python plugins are out-of-tree third-party code that run with full process privilege and SHALL NOT auto-load without opt-in (enforced by REQ-EXT-04), and the AI client's outbound LLM endpoints/dependencies SHALL be restricted to user-configured, TLS-validated hosts (enforced by REQ-EXT-01). *NIST: GV.SC-04, ID.RA-09 | ISO: A.5.19, A.8.30 | Priority: high | Verify: confirm documentation of the plugin/AI supply-chain boundary exists and that the enforcing controls (REQ-EXT-01/04) have passing tests; confirm the AI module only contacts configured hosts with cert verification.* ### REQ-VUL — Vulnerability & Patch Management, Change Control > Dependency floors (REQ-SUP-03), wrap pinning (REQ-SUP-01), CVE scanning (REQ-SUP-04), reproducible CI images (REQ-SUP-02), and hardening verification (REQ-MEM-01) are consolidated above. #### REQ-VUL-01 **Documented vulnerability-disclosure policy with scope, supported versions and response SLA** `SECURITY.md` SHALL specify, beyond a contact and PGP key: supported/maintained versions, in-scope components (OMEMO/OTR/PGP/OX crypto paths, XMPP stanza parsing, TLS, plugins, AI client), out-of-scope items, a target acknowledgement and remediation timeframe, and coordinated-disclosure expectations; designated owners for security-critical subsystems SHALL be recorded. *(Merges REQ-SDLC-10.)* *NIST: RS.MA-01, ID.IM-01, GV.RR-02, GV.OC-02 | ISO: A.5.5, A.5.26, A.8.8, A.5.2 | Priority: high | Verify: inspect `SECURITY.md` for a Supported Versions section, enumerated in/out-of-scope components, an explicit ack/remediation SLA, and coordinated-disclosure terms; confirm the contact key/fingerprint is current; add a CI doc-lint failing if required headings are absent.* #### REQ-VUL-02 **Update-availability check verifies TLS and treats the response as untrusted** `release_get_latest()` SHALL explicitly set `CURLOPT_SSL_VERIFYPEER=1L` and `CURLOPT_SSL_VERIFYHOST=2L`, constrain itself to HTTPS, and treat the fetched version string as untrusted (strict `^\d+\.\d+\.\d+$` validation before display), never auto-downloading or executing anything. *NIST: ID.RA-01, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: high | Verify: read `src/common.c` and assert the curl handle sets VERIFYPEER/VERIFYHOST and rejects non-https; assert `cons_check_version`/`release_is_new` validate against the version regex before use; unit-test feeding malformed/oversized version strings.* #### REQ-VUL-03 **User-controllable suppression of version/OS disclosure to peers** XEP-0092 version replies and disco SHALL be gated by user preferences, default to NOT revealing OS details (`PREF_REVEAL_OS` off), and allow disabling the `<version>` element entirely, so a peer cannot fingerprint an un-patched client+OS+build to target known CVEs. *NIST: PR.AA-05, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: medium | Verify: inspect the `src/xmpp/iq.c` version-reply: confirm `<os>` is added only when `PREF_REVEAL_OS` is true and `is_custom_client` false, a pref exists to omit `<version>`, and `PREF_REVEAL_OS` defaults off; stabber/functional test asserting the reply omits `<os>` by default.* #### REQ-VUL-04 **Security fixes recorded in CHANGELOG with CVE/advisory references** Each release SHALL document security-relevant fixes in the CHANGELOG, referencing the associated CVE/advisory identifier where one exists, so downstream packagers and users can assess patch urgency. *NIST: RC.RP-05, ID.IM-04 | ISO: A.8.8, A.5.26 | Priority: medium | Verify: review the CHANGELOG for a security/fix section per release with CVE/advisory ids where applicable (precedent: `Fix CVE-2017-5592`); extend `RELEASE_GUIDE.md` to mandate a security-fix changelog review before tagging.* ### REQ-RES — Resilience, Robustness & Fail-Safe Behavior > Fail-closed E2EE (REQ-CRY-01), force-encryption gate (REQ-CRY-02), OMEMO fail-safe trust (REQ-CRY-10), bounded stanza/OMEMO parsing (REQ-INP-09), and bounded AI calls (REQ-EXT-03) are consolidated above. #### REQ-RES-01 **Disk-space and atomicity preflight for SQLite schema migrations** Before any chatlog schema migration cproof SHALL verify free disk space via `_check_available_space_for_db_migration()` and SHALL run each migration inside a single transaction (BEGIN/COMMIT with rollback on error), so a failed/interrupted migration leaves the DB at its prior consistent schema version; on failure it SHALL surface an error and refuse to continue with a partially-migrated DB. *NIST: RC.RP-01, PR.IR-01 | ISO: A.8.13, A.8.27 | Priority: high | Verify: local (non-CI) migration tests on v1/v2 fixtures: simulate ENOSPC and assert migration refused with version unchanged; abort mid-migration and assert `DbVersion` reflects the pre-migration version; static review each `_migrate_to_vN` wraps statements in a transaction with rollback.* #### REQ-RES-02 **Detect and gracefully handle corrupted history DB without crashing or data loss** cproof SHALL run `PRAGMA integrity_check` on the chatlog DB and SHALL handle a non-`ok` result (and `sqlite3_open`/prepare failures) by reporting to the user and degrading gracefully (e.g. disabling DB-backed history for the session) rather than `abort()`/`exit()` or silently writing into a corrupt store. *NIST: RC.RP-01, PR.IR-03 | ISO: A.8.27, A.8.13 | Priority: high | Verify: open a deliberately truncated/garbage DB file and assert the app reports an integrity issue and continues running (no abort/exit, history disabled); confirm `_sqlite_verify_integrity` results are inspected and surfaced.* #### REQ-RES-03 **Graceful connection-loss handling with state-preserving reconnect (no message loss, no security downgrade)** On unexpected connection failure cproof SHALL transition to a defined DISCONNECTED state, preserve XEP-0198 stream-management state for resumable reconnect, and gate reconnect/auto-ping on a valid (non-NULL) connection, so connection flicker does not cause crashes, duplicate/dropped messages, or any change in the per-chat negotiated encryption posture (E2EE flags MUST survive reconnect). *NIST: PR.IR-03, RC.RP-01 | ISO: A.8.27, A.5.29 | Priority: high | Verify: drop the socket mid-session and assert status becomes DISCONNECTED, `sm_state` is captured and re-applied on reconnect, no crash, and chat E2EE flags (`is_omemo`/`is_ox`/`pgp_send`) are unchanged after resume; confirm `iq_autoping_check`/reconnect paths null-check the connection (cf. 15dfc2bd).* #### REQ-RES-04 **Sanitize outgoing message content to prevent self-inflicted stanza corruption** Outgoing chat, MUC, and private message bodies SHALL pass through `str_xml_sanitize()` before stanza construction, ensuring user/plugin-supplied content cannot inject control characters or invalid XML that would corrupt the stream or cause a server/peer disconnect. *NIST: PR.IR-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: test that messages with control/invalid-XML characters are sanitized before stanza construction and the stanza serializes to well-formed XML; static check that every outbound path (`cl_ev_send_msg_correct`, `cl_ev_send_muc_msg_corrected`, `cl_ev_send_priv_msg`) calls `str_xml_sanitize` on plugin/user bodies.* ### REQ-SDLC — Secure Development Lifecycle & Governance > Hardening parity (REQ-MEM-01), sanitizer/Valgrind CI gate (REQ-MEM-02), CWE-134 attribute audit blocking in CI (REQ-INP-04), dependency pinning/floors (REQ-SUP-01/03), and the security policy/ownership doc (REQ-VUL-01) are consolidated above. #### REQ-SDLC-01 **No secrets, private keys, or real credentials committed to the repo; secret-scanning in CI** The repository (working tree and pre-merge feature-branch history) SHALL contain no account passwords, OMEMO/OTR/PGP private key material, or other live secrets; tests and example configs SHALL use only placeholder/mock values, and an automated secret-scanning step (gitleaks/detect-secrets) SHALL run in CI to block credential-like content. *NIST: PR.AA-01, GV.OC-03 | ISO: A.8.4, A.5.8 | Priority: critical | Verify: add a gitleaks/detect-secrets CI job over the diff and full history; grep for PEM private-key headers (`BEGIN .* PRIVATE KEY`) and `password=`; confirm `profrc.example` and functional-test fixtures use only `@localhost`/stabber mock identities and no real server/passwords.* #### REQ-SDLC-02 **Test/dev data isolated from real user profiles and live XMPP services** cproof tests SHALL NOT read from or write to a real user's profanity profile (`~/.local/share/profanity`, `~/.config/profanity`) nor connect to production XMPP servers; functional tests SHALL run against the stabber mock using throwaway `@localhost` identities and isolated temporary `XDG_DATA_HOME`/`XDG_CONFIG_HOME` dirs created/torn down by the harness. *NIST: PR.IR-01, ID.AM-08 | ISO: A.8.31, A.8.29 | Priority: high | Verify: inspect functional-test setup/teardown for an isolated tmpdir XDG_* targeting localhost only (including `PROF_FLATFILE` paths); grep tests for absolute `$HOME` profile paths and for any non-localhost host fixture.* #### REQ-SDLC-03 **Mandatory peer review with merge gating and CODEOWNERS for security-critical paths** Every change SHALL merge via a pull request that passed required CI checks (build matrix, unit + functional tests, Valgrind/sanitizers) and received review approval before merge to master; changes touching `src/omemo`, `src/otr`, `src/pgp`/OX, `src/xmpp/connection.c` (TLS/SASL), authentication, `src/plugins`, or `src/ai` SHALL require review by a designated owner recorded in `.github/CODEOWNERS`. Security-relevant changes SHALL carry a conventional-commit `fix(<scope>)`/`feat(<scope>)` subject naming the affected module. *(Merges REQ-VUL change-control.)* *NIST: GV.RR-02, PR.PS-06, ID.IM-03 | ISO: A.5.8, A.8.25, A.8.32 | Priority: high | Verify: confirm branch protection on master requires PR review and passing checks; confirm commitlint runs in CI and rejects non-conventional types; add/verify a `CODEOWNERS` mapping the security-critical dirs and that GitHub enforces owner review; spot-check `git log` for `fix(<scope>)` security subjects.* #### REQ-SDLC-04 **Enforced secure-coding style and static safety conventions (blocking)** cproof source SHALL conform to the project's CI-enforced conventions — clang-format (`.clang-format`), correct use/initialization of the GLib `auto_*` cleanup types, and codespell cleanliness — and the style/auto-type checks SHALL be blocking (not `continue-on-error`) as a precondition for merge. *NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.28 | Priority: medium | Verify: run `make doublecheck` (format + spell) and require pass; run the auto-type count/init grep checks from the workflow and require zero violations; remove `continue-on-error` from the format/auto-type steps so they block merges.* #### REQ-SDLC-05 **Fuzz harness for highest-risk untrusted-input parsers** cproof SHALL maintain at least one fuzz harness (libFuzzer/AFL++, built under sanitizers) exercising stanza/XML handling, JID parsing, and inbound encrypted-message decode (OMEMO/PGP/OX), runnable from the repo with an in-tree seed corpus and exercised periodically in a scheduled CI job that fails on new crashes. *NIST: PR.PS-06, ID.RA-01 | ISO: A.8.29, A.8.25 | Priority: medium | Verify: confirm a fuzz target exists and builds with `-fsanitize=fuzzer,address` over stanza and JID parse entry points; run it for a bounded time in scheduled CI and require no new crashes; track the seed corpus in-repo.* #### REQ-SDLC-06 **Documented security requirements and threat assumptions for crypto/plugin/AI surfaces** cproof SHALL document its security requirements and trust assumptions for security-sensitive features — E2EE invariants (no plaintext fallback, fingerprint/trust handling), TLS/cert-validation policy, the privilege/trust model for C/Python plugins, and the AI module's data-egress behavior (which providers receive content, under what consent) — and SHALL map each documented invariant to an enforcing unit/functional test or code guard (this catalog plus its REQ-IDs serve as that baseline). *NIST: GV.PO-01, PR.DS-02, GV.SC-04 | ISO: A.5.8, A.8.25 | Priority: medium | Verify: confirm a documented requirements/threat-model artifact covers E2EE no-plaintext-fallback, TLS policy, plugin trust boundary, and AI egress/consent; map each invariant to a test (e.g. `tests/unittests/command/test_cmd_pgp.c`, `test_cmd_otr.c`) or code guard referencing the relevant REQ-ID.* ### Traceability summary #### NIST CSF 2.0 → Requirements - **GV (Govern)** — GV.PO-01: REQ-DAR-07, REQ-EXT-02, REQ-SDLC-06. GV.RR-02: REQ-SDLC-03, REQ-VUL-01. GV.OC-02/03: REQ-VUL-01, REQ-SDLC-01. GV.SC-04/05/06/07/08: REQ-SUP-01, REQ-SUP-02, REQ-SUP-03, REQ-SUP-04, REQ-SUP-05, REQ-SUP-06, REQ-SDLC-06. - **ID (Identify)** — ID.RA-01/08/09: REQ-CRY-11(→SUP-03), REQ-SUP-03, REQ-SUP-04, REQ-VUL-02, REQ-EXT-03, REQ-EXT-04, REQ-SDLC-05, REQ-MEM-02. ID.IM-01/02/03/04: REQ-VUL-01, REQ-VUL-04, REQ-SUP-04, REQ-SDLC-03. ID.AM-08: REQ-SDLC-02, REQ-SUP-01. - **PR (Protect)** — PR.AA-01/03/04/05: REQ-CRY-04, REQ-CRY-09, REQ-CRY-10, REQ-AUTH-01..06, REQ-CFG-01, REQ-VUL-03, REQ-SDLC-01. PR.DS-01/02/10: REQ-CRY-01..10, REQ-AUTH-01/06, REQ-DAR-01..07, REQ-INP-01/03/05/08/10, REQ-CFG-01/02, REQ-EXT-01/02. PR.PS-01/02/04/05/06: REQ-MEM-01..09, REQ-INP-03/04/06/07, REQ-LOG-01..06, REQ-SUP-03/05, REQ-EXT-04, REQ-SDLC-03/04/05. PR.IR-01/03/04: REQ-CRY-01/02, REQ-INP-09, REQ-RES-01..04, REQ-EXT-03, REQ-LOG-05, REQ-SDLC-02. - **DE (Detect)** — DE.AE-02: REQ-LOG-01, REQ-LOG-02, REQ-LOG-03. DE.CM-01/06/09: REQ-MEM-02, REQ-MEM-08, REQ-LOG-01, REQ-LOG-02, REQ-LOG-06, REQ-CRY-04, REQ-CFG-02, REQ-INP-09. - **RS / RC (Respond / Recover)** — RS.MA-01: REQ-VUL-01. RC.RP-01/05: REQ-RES-01, REQ-RES-02, REQ-RES-03, REQ-VUL-04. #### ISO/IEC 27001:2022 Annex A clusters → Requirements - **A.5 (Organizational)** — A.5.5/5.26 (incident/vuln reporting): REQ-VUL-01, REQ-VUL-04. A.5.7 (threat intel): REQ-SUP-04. A.5.8 (project security): REQ-SDLC-01/03/06, REQ-VUL-01. A.5.14 (info transfer): REQ-EXT-01/02, REQ-CFG-01. A.5.17 (auth info): REQ-AUTH-01..06, REQ-CRY-09/10. A.5.19/5.20/5.21/5.23 (supplier/ICT supply chain): REQ-SUP-01..06. A.5.29 (continuity): REQ-RES-03. A.5.31 (legal/identity binding): REQ-CRY-09. A.5.34 (PII/minimization): REQ-DAR-07. A.5.10 (acceptable use of info): REQ-CRY-02. - **A.8 (Technological)** — A.8.2/8.3 (access/privileged access): REQ-AUTH-01/02/05, REQ-EXT-04. A.8.4 (source code/secrets): REQ-SDLC-01. A.8.5 (secure auth): REQ-AUTH-03. A.8.6 (capacity): REQ-LOG-05, REQ-EXT-03. A.8.8 (technical vuln mgmt): REQ-SUP-03/04, REQ-VUL-01..04. A.8.9 (config mgmt/secure config): REQ-CRY-03/04/05/10, REQ-AUTH-03, REQ-CFG-01/02. A.8.10/8.11/8.12 (data masking/leakage/storage): REQ-DAR-01..07, REQ-AUTH-01. A.8.13 (backup/integrity): REQ-RES-01/02. A.8.15/8.16 (logging/monitoring): REQ-LOG-01..06, REQ-DAR-03. A.8.19 (software on operational systems): REQ-EXT-04. A.8.20/8.21 (network/network services security): REQ-CRY-03/04, REQ-AUTH-03. A.8.24 (cryptography): REQ-CRY-01..10, REQ-AUTH-06, REQ-EXT-01, REQ-CFG-01. A.8.25/8.27/8.28 (secure SDLC/architecture/secure coding): REQ-MEM-01..09, REQ-INP-01..10, REQ-SUP-05, REQ-SDLC-03..06, REQ-RES-01/04. A.8.26 (application security requirements): REQ-INP-01..10, REQ-EXT-02, REQ-RES-04. A.8.29 (security testing): REQ-MEM-02, REQ-INP-04, REQ-SDLC-02/04/05. A.8.30 (outsourced/third-party dev): REQ-SUP-01/02/06. A.8.31 (env separation): REQ-SDLC-02. A.8.32 (change management): REQ-SDLC-03, REQ-VUL change-control. ### Out of scope The following ISO/CSF control areas are intentionally excluded as not meaningful for a single-user desktop chat client with no central server or organizational IT estate: - Physical and environmental security (A.7.* — secure areas, equipment siting, cabling, physical entry, clear-desk physical aspects). - Human-resources security (A.6.* — screening, terms of employment, disciplinary process, awareness-training programs as an organizational HR function). - Datacenter / server-room operations, redundant power/cooling/utilities, and physical media handling/disposal at facility scale. - Organizational asset inventory of an IT estate, enterprise endpoint/MDM fleet management, network perimeter/firewall/IDS operations, and centralized IAM/directory services. - Business-continuity/DR planning at the organizational level, insurance, and supplier-contract/SLA legal management beyond the source-dependency pinning covered in REQ-SUP. - Server-side XMPP service hardening (the client connects to, but does not operate, an XMPP server).
jabber.developer added the
Kind/Security
Status
Need More Info
2
Priority
Medium
3
labels 2026-06-29 17:11:39 +00:00
Collaborator

Security Compliance — NIST CSF 2.0, ISO/IEC 27001:2022 & CIS Controls v8.1 (cproof)

This document combines a requirements catalog with a static source-code compliance audit for cproof (a fork of the Profanity console XMPP/Jabber chat client) across three frameworks: NIST Cybersecurity Framework (CSF) 2.0, ISO/IEC 27001:2022, and CIS Critical Security Controls v8.1. The audit target is master at HEAD 622054fc6 (which includes the merged PGP plaintext-fallback fix), read directly from the working tree. Method: the NIST/ISO pass used 12 domain auditors with every consequential finding adversarially re-verified against the working tree; the CIS v8.1 pass used per-control auditors over the applicable controls with adversarial verification, cross-validated against the NIST/ISO pass. Part 1 is the compliance audit; Part 2 is the requirements catalog that the audit is traced against.

References

Note: the CIS v8.1 PDF originally linked for this audit was unreachable (HTTP 404), so the authoritative CIS v8.1 control/safeguard structure from the official source above was used.

Table of contents

Part 1 — Compliance audit

Target: cproof master, HEAD 622054fc6 (includes the merged PGP plaintext-fallback fix). Catalog: Part 2 below. Method: Static source analysis. NIST/ISO pass: 12 domain auditors, every consequential finding adversarially re-verified against the working tree. CIS Controls v8.1 pass: per-control auditors over the applicable controls with adversarial verification (see the CIS Controls v8.1 section below).

1. Executive summary

cproof's core E2EE wire guarantees are sound: after commit 622054fc6 no cleartext <body> reaches the network on any PGP, OX, or OMEMO encryption failure (REQ-CRY-01), TLS is mandatory by default and certificate validation fails closed with explicit, persisted TOFU trust (REQ-CRY-04, REQ-CRY-10), and the auto-cleanup/secure-coding conventions are real and CI-checked (REQ-MEM-06). However, the audit surfaced several serious gaps that an E2EE chat client cannot treat as cosmetic. The single most dangerous is REQ-EXT-01 (Gap, critical residual): the AI/LLM client never sets CURLOPT_SSL_VERIFYPEER/VERIFYHOST and accepts http:// provider URLs, so the API key and full conversation can be sent in cleartext or to an unverified peer. On the inbound side, REQ-INP-01 (Gap, high) is a remotely-triggerable NULL-deref DoS: four receive-path handlers dereference jid_create() results without a NULL guard on a malformed from (e.g. a@@b), and the exact fix the requirement names was never applied. REQ-DAR-03 / REQ-LOG-03 (Gap, high) leak decrypted plaintext bodies into profanity.log in the default dblog=on mode via four log_* sites in database_sqlite.c. REQ-AUTH-01 (Partial, high) leaves the OTR private key (keys.txt) and the plaintext history DB (chatlog.db) world/group-readable (no chmod after creation). REQ-CRY-06 (Partial) persists unauthenticated decrypted OMEMO media to the user's chosen path before the GCM tag is verified. Supply-chain posture is weak across the board: the libstrophe Meson wrap is pinned to revision = HEAD (REQ-SUP-01, Gap critical), CI base images and bootstrapped deps float (REQ-SUP-02), and there is no dependency-CVE scanning, no secret-scanning, no fuzzing, and no CODEOWNERS (REQ-SUP-04, REQ-SDLC-01/REQ-SDLC-03/REQ-SDLC-05). Build hardening is partial — autotools omits PIE/noexecstack and the Meson release path ships with zero hardening (REQ-MEM-01), and the ASan/UBSan suite is never run in CI (REQ-MEM-02). The recurring theme: the cryptographic primitives and wire paths are well-built, but the local data-at-rest, operational-logging, build-hardening, and supply-chain/process controls lag, and several requirement-named defects remain literally unfixed at HEAD.

2. Compliance scorecard

Domain Met Partial Gap N/A Unknown
CRY (crypto/TLS) 3 7 0 0 0
AUTH (auth/secrets) 2 4 0 0 0
INP (input validation) 4 4 2 0 0
MEM (memory safety) 1 5 3 0 0
DAR (data at rest) 6 0 1 0 0
LOG (logging) 3 2 1 0 0
CFG (config) 1 1 0 0 0
EXT (external/AI/plugins) 1 2 1 0 0
SUP (supply chain) 0 1 5 0 0
VUL (vuln mgmt) 0 3 1 0 0
RES (resilience) 3 1 0 0 0
SDLC 1 3 2 0 0
Overall 25 33 16 0 0

How to read this: Met = the requirement's normative SHALL clauses are satisfied in code on master (residual risk usually low). Partial = the core control exists but at least one mandated clause/sub-path/test is unmet. Gap = the control is materially absent or a requirement-named defect is unfixed. Residual risk is judged by exploitability and blast radius, not by status alone — e.g. several Partial items (REQ-AUTH-01, REQ-CRY-03) carry high residual risk, while some Gap items (REQ-SUP-05) are low.

Note: the overall tally counts 74 verified requirements; the four findings the verifier left as null (REQ-AUTH-04, REQ-INP-10, REQ-MEM-06, REQ-DAR-04/REQ-DAR-05/REQ-DAR-06/REQ-DAR-07, REQ-CFG-02, REQ-LOG-04/REQ-LOG-05, REQ-RES-04, REQ-SDLC...) retain their auditor status, all Met.

3. Critical & high-priority gaps (ranked)

3.1 — REQ-EXT-01 — Enforce TLS verification & https on AI/LLM outbound — Gap (critical residual)

  • Evidence: src/ai/ai_client.c:811-815 and :1596-1601 set only URL/HTTPHEADER/WRITEFUNCTION/WRITEDATA/POSTFIELDS/TIMEOUT. The provider scheme check (src/command/cmd_funcs.c:7019) explicitly accepts both http and https (g_strcmp0(scheme,"http")==0 || …"https"…), and ai_provider_new (ai_client.c:388) stores api_url verbatim. _build_curl_headers (ai_client.c:795) attaches Authorization: Bearer <key>; request_url is concatenated from provider->api_url (ai_client.c:1590). tests/functionaltests/test_ai.c:85,95 codifies acceptance of http://127.0.0.1:1/.

Correction: The original auditor framing ("no CURLOPT_SSL_VERIFYPEER/VERIFYHOST ⇒ unverified HTTPS") is overstated: libcurl defaults VERIFYPEER=1/VERIFYHOST=2, so HTTPS provider URLs are certificate-verified by default. There is also a scheme check (the auditor said there was none). The real, confirmed defect is narrower but still serious: http:// provider URLs are accepted, so /ai set provider … http://… ships the API key and the entire conversation in cleartext over plain HTTP. Residual risk: high (down from "critical" as originally rated).

  • Why it matters: NIST PR.DS-02 / ISO A.8.24, A.5.14 (data-in-transit). Cleartext egress of an API key + full message content to (or via) any on-path observer.
  • Fix: Reject non-https schemes in the provider scheme check (cmd_funcs.c:7019) and at request time before curl_easy_perform; flip test_ai.c to assert http:// is rejected. Additionally set CURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2L explicitly at both curl sites as defense-in-depth (guards against a non-default global curl config / libcurl built with relaxed defaults).

3.2 — REQ-SUP-01 — Pin libstrophe Meson wrap to an immutable revision — Gap (high residual, critical priority)

  • Evidence: subprojects/libstrophe.wrap line 3 revision = HEAD; no 40-hex SHA, tag, or source_hash. No CI lint catches it.
  • Why it matters: NIST ID.RA / GV.SC, ISO A.5.23/A.8.30. libstrophe is the XMPP parser on the untrusted-network path; any clone can silently incorporate an attacker-modified/regressed parser, and builds are non-reproducible.
  • Fix: Replace revision = HEAD with the vetted 40-hex commit SHA (or tag + source_hash); add a CI lint failing on revision = HEAD/bare-branch wraps.

3.3 — REQ-INP-01 / REQ-INP-09 — NULL-guard peer JIDs on receive — Gap (high)

  • Evidence: jid_create() returns NULL for NULL/empty/over-3071-char/invalid-UTF-8/double-@ (src/xmpp/jid.c:35-38,82-153). Four unguarded derefs: src/xmpp/message.c:1749-1750 (_should_ignore_based_on_silence, the exact function the requirement names), src/xmpp/presence.c:397-402 (_presence_error_handler, from not even NULL-checked), presence.c:453-454 (_unsubscribed_handler), presence.c:468-470 (_subscribed_handler). The sibling _subscribe_handler (presence.c:482-484) does guard, proving these are bugs.
  • Why it matters: NIST PR.DS / ISO A.8.28. A hostile peer sending a stanza with missing/malformed from (e.g. a@@b) causes a remote NULL-deref DoS — a crash on every connection attempt from that peer.
  • Fix: After every receive-path jid_create(from) add if (!from_jid) { return; } before field access. Add a regression test driving these handlers with absent/a@@b from under ASan.

3.4 — REQ-DAR-03 / REQ-LOG-03 — Plaintext bodies in operational logs — Gap (high)

  • Evidence: Default PREF_DBLOG="on" (src/config/preferences.c:2815) does not redact. src/database_sqlite.c:694 (LMC mismatch, message->plain), :717 (duplicate stanza-id, content: %s), :748 (log_debug("Writing to DB. Query: %s") — the INSERT embeds message->plain at :735), :760 (insert-failure, the requirement-named defect) all write decrypted bodies to profanity.log. Triggers are remotely influenceable (XEP-0308 LMC, server-assigned stanza-id). The libstrophe debug bridge _xmpp_file_logger (src/xmpp/connection.c:1088-1107) forwards raw SASL <auth>/<challenge>/<response> and XEP-0077 registration credentials with no redaction (gated by PREF_STROPHE_VERBOSITY, default off).
  • Why it matters: NIST PR.DS-01 / ISO A.8.10, A.8.12. Decrypted E2EE content lands in a plaintext log in the shipped default config, surviving all log-level filters — defeating the point of E2EE for anyone who shares logs for debugging.
  • Fix: Drop message->plain/content:/Message: from the format args at database_sqlite.c:694,717,760; do not log the full INSERT at :748 (log row id only). Redact <auth>/<challenge>/<response>/jabber:iq:register payloads in _xmpp_file_logger.

3.5 — REQ-AUTH-01 — Owner-only (0600) on every secret/history file — Partial (high)

  • Evidence: Met for keyfiles/accounts/OMEMO/tlscerts (prof_keyfile g_chmod 0600, src/common.c:186/202), main log (src/log.c:177), export (src/database_export.c:329). Unprotected: src/database_sqlite.c:144 sqlite3_open(filename,…) has no following chmod (plaintext history DB inherits umask, typically 0644); OTR keys.txt (src/otr/otr.c:372) and fingerprints.txt (otr.c:113,386) written by libotr with no chmod. No process-wide umask anywhere in the tree.
  • Why it matters: NIST PR.AC / ISO A.8.3, A.5.15. The OTR private signing key and the decrypted-history DB are readable by other local users on a default umask.
  • Fix: g_chmod(filename, S_IRUSR|S_IWUSR) after sqlite3_open; g_chmod after each otrl_privkey_generate/otrl_privkey_write_fingerprints.

3.6 — REQ-CRY-06 — Persisted unauthenticated OMEMO media plaintext — Partial (medium)

  • Evidence: aes256gcm_crypt_file writes all decrypted plaintext to the output FILE* (src/omemo/crypto.c:445) before gcry_cipher_checktag (:462). The caller opens outfh directly on the user's final destination (src/tools/aesgcm_download.c:66) and on tag failure only prints an error (:114-118) — it never removes the plaintext output (remove(tmpname) at :112 deletes only the ciphertext temp), and the external-command branch (:126-140) still runs on a failed decrypt. (Confirmed verbatim above.)
  • Why it matters: NIST PR.DS-08 / ISO A.8.24. Violates the AEAD "no plaintext output on tag mismatch" mandate — a tampered/truncated media file yields a fully-persisted, attacker-controlled file at the user's path, possibly fed to an external opener.
  • Fix: Decrypt to a temp file, verify the tag, then atomically rename on success; on failure remove() the output and skip the cmd_template branch.

3.7 — REQ-CRY-03 — No silent STARTTLS downgrade — Partial (high)

  • Evidence: Default (NULL/force) correctly sets MANDATORY_TLS (src/xmpp/connection.c:185-186). But the allowlisted allow policy (src/common.c:406) sets neither MANDATORY_TLS nor DISABLE_TLS (connection.c:185-194) → opportunistic STARTTLS, plaintext if the server omits it, with no log (flags==0 so the :201 diagnostic never fires). In-band registration uses xmpp_connect_raw (connection.c:496-515); _register_handle_features secures only "if possible" (:397-431), so even tls.policy=force sends new-account credentials in cleartext when STARTTLS is absent.
  • Why it matters: NIST PR.DS-02 / ISO A.8.24. SASL/registration credentials can traverse plaintext under a documented, autocompleted policy.
  • Fix: Map allow to a hard "TLS-required-or-fail" floor or warn loudly; enforce TLS before in-band registration sends credentials.

3.8 — REQ-MEM-01 — Full hardening baseline, build-system parity — Partial (high)

  • Evidence: autotools has stack-protector/FORTIFY/RELRO/-Wformat=2 but no -fPIE/-pie and no -Wl,-z,noexecstack (configure.ac). meson.build:64-71 gates stack-protector + FORTIFY behind if is_debug; the release path (:33, executable() at :536) gets none, and Meson has zero RELRO/now/noexecstack/PIE link args. No checksec/readelf CI gate.
  • Why it matters: NIST PR.PS / ISO A.8.31. Meson release binaries ship with no exploit mitigations; the two build systems are not at parity.
  • Fix: Add -fPIE -pie -Wl,-z,noexecstack (probed) to autotools; move Meson hardening out of if is_debug and add b_pie=true + relro/now/noexecstack link args; add a checksec CI assertion.

3.9 — REQ-MEM-02 — ASan/UBSan gate the test suite in CI — Partial (high)

  • Evidence: --enable-sanitizers exists (configure.ac:74-75,441-452) but no CI job passes it (ci-build.sh:193-205 defines 4 Linux configs, none with sanitizers; tree-wide grep finds no use). Valgrind is wired and gating (ci-build.sh:295).
  • Why it matters: NIST PR.PS / ISO A.8.28. The deliberate-heap-overflow / unsigned-overflow verification would not fail CI; memory-safety regressions can land.
  • Fix: Add a non-continue-on-error job running ./configure --enable-sanitizers && make check.

3.10 — REQ-EXT-04 — Plugin install: https + integrity verification — Partial (high)

  • Evidence: cmd_plugins_install's _http_based_uri_scheme accepts both http and https (src/command/cmd_funcs.c:7019); plugin download reuses http_file_get, which sets CURLOPT_SSL_VERIFY*=0L when account tls_policy=="trust" (src/tools/http_download.c:120-156); the downloaded .so/.py is plugins_install'd then dlopen'd at full privilege with no signature/checksum check (src/tools/plugin_download.c, src/plugins/plugins.c:198-218). copy_file (src/common.c:234-243) doesn't strip group/world-write. Trust model undocumented.
  • Why it matters: NIST GV.SC / ISO A.5.23, A.8.25. A network MitM can substitute a malicious plugin that runs with full process privilege and access to all secret stores — RCE via plugin update.
  • Fix: Require https for plugin install; force VERIFY*=1 regardless of XMPP tls_policy; verify a signature/checksum before install; chmod 0600 installed files; document the full-privilege trust model.

3.11 — Other high-priority Gaps (concise)

  • REQ-SUP-02 (Gap): CI base images use mutable tags (ci/Dockerfile.*), deps cloned at HEAD with no git checkout <sha>, Arch AUR snapshot fetched + makepkg'd unverified (Dockerfile.arch:61-65). → Digest-pin FROMs, commit-pin checkouts, sha256sum -c downloads.
  • REQ-SUP-03 (Partial, high): gpgme uses unversioned AC_CHECK_LIB([gpgme],[main]) (configure.ac:309); libgcrypt has no floor (:351); sqlite3/gcrypt/gpgme floors diverge from meson.build. → Versioned PKG_CHECK_MODULES, align floors, add a parity lint.
  • REQ-MEM-03 (Gap): strcpy/strcat on peer barejids (src/pgp/ox.c:153-156), sprintf on key/nonce (src/omemo/crypto.c:479,483) — forbidden primitives, no CI grep. (Buffers are correctly sized, so residual is medium.) → Replace with g_strdup_printf/snprintf; add a CI grep.
  • REQ-MEM-04 (Gap): No overflow-checked allocation anywhere; omemo.c:1440 malloc(len*2+1), http_upload.c:101-103 unchecked size*nmemb/realloc-overwrite, three omemo.c:455/475/492 malloc(*length) deref'd without NULL check. → __builtin_mul_overflow/g_malloc_n, NULL-check, save realloc to temp.
  • REQ-MEM-09 (Gap): alloca sized from peer barejid length (src/pgp/ox.c:150-151), no -Wvla. → Replace with g_strdup_printf; add -Werror=vla.
  • REQ-INP-08 (Gap): No control-char/bidi neutralization or length cap on display/log/DB sinks (src/ui/window.c:2024,2120); str_xml_sanitize is outgoing-only and skips U+202E. → Single sanitizer at display + log/DB write boundary.

4. Partial / medium findings (grouped)

CRY

  • REQ-CRY-01 — MUC OMEMO caller logs/echoes plaintext on NULL id (client_events.c:200-203, confirmed). → Guard if (id != NULL) like the 1:1 path.
  • REQ-CRY-02 — OTR-opportunistic first message bypasses allow_unencrypted_message (src/otr/otr.c:332-339,425-428). → Consult the gate before the tagged cleartext send.
  • REQ-CRY-07get_random_string uses g_rand_* (Mersenne-Twister) for stanza IDs and the profanity_instance_id HMAC key (src/common.c:712-717, connection.c:808,1123). → Switch identifiers/HMAC seed to gcry_create_nonce.
  • REQ-CRY-09blind/firstusage modes persist trust from inbound handlers (src/omemo/omemo.c:608,698). → Confine trust transitions to user commands.

AUTH

  • REQ-AUTH-03 — Insecure flags logged at log_debug, not WARN (connection.c:201-213). → WARN + cons_show on DISABLE_TLS|LEGACY_*|TRUST_TLS.
  • REQ-AUTH-05eval_password spawned with G_SPAWN_SEARCH_PATH (src/config/account.c:149). → Require absolute path / drop the flag.
  • REQ-AUTH-06 — Account/eval/PGP passphrases freed without wipe (account.c:184-185, session.c:581/591, gpg.c:79/104/108). → explicit_bzero before free.

INP

  • REQ-INP-02 — Validation correct; the prescribed boundary/forbidden/@@/UTF-8 test table is absent (tests/.../test_jid.c). → Add the table-driven suite.
  • REQ-INP-05 — MAM <result> id not disco-gated (message.c:1536-1571 → stored at :1409-1410). → Apply _stanza_id_by_trusted on the MAM path.
  • REQ-INP-06/url open|save accept any scheme (cmd_funcs.c:9633-9655,9678-9698). → Whitelist http/https/aesgcm.
  • REQ-INP-07 — OSX notifier uses system() with hand-rolled escaping (src/ui/notifier.c:160). → g_spawn argv / g_shell_quote.

MEM

  • REQ-MEM-05omemo.c:1437 identity_public_key_len-- lacks a >0 guard. → Reject zero-length before decrement.
  • REQ-MEM-07identity_key_store_destroy doesn't NULL freed fields (store.c:51-53); undocumented out-param ownership. (Double-free unreachable — memset at omemo.c:307.) → Document ownership + add =NULL.
  • REQ-MEM-08 — Good NULL-guarding by convention but -Wnull-dereference only under non-CI --enable-hardening. → Enable in one CI config.

LOG / DAR

  • REQ-LOG-01 — SASL auth failure at INFO (server_events.c:207); TLS cert-fail logs nothing (server_events.c:1135-1200). → Promote both to WARN.
  • REQ-LOG-02 — TLS-disabled / handshake-fail / see-other-host / insecure-login all DEBUG-only. → WARN on all four.
  • REQ-LOG-06 — stderr→log bridge applies no redaction (src/log.c:289,297). → Redact secret markers before log_msg.

CFG / VUL / SDLC

  • REQ-CFG-01tls.policy=trust disables curl verify silently on transfers, no user warning (http_upload.c:242-244, http_download.c:153-155). → Surface the insecure state per transfer.
  • REQ-VUL-02release_get_latest never sets VERIFY*/https-only (src/common.c:459-482). → Set them explicitly.
  • REQ-VUL-03PREF_REVEAL_OS defaults TRUE (preferences.c:2692,2706); no <version>-omit pref. → Default OFF; add version toggle.
  • REQ-VUL-04 — No per-release Security/CVE changelog convention; RELEASE_GUIDE.md has no security step. → Add convention + review step.
  • REQ-EXT-03 — No CURLOPT_CONNECTTIMEOUT; no cap/stall test (ai_client.c). → Add connect-timeout + tests.
  • REQ-RES-02PRAGMA integrity_check only on /history verify, never at DB open (database_sqlite.c). → Run quick_check in _sqlite_init.
  • REQ-SDLC-03 — No CODEOWNERS; commitlint config dormant (.commitlintrc.json unreferenced). → Add CODEOWNERS + commitlint CI job.
  • REQ-SDLC-04 — Style/auto-type/CWE-134/spell checks marked continue-on-error (ci-code.yml:40,94). → Remove continue-on-error.
  • REQ-SDLC-06 — No invariant→test REQ-ID traceability matrix. → Add compliance/REQ-TO-TEST.md.

5. What cproof does well (notable Met items)

  • REQ-CRY-01 — Fail-closed E2EE on the wire. Every PGP failure branch returns NULL before _send_message_stanza (message.c:491-517); OX returns NULL before send (:564-571); OMEMO keys==NULL does goto out without reaching message_send_chat_omemo (omemo.c:928-966). The 622054fc6 fix is real and the 1:1 caller guards if (id != NULL) (client_events.c:138/148/157).
  • REQ-CRY-04 — TLS cert validation fails closed. Certfail handler always registered (connection.c:226); blind trust only on explicit tls.policy=trust; sv_ev_certfail accepts only via prior-trust or explicit /tls allow|always, else return 0 (server_events.c:1134-1191).
  • REQ-CRY-10 — Fail-safe OMEMO trust. Default manual mode; untrusted devices get no session (omemo.c:702-705) and are skipped; keys==NULL aborts with a remediation listing and no stanza.
  • REQ-AUTH-02 — 0700 secret dirs. All dir creation routes through create_dirg_mkdir_with_parents(name, S_IRWXU) (common.c:224); no 0755 bypass anywhere.
  • REQ-INP-03 — No CWE-134 format-string sinks. Every variadic wrapper carries G_GNUC_PRINTF; all untrusted classes (URLs, plugin, AI response) are %s-wrapped; check-cwe134.sh exits 0.
  • REQ-MEM-06 — Auto-cleanup macros enforced. Nine auto_* macros, 1105 uses, CI asserts the set and fails on uninitialized auto_* locals (ci-code.yml:48,51).
  • REQ-DAR-01/REQ-DAR-02off/redact modes are airtight. Both backends early-return / placeholder-substitute before any persistence (database_sqlite.c:640-647, database_flatfile.c:363-373).
  • REQ-EXT-02 — AI consent is opt-in. Content reaches a provider only from a user-opened AI window; no inbound hook auto-forwards; default providers carry no key so no network call occurs; history is volatile-only.
  • REQ-RES-01/REQ-RES-03 — Migration preflight + state-preserving reconnect. Disk-space gate + single-transaction migrations with ROLLBACK (database_sqlite.c:252-258,783-918); XEP-0198 SM state captured/re-applied with no E2EE-flag downgrade on reconnect; OTR torn down fail-safe.

6. Notable verifier disagreements

Several auditor "Met" verdicts were overturned to Partial by the adversarial verifier, and these corrections are material — they prevent over-stating compliance:

  • REQ-CRY-01 — The auditor noted the caller logs/echoes only when id != NULL. The verifier found the MUC OMEMO caller (client_events.c:200-203) unconditionally logs the plaintext to the SQLite history DB and echoes it on screen even when omemo_on_message_send returns NULL. Confirmed verbatim in this audit. Net: no network plaintext, but a local DB + UX leak on the MUC failure path.
  • REQ-CRY-02 — Auditor: the gate covers all cleartext sends. Verifier: the OTR-opportunistic first message returns TRUE from otr_on_message_send, short-circuiting && allow_unencrypted_message(), so the force-encryption gate is never consulted for that cleartext send.
  • REQ-CRY-06 — The auditor flagged only a benign short-tag nuance. The verifier found the decisive defect: file plaintext is persisted to the user's destination before tag verification, and the post-download external command still runs on decrypt failure.
  • REQ-CRY-07 — The auditor (grep scoped to omemo/otr/pgp) declared CSPRNG "Met". The verifier found g_rand_* (named as forbidden in the requirement) used for stanza IDs and an HMAC key seed in src/common.c/src/xmpp.
  • REQ-CRY-09 / REQ-INP-05 — Auditor "Met"; the verifier found inbound-handler trust transitions (blind/firstusage) and an un-disco-gated MAM <result> id, respectively.
  • REQ-MEM-07 — The verifier lowered residual risk (medium→low): the "latent double-free" is unreachable because signal_protocol_store_context_destroy is guarded and immediately followed by memset(&omemo_ctx, 0, …) (omemo.c:302-307).

In every overturned case the verifier's corrected status is the one of record; the disagreements were due to the original auditors' greps being scoped too narrowly (missing src/common.c, src/xmpp, the MUC caller, the MAM path) rather than misreading the cited code.

7. Methodology & limitations

  • Scope: Static source analysis of master at HEAD 622054fc6, read directly from the working tree, requirement-driven against the catalog in Part 2. Each consequential finding was produced by a domain auditor and independently re-verified by an adversarial verifier; where they disagreed, the corrected status is reported.
  • Not a pentest: No dynamic exploitation, fuzzing, or runtime instrumentation was performed. DoS/overflow reachability is argued from code paths, not demonstrated with live inputs; exploitability assessments (e.g. the correctly-sized alloca/sprintf buffers in REQ-MEM-03/REQ-MEM-09) reflect code review, not runtime proof.
  • Server-side items are Unknown by nature: GitHub branch-protection / required-status-check enforcement (REQ-SDLC-03) cannot be verified from the tree; only the in-repo prerequisites (absent CODEOWNERS, dormant commitlint) are confirmed. The PGP fingerprint currency in SECURITY.md (REQ-VUL-01) is likewise unverifiable here.
  • Build-config caveats: Findings about Meson vs autotools parity (REQ-MEM-01, REQ-SUP-03) are read from build files, not from inspecting produced binaries; a checksec/readelf pass on actual release artifacts would confirm the shipped mitigation set.
  • No status was inflated: Several requirement-named defects (REQ-INP-01, REQ-DAR-03, REQ-MEM-03/REQ-MEM-09) were confirmed still present and unfixed at HEAD; conversely, null-verdict items retain their auditor "Met" status and were not independently re-derived here.

CIS Controls v8.1 — source compliance

Target: cproof master, HEAD 622054fc6 (includes the merged PGP plaintext-fallback fix). Catalog: Part 2 below (NIST CSF 2.0 + ISO 27001 + the CIS v8.1 section). Method: Static source analysis, per-control auditors with adversarial verification, cross-validated against the NIST/ISO source audit above.

CIS 1. Executive summary

Measured against the CIS v8.1 IG1 "essential cyber hygiene" baseline as it applies to a desktop client + its repository, cproof's runtime/cryptographic posture is strong but its producer/process posture is weak. The strongest1 areas are exactly where the NIST/ISO audit found Met items: vetted, well-established crypto modules rather than home-grown primitives (CIS-16.11 — libsignal/libomemo-c, libgcrypt, gpgme, libotr; REQ-CRY-06/REQ-CRY-07/REQ-CRY-08), TLS-mandatory-by-default in transit with fail-closed certificate validation (CIS-4.6/15.4 ↔ REQ-CRY-03/REQ-CRY-04), secure-by-default values (OMEMO trust manual, enc-warn/tls-show on; CIS-4.1 ↔ REQ-CRY-10/REQ-CFG-02), the ability to compile out or disable every higher-risk subsystem (CIS-4.8 — Met), and CWE-134-clean, auto-cleanup-enforced secure-coding conventions (CIS-16.12 partial ↔ REQ-INP-03/REQ-MEM-06). The most serious CIS gaps are all process/supply-chain/data-at-rest and they independently cross-validate gaps the NIST/ISO audit already found: the libstrophe Meson wrap pinned to revision = HEAD with no SHA/source_hash (CIS-2.2/2.6/7.2/16.5 ↔ REQ-SUP-01, confirmed in subprojects/libstrophe.wrap:3); decrypted plaintext message bodies written to profanity.log in the default dblog=on mode (CIS-8.5 ↔ REQ-DAR-03/REQ-LOG-03, four log_* sites in database_sqlite.c); no code-level/dependency security scanning in CI — no OSV/Dependabot/trivy, no secret scan, no fuzzing, and the --enable-sanitizers build never exercised (CIS-7.4/16.12/18.1 ↔ REQ-SUP-04/REQ-MEM-02/REQ-SDLC-05); no real vulnerability-disclosure process — SECURITY.md is 5 lines (contact + PGP fingerprint only) with no supported-versions, scope, or SLA (CIS-7.1/16.2 ↔ REQ-VUL-01); and the AI/LLM client accepting http:// provider URLs so the API key + full conversation can egress in cleartext (CIS-15.4 ↔ REQ-EXT-01). Two further first-class CIS gaps with no exact single REQ predecessor are the absence of any maintained SBOM/third-party-software inventory (CIS-2.1/16.4) and the absence of any documented audit-log-management/logging policy (CIS-8.1). Net IG1 verdict: cproof would not pass a clean IG1 review today — primarily on CIS-2 (inventory/supported-software), CIS-7 (vuln-management/patch automation), and CIS-8 (audit-log content/process), not on its cryptographic or transport controls.

CIS 2. Scorecard

Counts are over the relevant safeguards within each applicable control (the safeguards processed in this audit), plus a per-control Applicability tag. N/A counts the safeguards that are enterprise-IT within an otherwise-applicable control.

Control Applicability Met Partial Gap N/A
CIS-2 Inventory & Control of Software Assets Partial (software/deps) 0 3 0 0
CIS-4 Secure Configuration Partial (app config) 1 2 0 0
CIS-5 Account Management Partial (credential storage) 0 1 0 1
CIS-6 Access Control Management N/A (no exposed app) 0 0 0 1
CIS-7 Continuous Vulnerability Management Applicable (producer) 0 2 1 0
CIS-8 Audit Log Management Partial (app log) 0 2 2 7
CIS-9 Email & Web Browser Protections Partial (URL/file surface) 0 2 0 0
CIS-15 Service Provider Management Applicable (providers/upstreams) 0 2 0 0
CIS-16 Application Software Security Applicable (core) 5 6 3 0
CIS-18 Penetration Testing Partial (app fuzz/sanitizer) 0 0 2 0
Overall (applicable safeguards) 6 22 10 10

CIS-16 counts fold in the mapping from the catalog: Met = 16.10/16.11 (secure design + vetted modules), 16.8 (prod/non-prod), and the secure-coding-convention slices of 16.12; Partial = 16.2/16.4/16.5/16.6/16.7/16.9 (inventory/process/severity/hardening-doc/training gaps); Gap = 16.12 (no sanitizer/fuzz/scan in CI), 16.13/18.1 (no pen-test/fuzz program). Enterprise-only controls CIS-1/3/10/11/12/13/14/17 are summarized in CIS 5, not scored here.

CIS 3. Critical & high-priority CIS gaps (ranked)

3.1 — CIS-2.1 / 16.4 — No software / third-party-component inventory (SBOM) — Partial→Gap (IG1)

  • Evidence: The de-facto dependency list exists only as build declarations: configure.ac PKG_CHECK_MODULES for glib-2.0>=2.62.0 (:89), libcurl>=7.62.0 (:97), sqlite3>=3.22.0 (:107), libstrophe>=0.12.3 (:176), libotr>=4.0 (:326), libsignal-protocol-c>=2.3.2 (:349); meson.build mirrors a subset with divergent floors (sqlite3>=3.35.0 :96, libomemo-c>=0.5.1 :273, libgcrypt>=1.7.0 :276). No *.spdx/*.cdx/bom.* in the tree, no README/INSTALL dependency table, no machine-readable inventory; the only narrative inventory is REQ-SUP-03 in this very catalog.
  • Why it matters: IG1 foundational. Without an authoritative, version-accurate component list one cannot answer "are we affected by CVE-X?" — the precondition for CIS-7 and CIS-16.5. The two build files are not even self-consistent (sqlite floor 3.22.0 vs 3.35.0; gpgme/gcrypt unversioned in autotools), so no single source of truth exists.
  • Remediation: Ship a CycloneDX/SPDX SBOM generated+diff-checked in CI (REQ-CIS-01), listing every linked library with min-version/license/URL; reconcile the autotools-vs-meson floors to one source. Cross-validates REQ-SUP-03.

3.2 — CIS-7.1 / 16.2 — No vulnerability-disclosure / management process — Partial (IG1)

  • Evidence: SECURITY.md:1-7 is contact + PGP fingerprint (6F88C537A25E481489574956E3370C9A9A55BF71) only — confirmed verbatim — with no supported-versions, no in/out-of-scope component enumeration, no ack/remediation SLA, no coordinated-disclosure terms. No .github/dependabot.yml; a tree-wide search for osv-scanner/trivy/grype/snyk/renovate returns nothing. Dependency floors exist and the build fails below most of them, but they are static, undocumented as to rationale, and not driven by any recurring review cadence.
  • Why it matters: IG1. The producer has no defined, recurring path to identify, severity-rate, and triage vulnerabilities in the third-party set (libstrophe/gpgme/libsignal/libgcrypt/libotr/sqlite/glib/libcurl), and the inbound disclosure half lacks scope and SLA.
  • Remediation: REQ-CIS-02 + REQ-VUL-01: flesh out SECURITY.md with Supported Versions, scope, ack+remediation SLA, coordinated-disclosure terms; document a monthly + on-push review cadence fed by an automated scanner.

3.3 — CIS-7.4 / 16.12 — No automated dependency patch/CVE management in CI — Gap (IG1)

  • Evidence: .github/workflows/ci-code.yml runs only the Linux build matrix (arch/debian/ubuntu, :22-35), a continue-on-error code-style job (:37-89), a continue-on-error spell-check (:91-103), and coverage (:105-113). No dependency-CVE scan, no Dependabot, no OSV/trivy/grype/renovate. The opposite of patch management is present in image builds: ci/Dockerfile.debian:2 FROM debian:testing (mutable), :44-45 git clone --depth 1 of stabber and libstrophe at floating HEAD with no git checkout <sha>; ci/Dockerfile.arch:1 FROM archlinux:latest, :61-65 wget of an AUR libstrophe-git snapshot then makepkg with no sha256/gpg verification. GitHub Actions are pinned to mutable tags (actions/checkout@v4). The only patch-adjacent code, release_get_latest (src/common.c:459-482), merely fetches a version string for a newer-version notice — no download/verify/apply.
  • Why it matters: IG1. Even when an upstream fix exists, nothing surfaces or applies it, and floating-HEAD/:latest/:testing sources make builds non-reproducible — the same provenance gap as REQ-SUP-02.
  • Remediation: Add OSV-Scanner/Dependabot/Renovate on push + cron, failing above a severity threshold (REQ-SUP-04); digest-pin Dockerfile FROMs, commit-pin clones, sha256/gpg-verify the AUR snapshot (REQ-SUP-02); SHA-pin Actions (REQ-SUP-05). Captured as REQ-CIS-04.

3.4 — CIS-2.6 / 16.5 — Dependencies not pinned/allowlisted to an authorized identity — Partial (IG2)

  • Evidence: System deps are bounded only by a minimum-version lower bound (no upper bound, no exact pin, no checksum). The one wrapped dependency, libstrophe, is not pinned: subprojects/libstrophe.wrap:3 revision = HEAD with no 40-hex SHA or source_hash — confirmed verbatim — so any clone links whatever upstream HEAD is. Two crypto deps are not version-gated at all in autotools: gpgme via bare AC_CHECK_LIB([gpgme],[main]) (configure.ac:309) and libgcrypt via a symbol-presence probe AC_CHECK_LIB([gcrypt],[gcry_md_extract]) (:351). At runtime, dlopen'd plugins are directory-restricted (0700 dir) but not signature/checksum-verified, and /plugins install accepts http and reuses a downloader that disables TLS verify under tls_policy=trust.
  • Why it matters: IG2. Neither the build-time library set nor the runtime-loaded code is pinned to an integrity-verified identity; an attacker influencing the wrap source, a CI mirror, or a plugin download gets unauthorized code linked/loaded.
  • Remediation: Pin libstrophe.wrap to an immutable SHA/tag+hash and lint against revision = HEAD (REQ-SUP-01); versioned PKG_CHECK_MODULES for gpgme + a gcrypt floor (REQ-SUP-03); checksum/signature-verify plugins before dlopen (REQ-EXT-04). Captured as REQ-CIS-03.

3.5 — CIS-8.5 — Detailed logs capture decrypted plaintext (and, at DEBUG, credentials) — Partial, critical-priority (IG2)

  • Evidence: In the default PREF_DBLOG="on" mode, database_sqlite.c:694 embeds message->plain (LMC sender mismatch), :717 ...content: %s (duplicate stanza-id), :748 log_debug("Writing to DB. Query: %s") logs the full INSERT that embeds message->plain (bound at :735), and :760 ...content: %s on insert failure. Triggers at :694/:717 are remotely influenceable (XEP-0308 LMC, server-assigned stanza-id). When strophe verbosity/DEBUG is raised, _xmpp_file_logger (connection.c:1088-1107) forwards raw SASL <auth>/<challenge>/<response> and XEP-0077 registration credentials with no redaction; the stderr bridge (log.c:269-300) also redacts nothing.
  • Why it matters: IG2, but the highest-severity CIS finding for an E2EE client: the "detailed" tier captures decrypted bodies in the shipped default, defeating E2EE for anyone who shares a log. Cross-validates REQ-DAR-03/REQ-LOG-03.
  • Remediation: Drop message->plain/content:/Message: from the format args at :694/:717/:760 (log JID/id/size/enc-type only); log the row id, not the INSERT, at :748; redact <auth>/<challenge>/<response>/jabber:iq:register in _xmpp_file_logger; add a CI grep blocking log_* interpolation of message->plain/body/password/api_key/Bearer.

3.6 — CIS-15.4 — No enforced security baseline toward configurable providers (AI http://) — Partial, high (IG2)

  • Evidence: /ai set provider <name> <url> (cmd_funcs.c:10752-10757) and ai_add_provider (ai_client.c:574) store api_url verbatim with no scheme validation, so http:// is accepted; both AI curl sites (ai_client.c:811-815, 1596-1601) set neither CURLOPT_SSL_VERIFYPEER nor VERIFYHOST (rely on libcurl defaults), while Authorization: Bearer <key> plus the full conversation is POSTed. The XMPP allow policy sets neither MANDATORY_TLS nor DISABLE_TLS (connection.c:185-194) — silent opportunistic STARTTLS. libstrophe is consumed at mutable HEAD (no integrity requirement on that supplier).
  • Why it matters: IG2. A user can configure a provider that receives the API key and all message content over cleartext HTTP. Cross-validates REQ-EXT-01 (AI http://) and REQ-CRY-03 (allow-policy downgrade).
  • Remediation: Reject non-https api_url at provider-add and request time and set VERIFYPEER=1L/VERIFYHOST=2L explicitly (REQ-EXT-01); map allow to a TLS-required-or-fail floor (REQ-CRY-03); pin the libstrophe wrap (REQ-SUP-01). Captured as REQ-CIS-08.

3.7 — CIS-8.2 — Two security events fall below the default WARN filter — Partial, high (IG1)

  • Evidence: The app log is collected (src/log.c _log_msg, default WARN, 0600 at :177, size rotation, stderr bridge). But SASL/login failure logs at log_info ("Login failed", server_events.c:208) and TLS certificate-validation failure (sv_ev_certfail, server_events.c:1135) emits no log_* at all (UI cons_show only) — both below the shipped WARN filter, so they are effectively not collected at the default level. Cross-validates REQ-LOG-01/REQ-LOG-02.
  • Remediation: Promote SASL-failure to log_warning (server_events.c:208); add a log_warning in sv_ev_certfail before the prompt; add WARN lines on DISABLE_TLS/non-mandatory-TLS/see-other-host (REQ-LOG-02).

3.8 — CIS-2.2 — Authorized software not assured current/supported — Partial, high (IG1)

  • Evidence: Floors are declared and the build hard-fails below them for the required deps, but gpgme and libgcrypt have no version floor in autotools (any/EOL accepted), the libstrophe wrap floats to HEAD with no supported pinned release, floors carry no support/EOL rationale, and there is no automated "is this version still maintained?" check.
  • Remediation: Versioned PKG_CHECK_MODULES([gpgme], …) + a gcrypt floor with CVE/support rationale; pin libstrophe to a supported tag/commit (REQ-SUP-01); add a CI EOL/CVE scan (REQ-SUP-04). Cross-validates REQ-SUP-03.

3.9 — CIS-8.1 — No audit-log-management / logging policy — Gap, medium (IG1)

  • Evidence: compliance/ holds only the catalog + source analysis; docs/ holds only man pages; no doc defines which security events SHALL be captured, at what level, retention, or review. Mechanics exist (_should_log level filter, size rotation, default WARN) but per-event decisions are inconsistent (see 3.7: cert-fail/SASL-fail below WARN, plaintext bodies logged by default).
  • Remediation: Add REQ-CIS-05 — a short logging policy enumerating WARN+ security events, the no-secrets/no-plaintext-body constraint (REQ-DAR-03), default level (REQ-LOG-04), rotation bounds (REQ-LOG-05), 0600 mode (REQ-AUTH-01), plus a CI grep on plaintext/secret interpolation.

3.10 — CIS-7.2 — Remediation capability exists but is ungoverned — Partial, high (IG1)

  • Evidence: cproof does remediate (CHANGELOG:1018 fixes CVE-2017-5592; HEAD 622054fc6 fix(pgp): prevent plaintext fallback), but no prioritization/SLA is documented, the HEAD-pinned libstrophe gives no immutable baseline to remediate to (REQ-SUP-01), and RELEASE_GUIDE.md has no security/CVE step (grep for security|cve|vuln|scan|patch returns nothing).
  • Remediation: REQ-CIS-02: severity-keyed remediation SLA, a per-fix regression test, a per-release Security/CVE changelog convention (REQ-VUL-04), and SHA-pinned libstrophe (REQ-SUP-01).

3.11 — CIS-18.1 / 16.13 — No penetration-testing / fuzzing program; sanitizers unused in CI — Gap, medium (IG2/IG3)

  • Evidence: Tree-wide grep for fuzz|pentest|oss-fuzz|libfuzzer|afl matches only the compliance docs; no */fuzz*//*/corpus*/ files exist. Only defensive testing is present: scan-build + check-cwe134.sh, cmocka under Valgrind, coverage. The --enable-sanitizers ASan/UBSan build exists (configure.ac) but is never run in CI (cross-validates REQ-MEM-02). No fuzz harness over the untrusted-network parsers (stanza/XML, jid_create, OMEMO/PGP/OX decode) — cross-validates REQ-SDLC-05 and explains why DoS findings like the remote NULL-deref (REQ-INP-01, message.c/presence.c) are argued from code, not demonstrated.
  • Remediation: REQ-CIS-04: stand up a libFuzzer/AFL++ target over the parse paths in a scheduled CI job; wire --enable-sanitizers && make check as a non-continue-on-error job; document a proportionate per-release adversarial-review checklist. A full external network pen-test (18.1 as literally written) is N/A — no estate.

CIS 4. What cproof does well (CIS)

  • CIS-16.11 — Leverage vetted modules for security components. All E2EE/crypto is delegated to established libraries (libsignal-protocol-c/libomemo-c double ratchet, libgcrypt CSPRNG + AEAD, gpgme, libotr); cproof implements no home-grown KDF/ratchet (REQ-CRY-06/REQ-CRY-07/REQ-CRY-08, Met).
  • CIS-4.6 / 15.4 / 3.10-equiv — TLS-in-transit mandatory + fail-closed. NULL/empty/force tls.policy sets XMPP_CONN_FLAG_MANDATORY_TLS (connection.c:185-186); insecure flags reachable only via explicit policy strings; tls.policy allowlist-validated on load with secure fallback (common.c:406); certfail handler always registered, defaulting to abort (REQ-CRY-03/REQ-CRY-04/REQ-CRY-05, Met-default).
  • CIS-4.1 — Secure-by-default configuration. OMEMO trust manual not blind (preferences.c:2789-2790), PREF_ENC_WARN/PREF_TLS_SHOW default TRUE (:2663-2706), force-encryption resend-to-confirm default (REQ-CRY-10/REQ-CFG-02).
  • CIS-4.8 — Disable unnecessary services (Met). Compile-time toggles for every risk-bearing subsystem (--enable-otr/pgp/omemo, --enable-c-plugins/python-plugins/plugins, --enable-notifications); AI egress disabled by default (empty PREF_AI_API_KEY, no network call); plugins never auto-load and load only from the 0700 dir (REQ-EXT-02/REQ-EXT-04).
  • CIS-16.12 (slice) — Code-level secure-coding conventions. Every variadic wrapper carries G_GNUC_PRINTF; all untrusted classes are %s-wrapped; check-cwe134.sh exits 0; nine auto_* cleanup macros are CI-asserted (REQ-INP-03/REQ-MEM-06, Met).
  • CIS-16.8 — Production/non-production separation. Functional tests run against the stabber mock with isolated XDG dirs and @localhost identities; no real credentials in fixtures (REQ-SDLC-02).
  • CIS-9.3/9.6 (mitigations) — No shell injection on the URL/file surface. URLs reach external helpers only as discrete argv tokens via format_call_external_argv + call_external (g_spawn_async, never a shell), and peer download filenames collapse path traversal via g_file_get_basename (REQ-INP-06/REQ-CFG-01) — so the residual risk is scheme/file-type abuse, not RCE.

CIS 5. Not applicable (enterprise-IT) controls

  • CIS-1 Inventory and Control of Enterprise Assets — no hardware/device fleet to discover or control.
  • CIS-3 Data Protection (org data-classification/DLP) — client-side at-rest is covered by REQ-AUTH-01/REQ-AUTH-02, REQ-DAR-01..07; org-wide classification/DLP is N/A.
  • CIS-5.4 / 6.3 — no administrator-account model; no inbound externally-exposed application (outbound SASL client; MFA is a server/SASL concern). Adjacent posture: REQ-AUTH-03/REQ-CRY-03.
  • CIS-8.6/8.7/8.8/8.9/8.10/8.11/8.12 — DNS/URL/command-line/centralized/retention/review/service-provider log safeguards target network/SIEM/SOC infrastructure a client does not operate.
  • CIS-10 Malware Defenses — host-OS AV/EDR responsibility.
  • CIS-11 Data Recovery — enterprise backup program; the client analogue is migration integrity (REQ-RES-01/REQ-RES-02).
  • CIS-12 Network Infrastructure Management / CIS-13 Network Monitoring and Defense — no owned network infrastructure or monitoring estate.
  • CIS-14 Security Awareness Training — org HR program; developer secure-coding maps to CIS-16.9 ↔ REQ-SDLC-04/REQ-SDLC-06.
  • CIS-17 Incident Response Management — org IR program; in-repo analogue is vuln handling, CIS-16.2/7.1 ↔ REQ-VUL-01 / REQ-CIS-02.
  • CIS-18.2/18.4/18.5 — external estate pen-test operations; the applicable application slice (18.1/18.3, 16.13) is handled via REQ-CIS-04 / REQ-SDLC-05 / REQ-MEM-02.

CIS 6. Methodology & limitations

  • Scope: Static source analysis of master at HEAD 622054fc6, read directly from the working tree, against the official CIS v8.1 control/safeguard structure. Each applicable safeguard was assessed by a control auditor and adversarially verified; verifier-corrected statuses are reported.
  • Source caveat: The CIS v8.1 PDF originally linked returned HTTP 404; the authoritative CIS v8.1 control numbers, safeguard numbers/titles, and IG1/IG2/IG3 tags were taken from the official CIS Controls v8.1 structure (https://www.cisecurity.org/controls/v8-1) and used verbatim — no safeguard numbers were invented.
  • Reinterpretation: CIS is an enterprise-IT framework; many safeguards were reinterpreted for a single-user client + its repository (e.g. 2.6 "allowlist authorized libraries" → build/runtime dependency pinning; 8.x "audit logs" → the application log's content/level/policy; 15.4 "service-provider contracts" → the technical security baseline enforced toward each provider). Enterprise-only controls are summarized as N/A.
  • Not a pentest: No dynamic exploitation, fuzzing, or runtime instrumentation was performed — itself the substance of the CIS-18.1/16.13 gap. Reachability of DoS/leak paths is argued from code, not demonstrated.
  • Cross-validation: Every CIS finding was reconciled against the NIST CSF 2.0 / ISO 27001 source audit above; where CIS independently confirms an already-found gap (libstrophe revision = HEAD, plaintext in logs, no CI security scanning, contact-only SECURITY.md, AI http:// egress), the existing REQ-ID is cited rather than re-derived.

Part 2 — Requirements catalog

This catalog defines the software security requirements for cproof. Each requirement is rendered as its own heading whose text is the bare ID, so the auto-anchor is exactly the lowercased id; the bold title, the SHALL statement, and the italic traceability line follow.

1. Scope and Introduction

This catalog defines the software security requirements for cproof, a fork of the Profanity console XMPP/Jabber chat client (~90k LOC, C, autotools build). Requirements are derived from and traced to NIST CSF 2.0 (Govern/Identify/Protect/Detect/Respond/Recover Categories and Subcategories) and ISO/IEC 27001:2022 Annex A controls. The target system is a desktop, single-user end-to-end-encrypted (E2EE) chat client offering OMEMO, OTR, OpenPGP (XEP-0027) and OX (XEP-0373), transported over XMPP/TLS via libstrophe, with a local SQLite/flat-file history store, plaintext GKeyFile account config, C/Python plugin extensibility, and an outbound AI/LLM client module (src/ai). The assumed threat model is: an active or passive network/server adversary (MitM, downgrade, key injection, stanza spoofing, server-side archive); a hostile peer sending attacker-controlled XML/stanzas, message bodies, MUC fields, URLs and file transfers; a malicious or compromised LLM provider or update endpoint; other local users on a shared host reading at-rest secrets; and a compromised supply chain (dependencies, CI, wrapped subprojects, plugins). In scope are the client's crypto correctness/enforcement, transport security, input validation, memory safety, data-at-rest protection, logging/detection, secure defaults, supply-chain and vulnerability management, extension/external-interface security, resilience, and the secure-development lifecycle of the repository itself. Explicitly out of scope are purely physical, HR, and datacenter/organizational-estate controls (there is no central server or IT estate); these are enumerated at the end. A subsequent CIS Controls v8.1 section (after the traceability summary) maps these requirements to CIS safeguards across Implementation Groups IG1–IG3 and adds CIS-specific requirements (REQ-CIS-01REQ-CIS-08).

2. How to read this

  • IDs are stable and domain-prefixed: REQ-<DOMAIN>-NN (CRY cryptography, AUTH authentication/access/secrets, INP input validation, MEM memory safety, DAR data-at-rest/privacy, LOG logging/detection, CFG secure config/defaults, SUP supply chain, VUL vulnerability/patch/change-control, EXT extension/external interfaces, RES resilience/fail-safe, SDLC secure-development lifecycle/governance). IDs are intended to be cited from source-code audit findings and never renumbered.
  • Priority is one of critical / high / medium / low, reflecting blast radius for an E2EE client (a plaintext leak or MitM bypass is critical; a hardening or documentation gap is medium/low).
  • Each requirement carries an italic traceability line: NIST | ISO | Priority | Verify. The verification text is written to be concretely checkable against the cproof source tree.
  • Where one requirement satisfies a control raised by several domains, the duplicates are merged and the merge noted in the statement or applicability. The first appearance of a hardening/permissions/no-plaintext-log control is canonical; later domains reference it.
  • "SHALL" denotes a mandatory requirement; "SHOULD" a strong recommendation that may be risk-accepted with justification.

3. Requirements

REQ-CRY — Cryptography & Confidentiality

REQ-CRY-01

Fail-closed E2EE: no plaintext fallback on encryption failure

When a chat or MUC window has an active encryption mode (OMEMO, OTR, PGP XEP-0027, OX XEP-0373), cproof SHALL NOT transmit the message body in cleartext if encryption fails, the recipient key/session is missing, the sender key is missing, the crypto context cannot be created, or the crypto backend is unavailable (HAVE_LIBGPGME/HAVE_OMEMO undefined); every such path SHALL abort the send, surface a specific visible error in the relevant window, and return NULL/failure so the caller skips network send, history logging and on-screen echo. (Merges REQ-RES-01; satisfies the same E2EE-integrity control from the Cryptography and Resilience domains.)

NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.8.26 | Priority: critical | Verify: regression/integration tests forcing each failure path (deleted recipient key, unref'd sender key, induced gpgme error, empty OMEMO device_list, backend compiled out) assert no <body> cleartext stanza is emitted and the send returns NULL; grep that p_gpg_encrypt/OMEMO/OX encrypt failure branches never reach message_send_chat/_send_message_stanza; confirm cl_ev_send_msg_correct echoes/logs only when the returned id is non-NULL (guards the 622054fc6 fix).

REQ-CRY-02

Mandatory force-encryption policy as last-line plaintext gate

For conversations without an active E2EE session, cproof SHALL consult allow_unencrypted_message() before any cleartext send; under PREF_FORCE_ENCRYPTION mode block it SHALL refuse with no per-message opt-out, under resend-to-confirm it SHALL require an explicit second user action, and any unrecognized/empty mode SHALL fail closed (block) rather than fail open. (Merges REQ-RES-02.)

NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.5.10 | Priority: critical | Verify: unit-test allow_unencrypted_message() returns FALSE for block; for resend-to-confirm first Enter blocks and an identical re-send proceeds; an invalid/empty mode returns FALSE; confirm the final fallthrough returns FALSE.

REQ-CRY-03

TLS mandatory by default; no silent STARTTLS downgrade

For every account, cproof SHALL map an absent/empty/force tls.policy to XMPP_CONN_FLAG_MANDATORY_TLS, SHALL set XMPP_CONN_FLAG_DISABLE_TLS only when the user explicitly sets tls.policy=disable, and SHALL NOT proceed with SASL authentication or message traffic over a connection that failed to upgrade to TLS under any non-disabled policy. (Merges REQ-AUTH-03 transport half, REQ-CFG-01.)

NIST: PR.DS-02, PR.PS-01 | ISO: A.8.20, A.8.9, A.8.24 | Priority: critical | Verify: unit-test _conn_apply_settings flag computation in src/xmpp/connection.c: NULL/force/trust set MANDATORY_TLS and exclude DISABLE_TLS/LEGACY_SSL; disable sets DISABLE_TLS only on explicit config; integration test against a server advertising no STARTTLS aborts under default policy; confirm no auth/send occurs before TLS established.

REQ-CRY-04

TLS certificate validation enforced; trust-bypass is explicit and fails closed

cproof SHALL perform full X.509 chain and hostname validation for XMPP TLS by default, SHALL set XMPP_CONN_FLAG_TRUST_TLS (blind trust) only when tls.policy=trust is explicitly chosen, SHALL always register the certfail handler, and on validation failure SHALL default to abort — accepting a cert only via explicit interactive /tls allow (session) or /tls always (persist by SHA-256 fingerprint via tlscerts/cafile), and rejecting on /tls deny or EOF. (Merges REQ-CFG-02, REQ-CFG-03.)

NIST: PR.AA-03, DE.CM-09 | ISO: A.8.21, A.8.9, A.8.24 | Priority: critical | Verify: test only literal policy trust yields TRUST_TLS and no default/auto-fallback does; test _connection_certfail_cb/sv_ev_certfail returns accept only for explicit allow/always and abort (0) for deny/EOF; assert tlscerts_exists/fingerprint-equality gate silent re-acceptance; negative test: untrusted self-signed cert under default policy must not connect.

REQ-CRY-05

Per-account tls.policy validated against an allowlist on load

cproof SHALL validate each stored tls.policy against the known allowlist (force, trust, disable, direct/legacy) via valid_tls_policy_option on load, and SHALL discard an unrecognized value by falling back to the secure default (force/MANDATORY_TLS) rather than passing an unknown string to the connection layer.

NIST: PR.PS-01, PR.DS-02 | ISO: A.8.9 | Priority: high | Verify: test that loading an account with tls.policy="bogus" yields tls_policy=NULL (interpreted as force) and that valid_tls_policy_option rejects out-of-allowlist values.

REQ-CRY-06

Approved cipher/key-length and mandatory AEAD tag verification for OMEMO

OMEMO message and file encryption SHALL use only AES in an authenticated/standardized mode at ≥128-bit strength (AES-256-GCM for payload/file; AES-128-GCM and AES-CBC-PKCS5 only where the OMEMO/Signal spec requires), SHALL reject unknown cipher/key-length selectors (switch default returns error, no silent weaker substitution), and SHALL call gcry_cipher_checktag on every decrypt and abort with no plaintext output on tag mismatch.

NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: review the key_len switch in src/omemo/crypto.c handles only sanctioned lengths with an erroring default (no fall-through); confirm gcry_cipher_checktag failure aborts decrypt; add a tampered-tag test asserting decrypt fails and emits no plaintext.

REQ-CRY-07

CSPRNG for all key/nonce/IV/identifier generation

All key material, IVs/nonces, and identifiers SHALL be drawn from libgcrypt at GCRY_VERY_STRONG_RANDOM (gcry_randomize/gcry_random_bytes_secure); libgcrypt secure-memory init and GCRYCTL_INITIALIZATION_FINISHED SHALL complete (and be checked via GCRYCTL_INITIALIZATION_FINISHED_P) before any keygen; no predictable source (rand()/random()/g_random_*) SHALL be used for crypto material.

NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: confirm omemo_random_func uses VERY_STRONG_RANDOM; verify init-finished is set before keygen and checked; grep crypto/omemo/otr/pgp sources for rand()/random()/g_random_ used as key/IV/nonce and assert none.

REQ-CRY-08

Forward-secret ratcheting enforced; one-time prekeys consumed

OMEMO sessions SHALL use the libsignal/libomemo-c double ratchet and OTR the libotr AKE/ratchet; cproof SHALL NOT implement its own session KDF/ratchet, and consumed one-time prekeys SHALL be removed from the prekey store (remove_pre_key) after use so that long-term identity-key compromise does not expose past messages.

NIST: PR.DS-02 | ISO: A.8.24 | Priority: high | Verify: confirm session crypto is delegated to the libraries (no custom KDF/ratchet); verify remove_pre_key is wired and invoked on session establishment; review signed-prekey rotation; integration test that a replayed prekey bundle does not reproduce a usable session key.

REQ-CRY-09

TOFU trust model: explicit, persisted fingerprint verification before trusted send

cproof SHALL bind peer identity to cryptographic fingerprints (OMEMO identity-key fingerprints, OTR fingerprints, PGP key IDs), distinguish untrusted/blind/verified states, require an explicit user action (/omemo trust, /otr fingerprint/SMP, /pgp trust) to mark a fingerprint verified, persist trust decisions, and treat a changed peer identity key as untrusted rather than auto-accepting it. Trust transitions SHALL be driven only by user commands, never by inbound-stanza handlers. (Merges REQ-AUTH-07.)

NIST: PR.AA-03, PR.AA-04 | ISO: A.5.31, A.5.17 | Priority: high | Verify: unit-test is_trusted_identity returns untrusted for unknown/changed keys; verify trust-store round-trips and a new identity key for a known JID does not auto-trust; simulate an inbound message from an unverified peer and assert identity stays untrusted until a verify command runs.

REQ-CRY-10

Fail-safe OMEMO trust on send and receive

On send, cproof SHALL omit untrusted OMEMO recipient devices and SHALL abort the send (with an explanatory untrusted-fingerprint listing) when no trusted device remains, never silently encrypting to an unverified device; on receive, a message from an untrusted identity SHALL be flagged untrusted to the user rather than displayed as trusted. The default PREF_OMEMO_TRUST_MODE SHALL be manual (not blind); blind/firstusage SHALL surface a visible auto-trust notice. (Merges REQ-CFG-07, REQ-RES-06.)

NIST: PR.AA-03, PR.DS-02 | ISO: A.8.24, A.8.9, A.5.17 | Priority: critical | Verify: test prefs_get_string(PREF_OMEMO_TRUST_MODE) default == manual; test that with all recipient devices untrusted no stanza is sent and the untrusted-fingerprint guidance shows (omemo_on_message_send keys==NULL branch); test receiving from an untrusted identity sets the trusted flag FALSE and marks the UI; confirm blind/firstusage emit a cons_show notice.

REQ-AUTH — Authentication, Access Control & Secret Management

REQ-AUTH-01

Owner-only (0600) permissions for every secret/credential/trust/history file

cproof SHALL create and re-assert mode 0600 (S_IRUSR|S_IWUSR) on every file holding secrets or security-critical state — accounts keyfile (passwords/eval_password), OMEMO identity.txt/trust.txt/sessions.txt/known_devices.txt, OTR keys.txt/fingerprints.txt, PGP material, TLS pinned certs (tlscerts), the SQLite history DB and flat-file backend, history export files, and the main log — both on creation and on every save, regardless of pre-existing mode. Files written by third-party libraries (e.g. libotr otrl_privkey_write_fingerprints) SHALL get an explicit chmod afterward. (Canonical permissions requirement; merges REQ-CRY perms, REQ-DAR-01, REQ-DAR-02, REQ-CFG-09, REQ-LOG-03.)

NIST: PR.DS-01, PR.AA-01, PR.AA-05 | ISO: A.8.3, A.8.10, A.5.17 | Priority: critical | Verify: static — every fopen/g_creat/g_key_file_save_to_file/sqlite3_open for a secret/history path is followed by g_chmod/fchmod(...,S_IRUSR|S_IWUSR) with no S_IRGRP/S_IROTH; the SQLite sqlite3_open in _sqlite_init (src/database_sqlite.c) currently lacks this — close the gap. Runtime — under umask 0, init account/OMEMO/OTR/DB/export, stat() each and assert (mode & 0777)==0600. Template: database_flatfile_verify.c / database_export.c fchmod.

REQ-AUTH-02

Owner-only (0700) permissions for all secret-bearing directories

cproof SHALL create every directory under the data/config root that contains secret material or loadable code (per-account OTR/OMEMO/PGP subdirs, plugins dir) with mode 0700 (S_IRWXU) via the central create_dir() helper and SHALL NOT widen them, so secret files are not enumerable or subject to TOCTOU by other local users.

NIST: PR.AA-05, PR.DS-01 | ISO: A.8.3, A.8.2 | Priority: high | Verify: confirm create_dir() uses S_IRWXU and all account/OTR/OMEMO/PGP/plugins dir creation routes through it (no raw mkdir/g_mkdir with 0755); runtime stat() asserts (mode & 0777)==0700.

REQ-AUTH-03

Insecure transport/auth policies are explicit opt-in and logged

cproof SHALL default to MANDATORY_TLS for SASL credential exchange and SHALL enable DISABLE_TLS, TRUST_TLS, LEGACY_SSL, or LEGACY_AUTH only from explicit per-account tls.policy/auth.policy strings, logging a WARN-level diagnostic whenever a credential-exposing flag is active; none SHALL be on by default. (Complements REQ-CRY-03/04; merges REQ-CFG-12.)

NIST: PR.AA-03, PR.DS-02 | ISO: A.5.17, A.8.5, A.8.9, A.8.20 | Priority: critical | Verify: confirm in _conn_apply_settings the NULL/force branch sets MANDATORY_TLS and that DISABLE_TLS/TRUST_TLS/LEGACY_ are reachable only via explicit policy strings; test that default flags contain neither LEGACY_SSL nor LEGACY_AUTH; drive with disable/legacy and assert a warning is logged (LOG_FLAG_IF_SET path).*

REQ-AUTH-04

Account passwords are opt-in at rest; eval_password retrieval is the recommended path

cproof SHALL NOT write the account password to the accounts keyfile unless the user explicitly stored it, SHALL support non-persistent retrieval at connect time via eval_password (e.g. a system keyring), and SHALL remove the persisted password from the keyfile (g_key_file_remove_key + save) on /account clear ... password. (Merges REQ-DAR-08 clear-on-disk half.)

NIST: PR.DS-01, PR.AA-01 | ISO: A.5.17, A.8.3 | Priority: medium | Verify: configure an account with only eval_password set and assert no password key is written yet login works; after /account clear password assert the key is absent from the keyfile.

REQ-AUTH-05

eval_password runs without a shell and without ambiguous PATH resolution

cproof SHALL execute the eval_password command via direct argv spawn (g_shell_parse_argv + g_spawn_sync, never system()/popen()//bin/sh -c), SHALL treat a non-zero exit or empty output as authentication failure (never falling back to an empty password), and SHOULD avoid G_SPAWN_SEARCH_PATH so the helper binary is unambiguous and not hijackable via a poisoned PATH.

NIST: PR.AA-01, PR.AA-05 | ISO: A.8.2, A.5.17 | Priority: high | Verify: confirm no shell is used in account_eval_password and empty/failed output returns FALSE (no empty password to xmpp_conn_set_pass); review G_SPAWN_SEARCH_PATH; stub a helper returning non-zero/empty and assert FALSE and account->password stays NULL.

REQ-AUTH-06

Secrets zeroized with a non-elidable wipe before free; key buffers protected in memory

cproof SHALL overwrite, with a compiler-non-elidable routine (explicit_bzero/gcry_free of secure memory/signal_buffer_bzero_free), the buffers holding account passwords, eval_password output, reconnect-saved password copies, PGP passphrases, and OMEMO/OTR/PGP private-key/plaintext-key material immediately before release, and SHALL use libgcrypt secure memory for OMEMO key operations; ordinary free()/g_free() of a secret without a preceding wipe is non-conformant. (Merges REQ-MEM-12, REQ-DAR-08 in-memory half; OMEMO signal_buffer_bzero_free/gcry_free is the reference pattern.)

NIST: PR.DS-01, PR.AA-01, PR.PS-01 | ISO: A.5.17, A.8.3, A.8.24 | Priority: high | Verify: grep that every free/g_free of a password/passphrase/private-key buffer (src/config/account.c, src/xmpp/session.c saved_account.passwd/saved_details.passwd, src/pgp/gpg.c, src/omemo/*) is preceded by a non-elidable wipe over its length; teardown unit test asserts key buffers are zero after *_destroy; optional heap-scan-after-logout test asserts the password byte pattern is absent.

REQ-INP — Input Validation & Untrusted-Data Handling

REQ-INP-01

Validate every peer-supplied JID and fail safe on parse failure

cproof SHALL parse every JID from an untrusted source (stanza from/to, MUC item jids, roster pushes, carbons/MAM by, vCard/avatar refs) via jid_create()/jid_is_valid() and SHALL treat a NULL return as a hard parse failure — abandoning the stanza — rather than dereferencing Jid* or any part. Confirmed defect to fix: _should_ignore_based_on_silence() in src/xmpp/message.c reads from_jid->barejid after jid_create(from) with no NULL guard (remote NULL-deref DoS).

NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: grep all jid_create( call sites and assert each guards NULL before field access; unit-test test_jid.c with NULL/empty/a@@b/non-UTF-8/over-3071-char input asserting NULL; regression test driving _should_ignore_based_on_silence with a stanza lacking from asserts no crash; run under ASan.

REQ-INP-02

Enforce RFC 6122 length and character bounds in JID validation

jid_is_valid() SHALL reject any JID exceeding JID_MAX_TOTAL_LEN (3071) total or JID_MAX_PART_LEN (1023) per part, any localpart containing RFC 6122 forbidden chars (space " & ' / : < > @), more than one @ in the bare portion, empty localpart when @ is present, empty domainpart, or input failing g_utf8_validate().

NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: table-driven test_jid.c covering boundary lengths (1023/1024 per part, 3071/3072 total), each forbidden char, double-@, missing localpart/domain, invalid UTF-8; CI fails on any mis-classification.

REQ-INP-03

Never pass untrusted data as a printf/format-string argument (CWE-134)

cproof SHALL NOT pass any peer- or user-controlled string (message body/subject, nickname, JID, MUC status, URL, filename, presence status, library error text) as the format-string parameter of cons_show*, win_print*/win_println*, log_*, g_strdup_printf, or any printf-family/varargs function; such data SHALL appear only as a %s argument. Every variadic printf-like wrapper SHALL carry G_GNUC_PRINTF/__attribute__((format(printf,N,M))). (Merges REQ-MEM-09; data crossing the plugin/AI boundary (REQ-EXT-04) is covered here too.)

NIST: PR.PS-06, PR.PS-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: build with -Wformat=2 -Werror=format-security -Werror=format-nonliteral; run ./check-cwe134.sh src and require exit 0; grep cons_show(/win_print( whose first arg is a bare variable; fuzz/unit test feeding %-bearing stanza/plugin/AI content through a display sink.

REQ-INP-04

Build promotes format-security diagnostics to errors

The build SHALL set -Wformat=2 and promote format diagnostics to errors (-Werror=format-security, and -Werror=format-nonliteral where supported) for cproof translation units so any CWE-134 regression fails the build; the check-cwe134.sh audit SHALL run in CI as a blocking (not continue-on-error) step.

NIST: PR.PS-06, PR.PS-01 | ISO: A.8.28, A.8.25, A.8.29 | Priority: high | Verify: inspect configure.ac AM_CFLAGS for -Werror=format-security; confirm the CWE-134 CI step is blocking; introduce a deliberate non-literal format and confirm the build/CI fails.

REQ-INP-05

Gate XEP-0359 stanza-id/origin-id trust on the asserting entity's disco support

When consuming a <stanza-id by='…'> or MAM <result> id, cproof SHALL honour it only when by equals the user's own bare JID or the message's authoritative archive (MUC bare JID or account domain) AND that entity advertises urn:xmpp:sid:0 via disco; ids from any other by SHALL be ignored. Ownership of an outgoing message SHALL be established by verifying the HMAC in origin-id (message_is_sent_by_us), not by id-string match.

NIST: PR.DS-02, PR.AA-03 | ISO: A.8.26, A.8.28 | Priority: high | Verify: test a <stanza-id by='attacker@evil'> from a different sender is not trusted (_stanza_id_by_trusted); toggle urn:xmpp:sid:0 in features and assert the gate; assert message_is_sent_by_us rejects a valid-length prefix with a wrong HMAC.

REQ-INP-06

Validate URL scheme and pass URLs/filenames as argv, never to a shell

Before acting on a peer/user-supplied URL (/url open, /url save, OOB/file-transfer refs), cproof SHALL whitelist the scheme via g_uri_parse_scheme() (only http, https, aesgcm) and SHALL invoke any external open/download helper by building an explicit argv vector (format_call_external_argv + call_external/g_spawn, no shell), so URL/filename content can never be interpreted as shell metacharacters or extra args. (Merges REQ-EXT-05 URL half.)

NIST: PR.PS-06, PR.PS-05, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: grep system(/popen(/sh -c/g_spawn_*sync over src/ and confirm none operate on URL input; unit-test scheme handling rejects file:/javascript:/data:; test a URL with spaces/quotes/; reaches the helper as one argv element.

REQ-INP-07

No shell-string desktop notification command injection

Desktop notification invocation SHALL NOT construct a shell command string from message-derived content for system(); it SHALL use a libnotify/g_spawn argv API or rigorously g_shell_quote every interpolated value, treating message/sender content as untrusted data. Defect to remediate: src/ui/notifier.c builds a notify string and runs it via system().

NIST: PR.PS-05 | ISO: A.8.26, A.8.28 | Priority: high | Verify: grep notifier.c for system(/popen(; confirm migration to argv g_spawn or full quoting; test a sender/message with quotes, $(), backticks, ; spawns no extra process.

REQ-INP-08

Bound and sanitize peer-controlled text before display, logging and storage

cproof SHALL treat decrypted bodies, subjects, MUC topics, presence status, nicknames and resource parts as untrusted and SHALL neutralize terminal-control and Unicode bidi-override sequences (e.g. ESC/CSI, U+202E) and enforce a sane maximum rendered length before writing to the ncurses UI, log files, or the SQLite/flat-file backend, so peer content cannot inject terminal escapes, spoof UI via bidi, or corrupt log/DB output.

NIST: PR.DS-01, PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test a body containing raw ESC[2J, a NUL, and U+202E asserting the stored/displayed form is sanitized/escaped; review the display sink for a control-char filter and a length cap on rendered peer strings.

REQ-INP-09

Strict structural validation of incoming stanzas; cap MAM result sets

cproof SHALL guard every stanza consumer so that missing/malformed mandatory children/attributes (absent body/from, non-JID from, unexpected namespaces) cause the stanza to be skipped rather than NULL-dereferencing the result of xmpp_stanza_get_child_by_name/get_attribute, SHALL not call abort()/assert() on attacker-influenced conditions, and SHALL cap MAM results requested/processed per query (MESSAGES_TO_RETRIEVE). (Merges REQ-MEM-08, REQ-RES-05.)

NIST: PR.PS-06, PR.DS-02, PR.IR-01 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: fuzz/replay malformed stanzas (missing body/from/jid, deeply nested, huge MAM batches, oversized base64 OMEMO key) into message.c/iq.c/presence.c/muc.c/omemo.c handlers under ASan/UBSan asserting clean rejection and no crash; code-review NULL checks after each xmpp_stanza_get_* on the receive path; confirm assert() in receive paths is debug-only and unreachable from peer data.

REQ-INP-10

Validate and escape data crossing into AI/LLM outbound requests and responses

Where src/ai forwards user/peer content into an outbound LLM request, it SHALL JSON-escape all such content via ai_json_escape() before embedding it, and SHALL parse provider responses with bounds-checked unescaping (_json_unescape_substring/_extract_json_string) so an embedded quote or control byte cannot break the JSON string context.

NIST: PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test ai_json_escape() with ", \, \n and control bytes asserting valid escaped output; test _extract_json_string() against a response value embedding an escaped quote; confirm every request-body assembly routes content through ai_json_escape().

REQ-MEM — Memory Safety & Secure Coding

REQ-MEM-01

Full compiler/linker hardening baseline in release builds, at parity across build systems

Every released cproof binary and plugin SHALL be compiled/linked with -fstack-protector-strong, -fno-common, -D_FORTIFY_SOURCE=2 (or 3 where supported), -Wformat=2, full RELRO and immediate binding (-Wl,-z,relro -Wl,-z,now), non-executable stack (-Wl,-z,noexecstack), and a position-independent executable (-fPIE -pie); the autotools and meson definitions SHALL stay at flag parity, and the build SHALL fail if any flag is silently dropped. Known gaps to close: configure.ac omits -fPIE/-pie; meson.build omits the RELRO/-z,now linker hardening present in autotools. (Canonical hardening requirement; merges REQ-CRY-12, REQ-AUTH-09, REQ-CFG-10, REQ-SUP-07, REQ-EXT-10, REQ-RES-10, REQ-SDLC-01, REQ-VUL-09.)

NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.27, A.8.28 | Priority: critical | Verify: grep configure.ac and meson.build for each flag and assert parity; on the built ./profanity run checksec/readelf -d/readelf -h and assert Full RELRO, BIND_NOW, NX, PIE (Type DYN), stack canary, FORTIFY symbols present (and CET/-fstack-clash-protection where the toolchain supports them); add a CI step that fails when any mitigation is missing on the produced binary and loaded .so plugins.

REQ-MEM-02

ASan + UBSan + unsigned-overflow build gates the test suite in CI

The project SHALL provide an --enable-sanitizers build (ASan + UBSan + -fsanitize=unsigned-integer-overflow, -fno-sanitize-recover=all) and SHALL run the full cmocka unit/functional suite under it in CI on every pull request, AND additionally run the suite under Valgrind with --error-exitcode=1 --leak-check=full; any sanitizer/Valgrind error or definite leak SHALL fail the build. (Merges REQ-SDLC-02, REQ-RES-10 sanitizer half.)

NIST: PR.PS-06, DE.CM-01, ID.RA-01 | ISO: A.8.28, A.8.29, A.8.25 | Priority: critical | Verify: confirm a non-continue-on-error CI job runs ./configure --enable-sanitizers && make && make check and that ci-build.sh invokes Valgrind with --error-exitcode=1; intentionally introduce a heap overflow in a test and confirm CI fails.

REQ-MEM-03

No unbounded/unsafe C string or format primitives on untrusted data

cproof SHALL NOT use strcpy, strcat, sprintf, gets, or unbounded scanf on data derived from stanzas, peer content, MUC, file-transfer/URL input, or remote JIDs; such code SHALL use length-bounded equivalents (g_strdup_printf, g_strlcpy, snprintf with checked return). Defects to remediate: strcpy/strcat on peer barejids in src/pgp/ox.c:153-156; sprintf in src/omemo/crypto.c:479/483 and src/xmpp/iq.c:1787.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: grep -rnE '\b(strcpy|strcat|sprintf|gets)\(' src/ and confirm each remaining hit operates only on fixed compile-time-constant strings; add a CI grep that fails on these calls in stanza/crypto-handling files.

REQ-MEM-04

Overflow-checked allocation sizes

Every malloc/realloc/g_malloc size that multiplies or adds attacker-influenced lengths SHALL be computed with overflow-checked arithmetic (__builtin_mul_overflow/__builtin_add_overflow or g_malloc_n-style helpers) and SHALL error/abort rather than allocate a truncated buffer; realloc results SHALL be checked before the old pointer is overwritten.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: review multiplicative/additive malloc/realloc args in src/tools/http_upload.c/http_download.c, src/common.c, src/xmpp/connection.c, src/omemo/omemo.c (identity_public_key_len*2+1, malloc(*length)) for a preceding overflow check; UBSan unsigned-overflow tests feeding near-SIZE_MAX lengths.

REQ-MEM-05

Underflow-guarded unsigned subtraction on sizes/offsets (per project unsigned policy)

cproof SHALL guard every subtraction of unsigned size/length/offset values that could go negative against wrap to ~SIZE_MAX, using pre-checks or builtins rather than casting an operand to signed, consistent with the project's unsigned-arithmetic policy.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: build with -fsanitize=unsigned-integer-overflow and run tests/fuzz over UI scroll/pad math (src/ui/inputwin.c documented SIZE_MAX guard) and parser/crypto buffer-slicing; code-review subtractions of size_t/guint operands for a lower-bound check; confirm no size_t is cast to int to mask a wrap.

REQ-MEM-06

Mandatory auto-cleanup macros for heap/FILE/fd ownership

Local owning pointers and resources SHALL use the project auto-cleanup attributes (auto_gchar, auto_char, auto_gcharv, auto_guchar, auto_FILE, auto_gfd, auto_gerror, auto_jid, auto_sqlite) instead of manual free/g_free/fclose/close on early-return paths, so ownership is released exactly once on every exit path.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: medium | Verify: code-review that new owning locals use auto_* rather than trailing frees; run make check under ASan/LeakSanitizer asserting no leaks/double-frees; grep changed handlers for free()/g_free() paired with early returns that should use auto_*.

REQ-MEM-07

Null freed pointers; document out-parameter ownership; no UAF on crypto buffers

After freeing a heap pointer that remains in scope or in a struct field, cproof SHALL set it to NULL; functions returning allocated memory via out-parameters SHALL define ownership; freed crypto/session/key buffers (gcry_free/free paths in src/omemo, src/otr, src/pgp) SHALL NOT be referenced after release.

NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: ASan/UBSan make check over crypto and session teardown; code-review each free of a still-live pointer is followed by =NULL; targeted tests double-invoking teardown to confirm no double-free.

REQ-MEM-08

NULL-checks on every peer/remote-derived parse/lookup before dereference

cproof SHALL validate for NULL the result of every parse/lookup that can fail (XMPP attribute/child lookups, JID parsing, base64/hex decode, GKeyFile reads, library returns) before dereferencing and SHALL handle absence as a recoverable error. (Closely related to REQ-INP-09; this is the codebase-wide secure-coding form.)

NIST: PR.PS-01, DE.CM-01 | ISO: A.8.28 | Priority: high | Verify: enable -Wnull-dereference and review warnings; run crafted-stanza/malformed-input tests under ASan asserting no NULL-deref crash; code-review each new xmpp_stanza_get_*/g_key_file_get_*/decode result for a NULL guard (cf. the autoping NULL-domain guard 15dfc2bd).

REQ-MEM-09

No alloca/VLA sized from untrusted lengths

cproof SHALL NOT size stack allocations (alloca or VLAs) from attacker-influenced lengths; such buffers SHALL be heap-allocated with checked sizes or bounded by a validated compile-time maximum. Defects to remediate: alloca((strlen(barejid)+6)) in src/pgp/ox.c:150-156; unsigned char out[mac_len] VLA in src/omemo/crypto.c:107.

NIST: PR.PS-01 | ISO: A.8.28, A.8.27 | Priority: medium | Verify: grep -rnE '\balloca\(' src/ and review for non-constant sizes; compile with -Wvla and treat as error to flag VLAs; replace flagged sites with checked heap allocation + auto_* cleanup.

REQ-DAR — Data Protection at Rest & Privacy

File/directory permission and in-memory/at-rest secret-zeroization requirements for at-rest material are consolidated in REQ-AUTH-01 (0600 files, including the SQLite DB gap), REQ-AUTH-02 (0700 dirs), and REQ-AUTH-06 (zeroization). The requirements below cover history-content minimization, redaction, and at-rest integrity.

REQ-DAR-01

Functional no-storage (off) mode writes no message content

When PREF_DBLOG is off, cproof SHALL NOT write any message body, sender/recipient JID-pair, or per-message timestamp derived from chat traffic to any history backend (SQLite or flat-file), for incoming, outgoing, MUC and MUC-PM messages alike.

NIST: PR.DS-01 | ISO: A.8.10 | Priority: high | Verify: set PREF_DBLOG=off, drive add_incoming + all add_outgoing_*, assert zero rows in SQLite ChatLogs and zero content lines in the flat-file; confirm get_previous_chat returns nothing; pin the early-return in both backends (database_sqlite.c, database_flatfile.c).

REQ-DAR-02

Redaction (redact) mode never leaks plaintext into storage

When PREF_DBLOG is redact, cproof SHALL substitute the message body with a fixed placeholder ([REDACTED]) before it reaches any persistence call, so no original body character is passed to sqlite3_bind or written to the flat-file; non-content metadata may be retained.

NIST: PR.DS-01 | ISO: A.8.11 | Priority: high | Verify: with redact, store a message containing a unique sentinel, then grep the raw chatlog.db bytes and flat-file and assert the sentinel is absent while a [REDACTED] row/line exists; confirm redaction precedes the bind/write.

REQ-DAR-03

No plaintext chat content or secrets in operational/debug logs

cproof SHALL NOT write decrypted message bodies, account passwords, eval_password output, SASL credentials, OMEMO/OTR/PGP private keys, or AI provider API keys/Bearer tokens into profanity.log at any level; statements involving message content SHALL log only metadata (JIDs, ids, sizes, encryption type, library error code). Defect to remediate: src/database_sqlite.c:760 log_error('content: %s', message->plain) on insert failure (bypasses no-storage/redact). The libstrophe debug bridge _xmpp_file_logger SHALL redact SASL <auth>/<challenge>/<response> and XEP-0077 registration credentials before they reach _log_msg. (Merges REQ-LOG-01, REQ-LOG-02, REQ-EXT-03 log half; SDLC secret-scanning is REQ-SDLC-04.)

NIST: PR.PS-04, PR.DS-01 | ISO: A.8.12, A.8.15 | Priority: critical | Verify: grep log_* for interpolation of message->plain/message->body/password/api_key/token/Bearer/key bytes and assert none print the value; drive login+decrypt+AI-request with a sentinel secret and assert it never appears in the log; fix database_sqlite.c:760; confirm src/ai/ai_client.c never logs the api_key and never sets CURLOPT_VERBOSE; connect with XMPP debug logging and a sentinel password and assert it (and its base64) is absent.

REQ-DAR-04

Atomic, permission-preserving writes for history export and keyfile rewrites

cproof SHALL write history exports and rewritten secret/history files atomically via a private (0600) temp file created with mkstemp/O_EXCL followed by rename, never via a predictable fixed temp path, so a concurrent or hostile local process cannot pre-create, read, or race the file.

NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: review src/database_export.c (mkstemp+fchmod 0600+rename) and any flat-file rewrite/compaction for the same pattern with no fixed-name fallback; test pre-creating the legacy predictable tmp name as a symlink and assert the export refuses or writes through a fresh mkstemp 0600 file.

REQ-DAR-05

Encryption-state and metadata accuracy in stored records

cproof SHALL persist the correct encryption type (none/omemo/otr/pgp/ox) for every stored message row matching the actual delivery path, and SHALL NOT record a decryption-failure message as encryption='none' plaintext or store raw ciphertext as the message body.

NIST: PR.DS-01 | ISO: A.8.11 | Priority: medium | Verify: tests asserting an OMEMO/OTR/PGP/OX message round-trips with the matching encryption value (_get_message_enc_type), and a decryption-failure message is either not stored or stored with an explicit non-none marker rather than its ciphertext under none.

REQ-DAR-06

Clear-on-close history minimization honored

When PREF_CLEAR_PERSIST_HISTORY is disabled, cproof SHALL discard in-window scrollback on window close without re-persisting it beyond the configured backend, and /clear behavior SHALL be consistent with the PREF_DBLOG retention setting.

NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: toggle PREF_CLEAR_PERSIST_HISTORY and assert closing a chat window with it disabled produces no new persisted rows; confirm /clear with persist off does not write back buffer content (src/ui/window.c, /clear handler).

REQ-DAR-07

Keep plaintext per-day chatlog subsystem disabled; document retention defaults

The legacy plaintext per-day chat-log writers (chatlog.c entry points) SHALL remain no-ops and SHALL NOT be re-enabled except behind an explicit, off-by-default preference that documents it stores decrypted content unencrypted; the flat-file/SQLite backends SHALL be the only message sinks, and PREF_DBLOG values (off/redact/flatfile/store) and their privacy implications SHALL be documented. (Merges REQ-LOG-09, REQ-DAR retention.)

NIST: PR.DS-01, PR.PS-04, GV.PO-01 | ISO: A.8.12, A.5.34 | Priority: low | Verify: assert chatlog.c functions remain no-ops (no fopen/write) via test/grep; send/receive an encrypted message and assert no chatlogs/ plaintext file under XDG_DATA_HOME; confirm PREF_DBLOG documented; require any new logging pref to default false.

REQ-LOG — Logging, Error Handling & Detection

Log-file 0600 permission is in REQ-AUTH-01; no-secrets-in-logs is in REQ-DAR-03.

REQ-LOG-01

Security-relevant events logged at WARN or higher

cproof SHALL record at PROF_LEVEL_WARN/ERROR (so they survive the default WARN filter): TLS certificate validation failure, OMEMO/OTR/PGP/OX decryption or MAC failure, use/rejection of an untrusted/unknown device fingerprint, and SASL authentication failure.

NIST: DE.CM-06, DE.AE-02 | ISO: A.8.15, A.8.16 | Priority: high | Verify: grep that cert-fail (_connection_certfail_cb/sv_ev_certfail), decrypt-fail (src/omemo, src/pgp/ox.c), untrusted-device and SASL-failure paths call log_warning/log_error (not log_debug); functional test injecting a bad cert and a corrupt OMEMO ciphertext asserts a WARN/ERROR line at the default filter.

REQ-LOG-02

Detect and log TLS/security downgrade conditions

cproof SHALL log a WARN-or-higher event whenever a connection is established/negotiated below the expected posture: TLS negotiation failed, connecting without mandatory TLS, an unencrypted (DISABLE_TLS) connection, or a followed see-other-host/stream redirect — so silent transport downgrades are detectable.

NIST: DE.AE-02, DE.CM-06 | ISO: A.8.16, A.8.15 | Priority: high | Verify: inspect src/xmpp/connection.c that DISABLE_TLS/non-mandatory/TLS-failed/see-other-host paths emit log_warning; test connecting with TLS disabled asserts a WARN line.

REQ-LOG-03

Safe, non-leaking error messages on crypto/parse failures

Error/warning messages on crypto-operation failure, stanza/XML parse failure, or DB/keyfile load failure SHALL contain only a failure category and a stable library error code/string, and SHALL NOT embed raw ciphertext, key bytes, full untrusted stanza payloads, or secret-derived values. (Reinforces REQ-DAR-03.)

NIST: PR.PS-04, DE.AE-02 | ISO: A.8.15, A.8.28 | Priority: medium | Verify: review crypto-failure log_error sites (src/omemo/omemo.c, src/pgp/ox.c gpgme_strerror, stanza handlers) to confirm they pass strerror codes/identifiers, not buffers/key material.

REQ-LOG-04

Log level user-configurable, defaulting to non-debug

cproof SHALL expose a configurable filter (DEBUG/INFO/WARN/ERROR) honored by _should_log(), default to a non-DEBUG level, map unknown level strings to WARN, and require explicit user action to enable DEBUG (which may capture additional protocol detail).

NIST: PR.PS-04, ID.RA-01 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: unit-test _should_log across levels confirming below-filter records drop; confirm log_level_from_string defaults unknown to WARN and DEBUG is not the startup default.

REQ-LOG-05

Bounded log growth via size-based rotation

When rotation is enabled, the logger SHALL rotate the main log once it reaches max_log_size, SHALL bound the number of rotated files, and rotated files SHALL inherit 0600, preventing unbounded disk consumption while preserving a finite reviewable history.

NIST: PR.PS-04, PR.IR-04 | ISO: A.8.15, A.8.6 | Priority: medium | Verify: test that writing past max_log_size triggers _rotate_log_file(), produces a bounded-index profanity.NNN.log (also 0600), reopens the main file; confirm user-provided log files are excluded from auto-rotation.

REQ-LOG-06

stderr capture fails safe and respects sensitive-data constraints

The stderr-capture facility (log_stderr_init/log_stderr_handler) that redirects process stderr into the log SHALL handle init/read errors (including EINTR/partial reads) without crashing or losing the fd, and SHALL be subject to the same no-secrets constraint so crypto-library diagnostic output to stderr (gcrypt/gpgme/libsignal) does not persist key/passphrase material.

NIST: PR.PS-04, DE.CM-06 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: review error paths in log_stderr_init for correct fd cleanup and non-fatal failure; write a sentinel secret to stderr and assert redaction/handling; verify the handler tolerates EINTR/partial reads without data loss or crash.

REQ-CFG — Secure Configuration & Secure Defaults

TLS-mandatory default (REQ-CRY-03), cert-validation/TOFU defaults (REQ-CRY-04), tls.policy allowlist (REQ-CRY-05), OMEMO manual trust default (REQ-CRY-10), legacy-flag gating (REQ-AUTH-03), and toolchain hardening (REQ-MEM-01) are consolidated above. The CFG-specific defaults remain:

REQ-CFG-01

HTTP(S) file transfer must not silently disable TLS verification

cproof SHALL keep CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST enabled for XEP-0363 upload and HTTP download by default, and SHALL disable them ONLY as an explicit consequence of the operator-selected account tls.policy=trust (or an explicit PREF_TLS_CERTPATH=none), never from any other condition or default; that insecure state SHALL be surfaced to the user, and downloaded files SHALL be written canonicalized within the configured downloads directory (rejecting peer-supplied path traversal). (Merges REQ-EXT-08.)

NIST: PR.AA-03, PR.DS-02, PR.DS-10 | ISO: A.8.9, A.8.24, A.5.14 | Priority: high | Verify: confirm in src/tools/http_upload.c/http_download.c the insecure boolean derives solely from tls_policy=="trust"/explicit none, and that with force/NULL the handles retain VERIFYPEER=1/VERIFYHOST=2; grep for any unconditional SSL_VERIFY*=0; test a peer filename containing ../ stays inside the downloads dir.

REQ-CFG-02

Encryption-loss warning and TLS indicator on by default

cproof SHALL default PREF_ENC_WARN and PREF_TLS_SHOW to enabled so the user is warned before sending unencrypted and is shown transport security state out of the box.

NIST: PR.PS-01, DE.CM-09 | ISO: A.8.9 | Priority: medium | Verify: unit-test prefs_get_boolean(PREF_ENC_WARN) and prefs_get_boolean(PREF_TLS_SHOW) both return TRUE on a fresh config; review neither falls through to the FALSE branch.

REQ-EXT — Extension & External-Interface Security (Plugins, AI, File/URL/Command Surfaces)

URL/shell handling is REQ-INP-06; notification injection REQ-INP-07; AI JSON escaping REQ-INP-10; AI key-in-log/at-rest REQ-DAR-03/REQ-AUTH-01; file-transfer TLS/traversal REQ-CFG-01; hardening REQ-MEM-01.

REQ-EXT-01

Enforce TLS peer/host verification and https scheme on AI LLM outbound requests

The AI client SHALL set CURLOPT_SSL_VERIFYPEER=1 and CURLOPT_SSL_VERIFYHOST=2 on every outbound LLM request, SHALL require the provider api_url scheme to be https (rejecting http:// at provider-add and request time), and SHALL NOT expose any per-provider option to disable verification, so the API key and conversation content are never sent in cleartext or to an unverified peer. (Merges REQ-CFG-06.)

NIST: PR.DS-02, PR.PS-01 | ISO: A.5.14, A.8.24, A.8.27 | Priority: critical | Verify: grep src/ai/ai_client.c (_curl_exec_and_handle/_ai_request_thread) for VERIFYPEER/VERIFYHOST set non-zero on both curl sites and never to 0L; add a test that ai_add_provider rejects an http:// api_url.

REQ-EXT-02

Explicit opt-in consent before transmitting conversation content to LLM providers

The AI client SHALL transmit message/conversation content to an external LLM provider only after the user has explicitly enabled the feature and configured a provider, SHALL be disabled by default, and SHALL NOT auto-forward received XMPP chat/MUC messages to any provider without an explicit per-action user command (no wiring to inbound-message plugin/stanza hooks). AI prompt/response content SHALL NOT be ingested into the XMPP chat history DB/chatlogs and SHALL live only in volatile per-session memory unless the user opts into persistence. (Merges REQ-EXT-09.)

NIST: PR.DS-01, GV.PO-01 | ISO: A.5.14, A.8.24, A.8.26 | Priority: critical | Verify: confirm the AI feature is gated behind a default-off preference and explicit /ai command; verify no prof_pre/post chat/room hook or stanza-receive path calls ai_send_prompt; test that with no provider configured ai_send_prompt performs no network call; confirm no code path passes AI session content into chatlog_/database_ APIs and history is freed on ai_session_unref.

REQ-EXT-03

Bounded, time-limited AI calls with response-size cap; bounded plugin/AI buffering

Every AI curl handle SHALL set a finite CURLOPT_TIMEOUT (and ideally CURLOPT_CONNECTTIMEOUT), and _write_callback SHALL cap accumulated response size (current 10 MB) and abort the transfer when exceeded, so a slow/hung/malicious endpoint cannot hang the UI thread or exhaust memory.

NIST: PR.IR-03, ID.RA-09 | ISO: A.8.6, A.8.16 | Priority: high | Verify: review every AI curl handle sets CURLOPT_TIMEOUT and that _write_callback returns short to abort at the cap; test with a mock server that streams past the cap and one that stalls, asserting bounded abort.

REQ-EXT-04

Plugins load only from the protected 0700 plugins directory; run with full privilege; downloads verified

C plugins (dlopen with RTLD_NOW|RTLD_GLOBAL) and Python plugins SHALL be loaded only from the 0700 per-user plugins directory under the data path (files_get_data_path(DIR_PLUGINS), no user-supplied absolute/CWD/relative/shared path), SHALL NOT be auto-loaded without explicit user opt-in, SHALL be stored without world/group-write bits, and any /plugins install download SHALL require an https source with TLS verification. The full-privilege trust model (plugins access all secret stores) SHALL be documented. (Merges REQ-AUTH-08, REQ-SUP-09 plugin half.)

NIST: PR.AA-05, PR.PS-01, ID.RA-09 | ISO: A.8.19, A.8.25, A.8.2 | Priority: high | Verify: confirm the load dir derives only from files_get_data_path(DIR_PLUGINS) and is created S_IRWXU; confirm no implicit auto-load of untrusted paths; verify plugin_download.c enforces https + TLS verify (no SSL_VERIFY 0L); check installed plugin files lack world/group write; document the plugin trust model in user docs.

REQ-SUP — Supply Chain & Third-Party Dependencies

REQ-SUP-01

Pin the libstrophe Meson subproject to an immutable revision/checksum

subprojects/libstrophe.wrap SHALL pin libstrophe to an exact immutable revision (40-hex commit SHA or release tag plus source_hash/wrapdb checksum) and SHALL NOT use revision = HEAD or a bare branch, so a build cannot silently incorporate an attacker-modified or regressed XMPP parser. (Merges REQ-VUL-03, REQ-SDLC-07 wrap half; libstrophe sits on the untrusted network path.)

NIST: GV.SC-05, ID.RA-09, PR.PS-01 | ISO: A.5.21, A.5.23, A.8.8, A.8.28, A.8.30 | Priority: critical | Verify: grep the .wrap: assert no revision = HEAD/bare branch and presence of a 40-hex revision or source_hash; add a CI lint failing on unpinned wrap revisions; confirm two clean builds at the pinned revision produce identical libstrophe object hashes.

REQ-SUP-02

Pin CI base images by digest and bootstrapped deps by commit; verify download provenance

All ci/Dockerfile.* base images SHALL be pinned by immutable digest (FROM image@sha256:…, not debian:testing/archlinux:latest), and all source dependencies cloned/downloaded during image build (libstrophe, stabber, AUR libstrophe-git) SHALL be checked out at a pinned commit and obtained over authenticated HTTPS with integrity verification (commit/checksum/signature), not --depth 1 HEAD or unverified wget+makepkg. (Merges REQ-SUP-08, REQ-VUL-10.)

NIST: GV.SC-06, GV.SC-07, GV.SC-05 | ISO: A.5.20, A.5.21, A.5.23, A.8.30 | Priority: critical | Verify: grep Dockerfiles for FROM .*:latest/:testing, clone --depth 1 without a following git checkout <sha>, and wget/curl downloads lacking a subsequent sha256sum/gpg check; assert each FROM contains @sha256:; confirm makepkg integrity checks are not bypassed.

REQ-SUP-03

Enforce minimum, CVE-informed versions for all security-relevant dependencies (build fails below floor)

The build SHALL fail (AC_MSG_ERROR, not notice/warn) when any security-relevant dependency is below a documented CVE-informed minimum: libstrophe, libcurl, sqlite3, glib, gpgme, libotr, libsignal-protocol-c/libomemo-c, libgcrypt. gpgme in particular SHALL be checked via versioned PKG_CHECK_MODULES([gpgme],[gpgme >= X.Y]), replacing the unversioned AC_CHECK_LIB([gpgme],[main]). The declared minimums SHALL be identical between configure.ac and meson.build (current divergences: sqlite3 3.22.0 vs 3.35.0; gpgme unversioned), each floor documented with security rationale. (Merges REQ-CRY-11, REQ-SUP-03, REQ-SUP-04, REQ-VUL-02.)

NIST: ID.RA-09, GV.SC-04, PR.PS-02 | ISO: A.5.21, A.8.8, A.8.28 | Priority: critical | Verify: grep both files to confirm a version-bearing PKG_CHECK_MODULES (no bare AC_CHECK_LIB([gpgme],[main])); a script diffs the (name,minversion) tuples and fails on mismatch; configuring against a stubbed under-floor .pc aborts configure non-zero; confirm gcry_check_version(GCRYPT_VERSION) gates init.

REQ-SUP-04

Continuous dependency-CVE scanning in CI

CI SHALL run an automated known-vulnerability scanner (OSV-Scanner/Dependabot/grype/Trivy) over the third-party set (libstrophe, gpgme, libsignal/libomemo-c, openssl/gnutls, libgcrypt, libotr, sqlite, glib, libcurl) on each push and on a recurring schedule, with results triaged and surfaced; a seeded known-vulnerable pinned version SHALL trigger an alert. (Merges REQ-VUL-04.)

NIST: ID.RA-01, GV.SC-08, ID.IM-02 | ISO: A.5.7, A.8.8 | Priority: high | Verify: confirm a scan job exists in .github/workflows (or dependabot.yml for the wrap/Docker ecosystems), runs on cron, and fails/reports above a defined severity; verify a seeded vulnerable version triggers an alert; check triage records exist.

REQ-SUP-05

Pin GitHub Actions to commit SHAs

All third-party GitHub Actions SHALL be pinned to a full commit SHA, not a mutable tag (actions/checkout@<sha> not @v4).

NIST: GV.SC-06, PR.PS-01 | ISO: A.8.30, A.8.28 | Priority: medium | Verify: grep .github/workflows/*.yml for uses:.*@v[0-9] or any non-40-hex ref and fail; optionally enforce with a pinning linter (zizmor/ratchet) in CI.

REQ-SUP-06

Govern the C/Python plugin and AI-provider dependency boundary (documentation)

The project SHALL document and enforce the runtime trust boundary: dlopen'd C plugins and Python plugins are out-of-tree third-party code that run with full process privilege and SHALL NOT auto-load without opt-in (enforced by REQ-EXT-04), and the AI client's outbound LLM endpoints/dependencies SHALL be restricted to user-configured, TLS-validated hosts (enforced by REQ-EXT-01).

NIST: GV.SC-04, ID.RA-09 | ISO: A.5.19, A.8.30 | Priority: high | Verify: confirm documentation of the plugin/AI supply-chain boundary exists and that the enforcing controls (REQ-EXT-01/REQ-EXT-04) have passing tests; confirm the AI module only contacts configured hosts with cert verification.

REQ-VUL — Vulnerability & Patch Management, Change Control

Dependency floors (REQ-SUP-03), wrap pinning (REQ-SUP-01), CVE scanning (REQ-SUP-04), reproducible CI images (REQ-SUP-02), and hardening verification (REQ-MEM-01) are consolidated above.

REQ-VUL-01

Documented vulnerability-disclosure policy with scope, supported versions and response SLA

SECURITY.md SHALL specify, beyond a contact and PGP key: supported/maintained versions, in-scope components (OMEMO/OTR/PGP/OX crypto paths, XMPP stanza parsing, TLS, plugins, AI client), out-of-scope items, a target acknowledgement and remediation timeframe, and coordinated-disclosure expectations; designated owners for security-critical subsystems SHALL be recorded. (Merges REQ-SDLC-10.)

NIST: RS.MA-01, ID.IM-01, GV.RR-02, GV.OC-02 | ISO: A.5.5, A.5.26, A.8.8, A.5.2 | Priority: high | Verify: inspect SECURITY.md for a Supported Versions section, enumerated in/out-of-scope components, an explicit ack/remediation SLA, and coordinated-disclosure terms; confirm the contact key/fingerprint is current; add a CI doc-lint failing if required headings are absent.

REQ-VUL-02

Update-availability check verifies TLS and treats the response as untrusted

release_get_latest() SHALL explicitly set CURLOPT_SSL_VERIFYPEER=1L and CURLOPT_SSL_VERIFYHOST=2L, constrain itself to HTTPS, and treat the fetched version string as untrusted (strict ^\d+\.\d+\.\d+$ validation before display), never auto-downloading or executing anything.

NIST: ID.RA-01, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: high | Verify: read src/common.c and assert the curl handle sets VERIFYPEER/VERIFYHOST and rejects non-https; assert cons_check_version/release_is_new validate against the version regex before use; unit-test feeding malformed/oversized version strings.

REQ-VUL-03

User-controllable suppression of version/OS disclosure to peers

XEP-0092 version replies and disco SHALL be gated by user preferences, default to NOT revealing OS details (PREF_REVEAL_OS off), and allow disabling the <version> element entirely, so a peer cannot fingerprint an un-patched client+OS+build to target known CVEs.

NIST: PR.AA-05, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: medium | Verify: inspect the src/xmpp/iq.c version-reply: confirm <os> is added only when PREF_REVEAL_OS is true and is_custom_client false, a pref exists to omit <version>, and PREF_REVEAL_OS defaults off; stabber/functional test asserting the reply omits <os> by default.

REQ-VUL-04

Security fixes recorded in CHANGELOG with CVE/advisory references

Each release SHALL document security-relevant fixes in the CHANGELOG, referencing the associated CVE/advisory identifier where one exists, so downstream packagers and users can assess patch urgency.

NIST: RC.RP-05, ID.IM-04 | ISO: A.8.8, A.5.26 | Priority: medium | Verify: review the CHANGELOG for a security/fix section per release with CVE/advisory ids where applicable (precedent: Fix CVE-2017-5592); extend RELEASE_GUIDE.md to mandate a security-fix changelog review before tagging.

REQ-RES — Resilience, Robustness & Fail-Safe Behavior

Fail-closed E2EE (REQ-CRY-01), force-encryption gate (REQ-CRY-02), OMEMO fail-safe trust (REQ-CRY-10), bounded stanza/OMEMO parsing (REQ-INP-09), and bounded AI calls (REQ-EXT-03) are consolidated above.

REQ-RES-01

Disk-space and atomicity preflight for SQLite schema migrations

Before any chatlog schema migration cproof SHALL verify free disk space via _check_available_space_for_db_migration() and SHALL run each migration inside a single transaction (BEGIN/COMMIT with rollback on error), so a failed/interrupted migration leaves the DB at its prior consistent schema version; on failure it SHALL surface an error and refuse to continue with a partially-migrated DB.

NIST: RC.RP-01, PR.IR-01 | ISO: A.8.13, A.8.27 | Priority: high | Verify: local (non-CI) migration tests on v1/v2 fixtures: simulate ENOSPC and assert migration refused with version unchanged; abort mid-migration and assert DbVersion reflects the pre-migration version; static review each _migrate_to_vN wraps statements in a transaction with rollback.

REQ-RES-02

Detect and gracefully handle corrupted history DB without crashing or data loss

cproof SHALL run PRAGMA integrity_check on the chatlog DB and SHALL handle a non-ok result (and sqlite3_open/prepare failures) by reporting to the user and degrading gracefully (e.g. disabling DB-backed history for the session) rather than abort()/exit() or silently writing into a corrupt store.

NIST: RC.RP-01, PR.IR-03 | ISO: A.8.27, A.8.13 | Priority: high | Verify: open a deliberately truncated/garbage DB file and assert the app reports an integrity issue and continues running (no abort/exit, history disabled); confirm _sqlite_verify_integrity results are inspected and surfaced.

REQ-RES-03

Graceful connection-loss handling with state-preserving reconnect (no message loss, no security downgrade)

On unexpected connection failure cproof SHALL transition to a defined DISCONNECTED state, preserve XEP-0198 stream-management state for resumable reconnect, and gate reconnect/auto-ping on a valid (non-NULL) connection, so connection flicker does not cause crashes, duplicate/dropped messages, or any change in the per-chat negotiated encryption posture (E2EE flags MUST survive reconnect).

NIST: PR.IR-03, RC.RP-01 | ISO: A.8.27, A.5.29 | Priority: high | Verify: drop the socket mid-session and assert status becomes DISCONNECTED, sm_state is captured and re-applied on reconnect, no crash, and chat E2EE flags (is_omemo/is_ox/pgp_send) are unchanged after resume; confirm iq_autoping_check/reconnect paths null-check the connection (cf. 15dfc2bd).

REQ-RES-04

Sanitize outgoing message content to prevent self-inflicted stanza corruption

Outgoing chat, MUC, and private message bodies SHALL pass through str_xml_sanitize() before stanza construction, ensuring user/plugin-supplied content cannot inject control characters or invalid XML that would corrupt the stream or cause a server/peer disconnect.

NIST: PR.IR-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: test that messages with control/invalid-XML characters are sanitized before stanza construction and the stanza serializes to well-formed XML; static check that every outbound path (cl_ev_send_msg_correct, cl_ev_send_muc_msg_corrected, cl_ev_send_priv_msg) calls str_xml_sanitize on plugin/user bodies.

REQ-SDLC — Secure Development Lifecycle & Governance

Hardening parity (REQ-MEM-01), sanitizer/Valgrind CI gate (REQ-MEM-02), CWE-134 attribute audit blocking in CI (REQ-INP-04), dependency pinning/floors (REQ-SUP-01/REQ-SUP-03), and the security policy/ownership doc (REQ-VUL-01) are consolidated above.

REQ-SDLC-01

No secrets, private keys, or real credentials committed to the repo; secret-scanning in CI

The repository (working tree and pre-merge feature-branch history) SHALL contain no account passwords, OMEMO/OTR/PGP private key material, or other live secrets; tests and example configs SHALL use only placeholder/mock values, and an automated secret-scanning step (gitleaks/detect-secrets) SHALL run in CI to block credential-like content.

NIST: PR.AA-01, GV.OC-03 | ISO: A.8.4, A.5.8 | Priority: critical | Verify: add a gitleaks/detect-secrets CI job over the diff and full history; grep for PEM private-key headers (BEGIN .* PRIVATE KEY) and password=; confirm profrc.example and functional-test fixtures use only @localhost/stabber mock identities and no real server/passwords.

REQ-SDLC-02

Test/dev data isolated from real user profiles and live XMPP services

cproof tests SHALL NOT read from or write to a real user's profanity profile (~/.local/share/profanity, ~/.config/profanity) nor connect to production XMPP servers; functional tests SHALL run against the stabber mock using throwaway @localhost identities and isolated temporary XDG_DATA_HOME/XDG_CONFIG_HOME dirs created/torn down by the harness.

NIST: PR.IR-01, ID.AM-08 | ISO: A.8.31, A.8.29 | Priority: high | Verify: inspect functional-test setup/teardown for an isolated tmpdir XDG_ targeting localhost only (including PROF_FLATFILE paths); grep tests for absolute $HOME profile paths and for any non-localhost host fixture.*

REQ-SDLC-03

Mandatory peer review with merge gating and CODEOWNERS for security-critical paths

Every change SHALL merge via a pull request that passed required CI checks (build matrix, unit + functional tests, Valgrind/sanitizers) and received review approval before merge to master; changes touching src/omemo, src/otr, src/pgp/OX, src/xmpp/connection.c (TLS/SASL), authentication, src/plugins, or src/ai SHALL require review by a designated owner recorded in .github/CODEOWNERS. Security-relevant changes SHALL carry a conventional-commit fix(<scope>)/feat(<scope>) subject naming the affected module. (Merges REQ-VUL change-control.)

NIST: GV.RR-02, PR.PS-06, ID.IM-03 | ISO: A.5.8, A.8.25, A.8.32 | Priority: high | Verify: confirm branch protection on master requires PR review and passing checks; confirm commitlint runs in CI and rejects non-conventional types; add/verify a CODEOWNERS mapping the security-critical dirs and that GitHub enforces owner review; spot-check git log for fix(<scope>) security subjects.

REQ-SDLC-04

Enforced secure-coding style and static safety conventions (blocking)

cproof source SHALL conform to the project's CI-enforced conventions — clang-format (.clang-format), correct use/initialization of the GLib auto_* cleanup types, and codespell cleanliness — and the style/auto-type checks SHALL be blocking (not continue-on-error) as a precondition for merge.

NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.28 | Priority: medium | Verify: run make doublecheck (format + spell) and require pass; run the auto-type count/init grep checks from the workflow and require zero violations; remove continue-on-error from the format/auto-type steps so they block merges.

REQ-SDLC-05

Fuzz harness for highest-risk untrusted-input parsers

cproof SHALL maintain at least one fuzz harness (libFuzzer/AFL++, built under sanitizers) exercising stanza/XML handling, JID parsing, and inbound encrypted-message decode (OMEMO/PGP/OX), runnable from the repo with an in-tree seed corpus and exercised periodically in a scheduled CI job that fails on new crashes.

NIST: PR.PS-06, ID.RA-01 | ISO: A.8.29, A.8.25 | Priority: medium | Verify: confirm a fuzz target exists and builds with -fsanitize=fuzzer,address over stanza and JID parse entry points; run it for a bounded time in scheduled CI and require no new crashes; track the seed corpus in-repo.

REQ-SDLC-06

Documented security requirements and threat assumptions for crypto/plugin/AI surfaces

cproof SHALL document its security requirements and trust assumptions for security-sensitive features — E2EE invariants (no plaintext fallback, fingerprint/trust handling), TLS/cert-validation policy, the privilege/trust model for C/Python plugins, and the AI module's data-egress behavior (which providers receive content, under what consent) — and SHALL map each documented invariant to an enforcing unit/functional test or code guard (this catalog plus its REQ-IDs serve as that baseline).

NIST: GV.PO-01, PR.DS-02, GV.SC-04 | ISO: A.5.8, A.8.25 | Priority: medium | Verify: confirm a documented requirements/threat-model artifact covers E2EE no-plaintext-fallback, TLS policy, plugin trust boundary, and AI egress/consent; map each invariant to a test (e.g. tests/unittests/command/test_cmd_pgp.c, test_cmd_otr.c) or code guard referencing the relevant REQ-ID.

4. Traceability Summary

NIST CSF 2.0 → Requirements

  • GV (Govern) — GV.PO-01: REQ-DAR-07, REQ-EXT-02, REQ-SDLC-06. GV.RR-02: REQ-SDLC-03, REQ-VUL-01. GV.OC-02/03: REQ-VUL-01, REQ-SDLC-01. GV.SC-04/05/06/07/08: REQ-SUP-01, REQ-SUP-02, REQ-SUP-03, REQ-SUP-04, REQ-SUP-05, REQ-SUP-06, REQ-SDLC-06.
  • ID (Identify) — ID.RA-01/08/09: REQ-CRY-11(→SUP-03), REQ-SUP-03, REQ-SUP-04, REQ-VUL-02, REQ-EXT-03, REQ-EXT-04, REQ-SDLC-05, REQ-MEM-02. ID.IM-01/02/03/04: REQ-VUL-01, REQ-VUL-04, REQ-SUP-04, REQ-SDLC-03. ID.AM-08: REQ-SDLC-02, REQ-SUP-01.
  • PR (Protect) — PR.AA-01/03/04/05: REQ-CRY-04, REQ-CRY-09, REQ-CRY-10, REQ-AUTH-01..06, REQ-CFG-01, REQ-VUL-03, REQ-SDLC-01. PR.DS-01/02/10: REQ-CRY-01..10, REQ-AUTH-01/06, REQ-DAR-01..07, REQ-INP-01/03/05/08/10, REQ-CFG-01/02, REQ-EXT-01/02. PR.PS-01/02/04/05/06: REQ-MEM-01..09, REQ-INP-03/04/06/07, REQ-LOG-01..06, REQ-SUP-03/05, REQ-EXT-04, REQ-SDLC-03/04/05. PR.IR-01/03/04: REQ-CRY-01/02, REQ-INP-09, REQ-RES-01..04, REQ-EXT-03, REQ-LOG-05, REQ-SDLC-02.
  • DE (Detect) — DE.AE-02: REQ-LOG-01, REQ-LOG-02, REQ-LOG-03. DE.CM-01/06/09: REQ-MEM-02, REQ-MEM-08, REQ-LOG-01, REQ-LOG-02, REQ-LOG-06, REQ-CRY-04, REQ-CFG-02, REQ-INP-09.
  • RS / RC (Respond / Recover) — RS.MA-01: REQ-VUL-01. RC.RP-01/05: REQ-RES-01, REQ-RES-02, REQ-RES-03, REQ-VUL-04.

ISO/IEC 27001:2022 Annex A clusters → Requirements

  • A.5 (Organizational) — A.5.5/5.26 (incident/vuln reporting): REQ-VUL-01, REQ-VUL-04. A.5.7 (threat intel): REQ-SUP-04. A.5.8 (project security): REQ-SDLC-01/03/06, REQ-VUL-01. A.5.14 (info transfer): REQ-EXT-01/02, REQ-CFG-01. A.5.17 (auth info): REQ-AUTH-01..06, REQ-CRY-09/10. A.5.19/5.20/5.21/5.23 (supplier/ICT supply chain): REQ-SUP-01..06. A.5.29 (continuity): REQ-RES-03. A.5.31 (legal/identity binding): REQ-CRY-09. A.5.34 (PII/minimization): REQ-DAR-07. A.5.10 (acceptable use of info): REQ-CRY-02.
  • A.8 (Technological) — A.8.2/8.3 (access/privileged access): REQ-AUTH-01/02/05, REQ-EXT-04. A.8.4 (source code/secrets): REQ-SDLC-01. A.8.5 (secure auth): REQ-AUTH-03. A.8.6 (capacity): REQ-LOG-05, REQ-EXT-03. A.8.8 (technical vuln mgmt): REQ-SUP-03/04, REQ-VUL-01..04. A.8.9 (config mgmt/secure config): REQ-CRY-03/04/05/10, REQ-AUTH-03, REQ-CFG-01/02. A.8.10/8.11/8.12 (data masking/leakage/storage): REQ-DAR-01..07, REQ-AUTH-01. A.8.13 (backup/integrity): REQ-RES-01/02. A.8.15/8.16 (logging/monitoring): REQ-LOG-01..06, REQ-DAR-03. A.8.19 (software on operational systems): REQ-EXT-04. A.8.20/8.21 (network/network services security): REQ-CRY-03/04, REQ-AUTH-03. A.8.24 (cryptography): REQ-CRY-01..10, REQ-AUTH-06, REQ-EXT-01, REQ-CFG-01. A.8.25/8.27/8.28 (secure SDLC/architecture/secure coding): REQ-MEM-01..09, REQ-INP-01..10, REQ-SUP-05, REQ-SDLC-03..06, REQ-RES-01/04. A.8.26 (application security requirements): REQ-INP-01..10, REQ-EXT-02, REQ-RES-04. A.8.29 (security testing): REQ-MEM-02, REQ-INP-04, REQ-SDLC-02/04/05. A.8.30 (outsourced/third-party dev): REQ-SUP-01/02/06. A.8.31 (env separation): REQ-SDLC-02. A.8.32 (change management): REQ-SDLC-03, REQ-VUL change-control.

Explicitly Out of Scope

The following ISO/CSF control areas are intentionally excluded as not meaningful for a single-user desktop chat client with no central server or organizational IT estate:

  • Physical and environmental security (A.7.* — secure areas, equipment siting, cabling, physical entry, clear-desk physical aspects).
  • Human-resources security (A.6.* — screening, terms of employment, disciplinary process, awareness-training programs as an organizational HR function).
  • Datacenter / server-room operations, redundant power/cooling/utilities, and physical media handling/disposal at facility scale.
  • Organizational asset inventory of an IT estate, enterprise endpoint/MDM fleet management, network perimeter/firewall/IDS operations, and centralized IAM/directory services.
  • Business-continuity/DR planning at the organizational level, insurance, and supplier-contract/SLA legal management beyond the source-dependency pinning covered in REQ-SUP-01..REQ-SUP-06.
  • Server-side XMPP service hardening (the client connects to, but does not operate, an XMPP server).

CIS Controls v8.1 — requirements catalog

The CIS Critical Security Controls v8.1 (Center for Internet Security) are a prioritized set of 18 controls comprising 153 safeguards that an organization implements to defend against the most common attack patterns. v8.1 refines v8 by tightening safeguard wording and aligning each safeguard to a security function and to the three Implementation Groups: IG1 is "essential cyber hygiene" — the foundational subset every organization should meet; IG2 builds on IG1 for organizations managing more sensitive data and resources; IG3 adds the remaining safeguards for mature programs defending against sophisticated adversaries (IG3 = all 153 safeguards). Every IG1 safeguard is also in IG2 and IG3.

Scope for cproof. CIS v8.1 is an enterprise-IT framework (asset/account/network/SOC operations of an organizational estate). cproof is a single-user desktop E2EE chat client, not an estate, so most controls are summarized as not applicable (no fleet, no central directory, no network gateway, no SOC). The controls with a real software/source-code surface in a client + its repository — CIS-2 (software/dependency inventory & versioning), CIS-4 (secure configuration), CIS-5/6 (credential handling, MFA boundary), CIS-7 (vulnerability-management process), CIS-8 (audit-log management & content), CIS-9 (URL/file-type handling on the chat surface), CIS-15 (service-provider/upstream inventory & TLS enforcement toward providers), CIS-16 (application software security), and CIS-18 (penetration-testing/fuzzing & remediation) — are processed in depth and mapped to the existing REQ-* requirements wherever the control is already covered.

Source note. The CIS v8.1 PDF originally linked for this audit was unreachable (HTTP 404). The authoritative CIS v8.1 control numbers, safeguard numbers/titles, and IG1/IG2/IG3 tags were therefore taken from the official CIS Controls v8.1 structure (see https://www.cisecurity.org/controls/v8-1) and are used verbatim; no safeguard numbers were invented.

CIS → existing requirements traceability

This table maps each in-depth CIS v8.1 safeguard to the existing REQ-* requirement(s) that already cover it. "—" means the safeguard opens genuinely new ground and a new REQ-CIS-* requirement is introduced below. CIS-16 (Application Software Security) is the control most directly aligned with this catalog's whole purpose; its safeguards are mapped to the producer-side requirements en bloc.

CIS safeguard Title IG Existing REQ-ID(s)
2.1 Establish and Maintain a Software Inventory IG1 REQ-SUP-03 (+ new REQ-CIS-01 SBOM)
2.2 Ensure Authorized Software is Currently Supported IG1 REQ-SUP-03, REQ-SUP-01, REQ-SUP-04
2.6 Allowlist Authorized Libraries IG2 REQ-SUP-01, REQ-SUP-02, REQ-EXT-04 (+ new REQ-CIS-03 pinning)
4.1 Establish and Maintain a Secure Configuration Process IG1 REQ-CRY-03/04/05/10, REQ-CFG-02, REQ-SDLC-06 (+ new REQ-CIS-06 baseline doc)
4.6 Securely Manage Enterprise Assets and Software IG1 REQ-CRY-03/05, REQ-AUTH-03, REQ-CFG-01/02
4.8 Uninstall or Disable Unnecessary Services IG2 REQ-EXT-02, REQ-EXT-04, REQ-CFG-02
5.2 Use Unique Passwords IG1 REQ-AUTH-04/05/06, REQ-AUTH-01
5.4 Restrict Administrator Privileges to Dedicated Admin Accounts IG1 N/A (no admin-account model)
6.3 Require MFA for Externally-Exposed Applications IG1 REQ-AUTH-03, REQ-CRY-03 (N/A as written)
7.1 Establish and Maintain a Vulnerability Management Process IG1 REQ-VUL-01, REQ-SUP-04, REQ-SUP-03 (+ new REQ-CIS-02)
7.2 Establish and Maintain a Remediation Process IG1 REQ-SUP-01, REQ-SUP-03, REQ-VUL-04, REQ-SUP-04 (+ new REQ-CIS-02)
7.4 Perform Automated Application Patch Management IG1 REQ-SUP-04, REQ-SUP-02, REQ-SUP-05
8.1 Establish and Maintain an Audit Log Management Process IG1 REQ-LOG-04, REQ-LOG-05, REQ-DAR-03 (+ new REQ-CIS-05 logging policy)
8.2 Collect Audit Logs IG1 REQ-LOG-01, REQ-LOG-02, REQ-LOG-04/05, REQ-AUTH-01
8.5 Collect Detailed Audit Logs IG2 REQ-LOG-01, REQ-LOG-03, REQ-DAR-03
9.3 Maintain and Enforce Network-Based URL Filters IG2 REQ-INP-06
9.6 Block Unnecessary File Types IG2 REQ-CRY-06, REQ-CFG-01
15.1 Establish and Maintain an Inventory of Service Providers IG1 REQ-EXT-02, REQ-SUP-06, REQ-SUP-01 (+ new REQ-CIS-01/REQ-CIS-08)
15.4 Ensure Service Provider Contracts Include Security Reqs IG2 REQ-EXT-01, REQ-CRY-03, REQ-SUP-01, REQ-VUL-01 (+ new REQ-CIS-08)
16.2 Establish and Maintain a Process to Accept and Address Software Vulnerabilities IG2 REQ-VUL-01 (+ new REQ-CIS-02)
16.3 Perform Root Cause Analysis on Security Vulnerabilities IG2 REQ-VUL-04, REQ-SDLC-03
16.4 Establish and Manage an Inventory of Third-Party Software Components IG2 REQ-SUP-03 (+ new REQ-CIS-01 SBOM)
16.5 Use Up-to-Date and Trusted Third-Party Software Components IG2 REQ-SUP-01/02/03/04 (+ new REQ-CIS-03)
16.6 Establish and Maintain a Severity Rating System and Process for Application Vulnerabilities IG2 new REQ-CIS-02
16.7 Use Standard Hardening Configuration Templates for Application Infrastructure IG2 REQ-MEM-01 (+ new REQ-CIS-06)
16.8 Separate Production and Non-Production Systems IG2 REQ-SDLC-02 (+ new REQ-CIS-07)
16.9 Train Developers in Application Security Concepts and Secure Coding IG2 REQ-SDLC-04/06 (process; doc-only)
16.10 Apply Secure Design Principles in Application Architectures IG2 REQ-CRY-01/02/10, REQ-SDLC-06
16.11 Leverage Vetted Modules or Services for Application Security Components IG2 REQ-CRY-06/07/08
16.12 Implement Code-Level Security Checks IG2 REQ-INP-03/04, REQ-MEM-02, REQ-SDLC-04/05 (+ new REQ-CIS-04 SAST/fuzz in CI)
16.13 Conduct Application Penetration Testing IG3 REQ-SDLC-05, REQ-MEM-02 (+ new REQ-CIS-04)
16.14 Conduct Threat Modeling IG3 REQ-SDLC-06
18.1 Establish and Maintain a Penetration Testing Program IG2 REQ-MEM-02, REQ-SDLC-05, REQ-INP-01/09 (+ new REQ-CIS-04)
18.3 Remediate Penetration Test Findings IG2 REQ-VUL-01/04, REQ-SDLC-03 (+ new REQ-CIS-02)

CIS-specific requirements (new)

The following requirements cover ground that the CIS v8.1 application-security and supply-chain safeguards demand but that the existing NIST/ISO catalog only addresses indirectly (or via a Verify clause rather than a normative SHALL). IDs are stable and never renumbered. Where an existing REQ-* already enforces the control, no new requirement is created — the traceability table above references it instead.

REQ-CIS-01

Maintain a machine-readable third-party software inventory / SBOM

The project SHALL maintain, in-repo, an authoritative inventory of every third-party software component cproof links or bundles (libstrophe, gpgme, libsignal-protocol-c/libomemo-c, libotr, sqlite3, glib/gio, libcurl, libgcrypt, ncursesw, readline, gtk, libnotify, qrencode) recording for each: declared minimum version, license, and upstream URL; the inventory SHALL be emitted/validated as a CycloneDX or SPDX SBOM in CI so it stays in sync with configure.ac/meson.build, and the divergent autotools-vs-meson version floors (e.g. sqlite3 3.22.0 vs 3.35.0; gpgme/libgcrypt declared in meson but unversioned in autotools) SHALL be reconciled to a single source of truth that feeds the inventory. (Today the only narrative inventory of the third-party set is REQ-SUP-03 in this catalog; no SBOM/SPDX/CycloneDX artifact exists in the tree.)

CIS: 2.1 (IG1), 16.4 (IG2), 15.1 (IG1) | NIST: ID.AM-08, GV.SC-04 | ISO: A.5.21, A.8.8 | Priority: medium | Verify: confirm a compliance/SBOM.{cdx,spdx}.json (or generated equivalent) exists, lists every linked library with version/license/URL, and is regenerated+diff-checked in CI; assert the autotools and meson floors are identical for every shared dependency.

REQ-CIS-02

Documented process to accept, severity-rate, and remediate software vulnerabilities

The project SHALL maintain a documented, recurring process for accepting and addressing security vulnerabilities in cproof and its dependencies that, beyond the inbound contact, defines: an intake/triage path (extending SECURITY.md per REQ-VUL-01), a severity-rating scheme (e.g. CVSS-aligned, with E2EE plaintext-leak / MitM-bypass / RCE classed critical), severity-keyed remediation targets (SLA), a root-cause and regression-test requirement per fix, and a release gate so critical findings block a tag. (SECURITY.md is presently 5 lines — contact + PGP fingerprint only, no supported-versions, scope, SLA, or severity model; RELEASE_GUIDE.md has no security step.)

CIS: 7.1 (IG1), 7.2 (IG1), 16.2 (IG2), 16.6 (IG2), 18.3 (IG2) | NIST: RS.MA-01, ID.IM-01/02 | ISO: A.5.5, A.5.26, A.8.8 | Priority: high | Verify: confirm a documented vuln-handling+severity+SLA process exists (in SECURITY.md/CONTRIBUTING.md/a compliance/ doc); confirm each landed security fix carries a regression test and a CHANGELOG entry (REQ-VUL-04); confirm a release checklist gate on open critical findings.

REQ-CIS-03

Pin and integrity-verify all build-time and runtime library sources to an authorized identity

Every dependency source the build consumes SHALL be pinned to an immutable, integrity-verified identity: the libstrophe Meson wrap SHALL pin a 40-hex commit SHA or release tag + source_hash (not revision = HEAD); CI base images SHALL be digest-pinned and bootstrapped/cloned deps commit-pinned with checksum/signature verification; and at runtime, dynamically loaded plugin binaries SHALL be checksum/signature-verified before dlopen rather than only directory-restricted. (Reinforces and is enforced through REQ-SUP-01/REQ-SUP-02 and REQ-EXT-04; this requirement states the CIS "authorized libraries" allowlist intent across build and runtime as one principle.)

CIS: 2.6 (IG2), 16.5 (IG2) | NIST: GV.SC-05/06, ID.RA-09 | ISO: A.5.23, A.8.30 | Priority: high | Verify: grep subprojects/*.wrap for revision = HEAD/bare branch and fail; assert each ci/Dockerfile.* FROM carries @sha256: and every clone is followed by git checkout <sha>; confirm plugin install verifies a checksum/signature before dlopen (REQ-EXT-04).

REQ-CIS-04

Code-level security checks (SAST + fuzzing + sanitizers) run as blocking CI

CI SHALL run application code-level security checks as enforced (non-continue-on-error) gates: static analysis (clang scan-build and the CWE-134 audit, REQ-INP-04/REQ-SDLC-04), the sanitizer build (--enable-sanitizers: ASan/UBSan, REQ-MEM-02), and at least one fuzz harness over the untrusted-input parsers (stanza/XML, jid_create/jid_is_valid, inbound OMEMO/PGP/OX decode, REQ-SDLC-05) on a scheduled job that fails on new crashes; a dependency-CVE scan (REQ-SUP-04) and a secret scan (REQ-SDLC-01) SHALL likewise run in CI. (Today the --enable-sanitizers build is never exercised in CI, no fuzz harness exists, and the style/CWE-134/spell checks are continue-on-error.)

CIS: 16.12 (IG2), 16.13 (IG3), 18.1 (IG2) | NIST: PR.PS-06, ID.RA-01, DE.CM-01 | ISO: A.8.28, A.8.29 | Priority: high | Verify: confirm CI jobs for scan-build, --enable-sanitizers && make check, a scheduled fuzz target, OSV/Dependabot, and gitleaks/detect-secrets, all blocking; intentionally introduce a heap overflow and confirm the sanitizer job fails.

REQ-CIS-05

Documented application logging policy (events, levels, no-secrets constraint, retention)

The project SHALL maintain a short, governable logging policy enumerating which security-relevant events are captured and at which level (TLS certificate-validation failure, decrypt/MAC failure, untrusted-device use, SASL/login failure, TLS downgrade — all at WARN or higher so they survive the default filter, REQ-LOG-01/REQ-LOG-02), the absolute no-secrets / no-plaintext-body constraint (REQ-DAR-03), the default level (WARN, non-DEBUG, REQ-LOG-04), rotation/size bounds (REQ-LOG-05), and the 0600 file mode (REQ-AUTH-01); a CI grep SHALL fail on any log_* interpolation of message->plain/body/password/api_key/Bearer. (Logging mechanics exist but no written policy does, and per-event level decisions are inconsistent — cert-fail and SASL-fail fall below WARN while plaintext bodies are logged by default; see CIS-8.2/8.5 in the audit.)

CIS: 8.1 (IG1), 8.5 (IG2) | NIST: PR.PS-04, DE.AE-02 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: confirm a logging-policy doc lists WARN+ security events, the no-secrets constraint, default level, rotation bounds, and 0600 mode; confirm a CI grep blocks plaintext-body/secret interpolation in log_*.

REQ-CIS-06

Documented secure-configuration baseline (hardened defaults + standard hardening template)

The project SHALL document and maintain a secure-configuration baseline (e.g. docs/SECURE-CONFIGURATION.md or an extension of SECURITY.md) enumerating cproof's shipped hardened defaults — TLS mandatory (REQ-CRY-03), OMEMO trust manual (REQ-CRY-10), PREF_ENC_WARN/PREF_TLS_SHOW on, force-encryption resend-to-confirm, PREF_DBLOG privacy modes — with rationale, recommended overrides for a minimal-surface build (build without C plugins/AI, keep AI off, per CIS-4.8), the toolchain hardening template (REQ-MEM-01), and a stated review cadence; PREF_REVEAL_OS SHALL default OFF per REQ-VUL-03 to align with the baseline. (Defaults exist in code but are not captured as a governable standard; SECURITY.md is contact-only and docs/ holds only man pages.)

CIS: 4.1 (IG1), 16.7 (IG2) | NIST: PR.PS-01, GV.PO-01 | ISO: A.8.9, A.8.27 | Priority: medium | Verify: confirm a secure-config/hardening doc enumerates the shipped defaults with rationale + review cadence and references the hardening flags; confirm PREF_REVEAL_OS defaults FALSE.

REQ-CIS-07

Separate production and non-production data; no real user data in test/dev

The project SHALL ensure test, functional, and development runs never read from or write to a real user's profile (~/.local/share/profanity, ~/.config/profanity) or a production XMPP server, using isolated temporary XDG_DATA_HOME/XDG_CONFIG_HOME dirs and throwaway @localhost/stabber-mock identities only, and SHALL keep all fixtures/example configs free of real credentials (enforced jointly with REQ-SDLC-01/REQ-SDLC-02). (Largely satisfied today via the stabber harness; stated here so CIS-16.8 has an explicit normative home.)

CIS: 16.8 (IG2) | NIST: PR.IR-01, ID.AM-08 | ISO: A.8.31, A.8.29 | Priority: medium | Verify: inspect functional-test setup/teardown for isolated tmpdir XDG_ targeting localhost; grep tests for absolute $HOME profile paths and non-localhost hosts.*

REQ-CIS-08

Enforce a TLS/integrity security baseline toward every external service provider

cproof SHALL enforce a minimum security posture toward each external service provider it contacts: AI/LLM providers SHALL be rejected unless the api_url scheme is https, with CURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2L set explicitly at every AI curl site (REQ-EXT-01); the XMPP-server tls.policy=allow path SHALL be mapped to a TLS-required-or-fail floor (or warn loudly) so SASL/registration credentials never traverse plaintext (REQ-CRY-03); and dependency upstreams (notably the libstrophe wrap) SHALL be consumed only at an immutable, integrity-pinned revision (REQ-SUP-01/REQ-CIS-03). A short PROVIDERS/THIRD_PARTY doc SHALL classify each provider category by the data sensitivity it sees (AI endpoint: API key + conversation; XMPP server: SASL creds + ciphertext; dependency upstreams). (Cross-validates the AI http:// egress gap and the allow-policy silent downgrade.)

CIS: 15.1 (IG1), 15.4 (IG2) | NIST: PR.DS-02, GV.SC-04/05 | ISO: A.5.14, A.5.19, A.8.24, A.8.30 | Priority: high | Verify: assert ai_add_provider/the /ai set provider handler reject non-https and both AI curl sites set VERIFYPEER/VERIFYHOST non-zero; assert tls.policy=allow cannot send credentials in cleartext; assert the wrap is SHA-pinned; confirm a provider-classification doc exists.

Not applicable (enterprise-IT) controls

Each of the following CIS v8.1 controls (or, for partly-applicable controls, the listed safeguards) governs an organizational IT estate with no source-code surface in a single-user desktop client; they are recorded as not applicable.

  • CIS-1 — Inventory and Control of Enterprise Assets. Hardware/device discovery and control across a fleet; cproof has no estate to inventory.
  • CIS-3 — Data Protection (enterprise data-classification/DLP scope). The client-side data-at-rest concerns are covered by REQ-AUTH-01/REQ-AUTH-02, REQ-DAR-01..07; org-wide data inventory/classification/DLP is N/A.
  • CIS-5.4 — Restrict Administrator Privileges to Dedicated Admin Accounts. No administrator/privileged-account model exists in a single-user client (host-OS / IdP concern).
  • CIS-6.3 — Require MFA for Externally-Exposed Applications. cproof exposes no inbound service; it is an outbound SASL client. SASL/MFA negotiation is delegated to libstrophe + the XMPP server, not implemented in the client (adjacent client-auth posture is REQ-AUTH-03/REQ-CRY-03).
  • CIS-8.6/8.7/8.8/8.9/8.10/8.11/8.12 — DNS/URL/command-line/centralized/retained/reviewed/service-provider audit logs. Network-infrastructure, gateway, SIEM, and SOC-process log safeguards; a desktop client operates no DNS/proxy/aggregation/retention estate.
  • CIS-10 — Malware Defenses. Endpoint AV/EDR deployment and management; host-OS responsibility.
  • CIS-11 — Data Recovery. Enterprise backup/recovery program; the client's only recovery-adjacent controls are migration safety/integrity (REQ-RES-01/REQ-RES-02).
  • CIS-12 — Network Infrastructure Management. Router/switch/firewall configuration management; no network infrastructure is owned.
  • CIS-13 — Network Monitoring and Defense. NIDS/NIPS/NetFlow/SIEM operations across an estate; N/A.
  • CIS-14 — Security Awareness and Skills Training. Organizational HR/awareness program; the developer-secure-coding analogue is CIS-16.9 ↔ REQ-SDLC-04/REQ-SDLC-06 (doc-only).
  • CIS-17 — Incident Response Management. Org-level IR plan/team/exercises; the in-repo analogue is vulnerability handling, CIS-16.2/7.1 ↔ REQ-VUL-01 / REQ-CIS-02.
  • CIS-18.2 (and 18.4/18.5) — external/remediation-validation pen-test estate operations. A full external network/infrastructure pen-test presupposes an estate; the applicable application-level slice (18.1/18.3, 16.13) is processed above via REQ-CIS-04 / REQ-SDLC-05 / REQ-MEM-02.
# Security Compliance — NIST CSF 2.0, ISO/IEC 27001:2022 & CIS Controls v8.1 (cproof) This document combines a requirements catalog with a static source-code compliance audit for **cproof** (a fork of the Profanity console XMPP/Jabber chat client) across three frameworks: NIST Cybersecurity Framework (CSF) 2.0, ISO/IEC 27001:2022, and CIS Critical Security Controls v8.1. The audit target is `master` at HEAD `622054fc6` (which includes the merged PGP plaintext-fallback fix), read directly from the working tree. Method: the NIST/ISO pass used 12 domain auditors with every consequential finding adversarially re-verified against the working tree; the CIS v8.1 pass used per-control auditors over the applicable controls with adversarial verification, cross-validated against the NIST/ISO pass. Part 1 is the compliance audit; Part 2 is the requirements catalog that the audit is traced against. ## References - NIST Cybersecurity Framework (CSF) 2.0 — overview: <https://www.nist.gov/cyberframework> - NIST CSF 2.0 (NIST CSWP 29, full text): <https://doi.org/10.6028/NIST.CSWP.29> - ISO/IEC 27001:2022 — Information security management systems: <https://www.iso.org/standard/27001> - ISO/IEC 27002:2022 — Information security controls (Annex A control set): <https://www.iso.org/standard/75652.html> - CIS Critical Security Controls v8.1: <https://www.cisecurity.org/controls/v8-1> Note: the CIS v8.1 PDF originally linked for this audit was unreachable (HTTP 404), so the authoritative CIS v8.1 control/safeguard structure from the official source above was used. ## Table of contents - [References](#references) - [Part 1 — Compliance audit](#part-1--compliance-audit) - [1. Executive summary](#1-executive-summary) - [2. Compliance scorecard](#2-compliance-scorecard) - [3. Critical & high-priority gaps (ranked)](#3-critical--high-priority-gaps-ranked) - [4. Partial / medium findings (grouped)](#4-partial--medium-findings-grouped) - [5. What cproof does well (notable Met items)](#5-what-cproof-does-well-notable-met-items) - [6. Notable verifier disagreements](#6-notable-verifier-disagreements) - [7. Methodology & limitations](#7-methodology--limitations) - [CIS Controls v8.1 — source compliance](#cis-controls-v81--source-compliance) - [CIS 1. Executive summary](#cis-1-executive-summary) - [CIS 2. Scorecard](#cis-2-scorecard) - [CIS 3. Critical & high-priority CIS gaps (ranked)](#cis-3-critical--high-priority-cis-gaps-ranked) - [CIS 4. What cproof does well (CIS)](#cis-4-what-cproof-does-well-cis) - [CIS 5. Not applicable (enterprise-IT) controls](#cis-5-not-applicable-enterprise-it-controls) - [CIS 6. Methodology & limitations](#cis-6-methodology--limitations) - [Part 2 — Requirements catalog](#part-2--requirements-catalog) - [1. Scope and Introduction](#1-scope-and-introduction) - [2. How to read this](#2-how-to-read-this) - [3. Requirements](#3-requirements) - [REQ-CRY — Cryptography & Confidentiality](#req-cry--cryptography--confidentiality) - [REQ-AUTH — Authentication, Access Control & Secret Management](#req-auth--authentication-access-control--secret-management) - [REQ-INP — Input Validation & Untrusted-Data Handling](#req-inp--input-validation--untrusted-data-handling) - [REQ-MEM — Memory Safety & Secure Coding](#req-mem--memory-safety--secure-coding) - [REQ-DAR — Data Protection at Rest & Privacy](#req-dar--data-protection-at-rest--privacy) - [REQ-LOG — Logging, Error Handling & Detection](#req-log--logging-error-handling--detection) - [REQ-CFG — Secure Configuration & Secure Defaults](#req-cfg--secure-configuration--secure-defaults) - [REQ-EXT — Extension & External-Interface Security (Plugins, AI, File/URL/Command Surfaces)](#req-ext--extension--external-interface-security-plugins-ai-fileurlcommand-surfaces) - [REQ-SUP — Supply Chain & Third-Party Dependencies](#req-sup--supply-chain--third-party-dependencies) - [REQ-VUL — Vulnerability & Patch Management, Change Control](#req-vul--vulnerability--patch-management-change-control) - [REQ-RES — Resilience, Robustness & Fail-Safe Behavior](#req-res--resilience-robustness--fail-safe-behavior) - [REQ-SDLC — Secure Development Lifecycle & Governance](#req-sdlc--secure-development-lifecycle--governance) - [4. Traceability Summary](#4-traceability-summary) - [CIS Controls v8.1 — requirements catalog](#cis-controls-v81--requirements-catalog) - [CIS → existing requirements traceability](#cis--existing-requirements-traceability) - [CIS-specific requirements (new)](#cis-specific-requirements-new) - [Not applicable (enterprise-IT) controls](#not-applicable-enterprise-it-controls) ## Part 1 — Compliance audit **Target:** cproof `master`, HEAD `622054fc6` (includes the merged PGP plaintext-fallback fix). **Catalog:** Part 2 below. **Method:** Static source analysis. NIST/ISO pass: 12 domain auditors, every consequential finding adversarially re-verified against the working tree. CIS Controls v8.1 pass: per-control auditors over the applicable controls with adversarial verification (see the CIS Controls v8.1 section below). ### 1. Executive summary cproof's core E2EE *wire* guarantees are sound: after commit `622054fc6` no cleartext `<body>` reaches the network on any PGP, OX, or OMEMO encryption failure ([REQ-CRY-01](#req-cry-01)), TLS is mandatory by default and certificate validation fails closed with explicit, persisted TOFU trust ([REQ-CRY-04](#req-cry-04), [REQ-CRY-10](#req-cry-10)), and the auto-cleanup/secure-coding conventions are real and CI-checked ([REQ-MEM-06](#req-mem-06)). However, the audit surfaced several serious gaps that an E2EE chat client cannot treat as cosmetic. The single most dangerous is **[REQ-EXT-01](#req-ext-01) (Gap, critical residual)**: the AI/LLM client never sets `CURLOPT_SSL_VERIFYPEER/VERIFYHOST` and accepts `http://` provider URLs, so the API key and full conversation can be sent in cleartext or to an unverified peer. On the inbound side, **[REQ-INP-01](#req-inp-01) (Gap, high)** is a remotely-triggerable NULL-deref DoS: four receive-path handlers dereference `jid_create()` results without a NULL guard on a malformed `from` (e.g. `a@@b`), and the exact fix the requirement names was never applied. **[REQ-DAR-03](#req-dar-03) / [REQ-LOG-03](#req-log-03) (Gap, high)** leak decrypted plaintext bodies into `profanity.log` in the *default* `dblog=on` mode via four `log_*` sites in `database_sqlite.c`. **[REQ-AUTH-01](#req-auth-01) (Partial, high)** leaves the OTR private key (`keys.txt`) and the plaintext history DB (`chatlog.db`) world/group-readable (no `chmod` after creation). **[REQ-CRY-06](#req-cry-06) (Partial)** persists unauthenticated decrypted OMEMO media to the user's chosen path before the GCM tag is verified. Supply-chain posture is weak across the board: the libstrophe Meson wrap is pinned to `revision = HEAD` ([REQ-SUP-01](#req-sup-01), Gap critical), CI base images and bootstrapped deps float ([REQ-SUP-02](#req-sup-02)), and there is no dependency-CVE scanning, no secret-scanning, no fuzzing, and no `CODEOWNERS` ([REQ-SUP-04](#req-sup-04), [REQ-SDLC-01](#req-sdlc-01)/[REQ-SDLC-03](#req-sdlc-03)/[REQ-SDLC-05](#req-sdlc-05)). Build hardening is partial — autotools omits PIE/noexecstack and the Meson *release* path ships with zero hardening ([REQ-MEM-01](#req-mem-01)), and the ASan/UBSan suite is never run in CI ([REQ-MEM-02](#req-mem-02)). The recurring theme: the cryptographic *primitives and wire paths* are well-built, but the **local data-at-rest, operational-logging, build-hardening, and supply-chain/process controls** lag, and several requirement-named defects remain literally unfixed at HEAD. ### 2. Compliance scorecard | Domain | Met | Partial | Gap | N/A | Unknown | |---|---|---|---|---|---| | [CRY](#req-cry--cryptography--confidentiality) (crypto/TLS) | 3 | 7 | 0 | 0 | 0 | | [AUTH](#req-auth--authentication-access-control--secret-management) (auth/secrets) | 2 | 4 | 0 | 0 | 0 | | [INP](#req-inp--input-validation--untrusted-data-handling) (input validation) | 4 | 4 | 2 | 0 | 0 | | [MEM](#req-mem--memory-safety--secure-coding) (memory safety) | 1 | 5 | 3 | 0 | 0 | | [DAR](#req-dar--data-protection-at-rest--privacy) (data at rest) | 6 | 0 | 1 | 0 | 0 | | [LOG](#req-log--logging-error-handling--detection) (logging) | 3 | 2 | 1 | 0 | 0 | | [CFG](#req-cfg--secure-configuration--secure-defaults) (config) | 1 | 1 | 0 | 0 | 0 | | [EXT](#req-ext--extension--external-interface-security-plugins-ai-fileurlcommand-surfaces) (external/AI/plugins) | 1 | 2 | 1 | 0 | 0 | | [SUP](#req-sup--supply-chain--third-party-dependencies) (supply chain) | 0 | 1 | 5 | 0 | 0 | | [VUL](#req-vul--vulnerability--patch-management-change-control) (vuln mgmt) | 0 | 3 | 1 | 0 | 0 | | [RES](#req-res--resilience-robustness--fail-safe-behavior) (resilience) | 3 | 1 | 0 | 0 | 0 | | [SDLC](#req-sdlc--secure-development-lifecycle--governance) | 1 | 3 | 2 | 0 | 0 | | **Overall** | **25** | **33** | **16** | **0** | **0** | > **How to read this:** *Met* = the requirement's normative SHALL clauses are satisfied in code on master (residual risk usually low). *Partial* = the core control exists but at least one mandated clause/sub-path/test is unmet. *Gap* = the control is materially absent or a requirement-named defect is unfixed. Residual risk is judged by exploitability and blast radius, not by status alone — e.g. several *Partial* items ([REQ-AUTH-01](#req-auth-01), [REQ-CRY-03](#req-cry-03)) carry **high** residual risk, while some *Gap* items ([REQ-SUP-05](#req-sup-05)) are **low**. > > Note: the overall tally counts 74 verified requirements; the four findings the verifier left as `null` ([REQ-AUTH-04](#req-auth-04), [REQ-INP-10](#req-inp-10), [REQ-MEM-06](#req-mem-06), [REQ-DAR-04](#req-dar-04)/[REQ-DAR-05](#req-dar-05)/[REQ-DAR-06](#req-dar-06)/[REQ-DAR-07](#req-dar-07), [REQ-CFG-02](#req-cfg-02), [REQ-LOG-04](#req-log-04)/[REQ-LOG-05](#req-log-05), [REQ-RES-04](#req-res-04), REQ-SDLC...) retain their auditor status, all Met. ### 3. Critical & high-priority gaps (ranked) #### 3.1 — [REQ-EXT-01](#req-ext-01) — Enforce TLS verification & https on AI/LLM outbound — **Gap (critical residual)** - **Evidence:** `src/ai/ai_client.c:811-815` and `:1596-1601` set only URL/HTTPHEADER/WRITEFUNCTION/WRITEDATA/POSTFIELDS/TIMEOUT. The provider scheme check (`src/command/cmd_funcs.c:7019`) explicitly **accepts both `http` and `https`** (`g_strcmp0(scheme,"http")==0 || …"https"…`), and `ai_provider_new` (`ai_client.c:388`) stores `api_url` verbatim. `_build_curl_headers` (`ai_client.c:795`) attaches `Authorization: Bearer <key>`; `request_url` is concatenated from `provider->api_url` (`ai_client.c:1590`). `tests/functionaltests/test_ai.c:85,95` codifies acceptance of `http://127.0.0.1:1/`. > **Correction:** The original auditor framing ("no `CURLOPT_SSL_VERIFYPEER/VERIFYHOST` ⇒ unverified HTTPS") is **overstated**: libcurl defaults `VERIFYPEER=1`/`VERIFYHOST=2`, so HTTPS provider URLs *are* certificate-verified by default. There is also a scheme check (the auditor said there was none). The real, confirmed defect is narrower but still serious: **`http://` provider URLs are accepted**, so `/ai set provider … http://…` ships the API key and the entire conversation in **cleartext over plain HTTP**. Residual risk: **high** (down from "critical" as originally rated). - **Why it matters:** NIST PR.DS-02 / ISO A.8.24, A.5.14 (data-in-transit). Cleartext egress of an API key + full message content to (or via) any on-path observer. - **Fix:** Reject non-`https` schemes in the provider scheme check (`cmd_funcs.c:7019`) and at request time before `curl_easy_perform`; flip `test_ai.c` to assert `http://` is rejected. Additionally set `CURLOPT_SSL_VERIFYPEER=1L`/`VERIFYHOST=2L` explicitly at both curl sites as defense-in-depth (guards against a non-default global curl config / libcurl built with relaxed defaults). #### 3.2 — [REQ-SUP-01](#req-sup-01) — Pin libstrophe Meson wrap to an immutable revision — **Gap (high residual, critical priority)** - **Evidence:** `subprojects/libstrophe.wrap` line 3 `revision = HEAD`; no 40-hex SHA, tag, or `source_hash`. No CI lint catches it. - **Why it matters:** NIST ID.RA / GV.SC, ISO A.5.23/A.8.30. libstrophe is the XMPP parser on the untrusted-network path; any clone can silently incorporate an attacker-modified/regressed parser, and builds are non-reproducible. - **Fix:** Replace `revision = HEAD` with the vetted 40-hex commit SHA (or tag + `source_hash`); add a CI lint failing on `revision = HEAD`/bare-branch wraps. #### 3.3 — [REQ-INP-01](#req-inp-01) / [REQ-INP-09](#req-inp-09) — NULL-guard peer JIDs on receive — **Gap (high)** - **Evidence:** `jid_create()` returns NULL for NULL/empty/over-3071-char/invalid-UTF-8/double-`@` (`src/xmpp/jid.c:35-38,82-153`). Four unguarded derefs: `src/xmpp/message.c:1749-1750` (`_should_ignore_based_on_silence`, the exact function the requirement names), `src/xmpp/presence.c:397-402` (`_presence_error_handler`, `from` not even NULL-checked), `presence.c:453-454` (`_unsubscribed_handler`), `presence.c:468-470` (`_subscribed_handler`). The sibling `_subscribe_handler` (`presence.c:482-484`) *does* guard, proving these are bugs. - **Why it matters:** NIST PR.DS / ISO A.8.28. A hostile peer sending a stanza with missing/malformed `from` (e.g. `a@@b`) causes a remote NULL-deref DoS — a crash on every connection attempt from that peer. - **Fix:** After every receive-path `jid_create(from)` add `if (!from_jid) { return; }` before field access. Add a regression test driving these handlers with absent/`a@@b` `from` under ASan. #### 3.4 — [REQ-DAR-03](#req-dar-03) / [REQ-LOG-03](#req-log-03) — Plaintext bodies in operational logs — **Gap (high)** - **Evidence:** Default `PREF_DBLOG="on"` (`src/config/preferences.c:2815`) does not redact. `src/database_sqlite.c:694` (LMC mismatch, `message->plain`), `:717` (duplicate stanza-id, `content: %s`), `:748` (`log_debug("Writing to DB. Query: %s")` — the INSERT embeds `message->plain` at `:735`), `:760` (insert-failure, the requirement-named defect) all write decrypted bodies to `profanity.log`. Triggers are remotely influenceable (XEP-0308 LMC, server-assigned stanza-id). The libstrophe debug bridge `_xmpp_file_logger` (`src/xmpp/connection.c:1088-1107`) forwards raw SASL `<auth>/<challenge>/<response>` and XEP-0077 registration credentials with no redaction (gated by `PREF_STROPHE_VERBOSITY`, default off). - **Why it matters:** NIST PR.DS-01 / ISO A.8.10, A.8.12. Decrypted E2EE content lands in a plaintext log in the *shipped default config*, surviving all log-level filters — defeating the point of E2EE for anyone who shares logs for debugging. - **Fix:** Drop `message->plain`/`content:`/`Message:` from the format args at `database_sqlite.c:694,717,760`; do not log the full INSERT at `:748` (log row id only). Redact `<auth>/<challenge>/<response>`/`jabber:iq:register` payloads in `_xmpp_file_logger`. #### 3.5 — [REQ-AUTH-01](#req-auth-01) — Owner-only (0600) on every secret/history file — **Partial (high)** - **Evidence:** Met for keyfiles/accounts/OMEMO/tlscerts (`prof_keyfile` `g_chmod 0600`, `src/common.c:186/202`), main log (`src/log.c:177`), export (`src/database_export.c:329`). **Unprotected:** `src/database_sqlite.c:144` `sqlite3_open(filename,…)` has **no** following `chmod` (plaintext history DB inherits umask, typically 0644); OTR `keys.txt` (`src/otr/otr.c:372`) and `fingerprints.txt` (`otr.c:113,386`) written by libotr with no `chmod`. No process-wide `umask` anywhere in the tree. - **Why it matters:** NIST PR.AC / ISO A.8.3, A.5.15. The OTR *private signing key* and the decrypted-history DB are readable by other local users on a default umask. - **Fix:** `g_chmod(filename, S_IRUSR|S_IWUSR)` after `sqlite3_open`; `g_chmod` after each `otrl_privkey_generate`/`otrl_privkey_write_fingerprints`. #### 3.6 — [REQ-CRY-06](#req-cry-06) — Persisted unauthenticated OMEMO media plaintext — **Partial (medium)** - **Evidence:** `aes256gcm_crypt_file` writes all decrypted plaintext to the output `FILE*` (`src/omemo/crypto.c:445`) *before* `gcry_cipher_checktag` (`:462`). The caller opens `outfh` directly on the user's final destination (`src/tools/aesgcm_download.c:66`) and on tag failure only prints an error (`:114-118`) — it never removes the plaintext output (`remove(tmpname)` at `:112` deletes only the ciphertext temp), and the external-command branch (`:126-140`) still runs on a failed decrypt. *(Confirmed verbatim above.)* - **Why it matters:** NIST PR.DS-08 / ISO A.8.24. Violates the AEAD "no plaintext output on tag mismatch" mandate — a tampered/truncated media file yields a fully-persisted, attacker-controlled file at the user's path, possibly fed to an external opener. - **Fix:** Decrypt to a temp file, verify the tag, then atomically rename on success; on failure `remove()` the output and skip the `cmd_template` branch. #### 3.7 — [REQ-CRY-03](#req-cry-03) — No silent STARTTLS downgrade — **Partial (high)** - **Evidence:** Default (NULL/`force`) correctly sets `MANDATORY_TLS` (`src/xmpp/connection.c:185-186`). But the allowlisted `allow` policy (`src/common.c:406`) sets *neither* `MANDATORY_TLS` nor `DISABLE_TLS` (`connection.c:185-194`) → opportunistic STARTTLS, plaintext if the server omits it, with no log (flags==0 so the `:201` diagnostic never fires). In-band registration uses `xmpp_connect_raw` (`connection.c:496-515`); `_register_handle_features` secures only "if possible" (`:397-431`), so even `tls.policy=force` sends new-account credentials in cleartext when STARTTLS is absent. - **Why it matters:** NIST PR.DS-02 / ISO A.8.24. SASL/registration credentials can traverse plaintext under a documented, autocompleted policy. - **Fix:** Map `allow` to a hard "TLS-required-or-fail" floor or warn loudly; enforce TLS before in-band registration sends credentials. #### 3.8 — [REQ-MEM-01](#req-mem-01) — Full hardening baseline, build-system parity — **Partial (high)** - **Evidence:** autotools has stack-protector/FORTIFY/RELRO/`-Wformat=2` but **no** `-fPIE/-pie` and **no** `-Wl,-z,noexecstack` (`configure.ac`). `meson.build:64-71` gates stack-protector + FORTIFY behind `if is_debug`; the *release* path (`:33`, `executable()` at `:536`) gets none, and Meson has zero RELRO/now/noexecstack/PIE link args. No `checksec`/`readelf` CI gate. - **Why it matters:** NIST PR.PS / ISO A.8.31. Meson release binaries ship with no exploit mitigations; the two build systems are not at parity. - **Fix:** Add `-fPIE -pie -Wl,-z,noexecstack` (probed) to autotools; move Meson hardening out of `if is_debug` and add `b_pie=true` + relro/now/noexecstack link args; add a `checksec` CI assertion. #### 3.9 — [REQ-MEM-02](#req-mem-02) — ASan/UBSan gate the test suite in CI — **Partial (high)** - **Evidence:** `--enable-sanitizers` exists (`configure.ac:74-75,441-452`) but no CI job passes it (`ci-build.sh:193-205` defines 4 Linux configs, none with sanitizers; tree-wide grep finds no use). Valgrind *is* wired and gating (`ci-build.sh:295`). - **Why it matters:** NIST PR.PS / ISO A.8.28. The deliberate-heap-overflow / unsigned-overflow verification would not fail CI; memory-safety regressions can land. - **Fix:** Add a non-`continue-on-error` job running `./configure --enable-sanitizers && make check`. #### 3.10 — [REQ-EXT-04](#req-ext-04) — Plugin install: https + integrity verification — **Partial (high)** - **Evidence:** `cmd_plugins_install`'s `_http_based_uri_scheme` accepts both `http` and `https` (`src/command/cmd_funcs.c:7019`); plugin download reuses `http_file_get`, which sets `CURLOPT_SSL_VERIFY*=0L` when account `tls_policy=="trust"` (`src/tools/http_download.c:120-156`); the downloaded `.so/.py` is `plugins_install`'d then `dlopen`'d at full privilege with **no** signature/checksum check (`src/tools/plugin_download.c`, `src/plugins/plugins.c:198-218`). `copy_file` (`src/common.c:234-243`) doesn't strip group/world-write. Trust model undocumented. - **Why it matters:** NIST GV.SC / ISO A.5.23, A.8.25. A network MitM can substitute a malicious plugin that runs with full process privilege and access to all secret stores — RCE via plugin update. - **Fix:** Require `https` for plugin install; force `VERIFY*=1` regardless of XMPP `tls_policy`; verify a signature/checksum before install; `chmod 0600` installed files; document the full-privilege trust model. #### 3.11 — Other high-priority Gaps (concise) - **[REQ-SUP-02](#req-sup-02) (Gap):** CI base images use mutable tags (`ci/Dockerfile.*`), deps cloned at HEAD with no `git checkout <sha>`, Arch AUR snapshot fetched + `makepkg`'d unverified (`Dockerfile.arch:61-65`). → Digest-pin FROMs, commit-pin checkouts, `sha256sum -c` downloads. - **[REQ-SUP-03](#req-sup-03) (Partial, high):** gpgme uses unversioned `AC_CHECK_LIB([gpgme],[main])` (`configure.ac:309`); libgcrypt has no floor (`:351`); sqlite3/gcrypt/gpgme floors diverge from `meson.build`. → Versioned `PKG_CHECK_MODULES`, align floors, add a parity lint. - **[REQ-MEM-03](#req-mem-03) (Gap):** `strcpy/strcat` on peer barejids (`src/pgp/ox.c:153-156`), `sprintf` on key/nonce (`src/omemo/crypto.c:479,483`) — forbidden primitives, no CI grep. (Buffers are correctly sized, so residual is medium.) → Replace with `g_strdup_printf`/`snprintf`; add a CI grep. - **[REQ-MEM-04](#req-mem-04) (Gap):** No overflow-checked allocation anywhere; `omemo.c:1440 malloc(len*2+1)`, `http_upload.c:101-103` unchecked `size*nmemb`/realloc-overwrite, three `omemo.c:455/475/492 malloc(*length)` deref'd without NULL check. → `__builtin_mul_overflow`/`g_malloc_n`, NULL-check, save realloc to temp. - **[REQ-MEM-09](#req-mem-09) (Gap):** `alloca` sized from peer barejid length (`src/pgp/ox.c:150-151`), no `-Wvla`. → Replace with `g_strdup_printf`; add `-Werror=vla`. - **[REQ-INP-08](#req-inp-08) (Gap):** No control-char/bidi neutralization or length cap on display/log/DB sinks (`src/ui/window.c:2024,2120`); `str_xml_sanitize` is outgoing-only and skips U+202E. → Single sanitizer at display + log/DB write boundary. ### 4. Partial / medium findings (grouped) **CRY** - [REQ-CRY-01](#req-cry-01) — MUC OMEMO caller logs/echoes plaintext on NULL id (`client_events.c:200-203`, confirmed). → Guard `if (id != NULL)` like the 1:1 path. - [REQ-CRY-02](#req-cry-02) — OTR-opportunistic first message bypasses `allow_unencrypted_message` (`src/otr/otr.c:332-339,425-428`). → Consult the gate before the tagged cleartext send. - [REQ-CRY-07](#req-cry-07) — `get_random_string` uses `g_rand_*` (Mersenne-Twister) for stanza IDs and the `profanity_instance_id` HMAC key (`src/common.c:712-717`, `connection.c:808,1123`). → Switch identifiers/HMAC seed to `gcry_create_nonce`. - [REQ-CRY-09](#req-cry-09) — `blind`/`firstusage` modes persist trust from inbound handlers (`src/omemo/omemo.c:608,698`). → Confine trust transitions to user commands. **AUTH** - [REQ-AUTH-03](#req-auth-03) — Insecure flags logged at `log_debug`, not WARN (`connection.c:201-213`). → WARN + `cons_show` on `DISABLE_TLS|LEGACY_*|TRUST_TLS`. - [REQ-AUTH-05](#req-auth-05) — `eval_password` spawned with `G_SPAWN_SEARCH_PATH` (`src/config/account.c:149`). → Require absolute path / drop the flag. - [REQ-AUTH-06](#req-auth-06) — Account/eval/PGP passphrases freed without wipe (`account.c:184-185`, `session.c:581/591`, `gpg.c:79/104/108`). → `explicit_bzero` before free. **INP** - [REQ-INP-02](#req-inp-02) — Validation correct; the prescribed boundary/forbidden/`@@`/UTF-8 test table is absent (`tests/.../test_jid.c`). → Add the table-driven suite. - [REQ-INP-05](#req-inp-05) — MAM `<result>` id not disco-gated (`message.c:1536-1571` → stored at `:1409-1410`). → Apply `_stanza_id_by_trusted` on the MAM path. - [REQ-INP-06](#req-inp-06) — `/url open|save` accept any scheme (`cmd_funcs.c:9633-9655,9678-9698`). → Whitelist http/https/aesgcm. - [REQ-INP-07](#req-inp-07) — OSX notifier uses `system()` with hand-rolled escaping (`src/ui/notifier.c:160`). → `g_spawn` argv / `g_shell_quote`. **MEM** - [REQ-MEM-05](#req-mem-05) — `omemo.c:1437 identity_public_key_len--` lacks a `>0` guard. → Reject zero-length before decrement. - [REQ-MEM-07](#req-mem-07) — `identity_key_store_destroy` doesn't NULL freed fields (`store.c:51-53`); undocumented out-param ownership. *(Double-free unreachable — `memset` at `omemo.c:307`.)* → Document ownership + add `=NULL`. - [REQ-MEM-08](#req-mem-08) — Good NULL-guarding by convention but `-Wnull-dereference` only under non-CI `--enable-hardening`. → Enable in one CI config. **LOG / DAR** - [REQ-LOG-01](#req-log-01) — SASL auth failure at INFO (`server_events.c:207`); TLS cert-fail logs nothing (`server_events.c:1135-1200`). → Promote both to WARN. - [REQ-LOG-02](#req-log-02) — TLS-disabled / handshake-fail / see-other-host / insecure-login all DEBUG-only. → WARN on all four. - [REQ-LOG-06](#req-log-06) — stderr→log bridge applies no redaction (`src/log.c:289,297`). → Redact secret markers before `log_msg`. **CFG / VUL / SDLC** - [REQ-CFG-01](#req-cfg-01) — `tls.policy=trust` disables curl verify silently on transfers, no user warning (`http_upload.c:242-244`, `http_download.c:153-155`). → Surface the insecure state per transfer. - [REQ-VUL-02](#req-vul-02) — `release_get_latest` never sets `VERIFY*`/https-only (`src/common.c:459-482`). → Set them explicitly. - [REQ-VUL-03](#req-vul-03) — `PREF_REVEAL_OS` defaults TRUE (`preferences.c:2692,2706`); no `<version>`-omit pref. → Default OFF; add version toggle. - [REQ-VUL-04](#req-vul-04) — No per-release Security/CVE changelog convention; `RELEASE_GUIDE.md` has no security step. → Add convention + review step. - [REQ-EXT-03](#req-ext-03) — No `CURLOPT_CONNECTTIMEOUT`; no cap/stall test (`ai_client.c`). → Add connect-timeout + tests. - [REQ-RES-02](#req-res-02) — `PRAGMA integrity_check` only on `/history verify`, never at DB open (`database_sqlite.c`). → Run quick_check in `_sqlite_init`. - [REQ-SDLC-03](#req-sdlc-03) — No `CODEOWNERS`; commitlint config dormant (`.commitlintrc.json` unreferenced). → Add CODEOWNERS + commitlint CI job. - [REQ-SDLC-04](#req-sdlc-04) — Style/auto-type/CWE-134/spell checks marked `continue-on-error` (`ci-code.yml:40,94`). → Remove `continue-on-error`. - [REQ-SDLC-06](#req-sdlc-06) — No invariant→test REQ-ID traceability matrix. → Add `compliance/REQ-TO-TEST.md`. ### 5. What cproof does well (notable Met items) - **[REQ-CRY-01](#req-cry-01) — Fail-closed E2EE on the wire.** Every PGP failure branch returns NULL before `_send_message_stanza` (`message.c:491-517`); OX returns NULL before send (`:564-571`); OMEMO `keys==NULL` does `goto out` without reaching `message_send_chat_omemo` (`omemo.c:928-966`). The `622054fc6` fix is real and the 1:1 caller guards `if (id != NULL)` (`client_events.c:138/148/157`). - **[REQ-CRY-04](#req-cry-04) — TLS cert validation fails closed.** Certfail handler always registered (`connection.c:226`); blind trust only on explicit `tls.policy=trust`; `sv_ev_certfail` accepts only via prior-trust or explicit `/tls allow|always`, else `return 0` (`server_events.c:1134-1191`). - **[REQ-CRY-10](#req-cry-10) — Fail-safe OMEMO trust.** Default `manual` mode; untrusted devices get no session (`omemo.c:702-705`) and are skipped; `keys==NULL` aborts with a remediation listing and no stanza. - **[REQ-AUTH-02](#req-auth-02) — 0700 secret dirs.** All dir creation routes through `create_dir` → `g_mkdir_with_parents(name, S_IRWXU)` (`common.c:224`); no 0755 bypass anywhere. - **[REQ-INP-03](#req-inp-03) — No CWE-134 format-string sinks.** Every variadic wrapper carries `G_GNUC_PRINTF`; all untrusted classes (URLs, plugin, AI response) are `%s`-wrapped; `check-cwe134.sh` exits 0. - **[REQ-MEM-06](#req-mem-06) — Auto-cleanup macros enforced.** Nine `auto_*` macros, 1105 uses, CI asserts the set and fails on uninitialized `auto_*` locals (`ci-code.yml:48,51`). - **[REQ-DAR-01](#req-dar-01)/[REQ-DAR-02](#req-dar-02) — `off`/`redact` modes are airtight.** Both backends early-return / placeholder-substitute *before* any persistence (`database_sqlite.c:640-647`, `database_flatfile.c:363-373`). - **[REQ-EXT-02](#req-ext-02) — AI consent is opt-in.** Content reaches a provider only from a user-opened AI window; no inbound hook auto-forwards; default providers carry no key so no network call occurs; history is volatile-only. - **[REQ-RES-01](#req-res-01)/[REQ-RES-03](#req-res-03) — Migration preflight + state-preserving reconnect.** Disk-space gate + single-transaction migrations with ROLLBACK (`database_sqlite.c:252-258,783-918`); XEP-0198 SM state captured/re-applied with no E2EE-flag downgrade on reconnect; OTR torn down fail-safe. ### 6. Notable verifier disagreements Several auditor "Met" verdicts were **overturned to Partial** by the adversarial verifier, and these corrections are material — they prevent over-stating compliance: - **[REQ-CRY-01](#req-cry-01)** — The auditor noted the caller logs/echoes only when `id != NULL`. The verifier found the **MUC OMEMO** caller (`client_events.c:200-203`) unconditionally logs the plaintext to the SQLite history DB and echoes it on screen even when `omemo_on_message_send` returns NULL. Confirmed verbatim in this audit. Net: no network plaintext, but a local DB + UX leak on the MUC failure path. - **[REQ-CRY-02](#req-cry-02)** — Auditor: the gate covers all cleartext sends. Verifier: the OTR-opportunistic first message returns TRUE from `otr_on_message_send`, short-circuiting `&& allow_unencrypted_message()`, so the force-encryption gate is never consulted for that cleartext send. - **[REQ-CRY-06](#req-cry-06)** — The auditor flagged only a benign short-tag nuance. The verifier found the decisive defect: file plaintext is persisted to the user's destination *before* tag verification, and the post-download external command still runs on decrypt failure. - **[REQ-CRY-07](#req-cry-07)** — The auditor (grep scoped to `omemo/otr/pgp`) declared CSPRNG "Met". The verifier found `g_rand_*` (named as forbidden in the requirement) used for stanza IDs and an HMAC key seed in `src/common.c`/`src/xmpp`. - **[REQ-CRY-09](#req-cry-09) / [REQ-INP-05](#req-inp-05)** — Auditor "Met"; the verifier found inbound-handler trust transitions (blind/firstusage) and an un-disco-gated MAM `<result>` id, respectively. - **[REQ-MEM-07](#req-mem-07)** — The verifier *lowered* residual risk (medium→low): the "latent double-free" is unreachable because `signal_protocol_store_context_destroy` is guarded and immediately followed by `memset(&omemo_ctx, 0, …)` (`omemo.c:302-307`). In every overturned case the verifier's corrected status is the one of record; the disagreements were due to the original auditors' greps being scoped too narrowly (missing `src/common.c`, `src/xmpp`, the MUC caller, the MAM path) rather than misreading the cited code. ### 7. Methodology & limitations - **Scope:** Static source analysis of `master` at HEAD `622054fc6`, read directly from the working tree, requirement-driven against the catalog in Part 2. Each consequential finding was produced by a domain auditor and independently re-verified by an adversarial verifier; where they disagreed, the corrected status is reported. - **Not a pentest:** No dynamic exploitation, fuzzing, or runtime instrumentation was performed. DoS/overflow reachability is argued from code paths, not demonstrated with live inputs; exploitability assessments (e.g. the correctly-sized `alloca`/`sprintf` buffers in [REQ-MEM-03](#req-mem-03)/[REQ-MEM-09](#req-mem-09)) reflect code review, not runtime proof. - **Server-side items are Unknown by nature:** GitHub branch-protection / required-status-check enforcement ([REQ-SDLC-03](#req-sdlc-03)) cannot be verified from the tree; only the in-repo prerequisites (absent `CODEOWNERS`, dormant commitlint) are confirmed. The PGP fingerprint currency in `SECURITY.md` ([REQ-VUL-01](#req-vul-01)) is likewise unverifiable here. - **Build-config caveats:** Findings about Meson vs autotools parity ([REQ-MEM-01](#req-mem-01), [REQ-SUP-03](#req-sup-03)) are read from build files, not from inspecting produced binaries; a `checksec`/`readelf` pass on actual release artifacts would confirm the shipped mitigation set. - **No status was inflated:** Several requirement-named defects ([REQ-INP-01](#req-inp-01), [REQ-DAR-03](#req-dar-03), [REQ-MEM-03](#req-mem-03)/[REQ-MEM-09](#req-mem-09)) were confirmed *still present and unfixed* at HEAD; conversely, `null`-verdict items retain their auditor "Met" status and were not independently re-derived here. ### CIS Controls v8.1 — source compliance **Target:** cproof `master`, HEAD `622054fc6` (includes the merged PGP plaintext-fallback fix). **Catalog:** Part 2 below (NIST CSF 2.0 + ISO 27001 + the CIS v8.1 section). **Method:** Static source analysis, per-control auditors with adversarial verification, cross-validated against the NIST/ISO source audit above. #### CIS 1. Executive summary Measured against the CIS v8.1 **IG1 "essential cyber hygiene"** baseline as it applies to a desktop client + its repository, cproof's *runtime/cryptographic* posture is strong but its *producer/process* posture is weak. The strongest1 areas are exactly where the NIST/ISO audit found Met items: vetted, well-established crypto modules rather than home-grown primitives (CIS-16.11 — libsignal/libomemo-c, libgcrypt, gpgme, libotr; [REQ-CRY-06](#req-cry-06)/[REQ-CRY-07](#req-cry-07)/[REQ-CRY-08](#req-cry-08)), TLS-mandatory-by-default in transit with fail-closed certificate validation (CIS-4.6/15.4 ↔ [REQ-CRY-03](#req-cry-03)/[REQ-CRY-04](#req-cry-04)), secure-by-default values (OMEMO trust `manual`, enc-warn/tls-show on; CIS-4.1 ↔ [REQ-CRY-10](#req-cry-10)/[REQ-CFG-02](#req-cfg-02)), the ability to compile out or disable every higher-risk subsystem (CIS-4.8 — Met), and CWE-134-clean, auto-cleanup-enforced secure-coding conventions (CIS-16.12 partial ↔ [REQ-INP-03](#req-inp-03)/[REQ-MEM-06](#req-mem-06)). The most serious CIS gaps are all *process/supply-chain/data-at-rest* and they **independently cross-validate gaps the NIST/ISO audit already found**: the libstrophe Meson wrap pinned to `revision = HEAD` with no SHA/`source_hash` (CIS-2.2/2.6/7.2/16.5 ↔ [REQ-SUP-01](#req-sup-01), confirmed in `subprojects/libstrophe.wrap:3`); decrypted plaintext message bodies written to `profanity.log` in the **default** `dblog=on` mode (CIS-8.5 ↔ [REQ-DAR-03](#req-dar-03)/[REQ-LOG-03](#req-log-03), four `log_*` sites in `database_sqlite.c`); no code-level/dependency security scanning in CI — no OSV/Dependabot/trivy, no secret scan, no fuzzing, and the `--enable-sanitizers` build never exercised (CIS-7.4/16.12/18.1 ↔ [REQ-SUP-04](#req-sup-04)/[REQ-MEM-02](#req-mem-02)/[REQ-SDLC-05](#req-sdlc-05)); no real vulnerability-disclosure process — `SECURITY.md` is 5 lines (contact + PGP fingerprint only) with no supported-versions, scope, or SLA (CIS-7.1/16.2 ↔ [REQ-VUL-01](#req-vul-01)); and the AI/LLM client accepting `http://` provider URLs so the API key + full conversation can egress in cleartext (CIS-15.4 ↔ [REQ-EXT-01](#req-ext-01)). Two further first-class CIS gaps with no exact single REQ predecessor are the **absence of any maintained SBOM/third-party-software inventory** (CIS-2.1/16.4) and the **absence of any documented audit-log-management/logging policy** (CIS-8.1). Net IG1 verdict: cproof would *not* pass a clean IG1 review today — primarily on CIS-2 (inventory/supported-software), CIS-7 (vuln-management/patch automation), and CIS-8 (audit-log content/process), not on its cryptographic or transport controls. #### CIS 2. Scorecard Counts are over the *relevant safeguards within each applicable control* (the safeguards processed in this audit), plus a per-control Applicability tag. N/A counts the safeguards that are enterprise-IT within an otherwise-applicable control. | Control | Applicability | Met | Partial | Gap | N/A | |---|---|---|---|---|---| | CIS-2 Inventory & Control of Software Assets | Partial (software/deps) | 0 | 3 | 0 | 0 | | CIS-4 Secure Configuration | Partial (app config) | 1 | 2 | 0 | 0 | | CIS-5 Account Management | Partial (credential storage) | 0 | 1 | 0 | 1 | | CIS-6 Access Control Management | N/A (no exposed app) | 0 | 0 | 0 | 1 | | CIS-7 Continuous Vulnerability Management | Applicable (producer) | 0 | 2 | 1 | 0 | | CIS-8 Audit Log Management | Partial (app log) | 0 | 2 | 2 | 7 | | CIS-9 Email & Web Browser Protections | Partial (URL/file surface) | 0 | 2 | 0 | 0 | | CIS-15 Service Provider Management | Applicable (providers/upstreams) | 0 | 2 | 0 | 0 | | CIS-16 Application Software Security | Applicable (core) | 5 | 6 | 3 | 0 | | CIS-18 Penetration Testing | Partial (app fuzz/sanitizer) | 0 | 0 | 2 | 0 | | **Overall (applicable safeguards)** | — | **6** | **22** | **10** | **10** | > CIS-16 counts fold in the mapping from the catalog: Met = 16.10/16.11 (secure design + vetted modules), 16.8 (prod/non-prod), and the secure-coding-convention slices of 16.12; Partial = 16.2/16.4/16.5/16.6/16.7/16.9 (inventory/process/severity/hardening-doc/training gaps); Gap = 16.12 (no sanitizer/fuzz/scan in CI), 16.13/18.1 (no pen-test/fuzz program). Enterprise-only controls CIS-1/3/10/11/12/13/14/17 are summarized in [CIS 5](#cis-5-not-applicable-enterprise-it-controls), not scored here. #### CIS 3. Critical & high-priority CIS gaps (ranked) **3.1 — CIS-2.1 / 16.4 — No software / third-party-component inventory (SBOM) — Partial→Gap (IG1)** - **Evidence:** The de-facto dependency list exists only as build declarations: `configure.ac` `PKG_CHECK_MODULES` for glib-2.0>=2.62.0 (:89), libcurl>=7.62.0 (:97), sqlite3>=3.22.0 (:107), libstrophe>=0.12.3 (:176), libotr>=4.0 (:326), libsignal-protocol-c>=2.3.2 (:349); `meson.build` mirrors a subset with **divergent floors** (sqlite3>=3.35.0 :96, libomemo-c>=0.5.1 :273, libgcrypt>=1.7.0 :276). No `*.spdx`/`*.cdx`/`bom.*` in the tree, no README/INSTALL dependency table, no machine-readable inventory; the only narrative inventory is [REQ-SUP-03](#req-sup-03) in this very catalog. - **Why it matters:** IG1 foundational. Without an authoritative, version-accurate component list one cannot answer "are we affected by CVE-X?" — the precondition for CIS-7 and CIS-16.5. The two build files are not even self-consistent (sqlite floor 3.22.0 vs 3.35.0; gpgme/gcrypt unversioned in autotools), so no single source of truth exists. - **Remediation:** Ship a CycloneDX/SPDX SBOM generated+diff-checked in CI ([REQ-CIS-01](#req-cis-01)), listing every linked library with min-version/license/URL; reconcile the autotools-vs-meson floors to one source. Cross-validates [REQ-SUP-03](#req-sup-03). **3.2 — CIS-7.1 / 16.2 — No vulnerability-disclosure / management process — Partial (IG1)** - **Evidence:** `SECURITY.md:1-7` is contact + PGP fingerprint (`6F88C537A25E481489574956E3370C9A9A55BF71`) only — confirmed verbatim — with no supported-versions, no in/out-of-scope component enumeration, no ack/remediation SLA, no coordinated-disclosure terms. No `.github/dependabot.yml`; a tree-wide search for osv-scanner/trivy/grype/snyk/renovate returns nothing. Dependency floors exist and the build fails below most of them, but they are static, undocumented as to rationale, and not driven by any recurring review cadence. - **Why it matters:** IG1. The producer has no defined, recurring path to identify, severity-rate, and triage vulnerabilities in the third-party set (libstrophe/gpgme/libsignal/libgcrypt/libotr/sqlite/glib/libcurl), and the inbound disclosure half lacks scope and SLA. - **Remediation:** [REQ-CIS-02](#req-cis-02) + [REQ-VUL-01](#req-vul-01): flesh out `SECURITY.md` with Supported Versions, scope, ack+remediation SLA, coordinated-disclosure terms; document a monthly + on-push review cadence fed by an automated scanner. **3.3 — CIS-7.4 / 16.12 — No automated dependency patch/CVE management in CI — Gap (IG1)** - **Evidence:** `.github/workflows/ci-code.yml` runs only the Linux build matrix (arch/debian/ubuntu, :22-35), a `continue-on-error` code-style job (:37-89), a `continue-on-error` spell-check (:91-103), and coverage (:105-113). No dependency-CVE scan, no Dependabot, no OSV/trivy/grype/renovate. The opposite of patch management is present in image builds: `ci/Dockerfile.debian:2` `FROM debian:testing` (mutable), :44-45 `git clone --depth 1` of stabber and libstrophe at floating HEAD with no `git checkout <sha>`; `ci/Dockerfile.arch:1` `FROM archlinux:latest`, :61-65 `wget` of an AUR `libstrophe-git` snapshot then `makepkg` with no sha256/gpg verification. GitHub Actions are pinned to mutable tags (`actions/checkout@v4`). The only patch-adjacent code, `release_get_latest` (`src/common.c:459-482`), merely fetches a version string for a newer-version notice — no download/verify/apply. - **Why it matters:** IG1. Even when an upstream fix exists, nothing surfaces or applies it, and floating-HEAD/`:latest`/`:testing` sources make builds non-reproducible — the same provenance gap as [REQ-SUP-02](#req-sup-02). - **Remediation:** Add OSV-Scanner/Dependabot/Renovate on push + cron, failing above a severity threshold ([REQ-SUP-04](#req-sup-04)); digest-pin Dockerfile `FROM`s, commit-pin clones, sha256/gpg-verify the AUR snapshot ([REQ-SUP-02](#req-sup-02)); SHA-pin Actions ([REQ-SUP-05](#req-sup-05)). Captured as [REQ-CIS-04](#req-cis-04). **3.4 — CIS-2.6 / 16.5 — Dependencies not pinned/allowlisted to an authorized identity — Partial (IG2)** - **Evidence:** System deps are bounded only by a minimum-version *lower bound* (no upper bound, no exact pin, no checksum). The one wrapped dependency, libstrophe, is **not** pinned: `subprojects/libstrophe.wrap:3` `revision = HEAD` with no 40-hex SHA or `source_hash` — confirmed verbatim — so any clone links whatever upstream HEAD is. Two crypto deps are not version-gated at all in autotools: gpgme via bare `AC_CHECK_LIB([gpgme],[main])` (`configure.ac:309`) and libgcrypt via a symbol-presence probe `AC_CHECK_LIB([gcrypt],[gcry_md_extract])` (`:351`). At runtime, dlopen'd plugins are directory-restricted (0700 dir) but **not** signature/checksum-verified, and `/plugins install` accepts `http` and reuses a downloader that disables TLS verify under `tls_policy=trust`. - **Why it matters:** IG2. Neither the build-time library set nor the runtime-loaded code is pinned to an integrity-verified identity; an attacker influencing the wrap source, a CI mirror, or a plugin download gets unauthorized code linked/loaded. - **Remediation:** Pin `libstrophe.wrap` to an immutable SHA/tag+hash and lint against `revision = HEAD` ([REQ-SUP-01](#req-sup-01)); versioned `PKG_CHECK_MODULES` for gpgme + a gcrypt floor ([REQ-SUP-03](#req-sup-03)); checksum/signature-verify plugins before `dlopen` ([REQ-EXT-04](#req-ext-04)). Captured as [REQ-CIS-03](#req-cis-03). **3.5 — CIS-8.5 — Detailed logs capture decrypted plaintext (and, at DEBUG, credentials) — Partial, critical-priority (IG2)** - **Evidence:** In the **default** `PREF_DBLOG="on"` mode, `database_sqlite.c:694` embeds `message->plain` (LMC sender mismatch), `:717` `...content: %s` (duplicate stanza-id), `:748` `log_debug("Writing to DB. Query: %s")` logs the full INSERT that embeds `message->plain` (bound at :735), and `:760` `...content: %s` on insert failure. Triggers at :694/:717 are remotely influenceable (XEP-0308 LMC, server-assigned stanza-id). When strophe verbosity/DEBUG is raised, `_xmpp_file_logger` (`connection.c:1088-1107`) forwards raw SASL `<auth>/<challenge>/<response>` and XEP-0077 registration credentials with no redaction; the stderr bridge (`log.c:269-300`) also redacts nothing. - **Why it matters:** IG2, but the highest-severity CIS finding for an E2EE client: the "detailed" tier captures decrypted bodies in the shipped default, defeating E2EE for anyone who shares a log. Cross-validates [REQ-DAR-03](#req-dar-03)/[REQ-LOG-03](#req-log-03). - **Remediation:** Drop `message->plain`/`content:`/`Message:` from the format args at :694/:717/:760 (log JID/id/size/enc-type only); log the row id, not the INSERT, at :748; redact `<auth>/<challenge>/<response>`/`jabber:iq:register` in `_xmpp_file_logger`; add a CI grep blocking `log_*` interpolation of `message->plain`/`body`/`password`/`api_key`/`Bearer`. **3.6 — CIS-15.4 — No enforced security baseline toward configurable providers (AI http://) — Partial, high (IG2)** - **Evidence:** `/ai set provider <name> <url>` (`cmd_funcs.c:10752-10757`) and `ai_add_provider` (`ai_client.c:574`) store `api_url` verbatim with **no scheme validation**, so `http://` is accepted; both AI curl sites (`ai_client.c:811-815`, `1596-1601`) set neither `CURLOPT_SSL_VERIFYPEER` nor `VERIFYHOST` (rely on libcurl defaults), while `Authorization: Bearer <key>` plus the full conversation is POSTed. The XMPP `allow` policy sets neither MANDATORY_TLS nor DISABLE_TLS (`connection.c:185-194`) — silent opportunistic STARTTLS. libstrophe is consumed at mutable HEAD (no integrity requirement on that supplier). - **Why it matters:** IG2. A user can configure a provider that receives the API key and all message content over cleartext HTTP. Cross-validates [REQ-EXT-01](#req-ext-01) (AI http://) and [REQ-CRY-03](#req-cry-03) (allow-policy downgrade). - **Remediation:** Reject non-`https` `api_url` at provider-add and request time and set VERIFYPEER=1L/VERIFYHOST=2L explicitly ([REQ-EXT-01](#req-ext-01)); map `allow` to a TLS-required-or-fail floor ([REQ-CRY-03](#req-cry-03)); pin the libstrophe wrap ([REQ-SUP-01](#req-sup-01)). Captured as [REQ-CIS-08](#req-cis-08). **3.7 — CIS-8.2 — Two security events fall below the default WARN filter — Partial, high (IG1)** - **Evidence:** The app log is collected (`src/log.c` `_log_msg`, default WARN, 0600 at :177, size rotation, stderr bridge). But SASL/login failure logs at `log_info` ("Login failed", `server_events.c:208`) and TLS certificate-validation failure (`sv_ev_certfail`, `server_events.c:1135`) emits no `log_*` at all (UI `cons_show` only) — both below the shipped WARN filter, so they are effectively not collected at the default level. Cross-validates [REQ-LOG-01](#req-log-01)/[REQ-LOG-02](#req-log-02). - **Remediation:** Promote SASL-failure to `log_warning` (`server_events.c:208`); add a `log_warning` in `sv_ev_certfail` before the prompt; add WARN lines on DISABLE_TLS/non-mandatory-TLS/see-other-host ([REQ-LOG-02](#req-log-02)). **3.8 — CIS-2.2 — Authorized software not assured current/supported — Partial, high (IG1)** - **Evidence:** Floors are declared and the build hard-fails below them for the required deps, but gpgme and libgcrypt have **no version floor in autotools** (any/EOL accepted), the libstrophe wrap floats to HEAD with no supported pinned release, floors carry no support/EOL rationale, and there is no automated "is this version still maintained?" check. - **Remediation:** Versioned `PKG_CHECK_MODULES([gpgme], …)` + a gcrypt floor with CVE/support rationale; pin libstrophe to a supported tag/commit ([REQ-SUP-01](#req-sup-01)); add a CI EOL/CVE scan ([REQ-SUP-04](#req-sup-04)). Cross-validates [REQ-SUP-03](#req-sup-03). **3.9 — CIS-8.1 — No audit-log-management / logging policy — Gap, medium (IG1)** - **Evidence:** `compliance/` holds only the catalog + source analysis; `docs/` holds only man pages; no doc defines which security events SHALL be captured, at what level, retention, or review. Mechanics exist (`_should_log` level filter, size rotation, default WARN) but per-event decisions are inconsistent (see 3.7: cert-fail/SASL-fail below WARN, plaintext bodies logged by default). - **Remediation:** Add [REQ-CIS-05](#req-cis-05) — a short logging policy enumerating WARN+ security events, the no-secrets/no-plaintext-body constraint ([REQ-DAR-03](#req-dar-03)), default level ([REQ-LOG-04](#req-log-04)), rotation bounds ([REQ-LOG-05](#req-log-05)), 0600 mode ([REQ-AUTH-01](#req-auth-01)), plus a CI grep on plaintext/secret interpolation. **3.10 — CIS-7.2 — Remediation capability exists but is ungoverned — Partial, high (IG1)** - **Evidence:** cproof *does* remediate (CHANGELOG:1018 fixes CVE-2017-5592; HEAD `622054fc6 fix(pgp): prevent plaintext fallback`), but no prioritization/SLA is documented, the HEAD-pinned libstrophe gives no immutable baseline to remediate *to* ([REQ-SUP-01](#req-sup-01)), and `RELEASE_GUIDE.md` has no security/CVE step (grep for security|cve|vuln|scan|patch returns nothing). - **Remediation:** [REQ-CIS-02](#req-cis-02): severity-keyed remediation SLA, a per-fix regression test, a per-release Security/CVE changelog convention ([REQ-VUL-04](#req-vul-04)), and SHA-pinned libstrophe ([REQ-SUP-01](#req-sup-01)). **3.11 — CIS-18.1 / 16.13 — No penetration-testing / fuzzing program; sanitizers unused in CI — Gap, medium (IG2/IG3)** - **Evidence:** Tree-wide grep for fuzz|pentest|oss-fuzz|libfuzzer|afl matches only the compliance docs; no `*/fuzz*//*/corpus*/` files exist. Only defensive testing is present: scan-build + check-cwe134.sh, cmocka under Valgrind, coverage. The `--enable-sanitizers` ASan/UBSan build exists (`configure.ac`) but is **never run in CI** (cross-validates [REQ-MEM-02](#req-mem-02)). No fuzz harness over the untrusted-network parsers (stanza/XML, `jid_create`, OMEMO/PGP/OX decode) — cross-validates [REQ-SDLC-05](#req-sdlc-05) and explains why DoS findings like the remote NULL-deref ([REQ-INP-01](#req-inp-01), `message.c`/`presence.c`) are argued from code, not demonstrated. - **Remediation:** [REQ-CIS-04](#req-cis-04): stand up a libFuzzer/AFL++ target over the parse paths in a scheduled CI job; wire `--enable-sanitizers && make check` as a non-`continue-on-error` job; document a proportionate per-release adversarial-review checklist. A full external network pen-test (18.1 as literally written) is N/A — no estate. #### CIS 4. What cproof does well (CIS) - **CIS-16.11 — Leverage vetted modules for security components.** All E2EE/crypto is delegated to established libraries (libsignal-protocol-c/libomemo-c double ratchet, libgcrypt CSPRNG + AEAD, gpgme, libotr); cproof implements no home-grown KDF/ratchet ([REQ-CRY-06](#req-cry-06)/[REQ-CRY-07](#req-cry-07)/[REQ-CRY-08](#req-cry-08), Met). - **CIS-4.6 / 15.4 / 3.10-equiv — TLS-in-transit mandatory + fail-closed.** NULL/empty/`force` `tls.policy` sets `XMPP_CONN_FLAG_MANDATORY_TLS` (`connection.c:185-186`); insecure flags reachable only via explicit policy strings; `tls.policy` allowlist-validated on load with secure fallback (`common.c:406`); certfail handler always registered, defaulting to abort ([REQ-CRY-03](#req-cry-03)/[REQ-CRY-04](#req-cry-04)/[REQ-CRY-05](#req-cry-05), Met-default). - **CIS-4.1 — Secure-by-default configuration.** OMEMO trust `manual` not blind (`preferences.c:2789-2790`), `PREF_ENC_WARN`/`PREF_TLS_SHOW` default TRUE (`:2663-2706`), force-encryption resend-to-confirm default ([REQ-CRY-10](#req-cry-10)/[REQ-CFG-02](#req-cfg-02)). - **CIS-4.8 — Disable unnecessary services (Met).** Compile-time toggles for every risk-bearing subsystem (`--enable-otr/pgp/omemo`, `--enable-c-plugins/python-plugins/plugins`, `--enable-notifications`); AI egress disabled by default (empty `PREF_AI_API_KEY`, no network call); plugins never auto-load and load only from the 0700 dir ([REQ-EXT-02](#req-ext-02)/[REQ-EXT-04](#req-ext-04)). - **CIS-16.12 (slice) — Code-level secure-coding conventions.** Every variadic wrapper carries `G_GNUC_PRINTF`; all untrusted classes are `%s`-wrapped; `check-cwe134.sh` exits 0; nine `auto_*` cleanup macros are CI-asserted ([REQ-INP-03](#req-inp-03)/[REQ-MEM-06](#req-mem-06), Met). - **CIS-16.8 — Production/non-production separation.** Functional tests run against the stabber mock with isolated XDG dirs and `@localhost` identities; no real credentials in fixtures ([REQ-SDLC-02](#req-sdlc-02)). - **CIS-9.3/9.6 (mitigations) — No shell injection on the URL/file surface.** URLs reach external helpers only as discrete argv tokens via `format_call_external_argv` + `call_external` (`g_spawn_async`, never a shell), and peer download filenames collapse path traversal via `g_file_get_basename` ([REQ-INP-06](#req-inp-06)/[REQ-CFG-01](#req-cfg-01)) — so the residual risk is scheme/file-type abuse, not RCE. #### CIS 5. Not applicable (enterprise-IT) controls - **CIS-1 Inventory and Control of Enterprise Assets** — no hardware/device fleet to discover or control. - **CIS-3 Data Protection (org data-classification/DLP)** — client-side at-rest is covered by [REQ-AUTH-01](#req-auth-01)/[REQ-AUTH-02](#req-auth-02), [REQ-DAR-01](#req-dar-01)..[07](#req-dar-07); org-wide classification/DLP is N/A. - **CIS-5.4 / 6.3** — no administrator-account model; no inbound externally-exposed application (outbound SASL client; MFA is a server/SASL concern). Adjacent posture: [REQ-AUTH-03](#req-auth-03)/[REQ-CRY-03](#req-cry-03). - **CIS-8.6/8.7/8.8/8.9/8.10/8.11/8.12** — DNS/URL/command-line/centralized/retention/review/service-provider log safeguards target network/SIEM/SOC infrastructure a client does not operate. - **CIS-10 Malware Defenses** — host-OS AV/EDR responsibility. - **CIS-11 Data Recovery** — enterprise backup program; the client analogue is migration integrity ([REQ-RES-01](#req-res-01)/[REQ-RES-02](#req-res-02)). - **CIS-12 Network Infrastructure Management / CIS-13 Network Monitoring and Defense** — no owned network infrastructure or monitoring estate. - **CIS-14 Security Awareness Training** — org HR program; developer secure-coding maps to CIS-16.9 ↔ [REQ-SDLC-04](#req-sdlc-04)/[REQ-SDLC-06](#req-sdlc-06). - **CIS-17 Incident Response Management** — org IR program; in-repo analogue is vuln handling, CIS-16.2/7.1 ↔ [REQ-VUL-01](#req-vul-01) / [REQ-CIS-02](#req-cis-02). - **CIS-18.2/18.4/18.5** — external estate pen-test operations; the applicable application slice (18.1/18.3, 16.13) is handled via [REQ-CIS-04](#req-cis-04) / [REQ-SDLC-05](#req-sdlc-05) / [REQ-MEM-02](#req-mem-02). #### CIS 6. Methodology & limitations - **Scope:** Static source analysis of `master` at HEAD `622054fc6`, read directly from the working tree, against the official CIS v8.1 control/safeguard structure. Each applicable safeguard was assessed by a control auditor and adversarially verified; verifier-corrected statuses are reported. - **Source caveat:** The CIS v8.1 PDF originally linked returned HTTP 404; the authoritative CIS v8.1 control numbers, safeguard numbers/titles, and IG1/IG2/IG3 tags were taken from the official CIS Controls v8.1 structure (https://www.cisecurity.org/controls/v8-1) and used verbatim — no safeguard numbers were invented. - **Reinterpretation:** CIS is an enterprise-IT framework; many safeguards were reinterpreted for a single-user client + its repository (e.g. 2.6 "allowlist authorized libraries" → build/runtime dependency pinning; 8.x "audit logs" → the application log's content/level/policy; 15.4 "service-provider contracts" → the technical security baseline enforced toward each provider). Enterprise-only controls are summarized as N/A. - **Not a pentest:** No dynamic exploitation, fuzzing, or runtime instrumentation was performed — itself the substance of the CIS-18.1/16.13 gap. Reachability of DoS/leak paths is argued from code, not demonstrated. - **Cross-validation:** Every CIS finding was reconciled against the NIST CSF 2.0 / ISO 27001 source audit above; where CIS independently confirms an already-found gap (libstrophe `revision = HEAD`, plaintext in logs, no CI security scanning, contact-only SECURITY.md, AI `http://` egress), the existing REQ-ID is cited rather than re-derived. ## Part 2 — Requirements catalog This catalog defines the software security requirements for cproof. Each requirement is rendered as its own heading whose text is the bare ID, so the auto-anchor is exactly the lowercased id; the bold title, the SHALL statement, and the italic traceability line follow. ### 1. Scope and Introduction This catalog defines the software security requirements for **cproof**, a fork of the Profanity console XMPP/Jabber chat client (~90k LOC, C, autotools build). Requirements are derived from and traced to **NIST CSF 2.0** (Govern/Identify/Protect/Detect/Respond/Recover Categories and Subcategories) and **ISO/IEC 27001:2022 Annex A** controls. The target system is a **desktop, single-user end-to-end-encrypted (E2EE) chat client** offering OMEMO, OTR, OpenPGP (XEP-0027) and OX (XEP-0373), transported over XMPP/TLS via libstrophe, with a local SQLite/flat-file history store, plaintext GKeyFile account config, C/Python plugin extensibility, and an outbound AI/LLM client module (`src/ai`). The assumed threat model is: an active or passive network/server adversary (MitM, downgrade, key injection, stanza spoofing, server-side archive); a hostile peer sending attacker-controlled XML/stanzas, message bodies, MUC fields, URLs and file transfers; a malicious or compromised LLM provider or update endpoint; other local users on a shared host reading at-rest secrets; and a compromised supply chain (dependencies, CI, wrapped subprojects, plugins). In scope are the client's crypto correctness/enforcement, transport security, input validation, memory safety, data-at-rest protection, logging/detection, secure defaults, supply-chain and vulnerability management, extension/external-interface security, resilience, and the secure-development lifecycle of the repository itself. Explicitly out of scope are purely physical, HR, and datacenter/organizational-estate controls (there is no central server or IT estate); these are enumerated at the end. A subsequent **CIS Controls v8.1** section (after the traceability summary) maps these requirements to CIS safeguards across Implementation Groups IG1–IG3 and adds CIS-specific requirements ([REQ-CIS-01](#req-cis-01)…[REQ-CIS-08](#req-cis-08)). ### 2. How to read this - **IDs** are stable and domain-prefixed: `REQ-<DOMAIN>-NN` (CRY cryptography, AUTH authentication/access/secrets, INP input validation, MEM memory safety, DAR data-at-rest/privacy, LOG logging/detection, CFG secure config/defaults, SUP supply chain, VUL vulnerability/patch/change-control, EXT extension/external interfaces, RES resilience/fail-safe, SDLC secure-development lifecycle/governance). IDs are intended to be cited from source-code audit findings and never renumbered. - **Priority** is one of `critical` / `high` / `medium` / `low`, reflecting blast radius for an E2EE client (a plaintext leak or MitM bypass is critical; a hardening or documentation gap is medium/low). - Each requirement carries an italic **traceability line**: `NIST | ISO | Priority | Verify`. The verification text is written to be concretely checkable against the cproof source tree. - Where one requirement satisfies a control raised by several domains, the duplicates are **merged** and the merge noted in the statement or applicability. The first appearance of a hardening/permissions/no-plaintext-log control is canonical; later domains reference it. - "SHALL" denotes a mandatory requirement; "SHOULD" a strong recommendation that may be risk-accepted with justification. ### 3. Requirements ### REQ-CRY — Cryptography & Confidentiality #### REQ-CRY-01 **Fail-closed E2EE: no plaintext fallback on encryption failure** When a chat or MUC window has an active encryption mode (OMEMO, OTR, PGP XEP-0027, OX XEP-0373), cproof SHALL NOT transmit the message body in cleartext if encryption fails, the recipient key/session is missing, the sender key is missing, the crypto context cannot be created, or the crypto backend is unavailable (`HAVE_LIBGPGME`/`HAVE_OMEMO` undefined); every such path SHALL abort the send, surface a specific visible error in the relevant window, and return NULL/failure so the caller skips network send, history logging and on-screen echo. *(Merges REQ-RES-01; satisfies the same E2EE-integrity control from the Cryptography and Resilience domains.)* *NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.8.26 | Priority: critical | Verify: regression/integration tests forcing each failure path (deleted recipient key, unref'd sender key, induced gpgme error, empty OMEMO device_list, backend compiled out) assert no `<body>` cleartext stanza is emitted and the send returns NULL; grep that `p_gpg_encrypt`/OMEMO/OX encrypt failure branches never reach `message_send_chat`/`_send_message_stanza`; confirm `cl_ev_send_msg_correct` echoes/logs only when the returned id is non-NULL (guards the 622054fc6 fix).* #### REQ-CRY-02 **Mandatory force-encryption policy as last-line plaintext gate** For conversations without an active E2EE session, cproof SHALL consult `allow_unencrypted_message()` before any cleartext send; under `PREF_FORCE_ENCRYPTION` mode `block` it SHALL refuse with no per-message opt-out, under `resend-to-confirm` it SHALL require an explicit second user action, and any unrecognized/empty mode SHALL fail closed (block) rather than fail open. *(Merges REQ-RES-02.)* *NIST: PR.DS-02, PR.IR-01 | ISO: A.8.24, A.5.10 | Priority: critical | Verify: unit-test `allow_unencrypted_message()` returns FALSE for `block`; for `resend-to-confirm` first Enter blocks and an identical re-send proceeds; an invalid/empty mode returns FALSE; confirm the final fallthrough returns FALSE.* #### REQ-CRY-03 **TLS mandatory by default; no silent STARTTLS downgrade** For every account, cproof SHALL map an absent/empty/`force` `tls.policy` to `XMPP_CONN_FLAG_MANDATORY_TLS`, SHALL set `XMPP_CONN_FLAG_DISABLE_TLS` only when the user explicitly sets `tls.policy=disable`, and SHALL NOT proceed with SASL authentication or message traffic over a connection that failed to upgrade to TLS under any non-disabled policy. *(Merges REQ-AUTH-03 transport half, REQ-CFG-01.)* *NIST: PR.DS-02, PR.PS-01 | ISO: A.8.20, A.8.9, A.8.24 | Priority: critical | Verify: unit-test `_conn_apply_settings` flag computation in `src/xmpp/connection.c`: NULL/`force`/`trust` set MANDATORY_TLS and exclude DISABLE_TLS/LEGACY_SSL; `disable` sets DISABLE_TLS only on explicit config; integration test against a server advertising no STARTTLS aborts under default policy; confirm no auth/send occurs before TLS established.* #### REQ-CRY-04 **TLS certificate validation enforced; trust-bypass is explicit and fails closed** cproof SHALL perform full X.509 chain and hostname validation for XMPP TLS by default, SHALL set `XMPP_CONN_FLAG_TRUST_TLS` (blind trust) only when `tls.policy=trust` is explicitly chosen, SHALL always register the certfail handler, and on validation failure SHALL default to abort — accepting a cert only via explicit interactive `/tls allow` (session) or `/tls always` (persist by SHA-256 fingerprint via tlscerts/cafile), and rejecting on `/tls deny` or EOF. *(Merges REQ-CFG-02, REQ-CFG-03.)* *NIST: PR.AA-03, DE.CM-09 | ISO: A.8.21, A.8.9, A.8.24 | Priority: critical | Verify: test only literal policy `trust` yields TRUST_TLS and no default/auto-fallback does; test `_connection_certfail_cb`/`sv_ev_certfail` returns accept only for explicit allow/always and abort (0) for deny/EOF; assert `tlscerts_exists`/fingerprint-equality gate silent re-acceptance; negative test: untrusted self-signed cert under default policy must not connect.* #### REQ-CRY-05 **Per-account tls.policy validated against an allowlist on load** cproof SHALL validate each stored `tls.policy` against the known allowlist (`force`, `trust`, `disable`, `direct`/`legacy`) via `valid_tls_policy_option` on load, and SHALL discard an unrecognized value by falling back to the secure default (`force`/MANDATORY_TLS) rather than passing an unknown string to the connection layer. *NIST: PR.PS-01, PR.DS-02 | ISO: A.8.9 | Priority: high | Verify: test that loading an account with `tls.policy="bogus"` yields `tls_policy=NULL` (interpreted as force) and that `valid_tls_policy_option` rejects out-of-allowlist values.* #### REQ-CRY-06 **Approved cipher/key-length and mandatory AEAD tag verification for OMEMO** OMEMO message and file encryption SHALL use only AES in an authenticated/standardized mode at ≥128-bit strength (AES-256-GCM for payload/file; AES-128-GCM and AES-CBC-PKCS5 only where the OMEMO/Signal spec requires), SHALL reject unknown cipher/key-length selectors (switch default returns error, no silent weaker substitution), and SHALL call `gcry_cipher_checktag` on every decrypt and abort with no plaintext output on tag mismatch. *NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: review the `key_len` switch in `src/omemo/crypto.c` handles only sanctioned lengths with an erroring default (no fall-through); confirm `gcry_cipher_checktag` failure aborts decrypt; add a tampered-tag test asserting decrypt fails and emits no plaintext.* #### REQ-CRY-07 **CSPRNG for all key/nonce/IV/identifier generation** All key material, IVs/nonces, and identifiers SHALL be drawn from libgcrypt at `GCRY_VERY_STRONG_RANDOM` (`gcry_randomize`/`gcry_random_bytes_secure`); libgcrypt secure-memory init and `GCRYCTL_INITIALIZATION_FINISHED` SHALL complete (and be checked via `GCRYCTL_INITIALIZATION_FINISHED_P`) before any keygen; no predictable source (`rand()`/`random()`/`g_random_*`) SHALL be used for crypto material. *NIST: PR.DS-02 | ISO: A.8.24 | Priority: critical | Verify: confirm `omemo_random_func` uses VERY_STRONG_RANDOM; verify init-finished is set before keygen and checked; grep crypto/omemo/otr/pgp sources for `rand()/random()/g_random_` used as key/IV/nonce and assert none.* #### REQ-CRY-08 **Forward-secret ratcheting enforced; one-time prekeys consumed** OMEMO sessions SHALL use the libsignal/libomemo-c double ratchet and OTR the libotr AKE/ratchet; cproof SHALL NOT implement its own session KDF/ratchet, and consumed one-time prekeys SHALL be removed from the prekey store (`remove_pre_key`) after use so that long-term identity-key compromise does not expose past messages. *NIST: PR.DS-02 | ISO: A.8.24 | Priority: high | Verify: confirm session crypto is delegated to the libraries (no custom KDF/ratchet); verify `remove_pre_key` is wired and invoked on session establishment; review signed-prekey rotation; integration test that a replayed prekey bundle does not reproduce a usable session key.* #### REQ-CRY-09 **TOFU trust model: explicit, persisted fingerprint verification before trusted send** cproof SHALL bind peer identity to cryptographic fingerprints (OMEMO identity-key fingerprints, OTR fingerprints, PGP key IDs), distinguish untrusted/blind/verified states, require an explicit user action (`/omemo trust`, `/otr fingerprint`/SMP, `/pgp` trust) to mark a fingerprint verified, persist trust decisions, and treat a changed peer identity key as untrusted rather than auto-accepting it. Trust transitions SHALL be driven only by user commands, never by inbound-stanza handlers. *(Merges REQ-AUTH-07.)* *NIST: PR.AA-03, PR.AA-04 | ISO: A.5.31, A.5.17 | Priority: high | Verify: unit-test `is_trusted_identity` returns untrusted for unknown/changed keys; verify trust-store round-trips and a new identity key for a known JID does not auto-trust; simulate an inbound message from an unverified peer and assert identity stays untrusted until a verify command runs.* #### REQ-CRY-10 **Fail-safe OMEMO trust on send and receive** On send, cproof SHALL omit untrusted OMEMO recipient devices and SHALL abort the send (with an explanatory untrusted-fingerprint listing) when no trusted device remains, never silently encrypting to an unverified device; on receive, a message from an untrusted identity SHALL be flagged untrusted to the user rather than displayed as trusted. The default `PREF_OMEMO_TRUST_MODE` SHALL be `manual` (not `blind`); `blind`/`firstusage` SHALL surface a visible auto-trust notice. *(Merges REQ-CFG-07, REQ-RES-06.)* *NIST: PR.AA-03, PR.DS-02 | ISO: A.8.24, A.8.9, A.5.17 | Priority: critical | Verify: test `prefs_get_string(PREF_OMEMO_TRUST_MODE)` default == `manual`; test that with all recipient devices untrusted no stanza is sent and the untrusted-fingerprint guidance shows (`omemo_on_message_send` keys==NULL branch); test receiving from an untrusted identity sets the trusted flag FALSE and marks the UI; confirm blind/firstusage emit a `cons_show` notice.* ### REQ-AUTH — Authentication, Access Control & Secret Management #### REQ-AUTH-01 **Owner-only (0600) permissions for every secret/credential/trust/history file** cproof SHALL create and re-assert mode 0600 (`S_IRUSR|S_IWUSR`) on every file holding secrets or security-critical state — accounts keyfile (passwords/eval_password), OMEMO `identity.txt`/`trust.txt`/`sessions.txt`/`known_devices.txt`, OTR `keys.txt`/`fingerprints.txt`, PGP material, TLS pinned certs (tlscerts), the SQLite history DB and flat-file backend, history export files, and the main log — both on creation and on every save, regardless of pre-existing mode. Files written by third-party libraries (e.g. libotr `otrl_privkey_write_fingerprints`) SHALL get an explicit chmod afterward. *(Canonical permissions requirement; merges REQ-CRY perms, REQ-DAR-01, REQ-DAR-02, REQ-CFG-09, REQ-LOG-03.)* *NIST: PR.DS-01, PR.AA-01, PR.AA-05 | ISO: A.8.3, A.8.10, A.5.17 | Priority: critical | Verify: static — every `fopen`/`g_creat`/`g_key_file_save_to_file`/`sqlite3_open` for a secret/history path is followed by `g_chmod`/`fchmod(...,S_IRUSR|S_IWUSR)` with no `S_IRGRP`/`S_IROTH`; the SQLite `sqlite3_open` in `_sqlite_init` (`src/database_sqlite.c`) currently lacks this — close the gap. Runtime — under umask 0, init account/OMEMO/OTR/DB/export, `stat()` each and assert `(mode & 0777)==0600`. Template: `database_flatfile_verify.c` / `database_export.c` fchmod.* #### REQ-AUTH-02 **Owner-only (0700) permissions for all secret-bearing directories** cproof SHALL create every directory under the data/config root that contains secret material or loadable code (per-account OTR/OMEMO/PGP subdirs, plugins dir) with mode 0700 (`S_IRWXU`) via the central `create_dir()` helper and SHALL NOT widen them, so secret files are not enumerable or subject to TOCTOU by other local users. *NIST: PR.AA-05, PR.DS-01 | ISO: A.8.3, A.8.2 | Priority: high | Verify: confirm `create_dir()` uses `S_IRWXU` and all account/OTR/OMEMO/PGP/plugins dir creation routes through it (no raw `mkdir`/`g_mkdir` with 0755); runtime `stat()` asserts `(mode & 0777)==0700`.* #### REQ-AUTH-03 **Insecure transport/auth policies are explicit opt-in and logged** cproof SHALL default to MANDATORY_TLS for SASL credential exchange and SHALL enable `DISABLE_TLS`, `TRUST_TLS`, `LEGACY_SSL`, or `LEGACY_AUTH` only from explicit per-account `tls.policy`/`auth.policy` strings, logging a WARN-level diagnostic whenever a credential-exposing flag is active; none SHALL be on by default. *(Complements REQ-CRY-03/04; merges REQ-CFG-12.)* *NIST: PR.AA-03, PR.DS-02 | ISO: A.5.17, A.8.5, A.8.9, A.8.20 | Priority: critical | Verify: confirm in `_conn_apply_settings` the NULL/`force` branch sets MANDATORY_TLS and that DISABLE_TLS/TRUST_TLS/LEGACY_* are reachable only via explicit policy strings; test that default flags contain neither LEGACY_SSL nor LEGACY_AUTH; drive with `disable`/`legacy` and assert a warning is logged (LOG_FLAG_IF_SET path).* #### REQ-AUTH-04 **Account passwords are opt-in at rest; eval_password retrieval is the recommended path** cproof SHALL NOT write the account password to the accounts keyfile unless the user explicitly stored it, SHALL support non-persistent retrieval at connect time via `eval_password` (e.g. a system keyring), and SHALL remove the persisted password from the keyfile (`g_key_file_remove_key` + save) on `/account clear ... password`. *(Merges REQ-DAR-08 clear-on-disk half.)* *NIST: PR.DS-01, PR.AA-01 | ISO: A.5.17, A.8.3 | Priority: medium | Verify: configure an account with only `eval_password` set and assert no `password` key is written yet login works; after `/account clear password` assert the key is absent from the keyfile.* #### REQ-AUTH-05 **eval_password runs without a shell and without ambiguous PATH resolution** cproof SHALL execute the `eval_password` command via direct argv spawn (`g_shell_parse_argv` + `g_spawn_sync`, never `system()`/`popen()`/`/bin/sh -c`), SHALL treat a non-zero exit or empty output as authentication failure (never falling back to an empty password), and SHOULD avoid `G_SPAWN_SEARCH_PATH` so the helper binary is unambiguous and not hijackable via a poisoned PATH. *NIST: PR.AA-01, PR.AA-05 | ISO: A.8.2, A.5.17 | Priority: high | Verify: confirm no shell is used in `account_eval_password` and empty/failed output returns FALSE (no empty password to `xmpp_conn_set_pass`); review `G_SPAWN_SEARCH_PATH`; stub a helper returning non-zero/empty and assert FALSE and `account->password` stays NULL.* #### REQ-AUTH-06 **Secrets zeroized with a non-elidable wipe before free; key buffers protected in memory** cproof SHALL overwrite, with a compiler-non-elidable routine (`explicit_bzero`/`gcry_free` of secure memory/`signal_buffer_bzero_free`), the buffers holding account passwords, eval_password output, reconnect-saved password copies, PGP passphrases, and OMEMO/OTR/PGP private-key/plaintext-key material immediately before release, and SHALL use libgcrypt secure memory for OMEMO key operations; ordinary `free()`/`g_free()` of a secret without a preceding wipe is non-conformant. *(Merges REQ-MEM-12, REQ-DAR-08 in-memory half; OMEMO `signal_buffer_bzero_free`/`gcry_free` is the reference pattern.)* *NIST: PR.DS-01, PR.AA-01, PR.PS-01 | ISO: A.5.17, A.8.3, A.8.24 | Priority: high | Verify: grep that every `free`/`g_free` of a password/passphrase/private-key buffer (`src/config/account.c`, `src/xmpp/session.c` `saved_account.passwd`/`saved_details.passwd`, `src/pgp/gpg.c`, `src/omemo/*`) is preceded by a non-elidable wipe over its length; teardown unit test asserts key buffers are zero after `*_destroy`; optional heap-scan-after-logout test asserts the password byte pattern is absent.* ### REQ-INP — Input Validation & Untrusted-Data Handling #### REQ-INP-01 **Validate every peer-supplied JID and fail safe on parse failure** cproof SHALL parse every JID from an untrusted source (stanza `from`/`to`, MUC item jids, roster pushes, carbons/MAM `by`, vCard/avatar refs) via `jid_create()`/`jid_is_valid()` and SHALL treat a NULL return as a hard parse failure — abandoning the stanza — rather than dereferencing `Jid*` or any part. Confirmed defect to fix: `_should_ignore_based_on_silence()` in `src/xmpp/message.c` reads `from_jid->barejid` after `jid_create(from)` with no NULL guard (remote NULL-deref DoS). *NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: grep all `jid_create(` call sites and assert each guards NULL before field access; unit-test `test_jid.c` with NULL/empty/`a@@b`/non-UTF-8/over-3071-char input asserting NULL; regression test driving `_should_ignore_based_on_silence` with a stanza lacking `from` asserts no crash; run under ASan.* #### REQ-INP-02 **Enforce RFC 6122 length and character bounds in JID validation** `jid_is_valid()` SHALL reject any JID exceeding `JID_MAX_TOTAL_LEN` (3071) total or `JID_MAX_PART_LEN` (1023) per part, any localpart containing RFC 6122 forbidden chars (space `" & ' / : < > @`), more than one `@` in the bare portion, empty localpart when `@` is present, empty domainpart, or input failing `g_utf8_validate()`. *NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: table-driven `test_jid.c` covering boundary lengths (1023/1024 per part, 3071/3072 total), each forbidden char, double-`@`, missing localpart/domain, invalid UTF-8; CI fails on any mis-classification.* #### REQ-INP-03 **Never pass untrusted data as a printf/format-string argument (CWE-134)** cproof SHALL NOT pass any peer- or user-controlled string (message body/subject, nickname, JID, MUC status, URL, filename, presence status, library error text) as the format-string parameter of `cons_show*`, `win_print*`/`win_println*`, `log_*`, `g_strdup_printf`, or any printf-family/varargs function; such data SHALL appear only as a `%s` argument. Every variadic printf-like wrapper SHALL carry `G_GNUC_PRINTF`/`__attribute__((format(printf,N,M)))`. *(Merges REQ-MEM-09; data crossing the plugin/AI boundary (REQ-EXT-04) is covered here too.)* *NIST: PR.PS-06, PR.PS-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: build with `-Wformat=2 -Werror=format-security -Werror=format-nonliteral`; run `./check-cwe134.sh src` and require exit 0; grep `cons_show(`/`win_print(` whose first arg is a bare variable; fuzz/unit test feeding `%`-bearing stanza/plugin/AI content through a display sink.* #### REQ-INP-04 **Build promotes format-security diagnostics to errors** The build SHALL set `-Wformat=2` and promote format diagnostics to errors (`-Werror=format-security`, and `-Werror=format-nonliteral` where supported) for cproof translation units so any CWE-134 regression fails the build; the `check-cwe134.sh` audit SHALL run in CI as a blocking (not `continue-on-error`) step. *NIST: PR.PS-06, PR.PS-01 | ISO: A.8.28, A.8.25, A.8.29 | Priority: high | Verify: inspect `configure.ac` AM_CFLAGS for `-Werror=format-security`; confirm the CWE-134 CI step is blocking; introduce a deliberate non-literal format and confirm the build/CI fails.* #### REQ-INP-05 **Gate XEP-0359 stanza-id/origin-id trust on the asserting entity's disco support** When consuming a `<stanza-id by='…'>` or MAM `<result>` id, cproof SHALL honour it only when `by` equals the user's own bare JID or the message's authoritative archive (MUC bare JID or account domain) AND that entity advertises `urn:xmpp:sid:0` via disco; ids from any other `by` SHALL be ignored. Ownership of an outgoing message SHALL be established by verifying the HMAC in origin-id (`message_is_sent_by_us`), not by id-string match. *NIST: PR.DS-02, PR.AA-03 | ISO: A.8.26, A.8.28 | Priority: high | Verify: test a `<stanza-id by='attacker@evil'>` from a different sender is not trusted (`_stanza_id_by_trusted`); toggle `urn:xmpp:sid:0` in features and assert the gate; assert `message_is_sent_by_us` rejects a valid-length prefix with a wrong HMAC.* #### REQ-INP-06 **Validate URL scheme and pass URLs/filenames as argv, never to a shell** Before acting on a peer/user-supplied URL (`/url open`, `/url save`, OOB/file-transfer refs), cproof SHALL whitelist the scheme via `g_uri_parse_scheme()` (only `http`, `https`, `aesgcm`) and SHALL invoke any external open/download helper by building an explicit argv vector (`format_call_external_argv` + `call_external`/`g_spawn`, no shell), so URL/filename content can never be interpreted as shell metacharacters or extra args. *(Merges REQ-EXT-05 URL half.)* *NIST: PR.PS-06, PR.PS-05, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: grep `system(`/`popen(`/`sh -c`/`g_spawn_*sync` over `src/` and confirm none operate on URL input; unit-test scheme handling rejects `file:`/`javascript:`/`data:`; test a URL with spaces/quotes/`;` reaches the helper as one argv element.* #### REQ-INP-07 **No shell-string desktop notification command injection** Desktop notification invocation SHALL NOT construct a shell command string from message-derived content for `system()`; it SHALL use a libnotify/`g_spawn` argv API or rigorously `g_shell_quote` every interpolated value, treating message/sender content as untrusted data. Defect to remediate: `src/ui/notifier.c` builds a notify string and runs it via `system()`. *NIST: PR.PS-05 | ISO: A.8.26, A.8.28 | Priority: high | Verify: grep `notifier.c` for `system(`/`popen(`; confirm migration to argv `g_spawn` or full quoting; test a sender/message with quotes, `$()`, backticks, `;` spawns no extra process.* #### REQ-INP-08 **Bound and sanitize peer-controlled text before display, logging and storage** cproof SHALL treat decrypted bodies, subjects, MUC topics, presence status, nicknames and resource parts as untrusted and SHALL neutralize terminal-control and Unicode bidi-override sequences (e.g. ESC/CSI, U+202E) and enforce a sane maximum rendered length before writing to the ncurses UI, log files, or the SQLite/flat-file backend, so peer content cannot inject terminal escapes, spoof UI via bidi, or corrupt log/DB output. *NIST: PR.DS-01, PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test a body containing raw `ESC[2J`, a NUL, and U+202E asserting the stored/displayed form is sanitized/escaped; review the display sink for a control-char filter and a length cap on rendered peer strings.* #### REQ-INP-09 **Strict structural validation of incoming stanzas; cap MAM result sets** cproof SHALL guard every stanza consumer so that missing/malformed mandatory children/attributes (absent body/`from`, non-JID `from`, unexpected namespaces) cause the stanza to be skipped rather than NULL-dereferencing the result of `xmpp_stanza_get_child_by_name`/`get_attribute`, SHALL not call `abort()`/`assert()` on attacker-influenced conditions, and SHALL cap MAM results requested/processed per query (`MESSAGES_TO_RETRIEVE`). *(Merges REQ-MEM-08, REQ-RES-05.)* *NIST: PR.PS-06, PR.DS-02, PR.IR-01 | ISO: A.8.28, A.8.26 | Priority: critical | Verify: fuzz/replay malformed stanzas (missing body/from/jid, deeply nested, huge MAM batches, oversized base64 OMEMO key) into `message.c`/`iq.c`/`presence.c`/`muc.c`/`omemo.c` handlers under ASan/UBSan asserting clean rejection and no crash; code-review NULL checks after each `xmpp_stanza_get_*` on the receive path; confirm `assert()` in receive paths is debug-only and unreachable from peer data.* #### REQ-INP-10 **Validate and escape data crossing into AI/LLM outbound requests and responses** Where `src/ai` forwards user/peer content into an outbound LLM request, it SHALL JSON-escape all such content via `ai_json_escape()` before embedding it, and SHALL parse provider responses with bounds-checked unescaping (`_json_unescape_substring`/`_extract_json_string`) so an embedded quote or control byte cannot break the JSON string context. *NIST: PR.DS-02, PR.PS-06 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: unit-test `ai_json_escape()` with `"`, `\`, `\n` and control bytes asserting valid escaped output; test `_extract_json_string()` against a response value embedding an escaped quote; confirm every request-body assembly routes content through `ai_json_escape()`.* ### REQ-MEM — Memory Safety & Secure Coding #### REQ-MEM-01 **Full compiler/linker hardening baseline in release builds, at parity across build systems** Every released cproof binary and plugin SHALL be compiled/linked with `-fstack-protector-strong`, `-fno-common`, `-D_FORTIFY_SOURCE=2` (or 3 where supported), `-Wformat=2`, full RELRO and immediate binding (`-Wl,-z,relro -Wl,-z,now`), non-executable stack (`-Wl,-z,noexecstack`), and a position-independent executable (`-fPIE -pie`); the autotools and meson definitions SHALL stay at flag parity, and the build SHALL fail if any flag is silently dropped. Known gaps to close: configure.ac omits `-fPIE/-pie`; meson.build omits the RELRO/`-z,now` linker hardening present in autotools. *(Canonical hardening requirement; merges REQ-CRY-12, REQ-AUTH-09, REQ-CFG-10, REQ-SUP-07, REQ-EXT-10, REQ-RES-10, REQ-SDLC-01, REQ-VUL-09.)* *NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.27, A.8.28 | Priority: critical | Verify: grep `configure.ac` and `meson.build` for each flag and assert parity; on the built `./profanity` run `checksec`/`readelf -d`/`readelf -h` and assert Full RELRO, BIND_NOW, NX, PIE (Type DYN), stack canary, FORTIFY symbols present (and CET/`-fstack-clash-protection` where the toolchain supports them); add a CI step that fails when any mitigation is missing on the produced binary and loaded `.so` plugins.* #### REQ-MEM-02 **ASan + UBSan + unsigned-overflow build gates the test suite in CI** The project SHALL provide an `--enable-sanitizers` build (ASan + UBSan + `-fsanitize=unsigned-integer-overflow`, `-fno-sanitize-recover=all`) and SHALL run the full cmocka unit/functional suite under it in CI on every pull request, AND additionally run the suite under Valgrind with `--error-exitcode=1 --leak-check=full`; any sanitizer/Valgrind error or definite leak SHALL fail the build. *(Merges REQ-SDLC-02, REQ-RES-10 sanitizer half.)* *NIST: PR.PS-06, DE.CM-01, ID.RA-01 | ISO: A.8.28, A.8.29, A.8.25 | Priority: critical | Verify: confirm a non-`continue-on-error` CI job runs `./configure --enable-sanitizers && make && make check` and that `ci-build.sh` invokes Valgrind with `--error-exitcode=1`; intentionally introduce a heap overflow in a test and confirm CI fails.* #### REQ-MEM-03 **No unbounded/unsafe C string or format primitives on untrusted data** cproof SHALL NOT use `strcpy`, `strcat`, `sprintf`, `gets`, or unbounded `scanf` on data derived from stanzas, peer content, MUC, file-transfer/URL input, or remote JIDs; such code SHALL use length-bounded equivalents (`g_strdup_printf`, `g_strlcpy`, `snprintf` with checked return). Defects to remediate: `strcpy`/`strcat` on peer barejids in `src/pgp/ox.c:153-156`; `sprintf` in `src/omemo/crypto.c:479/483` and `src/xmpp/iq.c:1787`. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: `grep -rnE '\b(strcpy|strcat|sprintf|gets)\(' src/` and confirm each remaining hit operates only on fixed compile-time-constant strings; add a CI grep that fails on these calls in stanza/crypto-handling files.* #### REQ-MEM-04 **Overflow-checked allocation sizes** Every `malloc`/`realloc`/`g_malloc` size that multiplies or adds attacker-influenced lengths SHALL be computed with overflow-checked arithmetic (`__builtin_mul_overflow`/`__builtin_add_overflow` or `g_malloc_n`-style helpers) and SHALL error/abort rather than allocate a truncated buffer; `realloc` results SHALL be checked before the old pointer is overwritten. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: review multiplicative/additive `malloc`/`realloc` args in `src/tools/http_upload.c`/`http_download.c`, `src/common.c`, `src/xmpp/connection.c`, `src/omemo/omemo.c` (`identity_public_key_len*2+1`, `malloc(*length)`) for a preceding overflow check; UBSan unsigned-overflow tests feeding near-`SIZE_MAX` lengths.* #### REQ-MEM-05 **Underflow-guarded unsigned subtraction on sizes/offsets (per project unsigned policy)** cproof SHALL guard every subtraction of unsigned size/length/offset values that could go negative against wrap to `~SIZE_MAX`, using pre-checks or builtins rather than casting an operand to signed, consistent with the project's unsigned-arithmetic policy. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: build with `-fsanitize=unsigned-integer-overflow` and run tests/fuzz over UI scroll/pad math (`src/ui/inputwin.c` documented SIZE_MAX guard) and parser/crypto buffer-slicing; code-review subtractions of `size_t`/`guint` operands for a lower-bound check; confirm no `size_t` is cast to `int` to mask a wrap.* #### REQ-MEM-06 **Mandatory auto-cleanup macros for heap/FILE/fd ownership** Local owning pointers and resources SHALL use the project auto-cleanup attributes (`auto_gchar`, `auto_char`, `auto_gcharv`, `auto_guchar`, `auto_FILE`, `auto_gfd`, `auto_gerror`, `auto_jid`, `auto_sqlite`) instead of manual `free`/`g_free`/`fclose`/`close` on early-return paths, so ownership is released exactly once on every exit path. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: medium | Verify: code-review that new owning locals use `auto_*` rather than trailing frees; run `make check` under ASan/LeakSanitizer asserting no leaks/double-frees; grep changed handlers for `free()`/`g_free()` paired with early returns that should use `auto_*`.* #### REQ-MEM-07 **Null freed pointers; document out-parameter ownership; no UAF on crypto buffers** After freeing a heap pointer that remains in scope or in a struct field, cproof SHALL set it to NULL; functions returning allocated memory via out-parameters SHALL define ownership; freed crypto/session/key buffers (`gcry_free`/`free` paths in `src/omemo`, `src/otr`, `src/pgp`) SHALL NOT be referenced after release. *NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: ASan/UBSan `make check` over crypto and session teardown; code-review each free of a still-live pointer is followed by `=NULL`; targeted tests double-invoking teardown to confirm no double-free.* #### REQ-MEM-08 **NULL-checks on every peer/remote-derived parse/lookup before dereference** cproof SHALL validate for NULL the result of every parse/lookup that can fail (XMPP attribute/child lookups, JID parsing, base64/hex decode, GKeyFile reads, library returns) before dereferencing and SHALL handle absence as a recoverable error. *(Closely related to REQ-INP-09; this is the codebase-wide secure-coding form.)* *NIST: PR.PS-01, DE.CM-01 | ISO: A.8.28 | Priority: high | Verify: enable `-Wnull-dereference` and review warnings; run crafted-stanza/malformed-input tests under ASan asserting no NULL-deref crash; code-review each new `xmpp_stanza_get_*`/`g_key_file_get_*`/decode result for a NULL guard (cf. the autoping NULL-domain guard 15dfc2bd).* #### REQ-MEM-09 **No alloca/VLA sized from untrusted lengths** cproof SHALL NOT size stack allocations (`alloca` or VLAs) from attacker-influenced lengths; such buffers SHALL be heap-allocated with checked sizes or bounded by a validated compile-time maximum. Defects to remediate: `alloca((strlen(barejid)+6))` in `src/pgp/ox.c:150-156`; `unsigned char out[mac_len]` VLA in `src/omemo/crypto.c:107`. *NIST: PR.PS-01 | ISO: A.8.28, A.8.27 | Priority: medium | Verify: `grep -rnE '\balloca\(' src/` and review for non-constant sizes; compile with `-Wvla` and treat as error to flag VLAs; replace flagged sites with checked heap allocation + `auto_*` cleanup.* ### REQ-DAR — Data Protection at Rest & Privacy > File/directory permission and in-memory/at-rest secret-zeroization requirements for at-rest material are consolidated in **[REQ-AUTH-01](#req-auth-01)** (0600 files, including the SQLite DB gap), **[REQ-AUTH-02](#req-auth-02)** (0700 dirs), and **[REQ-AUTH-06](#req-auth-06)** (zeroization). The requirements below cover history-content minimization, redaction, and at-rest integrity. #### REQ-DAR-01 **Functional no-storage (`off`) mode writes no message content** When `PREF_DBLOG` is `off`, cproof SHALL NOT write any message body, sender/recipient JID-pair, or per-message timestamp derived from chat traffic to any history backend (SQLite or flat-file), for incoming, outgoing, MUC and MUC-PM messages alike. *NIST: PR.DS-01 | ISO: A.8.10 | Priority: high | Verify: set `PREF_DBLOG=off`, drive `add_incoming` + all `add_outgoing_*`, assert zero rows in SQLite ChatLogs and zero content lines in the flat-file; confirm `get_previous_chat` returns nothing; pin the early-return in both backends (`database_sqlite.c`, `database_flatfile.c`).* #### REQ-DAR-02 **Redaction (`redact`) mode never leaks plaintext into storage** When `PREF_DBLOG` is `redact`, cproof SHALL substitute the message body with a fixed placeholder (`[REDACTED]`) before it reaches any persistence call, so no original body character is passed to `sqlite3_bind` or written to the flat-file; non-content metadata may be retained. *NIST: PR.DS-01 | ISO: A.8.11 | Priority: high | Verify: with `redact`, store a message containing a unique sentinel, then grep the raw `chatlog.db` bytes and flat-file and assert the sentinel is absent while a `[REDACTED]` row/line exists; confirm redaction precedes the bind/write.* #### REQ-DAR-03 **No plaintext chat content or secrets in operational/debug logs** cproof SHALL NOT write decrypted message bodies, account passwords, eval_password output, SASL credentials, OMEMO/OTR/PGP private keys, or AI provider API keys/Bearer tokens into `profanity.log` at any level; statements involving message content SHALL log only metadata (JIDs, ids, sizes, encryption type, library error code). Defect to remediate: `src/database_sqlite.c:760` `log_error('content: %s', message->plain)` on insert failure (bypasses no-storage/redact). The libstrophe debug bridge `_xmpp_file_logger` SHALL redact SASL `<auth>/<challenge>/<response>` and XEP-0077 registration credentials before they reach `_log_msg`. *(Merges REQ-LOG-01, REQ-LOG-02, REQ-EXT-03 log half; SDLC secret-scanning is REQ-SDLC-04.)* *NIST: PR.PS-04, PR.DS-01 | ISO: A.8.12, A.8.15 | Priority: critical | Verify: grep `log_*` for interpolation of `message->plain`/`message->body`/`password`/`api_key`/`token`/`Bearer`/key bytes and assert none print the value; drive login+decrypt+AI-request with a sentinel secret and assert it never appears in the log; fix `database_sqlite.c:760`; confirm `src/ai/ai_client.c` never logs the api_key and never sets `CURLOPT_VERBOSE`; connect with XMPP debug logging and a sentinel password and assert it (and its base64) is absent.* #### REQ-DAR-04 **Atomic, permission-preserving writes for history export and keyfile rewrites** cproof SHALL write history exports and rewritten secret/history files atomically via a private (0600) temp file created with `mkstemp`/`O_EXCL` followed by `rename`, never via a predictable fixed temp path, so a concurrent or hostile local process cannot pre-create, read, or race the file. *NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: review `src/database_export.c` (mkstemp+fchmod 0600+rename) and any flat-file rewrite/compaction for the same pattern with no fixed-name fallback; test pre-creating the legacy predictable tmp name as a symlink and assert the export refuses or writes through a fresh mkstemp 0600 file.* #### REQ-DAR-05 **Encryption-state and metadata accuracy in stored records** cproof SHALL persist the correct encryption type (`none`/`omemo`/`otr`/`pgp`/`ox`) for every stored message row matching the actual delivery path, and SHALL NOT record a decryption-failure message as `encryption='none'` plaintext or store raw ciphertext as the `message` body. *NIST: PR.DS-01 | ISO: A.8.11 | Priority: medium | Verify: tests asserting an OMEMO/OTR/PGP/OX message round-trips with the matching `encryption` value (`_get_message_enc_type`), and a decryption-failure message is either not stored or stored with an explicit non-`none` marker rather than its ciphertext under `none`.* #### REQ-DAR-06 **Clear-on-close history minimization honored** When `PREF_CLEAR_PERSIST_HISTORY` is disabled, cproof SHALL discard in-window scrollback on window close without re-persisting it beyond the configured backend, and `/clear` behavior SHALL be consistent with the `PREF_DBLOG` retention setting. *NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: toggle `PREF_CLEAR_PERSIST_HISTORY` and assert closing a chat window with it disabled produces no new persisted rows; confirm `/clear` with persist off does not write back buffer content (`src/ui/window.c`, `/clear` handler).* #### REQ-DAR-07 **Keep plaintext per-day chatlog subsystem disabled; document retention defaults** The legacy plaintext per-day chat-log writers (`chatlog.c` entry points) SHALL remain no-ops and SHALL NOT be re-enabled except behind an explicit, off-by-default preference that documents it stores decrypted content unencrypted; the flat-file/SQLite backends SHALL be the only message sinks, and `PREF_DBLOG` values (`off`/`redact`/`flatfile`/`store`) and their privacy implications SHALL be documented. *(Merges REQ-LOG-09, REQ-DAR retention.)* *NIST: PR.DS-01, PR.PS-04, GV.PO-01 | ISO: A.8.12, A.5.34 | Priority: low | Verify: assert `chatlog.c` functions remain no-ops (no `fopen`/write) via test/grep; send/receive an encrypted message and assert no `chatlogs/` plaintext file under `XDG_DATA_HOME`; confirm `PREF_DBLOG` documented; require any new logging pref to default false.* ### REQ-LOG — Logging, Error Handling & Detection > Log-file 0600 permission is in **[REQ-AUTH-01](#req-auth-01)**; no-secrets-in-logs is in **[REQ-DAR-03](#req-dar-03)**. #### REQ-LOG-01 **Security-relevant events logged at WARN or higher** cproof SHALL record at `PROF_LEVEL_WARN`/`ERROR` (so they survive the default WARN filter): TLS certificate validation failure, OMEMO/OTR/PGP/OX decryption or MAC failure, use/rejection of an untrusted/unknown device fingerprint, and SASL authentication failure. *NIST: DE.CM-06, DE.AE-02 | ISO: A.8.15, A.8.16 | Priority: high | Verify: grep that cert-fail (`_connection_certfail_cb`/`sv_ev_certfail`), decrypt-fail (`src/omemo`, `src/pgp/ox.c`), untrusted-device and SASL-failure paths call `log_warning`/`log_error` (not `log_debug`); functional test injecting a bad cert and a corrupt OMEMO ciphertext asserts a WARN/ERROR line at the default filter.* #### REQ-LOG-02 **Detect and log TLS/security downgrade conditions** cproof SHALL log a WARN-or-higher event whenever a connection is established/negotiated below the expected posture: TLS negotiation failed, connecting without mandatory TLS, an unencrypted (`DISABLE_TLS`) connection, or a followed see-other-host/stream redirect — so silent transport downgrades are detectable. *NIST: DE.AE-02, DE.CM-06 | ISO: A.8.16, A.8.15 | Priority: high | Verify: inspect `src/xmpp/connection.c` that DISABLE_TLS/non-mandatory/TLS-failed/see-other-host paths emit `log_warning`; test connecting with TLS disabled asserts a WARN line.* #### REQ-LOG-03 **Safe, non-leaking error messages on crypto/parse failures** Error/warning messages on crypto-operation failure, stanza/XML parse failure, or DB/keyfile load failure SHALL contain only a failure category and a stable library error code/string, and SHALL NOT embed raw ciphertext, key bytes, full untrusted stanza payloads, or secret-derived values. *(Reinforces REQ-DAR-03.)* *NIST: PR.PS-04, DE.AE-02 | ISO: A.8.15, A.8.28 | Priority: medium | Verify: review crypto-failure `log_error` sites (`src/omemo/omemo.c`, `src/pgp/ox.c` gpgme_strerror, stanza handlers) to confirm they pass strerror codes/identifiers, not buffers/key material.* #### REQ-LOG-04 **Log level user-configurable, defaulting to non-debug** cproof SHALL expose a configurable filter (DEBUG/INFO/WARN/ERROR) honored by `_should_log()`, default to a non-DEBUG level, map unknown level strings to WARN, and require explicit user action to enable DEBUG (which may capture additional protocol detail). *NIST: PR.PS-04, ID.RA-01 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: unit-test `_should_log` across levels confirming below-filter records drop; confirm `log_level_from_string` defaults unknown to WARN and DEBUG is not the startup default.* #### REQ-LOG-05 **Bounded log growth via size-based rotation** When rotation is enabled, the logger SHALL rotate the main log once it reaches `max_log_size`, SHALL bound the number of rotated files, and rotated files SHALL inherit 0600, preventing unbounded disk consumption while preserving a finite reviewable history. *NIST: PR.PS-04, PR.IR-04 | ISO: A.8.15, A.8.6 | Priority: medium | Verify: test that writing past `max_log_size` triggers `_rotate_log_file()`, produces a bounded-index `profanity.NNN.log` (also 0600), reopens the main file; confirm user-provided log files are excluded from auto-rotation.* #### REQ-LOG-06 **stderr capture fails safe and respects sensitive-data constraints** The stderr-capture facility (`log_stderr_init`/`log_stderr_handler`) that redirects process stderr into the log SHALL handle init/read errors (including EINTR/partial reads) without crashing or losing the fd, and SHALL be subject to the same no-secrets constraint so crypto-library diagnostic output to stderr (gcrypt/gpgme/libsignal) does not persist key/passphrase material. *NIST: PR.PS-04, DE.CM-06 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: review error paths in `log_stderr_init` for correct fd cleanup and non-fatal failure; write a sentinel secret to stderr and assert redaction/handling; verify the handler tolerates EINTR/partial reads without data loss or crash.* ### REQ-CFG — Secure Configuration & Secure Defaults > TLS-mandatory default ([REQ-CRY-03](#req-cry-03)), cert-validation/TOFU defaults ([REQ-CRY-04](#req-cry-04)), tls.policy allowlist ([REQ-CRY-05](#req-cry-05)), OMEMO manual trust default ([REQ-CRY-10](#req-cry-10)), legacy-flag gating ([REQ-AUTH-03](#req-auth-03)), and toolchain hardening ([REQ-MEM-01](#req-mem-01)) are consolidated above. The CFG-specific defaults remain: #### REQ-CFG-01 **HTTP(S) file transfer must not silently disable TLS verification** cproof SHALL keep `CURLOPT_SSL_VERIFYPEER` and `CURLOPT_SSL_VERIFYHOST` enabled for XEP-0363 upload and HTTP download by default, and SHALL disable them ONLY as an explicit consequence of the operator-selected account `tls.policy=trust` (or an explicit `PREF_TLS_CERTPATH=none`), never from any other condition or default; that insecure state SHALL be surfaced to the user, and downloaded files SHALL be written canonicalized within the configured downloads directory (rejecting peer-supplied path traversal). *(Merges REQ-EXT-08.)* *NIST: PR.AA-03, PR.DS-02, PR.DS-10 | ISO: A.8.9, A.8.24, A.5.14 | Priority: high | Verify: confirm in `src/tools/http_upload.c`/`http_download.c` the `insecure` boolean derives solely from `tls_policy=="trust"`/explicit none, and that with `force`/NULL the handles retain VERIFYPEER=1/VERIFYHOST=2; grep for any unconditional `SSL_VERIFY*=0`; test a peer filename containing `../` stays inside the downloads dir.* #### REQ-CFG-02 **Encryption-loss warning and TLS indicator on by default** cproof SHALL default `PREF_ENC_WARN` and `PREF_TLS_SHOW` to enabled so the user is warned before sending unencrypted and is shown transport security state out of the box. *NIST: PR.PS-01, DE.CM-09 | ISO: A.8.9 | Priority: medium | Verify: unit-test `prefs_get_boolean(PREF_ENC_WARN)` and `prefs_get_boolean(PREF_TLS_SHOW)` both return TRUE on a fresh config; review neither falls through to the FALSE branch.* ### REQ-EXT — Extension & External-Interface Security (Plugins, AI, File/URL/Command Surfaces) > URL/shell handling is [REQ-INP-06](#req-inp-06); notification injection [REQ-INP-07](#req-inp-07); AI JSON escaping [REQ-INP-10](#req-inp-10); AI key-in-log/at-rest [REQ-DAR-03](#req-dar-03)/[REQ-AUTH-01](#req-auth-01); file-transfer TLS/traversal [REQ-CFG-01](#req-cfg-01); hardening [REQ-MEM-01](#req-mem-01). #### REQ-EXT-01 **Enforce TLS peer/host verification and https scheme on AI LLM outbound requests** The AI client SHALL set `CURLOPT_SSL_VERIFYPEER=1` and `CURLOPT_SSL_VERIFYHOST=2` on every outbound LLM request, SHALL require the provider `api_url` scheme to be `https` (rejecting `http://` at provider-add and request time), and SHALL NOT expose any per-provider option to disable verification, so the API key and conversation content are never sent in cleartext or to an unverified peer. *(Merges REQ-CFG-06.)* *NIST: PR.DS-02, PR.PS-01 | ISO: A.5.14, A.8.24, A.8.27 | Priority: critical | Verify: grep `src/ai/ai_client.c` (`_curl_exec_and_handle`/`_ai_request_thread`) for VERIFYPEER/VERIFYHOST set non-zero on both curl sites and never to 0L; add a test that `ai_add_provider` rejects an `http://` api_url.* #### REQ-EXT-02 **Explicit opt-in consent before transmitting conversation content to LLM providers** The AI client SHALL transmit message/conversation content to an external LLM provider only after the user has explicitly enabled the feature and configured a provider, SHALL be disabled by default, and SHALL NOT auto-forward received XMPP chat/MUC messages to any provider without an explicit per-action user command (no wiring to inbound-message plugin/stanza hooks). AI prompt/response content SHALL NOT be ingested into the XMPP chat history DB/chatlogs and SHALL live only in volatile per-session memory unless the user opts into persistence. *(Merges REQ-EXT-09.)* *NIST: PR.DS-01, GV.PO-01 | ISO: A.5.14, A.8.24, A.8.26 | Priority: critical | Verify: confirm the AI feature is gated behind a default-off preference and explicit `/ai` command; verify no `prof_pre/post` chat/room hook or stanza-receive path calls `ai_send_prompt`; test that with no provider configured `ai_send_prompt` performs no network call; confirm no code path passes AI session content into `chatlog_`/`database_` APIs and history is freed on `ai_session_unref`.* #### REQ-EXT-03 **Bounded, time-limited AI calls with response-size cap; bounded plugin/AI buffering** Every AI curl handle SHALL set a finite `CURLOPT_TIMEOUT` (and ideally `CURLOPT_CONNECTTIMEOUT`), and `_write_callback` SHALL cap accumulated response size (current 10 MB) and abort the transfer when exceeded, so a slow/hung/malicious endpoint cannot hang the UI thread or exhaust memory. *NIST: PR.IR-03, ID.RA-09 | ISO: A.8.6, A.8.16 | Priority: high | Verify: review every AI curl handle sets `CURLOPT_TIMEOUT` and that `_write_callback` returns short to abort at the cap; test with a mock server that streams past the cap and one that stalls, asserting bounded abort.* #### REQ-EXT-04 **Plugins load only from the protected 0700 plugins directory; run with full privilege; downloads verified** C plugins (`dlopen` with `RTLD_NOW|RTLD_GLOBAL`) and Python plugins SHALL be loaded only from the 0700 per-user plugins directory under the data path (`files_get_data_path(DIR_PLUGINS)`, no user-supplied absolute/CWD/relative/shared path), SHALL NOT be auto-loaded without explicit user opt-in, SHALL be stored without world/group-write bits, and any `/plugins install` download SHALL require an https source with TLS verification. The full-privilege trust model (plugins access all secret stores) SHALL be documented. *(Merges REQ-AUTH-08, REQ-SUP-09 plugin half.)* *NIST: PR.AA-05, PR.PS-01, ID.RA-09 | ISO: A.8.19, A.8.25, A.8.2 | Priority: high | Verify: confirm the load dir derives only from `files_get_data_path(DIR_PLUGINS)` and is created `S_IRWXU`; confirm no implicit auto-load of untrusted paths; verify `plugin_download.c` enforces https + TLS verify (no `SSL_VERIFY 0L`); check installed plugin files lack world/group write; document the plugin trust model in user docs.* ### REQ-SUP — Supply Chain & Third-Party Dependencies #### REQ-SUP-01 **Pin the libstrophe Meson subproject to an immutable revision/checksum** `subprojects/libstrophe.wrap` SHALL pin libstrophe to an exact immutable revision (40-hex commit SHA or release tag plus `source_hash`/wrapdb checksum) and SHALL NOT use `revision = HEAD` or a bare branch, so a build cannot silently incorporate an attacker-modified or regressed XMPP parser. *(Merges REQ-VUL-03, REQ-SDLC-07 wrap half; libstrophe sits on the untrusted network path.)* *NIST: GV.SC-05, ID.RA-09, PR.PS-01 | ISO: A.5.21, A.5.23, A.8.8, A.8.28, A.8.30 | Priority: critical | Verify: grep the `.wrap`: assert no `revision = HEAD`/bare branch and presence of a 40-hex `revision` or `source_hash`; add a CI lint failing on unpinned wrap revisions; confirm two clean builds at the pinned revision produce identical libstrophe object hashes.* #### REQ-SUP-02 **Pin CI base images by digest and bootstrapped deps by commit; verify download provenance** All `ci/Dockerfile.*` base images SHALL be pinned by immutable digest (`FROM image@sha256:…`, not `debian:testing`/`archlinux:latest`), and all source dependencies cloned/downloaded during image build (libstrophe, stabber, AUR `libstrophe-git`) SHALL be checked out at a pinned commit and obtained over authenticated HTTPS with integrity verification (commit/checksum/signature), not `--depth 1` HEAD or unverified `wget`+`makepkg`. *(Merges REQ-SUP-08, REQ-VUL-10.)* *NIST: GV.SC-06, GV.SC-07, GV.SC-05 | ISO: A.5.20, A.5.21, A.5.23, A.8.30 | Priority: critical | Verify: grep Dockerfiles for `FROM .*:latest`/`:testing`, `clone --depth 1` without a following `git checkout <sha>`, and `wget`/`curl` downloads lacking a subsequent `sha256sum`/`gpg` check; assert each `FROM` contains `@sha256:`; confirm `makepkg` integrity checks are not bypassed.* #### REQ-SUP-03 **Enforce minimum, CVE-informed versions for all security-relevant dependencies (build fails below floor)** The build SHALL fail (`AC_MSG_ERROR`, not notice/warn) when any security-relevant dependency is below a documented CVE-informed minimum: libstrophe, libcurl, sqlite3, glib, gpgme, libotr, libsignal-protocol-c/libomemo-c, libgcrypt. gpgme in particular SHALL be checked via versioned `PKG_CHECK_MODULES([gpgme],[gpgme >= X.Y])`, replacing the unversioned `AC_CHECK_LIB([gpgme],[main])`. The declared minimums SHALL be identical between `configure.ac` and `meson.build` (current divergences: sqlite3 3.22.0 vs 3.35.0; gpgme unversioned), each floor documented with security rationale. *(Merges REQ-CRY-11, REQ-SUP-03, REQ-SUP-04, REQ-VUL-02.)* *NIST: ID.RA-09, GV.SC-04, PR.PS-02 | ISO: A.5.21, A.8.8, A.8.28 | Priority: critical | Verify: grep both files to confirm a version-bearing `PKG_CHECK_MODULES` (no bare `AC_CHECK_LIB([gpgme],[main])`); a script diffs the `(name,minversion)` tuples and fails on mismatch; configuring against a stubbed under-floor `.pc` aborts configure non-zero; confirm `gcry_check_version(GCRYPT_VERSION)` gates init.* #### REQ-SUP-04 **Continuous dependency-CVE scanning in CI** CI SHALL run an automated known-vulnerability scanner (OSV-Scanner/Dependabot/grype/Trivy) over the third-party set (libstrophe, gpgme, libsignal/libomemo-c, openssl/gnutls, libgcrypt, libotr, sqlite, glib, libcurl) on each push and on a recurring schedule, with results triaged and surfaced; a seeded known-vulnerable pinned version SHALL trigger an alert. *(Merges REQ-VUL-04.)* *NIST: ID.RA-01, GV.SC-08, ID.IM-02 | ISO: A.5.7, A.8.8 | Priority: high | Verify: confirm a scan job exists in `.github/workflows` (or `dependabot.yml` for the wrap/Docker ecosystems), runs on cron, and fails/reports above a defined severity; verify a seeded vulnerable version triggers an alert; check triage records exist.* #### REQ-SUP-05 **Pin GitHub Actions to commit SHAs** All third-party GitHub Actions SHALL be pinned to a full commit SHA, not a mutable tag (`actions/checkout@<sha>` not `@v4`). *NIST: GV.SC-06, PR.PS-01 | ISO: A.8.30, A.8.28 | Priority: medium | Verify: grep `.github/workflows/*.yml` for `uses:.*@v[0-9]` or any non-40-hex ref and fail; optionally enforce with a pinning linter (zizmor/ratchet) in CI.* #### REQ-SUP-06 **Govern the C/Python plugin and AI-provider dependency boundary (documentation)** The project SHALL document and enforce the runtime trust boundary: dlopen'd C plugins and Python plugins are out-of-tree third-party code that run with full process privilege and SHALL NOT auto-load without opt-in (enforced by [REQ-EXT-04](#req-ext-04)), and the AI client's outbound LLM endpoints/dependencies SHALL be restricted to user-configured, TLS-validated hosts (enforced by [REQ-EXT-01](#req-ext-01)). *NIST: GV.SC-04, ID.RA-09 | ISO: A.5.19, A.8.30 | Priority: high | Verify: confirm documentation of the plugin/AI supply-chain boundary exists and that the enforcing controls ([REQ-EXT-01](#req-ext-01)/[REQ-EXT-04](#req-ext-04)) have passing tests; confirm the AI module only contacts configured hosts with cert verification.* ### REQ-VUL — Vulnerability & Patch Management, Change Control > Dependency floors ([REQ-SUP-03](#req-sup-03)), wrap pinning ([REQ-SUP-01](#req-sup-01)), CVE scanning ([REQ-SUP-04](#req-sup-04)), reproducible CI images ([REQ-SUP-02](#req-sup-02)), and hardening verification ([REQ-MEM-01](#req-mem-01)) are consolidated above. #### REQ-VUL-01 **Documented vulnerability-disclosure policy with scope, supported versions and response SLA** `SECURITY.md` SHALL specify, beyond a contact and PGP key: supported/maintained versions, in-scope components (OMEMO/OTR/PGP/OX crypto paths, XMPP stanza parsing, TLS, plugins, AI client), out-of-scope items, a target acknowledgement and remediation timeframe, and coordinated-disclosure expectations; designated owners for security-critical subsystems SHALL be recorded. *(Merges REQ-SDLC-10.)* *NIST: RS.MA-01, ID.IM-01, GV.RR-02, GV.OC-02 | ISO: A.5.5, A.5.26, A.8.8, A.5.2 | Priority: high | Verify: inspect `SECURITY.md` for a Supported Versions section, enumerated in/out-of-scope components, an explicit ack/remediation SLA, and coordinated-disclosure terms; confirm the contact key/fingerprint is current; add a CI doc-lint failing if required headings are absent.* #### REQ-VUL-02 **Update-availability check verifies TLS and treats the response as untrusted** `release_get_latest()` SHALL explicitly set `CURLOPT_SSL_VERIFYPEER=1L` and `CURLOPT_SSL_VERIFYHOST=2L`, constrain itself to HTTPS, and treat the fetched version string as untrusted (strict `^\d+\.\d+\.\d+$` validation before display), never auto-downloading or executing anything. *NIST: ID.RA-01, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: high | Verify: read `src/common.c` and assert the curl handle sets VERIFYPEER/VERIFYHOST and rejects non-https; assert `cons_check_version`/`release_is_new` validate against the version regex before use; unit-test feeding malformed/oversized version strings.* #### REQ-VUL-03 **User-controllable suppression of version/OS disclosure to peers** XEP-0092 version replies and disco SHALL be gated by user preferences, default to NOT revealing OS details (`PREF_REVEAL_OS` off), and allow disabling the `<version>` element entirely, so a peer cannot fingerprint an un-patched client+OS+build to target known CVEs. *NIST: PR.AA-05, PR.DS-02 | ISO: A.8.8, A.8.24 | Priority: medium | Verify: inspect the `src/xmpp/iq.c` version-reply: confirm `<os>` is added only when `PREF_REVEAL_OS` is true and `is_custom_client` false, a pref exists to omit `<version>`, and `PREF_REVEAL_OS` defaults off; stabber/functional test asserting the reply omits `<os>` by default.* #### REQ-VUL-04 **Security fixes recorded in CHANGELOG with CVE/advisory references** Each release SHALL document security-relevant fixes in the CHANGELOG, referencing the associated CVE/advisory identifier where one exists, so downstream packagers and users can assess patch urgency. *NIST: RC.RP-05, ID.IM-04 | ISO: A.8.8, A.5.26 | Priority: medium | Verify: review the CHANGELOG for a security/fix section per release with CVE/advisory ids where applicable (precedent: `Fix CVE-2017-5592`); extend `RELEASE_GUIDE.md` to mandate a security-fix changelog review before tagging.* ### REQ-RES — Resilience, Robustness & Fail-Safe Behavior > Fail-closed E2EE ([REQ-CRY-01](#req-cry-01)), force-encryption gate ([REQ-CRY-02](#req-cry-02)), OMEMO fail-safe trust ([REQ-CRY-10](#req-cry-10)), bounded stanza/OMEMO parsing ([REQ-INP-09](#req-inp-09)), and bounded AI calls ([REQ-EXT-03](#req-ext-03)) are consolidated above. #### REQ-RES-01 **Disk-space and atomicity preflight for SQLite schema migrations** Before any chatlog schema migration cproof SHALL verify free disk space via `_check_available_space_for_db_migration()` and SHALL run each migration inside a single transaction (BEGIN/COMMIT with rollback on error), so a failed/interrupted migration leaves the DB at its prior consistent schema version; on failure it SHALL surface an error and refuse to continue with a partially-migrated DB. *NIST: RC.RP-01, PR.IR-01 | ISO: A.8.13, A.8.27 | Priority: high | Verify: local (non-CI) migration tests on v1/v2 fixtures: simulate ENOSPC and assert migration refused with version unchanged; abort mid-migration and assert `DbVersion` reflects the pre-migration version; static review each `_migrate_to_vN` wraps statements in a transaction with rollback.* #### REQ-RES-02 **Detect and gracefully handle corrupted history DB without crashing or data loss** cproof SHALL run `PRAGMA integrity_check` on the chatlog DB and SHALL handle a non-`ok` result (and `sqlite3_open`/prepare failures) by reporting to the user and degrading gracefully (e.g. disabling DB-backed history for the session) rather than `abort()`/`exit()` or silently writing into a corrupt store. *NIST: RC.RP-01, PR.IR-03 | ISO: A.8.27, A.8.13 | Priority: high | Verify: open a deliberately truncated/garbage DB file and assert the app reports an integrity issue and continues running (no abort/exit, history disabled); confirm `_sqlite_verify_integrity` results are inspected and surfaced.* #### REQ-RES-03 **Graceful connection-loss handling with state-preserving reconnect (no message loss, no security downgrade)** On unexpected connection failure cproof SHALL transition to a defined DISCONNECTED state, preserve XEP-0198 stream-management state for resumable reconnect, and gate reconnect/auto-ping on a valid (non-NULL) connection, so connection flicker does not cause crashes, duplicate/dropped messages, or any change in the per-chat negotiated encryption posture (E2EE flags MUST survive reconnect). *NIST: PR.IR-03, RC.RP-01 | ISO: A.8.27, A.5.29 | Priority: high | Verify: drop the socket mid-session and assert status becomes DISCONNECTED, `sm_state` is captured and re-applied on reconnect, no crash, and chat E2EE flags (`is_omemo`/`is_ox`/`pgp_send`) are unchanged after resume; confirm `iq_autoping_check`/reconnect paths null-check the connection (cf. 15dfc2bd).* #### REQ-RES-04 **Sanitize outgoing message content to prevent self-inflicted stanza corruption** Outgoing chat, MUC, and private message bodies SHALL pass through `str_xml_sanitize()` before stanza construction, ensuring user/plugin-supplied content cannot inject control characters or invalid XML that would corrupt the stream or cause a server/peer disconnect. *NIST: PR.IR-01, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: medium | Verify: test that messages with control/invalid-XML characters are sanitized before stanza construction and the stanza serializes to well-formed XML; static check that every outbound path (`cl_ev_send_msg_correct`, `cl_ev_send_muc_msg_corrected`, `cl_ev_send_priv_msg`) calls `str_xml_sanitize` on plugin/user bodies.* ### REQ-SDLC — Secure Development Lifecycle & Governance > Hardening parity ([REQ-MEM-01](#req-mem-01)), sanitizer/Valgrind CI gate ([REQ-MEM-02](#req-mem-02)), CWE-134 attribute audit blocking in CI ([REQ-INP-04](#req-inp-04)), dependency pinning/floors ([REQ-SUP-01](#req-sup-01)/[REQ-SUP-03](#req-sup-03)), and the security policy/ownership doc ([REQ-VUL-01](#req-vul-01)) are consolidated above. #### REQ-SDLC-01 **No secrets, private keys, or real credentials committed to the repo; secret-scanning in CI** The repository (working tree and pre-merge feature-branch history) SHALL contain no account passwords, OMEMO/OTR/PGP private key material, or other live secrets; tests and example configs SHALL use only placeholder/mock values, and an automated secret-scanning step (gitleaks/detect-secrets) SHALL run in CI to block credential-like content. *NIST: PR.AA-01, GV.OC-03 | ISO: A.8.4, A.5.8 | Priority: critical | Verify: add a gitleaks/detect-secrets CI job over the diff and full history; grep for PEM private-key headers (`BEGIN .* PRIVATE KEY`) and `password=`; confirm `profrc.example` and functional-test fixtures use only `@localhost`/stabber mock identities and no real server/passwords.* #### REQ-SDLC-02 **Test/dev data isolated from real user profiles and live XMPP services** cproof tests SHALL NOT read from or write to a real user's profanity profile (`~/.local/share/profanity`, `~/.config/profanity`) nor connect to production XMPP servers; functional tests SHALL run against the stabber mock using throwaway `@localhost` identities and isolated temporary `XDG_DATA_HOME`/`XDG_CONFIG_HOME` dirs created/torn down by the harness. *NIST: PR.IR-01, ID.AM-08 | ISO: A.8.31, A.8.29 | Priority: high | Verify: inspect functional-test setup/teardown for an isolated tmpdir XDG_* targeting localhost only (including `PROF_FLATFILE` paths); grep tests for absolute `$HOME` profile paths and for any non-localhost host fixture.* #### REQ-SDLC-03 **Mandatory peer review with merge gating and CODEOWNERS for security-critical paths** Every change SHALL merge via a pull request that passed required CI checks (build matrix, unit + functional tests, Valgrind/sanitizers) and received review approval before merge to master; changes touching `src/omemo`, `src/otr`, `src/pgp`/OX, `src/xmpp/connection.c` (TLS/SASL), authentication, `src/plugins`, or `src/ai` SHALL require review by a designated owner recorded in `.github/CODEOWNERS`. Security-relevant changes SHALL carry a conventional-commit `fix(<scope>)`/`feat(<scope>)` subject naming the affected module. *(Merges REQ-VUL change-control.)* *NIST: GV.RR-02, PR.PS-06, ID.IM-03 | ISO: A.5.8, A.8.25, A.8.32 | Priority: high | Verify: confirm branch protection on master requires PR review and passing checks; confirm commitlint runs in CI and rejects non-conventional types; add/verify a `CODEOWNERS` mapping the security-critical dirs and that GitHub enforces owner review; spot-check `git log` for `fix(<scope>)` security subjects.* #### REQ-SDLC-04 **Enforced secure-coding style and static safety conventions (blocking)** cproof source SHALL conform to the project's CI-enforced conventions — clang-format (`.clang-format`), correct use/initialization of the GLib `auto_*` cleanup types, and codespell cleanliness — and the style/auto-type checks SHALL be blocking (not `continue-on-error`) as a precondition for merge. *NIST: PR.PS-01, PR.PS-06 | ISO: A.8.25, A.8.28 | Priority: medium | Verify: run `make doublecheck` (format + spell) and require pass; run the auto-type count/init grep checks from the workflow and require zero violations; remove `continue-on-error` from the format/auto-type steps so they block merges.* #### REQ-SDLC-05 **Fuzz harness for highest-risk untrusted-input parsers** cproof SHALL maintain at least one fuzz harness (libFuzzer/AFL++, built under sanitizers) exercising stanza/XML handling, JID parsing, and inbound encrypted-message decode (OMEMO/PGP/OX), runnable from the repo with an in-tree seed corpus and exercised periodically in a scheduled CI job that fails on new crashes. *NIST: PR.PS-06, ID.RA-01 | ISO: A.8.29, A.8.25 | Priority: medium | Verify: confirm a fuzz target exists and builds with `-fsanitize=fuzzer,address` over stanza and JID parse entry points; run it for a bounded time in scheduled CI and require no new crashes; track the seed corpus in-repo.* #### REQ-SDLC-06 **Documented security requirements and threat assumptions for crypto/plugin/AI surfaces** cproof SHALL document its security requirements and trust assumptions for security-sensitive features — E2EE invariants (no plaintext fallback, fingerprint/trust handling), TLS/cert-validation policy, the privilege/trust model for C/Python plugins, and the AI module's data-egress behavior (which providers receive content, under what consent) — and SHALL map each documented invariant to an enforcing unit/functional test or code guard (this catalog plus its REQ-IDs serve as that baseline). *NIST: GV.PO-01, PR.DS-02, GV.SC-04 | ISO: A.5.8, A.8.25 | Priority: medium | Verify: confirm a documented requirements/threat-model artifact covers E2EE no-plaintext-fallback, TLS policy, plugin trust boundary, and AI egress/consent; map each invariant to a test (e.g. `tests/unittests/command/test_cmd_pgp.c`, `test_cmd_otr.c`) or code guard referencing the relevant REQ-ID.* ### 4. Traceability Summary **NIST CSF 2.0 → Requirements** - **GV (Govern)** — GV.PO-01: REQ-DAR-07, REQ-EXT-02, REQ-SDLC-06. GV.RR-02: REQ-SDLC-03, REQ-VUL-01. GV.OC-02/03: REQ-VUL-01, REQ-SDLC-01. GV.SC-04/05/06/07/08: REQ-SUP-01, REQ-SUP-02, REQ-SUP-03, REQ-SUP-04, REQ-SUP-05, REQ-SUP-06, REQ-SDLC-06. - **ID (Identify)** — ID.RA-01/08/09: REQ-CRY-11(→SUP-03), REQ-SUP-03, REQ-SUP-04, REQ-VUL-02, REQ-EXT-03, REQ-EXT-04, REQ-SDLC-05, REQ-MEM-02. ID.IM-01/02/03/04: REQ-VUL-01, REQ-VUL-04, REQ-SUP-04, REQ-SDLC-03. ID.AM-08: REQ-SDLC-02, REQ-SUP-01. - **PR (Protect)** — PR.AA-01/03/04/05: REQ-CRY-04, REQ-CRY-09, REQ-CRY-10, REQ-AUTH-01..06, REQ-CFG-01, REQ-VUL-03, REQ-SDLC-01. PR.DS-01/02/10: REQ-CRY-01..10, REQ-AUTH-01/06, REQ-DAR-01..07, REQ-INP-01/03/05/08/10, REQ-CFG-01/02, REQ-EXT-01/02. PR.PS-01/02/04/05/06: REQ-MEM-01..09, REQ-INP-03/04/06/07, REQ-LOG-01..06, REQ-SUP-03/05, REQ-EXT-04, REQ-SDLC-03/04/05. PR.IR-01/03/04: REQ-CRY-01/02, REQ-INP-09, REQ-RES-01..04, REQ-EXT-03, REQ-LOG-05, REQ-SDLC-02. - **DE (Detect)** — DE.AE-02: REQ-LOG-01, REQ-LOG-02, REQ-LOG-03. DE.CM-01/06/09: REQ-MEM-02, REQ-MEM-08, REQ-LOG-01, REQ-LOG-02, REQ-LOG-06, REQ-CRY-04, REQ-CFG-02, REQ-INP-09. - **RS / RC (Respond / Recover)** — RS.MA-01: REQ-VUL-01. RC.RP-01/05: REQ-RES-01, REQ-RES-02, REQ-RES-03, REQ-VUL-04. **ISO/IEC 27001:2022 Annex A clusters → Requirements** - **A.5 (Organizational)** — A.5.5/5.26 (incident/vuln reporting): REQ-VUL-01, REQ-VUL-04. A.5.7 (threat intel): REQ-SUP-04. A.5.8 (project security): REQ-SDLC-01/03/06, REQ-VUL-01. A.5.14 (info transfer): REQ-EXT-01/02, REQ-CFG-01. A.5.17 (auth info): REQ-AUTH-01..06, REQ-CRY-09/10. A.5.19/5.20/5.21/5.23 (supplier/ICT supply chain): REQ-SUP-01..06. A.5.29 (continuity): REQ-RES-03. A.5.31 (legal/identity binding): REQ-CRY-09. A.5.34 (PII/minimization): REQ-DAR-07. A.5.10 (acceptable use of info): REQ-CRY-02. - **A.8 (Technological)** — A.8.2/8.3 (access/privileged access): REQ-AUTH-01/02/05, REQ-EXT-04. A.8.4 (source code/secrets): REQ-SDLC-01. A.8.5 (secure auth): REQ-AUTH-03. A.8.6 (capacity): REQ-LOG-05, REQ-EXT-03. A.8.8 (technical vuln mgmt): REQ-SUP-03/04, REQ-VUL-01..04. A.8.9 (config mgmt/secure config): REQ-CRY-03/04/05/10, REQ-AUTH-03, REQ-CFG-01/02. A.8.10/8.11/8.12 (data masking/leakage/storage): REQ-DAR-01..07, REQ-AUTH-01. A.8.13 (backup/integrity): REQ-RES-01/02. A.8.15/8.16 (logging/monitoring): REQ-LOG-01..06, REQ-DAR-03. A.8.19 (software on operational systems): REQ-EXT-04. A.8.20/8.21 (network/network services security): REQ-CRY-03/04, REQ-AUTH-03. A.8.24 (cryptography): REQ-CRY-01..10, REQ-AUTH-06, REQ-EXT-01, REQ-CFG-01. A.8.25/8.27/8.28 (secure SDLC/architecture/secure coding): REQ-MEM-01..09, REQ-INP-01..10, REQ-SUP-05, REQ-SDLC-03..06, REQ-RES-01/04. A.8.26 (application security requirements): REQ-INP-01..10, REQ-EXT-02, REQ-RES-04. A.8.29 (security testing): REQ-MEM-02, REQ-INP-04, REQ-SDLC-02/04/05. A.8.30 (outsourced/third-party dev): REQ-SUP-01/02/06. A.8.31 (env separation): REQ-SDLC-02. A.8.32 (change management): REQ-SDLC-03, REQ-VUL change-control. **Explicitly Out of Scope** The following ISO/CSF control areas are intentionally excluded as not meaningful for a single-user desktop chat client with no central server or organizational IT estate: - Physical and environmental security (A.7.* — secure areas, equipment siting, cabling, physical entry, clear-desk physical aspects). - Human-resources security (A.6.* — screening, terms of employment, disciplinary process, awareness-training programs as an organizational HR function). - Datacenter / server-room operations, redundant power/cooling/utilities, and physical media handling/disposal at facility scale. - Organizational asset inventory of an IT estate, enterprise endpoint/MDM fleet management, network perimeter/firewall/IDS operations, and centralized IAM/directory services. - Business-continuity/DR planning at the organizational level, insurance, and supplier-contract/SLA legal management beyond the source-dependency pinning covered in [REQ-SUP-01](#req-sup-01)..[REQ-SUP-06](#req-sup-06). - Server-side XMPP service hardening (the client connects to, but does not operate, an XMPP server). ### CIS Controls v8.1 — requirements catalog The **CIS Critical Security Controls v8.1** (Center for Internet Security) are a prioritized set of 18 controls comprising 153 *safeguards* that an organization implements to defend against the most common attack patterns. v8.1 refines v8 by tightening safeguard wording and aligning each safeguard to a security function and to the three **Implementation Groups**: **IG1** is "essential cyber hygiene" — the foundational subset every organization should meet; **IG2** builds on IG1 for organizations managing more sensitive data and resources; **IG3** adds the remaining safeguards for mature programs defending against sophisticated adversaries (IG3 = all 153 safeguards). Every IG1 safeguard is also in IG2 and IG3. **Scope for cproof.** CIS v8.1 is an *enterprise-IT* framework (asset/account/network/SOC operations of an organizational estate). cproof is a single-user desktop E2EE chat client, not an estate, so most controls are summarized as *not applicable* (no fleet, no central directory, no network gateway, no SOC). The controls with a real **software/source-code surface** in a client + its repository — CIS-2 (software/dependency inventory & versioning), CIS-4 (secure configuration), CIS-5/6 (credential handling, MFA boundary), CIS-7 (vulnerability-management process), CIS-8 (audit-log management & content), CIS-9 (URL/file-type handling on the chat surface), CIS-15 (service-provider/upstream inventory & TLS enforcement toward providers), CIS-16 (application software security), and CIS-18 (penetration-testing/fuzzing & remediation) — are processed in depth and mapped to the existing `REQ-*` requirements wherever the control is already covered. > **Source note.** The CIS v8.1 PDF originally linked for this audit was unreachable (HTTP 404). The authoritative CIS v8.1 control numbers, safeguard numbers/titles, and IG1/IG2/IG3 tags were therefore taken from the official CIS Controls v8.1 structure (see https://www.cisecurity.org/controls/v8-1) and are used verbatim; no safeguard numbers were invented. #### CIS → existing requirements traceability This table maps each in-depth CIS v8.1 safeguard to the existing `REQ-*` requirement(s) that already cover it. "—" means the safeguard opens genuinely new ground and a new `REQ-CIS-*` requirement is introduced below. CIS-16 (Application Software Security) is the control most directly aligned with this catalog's whole purpose; its safeguards are mapped to the producer-side requirements en bloc. | CIS safeguard | Title | IG | Existing REQ-ID(s) | |---|---|---|---| | 2.1 | Establish and Maintain a Software Inventory | IG1 | REQ-SUP-03 (+ new REQ-CIS-01 SBOM) | | 2.2 | Ensure Authorized Software is Currently Supported | IG1 | REQ-SUP-03, REQ-SUP-01, REQ-SUP-04 | | 2.6 | Allowlist Authorized Libraries | IG2 | REQ-SUP-01, REQ-SUP-02, REQ-EXT-04 (+ new REQ-CIS-03 pinning) | | 4.1 | Establish and Maintain a Secure Configuration Process | IG1 | REQ-CRY-03/04/05/10, REQ-CFG-02, REQ-SDLC-06 (+ new REQ-CIS-06 baseline doc) | | 4.6 | Securely Manage Enterprise Assets and Software | IG1 | REQ-CRY-03/05, REQ-AUTH-03, REQ-CFG-01/02 | | 4.8 | Uninstall or Disable Unnecessary Services | IG2 | REQ-EXT-02, REQ-EXT-04, REQ-CFG-02 | | 5.2 | Use Unique Passwords | IG1 | REQ-AUTH-04/05/06, REQ-AUTH-01 | | 5.4 | Restrict Administrator Privileges to Dedicated Admin Accounts | IG1 | N/A (no admin-account model) | | 6.3 | Require MFA for Externally-Exposed Applications | IG1 | REQ-AUTH-03, REQ-CRY-03 (N/A as written) | | 7.1 | Establish and Maintain a Vulnerability Management Process | IG1 | REQ-VUL-01, REQ-SUP-04, REQ-SUP-03 (+ new REQ-CIS-02) | | 7.2 | Establish and Maintain a Remediation Process | IG1 | REQ-SUP-01, REQ-SUP-03, REQ-VUL-04, REQ-SUP-04 (+ new REQ-CIS-02) | | 7.4 | Perform Automated Application Patch Management | IG1 | REQ-SUP-04, REQ-SUP-02, REQ-SUP-05 | | 8.1 | Establish and Maintain an Audit Log Management Process | IG1 | REQ-LOG-04, REQ-LOG-05, REQ-DAR-03 (+ new REQ-CIS-05 logging policy) | | 8.2 | Collect Audit Logs | IG1 | REQ-LOG-01, REQ-LOG-02, REQ-LOG-04/05, REQ-AUTH-01 | | 8.5 | Collect Detailed Audit Logs | IG2 | REQ-LOG-01, REQ-LOG-03, REQ-DAR-03 | | 9.3 | Maintain and Enforce Network-Based URL Filters | IG2 | REQ-INP-06 | | 9.6 | Block Unnecessary File Types | IG2 | REQ-CRY-06, REQ-CFG-01 | | 15.1 | Establish and Maintain an Inventory of Service Providers | IG1 | REQ-EXT-02, REQ-SUP-06, REQ-SUP-01 (+ new REQ-CIS-01/REQ-CIS-08) | | 15.4 | Ensure Service Provider Contracts Include Security Reqs | IG2 | REQ-EXT-01, REQ-CRY-03, REQ-SUP-01, REQ-VUL-01 (+ new REQ-CIS-08) | | 16.2 | Establish and Maintain a Process to Accept and Address Software Vulnerabilities | IG2 | REQ-VUL-01 (+ new REQ-CIS-02) | | 16.3 | Perform Root Cause Analysis on Security Vulnerabilities | IG2 | REQ-VUL-04, REQ-SDLC-03 | | 16.4 | Establish and Manage an Inventory of Third-Party Software Components | IG2 | REQ-SUP-03 (+ new REQ-CIS-01 SBOM) | | 16.5 | Use Up-to-Date and Trusted Third-Party Software Components | IG2 | REQ-SUP-01/02/03/04 (+ new REQ-CIS-03) | | 16.6 | Establish and Maintain a Severity Rating System and Process for Application Vulnerabilities | IG2 | new REQ-CIS-02 | | 16.7 | Use Standard Hardening Configuration Templates for Application Infrastructure | IG2 | REQ-MEM-01 (+ new REQ-CIS-06) | | 16.8 | Separate Production and Non-Production Systems | IG2 | REQ-SDLC-02 (+ new REQ-CIS-07) | | 16.9 | Train Developers in Application Security Concepts and Secure Coding | IG2 | REQ-SDLC-04/06 (process; doc-only) | | 16.10 | Apply Secure Design Principles in Application Architectures | IG2 | REQ-CRY-01/02/10, REQ-SDLC-06 | | 16.11 | Leverage Vetted Modules or Services for Application Security Components | IG2 | REQ-CRY-06/07/08 | | 16.12 | Implement Code-Level Security Checks | IG2 | REQ-INP-03/04, REQ-MEM-02, REQ-SDLC-04/05 (+ new REQ-CIS-04 SAST/fuzz in CI) | | 16.13 | Conduct Application Penetration Testing | IG3 | REQ-SDLC-05, REQ-MEM-02 (+ new REQ-CIS-04) | | 16.14 | Conduct Threat Modeling | IG3 | REQ-SDLC-06 | | 18.1 | Establish and Maintain a Penetration Testing Program | IG2 | REQ-MEM-02, REQ-SDLC-05, REQ-INP-01/09 (+ new REQ-CIS-04) | | 18.3 | Remediate Penetration Test Findings | IG2 | REQ-VUL-01/04, REQ-SDLC-03 (+ new REQ-CIS-02) | #### CIS-specific requirements (new) The following requirements cover ground that the CIS v8.1 application-security and supply-chain safeguards demand but that the existing NIST/ISO catalog only addresses indirectly (or via a *Verify* clause rather than a normative SHALL). IDs are stable and never renumbered. Where an existing `REQ-*` already enforces the control, no new requirement is created — the traceability table above references it instead. #### REQ-CIS-01 **Maintain a machine-readable third-party software inventory / SBOM** The project SHALL maintain, in-repo, an authoritative inventory of every third-party software component cproof links or bundles (libstrophe, gpgme, libsignal-protocol-c/libomemo-c, libotr, sqlite3, glib/gio, libcurl, libgcrypt, ncursesw, readline, gtk, libnotify, qrencode) recording for each: declared minimum version, license, and upstream URL; the inventory SHALL be emitted/validated as a CycloneDX or SPDX SBOM in CI so it stays in sync with `configure.ac`/`meson.build`, and the divergent autotools-vs-meson version floors (e.g. sqlite3 3.22.0 vs 3.35.0; gpgme/libgcrypt declared in meson but unversioned in autotools) SHALL be reconciled to a single source of truth that feeds the inventory. *(Today the only narrative inventory of the third-party set is [REQ-SUP-03](#req-sup-03) in this catalog; no SBOM/SPDX/CycloneDX artifact exists in the tree.)* *CIS: 2.1 (IG1), 16.4 (IG2), 15.1 (IG1) | NIST: ID.AM-08, GV.SC-04 | ISO: A.5.21, A.8.8 | Priority: medium | Verify: confirm a `compliance/SBOM.{cdx,spdx}.json` (or generated equivalent) exists, lists every linked library with version/license/URL, and is regenerated+diff-checked in CI; assert the autotools and meson floors are identical for every shared dependency.* #### REQ-CIS-02 **Documented process to accept, severity-rate, and remediate software vulnerabilities** The project SHALL maintain a documented, recurring process for accepting and addressing security vulnerabilities in cproof and its dependencies that, beyond the inbound contact, defines: an intake/triage path (extending `SECURITY.md` per [REQ-VUL-01](#req-vul-01)), a severity-rating scheme (e.g. CVSS-aligned, with E2EE plaintext-leak / MitM-bypass / RCE classed critical), severity-keyed remediation targets (SLA), a root-cause and regression-test requirement per fix, and a release gate so critical findings block a tag. *(SECURITY.md is presently 5 lines — contact + PGP fingerprint only, no supported-versions, scope, SLA, or severity model; RELEASE_GUIDE.md has no security step.)* *CIS: 7.1 (IG1), 7.2 (IG1), 16.2 (IG2), 16.6 (IG2), 18.3 (IG2) | NIST: RS.MA-01, ID.IM-01/02 | ISO: A.5.5, A.5.26, A.8.8 | Priority: high | Verify: confirm a documented vuln-handling+severity+SLA process exists (in `SECURITY.md`/`CONTRIBUTING.md`/a `compliance/` doc); confirm each landed security fix carries a regression test and a CHANGELOG entry ([REQ-VUL-04](#req-vul-04)); confirm a release checklist gate on open critical findings.* #### REQ-CIS-03 **Pin and integrity-verify all build-time and runtime library sources to an authorized identity** Every dependency source the build consumes SHALL be pinned to an immutable, integrity-verified identity: the libstrophe Meson wrap SHALL pin a 40-hex commit SHA or release tag + `source_hash` (not `revision = HEAD`); CI base images SHALL be digest-pinned and bootstrapped/cloned deps commit-pinned with checksum/signature verification; and at runtime, dynamically loaded plugin binaries SHALL be checksum/signature-verified before `dlopen` rather than only directory-restricted. *(Reinforces and is enforced through [REQ-SUP-01](#req-sup-01)/[REQ-SUP-02](#req-sup-02) and [REQ-EXT-04](#req-ext-04); this requirement states the CIS "authorized libraries" allowlist intent across build and runtime as one principle.)* *CIS: 2.6 (IG2), 16.5 (IG2) | NIST: GV.SC-05/06, ID.RA-09 | ISO: A.5.23, A.8.30 | Priority: high | Verify: grep `subprojects/*.wrap` for `revision = HEAD`/bare branch and fail; assert each `ci/Dockerfile.*` `FROM` carries `@sha256:` and every clone is followed by `git checkout <sha>`; confirm plugin install verifies a checksum/signature before `dlopen` ([REQ-EXT-04](#req-ext-04)).* #### REQ-CIS-04 **Code-level security checks (SAST + fuzzing + sanitizers) run as blocking CI** CI SHALL run application code-level security checks as enforced (non-`continue-on-error`) gates: static analysis (clang `scan-build` and the CWE-134 audit, [REQ-INP-04](#req-inp-04)/[REQ-SDLC-04](#req-sdlc-04)), the sanitizer build (`--enable-sanitizers`: ASan/UBSan, [REQ-MEM-02](#req-mem-02)), and at least one fuzz harness over the untrusted-input parsers (stanza/XML, `jid_create`/`jid_is_valid`, inbound OMEMO/PGP/OX decode, [REQ-SDLC-05](#req-sdlc-05)) on a scheduled job that fails on new crashes; a dependency-CVE scan ([REQ-SUP-04](#req-sup-04)) and a secret scan ([REQ-SDLC-01](#req-sdlc-01)) SHALL likewise run in CI. *(Today the `--enable-sanitizers` build is never exercised in CI, no fuzz harness exists, and the style/CWE-134/spell checks are `continue-on-error`.)* *CIS: 16.12 (IG2), 16.13 (IG3), 18.1 (IG2) | NIST: PR.PS-06, ID.RA-01, DE.CM-01 | ISO: A.8.28, A.8.29 | Priority: high | Verify: confirm CI jobs for scan-build, `--enable-sanitizers && make check`, a scheduled fuzz target, OSV/Dependabot, and gitleaks/detect-secrets, all blocking; intentionally introduce a heap overflow and confirm the sanitizer job fails.* #### REQ-CIS-05 **Documented application logging policy (events, levels, no-secrets constraint, retention)** The project SHALL maintain a short, governable logging policy enumerating which security-relevant events are captured and at which level (TLS certificate-validation failure, decrypt/MAC failure, untrusted-device use, SASL/login failure, TLS downgrade — all at WARN or higher so they survive the default filter, [REQ-LOG-01](#req-log-01)/[REQ-LOG-02](#req-log-02)), the absolute no-secrets / no-plaintext-body constraint ([REQ-DAR-03](#req-dar-03)), the default level (WARN, non-DEBUG, [REQ-LOG-04](#req-log-04)), rotation/size bounds ([REQ-LOG-05](#req-log-05)), and the 0600 file mode ([REQ-AUTH-01](#req-auth-01)); a CI grep SHALL fail on any `log_*` interpolation of `message->plain`/`body`/`password`/`api_key`/`Bearer`. *(Logging mechanics exist but no written policy does, and per-event level decisions are inconsistent — cert-fail and SASL-fail fall below WARN while plaintext bodies are logged by default; see CIS-8.2/8.5 in the audit.)* *CIS: 8.1 (IG1), 8.5 (IG2) | NIST: PR.PS-04, DE.AE-02 | ISO: A.8.15, A.8.16 | Priority: medium | Verify: confirm a logging-policy doc lists WARN+ security events, the no-secrets constraint, default level, rotation bounds, and 0600 mode; confirm a CI grep blocks plaintext-body/secret interpolation in `log_*`.* #### REQ-CIS-06 **Documented secure-configuration baseline (hardened defaults + standard hardening template)** The project SHALL document and maintain a secure-configuration baseline (e.g. `docs/SECURE-CONFIGURATION.md` or an extension of `SECURITY.md`) enumerating cproof's shipped hardened defaults — TLS mandatory ([REQ-CRY-03](#req-cry-03)), OMEMO trust `manual` ([REQ-CRY-10](#req-cry-10)), `PREF_ENC_WARN`/`PREF_TLS_SHOW` on, force-encryption resend-to-confirm, `PREF_DBLOG` privacy modes — with rationale, recommended overrides for a minimal-surface build (build without C plugins/AI, keep AI off, per CIS-4.8), the toolchain hardening template ([REQ-MEM-01](#req-mem-01)), and a stated review cadence; `PREF_REVEAL_OS` SHALL default OFF per [REQ-VUL-03](#req-vul-03) to align with the baseline. *(Defaults exist in code but are not captured as a governable standard; SECURITY.md is contact-only and `docs/` holds only man pages.)* *CIS: 4.1 (IG1), 16.7 (IG2) | NIST: PR.PS-01, GV.PO-01 | ISO: A.8.9, A.8.27 | Priority: medium | Verify: confirm a secure-config/hardening doc enumerates the shipped defaults with rationale + review cadence and references the hardening flags; confirm `PREF_REVEAL_OS` defaults FALSE.* #### REQ-CIS-07 **Separate production and non-production data; no real user data in test/dev** The project SHALL ensure test, functional, and development runs never read from or write to a real user's profile (`~/.local/share/profanity`, `~/.config/profanity`) or a production XMPP server, using isolated temporary `XDG_DATA_HOME`/`XDG_CONFIG_HOME` dirs and throwaway `@localhost`/stabber-mock identities only, and SHALL keep all fixtures/example configs free of real credentials (enforced jointly with [REQ-SDLC-01](#req-sdlc-01)/[REQ-SDLC-02](#req-sdlc-02)). *(Largely satisfied today via the stabber harness; stated here so CIS-16.8 has an explicit normative home.)* *CIS: 16.8 (IG2) | NIST: PR.IR-01, ID.AM-08 | ISO: A.8.31, A.8.29 | Priority: medium | Verify: inspect functional-test setup/teardown for isolated tmpdir XDG_* targeting localhost; grep tests for absolute `$HOME` profile paths and non-localhost hosts.* #### REQ-CIS-08 **Enforce a TLS/integrity security baseline toward every external service provider** cproof SHALL enforce a minimum security posture toward each external service provider it contacts: AI/LLM providers SHALL be rejected unless the `api_url` scheme is `https`, with `CURLOPT_SSL_VERIFYPEER=1L`/`VERIFYHOST=2L` set explicitly at every AI curl site ([REQ-EXT-01](#req-ext-01)); the XMPP-server `tls.policy=allow` path SHALL be mapped to a TLS-required-or-fail floor (or warn loudly) so SASL/registration credentials never traverse plaintext ([REQ-CRY-03](#req-cry-03)); and dependency upstreams (notably the libstrophe wrap) SHALL be consumed only at an immutable, integrity-pinned revision ([REQ-SUP-01](#req-sup-01)/[REQ-CIS-03](#req-cis-03)). A short `PROVIDERS`/`THIRD_PARTY` doc SHALL classify each provider category by the data sensitivity it sees (AI endpoint: API key + conversation; XMPP server: SASL creds + ciphertext; dependency upstreams). *(Cross-validates the AI `http://` egress gap and the `allow`-policy silent downgrade.)* *CIS: 15.1 (IG1), 15.4 (IG2) | NIST: PR.DS-02, GV.SC-04/05 | ISO: A.5.14, A.5.19, A.8.24, A.8.30 | Priority: high | Verify: assert `ai_add_provider`/the `/ai set provider` handler reject non-`https` and both AI curl sites set VERIFYPEER/VERIFYHOST non-zero; assert `tls.policy=allow` cannot send credentials in cleartext; assert the wrap is SHA-pinned; confirm a provider-classification doc exists.* #### Not applicable (enterprise-IT) controls Each of the following CIS v8.1 controls (or, for partly-applicable controls, the listed safeguards) governs an organizational IT estate with no source-code surface in a single-user desktop client; they are recorded as not applicable. - **CIS-1 — Inventory and Control of Enterprise Assets.** Hardware/device discovery and control across a fleet; cproof has no estate to inventory. - **CIS-3 — Data Protection (enterprise data-classification/DLP scope).** The client-side data-at-rest concerns are covered by [REQ-AUTH-01](#req-auth-01)/[REQ-AUTH-02](#req-auth-02), [REQ-DAR-01](#req-dar-01)..[07](#req-dar-07); org-wide data inventory/classification/DLP is N/A. - **CIS-5.4 — Restrict Administrator Privileges to Dedicated Admin Accounts.** No administrator/privileged-account model exists in a single-user client (host-OS / IdP concern). - **CIS-6.3 — Require MFA for Externally-Exposed Applications.** cproof exposes no inbound service; it is an outbound SASL client. SASL/MFA negotiation is delegated to libstrophe + the XMPP server, not implemented in the client (adjacent client-auth posture is [REQ-AUTH-03](#req-auth-03)/[REQ-CRY-03](#req-cry-03)). - **CIS-8.6/8.7/8.8/8.9/8.10/8.11/8.12 — DNS/URL/command-line/centralized/retained/reviewed/service-provider audit logs.** Network-infrastructure, gateway, SIEM, and SOC-process log safeguards; a desktop client operates no DNS/proxy/aggregation/retention estate. - **CIS-10 — Malware Defenses.** Endpoint AV/EDR deployment and management; host-OS responsibility. - **CIS-11 — Data Recovery.** Enterprise backup/recovery program; the client's only recovery-adjacent controls are migration safety/integrity ([REQ-RES-01](#req-res-01)/[REQ-RES-02](#req-res-02)). - **CIS-12 — Network Infrastructure Management.** Router/switch/firewall configuration management; no network infrastructure is owned. - **CIS-13 — Network Monitoring and Defense.** NIDS/NIPS/NetFlow/SIEM operations across an estate; N/A. - **CIS-14 — Security Awareness and Skills Training.** Organizational HR/awareness program; the developer-secure-coding analogue is CIS-16.9 ↔ [REQ-SDLC-04](#req-sdlc-04)/[REQ-SDLC-06](#req-sdlc-06) (doc-only). - **CIS-17 — Incident Response Management.** Org-level IR plan/team/exercises; the in-repo analogue is vulnerability handling, CIS-16.2/7.1 ↔ [REQ-VUL-01](#req-vul-01) / [REQ-CIS-02](#req-cis-02). - **CIS-18.2 (and 18.4/18.5) — external/remediation-validation pen-test estate operations.** A full external network/infrastructure pen-test presupposes an estate; the applicable application-level slice (18.1/18.3, 16.13) is processed above via [REQ-CIS-04](#req-cis-04) / [REQ-SDLC-05](#req-sdlc-05) / [REQ-MEM-02](#req-mem-02).
jabber.developer added this to the Plans (2025-2026) project 2026-07-01 11:59:38 +00:00
jabber.developer self-assigned this 2026-07-01 11:59:47 +00:00
jabber.developer2 was assigned by jabber.developer 2026-07-01 11:59:47 +00:00
Collaborator

cproof — Consolidated Compliance Findings Summary

This is a single, deduped view of every actionable Gap/Partial finding from the combined NIST CSF 2.0 + ISO/IEC 27001 + CIS Controls v8.1 audit of cproof at master (622054fc6). It merges the source audit (compliance/SOURCE-COMPLIANCE-ANALYSIS.md) with the requirements catalog (compliance/REQUIREMENTS-nist-iso-cis.md) and adds a code-aware effort estimate per item. Where one requirement was raised under multiple frameworks (or as a CIS→REQ duplicate), the rows are merged into one line and the merged IDs are noted; where two findings are fixed by the same change, the shared work is flagged in the Fix cell and counted once in the rollup.

Severity: critical (E2EE plaintext leak / credential egress / RCE-class), high, medium, low, nit. Status: Gap (control absent) or Partial (control exists but incomplete/misconfigured).

Effort scale: XS = trivial, < 2h (one-liner, a flag, a CI lint line). S = a few hours / ~0.5 day (localized change + a test). M = 1–2 days (multi-site change + tests, or one CI job). L = 3–5 days (subsystem change, new CI pipeline, hardening across both build systems). XL = 1 week+ (a process/program deliverable). Effort type: code | build-ci | process-docs | mixed.

Rollup

By severity: critical 1 · high 19 · medium 33 · low 2 · nit 0 — 55 findings.

By effort size: XS 9 · S 28 · M 10 · L 5 · XL 3.

Indicative total effort: roughly 45–70 engineer-days gross if every item ships; netting out shared-fix work (curl setopt sites, the redaction helper, the wrap/ref-pinning lint, the sanitizer/hardened CI job, the plugin-signature/trust design) and the REQ-CIS-04 CI bundle (which absorbs REQ-MEM-02 / REQ-SDLC-04 / REQ-SUP-04 / REQ-SDLC-01 / REQ-SDLC-05) trims this toward ~40–55 net. Treat this as an order-of-magnitude band for a small OSS C project, not a committed estimate — the three XL process/program items (fuzzing program, vuln-management process, blocking-CI build-out) plus the five L items dominate and carry the widest variance.

Quick wins (high/critical severity AND XS/S effort):

  • REQ-EXT-01 — reject AI http:// provider URLs + explicit VERIFY* (S · ~4h) — confirmed cleartext key/conversation egress.
  • REQ-INP-01 — guard 4 unguarded jid_create(from) derefs (S · 0.5d) — confirmed remote NULL-deref DoS.
  • REQ-DAR-03 — drop plaintext bodies/creds from profanity.log (S · 0.5–1d) — confirmed plaintext-in-logs.
  • REQ-SUP-01 — pin libstrophe wrap off revision = HEAD + CI lint (S · ~2–4h).
  • REQ-AUTH-01 — chmod 0600 OTR keystores + history DB (S · ~4h).
  • REQ-SUP-03 — versioned gpgme/gcrypt floors, align sqlite3 floor (S · 0.5d).
  • REQ-SUP-05 — pin GitHub Actions to commit SHAs (XS · ~1h).
  • REQ-VUL-01 — flesh out 7-line SECURITY.md (S · 0.5–1d).
  • REQ-SDLC-01 — add gitleaks/detect-secrets CI job (S · 3–5h).

Big rocks (XL / program-level process items):

  • REQ-CIS-02 — documented vuln accept/severity/remediate process (XL · 1w+).
  • REQ-CIS-04 — make SAST/sanitizers/fuzz/CVE/secret scans all blocking CI gates (XL · 1w+).
  • REQ-SDLC-05 — stand up a libFuzzer/AFL++ fuzzing program over untrusted parsers (XL · 1–2w).
  • (L, adjacent) REQ-EXT-04 plugin signature/trust model; REQ-MEM-01 toolchain hardening across both build systems; REQ-INP-08 control-char/bidi sanitizer; REQ-CIS-01 SBOM generation + CI reconciliation; REQ-CIS-03 integrity-pinned sources.

Findings

ID Finding Sev Status Effort Type Frameworks Evidence Fix
REQ-EXT-01 AI/LLM client accepts http:// provider URLs — cleartext API key + conversation egress (merges REQ-CIS-08) critical Gap S · ~0.5d (4h) code NIST PR.DS-02/PR.PS-01; ISO A.5.14/A.8.24/A.8.27; CIS 15.4 cmd_funcs.c:7019; ai_client.c:388,811-815,1596-1601 Reject non-https at provider-add (cmd_ai_set) and in ai_add_provider; set CURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2L at both curl sites; flip test_ai.c:85-95 to assert reject (shares curl sites with REQ-EXT-03)
REQ-AUTH-03 Insecure transport flags logged at debug, not WARN high Partial XS · ~1–2h code NIST PR.AA-03/PR.DS-02; ISO A.5.17/A.8.5/A.8.9/A.8.20; CIS 8.2 connection.c:201-213 Split DISABLE_TLS/LEGACY_*/TRUST_TLS to log_warning + cons_show so the downgrade is visible
REQ-SUP-05 GitHub Actions pinned to mutable tags, not commit SHAs low Gap XS · ~1h build-ci NIST GV.SC-06/PR.PS-01; ISO A.8.30/A.8.28; CIS 7.4 .github/workflows/*.yml actions/checkout@v4 Pin all @v4 (6 sites) to 40-hex SHAs; optional grep lint on uses:.*@v[0-9] (shared with REQ-SUP-01 lint)
REQ-SUP-01 libstrophe Meson wrap pinned to revision = HEAD (mutable XMPP parser) (merges CIS-2.6/16.5) high Gap S · ~2–4h mixed NIST GV.SC-05/ID.RA-09/PR.PS-01; ISO A.5.21/A.5.23/A.8.8/A.8.30; CIS 2.2/2.6/7.2/16.5 subprojects/libstrophe.wrap:3 Replace revision = HEAD with vetted 40-hex SHA (or tag+source_hash); add CI grep failing on revision = HEAD/bare-branch wraps (shared lint with REQ-SUP-05)
REQ-INP-01 Remote NULL-deref DoS: unguarded jid_create(from) on four receive-path handlers (merges REQ-INP-09) high Gap S · 0.5d code NIST PR.PS-06/PR.DS-02; ISO A.8.28/A.8.26 message.c:1749-1750; presence.c:397-402,453-454,468-470 Add if(!from_jid) return; at each site (copy sibling _subscribe_handler pattern); add ASan regression test with absent/a@@b from
REQ-AUTH-01 OTR keys.txt/fingerprints.txt and SQLite history DB left world/group-readable (no chmod 0600) high Partial S · ~0.5d (4h) code NIST PR.DS-01/PR.AA-01/PR.AA-05; ISO A.8.3/A.8.10/A.5.15/A.5.17 database_sqlite.c:144; otr.c:113,372,386 g_chmod(file, S_IRUSR|S_IWUSR) after sqlite3_open, otrl_privkey_generate and both write_fingerprints; manual perms check
REQ-DAR-03 Decrypted plaintext bodies (and SASL/registration creds) written to profanity.log under default dblog=on (merges REQ-LOG-03/CIS-8.5) high Gap S · 0.5–1d mixed NIST PR.PS-04/PR.DS-01; ISO A.8.10/A.8.12/A.8.15; CIS 8.5 database_sqlite.c:694,717,748,760; connection.c:1088-1107 Drop message->plain from log args (:694/717/760), log row id not INSERT (:748); add redaction helper in _xmpp_file_logger for SASL/jabber:iq:register; add CI grep (redaction helper shared with REQ-LOG-06)
REQ-SUP-03 gpgme unversioned, libgcrypt no floor, autotools/meson version floors diverge high Partial S · 0.5d mixed NIST ID.RA-09/GV.SC-04/PR.PS-02; ISO A.5.21/A.8.8/A.8.28; CIS 2.1/2.2/16.4 configure.ac:309,351; meson.build:96 (sqlite 3.22.0 vs 3.35.0) Versioned PKG_CHECK_MODULES for gpgme + gcrypt floor; align sqlite3 floor to one source; optional parity lint
REQ-VUL-01 SECURITY.md is 7 lines (contact+PGP only) — no supported versions/scope/SLA high Partial S · 0.5–1d process-docs NIST RS.MA-01/ID.IM-01/GV.RR-02/GV.OC-02; ISO A.5.5/A.5.26/A.8.8/A.5.2; CIS 7.1/16.2 SECURITY.md:1-7 Add Supported Versions, in/out-of-scope, ack+remediation SLA, coordinated-disclosure terms, owners (shares changelog step with REQ-VUL-04)
REQ-SDLC-01 No secret-scanning step in CI high Partial S · 3–5h build-ci NIST PR.AA-01/GV.OC-03; ISO A.8.4/A.5.8; CIS 16.12 no gitleaks/detect-secrets in .github/workflows Add a gitleaks-action job (PR diff + full-history with fetch-depth:0) + a .gitleaks.toml allowlist for test-fixture false positives
REQ-MEM-02 ASan/UBSan sanitizer build never exercised in CI (merges REQ-CIS-04-san) high Partial M · 1–2d build-ci NIST PR.PS-06/DE.CM-01/ID.RA-01; ISO A.8.28/A.8.29/A.8.25; CIS 16.12 configure.ac:74-75,441-452; ci-build.sh:193-205 Add non-continue-on-error job ./configure --enable-sanitizers && make check + suppressions; triage first-green findings (shared job with REQ-MEM-08)
REQ-SUP-02 CI base images use mutable tags; bootstrapped deps cloned at HEAD unverified high Gap M · 1–2d build-ci NIST GV.SC-05/06/07; ISO A.5.20/A.5.21/A.5.23/A.8.30; CIS 7.4/16.5 Dockerfile.debian:2,44-45; Dockerfile.arch:1,61-65 Digest-pin both FROMs; commit-pin the three git clone --depth 1; sha256/gpg-verify the AUR libstrophe-git snapshot
REQ-SUP-04 No dependency-CVE scanning in CI (no OSV/Dependabot/Trivy) high Gap M · 1–2d build-ci NIST ID.RA-01/GV.SC-08/ID.IM-02; ISO A.5.7/A.8.8; CIS 7.4/16.5 ci-code.yml (no CVE scan); no .github/dependabot.yml Add OSV-Scanner/Trivy (over a curated component list or the built image) + Dependabot on push+cron with a severity gate
REQ-CRY-03 tls.policy=allow gives silent opportunistic STARTTLS; in-band registration sends creds cleartext without TLS (merges REQ-CIS-08-cry) high Partial M · 1–2d code NIST PR.DS-02/PR.PS-01; ISO A.8.20/A.8.9/A.8.24; CIS 15.4 common.c:406; connection.c:185-194,397-431 Map allow→MANDATORY_TLS floor (or loud WARN); enforce TLS-required-or-fail before in-band registration sends credentials; integration test vs no-STARTTLS server (shared with REQ-CIS-08)
REQ-MEM-04 No overflow-checked allocation; unchecked size*nmemb/realloc and NULL-unchecked mallocs high Gap S · 0.5–1d code NIST PR.PS-01; ISO A.8.28 omemo.c:1440,455/475/492; http_upload.c:101-103 Use __builtin_mul_overflow/g_malloc_n, NULL-check, save realloc to temp before overwrite; optional UBSan overflow test
REQ-MEM-01 Incomplete hardening: autotools lacks PIE/noexecstack, Meson release path ships zero hardening high Partial L · 3–4d build-ci NIST PR.PS-01/PR.PS-06; ISO A.8.25/A.8.27/A.8.28; CIS 16.7 configure.ac (no -fPIE/-pie); meson.build:33,64-71,536 Add probed -fPIE -pie -Wl,-z,noexecstack to autotools; move Meson hardening out of is_debug + b_pie/relro/now; add checksec/readelf CI gate on binary + .so across both build systems
REQ-EXT-04 Plugin install accepts http, disables TLS verify under tls_policy=trust, dlopens with no signature check high Partial L · 3–4d code NIST GV.SC/PR.AA-05/PR.PS-01; ISO A.5.23/A.8.25/A.8.19; CIS 2.6/16.5 cmd_funcs.c:7019; http_download.c:120-156; plugins.c:198-218; common.c:234-243 Require https + force VERIFY*=1; verify signature/checksum before dlopen; chmod 0600 installed files; document full-privilege trust model (shares https helper with REQ-EXT-01, chmod with REQ-AUTH-01)
REQ-CIS-08 No enforced TLS/integrity baseline toward external providers + no provider-classification doc high Gap M · 1–2d mixed CIS 15.1/15.4; NIST PR.DS-02/GV.SC-04/05; ISO A.5.14/A.5.19/A.8.24/A.8.30 ai_client.c; connection.c:185-194; libstrophe.wrap; no PROVIDERS doc Reject non-https AI providers + explicit VERIFY*; allow-policy TLS floor; SHA-pin wrap; add provider-classification doc (bundles REQ-EXT-01 + REQ-CRY-03; wrap-pin shared with REQ-CIS-03/CIS-01)
REQ-CIS-02 No documented vuln accept/severity-rate/remediate process (SLA, RCA, release gate) (merges CIS-7.2/16.6) high Gap XL · 1w+ process-docs CIS 7.1/7.2/16.2/16.6/18.3; NIST RS.MA-01/ID.IM-01/02; ISO A.5.5/A.5.26/A.8.8 SECURITY.md contact-only; RELEASE_GUIDE.md no security step Author intake/triage, CVSS-aligned severity, remediation SLA, per-fix RCA+regression test, release gate blocking tags on open criticals
REQ-CIS-03 Build-time and runtime library sources not pinned to an integrity-verified identity high Gap L · 3–5d mixed CIS 2.6/16.5; NIST GV.SC-05/06/ID.RA-09; ISO A.5.23/A.8.30 libstrophe.wrap:3; Dockerfiles unpinned; plugins dlopen unverified SHA/tag+hash-pin wrap + CI lint; digest-pin images + commit-pin clones; checksum/signature-verify plugin before dlopen + trust model (shared with REQ-CIS-04/CIS-08, REQ-EXT-04)
REQ-CIS-04 Code-level security checks (SAST/fuzz/sanitizers/CVE/secret scan) not blocking CI (merges REQ-MEM-02-ci/CIS-16.12) high Gap XL · 1w+ build-ci CIS 16.12/16.13/18.1; NIST PR.PS-06/ID.RA-01/DE.CM-01; ISO A.8.28/A.8.29 ci-code.yml continue-on-error; no sanitizer/fuzz job Make scan-build, --enable-sanitizers && make check, scheduled fuzz, OSV/Dependabot, gitleaks all blocking gates (bundles REQ-MEM-02, REQ-SDLC-04, REQ-SUP-04, REQ-SDLC-01, REQ-SDLC-05)
REQ-CRY-01 MUC OMEMO caller logs/echoes plaintext to history DB on NULL id (send failure) medium Partial XS · ~1h code NIST PR.DS-02/PR.IR-01; ISO A.8.24/A.8.26 event/client_events.c:200-203 Wrap the three MUC-path log/echo lines in if (id != NULL) like the 1:1 path (:138,148,157)
REQ-AUTH-05 eval_password spawned with G_SPAWN_SEARCH_PATH (PATH hijack) medium Partial XS · ~1–2h code NIST PR.AA-01/PR.AA-05; ISO A.8.2/A.5.17 account.c:149 Drop G_SPAWN_SEARCH_PATH / require absolute argv[0]; warn on relative; doc note on back-compat
REQ-MEM-05 identity_public_key_len-- lacks a >0 guard (unsigned underflow) medium Partial XS · ~1h code NIST PR.PS-01; ISO A.8.28 omemo.c:1437 Reject/early-return when signal_buffer_len==0 before the decrement
REQ-LOG-01 SASL auth failure at INFO; TLS cert-fail logs nothing (below WARN filter) (merges CIS-8.2) medium Partial XS · ~1h code NIST DE.CM-06/DE.AE-02; ISO A.8.15/A.8.16; CIS 8.2 server_events.c:207-208,1135-1200 Promote sv_ev_failed_login to log_warning; add log_warning in sv_ev_certfail before the prompt (theme-shared with REQ-LOG-02)
REQ-LOG-02 TLS-disabled/handshake-fail/see-other-host/insecure-login are DEBUG-only medium Partial XS · ~1–2h code NIST DE.AE-02/DE.CM-06; ISO A.8.16/A.8.15 connection.c:191,367,923-928 Emit log_warning on all four downgrade conditions (theme-shared with REQ-LOG-01)
REQ-VUL-04 No per-release Security/CVE changelog convention; RELEASE_GUIDE has no security step medium Partial XS · ~1–2h process-docs NIST RC.RP-05/ID.IM-04; ISO A.8.8/A.5.26; CIS 7.2/16.3 RELEASE_GUIDE.md (no security step) Add a pre-tag CHANGELOG Security/CVE review step + a one-line changelog convention (shared with REQ-VUL-01)
REQ-SDLC-04 Style/auto-type/CWE-134/spell CI checks marked continue-on-error medium Partial XS · <1h build-ci NIST PR.PS-01/PR.PS-06; ISO A.8.25/A.8.28; CIS 16.9/16.12 ci-code.yml:40,94 Remove continue-on-error: true at both sites so checks block merges (fix any pre-existing violations surfaced)
REQ-MEM-09 alloca sized from peer barejid length; no -Wvla medium Gap S · ~2h mixed NIST PR.PS-01; ISO A.8.28/A.8.27 ox.c:150-151 Replace alloca with g_strdup_printf/heap+auto_* (same rewrite as REQ-MEM-03); add -Werror=vla to both build systems
REQ-MEM-03 Forbidden strcpy/strcat/sprintf on peer barejids and key/nonce medium Gap S · 0.5d mixed NIST PR.PS-01; ISO A.8.28 ox.c:153-156; omemo/crypto.c:479,483 g_strdup_printf on barejid, snprintf with remaining-size on key/nonce; add CI grep on strcpy/strcat/sprintf in stanza/crypto files (ox.c rewrite shared with REQ-MEM-09)
REQ-CRY-02 OTR-opportunistic first message bypasses allow_unencrypted_message gate medium Partial S · 0.5d code NIST PR.DS-02/PR.IR-01; ISO A.8.24/A.5.10 otr.c:332-339,425-428 Consult allow_unencrypted_message before the tagged cleartext send; return without sending if disallowed; add block-mode test
REQ-CRY-07 Non-CSPRNG g_rand_* used for stanza IDs and profanity_instance_id HMAC key medium Partial S · 0.5d code NIST PR.DS-02; ISO A.8.24 common.c:712-717; connection.c:808,1123 Rewrite get_random_string to draw from gcry_create_nonce (already in-tree at omemo.c:2076); handle modulo bias; one helper covers all callers
REQ-AUTH-06 Account/eval/PGP passphrases freed without non-elidable wipe medium Partial S · ~0.5d (4h) code NIST PR.DS-01/PR.AA-01/PR.PS-01; ISO A.5.17/A.8.3/A.8.24 account.c:184-185; session.c:581,591; gpg.c:79,104,108 Add a portable explicit_bzero helper (+ configure probe); wipe over length before free at ~6 sites
REQ-INP-02 JID validation lacks prescribed boundary/forbidden/@@/UTF-8 test table medium Partial S · 0.5d code NIST PR.PS-06/PR.DS-02; ISO A.8.28/A.8.26 tests/unittests/xmpp/test_jid.c Add table-driven suite: 1023/1024 per-part, 3071/3072 total, RFC6122 forbidden chars, double-@, empty parts, invalid UTF-8 (test-only)
REQ-INP-05 MAM <result> id not disco-gated (XEP-0359 trust) medium Partial S · 0.5d code NIST PR.DS-02/PR.AA-03; ISO A.8.26/A.8.28 message.c:1536-1571 (stored :1409-1410) Call existing _stanza_id_by_trusted() on the MAM result path; skip strdup when untrusted; toggle-feature unit test
REQ-INP-06 /url open|save accept any scheme medium Partial S · ~2h code NIST PR.PS-06/PR.PS-05/PR.DS-02; ISO A.8.28/A.8.26; CIS 9.3 cmd_funcs.c:9633-9655,9678-9698 Whitelist {http,https,aesgcm} via g_uri_parse_scheme after the NULL check; reject file:/javascript:/data: with a unit test
REQ-INP-07 OSX notifier builds shell string and runs via system() medium Partial S · 0.5d code NIST PR.PS-05; ISO A.8.26/A.8.28 ui/notifier.c:160 Use g_spawn argv API (or g_shell_quote every interpolated value); quote/$()/backtick injection test
REQ-MEM-08 -Wnull-dereference only under non-CI --enable-hardening medium Partial S · 0.5d build-ci NIST PR.PS-01/DE.CM-01; ISO A.8.28 configure.ac:454-464 (gated behind --enable-hardening) Enable -Wnull-dereference in one CI config; triage -O2 false positives (marginal if folded into REQ-MEM-02 job)
REQ-LOG-06 stderr→log bridge applies no secret redaction medium Partial S · ~2–4h code NIST PR.PS-04/DE.CM-06; ISO A.8.15/A.8.16 log.c:289,297 Run the same secret-marker redaction over s->str before log_msg at both sites (reuses REQ-DAR-03 redaction helper)
REQ-CFG-01 tls.policy=trust silently disables curl verify on file transfers with no per-transfer warning medium Partial S · 0.5d code NIST PR.AA-03/PR.DS-02/PR.DS-10; ISO A.8.9/A.8.24/A.5.14; CIS 9.6 http_upload.c:242-244; http_download.c:153-155 One shared message helper emitting a per-transfer insecure warning at both sites (aesgcm_download inherits)
REQ-VUL-02 release_get_latest never sets VERIFY*/https-only on update check medium Partial S · ~2–3h code NIST ID.RA-01/PR.DS-02; ISO A.8.8/A.8.24 common.c:459-482 Set VERIFYPEER=1L/VERIFYHOST=2L, constrain PROTOCOLS/redirect to https, tighten version regex; malformed-version unit test
REQ-VUL-03 PREF_REVEAL_OS defaults TRUE; no <version>-omit pref (peer fingerprinting) medium Partial S · 0.5d code NIST PR.AA-05/PR.DS-02; ISO A.8.8/A.8.24; CIS 4.1/16.7 preferences.c:2692,2706 Default PREF_REVEAL_OS OFF; add an include_version pref gating the <version> child (iq.c:1687-1728); stabber test
REQ-EXT-03 AI curl handle has no CURLOPT_CONNECTTIMEOUT; no cap/stall test medium Partial S · ~0.5d (4h) code NIST PR.IR-03/ID.RA-09; ISO A.8.6/A.8.16 ai_client.c:815,1601 Add CURLOPT_CONNECTTIMEOUT at both sites; add stall/connect-timeout + over-cap tests vs mock (shares curl sites with REQ-EXT-01)
REQ-RES-02 PRAGMA integrity_check only on /history verify, never at DB open medium Partial S · 0.5d code NIST RC.RP-01/PR.IR-03; ISO A.8.27/A.8.13 database_sqlite.c _sqlite_init (128-157) Run PRAGMA quick_check after sqlite3_open:144 and degrade gracefully on non-'ok'; corrupted-DB test
REQ-SDLC-03 No CODEOWNERS; commitlint config dormant/unreferenced medium Partial S · 0.5d build-ci NIST GV.RR-02/PR.PS-06/ID.IM-03; ISO A.5.8/A.8.25/A.8.32 .commitlintrc.json unreferenced; no .github/CODEOWNERS Add CODEOWNERS for crypto/TLS/plugin/AI dirs; add a Node-bootstrap commitlint CI job (no package.json today)
REQ-SUP-06 Plugin/AI-provider dependency trust boundary undocumented medium Partial S · 0.5d process-docs NIST GV.SC-04/ID.RA-09; ISO A.5.19/A.8.30; CIS 15.1 no plugin/AI supply-chain trust-boundary doc Document dlopen'd full-privilege plugins + user-configured TLS-validated AI providers (overlaps REQ-CIS-08 PROVIDERS doc)
REQ-CIS-07 Production/non-production data separation not stated as a normative control low Partial S · ~0.5d process-docs CIS 16.8; NIST PR.IR-01/ID.AM-08; ISO A.8.31/A.8.29 stabber harness satisfies it; no normative home State the no-real-data/isolated-XDG/localhost-only rule; add a CI grep on absolute $HOME/non-localhost hosts in tests
REQ-CRY-06 OMEMO media plaintext persisted to user path before GCM tag verified medium Partial M · 1–1.5d code NIST PR.DS-08/PR.DS-02; ISO A.8.24 omemo/crypto.c:445 (checktag :462); aesgcm_download.c:66,112-140 Decrypt to temp, verify tag, atomic rename on success; on failure remove() output and skip cmd_template branch; tampered-tag test
REQ-CRY-09 blind/firstusage OMEMO trust transitions driven from inbound handlers medium Partial M · 1–1.5d code NIST PR.AA-03/PR.AA-04; ISO A.5.31/A.5.17 omemo.c:608,698 Product decision then gate/surface the persisted-trust write so manual never auto-trusts from inbound while blind/firstusage still emit a visible notice; tests
REQ-SDLC-06 No invariant→test REQ-ID traceability matrix medium Partial M · 1–1.5d process-docs NIST GV.PO-01/PR.DS-02/GV.SC-04; ISO A.5.8/A.8.25; CIS 16.10/16.14 no compliance/REQ-TO-TEST.md Author compliance/REQ-TO-TEST.md mapping each REQ to concrete tests and flagging uncovered REQs (cross-referencing labor)
REQ-CIS-05 No documented application logging policy (events/levels/no-secrets/retention) (merges CIS-8.1) medium Gap M · 1–2d process-docs CIS 8.1/8.5; NIST PR.PS-04/DE.AE-02; ISO A.8.15/A.8.16 no logging policy in compliance/ or docs/ Add a logging-policy doc (WARN+ events, no-secrets constraint, default level, rotation, 0600); add CI grep on log_* interpolation of secrets
REQ-CIS-06 No documented secure-configuration/hardening baseline medium Gap M · 1–2d process-docs CIS 4.1/16.7; NIST PR.PS-01/GV.PO-01; ISO A.8.9/A.8.27 docs/ holds only man pages Add docs/SECURE-CONFIGURATION.md: shipped defaults + rationale, minimal-surface build, hardening-template pointer, review cadence
REQ-CIS-01 No machine-readable SBOM / third-party software inventory (merges CIS-2.1/16.4) medium Gap L · 3–4d mixed CIS 2.1/16.4/15.1; NIST ID.AM-08/GV.SC-04; ISO A.5.21/A.8.8 no .spdx/.cdx/bom.* in tree Author component inventory; script CycloneDX/SPDX generation from PKG_CHECK_MODULES floors; reconcile divergent autotools/meson floors; CI diff-check (overlaps REQ-SUP-03/CIS-08)
REQ-INP-08 No control-char/bidi neutralization or length cap on display/log/DB sinks medium Gap L · 3–4d code NIST PR.DS-01/PR.DS-02/PR.PS-06; ISO A.8.28/A.8.26 ui/window.c:2024,2120; str_xml_sanitize skips U+202E Add one control-char/bidi sanitizer + length cap threaded through display, log and DB-write boundaries; ESC/NUL/U+202E unit tests
REQ-SDLC-05 No fuzz harness over untrusted-input parsers medium Gap XL · 1–2w mixed NIST PR.PS-06/ID.RA-01; ISO A.8.29/A.8.25; CIS 16.13/18.1 no fuzz/oss-fuzz/libfuzzer targets or corpus Stand up libFuzzer/AFL++ harnesses over stanza/XML, jid_create, OMEMO/PGP/OX decode (decouple from session/ctx globals); build plumbing, seed corpora, scheduled CI

Notes & assumptions

  • Estimates are indicative, sized for a small OSS C project against the actual master tree at 622054fc6. They cover implementation + a first test, but exclude code-review rounds and CI-iteration overhead (each Dockerfile/build-system change needs a rebuild to confirm; first sanitizer/fuzz green runs can surface latent issues that expand scope).
  • Shared setup is counted once. Where shares_fix_with applies, the marginal cost is booked to the primary item and the dependent row notes "(shared with …)". Key shared work: the two AI curl setopt sites (REQ-EXT-01 / REQ-EXT-03 / REQ-CIS-08), the secret-redaction helper (REQ-DAR-03 / REQ-LOG-06), the wrap/ref-pinning CI lint (REQ-SUP-01 / REQ-SUP-05 / REQ-CIS-03), the ox.c g_strdup_printf rewrite (REQ-MEM-03 / REQ-MEM-09), the sanitizer/hardened CI job (REQ-MEM-02 / REQ-MEM-08 / REQ-CIS-04), and the plugin signature/trust design (REQ-EXT-04 / REQ-CIS-03). The rollup band already nets these out at its low end.
  • Highest-value quick fixes are the three confirmed code bugs: the remote NULL-deref DoS (REQ-INP-01), the AI http:// cleartext key/conversation egress (REQ-EXT-01), and decrypted plaintext + SASL/registration creds written to profanity.log under the default dblog=on (REQ-DAR-03). Each is small, high/critical, and directly exploitable or privacy-breaking — prioritize these ahead of the L/XL program items.
  • The three XL items (REQ-CIS-02 vuln-management process, REQ-CIS-04 blocking-CI security build-out, REQ-SDLC-05 fuzzing program) are program deliverables whose scope and duration carry the widest variance; REQ-CIS-04 substantially bundles several M/S CI items (REQ-MEM-02, REQ-SDLC-04, REQ-SUP-04, REQ-SDLC-01, REQ-SDLC-05), so those rows and the CIS-04 bundle should not be summed independently when planning.
# cproof — Consolidated Compliance Findings Summary This is a single, deduped view of every **actionable** Gap/Partial finding from the combined NIST CSF 2.0 + ISO/IEC 27001 + CIS Controls v8.1 audit of cproof at `master` (`622054fc6`). It merges the source audit (`compliance/SOURCE-COMPLIANCE-ANALYSIS.md`) with the requirements catalog (`compliance/REQUIREMENTS-nist-iso-cis.md`) and adds a code-aware effort estimate per item. Where one requirement was raised under multiple frameworks (or as a CIS→REQ duplicate), the rows are **merged into one line** and the merged IDs are noted; where two findings are fixed by the same change, the shared work is flagged in the Fix cell and counted once in the rollup. **Severity:** `critical` (E2EE plaintext leak / credential egress / RCE-class), `high`, `medium`, `low`, `nit`. **Status:** `Gap` (control absent) or `Partial` (control exists but incomplete/misconfigured). **Effort scale:** `XS` = trivial, < 2h (one-liner, a flag, a CI lint line). `S` = a few hours / ~0.5 day (localized change + a test). `M` = 1–2 days (multi-site change + tests, or one CI job). `L` = 3–5 days (subsystem change, new CI pipeline, hardening across both build systems). `XL` = 1 week+ (a process/program deliverable). Effort type: `code` | `build-ci` | `process-docs` | `mixed`. ## Rollup **By severity:** critical 1 · high 19 · medium 33 · low 2 · nit 0 — **55 findings**. **By effort size:** XS 9 · S 28 · M 10 · L 5 · XL 3. **Indicative total effort:** roughly **45–70 engineer-days** gross if every item ships; netting out shared-fix work (curl setopt sites, the redaction helper, the wrap/ref-pinning lint, the sanitizer/hardened CI job, the plugin-signature/trust design) and the REQ-CIS-04 CI bundle (which absorbs REQ-MEM-02 / REQ-SDLC-04 / REQ-SUP-04 / REQ-SDLC-01 / REQ-SDLC-05) trims this toward **~40–55 net**. Treat this as an order-of-magnitude band for a small OSS C project, not a committed estimate — the three XL process/program items (fuzzing program, vuln-management process, blocking-CI build-out) plus the five L items dominate and carry the widest variance. **Quick wins (high/critical severity AND XS/S effort):** - REQ-EXT-01 — reject AI `http://` provider URLs + explicit VERIFY* (S · ~4h) — confirmed cleartext key/conversation egress. - REQ-INP-01 — guard 4 unguarded `jid_create(from)` derefs (S · 0.5d) — confirmed remote NULL-deref DoS. - REQ-DAR-03 — drop plaintext bodies/creds from `profanity.log` (S · 0.5–1d) — confirmed plaintext-in-logs. - REQ-SUP-01 — pin libstrophe wrap off `revision = HEAD` + CI lint (S · ~2–4h). - REQ-AUTH-01 — chmod 0600 OTR keystores + history DB (S · ~4h). - REQ-SUP-03 — versioned gpgme/gcrypt floors, align sqlite3 floor (S · 0.5d). - REQ-SUP-05 — pin GitHub Actions to commit SHAs (XS · ~1h). - REQ-VUL-01 — flesh out 7-line SECURITY.md (S · 0.5–1d). - REQ-SDLC-01 — add gitleaks/detect-secrets CI job (S · 3–5h). **Big rocks (XL / program-level process items):** - REQ-CIS-02 — documented vuln accept/severity/remediate process (XL · 1w+). - REQ-CIS-04 — make SAST/sanitizers/fuzz/CVE/secret scans all blocking CI gates (XL · 1w+). - REQ-SDLC-05 — stand up a libFuzzer/AFL++ fuzzing program over untrusted parsers (XL · 1–2w). - (L, adjacent) REQ-EXT-04 plugin signature/trust model; REQ-MEM-01 toolchain hardening across both build systems; REQ-INP-08 control-char/bidi sanitizer; REQ-CIS-01 SBOM generation + CI reconciliation; REQ-CIS-03 integrity-pinned sources. ## Findings | ID | Finding | Sev | Status | Effort | Type | Frameworks | Evidence | Fix | |----|---------|-----|--------|--------|------|------------|----------|-----| | REQ-EXT-01 | AI/LLM client accepts `http://` provider URLs — cleartext API key + conversation egress (merges REQ-CIS-08) | critical | Gap | S · ~0.5d (4h) | code | NIST PR.DS-02/PR.PS-01; ISO A.5.14/A.8.24/A.8.27; CIS 15.4 | cmd_funcs.c:7019; ai_client.c:388,811-815,1596-1601 | Reject non-https at provider-add (cmd_ai_set) and in ai_add_provider; set CURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2L at both curl sites; flip test_ai.c:85-95 to assert reject (shares curl sites with REQ-EXT-03) | | REQ-AUTH-03 | Insecure transport flags logged at debug, not WARN | high | Partial | XS · ~1–2h | code | NIST PR.AA-03/PR.DS-02; ISO A.5.17/A.8.5/A.8.9/A.8.20; CIS 8.2 | connection.c:201-213 | Split DISABLE_TLS/LEGACY_*/TRUST_TLS to log_warning + cons_show so the downgrade is visible | | REQ-SUP-05 | GitHub Actions pinned to mutable tags, not commit SHAs | low | Gap | XS · ~1h | build-ci | NIST GV.SC-06/PR.PS-01; ISO A.8.30/A.8.28; CIS 7.4 | .github/workflows/*.yml `actions/checkout@v4` | Pin all `@v4` (6 sites) to 40-hex SHAs; optional grep lint on `uses:.*@v[0-9]` (shared with REQ-SUP-01 lint) | | REQ-SUP-01 | libstrophe Meson wrap pinned to `revision = HEAD` (mutable XMPP parser) (merges CIS-2.6/16.5) | high | Gap | S · ~2–4h | mixed | NIST GV.SC-05/ID.RA-09/PR.PS-01; ISO A.5.21/A.5.23/A.8.8/A.8.30; CIS 2.2/2.6/7.2/16.5 | subprojects/libstrophe.wrap:3 | Replace `revision = HEAD` with vetted 40-hex SHA (or tag+source_hash); add CI grep failing on `revision = HEAD`/bare-branch wraps (shared lint with REQ-SUP-05) | | REQ-INP-01 | Remote NULL-deref DoS: unguarded `jid_create(from)` on four receive-path handlers (merges REQ-INP-09) | high | Gap | S · 0.5d | code | NIST PR.PS-06/PR.DS-02; ISO A.8.28/A.8.26 | message.c:1749-1750; presence.c:397-402,453-454,468-470 | Add `if(!from_jid) return;` at each site (copy sibling _subscribe_handler pattern); add ASan regression test with absent/`a@@b` from | | REQ-AUTH-01 | OTR keys.txt/fingerprints.txt and SQLite history DB left world/group-readable (no chmod 0600) | high | Partial | S · ~0.5d (4h) | code | NIST PR.DS-01/PR.AA-01/PR.AA-05; ISO A.8.3/A.8.10/A.5.15/A.5.17 | database_sqlite.c:144; otr.c:113,372,386 | `g_chmod(file, S_IRUSR\|S_IWUSR)` after sqlite3_open, otrl_privkey_generate and both write_fingerprints; manual perms check | | REQ-DAR-03 | Decrypted plaintext bodies (and SASL/registration creds) written to profanity.log under default dblog=on (merges REQ-LOG-03/CIS-8.5) | high | Gap | S · 0.5–1d | mixed | NIST PR.PS-04/PR.DS-01; ISO A.8.10/A.8.12/A.8.15; CIS 8.5 | database_sqlite.c:694,717,748,760; connection.c:1088-1107 | Drop message->plain from log args (:694/717/760), log row id not INSERT (:748); add redaction helper in _xmpp_file_logger for SASL/`jabber:iq:register`; add CI grep (redaction helper shared with REQ-LOG-06) | | REQ-SUP-03 | gpgme unversioned, libgcrypt no floor, autotools/meson version floors diverge | high | Partial | S · 0.5d | mixed | NIST ID.RA-09/GV.SC-04/PR.PS-02; ISO A.5.21/A.8.8/A.8.28; CIS 2.1/2.2/16.4 | configure.ac:309,351; meson.build:96 (sqlite 3.22.0 vs 3.35.0) | Versioned PKG_CHECK_MODULES for gpgme + gcrypt floor; align sqlite3 floor to one source; optional parity lint | | REQ-VUL-01 | SECURITY.md is 7 lines (contact+PGP only) — no supported versions/scope/SLA | high | Partial | S · 0.5–1d | process-docs | NIST RS.MA-01/ID.IM-01/GV.RR-02/GV.OC-02; ISO A.5.5/A.5.26/A.8.8/A.5.2; CIS 7.1/16.2 | SECURITY.md:1-7 | Add Supported Versions, in/out-of-scope, ack+remediation SLA, coordinated-disclosure terms, owners (shares changelog step with REQ-VUL-04) | | REQ-SDLC-01 | No secret-scanning step in CI | high | Partial | S · 3–5h | build-ci | NIST PR.AA-01/GV.OC-03; ISO A.8.4/A.5.8; CIS 16.12 | no gitleaks/detect-secrets in .github/workflows | Add a gitleaks-action job (PR diff + full-history with fetch-depth:0) + a `.gitleaks.toml` allowlist for test-fixture false positives | | REQ-MEM-02 | ASan/UBSan sanitizer build never exercised in CI (merges REQ-CIS-04-san) | high | Partial | M · 1–2d | build-ci | NIST PR.PS-06/DE.CM-01/ID.RA-01; ISO A.8.28/A.8.29/A.8.25; CIS 16.12 | configure.ac:74-75,441-452; ci-build.sh:193-205 | Add non-`continue-on-error` job `./configure --enable-sanitizers && make check` + suppressions; triage first-green findings (shared job with REQ-MEM-08) | | REQ-SUP-02 | CI base images use mutable tags; bootstrapped deps cloned at HEAD unverified | high | Gap | M · 1–2d | build-ci | NIST GV.SC-05/06/07; ISO A.5.20/A.5.21/A.5.23/A.8.30; CIS 7.4/16.5 | Dockerfile.debian:2,44-45; Dockerfile.arch:1,61-65 | Digest-pin both FROMs; commit-pin the three `git clone --depth 1`; sha256/gpg-verify the AUR libstrophe-git snapshot | | REQ-SUP-04 | No dependency-CVE scanning in CI (no OSV/Dependabot/Trivy) | high | Gap | M · 1–2d | build-ci | NIST ID.RA-01/GV.SC-08/ID.IM-02; ISO A.5.7/A.8.8; CIS 7.4/16.5 | ci-code.yml (no CVE scan); no .github/dependabot.yml | Add OSV-Scanner/Trivy (over a curated component list or the built image) + Dependabot on push+cron with a severity gate | | REQ-CRY-03 | tls.policy=allow gives silent opportunistic STARTTLS; in-band registration sends creds cleartext without TLS (merges REQ-CIS-08-cry) | high | Partial | M · 1–2d | code | NIST PR.DS-02/PR.PS-01; ISO A.8.20/A.8.9/A.8.24; CIS 15.4 | common.c:406; connection.c:185-194,397-431 | Map `allow`→MANDATORY_TLS floor (or loud WARN); enforce TLS-required-or-fail before in-band registration sends credentials; integration test vs no-STARTTLS server (shared with REQ-CIS-08) | | REQ-MEM-04 | No overflow-checked allocation; unchecked size*nmemb/realloc and NULL-unchecked mallocs | high | Gap | S · 0.5–1d | code | NIST PR.PS-01; ISO A.8.28 | omemo.c:1440,455/475/492; http_upload.c:101-103 | Use __builtin_mul_overflow/g_malloc_n, NULL-check, save realloc to temp before overwrite; optional UBSan overflow test | | REQ-MEM-01 | Incomplete hardening: autotools lacks PIE/noexecstack, Meson release path ships zero hardening | high | Partial | L · 3–4d | build-ci | NIST PR.PS-01/PR.PS-06; ISO A.8.25/A.8.27/A.8.28; CIS 16.7 | configure.ac (no -fPIE/-pie); meson.build:33,64-71,536 | Add probed -fPIE -pie -Wl,-z,noexecstack to autotools; move Meson hardening out of is_debug + b_pie/relro/now; add checksec/readelf CI gate on binary + .so across both build systems | | REQ-EXT-04 | Plugin install accepts http, disables TLS verify under tls_policy=trust, dlopens with no signature check | high | Partial | L · 3–4d | code | NIST GV.SC/PR.AA-05/PR.PS-01; ISO A.5.23/A.8.25/A.8.19; CIS 2.6/16.5 | cmd_funcs.c:7019; http_download.c:120-156; plugins.c:198-218; common.c:234-243 | Require https + force VERIFY*=1; verify signature/checksum before dlopen; chmod 0600 installed files; document full-privilege trust model (shares https helper with REQ-EXT-01, chmod with REQ-AUTH-01) | | REQ-CIS-08 | No enforced TLS/integrity baseline toward external providers + no provider-classification doc | high | Gap | M · 1–2d | mixed | CIS 15.1/15.4; NIST PR.DS-02/GV.SC-04/05; ISO A.5.14/A.5.19/A.8.24/A.8.30 | ai_client.c; connection.c:185-194; libstrophe.wrap; no PROVIDERS doc | Reject non-https AI providers + explicit VERIFY*; allow-policy TLS floor; SHA-pin wrap; add provider-classification doc (bundles REQ-EXT-01 + REQ-CRY-03; wrap-pin shared with REQ-CIS-03/CIS-01) | | REQ-CIS-02 | No documented vuln accept/severity-rate/remediate process (SLA, RCA, release gate) (merges CIS-7.2/16.6) | high | Gap | XL · 1w+ | process-docs | CIS 7.1/7.2/16.2/16.6/18.3; NIST RS.MA-01/ID.IM-01/02; ISO A.5.5/A.5.26/A.8.8 | SECURITY.md contact-only; RELEASE_GUIDE.md no security step | Author intake/triage, CVSS-aligned severity, remediation SLA, per-fix RCA+regression test, release gate blocking tags on open criticals | | REQ-CIS-03 | Build-time and runtime library sources not pinned to an integrity-verified identity | high | Gap | L · 3–5d | mixed | CIS 2.6/16.5; NIST GV.SC-05/06/ID.RA-09; ISO A.5.23/A.8.30 | libstrophe.wrap:3; Dockerfiles unpinned; plugins dlopen unverified | SHA/tag+hash-pin wrap + CI lint; digest-pin images + commit-pin clones; checksum/signature-verify plugin before dlopen + trust model (shared with REQ-CIS-04/CIS-08, REQ-EXT-04) | | REQ-CIS-04 | Code-level security checks (SAST/fuzz/sanitizers/CVE/secret scan) not blocking CI (merges REQ-MEM-02-ci/CIS-16.12) | high | Gap | XL · 1w+ | build-ci | CIS 16.12/16.13/18.1; NIST PR.PS-06/ID.RA-01/DE.CM-01; ISO A.8.28/A.8.29 | ci-code.yml continue-on-error; no sanitizer/fuzz job | Make scan-build, `--enable-sanitizers && make check`, scheduled fuzz, OSV/Dependabot, gitleaks all blocking gates (bundles REQ-MEM-02, REQ-SDLC-04, REQ-SUP-04, REQ-SDLC-01, REQ-SDLC-05) | | REQ-CRY-01 | MUC OMEMO caller logs/echoes plaintext to history DB on NULL id (send failure) | medium | Partial | XS · ~1h | code | NIST PR.DS-02/PR.IR-01; ISO A.8.24/A.8.26 | event/client_events.c:200-203 | Wrap the three MUC-path log/echo lines in `if (id != NULL)` like the 1:1 path (:138,148,157) | | REQ-AUTH-05 | eval_password spawned with G_SPAWN_SEARCH_PATH (PATH hijack) | medium | Partial | XS · ~1–2h | code | NIST PR.AA-01/PR.AA-05; ISO A.8.2/A.5.17 | account.c:149 | Drop G_SPAWN_SEARCH_PATH / require absolute argv[0]; warn on relative; doc note on back-compat | | REQ-MEM-05 | `identity_public_key_len--` lacks a `>0` guard (unsigned underflow) | medium | Partial | XS · ~1h | code | NIST PR.PS-01; ISO A.8.28 | omemo.c:1437 | Reject/early-return when `signal_buffer_len==0` before the decrement | | REQ-LOG-01 | SASL auth failure at INFO; TLS cert-fail logs nothing (below WARN filter) (merges CIS-8.2) | medium | Partial | XS · ~1h | code | NIST DE.CM-06/DE.AE-02; ISO A.8.15/A.8.16; CIS 8.2 | server_events.c:207-208,1135-1200 | Promote sv_ev_failed_login to log_warning; add log_warning in sv_ev_certfail before the prompt (theme-shared with REQ-LOG-02) | | REQ-LOG-02 | TLS-disabled/handshake-fail/see-other-host/insecure-login are DEBUG-only | medium | Partial | XS · ~1–2h | code | NIST DE.AE-02/DE.CM-06; ISO A.8.16/A.8.15 | connection.c:191,367,923-928 | Emit log_warning on all four downgrade conditions (theme-shared with REQ-LOG-01) | | REQ-VUL-04 | No per-release Security/CVE changelog convention; RELEASE_GUIDE has no security step | medium | Partial | XS · ~1–2h | process-docs | NIST RC.RP-05/ID.IM-04; ISO A.8.8/A.5.26; CIS 7.2/16.3 | RELEASE_GUIDE.md (no security step) | Add a pre-tag CHANGELOG Security/CVE review step + a one-line changelog convention (shared with REQ-VUL-01) | | REQ-SDLC-04 | Style/auto-type/CWE-134/spell CI checks marked continue-on-error | medium | Partial | XS · <1h | build-ci | NIST PR.PS-01/PR.PS-06; ISO A.8.25/A.8.28; CIS 16.9/16.12 | ci-code.yml:40,94 | Remove `continue-on-error: true` at both sites so checks block merges (fix any pre-existing violations surfaced) | | REQ-MEM-09 | alloca sized from peer barejid length; no -Wvla | medium | Gap | S · ~2h | mixed | NIST PR.PS-01; ISO A.8.28/A.8.27 | ox.c:150-151 | Replace alloca with g_strdup_printf/heap+auto_* (same rewrite as REQ-MEM-03); add -Werror=vla to both build systems | | REQ-MEM-03 | Forbidden strcpy/strcat/sprintf on peer barejids and key/nonce | medium | Gap | S · 0.5d | mixed | NIST PR.PS-01; ISO A.8.28 | ox.c:153-156; omemo/crypto.c:479,483 | g_strdup_printf on barejid, snprintf with remaining-size on key/nonce; add CI grep on strcpy/strcat/sprintf in stanza/crypto files (ox.c rewrite shared with REQ-MEM-09) | | REQ-CRY-02 | OTR-opportunistic first message bypasses allow_unencrypted_message gate | medium | Partial | S · 0.5d | code | NIST PR.DS-02/PR.IR-01; ISO A.8.24/A.5.10 | otr.c:332-339,425-428 | Consult allow_unencrypted_message before the tagged cleartext send; return without sending if disallowed; add block-mode test | | REQ-CRY-07 | Non-CSPRNG g_rand_* used for stanza IDs and profanity_instance_id HMAC key | medium | Partial | S · 0.5d | code | NIST PR.DS-02; ISO A.8.24 | common.c:712-717; connection.c:808,1123 | Rewrite get_random_string to draw from gcry_create_nonce (already in-tree at omemo.c:2076); handle modulo bias; one helper covers all callers | | REQ-AUTH-06 | Account/eval/PGP passphrases freed without non-elidable wipe | medium | Partial | S · ~0.5d (4h) | code | NIST PR.DS-01/PR.AA-01/PR.PS-01; ISO A.5.17/A.8.3/A.8.24 | account.c:184-185; session.c:581,591; gpg.c:79,104,108 | Add a portable explicit_bzero helper (+ configure probe); wipe over length before free at ~6 sites | | REQ-INP-02 | JID validation lacks prescribed boundary/forbidden/@@/UTF-8 test table | medium | Partial | S · 0.5d | code | NIST PR.PS-06/PR.DS-02; ISO A.8.28/A.8.26 | tests/unittests/xmpp/test_jid.c | Add table-driven suite: 1023/1024 per-part, 3071/3072 total, RFC6122 forbidden chars, double-@, empty parts, invalid UTF-8 (test-only) | | REQ-INP-05 | MAM `<result>` id not disco-gated (XEP-0359 trust) | medium | Partial | S · 0.5d | code | NIST PR.DS-02/PR.AA-03; ISO A.8.26/A.8.28 | message.c:1536-1571 (stored :1409-1410) | Call existing `_stanza_id_by_trusted()` on the MAM result path; skip strdup when untrusted; toggle-feature unit test | | REQ-INP-06 | `/url open\|save` accept any scheme | medium | Partial | S · ~2h | code | NIST PR.PS-06/PR.PS-05/PR.DS-02; ISO A.8.28/A.8.26; CIS 9.3 | cmd_funcs.c:9633-9655,9678-9698 | Whitelist {http,https,aesgcm} via g_uri_parse_scheme after the NULL check; reject file:/javascript:/data: with a unit test | | REQ-INP-07 | OSX notifier builds shell string and runs via system() | medium | Partial | S · 0.5d | code | NIST PR.PS-05; ISO A.8.26/A.8.28 | ui/notifier.c:160 | Use g_spawn argv API (or g_shell_quote every interpolated value); quote/`$()`/backtick injection test | | REQ-MEM-08 | -Wnull-dereference only under non-CI --enable-hardening | medium | Partial | S · 0.5d | build-ci | NIST PR.PS-01/DE.CM-01; ISO A.8.28 | configure.ac:454-464 (gated behind --enable-hardening) | Enable -Wnull-dereference in one CI config; triage -O2 false positives (marginal if folded into REQ-MEM-02 job) | | REQ-LOG-06 | stderr→log bridge applies no secret redaction | medium | Partial | S · ~2–4h | code | NIST PR.PS-04/DE.CM-06; ISO A.8.15/A.8.16 | log.c:289,297 | Run the same secret-marker redaction over s->str before log_msg at both sites (reuses REQ-DAR-03 redaction helper) | | REQ-CFG-01 | tls.policy=trust silently disables curl verify on file transfers with no per-transfer warning | medium | Partial | S · 0.5d | code | NIST PR.AA-03/PR.DS-02/PR.DS-10; ISO A.8.9/A.8.24/A.5.14; CIS 9.6 | http_upload.c:242-244; http_download.c:153-155 | One shared message helper emitting a per-transfer insecure warning at both sites (aesgcm_download inherits) | | REQ-VUL-02 | release_get_latest never sets VERIFY*/https-only on update check | medium | Partial | S · ~2–3h | code | NIST ID.RA-01/PR.DS-02; ISO A.8.8/A.8.24 | common.c:459-482 | Set VERIFYPEER=1L/VERIFYHOST=2L, constrain PROTOCOLS/redirect to https, tighten version regex; malformed-version unit test | | REQ-VUL-03 | PREF_REVEAL_OS defaults TRUE; no `<version>`-omit pref (peer fingerprinting) | medium | Partial | S · 0.5d | code | NIST PR.AA-05/PR.DS-02; ISO A.8.8/A.8.24; CIS 4.1/16.7 | preferences.c:2692,2706 | Default PREF_REVEAL_OS OFF; add an include_version pref gating the `<version>` child (iq.c:1687-1728); stabber test | | REQ-EXT-03 | AI curl handle has no CURLOPT_CONNECTTIMEOUT; no cap/stall test | medium | Partial | S · ~0.5d (4h) | code | NIST PR.IR-03/ID.RA-09; ISO A.8.6/A.8.16 | ai_client.c:815,1601 | Add CURLOPT_CONNECTTIMEOUT at both sites; add stall/connect-timeout + over-cap tests vs mock (shares curl sites with REQ-EXT-01) | | REQ-RES-02 | PRAGMA integrity_check only on /history verify, never at DB open | medium | Partial | S · 0.5d | code | NIST RC.RP-01/PR.IR-03; ISO A.8.27/A.8.13 | database_sqlite.c _sqlite_init (128-157) | Run `PRAGMA quick_check` after sqlite3_open:144 and degrade gracefully on non-'ok'; corrupted-DB test | | REQ-SDLC-03 | No CODEOWNERS; commitlint config dormant/unreferenced | medium | Partial | S · 0.5d | build-ci | NIST GV.RR-02/PR.PS-06/ID.IM-03; ISO A.5.8/A.8.25/A.8.32 | .commitlintrc.json unreferenced; no .github/CODEOWNERS | Add CODEOWNERS for crypto/TLS/plugin/AI dirs; add a Node-bootstrap commitlint CI job (no package.json today) | | REQ-SUP-06 | Plugin/AI-provider dependency trust boundary undocumented | medium | Partial | S · 0.5d | process-docs | NIST GV.SC-04/ID.RA-09; ISO A.5.19/A.8.30; CIS 15.1 | no plugin/AI supply-chain trust-boundary doc | Document dlopen'd full-privilege plugins + user-configured TLS-validated AI providers (overlaps REQ-CIS-08 PROVIDERS doc) | | REQ-CIS-07 | Production/non-production data separation not stated as a normative control | low | Partial | S · ~0.5d | process-docs | CIS 16.8; NIST PR.IR-01/ID.AM-08; ISO A.8.31/A.8.29 | stabber harness satisfies it; no normative home | State the no-real-data/isolated-XDG/localhost-only rule; add a CI grep on absolute $HOME/non-localhost hosts in tests | | REQ-CRY-06 | OMEMO media plaintext persisted to user path before GCM tag verified | medium | Partial | M · 1–1.5d | code | NIST PR.DS-08/PR.DS-02; ISO A.8.24 | omemo/crypto.c:445 (checktag :462); aesgcm_download.c:66,112-140 | Decrypt to temp, verify tag, atomic rename on success; on failure remove() output and skip cmd_template branch; tampered-tag test | | REQ-CRY-09 | blind/firstusage OMEMO trust transitions driven from inbound handlers | medium | Partial | M · 1–1.5d | code | NIST PR.AA-03/PR.AA-04; ISO A.5.31/A.5.17 | omemo.c:608,698 | Product decision then gate/surface the persisted-trust write so manual never auto-trusts from inbound while blind/firstusage still emit a visible notice; tests | | REQ-SDLC-06 | No invariant→test REQ-ID traceability matrix | medium | Partial | M · 1–1.5d | process-docs | NIST GV.PO-01/PR.DS-02/GV.SC-04; ISO A.5.8/A.8.25; CIS 16.10/16.14 | no compliance/REQ-TO-TEST.md | Author compliance/REQ-TO-TEST.md mapping each REQ to concrete tests and flagging uncovered REQs (cross-referencing labor) | | REQ-CIS-05 | No documented application logging policy (events/levels/no-secrets/retention) (merges CIS-8.1) | medium | Gap | M · 1–2d | process-docs | CIS 8.1/8.5; NIST PR.PS-04/DE.AE-02; ISO A.8.15/A.8.16 | no logging policy in compliance/ or docs/ | Add a logging-policy doc (WARN+ events, no-secrets constraint, default level, rotation, 0600); add CI grep on log_* interpolation of secrets | | REQ-CIS-06 | No documented secure-configuration/hardening baseline | medium | Gap | M · 1–2d | process-docs | CIS 4.1/16.7; NIST PR.PS-01/GV.PO-01; ISO A.8.9/A.8.27 | docs/ holds only man pages | Add docs/SECURE-CONFIGURATION.md: shipped defaults + rationale, minimal-surface build, hardening-template pointer, review cadence | | REQ-CIS-01 | No machine-readable SBOM / third-party software inventory (merges CIS-2.1/16.4) | medium | Gap | L · 3–4d | mixed | CIS 2.1/16.4/15.1; NIST ID.AM-08/GV.SC-04; ISO A.5.21/A.8.8 | no *.spdx/*.cdx/bom.* in tree | Author component inventory; script CycloneDX/SPDX generation from PKG_CHECK_MODULES floors; reconcile divergent autotools/meson floors; CI diff-check (overlaps REQ-SUP-03/CIS-08) | | REQ-INP-08 | No control-char/bidi neutralization or length cap on display/log/DB sinks | medium | Gap | L · 3–4d | code | NIST PR.DS-01/PR.DS-02/PR.PS-06; ISO A.8.28/A.8.26 | ui/window.c:2024,2120; str_xml_sanitize skips U+202E | Add one control-char/bidi sanitizer + length cap threaded through display, log and DB-write boundaries; ESC/NUL/U+202E unit tests | | REQ-SDLC-05 | No fuzz harness over untrusted-input parsers | medium | Gap | XL · 1–2w | mixed | NIST PR.PS-06/ID.RA-01; ISO A.8.29/A.8.25; CIS 16.13/18.1 | no fuzz/oss-fuzz/libfuzzer targets or corpus | Stand up libFuzzer/AFL++ harnesses over stanza/XML, jid_create, OMEMO/PGP/OX decode (decouple from session/ctx globals); build plumbing, seed corpora, scheduled CI | ## Notes & assumptions - **Estimates are indicative**, sized for a small OSS C project against the actual `master` tree at `622054fc6`. They cover implementation + a first test, but **exclude** code-review rounds and CI-iteration overhead (each Dockerfile/build-system change needs a rebuild to confirm; first sanitizer/fuzz green runs can surface latent issues that expand scope). - **Shared setup is counted once.** Where `shares_fix_with` applies, the marginal cost is booked to the primary item and the dependent row notes "(shared with …)". Key shared work: the two AI curl setopt sites (REQ-EXT-01 / REQ-EXT-03 / REQ-CIS-08), the secret-redaction helper (REQ-DAR-03 / REQ-LOG-06), the wrap/ref-pinning CI lint (REQ-SUP-01 / REQ-SUP-05 / REQ-CIS-03), the `ox.c` g_strdup_printf rewrite (REQ-MEM-03 / REQ-MEM-09), the sanitizer/hardened CI job (REQ-MEM-02 / REQ-MEM-08 / REQ-CIS-04), and the plugin signature/trust design (REQ-EXT-04 / REQ-CIS-03). The rollup band already nets these out at its low end. - **Highest-value quick fixes are the three confirmed code bugs:** the remote NULL-deref DoS (REQ-INP-01), the AI `http://` cleartext key/conversation egress (REQ-EXT-01), and decrypted plaintext + SASL/registration creds written to `profanity.log` under the default `dblog=on` (REQ-DAR-03). Each is small, high/critical, and directly exploitable or privacy-breaking — prioritize these ahead of the L/XL program items. - The three **XL items** (REQ-CIS-02 vuln-management process, REQ-CIS-04 blocking-CI security build-out, REQ-SDLC-05 fuzzing program) are program deliverables whose scope and duration carry the widest variance; REQ-CIS-04 substantially **bundles** several M/S CI items (REQ-MEM-02, REQ-SDLC-04, REQ-SUP-04, REQ-SDLC-01, REQ-SDLC-05), so those rows and the CIS-04 bundle should not be summed independently when planning.
Collaborator

Suggested task teardown

Suggested task teardown
jabber.developer changed title from TBD: Ensure compliance to Security compliance: assessment, planning 2026-07-02 11:30:41 +00:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: devs/cproof#144
No description provided.