.. This file is part of PEPSI. Copyright (C) 2026 GNUnet e.V. PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. =================== pepsi-stage-encrypt =================== *Sign outgoing mail as its author, and encrypt it to each recipient.* Role ==== ``pepsi-stage-encrypt`` is the outbound half of Pepsi's end-to-end cryptography. For every locally submitted message it resolves what protection was asked for, applies it — signing with the **From:** author's key, then encrypting to the recipient's — and routes each recipient onward. It performs no network I/O: a recipient whose key is not cached pauses the message and a :doc:`pepsi-keydisc` service releases it. Reference: :manpage:`pepsi-stage-encrypt(1)`. It writes the same two standards :doc:`pepsi-stage-decrypt` reads — RFC 9580 with the RFC 3156 PGP/MIME containers for OpenPGP, and RFC 5652 CMS with the RFC 8551 profile for S/MIME — but the *writing* side has two choices the reading side does not, and both are visible in the configuration: * **Which container to encrypt into.** For OpenPGP this is negotiable: a certificate states which SEIPD versions its owner can read, so ``CRYPTO_ALLOW_DOWNGRADE = negotiate`` emits RFC 9580 SEIPDv2 (AEAD) to a correspondent whose key says they can read it and SEIPDv1+MDC to one whose key says they cannot. For S/MIME there is nothing to negotiate — an X.509 certificate advertises no content-encryption capability — so the code emits RFC 5083/5084 ``AuthEnvelopedData`` (AES-256-GCM) by default and only a global setting drops it to RFC 3565 AES-CBC. A packaged install *does* set it, in ``${DATADIR}/config.d/thunderbird.conf``, because Thunderbird cannot read ``AuthEnvelopedData``; deleting that file restores AEAD. :doc:`../interoperability` records which clients are known to read which. * **Which recipient info to build**, which is *not* a choice: it follows from the key type in the recipient's certificate. RSA gets an RFC 5652 §6.2.1 ``KeyTransRecipientInfo`` (RFC 4055 RSAES-OAEP, or PKCS#1 v1.5 where required), an EC certificate an RFC 5753 ``KeyAgreeRecipientInfo`` with the content key wrapped per RFC 3394. Inline (non-MIME) OpenPGP is never written; everything emitted is PGP/MIME. .. warning:: **Put this stage before the DKIM-signing stage.** The recommended outbound chain is ``submission → … → encrypt → srs → dkim-sign → relay``. DKIM must sign the bytes that are actually transmitted; the other way round produces valid-looking mail whose signature does not verify, which is worse than no signature. ``pepsi-setup`` warns when it finds the two the wrong way round. Features ======== * **The signer is the author.** Signing uses the key of the ``From:`` address, never the envelope sender: the signature is about the author the recipient sees. An author this deployment does not serve, or holds no material for, is sent **unsigned** rather than bounced — that is still legitimate mail, and DKIM already carries the domain-level claim. * **One ciphertext per recipient.** Every encrypted recipient gets their own ciphertext on their own queue row. No recipient-list disclosure, no shared content key, and ``Bcc`` leakage structurally impossible. When the recipients of one message diverge, the stage splits them one per row — ``state.dsn.rcpt`` sliced in lockstep — and each row re-enters this stage alone. Recipients that all share an outcome are never split. * **Protocol per recipient.** OpenPGP (PGP/MIME, RFC 3156) or S/MIME (CMS, RFC 8551), chosen by ``PREFER`` when a correspondent has both. An author signing with one protocol and encrypting to the other still signs *inside* the ciphertext. * **Per-recipient container negotiation.** Under the default ``CRYPTO_ALLOW_DOWNGRADE = negotiate``, OpenPGP falls back to SEIPDv1+MDC only when the recipient's own certificate proves it cannot read AEAD — a fact about the correspondent, logged and recorded, never silent. * **An explicit policy for "no key".** ``ON_NO_KEY`` derives from ``ENCRYPT``: ``opportunistic`` sends ordinary mail, ``required`` uses the secure-link portal when one is configured and bounces otherwise. ``required`` never degrades to cleartext. * **Request headers never reach the wire.** ``X-Pepsi-Sign`` and ``X-Pepsi-Encrypt`` (set by the Outlook add-in) are stripped from every outgoing message whatever they said, and are ignored entirely unless the message is local-origin. A matched subject keyword is removed from the ``Subject`` too. * **A visible size cap.** ``MAX_SIZE`` (default 25 MB) bounds what is encrypted: neither the CMS builder nor the OpenPGP paths stream, so the whole message is resident once per recipient. Above the cap, ``ON_OVERSIZE`` decides. * **The pEp preset.** ``ENABLE_PEP`` (default **on**) is a preset of defaults, not a forcing mode: it makes ``SIGN`` default to ``encrypted-only`` (sign only what is encrypted, inside the ciphertext) and creates an OpenPGP key for a local sender at a served domain on their first submission, when the address has no identity of any kind and ``[pepsi-crypto] AUTO_CREATE_IDENTITY`` allows it. With ``ENABLE_PEP = no`` a key is created only when an author *explicitly* asks for protection (a signature or only encryption) or under ``SIGN = always``. The key's owning login is resolved through ``[pepsi-crypto]``'s locality options, as for ``pepsi-keys``. Any option written out explicitly still wins. * **The user's own key.** A local sender who may speak for the ``From:`` address can register the key their mail client holds (from its ``Autocrypt:`` header, or an attached key for the address's first key) as an **MUA key** (``custody = client``). It becomes the address's *public face*: local mail is encrypted to it, WKD serves it, and Pepsi then adds no ``Autocrypt:`` header or key file of its own. * **Mail the client already protected is left alone.** A submission whose outermost layer is ciphertext is not encrypted, signed or given a key a second time (``state.crypto.out.client_protected``); one the client signed may still be encrypted, but gets no signature of Pepsi's. * **The key as a file.** ``ATTACH_KEYS_AS_FILES`` (default **off**) also attaches ``OpenPGP_0x.asc`` -- inside the ciphertext when encrypting -- until every recipient has proved they hold the key. * **Keys by e-mail.** With ``RESPONSE_STAGE`` set, a message to ``KEYS_CONTROL_LOCAL_PART@`` (default ``pepsi-keys@``) is a key command (``status``, ``generate``, ``register``, ``publish``, ``retire``) and is answered rather than relayed. * **It advertises the author's key.** ``AUTOCRYPT`` (default **on**) adds an Autocrypt Level 1 ``Autocrypt:`` header carrying the minimal transferable public key of an OpenPGP identity whose private half this deployment still holds — on **every** locally submitted message it commits, the plain cleartext pass-through included, since a correspondent must learn the key before there is anything to encrypt with. It is never added over a header the author's own MUA set, nor when the public face is an MUA key, and ``prefer-encrypt=mutual`` is claimed only when the configured ``ENCRYPT`` is ``required``. On the encrypting path the field goes on the **outer** header block. * **Gossip is opt-in.** ``AUTOCRYPT_GOSSIP`` (default **off**) additionally renders one ``Autocrypt-Gossip:`` field per *other* recipient inside the protected part, because that redistributes third parties' key material to correspondents who never asked for it. Subjects come from the parsed ``To:``/``Cc:`` only, so a ``Bcc`` recipient can never be gossiped; OpenPGP only; no discovery (an uncached address is dropped, never parked); and none at all above 50 visible recipients. ``GOSSIP_MIN_TRUST`` is its own floor (default ``owner-confirmed``), deliberately not inherited from ``MIN_TRUST``: republishing a key is not the same act as using one. Where it sits ============= .. code-block:: text submission → aliases → anti-spam → detect-language → encrypt → srs → dkim-sign → relay The two neighbours that read the message body — the pay-to-send gate and language detection — want cleartext, and they get it because encryption is last. ARC does not apply: it is for mail relayed on someone else's behalf, and this stage only ever touches mail Pepsi originated. A message that is *not* local-origin is advanced untouched (with its ``X-Pepsi-*`` headers stripped): encrypting a relayed third-party message would invalidate the originator's DKIM signature and ARC chain. Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-encrypt``, ``NEXT_STAGE`` (required) and ``BOUNCE_STAGE``, plus ``ENABLE_PEP``, ``SIGN``, ``ENCRYPT``, ``ATTACH_KEYS_AS_FILES``, ``KEYS_CONTROL_LOCAL_PART``, ``RESPONSE_STAGE``, ``PREFER``, ``ON_NO_KEY``, ``ON_OVERSIZE``, ``MIN_TRUST``, ``SUBJECT_KEYWORDS_SIGN`` / ``_ENCRYPT`` / ``_BOTH``, ``STRIP_REQUEST_HEADERS``, ``PROTECT_HEADERS``, ``MAX_SIZE``, ``AUTOCRYPT``, ``AUTOCRYPT_GOSSIP``, ``GOSSIP_MIN_TRUST``, ``SECURE_LINK_STAGE``, ``DISCOVERY`` and ``DISCOVERY_TIMEOUT``. ``pepsi-setup`` requires a ``BOUNCE_STAGE`` whenever ``ON_NO_KEY``/``ON_OVERSIZE`` resolve to ``bounce``, and checks that ``SECURE_LINK_STAGE`` and ``RESPONSE_STAGE`` name real stages. All are documented in :manpage:`pepsi.conf(5)`. Those are **behavioural** options and are per-address overridable through the ``pepsi.settings`` table (keyed, for outbound mail, on the envelope sender). The **cryptographic** policy — which algorithms, whether a downgraded container may be emitted, minimum key sizes — lives in ``[pepsi]``'s ``CRYPTO_*`` options and ``[pepsi-crypto]``, is system-administrator-only, and is deliberately not reachable from ``pepsi.settings`` or from :doc:`pepsi-stage-edit-settings`. Privilege ========= Installed **setuid** ``pepsi-crypto``, mode ``4750 pepsi-crypto:pepsi``, and therefore standalone rather than folded into the unified ``pepsi`` binary. Both halves of the key-custody boundary key on that identity: the database role ``pepsi-crypto`` is the only one granted ``crypto_identity.private_wrapped``, and PostgreSQL peer authentication keys off the **effective uid**; the key-encryption key is a ``0640`` fragment owned by the same account. A setgid binary would reach the fragment and still authenticate to the database as ``pepsi`` — the role that is denied the column. See :doc:`../key-management` for the custody model. State ===== Writes ``state.crypto.out``: whether the message was signed, by which identity, and one entry per recipient carrying ``outcome``, ``protocol``, ``fingerprint``, ``key_source``, ``trust``, ``content_algorithm`` and ``downgraded``, plus ``key_attached`` and ``client_protected`` when they apply. Sets ``state.encrypted = true`` when something was encrypted — the key :doc:`pepsi-stage-auto-whitelist` reads for its ``signature_required`` column. On a policy bounce it writes ``state.bounce`` with a ``5.7.0`` enhanced status. Every cleartext outcome reached while encryption was wanted is logged at ``WARN``: sending unprotected mail is never a silent event. See also ======== :doc:`pepsi-stage-decrypt` (the inbound half), :doc:`pepsi-stage-secure-link` (where the ``secure-link`` no-key route leads), :doc:`pepsi-stage-dkim-sign`, :doc:`pepsi-keys`, :doc:`pepsi-keydisc`, :doc:`../key-management` (identities, custody and the trust ladder this stage's ``MIN_TRUST`` selects on), :doc:`../secure-link`, :doc:`../interoperability` (which clients have been measured against what this stage emits), :doc:`../rfc-index`, :manpage:`pepsi.conf(5)`, :manpage:`pepsi.state(7)`