27. pepsi-stage-encrypt

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

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

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

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

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

27.4. Configuration

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

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

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

27.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)