.. This file is part of PEPSI. Copyright (C) 2026 Pepsi contributors 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. * **Lazy identity creation.** An author who *explicitly* asks for protection and has no key gets one generated inline (``[pepsi-crypto] AUTO_CREATE_IDENTITY``). An address that merely appeared in an envelope never triggers it, so a user who never uses the feature never has key material. 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 ``SIGN``, ``ENCRYPT``, ``PREFER``, ``ON_NO_KEY``, ``ON_OVERSIZE``, ``MIN_TRUST``, ``SUBJECT_KEYWORDS_SIGN`` / ``_ENCRYPT`` / ``_BOTH``, ``STRIP_REQUEST_HEADERS``, ``PROTECT_HEADERS``, ``MAX_SIZE``, ``SECURE_LINK_STAGE``, ``DISCOVERY`` and ``DISCOVERY_TIMEOUT``. 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``. 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)`