Security compliance: assessment, planning #144
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 themasterbranch at HEAD622054fc6, 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. EveryREQ-XXX-NNcited in Part 1 links to its catalog entry in Part 2.References
Table of contents
Part 1 — Compliance audit
Executive summary
cproof's core E2EE wire guarantees are sound: after commit
622054fc6no 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 setsCURLOPT_SSL_VERIFYPEER/VERIFYHOSTand acceptshttp://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 dereferencejid_create()results without a NULL guard on a malformedfrom(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 intoprofanity.login the defaultdblog=onmode via fourlog_*sites indatabase_sqlite.c. REQ-AUTH-01 (Partial, high) leaves the OTR private key (keys.txt) and the plaintext history DB (chatlog.db) world/group-readable (nochmodafter 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 torevision = 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 noCODEOWNERS(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
Critical & high-priority gaps (ranked)
1 — REQ-EXT-01 — Enforce TLS verification & https on AI/LLM outbound — Gap (critical residual)
References: REQ-EXT-01.
src/ai/ai_client.c:811-815and:1596-1601set only URL/HTTPHEADER/WRITEFUNCTION/WRITEDATA/POSTFIELDS/TIMEOUT. The provider scheme check (src/command/cmd_funcs.c:7019) explicitly accepts bothhttpandhttps(g_strcmp0(scheme,"http")==0 || …"https"…), andai_provider_new(ai_client.c:388) storesapi_urlverbatim._build_curl_headers(ai_client.c:795) attachesAuthorization: Bearer <key>;request_urlis concatenated fromprovider->api_url(ai_client.c:1590).tests/functionaltests/test_ai.c:85,95codifies acceptance ofhttp://127.0.0.1:1/.httpsschemes in the provider scheme check (cmd_funcs.c:7019) and at request time beforecurl_easy_perform; fliptest_ai.cto asserthttp://is rejected. Additionally setCURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2Lexplicitly 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.
subprojects/libstrophe.wrapline 3revision = HEAD; no 40-hex SHA, tag, orsource_hash. No CI lint catches it.revision = HEADwith the vetted 40-hex commit SHA (or tag +source_hash); add a CI lint failing onrevision = 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.
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,fromnot 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.from(e.g.a@@b) causes a remote NULL-deref DoS — a crash on every connection attempt from that peer.jid_create(from)addif (!from_jid) { return; }before field access. Add a regression test driving these handlers with absent/a@@bfromunder ASan.4 — REQ-DAR-03 / REQ-LOG-03 — Plaintext bodies in operational logs — Gap (high)
References: REQ-DAR-03, REQ-LOG-03.
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 embedsmessage->plainat:735),:760(insert-failure, the requirement-named defect) all write decrypted bodies toprofanity.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 byPREF_STROPHE_VERBOSITY, default off).message->plain/content:/Message:from the format args atdatabase_sqlite.c:694,717,760; do not log the full INSERT at:748(log row id only). Redact<auth>/<challenge>/<response>/jabber:iq:registerpayloads in_xmpp_file_logger.5 — REQ-AUTH-01 — Owner-only (0600) on every secret/history file — Partial (high)
References: REQ-AUTH-01.
prof_keyfileg_chmod 0600,src/common.c:186/202), main log (src/log.c:177), export (src/database_export.c:329). Unprotected:src/database_sqlite.c:144sqlite3_open(filename,…)has no followingchmod(plaintext history DB inherits umask, typically 0644); OTRkeys.txt(src/otr/otr.c:372) andfingerprints.txt(otr.c:113,386) written by libotr with nochmod. No process-wideumaskanywhere in the tree.g_chmod(filename, S_IRUSR|S_IWUSR)aftersqlite3_open;g_chmodafter eachotrl_privkey_generate/otrl_privkey_write_fingerprints.6 — REQ-CRY-06 — Persisted unauthenticated OMEMO media plaintext — Partial (medium)
References: REQ-CRY-06.
aes256gcm_crypt_filewrites all decrypted plaintext to the outputFILE*(src/omemo/crypto.c:445) beforegcry_cipher_checktag(:462). The caller opensoutfhdirectly 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:112deletes only the ciphertext temp), and the external-command branch (:126-140) still runs on a failed decrypt. (Confirmed verbatim against the tree.)remove()the output and skip thecmd_templatebranch.7 — REQ-CRY-03 — No silent STARTTLS downgrade — Partial (high)
References: REQ-CRY-03.
force) correctly setsMANDATORY_TLS(src/xmpp/connection.c:185-186). But the allowlistedallowpolicy (src/common.c:406) sets neitherMANDATORY_TLSnorDISABLE_TLS(connection.c:185-194) → opportunistic STARTTLS, plaintext if the server omits it, with no log (flags==0 so the:201diagnostic never fires). In-band registration usesxmpp_connect_raw(connection.c:496-515);_register_handle_featuressecures only "if possible" (:397-431), so eventls.policy=forcesends new-account credentials in cleartext when STARTTLS is absent.allowto 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.
-Wformat=2but no-fPIE/-pieand no-Wl,-z,noexecstack(configure.ac).meson.build:64-71gates stack-protector + FORTIFY behindif is_debug; the release path (:33,executable()at:536) gets none, and Meson has zero RELRO/now/noexecstack/PIE link args. Nochecksec/readelfCI gate.-fPIE -pie -Wl,-z,noexecstack(probed) to autotools; move Meson hardening out ofif is_debugand addb_pie=true+ relro/now/noexecstack link args; add achecksecCI assertion.9 — REQ-MEM-02 — ASan/UBSan gate the test suite in CI — Partial (high)
References: REQ-MEM-02.
--enable-sanitizersexists (configure.ac:74-75,441-452) but no CI job passes it (ci-build.sh:193-205defines 4 Linux configs, none with sanitizers; tree-wide grep finds no use). Valgrind is wired and gating (ci-build.sh:295).continue-on-errorjob running./configure --enable-sanitizers && make check.10 — REQ-EXT-04 — Plugin install: https + integrity verification — Partial (high)
References: REQ-EXT-04.
cmd_plugins_install's_http_based_uri_schemeaccepts bothhttpandhttps(src/command/cmd_funcs.c:7019); plugin download reuseshttp_file_get, which setsCURLOPT_SSL_VERIFY*=0Lwhen accounttls_policy=="trust"(src/tools/http_download.c:120-156); the downloaded.so/.pyisplugins_install'd thendlopen'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.httpsfor plugin install; forceVERIFY*=1regardless of XMPPtls_policy; verify a signature/checksum before install;chmod 0600installed files; document the full-privilege trust model.11 — Other high-priority Gaps (concise)
ci/Dockerfile.*), deps cloned at HEAD with nogit checkout <sha>, Arch AUR snapshot fetched +makepkg'd unverified (Dockerfile.arch:61-65). → Digest-pin FROMs, commit-pin checkouts,sha256sum -cdownloads.AC_CHECK_LIB([gpgme],[main])(configure.ac:309); libgcrypt has no floor (:351); sqlite3/gcrypt/gpgme floors diverge frommeson.build. → VersionedPKG_CHECK_MODULES, align floors, add a parity lint.strcpy/strcaton peer barejids (src/pgp/ox.c:153-156),sprintfon key/nonce (src/omemo/crypto.c:479,483) — forbidden primitives, no CI grep. (Buffers are correctly sized, so residual is medium.) → Replace withg_strdup_printf/snprintf; add a CI grep.omemo.c:1440 malloc(len*2+1),http_upload.c:101-103uncheckedsize*nmemb/realloc-overwrite, threeomemo.c:455/475/492 malloc(*length)deref'd without NULL check. →__builtin_mul_overflow/g_malloc_n, NULL-check, save realloc to temp.allocasized from peer barejid length (src/pgp/ox.c:150-151), no-Wvla. → Replace withg_strdup_printf; add-Werror=vla.src/ui/window.c:2024,2120);str_xml_sanitizeis outgoing-only and skips U+202E. → Single sanitizer at display + log/DB write boundary.Partial / medium findings (grouped)
CRY
client_events.c:200-203, confirmed). → Guardif (id != NULL)like the 1:1 path.allow_unencrypted_message(src/otr/otr.c:332-339,425-428). → Consult the gate before the tagged cleartext send.get_random_stringusesg_rand_*(Mersenne-Twister) for stanza IDs and theprofanity_instance_idHMAC key (src/common.c:712-717,connection.c:808,1123). → Switch identifiers/HMAC seed togcry_create_nonce.blind/firstusagemodes persist trust from inbound handlers (src/omemo/omemo.c:608,698). → Confine trust transitions to user commands.AUTH
log_debug, not WARN (connection.c:201-213). → WARN +cons_showonDISABLE_TLS|LEGACY_*|TRUST_TLS.eval_passwordspawned withG_SPAWN_SEARCH_PATH(src/config/account.c:149). → Require absolute path / drop the flag.account.c:184-185,session.c:581/591,gpg.c:79/104/108). →explicit_bzerobefore free.INP
@@/UTF-8 test table is absent (tests/.../test_jid.c). → Add the table-driven suite.<result>id not disco-gated (message.c:1536-1571→ stored at:1409-1410). → Apply_stanza_id_by_trustedon the MAM path./url open|saveaccept any scheme (cmd_funcs.c:9633-9655,9678-9698). → Whitelist http/https/aesgcm.system()with hand-rolled escaping (src/ui/notifier.c:160). →g_spawnargv /g_shell_quote.MEM
omemo.c:1437 identity_public_key_len--lacks a>0guard. → Reject zero-length before decrement.identity_key_store_destroydoesn't NULL freed fields (store.c:51-53); undocumented out-param ownership. (Double-free unreachable —memsetatomemo.c:307.) → Document ownership + add=NULL.-Wnull-dereferenceonly under non-CI--enable-hardening. → Enable in one CI config.LOG / DAR
server_events.c:207); TLS cert-fail logs nothing (server_events.c:1135-1200). → Promote both to WARN.src/log.c:289,297). → Redact secret markers beforelog_msg.CFG / VUL / SDLC
tls.policy=trustdisables curl verify silently on transfers, no user warning (http_upload.c:242-244,http_download.c:153-155). → Surface the insecure state per transfer.release_get_latestnever setsVERIFY*/https-only (src/common.c:459-482). → Set them explicitly.PREF_REVEAL_OSdefaults TRUE (preferences.c:2692,2706); no<version>-omit pref. → Default OFF; add version toggle.RELEASE_GUIDE.mdhas no security step. → Add convention + review step.CURLOPT_CONNECTTIMEOUT; no cap/stall test (ai_client.c). → Add connect-timeout + tests.PRAGMA integrity_checkonly on/history verify, never at DB open (database_sqlite.c). → Run quick_check in_sqlite_init.CODEOWNERS; commitlint config dormant (.commitlintrc.jsonunreferenced). → Add CODEOWNERS + commitlint CI job.continue-on-error(ci-code.yml:40,94). → Removecontinue-on-error.compliance/REQ-TO-TEST.md.What cproof does well (notable Met items)
_send_message_stanza(message.c:491-517); OX returns NULL before send (:564-571); OMEMOkeys==NULLdoesgoto outwithout reachingmessage_send_chat_omemo(omemo.c:928-966). The622054fc6fix is real and the 1:1 caller guardsif (id != NULL)(client_events.c:138/148/157).connection.c:226); blind trust only on explicittls.policy=trust;sv_ev_certfailaccepts only via prior-trust or explicit/tls allow|always, elsereturn 0(server_events.c:1134-1191).manualmode; untrusted devices get no session (omemo.c:702-705) and are skipped;keys==NULLaborts with a remediation listing and no stanza.create_dir→g_mkdir_with_parents(name, S_IRWXU)(common.c:224); no 0755 bypass anywhere.G_GNUC_PRINTF; all untrusted classes (URLs, plugin, AI response) are%s-wrapped;check-cwe134.shexits 0.auto_*macros, 1105 uses, CI asserts the set and fails on uninitializedauto_*locals (ci-code.yml:48,51).off/redactmodes are airtight. Both backends early-return / placeholder-substitute before any persistence (database_sqlite.c:640-647,database_flatfile.c:363-373).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:
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 whenomemo_on_message_sendreturns NULL. This was confirmed verbatim against the tree. Net: no network plaintext, but a local DB + UX leak on the MUC failure path.otr_on_message_send, short-circuiting&& allow_unencrypted_message(), so the force-encryption gate is never consulted for that cleartext send.omemo/otr/pgp) declared CSPRNG "Met". Verifier foundg_rand_*(named as forbidden in the requirement) used for stanza IDs and an HMAC key seed insrc/common.c/src/xmpp.<result>id, respectively.signal_protocol_store_context_destroyis guarded and immediately followed bymemset(&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
masterat HEAD622054fc6, 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.alloca/sprintfbuffers in REQ-MEM-03/REQ-MEM-09) reflect code review, not runtime proof.CODEOWNERS, dormant commitlint) are confirmed. The PGP fingerprint currency inSECURITY.md(REQ-VUL-01) is likewise unverifiable here.checksec/readelfpass on actual release artifacts would confirm the shipped mitigation set.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
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.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).NIST | ISO | Priority | Verify. The verification text is written to be concretely checkable against the cproof source tree.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_OMEMOundefined); 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 thatp_gpg_encrypt/OMEMO/OX encrypt failure branches never reachmessage_send_chat/_send_message_stanza; confirmcl_ev_send_msg_correctechoes/logs only when the returned id is non-NULL (guards the622054fc6fix).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; underPREF_FORCE_ENCRYPTIONmodeblockit SHALL refuse with no per-message opt-out, underresend-to-confirmit 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 forblock; forresend-to-confirmfirst 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/
forcetls.policytoXMPP_CONN_FLAG_MANDATORY_TLS, SHALL setXMPP_CONN_FLAG_DISABLE_TLSonly when the user explicitly setstls.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_settingsflag computation insrc/xmpp/connection.c: NULL/force/trustset MANDATORY_TLS and exclude DISABLE_TLS/LEGACY_SSL;disablesets 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 whentls.policy=trustis 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 denyor 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
trustyields TRUST_TLS and no default/auto-fallback does; test_connection_certfail_cb/sv_ev_certfailreturns accept only for explicit allow/always and abort (0) for deny/EOF; asserttlscerts_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.policyagainst the known allowlist (force,trust,disable,direct/legacy) viavalid_tls_policy_optionon 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"yieldstls_policy=NULL(interpreted as force) and thatvalid_tls_policy_optionrejects 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_checktagon 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_lenswitch insrc/omemo/crypto.chandles only sanctioned lengths with an erroring default (no fall-through); confirmgcry_cipher_checktagfailure 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 andGCRYCTL_INITIALIZATION_FINISHEDSHALL complete (and be checked viaGCRYCTL_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_funcuses VERY_STRONG_RANDOM; verify init-finished is set before keygen and checked; grep crypto/omemo/otr/pgp sources forrand()/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_keyis 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,/pgptrust) 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_identityreturns 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_MODESHALL bemanual(notblind);blind/firstusageSHALL 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_sendkeys==NULL branch); test receiving from an untrusted identity sets the trusted flag FALSE and marks the UI; confirm blind/firstusage emit acons_shownotice.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), OMEMOidentity.txt/trust.txt/sessions.txt/known_devices.txt, OTRkeys.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. libotrotrl_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_openfor a secret/history path is followed byg_chmod/fchmod(...,S_IRUSR|S_IWUSR)with noS_IRGRP/S_IROTH; the SQLitesqlite3_openin_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.cfchmod.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 centralcreate_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()usesS_IRWXUand all account/OTR/OMEMO/PGP/plugins dir creation routes through it (no rawmkdir/g_mkdirwith 0755); runtimestat()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, orLEGACY_AUTHonly from explicit per-accounttls.policy/auth.policystrings, 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_settingsthe NULL/forcebranch 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 withdisable/legacyand 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_passwordset and assert nopasswordkey is written yet login works; after/account clear passwordassert 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_passwordcommand via direct argv spawn (g_shell_parse_argv+g_spawn_sync, neversystem()/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 avoidG_SPAWN_SEARCH_PATHso 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_passwordand empty/failed output returns FALSE (no empty password toxmpp_conn_set_pass); reviewG_SPAWN_SEARCH_PATH; stub a helper returning non-zero/empty and assert FALSE andaccount->passwordstays 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_freeof 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; ordinaryfree()/g_free()of a secret without a preceding wipe is non-conformant. (Merges REQ-MEM-12, REQ-DAR-08 in-memory half; OMEMOsignal_buffer_bzero_free/gcry_freeis 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_freeof a password/passphrase/private-key buffer (src/config/account.c,src/xmpp/session.csaved_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/MAMby, vCard/avatar refs) viajid_create()/jid_is_valid()and SHALL treat a NULL return as a hard parse failure — abandoning the stanza — rather than dereferencingJid*or any part. Confirmed defect to fix:_should_ignore_based_on_silence()insrc/xmpp/message.creadsfrom_jid->barejidafterjid_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-testtest_jid.cwith NULL/empty/a@@b/non-UTF-8/over-3071-char input asserting NULL; regression test driving_should_ignore_based_on_silencewith a stanza lackingfromasserts 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 exceedingJID_MAX_TOTAL_LEN(3071) total orJID_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 failingg_utf8_validate().NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: table-driven
test_jid.ccovering 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%sargument. Every variadic printf-like wrapper SHALL carryG_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 srcand require exit 0; grepcons_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=2and promote format diagnostics to errors (-Werror=format-security, and-Werror=format-nonliteralwhere supported) for cproof translation units so any CWE-134 regression fails the build; thecheck-cwe134.shaudit SHALL run in CI as a blocking (notcontinue-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.acAM_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 whenbyequals the user's own bare JID or the message's authoritative archive (MUC bare JID or account domain) AND that entity advertisesurn:xmpp:sid:0via disco; ids from any otherbySHALL 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); toggleurn:xmpp:sid:0in features and assert the gate; assertmessage_is_sent_by_usrejects 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 viag_uri_parse_scheme()(onlyhttp,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_*syncoversrc/and confirm none operate on URL input; unit-test scheme handling rejectsfile:/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_spawnargv API or rigorouslyg_shell_quoteevery interpolated value, treating message/sender content as untrusted data. Defect to remediate:src/ui/notifier.cbuilds a notify string and runs it viasystem().NIST: PR.PS-05 | ISO: A.8.26, A.8.28 | Priority: high | Verify: grep
notifier.cforsystem(/popen(; confirm migration to argvg_spawnor 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-JIDfrom, unexpected namespaces) cause the stanza to be skipped rather than NULL-dereferencing the result ofxmpp_stanza_get_child_by_name/get_attribute, SHALL not callabort()/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.chandlers under ASan/UBSan asserting clean rejection and no crash; code-review NULL checks after eachxmpp_stanza_get_*on the receive path; confirmassert()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/aiforwards user/peer content into an outbound LLM request, it SHALL JSON-escape all such content viaai_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",\,\nand 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 throughai_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,nowlinker 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.acandmeson.buildfor each flag and assert parity; on the built./profanityrunchecksec/readelf -d/readelf -hand assert Full RELRO, BIND_NOW, NX, PIE (Type DYN), stack canary, FORTIFY symbols present (and CET/-fstack-clash-protectionwhere the toolchain supports them); add a CI step that fails when any mitigation is missing on the produced binary and loaded.soplugins.REQ-MEM-02
ASan + UBSan + unsigned-overflow build gates the test suite in CI
The project SHALL provide an
--enable-sanitizersbuild (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-errorCI job runs./configure --enable-sanitizers && make && make checkand thatci-build.shinvokes 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 unboundedscanfon 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,snprintfwith checked return). Defects to remediate:strcpy/strcaton peer barejids insrc/pgp/ox.c:153-156;sprintfinsrc/omemo/crypto.c:479/483andsrc/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_mallocsize that multiplies or adds attacker-influenced lengths SHALL be computed with overflow-checked arithmetic (__builtin_mul_overflow/__builtin_add_overfloworg_malloc_n-style helpers) and SHALL error/abort rather than allocate a truncated buffer;reallocresults SHALL be checked before the old pointer is overwritten.NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: review multiplicative/additive
malloc/reallocargs insrc/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_MAXlengths.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-overflowand run tests/fuzz over UI scroll/pad math (src/ui/inputwin.cdocumented SIZE_MAX guard) and parser/crypto buffer-slicing; code-review subtractions ofsize_t/guintoperands for a lower-bound check; confirm nosize_tis cast tointto 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 manualfree/g_free/fclose/closeon 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; runmake checkunder ASan/LeakSanitizer asserting no leaks/double-frees; grep changed handlers forfree()/g_free()paired with early returns that should useauto_*.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/freepaths insrc/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 checkover 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-dereferenceand review warnings; run crafted-stanza/malformed-input tests under ASan asserting no NULL-deref crash; code-review each newxmpp_stanza_get_*/g_key_file_get_*/decode result for a NULL guard (cf. the autoping NULL-domain guard15dfc2bd).REQ-MEM-09
No alloca/VLA sized from untrusted lengths
cproof SHALL NOT size stack allocations (
allocaor 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))insrc/pgp/ox.c:150-156;unsigned char out[mac_len]VLA insrc/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-Wvlaand treat as error to flag VLAs; replace flagged sites with checked heap allocation +auto_*cleanup.REQ-DAR — Data Protection at Rest & Privacy
REQ-DAR-01
Functional no-storage (
off) mode writes no message contentWhen
PREF_DBLOGisoff, 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, driveadd_incoming+ alladd_outgoing_*, assert zero rows in SQLite ChatLogs and zero content lines in the flat-file; confirmget_previous_chatreturns nothing; pin the early-return in both backends (database_sqlite.c,database_flatfile.c).REQ-DAR-02
Redaction (
redact) mode never leaks plaintext into storageWhen
PREF_DBLOGisredact, cproof SHALL substitute the message body with a fixed placeholder ([REDACTED]) before it reaches any persistence call, so no original body character is passed tosqlite3_bindor 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 rawchatlog.dbbytes 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.logat 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:760log_error('content: %s', message->plain)on insert failure (bypasses no-storage/redact). The libstrophe debug bridge_xmpp_file_loggerSHALL 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 ofmessage->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; fixdatabase_sqlite.c:760; confirmsrc/ai/ai_client.cnever logs the api_key and never setsCURLOPT_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_EXCLfollowed byrename, 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 asencryption='none'plaintext or store raw ciphertext as themessagebody.NIST: PR.DS-01 | ISO: A.8.11 | Priority: medium | Verify: tests asserting an OMEMO/OTR/PGP/OX message round-trips with the matching
encryptionvalue (_get_message_enc_type), and a decryption-failure message is either not stored or stored with an explicit non-nonemarker rather than its ciphertext undernone.REQ-DAR-06
Clear-on-close history minimization honored
When
PREF_CLEAR_PERSIST_HISTORYis disabled, cproof SHALL discard in-window scrollback on window close without re-persisting it beyond the configured backend, and/clearbehavior SHALL be consistent with thePREF_DBLOGretention setting.NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: toggle
PREF_CLEAR_PERSIST_HISTORYand assert closing a chat window with it disabled produces no new persisted rows; confirm/clearwith persist off does not write back buffer content (src/ui/window.c,/clearhandler).REQ-DAR-07
Keep plaintext per-day chatlog subsystem disabled; document retention defaults
The legacy plaintext per-day chat-log writers (
chatlog.centry 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, andPREF_DBLOGvalues (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.cfunctions remain no-ops (nofopen/write) via test/grep; send/receive an encrypted message and assert nochatlogs/plaintext file underXDG_DATA_HOME; confirmPREF_DBLOGdocumented; require any new logging pref to default false.REQ-LOG — Logging, Error Handling & Detection
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 calllog_warning/log_error(notlog_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.cthat DISABLE_TLS/non-mandatory/TLS-failed/see-other-host paths emitlog_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_errorsites (src/omemo/omemo.c,src/pgp/ox.cgpgme_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_logacross levels confirming below-filter records drop; confirmlog_level_from_stringdefaults 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_sizetriggers_rotate_log_file(), produces a bounded-indexprofanity.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_initfor 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
REQ-CFG-01
HTTP(S) file transfer must not silently disable TLS verification
cproof SHALL keep
CURLOPT_SSL_VERIFYPEERandCURLOPT_SSL_VERIFYHOSTenabled for XEP-0363 upload and HTTP download by default, and SHALL disable them ONLY as an explicit consequence of the operator-selected accounttls.policy=trust(or an explicitPREF_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.ctheinsecureboolean derives solely fromtls_policy=="trust"/explicit none, and that withforce/NULL the handles retain VERIFYPEER=1/VERIFYHOST=2; grep for any unconditionalSSL_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_WARNandPREF_TLS_SHOWto 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)andprefs_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)
REQ-EXT-01
Enforce TLS peer/host verification and https scheme on AI LLM outbound requests
The AI client SHALL set
CURLOPT_SSL_VERIFYPEER=1andCURLOPT_SSL_VERIFYHOST=2on every outbound LLM request, SHALL require the providerapi_urlscheme to behttps(rejectinghttp://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 thatai_add_providerrejects anhttp://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
/aicommand; verify noprof_pre/postchat/room hook or stanza-receive path callsai_send_prompt; test that with no provider configuredai_send_promptperforms no network call; confirm no code path passes AI session content intochatlog_/database_APIs and history is freed onai_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 ideallyCURLOPT_CONNECTTIMEOUT), and_write_callbackSHALL 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_TIMEOUTand that_write_callbackreturns 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 (
dlopenwithRTLD_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 installdownload 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 createdS_IRWXU; confirm no implicit auto-load of untrusted paths; verifyplugin_download.cenforces https + TLS verify (noSSL_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.wrapSHALL pin libstrophe to an exact immutable revision (40-hex commit SHA or release tag plussource_hash/wrapdb checksum) and SHALL NOT userevision = HEADor 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 norevision = HEAD/bare branch and presence of a 40-hexrevisionorsource_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:…, notdebian:testing/archlinux:latest), and all source dependencies cloned/downloaded during image build (libstrophe, stabber, AURlibstrophe-git) SHALL be checked out at a pinned commit and obtained over authenticated HTTPS with integrity verification (commit/checksum/signature), not--depth 1HEAD or unverifiedwget+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 1without a followinggit checkout <sha>, andwget/curldownloads lacking a subsequentsha256sum/gpgcheck; assert eachFROMcontains@sha256:; confirmmakepkgintegrity 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 versionedPKG_CHECK_MODULES([gpgme],[gpgme >= X.Y]), replacing the unversionedAC_CHECK_LIB([gpgme],[main]). The declared minimums SHALL be identical betweenconfigure.acandmeson.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 bareAC_CHECK_LIB([gpgme],[main])); a script diffs the(name,minversion)tuples and fails on mismatch; configuring against a stubbed under-floor.pcaborts configure non-zero; confirmgcry_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(ordependabot.ymlfor 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/*.ymlforuses:.*@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
REQ-VUL-01
Documented vulnerability-disclosure policy with scope, supported versions and response SLA
SECURITY.mdSHALL 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.mdfor 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 setCURLOPT_SSL_VERIFYPEER=1LandCURLOPT_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.cand assert the curl handle sets VERIFYPEER/VERIFYHOST and rejects non-https; assertcons_check_version/release_is_newvalidate 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_OSoff), 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.cversion-reply: confirm<os>is added only whenPREF_REVEAL_OSis true andis_custom_clientfalse, a pref exists to omit<version>, andPREF_REVEAL_OSdefaults 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); extendRELEASE_GUIDE.mdto mandate a security-fix changelog review before tagging.REQ-RES — Resilience, Robustness & Fail-Safe Behavior
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
DbVersionreflects the pre-migration version; static review each_migrate_to_vNwraps 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_checkon the chatlog DB and SHALL handle a non-okresult (andsqlite3_open/prepare failures) by reporting to the user and degrading gracefully (e.g. disabling DB-backed history for the session) rather thanabort()/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_integrityresults 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_stateis captured and re-applied on reconnect, no crash, and chat E2EE flags (is_omemo/is_ox/pgp_send) are unchanged after resume; confirmiq_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) callsstr_xml_sanitizeon plugin/user bodies.REQ-SDLC — Secure Development Lifecycle & Governance
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) andpassword=; confirmprofrc.exampleand 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@localhostidentities and isolated temporaryXDG_DATA_HOME/XDG_CONFIG_HOMEdirs 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_FLATFILEpaths); grep tests for absolute$HOMEprofile 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, orsrc/aiSHALL require review by a designated owner recorded in.github/CODEOWNERS. Security-relevant changes SHALL carry a conventional-commitfix(<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
CODEOWNERSmapping the security-critical dirs and that GitHub enforces owner review; spot-checkgit logforfix(<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 GLibauto_*cleanup types, and codespell cleanliness — and the style/auto-type checks SHALL be blocking (notcontinue-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; removecontinue-on-errorfrom 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,addressover 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
ISO/IEC 27001:2022 Annex A clusters → Requirements
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:
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
masterat HEAD622054fc6(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, HEAD622054fc6(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
622054fc6no 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 setsCURLOPT_SSL_VERIFYPEER/VERIFYHOSTand acceptshttp://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 dereferencejid_create()results without a NULL guard on a malformedfrom(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 intoprofanity.login the defaultdblog=onmode via fourlog_*sites indatabase_sqlite.c. REQ-AUTH-01 (Partial, high) leaves the OTR private key (keys.txt) and the plaintext history DB (chatlog.db) world/group-readable (nochmodafter 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 torevision = 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 noCODEOWNERS(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
3. Critical & high-priority gaps (ranked)
3.1 — REQ-EXT-01 — Enforce TLS verification & https on AI/LLM outbound — Gap (critical residual)
src/ai/ai_client.c:811-815and:1596-1601set only URL/HTTPHEADER/WRITEFUNCTION/WRITEDATA/POSTFIELDS/TIMEOUT. The provider scheme check (src/command/cmd_funcs.c:7019) explicitly accepts bothhttpandhttps(g_strcmp0(scheme,"http")==0 || …"https"…), andai_provider_new(ai_client.c:388) storesapi_urlverbatim._build_curl_headers(ai_client.c:795) attachesAuthorization: Bearer <key>;request_urlis concatenated fromprovider->api_url(ai_client.c:1590).tests/functionaltests/test_ai.c:85,95codifies acceptance ofhttp://127.0.0.1:1/.httpsschemes in the provider scheme check (cmd_funcs.c:7019) and at request time beforecurl_easy_perform; fliptest_ai.cto asserthttp://is rejected. Additionally setCURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2Lexplicitly 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)
subprojects/libstrophe.wrapline 3revision = HEAD; no 40-hex SHA, tag, orsource_hash. No CI lint catches it.revision = HEADwith the vetted 40-hex commit SHA (or tag +source_hash); add a CI lint failing onrevision = HEAD/bare-branch wraps.3.3 — REQ-INP-01 / REQ-INP-09 — NULL-guard peer JIDs on receive — Gap (high)
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,fromnot 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.from(e.g.a@@b) causes a remote NULL-deref DoS — a crash on every connection attempt from that peer.jid_create(from)addif (!from_jid) { return; }before field access. Add a regression test driving these handlers with absent/a@@bfromunder ASan.3.4 — REQ-DAR-03 / REQ-LOG-03 — Plaintext bodies in operational logs — Gap (high)
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 embedsmessage->plainat:735),:760(insert-failure, the requirement-named defect) all write decrypted bodies toprofanity.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 byPREF_STROPHE_VERBOSITY, default off).message->plain/content:/Message:from the format args atdatabase_sqlite.c:694,717,760; do not log the full INSERT at:748(log row id only). Redact<auth>/<challenge>/<response>/jabber:iq:registerpayloads in_xmpp_file_logger.3.5 — REQ-AUTH-01 — Owner-only (0600) on every secret/history file — Partial (high)
prof_keyfileg_chmod 0600,src/common.c:186/202), main log (src/log.c:177), export (src/database_export.c:329). Unprotected:src/database_sqlite.c:144sqlite3_open(filename,…)has no followingchmod(plaintext history DB inherits umask, typically 0644); OTRkeys.txt(src/otr/otr.c:372) andfingerprints.txt(otr.c:113,386) written by libotr with nochmod. No process-wideumaskanywhere in the tree.g_chmod(filename, S_IRUSR|S_IWUSR)aftersqlite3_open;g_chmodafter eachotrl_privkey_generate/otrl_privkey_write_fingerprints.3.6 — REQ-CRY-06 — Persisted unauthenticated OMEMO media plaintext — Partial (medium)
aes256gcm_crypt_filewrites all decrypted plaintext to the outputFILE*(src/omemo/crypto.c:445) beforegcry_cipher_checktag(:462). The caller opensoutfhdirectly 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:112deletes only the ciphertext temp), and the external-command branch (:126-140) still runs on a failed decrypt. (Confirmed verbatim above.)remove()the output and skip thecmd_templatebranch.3.7 — REQ-CRY-03 — No silent STARTTLS downgrade — Partial (high)
force) correctly setsMANDATORY_TLS(src/xmpp/connection.c:185-186). But the allowlistedallowpolicy (src/common.c:406) sets neitherMANDATORY_TLSnorDISABLE_TLS(connection.c:185-194) → opportunistic STARTTLS, plaintext if the server omits it, with no log (flags==0 so the:201diagnostic never fires). In-band registration usesxmpp_connect_raw(connection.c:496-515);_register_handle_featuressecures only "if possible" (:397-431), so eventls.policy=forcesends new-account credentials in cleartext when STARTTLS is absent.allowto 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)
-Wformat=2but no-fPIE/-pieand no-Wl,-z,noexecstack(configure.ac).meson.build:64-71gates stack-protector + FORTIFY behindif is_debug; the release path (:33,executable()at:536) gets none, and Meson has zero RELRO/now/noexecstack/PIE link args. Nochecksec/readelfCI gate.-fPIE -pie -Wl,-z,noexecstack(probed) to autotools; move Meson hardening out ofif is_debugand addb_pie=true+ relro/now/noexecstack link args; add achecksecCI assertion.3.9 — REQ-MEM-02 — ASan/UBSan gate the test suite in CI — Partial (high)
--enable-sanitizersexists (configure.ac:74-75,441-452) but no CI job passes it (ci-build.sh:193-205defines 4 Linux configs, none with sanitizers; tree-wide grep finds no use). Valgrind is wired and gating (ci-build.sh:295).continue-on-errorjob running./configure --enable-sanitizers && make check.3.10 — REQ-EXT-04 — Plugin install: https + integrity verification — Partial (high)
cmd_plugins_install's_http_based_uri_schemeaccepts bothhttpandhttps(src/command/cmd_funcs.c:7019); plugin download reuseshttp_file_get, which setsCURLOPT_SSL_VERIFY*=0Lwhen accounttls_policy=="trust"(src/tools/http_download.c:120-156); the downloaded.so/.pyisplugins_install'd thendlopen'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.httpsfor plugin install; forceVERIFY*=1regardless of XMPPtls_policy; verify a signature/checksum before install;chmod 0600installed files; document the full-privilege trust model.3.11 — Other high-priority Gaps (concise)
ci/Dockerfile.*), deps cloned at HEAD with nogit checkout <sha>, Arch AUR snapshot fetched +makepkg'd unverified (Dockerfile.arch:61-65). → Digest-pin FROMs, commit-pin checkouts,sha256sum -cdownloads.AC_CHECK_LIB([gpgme],[main])(configure.ac:309); libgcrypt has no floor (:351); sqlite3/gcrypt/gpgme floors diverge frommeson.build. → VersionedPKG_CHECK_MODULES, align floors, add a parity lint.strcpy/strcaton peer barejids (src/pgp/ox.c:153-156),sprintfon key/nonce (src/omemo/crypto.c:479,483) — forbidden primitives, no CI grep. (Buffers are correctly sized, so residual is medium.) → Replace withg_strdup_printf/snprintf; add a CI grep.omemo.c:1440 malloc(len*2+1),http_upload.c:101-103uncheckedsize*nmemb/realloc-overwrite, threeomemo.c:455/475/492 malloc(*length)deref'd without NULL check. →__builtin_mul_overflow/g_malloc_n, NULL-check, save realloc to temp.allocasized from peer barejid length (src/pgp/ox.c:150-151), no-Wvla. → Replace withg_strdup_printf; add-Werror=vla.src/ui/window.c:2024,2120);str_xml_sanitizeis outgoing-only and skips U+202E. → Single sanitizer at display + log/DB write boundary.4. Partial / medium findings (grouped)
CRY
client_events.c:200-203, confirmed). → Guardif (id != NULL)like the 1:1 path.allow_unencrypted_message(src/otr/otr.c:332-339,425-428). → Consult the gate before the tagged cleartext send.get_random_stringusesg_rand_*(Mersenne-Twister) for stanza IDs and theprofanity_instance_idHMAC key (src/common.c:712-717,connection.c:808,1123). → Switch identifiers/HMAC seed togcry_create_nonce.blind/firstusagemodes persist trust from inbound handlers (src/omemo/omemo.c:608,698). → Confine trust transitions to user commands.AUTH
log_debug, not WARN (connection.c:201-213). → WARN +cons_showonDISABLE_TLS|LEGACY_*|TRUST_TLS.eval_passwordspawned withG_SPAWN_SEARCH_PATH(src/config/account.c:149). → Require absolute path / drop the flag.account.c:184-185,session.c:581/591,gpg.c:79/104/108). →explicit_bzerobefore free.INP
@@/UTF-8 test table is absent (tests/.../test_jid.c). → Add the table-driven suite.<result>id not disco-gated (message.c:1536-1571→ stored at:1409-1410). → Apply_stanza_id_by_trustedon the MAM path./url open|saveaccept any scheme (cmd_funcs.c:9633-9655,9678-9698). → Whitelist http/https/aesgcm.system()with hand-rolled escaping (src/ui/notifier.c:160). →g_spawnargv /g_shell_quote.MEM
omemo.c:1437 identity_public_key_len--lacks a>0guard. → Reject zero-length before decrement.identity_key_store_destroydoesn't NULL freed fields (store.c:51-53); undocumented out-param ownership. (Double-free unreachable —memsetatomemo.c:307.) → Document ownership + add=NULL.-Wnull-dereferenceonly under non-CI--enable-hardening. → Enable in one CI config.LOG / DAR
server_events.c:207); TLS cert-fail logs nothing (server_events.c:1135-1200). → Promote both to WARN.src/log.c:289,297). → Redact secret markers beforelog_msg.CFG / VUL / SDLC
tls.policy=trustdisables curl verify silently on transfers, no user warning (http_upload.c:242-244,http_download.c:153-155). → Surface the insecure state per transfer.release_get_latestnever setsVERIFY*/https-only (src/common.c:459-482). → Set them explicitly.PREF_REVEAL_OSdefaults TRUE (preferences.c:2692,2706); no<version>-omit pref. → Default OFF; add version toggle.RELEASE_GUIDE.mdhas no security step. → Add convention + review step.CURLOPT_CONNECTTIMEOUT; no cap/stall test (ai_client.c). → Add connect-timeout + tests.PRAGMA integrity_checkonly on/history verify, never at DB open (database_sqlite.c). → Run quick_check in_sqlite_init.CODEOWNERS; commitlint config dormant (.commitlintrc.jsonunreferenced). → Add CODEOWNERS + commitlint CI job.continue-on-error(ci-code.yml:40,94). → Removecontinue-on-error.compliance/REQ-TO-TEST.md.5. What cproof does well (notable Met items)
_send_message_stanza(message.c:491-517); OX returns NULL before send (:564-571); OMEMOkeys==NULLdoesgoto outwithout reachingmessage_send_chat_omemo(omemo.c:928-966). The622054fc6fix is real and the 1:1 caller guardsif (id != NULL)(client_events.c:138/148/157).connection.c:226); blind trust only on explicittls.policy=trust;sv_ev_certfailaccepts only via prior-trust or explicit/tls allow|always, elsereturn 0(server_events.c:1134-1191).manualmode; untrusted devices get no session (omemo.c:702-705) and are skipped;keys==NULLaborts with a remediation listing and no stanza.create_dir→g_mkdir_with_parents(name, S_IRWXU)(common.c:224); no 0755 bypass anywhere.G_GNUC_PRINTF; all untrusted classes (URLs, plugin, AI response) are%s-wrapped;check-cwe134.shexits 0.auto_*macros, 1105 uses, CI asserts the set and fails on uninitializedauto_*locals (ci-code.yml:48,51).off/redactmodes are airtight. Both backends early-return / placeholder-substitute before any persistence (database_sqlite.c:640-647,database_flatfile.c:363-373).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:
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 whenomemo_on_message_sendreturns NULL. Confirmed verbatim in this audit. Net: no network plaintext, but a local DB + UX leak on the MUC failure path.otr_on_message_send, short-circuiting&& allow_unencrypted_message(), so the force-encryption gate is never consulted for that cleartext send.omemo/otr/pgp) declared CSPRNG "Met". The verifier foundg_rand_*(named as forbidden in the requirement) used for stanza IDs and an HMAC key seed insrc/common.c/src/xmpp.<result>id, respectively.signal_protocol_store_context_destroyis guarded and immediately followed bymemset(&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
masterat HEAD622054fc6, 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.alloca/sprintfbuffers in REQ-MEM-03/REQ-MEM-09) reflect code review, not runtime proof.CODEOWNERS, dormant commitlint) are confirmed. The PGP fingerprint currency inSECURITY.md(REQ-VUL-01) is likewise unverifiable here.checksec/readelfpass on actual release artifacts would confirm the shipped mitigation set.null-verdict items retain their auditor "Met" status and were not independently re-derived here.CIS Controls v8.1 — source compliance
Target: cproof
master, HEAD622054fc6(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 torevision = HEADwith no SHA/source_hash(CIS-2.2/2.6/7.2/16.5 ↔ REQ-SUP-01, confirmed insubprojects/libstrophe.wrap:3); decrypted plaintext message bodies written toprofanity.login the defaultdblog=onmode (CIS-8.5 ↔ REQ-DAR-03/REQ-LOG-03, fourlog_*sites indatabase_sqlite.c); no code-level/dependency security scanning in CI — no OSV/Dependabot/trivy, no secret scan, no fuzzing, and the--enable-sanitizersbuild never exercised (CIS-7.4/16.12/18.1 ↔ REQ-SUP-04/REQ-MEM-02/REQ-SDLC-05); no real vulnerability-disclosure process —SECURITY.mdis 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 acceptinghttp://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.
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)
configure.acPKG_CHECK_MODULESfor 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.buildmirrors 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.3.2 — CIS-7.1 / 16.2 — No vulnerability-disclosure / management process — Partial (IG1)
SECURITY.md:1-7is 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.SECURITY.mdwith 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)
.github/workflows/ci-code.ymlruns only the Linux build matrix (arch/debian/ubuntu, :22-35), acontinue-on-errorcode-style job (:37-89), acontinue-on-errorspell-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:2FROM debian:testing(mutable), :44-45git clone --depth 1of stabber and libstrophe at floating HEAD with nogit checkout <sha>;ci/Dockerfile.arch:1FROM archlinux:latest, :61-65wgetof an AURlibstrophe-gitsnapshot thenmakepkgwith 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.:latest/:testingsources make builds non-reproducible — the same provenance gap as REQ-SUP-02.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)
subprojects/libstrophe.wrap:3revision = HEADwith no 40-hex SHA orsource_hash— confirmed verbatim — so any clone links whatever upstream HEAD is. Two crypto deps are not version-gated at all in autotools: gpgme via bareAC_CHECK_LIB([gpgme],[main])(configure.ac:309) and libgcrypt via a symbol-presence probeAC_CHECK_LIB([gcrypt],[gcry_md_extract])(:351). At runtime, dlopen'd plugins are directory-restricted (0700 dir) but not signature/checksum-verified, and/plugins installacceptshttpand reuses a downloader that disables TLS verify undertls_policy=trust.libstrophe.wrapto an immutable SHA/tag+hash and lint againstrevision = HEAD(REQ-SUP-01); versionedPKG_CHECK_MODULESfor gpgme + a gcrypt floor (REQ-SUP-03); checksum/signature-verify plugins beforedlopen(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)
PREF_DBLOG="on"mode,database_sqlite.c:694embedsmessage->plain(LMC sender mismatch),:717...content: %s(duplicate stanza-id),:748log_debug("Writing to DB. Query: %s")logs the full INSERT that embedsmessage->plain(bound at :735), and:760...content: %son 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.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:registerin_xmpp_file_logger; add a CI grep blockinglog_*interpolation ofmessage->plain/body/password/api_key/Bearer.3.6 — CIS-15.4 — No enforced security baseline toward configurable providers (AI http://) — Partial, high (IG2)
/ai set provider <name> <url>(cmd_funcs.c:10752-10757) andai_add_provider(ai_client.c:574) storeapi_urlverbatim with no scheme validation, sohttp://is accepted; both AI curl sites (ai_client.c:811-815,1596-1601) set neitherCURLOPT_SSL_VERIFYPEERnorVERIFYHOST(rely on libcurl defaults), whileAuthorization: Bearer <key>plus the full conversation is POSTed. The XMPPallowpolicy 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).httpsapi_urlat provider-add and request time and set VERIFYPEER=1L/VERIFYHOST=2L explicitly (REQ-EXT-01); mapallowto 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)
src/log.c_log_msg, default WARN, 0600 at :177, size rotation, stderr bridge). But SASL/login failure logs atlog_info("Login failed",server_events.c:208) and TLS certificate-validation failure (sv_ev_certfail,server_events.c:1135) emits nolog_*at all (UIcons_showonly) — both below the shipped WARN filter, so they are effectively not collected at the default level. Cross-validates REQ-LOG-01/REQ-LOG-02.log_warning(server_events.c:208); add alog_warninginsv_ev_certfailbefore 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)
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)
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_loglevel 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).3.10 — CIS-7.2 — Remediation capability exists but is ungoverned — Partial, high (IG1)
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), andRELEASE_GUIDE.mdhas no security/CVE step (grep for security|cve|vuln|scan|patch returns nothing).3.11 — CIS-18.1 / 16.13 — No penetration-testing / fuzzing program; sanitizers unused in CI — Gap, medium (IG2/IG3)
*/fuzz*//*/corpus*/files exist. Only defensive testing is present: scan-build + check-cwe134.sh, cmocka under Valgrind, coverage. The--enable-sanitizersASan/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.--enable-sanitizers && make checkas a non-continue-on-errorjob; 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)
forcetls.policysetsXMPP_CONN_FLAG_MANDATORY_TLS(connection.c:185-186); insecure flags reachable only via explicit policy strings;tls.policyallowlist-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).manualnot blind (preferences.c:2789-2790),PREF_ENC_WARN/PREF_TLS_SHOWdefault TRUE (:2663-2706), force-encryption resend-to-confirm default (REQ-CRY-10/REQ-CFG-02).--enable-otr/pgp/omemo,--enable-c-plugins/python-plugins/plugins,--enable-notifications); AI egress disabled by default (emptyPREF_AI_API_KEY, no network call); plugins never auto-load and load only from the 0700 dir (REQ-EXT-02/REQ-EXT-04).G_GNUC_PRINTF; all untrusted classes are%s-wrapped;check-cwe134.shexits 0; nineauto_*cleanup macros are CI-asserted (REQ-INP-03/REQ-MEM-06, Met).@localhostidentities; no real credentials in fixtures (REQ-SDLC-02).format_call_external_argv+call_external(g_spawn_async, never a shell), and peer download filenames collapse path traversal viag_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 6. Methodology & limitations
masterat HEAD622054fc6, 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.revision = HEAD, plaintext in logs, no CI security scanning, contact-only SECURITY.md, AIhttp://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-08).2. How to read this
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.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).NIST | ISO | Priority | Verify. The verification text is written to be concretely checkable against the cproof source tree.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_OMEMOundefined); 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 thatp_gpg_encrypt/OMEMO/OX encrypt failure branches never reachmessage_send_chat/_send_message_stanza; confirmcl_ev_send_msg_correctechoes/logs only when the returned id is non-NULL (guards the622054fc6fix).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; underPREF_FORCE_ENCRYPTIONmodeblockit SHALL refuse with no per-message opt-out, underresend-to-confirmit 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 forblock; forresend-to-confirmfirst 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/
forcetls.policytoXMPP_CONN_FLAG_MANDATORY_TLS, SHALL setXMPP_CONN_FLAG_DISABLE_TLSonly when the user explicitly setstls.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_settingsflag computation insrc/xmpp/connection.c: NULL/force/trustset MANDATORY_TLS and exclude DISABLE_TLS/LEGACY_SSL;disablesets 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 whentls.policy=trustis 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 denyor 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
trustyields TRUST_TLS and no default/auto-fallback does; test_connection_certfail_cb/sv_ev_certfailreturns accept only for explicit allow/always and abort (0) for deny/EOF; asserttlscerts_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.policyagainst the known allowlist (force,trust,disable,direct/legacy) viavalid_tls_policy_optionon 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"yieldstls_policy=NULL(interpreted as force) and thatvalid_tls_policy_optionrejects 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_checktagon 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_lenswitch insrc/omemo/crypto.chandles only sanctioned lengths with an erroring default (no fall-through); confirmgcry_cipher_checktagfailure 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 andGCRYCTL_INITIALIZATION_FINISHEDSHALL complete (and be checked viaGCRYCTL_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_funcuses VERY_STRONG_RANDOM; verify init-finished is set before keygen and checked; grep crypto/omemo/otr/pgp sources forrand()/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_keyis 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,/pgptrust) 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_identityreturns 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_MODESHALL bemanual(notblind);blind/firstusageSHALL 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_sendkeys==NULL branch); test receiving from an untrusted identity sets the trusted flag FALSE and marks the UI; confirm blind/firstusage emit acons_shownotice.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), OMEMOidentity.txt/trust.txt/sessions.txt/known_devices.txt, OTRkeys.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. libotrotrl_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_openfor a secret/history path is followed byg_chmod/fchmod(...,S_IRUSR|S_IWUSR)with noS_IRGRP/S_IROTH; the SQLitesqlite3_openin_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.cfchmod.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 centralcreate_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()usesS_IRWXUand all account/OTR/OMEMO/PGP/plugins dir creation routes through it (no rawmkdir/g_mkdirwith 0755); runtimestat()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, orLEGACY_AUTHonly from explicit per-accounttls.policy/auth.policystrings, 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_settingsthe NULL/forcebranch 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 withdisable/legacyand 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_passwordset and assert nopasswordkey is written yet login works; after/account clear passwordassert 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_passwordcommand via direct argv spawn (g_shell_parse_argv+g_spawn_sync, neversystem()/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 avoidG_SPAWN_SEARCH_PATHso 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_passwordand empty/failed output returns FALSE (no empty password toxmpp_conn_set_pass); reviewG_SPAWN_SEARCH_PATH; stub a helper returning non-zero/empty and assert FALSE andaccount->passwordstays 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_freeof 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; ordinaryfree()/g_free()of a secret without a preceding wipe is non-conformant. (Merges REQ-MEM-12, REQ-DAR-08 in-memory half; OMEMOsignal_buffer_bzero_free/gcry_freeis 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_freeof a password/passphrase/private-key buffer (src/config/account.c,src/xmpp/session.csaved_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/MAMby, vCard/avatar refs) viajid_create()/jid_is_valid()and SHALL treat a NULL return as a hard parse failure — abandoning the stanza — rather than dereferencingJid*or any part. Confirmed defect to fix:_should_ignore_based_on_silence()insrc/xmpp/message.creadsfrom_jid->barejidafterjid_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-testtest_jid.cwith NULL/empty/a@@b/non-UTF-8/over-3071-char input asserting NULL; regression test driving_should_ignore_based_on_silencewith a stanza lackingfromasserts 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 exceedingJID_MAX_TOTAL_LEN(3071) total orJID_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 failingg_utf8_validate().NIST: PR.PS-06, PR.DS-02 | ISO: A.8.28, A.8.26 | Priority: high | Verify: table-driven
test_jid.ccovering 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%sargument. Every variadic printf-like wrapper SHALL carryG_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 srcand require exit 0; grepcons_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=2and promote format diagnostics to errors (-Werror=format-security, and-Werror=format-nonliteralwhere supported) for cproof translation units so any CWE-134 regression fails the build; thecheck-cwe134.shaudit SHALL run in CI as a blocking (notcontinue-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.acAM_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 whenbyequals the user's own bare JID or the message's authoritative archive (MUC bare JID or account domain) AND that entity advertisesurn:xmpp:sid:0via disco; ids from any otherbySHALL 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); toggleurn:xmpp:sid:0in features and assert the gate; assertmessage_is_sent_by_usrejects 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 viag_uri_parse_scheme()(onlyhttp,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_*syncoversrc/and confirm none operate on URL input; unit-test scheme handling rejectsfile:/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_spawnargv API or rigorouslyg_shell_quoteevery interpolated value, treating message/sender content as untrusted data. Defect to remediate:src/ui/notifier.cbuilds a notify string and runs it viasystem().NIST: PR.PS-05 | ISO: A.8.26, A.8.28 | Priority: high | Verify: grep
notifier.cforsystem(/popen(; confirm migration to argvg_spawnor 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-JIDfrom, unexpected namespaces) cause the stanza to be skipped rather than NULL-dereferencing the result ofxmpp_stanza_get_child_by_name/get_attribute, SHALL not callabort()/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.chandlers under ASan/UBSan asserting clean rejection and no crash; code-review NULL checks after eachxmpp_stanza_get_*on the receive path; confirmassert()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/aiforwards user/peer content into an outbound LLM request, it SHALL JSON-escape all such content viaai_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",\,\nand 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 throughai_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,nowlinker 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.acandmeson.buildfor each flag and assert parity; on the built./profanityrunchecksec/readelf -d/readelf -hand assert Full RELRO, BIND_NOW, NX, PIE (Type DYN), stack canary, FORTIFY symbols present (and CET/-fstack-clash-protectionwhere the toolchain supports them); add a CI step that fails when any mitigation is missing on the produced binary and loaded.soplugins.REQ-MEM-02
ASan + UBSan + unsigned-overflow build gates the test suite in CI
The project SHALL provide an
--enable-sanitizersbuild (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-errorCI job runs./configure --enable-sanitizers && make && make checkand thatci-build.shinvokes 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 unboundedscanfon 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,snprintfwith checked return). Defects to remediate:strcpy/strcaton peer barejids insrc/pgp/ox.c:153-156;sprintfinsrc/omemo/crypto.c:479/483andsrc/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_mallocsize that multiplies or adds attacker-influenced lengths SHALL be computed with overflow-checked arithmetic (__builtin_mul_overflow/__builtin_add_overfloworg_malloc_n-style helpers) and SHALL error/abort rather than allocate a truncated buffer;reallocresults SHALL be checked before the old pointer is overwritten.NIST: PR.PS-01 | ISO: A.8.28 | Priority: high | Verify: review multiplicative/additive
malloc/reallocargs insrc/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_MAXlengths.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-overflowand run tests/fuzz over UI scroll/pad math (src/ui/inputwin.cdocumented SIZE_MAX guard) and parser/crypto buffer-slicing; code-review subtractions ofsize_t/guintoperands for a lower-bound check; confirm nosize_tis cast tointto 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 manualfree/g_free/fclose/closeon 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; runmake checkunder ASan/LeakSanitizer asserting no leaks/double-frees; grep changed handlers forfree()/g_free()paired with early returns that should useauto_*.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/freepaths insrc/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 checkover 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-dereferenceand review warnings; run crafted-stanza/malformed-input tests under ASan asserting no NULL-deref crash; code-review each newxmpp_stanza_get_*/g_key_file_get_*/decode result for a NULL guard (cf. the autoping NULL-domain guard15dfc2bd).REQ-MEM-09
No alloca/VLA sized from untrusted lengths
cproof SHALL NOT size stack allocations (
allocaor 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))insrc/pgp/ox.c:150-156;unsigned char out[mac_len]VLA insrc/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-Wvlaand treat as error to flag VLAs; replace flagged sites with checked heap allocation +auto_*cleanup.REQ-DAR — Data Protection at Rest & Privacy
REQ-DAR-01
Functional no-storage (
off) mode writes no message contentWhen
PREF_DBLOGisoff, 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, driveadd_incoming+ alladd_outgoing_*, assert zero rows in SQLite ChatLogs and zero content lines in the flat-file; confirmget_previous_chatreturns nothing; pin the early-return in both backends (database_sqlite.c,database_flatfile.c).REQ-DAR-02
Redaction (
redact) mode never leaks plaintext into storageWhen
PREF_DBLOGisredact, cproof SHALL substitute the message body with a fixed placeholder ([REDACTED]) before it reaches any persistence call, so no original body character is passed tosqlite3_bindor 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 rawchatlog.dbbytes 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.logat 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:760log_error('content: %s', message->plain)on insert failure (bypasses no-storage/redact). The libstrophe debug bridge_xmpp_file_loggerSHALL 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 ofmessage->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; fixdatabase_sqlite.c:760; confirmsrc/ai/ai_client.cnever logs the api_key and never setsCURLOPT_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_EXCLfollowed byrename, 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 asencryption='none'plaintext or store raw ciphertext as themessagebody.NIST: PR.DS-01 | ISO: A.8.11 | Priority: medium | Verify: tests asserting an OMEMO/OTR/PGP/OX message round-trips with the matching
encryptionvalue (_get_message_enc_type), and a decryption-failure message is either not stored or stored with an explicit non-nonemarker rather than its ciphertext undernone.REQ-DAR-06
Clear-on-close history minimization honored
When
PREF_CLEAR_PERSIST_HISTORYis disabled, cproof SHALL discard in-window scrollback on window close without re-persisting it beyond the configured backend, and/clearbehavior SHALL be consistent with thePREF_DBLOGretention setting.NIST: PR.DS-01 | ISO: A.8.10 | Priority: medium | Verify: toggle
PREF_CLEAR_PERSIST_HISTORYand assert closing a chat window with it disabled produces no new persisted rows; confirm/clearwith persist off does not write back buffer content (src/ui/window.c,/clearhandler).REQ-DAR-07
Keep plaintext per-day chatlog subsystem disabled; document retention defaults
The legacy plaintext per-day chat-log writers (
chatlog.centry 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, andPREF_DBLOGvalues (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.cfunctions remain no-ops (nofopen/write) via test/grep; send/receive an encrypted message and assert nochatlogs/plaintext file underXDG_DATA_HOME; confirmPREF_DBLOGdocumented; require any new logging pref to default false.REQ-LOG — Logging, Error Handling & Detection
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 calllog_warning/log_error(notlog_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.cthat DISABLE_TLS/non-mandatory/TLS-failed/see-other-host paths emitlog_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_errorsites (src/omemo/omemo.c,src/pgp/ox.cgpgme_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_logacross levels confirming below-filter records drop; confirmlog_level_from_stringdefaults 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_sizetriggers_rotate_log_file(), produces a bounded-indexprofanity.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_initfor 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
REQ-CFG-01
HTTP(S) file transfer must not silently disable TLS verification
cproof SHALL keep
CURLOPT_SSL_VERIFYPEERandCURLOPT_SSL_VERIFYHOSTenabled for XEP-0363 upload and HTTP download by default, and SHALL disable them ONLY as an explicit consequence of the operator-selected accounttls.policy=trust(or an explicitPREF_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.ctheinsecureboolean derives solely fromtls_policy=="trust"/explicit none, and that withforce/NULL the handles retain VERIFYPEER=1/VERIFYHOST=2; grep for any unconditionalSSL_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_WARNandPREF_TLS_SHOWto 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)andprefs_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)
REQ-EXT-01
Enforce TLS peer/host verification and https scheme on AI LLM outbound requests
The AI client SHALL set
CURLOPT_SSL_VERIFYPEER=1andCURLOPT_SSL_VERIFYHOST=2on every outbound LLM request, SHALL require the providerapi_urlscheme to behttps(rejectinghttp://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 thatai_add_providerrejects anhttp://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
/aicommand; verify noprof_pre/postchat/room hook or stanza-receive path callsai_send_prompt; test that with no provider configuredai_send_promptperforms no network call; confirm no code path passes AI session content intochatlog_/database_APIs and history is freed onai_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 ideallyCURLOPT_CONNECTTIMEOUT), and_write_callbackSHALL 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_TIMEOUTand that_write_callbackreturns 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 (
dlopenwithRTLD_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 installdownload 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 createdS_IRWXU; confirm no implicit auto-load of untrusted paths; verifyplugin_download.cenforces https + TLS verify (noSSL_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.wrapSHALL pin libstrophe to an exact immutable revision (40-hex commit SHA or release tag plussource_hash/wrapdb checksum) and SHALL NOT userevision = HEADor 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 norevision = HEAD/bare branch and presence of a 40-hexrevisionorsource_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:…, notdebian:testing/archlinux:latest), and all source dependencies cloned/downloaded during image build (libstrophe, stabber, AURlibstrophe-git) SHALL be checked out at a pinned commit and obtained over authenticated HTTPS with integrity verification (commit/checksum/signature), not--depth 1HEAD or unverifiedwget+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 1without a followinggit checkout <sha>, andwget/curldownloads lacking a subsequentsha256sum/gpgcheck; assert eachFROMcontains@sha256:; confirmmakepkgintegrity 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 versionedPKG_CHECK_MODULES([gpgme],[gpgme >= X.Y]), replacing the unversionedAC_CHECK_LIB([gpgme],[main]). The declared minimums SHALL be identical betweenconfigure.acandmeson.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 bareAC_CHECK_LIB([gpgme],[main])); a script diffs the(name,minversion)tuples and fails on mismatch; configuring against a stubbed under-floor.pcaborts configure non-zero; confirmgcry_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(ordependabot.ymlfor 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/*.ymlforuses:.*@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
REQ-VUL-01
Documented vulnerability-disclosure policy with scope, supported versions and response SLA
SECURITY.mdSHALL 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.mdfor 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 setCURLOPT_SSL_VERIFYPEER=1LandCURLOPT_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.cand assert the curl handle sets VERIFYPEER/VERIFYHOST and rejects non-https; assertcons_check_version/release_is_newvalidate 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_OSoff), 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.cversion-reply: confirm<os>is added only whenPREF_REVEAL_OSis true andis_custom_clientfalse, a pref exists to omit<version>, andPREF_REVEAL_OSdefaults 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); extendRELEASE_GUIDE.mdto mandate a security-fix changelog review before tagging.REQ-RES — Resilience, Robustness & Fail-Safe Behavior
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
DbVersionreflects the pre-migration version; static review each_migrate_to_vNwraps 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_checkon the chatlog DB and SHALL handle a non-okresult (andsqlite3_open/prepare failures) by reporting to the user and degrading gracefully (e.g. disabling DB-backed history for the session) rather thanabort()/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_integrityresults 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_stateis captured and re-applied on reconnect, no crash, and chat E2EE flags (is_omemo/is_ox/pgp_send) are unchanged after resume; confirmiq_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) callsstr_xml_sanitizeon plugin/user bodies.REQ-SDLC — Secure Development Lifecycle & Governance
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) andpassword=; confirmprofrc.exampleand 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@localhostidentities and isolated temporaryXDG_DATA_HOME/XDG_CONFIG_HOMEdirs 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_FLATFILEpaths); grep tests for absolute$HOMEprofile 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, orsrc/aiSHALL require review by a designated owner recorded in.github/CODEOWNERS. Security-relevant changes SHALL carry a conventional-commitfix(<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
CODEOWNERSmapping the security-critical dirs and that GitHub enforces owner review; spot-checkgit logforfix(<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 GLibauto_*cleanup types, and codespell cleanliness — and the style/auto-type checks SHALL be blocking (notcontinue-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; removecontinue-on-errorfrom 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,addressover 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
ISO/IEC 27001:2022 Annex A clusters → Requirements
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:
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.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 newREQ-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-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.mdper 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/acompliance/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(notrevision = 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 beforedlopenrather 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/*.wrapforrevision = HEAD/bare branch and fail; assert eachci/Dockerfile.*FROMcarries@sha256:and every clone is followed bygit checkout <sha>; confirm plugin install verifies a checksum/signature beforedlopen(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 (clangscan-buildand 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-sanitizersbuild is never exercised in CI, no fuzz harness exists, and the style/CWE-134/spell checks arecontinue-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 ofmessage->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.mdor an extension ofSECURITY.md) enumerating cproof's shipped hardened defaults — TLS mandatory (REQ-CRY-03), OMEMO trustmanual(REQ-CRY-10),PREF_ENC_WARN/PREF_TLS_SHOWon, force-encryption resend-to-confirm,PREF_DBLOGprivacy 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_OSSHALL 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 anddocs/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_OSdefaults 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 temporaryXDG_DATA_HOME/XDG_CONFIG_HOMEdirs 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
$HOMEprofile 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_urlscheme ishttps, withCURLOPT_SSL_VERIFYPEER=1L/VERIFYHOST=2Lset explicitly at every AI curl site (REQ-EXT-01); the XMPP-servertls.policy=allowpath 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 shortPROVIDERS/THIRD_PARTYdoc 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 AIhttp://egress gap and theallow-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 providerhandler reject non-httpsand both AI curl sites set VERIFYPEER/VERIFYHOST non-zero; asserttls.policy=allowcannot 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.
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) orPartial(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):
http://provider URLs + explicit VERIFY* (S · ~4h) — confirmed cleartext key/conversation egress.jid_create(from)derefs (S · 0.5d) — confirmed remote NULL-deref DoS.profanity.log(S · 0.5–1d) — confirmed plaintext-in-logs.revision = HEAD+ CI lint (S · ~2–4h).Big rocks (XL / program-level process items):
Findings
http://provider URLs — cleartext API key + conversation egress (merges REQ-CIS-08)actions/checkout@v4@v4(6 sites) to 40-hex SHAs; optional grep lint onuses:.*@v[0-9](shared with REQ-SUP-01 lint)revision = HEAD(mutable XMPP parser) (merges CIS-2.6/16.5)revision = HEADwith vetted 40-hex SHA (or tag+source_hash); add CI grep failing onrevision = HEAD/bare-branch wraps (shared lint with REQ-SUP-05)jid_create(from)on four receive-path handlers (merges REQ-INP-09)if(!from_jid) return;at each site (copy sibling _subscribe_handler pattern); add ASan regression test with absent/a@@bfromg_chmod(file, S_IRUSR|S_IWUSR)after sqlite3_open, otrl_privkey_generate and both write_fingerprints; manual perms checkjabber:iq:register; add CI grep (redaction helper shared with REQ-LOG-06).gitleaks.tomlallowlist for test-fixture false positivescontinue-on-errorjob./configure --enable-sanitizers && make check+ suppressions; triage first-green findings (shared job with REQ-MEM-08)git clone --depth 1; sha256/gpg-verify the AUR libstrophe-git snapshotallow→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)--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)if (id != NULL)like the 1:1 path (:138,148,157)identity_public_key_len--lacks a>0guard (unsigned underflow)signal_buffer_len==0before the decrementcontinue-on-error: trueat both sites so checks block merges (fix any pre-existing violations surfaced)<result>id not disco-gated (XEP-0359 trust)_stanza_id_by_trusted()on the MAM result path; skip strdup when untrusted; toggle-feature unit test/url open|saveaccept any scheme$()/backtick injection test<version>-omit pref (peer fingerprinting)<version>child (iq.c:1687-1728); stabber testPRAGMA quick_checkafter sqlite3_open:144 and degrade gracefully on non-'ok'; corrupted-DB testNotes & assumptions
mastertree at622054fc6. 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).shares_fix_withapplies, 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), theox.cg_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.http://cleartext key/conversation egress (REQ-EXT-01), and decrypted plaintext + SASL/registration creds written toprofanity.logunder the defaultdblog=on(REQ-DAR-03). Each is small, high/critical, and directly exploitable or privacy-breaking — prioritize these ahead of the L/XL program items.Suggested task teardown
TBD: Ensure complianceto Security compliance: assessment, planning