85.1.27. pepsi-stage-reencrypt

seal decrypted inbound mail to the recipient’s own key before filing it

Manual section:

1

85.1.27.1.1. Name

pepsi-stage-reencrypt - the storage re-encryption stage of the Pepsi pipeline.

85.1.27.1.2. Synopsis

pepsi-stage-reencrypt [GLOBAL-OPTIONS] worker

85.1.27.1.3. Description

pepsi-stage-reencrypt is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It loads that pepsi.workqueue row (refusing to act unless its status is running) and reads its [stage-<stage>] section.

pepsi-stage-decrypt(1) opens mail encrypted to a user’s gateway (MTA) key, so that the language, spam, whitelist and key-learning stages can read it. The plaintext is then what would be filed in the mailbox. This stage runs after all of those readers and puts the message back under encryption — to the key the user registered for their own mail client (a custody = client row in pepsi.crypto_identity, the MUA key, registered with pepsi-keys(1) identity import, the e-mail key commands, or learnt from the Autocrypt: header of the user’s own submissions). What is filed is then ciphertext that only that client can open.

It acts only on a message the decrypt stage opened: state.crypto.in must say it arrived encrypted and was decrypted. Mail that arrived in plaintext is never touched (sealing it would claim a confidentiality it never had on the wire), and mail decrypt left unopened because it was already encrypted to the MUA key (for_client) needs nothing. Only recipients this host serves are considered; a recipient an alias expanded to another provider is left alone.

For each local recipient the newest active MUA key able to receive encryption is used, in whichever protocol it is (OpenPGP or S/MIME). A message with several recipients whose treatment differs is first split into one row per recipient (state.dsn.rcpt sliced in lockstep), since each copy is sealed to one person’s key.

85.1.27.1.4. Where to put it

Immediately before local delivery (pepsi-stage-relay-to-maildir(1), pepsi-stage-relay-to-lmtp(1)), and therefore:

  • after pepsi-stage-decrypt(1) — before it nothing has been opened, so the stage only passes mail through;

  • after every stage that reads the content: language detection and blocking, the whitelist and pay-to-send gates, pepsi-stage-autocrypt-learn(1) (which reads gossip from the plaintext), pepsi-stage-vacation(1);

  • after pepsi-stage-aliases(1) and pepsi-stage-dot-forward(1) — a copy forwarded elsewhere must not leave sealed to a local user’s key, which its new recipient cannot open.

pepsi-setup(1) warns when no decrypt stage leads to it, and when it leads to any of the stages above. Note that an LMTP server’s Sieve script sees the sealed message: rules on the body (or, with PROTECT_HEADERS, the subject) no longer match.

85.1.27.1.5. What it writes

The envelope split is the one pepsi-stage-encrypt(1) uses. The Content-* fields and Autocrypt-Gossip: (Autocrypt Level 1 §5.3) go inside the ciphertext; everything else stays on the outer block — Received:, From:/To:/Date:/Message-ID:, the sender’s own Autocrypt: header (a client must read it before it can decrypt anything), the DKIM/ARC fields, and X-Pepsi-Crypto, the gateway’s verdict on how the message arrived. With PROTECT_HEADERS the RFC 5322 fields are also copied inside and the outer Subject becomes ....

No signature is added. The verdict on the sender’s signature was reached by the decrypt stage and is recorded in state.crypto.in and in X-Pepsi-Crypto; a gateway signature would only say “the gateway sealed this”, which a mail client cannot tell from authorship. A sender signature that survived decryption as a MIME layer (S/MIME, or PGP/MIME multipart/signed inside the ciphertext) is inside the plaintext and so is sealed with it and still verifies in the client; an OpenPGP signature packed into the same stream as the encryption was consumed by decryption and cannot be reconstructed, so the client then shows an unsigned message and the gateway’s verdict header.

The container follows [pepsi] CRYPTO_ALLOW_DOWNGRADE exactly as for outbound encryption (see pepsi.conf(5)).

85.1.27.1.6. With no key

ON_NO_CLIENT_KEY decides what happens to a local recipient with no usable MUA key — none registered, or one that cannot be sealed to (an expired certificate, a container the policy refuses; that case is also logged as a warning):

plaintext (default)

File the plaintext, exactly as without this stage.

bounce

Refuse the message: route it to BOUNCE_STAGE with enhanced status 5.7.5 (RFC 3463, cryptographic failure). The returned DSN carries neither the body nor any header the sender could have protected — the header block is cut down to From, Sender, To, Cc, Date and Message-ID, the body is emptied and RET=HDRS is forced whatever the sender asked — because the sender encrypted the message and a DSN travels in clear. Without a BOUNCE_STAGE the row is failed instead (and pepsi-setup(1) refuses that combination).

A refusal bounces rather than defers on purpose: a deferral would keep the plaintext in the queue for the message’s whole lifetime — a longer exposure, not a shorter one — and nothing would wake it when a key is registered.

The option is read through the per-address override layer, so the site default in the section can be changed for one recipient with pepsi-settings(1):

pepsi-settings set bob@example.org reencrypt ON_NO_CLIENT_KEY bounce

85.1.27.1.7. Configuration

Options live in the stage’s own [stage-<name>] section (see pepsi.conf(5)):

NEXT_STAGE (mandatory)

Local delivery.

BOUNCE_STAGE

Where a refusal goes; required when ON_NO_CLIENT_KEY is bounce in the section, and advisable whenever a per-address override might say so.

ON_NO_CLIENT_KEY = plaintext | bounce

See With no key. Default plaintext.

PROTECT_HEADERS = yes | no

Copy the RFC 5322 fields inside the ciphertext and replace the outer Subject with ... (default no, like pepsi-stage-encrypt(1): client support is uneven, and a client that cannot look inside shows every subject as ...).

ENABLED = yes | no

Off, every message is passed through unchanged. Default yes.

LOCAL_DOMAINS, TARGETS, RECIPIENT_DELIMITER, REQUIRE_ACCOUNT

Which recipients this host serves, with the meaning and defaults of pepsi-stage-decrypt(1) (LOCAL_DOMAINS falls back to [pepsi-ingress] ACCEPTED_DOMAINS).

The stage is unprivileged and folded into the multi-call pepsi binary: sealing to a public key needs no private material, and it reads only the public columns of pepsi.crypto_identity, which the ordinary pepsi pipeline role may read.

85.1.27.1.8. State

Inputs: state.crypto.in.encrypted, state.crypto.in.decrypted and state.crypto.in.for_client, written by pepsi-stage-decrypt(1).

Outputs: state.crypto.store, whose presence also keeps the stage from ever sealing a row twice:

outcome

reencrypted, plaintext or bounced.

protocol, fingerprint, container, downgraded

For reencrypted: which MUA key was used and the container emitted.

reason

For plaintext/bounced: no-client-key, not-local or the sealing error.

The state layout is described in pepsi.state(7).

Transitions: advances to NEXT_STAGE; reroutes to BOUNCE_STAGE (or fails without one) under ON_NO_CLIENT_KEY = bounce; hands a multi-recipient row back to the queue after splitting it.

85.1.27.1.9. Commands

worker

Run as a persistent pepsi-dispatch(1) worker, reading message ids on standard input.

85.1.27.1.10. Global Options

-c FILE, –config FILE

Read the configuration from FILE instead of searching the default locations.

-L LOGLEVEL, –log LOGLEVEL

Set the logging verbosity (default info).

-v, –verbose

Show log messages from all sources.

-h, –help; -V, –version

Print a usage summary / the version and exit.

85.1.27.1.11. Exit Status

0

The message was processed and handed on.

1

An error occurred (message not found or not running, a misconfigured stage, a database error). The reason is written to the log.

78

The worker refused to start: its section or the [pepsi] crypto policy does not parse. The dispatcher requeues what it handed over and retries the stage later. A database error while a message is processed is retried the same way as any other fault of this host (see pepsi-dispatch(1)).

85.1.27.1.12. Examples

Re-encrypt message 42 (it must be running):

echo 42 | pepsi-stage-reencrypt -c /etc/pepsi/pepsi.conf worker

The tail of an inbound pipeline:

[stage-dot-forward]
PROGRAM = pepsi-stage-dot-forward
NEXT_STAGE = reencrypt

[stage-reencrypt]
PROGRAM = pepsi-stage-reencrypt
NEXT_STAGE = local
BOUNCE_STAGE = bounce

Never file readable mail for anybody, and hide subjects too:

[stage-reencrypt]
PROGRAM = pepsi-stage-reencrypt
NEXT_STAGE = local
BOUNCE_STAGE = bounce
ON_NO_CLIENT_KEY = bounce
PROTECT_HEADERS = yes

85.1.27.1.13. See Also

pepsi-stage-decrypt(1), pepsi-stage-encrypt(1), pepsi-keys(1), pepsi-settings(1), pepsi-stage-relay-to-maildir(1), pepsi-stage-relay-to-lmtp(1), pepsi-dispatch(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)

85.1.27.1.14. Bugs

Report bugs to the Pepsi issue tracker.