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 = negotiateemits 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/5084AuthEnvelopedData(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 readAuthEnvelopedData; 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 5753KeyAgreeRecipientInfowith 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
Bccleakage structurally impossible. When the recipients of one message diverge, the stage splits them one per row —state.dsn.rcptsliced 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
PREFERwhen 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_KEYderives fromENCRYPT:opportunisticsends ordinary mail,requireduses the secure-link portal when one is configured and bounces otherwise.requirednever degrades to cleartext.Request headers never reach the wire.
X-Pepsi-SignandX-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 theSubjecttoo.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_OVERSIZEdecides.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)