28. pepsi-stage-decrypt¶
Decrypt mail addressed to our users, and say what its signature is worth.
28.1. Role¶
pepsi-stage-decrypt is the inbound half of Pepsi’s end-to-end cryptography.
For a message addressed to a recipient this host serves it opens whatever
ciphertext the message carries, verifies whatever signature it carries against a
real trust chain, records the verdict, harvests the sender’s key from the message
itself, and removes any security indicator the sender forged. It performs no
network I/O: when the signer’s key is unknown the message is paused and a
pepsi-keydisc service releases it. Reference:
pepsi-stage-decrypt(1).
Both protocols it reads are standards, and the division of labour between them is worth having in view:
OpenPGP — RFC 9580 (the crypto refresh, superseding RFC 4880) for the packet grammar and algorithms, RFC 3156 for the PGP/MIME containers (
multipart/encrypted,multipart/signedand themicalgparameter). Inline (non-MIME) OpenPGP is read as well, though never written.S/MIME — RFC 5652 for the Cryptographic Message Syntax, RFC 5083 with RFC 5084 for
AuthEnvelopedData(AES-GCM), RFC 3565 for AES-CBCEnvelopedData, RFC 5753 for ECDH key agreement, and RFC 8551 for the message profile with RFC 8550 for certificate handling. The certificates themselves — path building, theemailProtectionextended key usage and therfc822Namebinding that ties a certificate to an address — are RFC 5280.
Two limits follow from the standards rather than from the implementation, and
are stated here so they are not mistaken for bugs. Certificate revocation is
never checked: neither CRLs (RFC 5280 §5) nor OCSP (RFC 6960) are fetched, so
a revoked certificate still validates and the reported revocation status is
always “not checked”. And an unauthenticated container is not reported as
good: AES-CBC EnvelopedData carries no MAC, so it opens with a verdict of
untrusted rather than good, while AES-GCM AuthEnvelopedData — which is
what Pepsi emits — is authenticated. Both are listed in RFC Index.
Warning
Put this stage after the ARC stage and before the content stages. The
recommended inbound chain is
init = arc → decrypt → aliases → (anti-spam / language / …) → local
delivery. An ARC set seals the message as it arrived; decrypting first
would make the sealed AMS describe bytes nobody else ever saw.
pepsi-setup warns when it finds an ARC stage reachable from a decrypt
stage, and again when no local-delivery stage is reachable from one.
28.2. Features¶
It only runs for recipients this host serves, and for anything else it does not even try. Decryption rewrites the body, which invalidates the sender’s DKIM signature — harmless for a message about to be filed in a mailbox here, not for one being relayed onward. A message with both kinds of recipient is split, so the forwarded copy is the one that arrived.
A verdict that distinguishes what matters.
validmeans the signature is sound and the key was anchored;valid-untrustedmeans sound with a key that could not be tied to anything (a first-seen key, a CA with no anchor here) — which is most of the world’s signed mail, and which is never reported asvalid.invalid,unverifiableandnonecomplete the set, and a message with several signatures takes the worst of them.Both failure paths deliver by default.
ON_DECRYPT_FAILURE = passthrough— the user may hold the key in their own client, and bouncing mail we merely could not open helps nobody.ON_BAD_SIGNATURE = record— mailing lists that rewrite bodies, forwarders and clients with broken canonicalisation all produce bad signatures on legitimate mail.quarantineandbounceare available for both.Forged indicators are removed, unconditionally. Every
X-Pepsi-*header goes from every inbound message, and this stage’s own subject tags go from the front of theSubject, before ours are added.X-Pepsi-Cryptois a private header and therefore forgeable by definition; the removal is the entire basis on which a client may believe it. (Pepsi-Originis deliberately not in the namespace and survives.)A signal the user actually sees.
[decrypted][verified]prepended to theSubjectin nesting order — for most users the only signal they will ever see, since nobody reading mail in a webmail client inspects headers. It can be switched off, and the wording is configurable.Certificates the message carries. S/MIME signer certificates and
application/pgp-keysparts are read out of the message and offered to the verifier — before the decision to wait for a key, because most signed mail carries the certificate that signed it. They are used and dropped: storing what an inbound message teaches is pepsi-stage-autocrypt-learn, which runs later, after the stages that decide whether the message is spam. Such a certificate can only lower a verdict’s standing, never raise it — a signature checked against one isvalid-untrustedand never setsstate.signature_verified.Two facts recorded for the learn stage. Decryption commits the plaintext over the bytes that arrived, so
state.crypto.incarriesencrypted(there was ciphertext) andouter_gossip(the arriving header block already had anAutocrypt-Gossip:field, which anything on the path could have written). Both are preconditions for believing a message’s gossip, and neither is answerable once the plaintext is in the row.Every non-revoked key is tried, including expired ones, selected by capability (
purposeencryptorboth) rather than by shape. Old mail must stay readable, which is why a decryption key survives its own expiry.
28.3. Where it sits¶
ingress → arc → decrypt → aliases → check-whitelist → anti-spam →
detect-language → local delivery
Everything that reads the body wants cleartext, and gets it because decryption is early. Everything that authenticates the message as it arrived — ARC — must come first.
Placement is a performance matter too. The stage declares Load::Full, so
every message that passes through it loads its whole body from the database, even
one that turns out to be ordinary mail: whether a message is protected cannot be
told from the envelope columns, and Load is fixed per stage. That is
affordable on an inbound path terminating in local delivery, and is not a stage
to put in front of bulk relay traffic.
28.4. Waiting for a key¶
When the signer’s key is in neither the message nor the peer_key cache, the
message is parked: a discovery request is enqueued and a
pepsi-keydisc service releases the message when it settles, found or
not. So the first message from a new correspondent gets a real verdict rather
than unverifiable. A fresh negative-cache entry short-circuits the wait
entirely, so only that first message ever waits.
Whether a parked message is stored decrypted depends on the shape of the message, and the difference is not cosmetic. An S/MIME signature nested inside an envelope survives decryption as an ordinary MIME layer, so the message is parked with the plaintext committed and decrypted exactly once. An OpenPGP signature lives only inside the packet stream and is gone the moment the plaintext is out, so committing the plaintext would destroy the very evidence the key is being fetched to check; such a message is parked unchanged and decrypted again on resume.
28.5. Configuration¶
[stage-<name>]: PROGRAM = pepsi-stage-decrypt, NEXT_STAGE (required)
and BOUNCE_STAGE, plus ENABLED, LOCAL_DOMAINS / TARGETS /
RECIPIENT_DELIMITER / REQUIRE_ACCOUNT, ON_DECRYPT_FAILURE,
ON_BAD_SIGNATURE, QUARANTINE_STAGE, SUBJECT_TAGS, TAG_DECRYPTED,
TAG_VERIFIED, TAG_BAD_SIGNATURE, STRIP_SUBJECT_TAGS,
STRIP_SUBJECT_KEYWORDS, ADD_RESULT_HEADER, MAX_LAYERS, TRUST_ANCHOR_TTL, 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 inbound mail, on an envelope recipient). The
cryptographic policy lives in [pepsi]’s CRYPTO_* options and
[pepsi-crypto], is system-administrator-only, and is deliberately not
reachable from pepsi.settings.
One of those does not apply in this direction: CRYPTO_ALLOW_DOWNGRADE governs
what may be emitted. SEIPDv1+MDC and AES-CBC EnvelopedData are always
readable, because refusing them inbound would make this host unable to receive
mail from most of the world while protecting nobody.
28.6. Privilege¶
Installed setuid pepsi-crypto, mode 4750 pepsi-crypto:pepsi, and
therefore standalone rather than folded into the unified pepsi binary — the
same arrangement as pepsi-stage-encrypt, and for the same two reasons: the
database role pepsi-crypto is the only one granted
crypto_identity.private_wrapped and PostgreSQL peer authentication keys off
the effective uid, and the key-encryption key is a 0640 fragment owned by
the same account. See Key management for the custody model.
28.7. State¶
Writes state.crypto.in: whether the message arrived encrypted, what became of
the ciphertext, which identity opened it, the signature verdict with its signer,
fingerprint, key source and trust rank, and the ordered layer list. Sets
state.encrypted = true for a message that arrived encrypted and
state.signature_verified = true for the valid verdict only — the two
keys pepsi-stage-check-whitelist and pepsi-stage-auto-whitelist
read, and which no stage produced until this one. On a policy bounce it writes
state.bounce with a 5.7.0 enhanced status.
28.8. See also¶
pepsi-stage-encrypt (the outbound half), pepsi-stage-arc,
pepsi-keys, pepsi-keydisc (the service that settles the parks this
stage creates), pepsi-stage-check-whitelist (which gates a whitelist row
on the state.signature_verified this stage sets), Key management,
Client interoperability, RFC Index, pepsi.conf(5),
pepsi.state(7)