33. pepsi-stage-encrypt

Sign outgoing mail as its author, and encrypt it to each recipient.

33.1. 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 pepsi-keydisc service releases it. Reference: pepsi-stage-encrypt(1).

It writes the same two standards 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. Client 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.

33.2. 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<long key ID>.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@<served domain> (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.

33.3. Where it sits

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.

33.4. Configuration

[stage-<name>]: 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 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 pepsi-stage-edit-settings.

33.5. 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 Key management for the custody model.

33.6. 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 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.

33.7. See also

pepsi-stage-decrypt (the inbound half), pepsi-stage-secure-link (where the secure-link no-key route leads), pepsi-stage-dkim-sign, pepsi-keys, pepsi-keydisc, Key management (identities, custody and the trust ladder this stage’s MIN_TRUST selects on), The secure-link fallback portal, Client interoperability (which clients have been measured against what this stage emits), RFC Index, pepsi.conf(5), pepsi.state(7)