85.1.6. pepsi-stage-encrypt¶
sign outgoing mail as its author and encrypt it to each recipient
- Manual section:
1
85.1.6.1.1. Name¶
pepsi-stage-encrypt - outbound end-to-end signing and encryption stage of the Pepsi pipeline.
85.1.6.1.2. Synopsis¶
pepsi-stage-encrypt [GLOBAL-OPTIONS] worker
85.1.6.1.3. Description¶
pepsi-stage-encrypt is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. For every locally submitted message it decides what protection to apply, applies it, and routes each recipient onward.
The name understates the stage: it signs as well as encrypts, in that order (sign inside, encrypt outside). Signing uses the key of the From: author, never the envelope sender — the signature is about the author the recipient sees, and a signature whose identity does not match is what verifiers flag. If the From: address is not one this deployment serves, or holds no signing material, the message is sent unsigned rather than bounced: an unservable author is still legitimate mail, and DKIM already carries the domain-level claim.
Encryption is per recipient. Every recipient that gets ciphertext gets their
own ciphertext and their own queue row: no recipient-list disclosure, no
content key shared between recipients, and Bcc leakage is structurally
impossible. Grouping S/MIME recipients into a single EnvelopedData would be
cheaper and is what the format is for, but it reveals the recipient set to each
recipient and lets one compromised key open the message for all of them.
The stage performs no network I/O of any kind. A recipient whose key is not in the cache pauses the message and enqueues a discovery request; a pepsi-keydisc(1) service releases it when the request settles, whether or not a key was found. A recipient with a fresh negative cache entry takes the no-key path immediately, with no pause, because there is nothing to wait for.
A message this deployment did not originate (no state.local_origin) is
advanced untouched. Encrypting a relayed third-party message would invalidate
the originator’s DKIM signature and ARC chain, and signing it as one of our
users would be a lie. Its X-Pepsi-* request headers are still stripped, so
an outside sender cannot leave Pepsi’s own control channel on the wire.
Warning
Place this stage before pepsi-stage-dkim-sign(1), not after.
The recommended outbound chain is:
submission -> ... -> encrypt -> srs -> dkim-sign -> relay
DKIM must sign the bytes that are actually transmitted. If DKIM signing runs first, this stage rewrites the body underneath the signature and the result is mail that looks perfectly valid and whose signature does not verify — which is worse than no signature at all, because a broken signature is a stronger negative signal to a receiver than an absent one. pepsi-setup(1) warns when it finds a DKIM-signing stage that leads to this one.
Two other neighbours want cleartext and get it: the pay-to-send gate (pepsi-stage-anti-spam(1)) and language detection (pepsi-stage-detect-language(1)) read the message body, and on the outbound path they run before this stage. That is why encryption is last.
85.1.6.1.3.1. Policy resolution¶
What protection a message asks for is resolved from four inputs, most specific first:
the
state.local_origingate — nothing below it applies to a message we are merely forwarding;the request headers an add-in set (
X-Pepsi-Sign,X-Pepsi-Encrypt, eachyes/no/required);a subject keyword (
SUBJECT_KEYWORDS_SIGN/_ENCRYPT/_BOTH);the
[stage-encrypt]defaultsSIGNandENCRYPT, which already carry any per-address override from thepepsi.settingstable.
The two request headers are stripped from the outgoing message whatever they
say and whatever STRIP_REQUEST_HEADERS is set to: a control channel that
survives onto the wire is a control channel the next hop can be told to obey. A
matched subject keyword is likewise removed from the Subject.
An encrypting subject keyword (SUBJECT_KEYWORDS_ENCRYPT or _BOTH) or an
X-Pepsi-Encrypt: required header raises the policy
to required for that message, and required never degrades to
cleartext — if the resolved ON_NO_KEY says cleartext, it is raised to
the secure link (or a bounce) for that message. A user who explicitly asked for
protection is not silently ignored.
With ENABLE_PEP = no, key material is created only when a message asks for
it: an explicit request of any kind (X-Pepsi-Sign/X-Pepsi-Encrypt other
than no, or any subject keyword – an [encrypt] keyword under SIGN =
no included), or SIGN = always unless X-Pepsi-Sign: no refused the
signature. When [pepsi-crypto] AUTO_CREATE_IDENTITY is on, a
KEY_WRAP_SECRET is configured, the From: domain is one of
[pepsi-ingress] ACCEPTED_DOMAINS and the author has no identity of any kind,
one is generated inline, in the protocol of the recipient’s key when the message
is being encrypted and in PREFER’s protocol otherwise. An address that merely
appeared in an envelope never triggers this, so a user who never uses the
feature never has key material and is never exposed by it. Under the default
ENABLE_PEP = yes a key is instead created on the sender’s first submission,
request or not; see The pEp preset below.
Whichever way it is created, the identity’s owner is the passwd login that
[pepsi-crypto]’s LOCAL_DOMAINS, TARGETS and RECIPIENT_DELIMITER
resolve the address to, exactly as for pepsi-keys(1); none when the address
is a role address or a hosted domain.
Either way, automatic creation is refused for an address that already has a
crypto_identity row of any kind – an MUA key registered by the user, an
imported public-only key, or a revoked key. A [sign] keyword from a user who
brought their own key therefore does not mint a second key behind their back;
a user who wants a server-managed key next to their own asks for one explicitly
(the generate command below, pepsi-keys identity generate, or the web
console).
A key that should be created and cannot be – the database refuses the write,
generation itself errors – is logged at warn, and the message proceeds as
for an address with no key: its policy decides whether it goes unsigned or is
refused. The user did not ask for the key to be made, and holding their mail
hostage to a broken key store is worse than sending it without one. (A
deployment with no KEY_WRAP_SECRET creates no keys at all and is not
affected.) Registering the key a user’s client advertises is best-effort in the
same way.
Once a signing key exists and cannot be opened (an unreadable key-encryption
key, a revoked grant), the message is not sent unsigned: that error is
retried until the stage’s MAX_LIFETIME (see pepsi-dispatch(1)), like
every other fault of this host, since sending unsigned would hide a broken
deployment. The stage’s section, the [pepsi] crypto policy,
[pepsi-keydiscovery] and, when a key-encryption key is configured, the key
ring built from it are checked when the worker starts: one that does not work
makes the worker refuse to start (exit 78), and the dispatcher holds the
queue until it is fixed.
85.1.6.1.3.2. The pEp preset¶
ENABLE_PEP (default yes) makes the stage behave like the pretty Easy
privacy (pEp) project: every local sender gets a key, mail is encrypted whenever
a recipient’s key is known or discoverable, and the sender’s public key goes out
with every message. It is a preset of defaults, not a forcing mode: it
changes the defaults of other options, and any option written out explicitly in
the section (or in a per-address pepsi.settings override) still wins.
Behaviour |
|
|
|---|---|---|
|
|
|
Key creation |
eager (first submission) |
on request, or |
|
|
|
|
|
|
|
|
|
The preset concerns only mail users send: under it, cleartext mail from a user who holds a key is not signed by Pepsi, and senders get keys they did not ask for. What anyone can receive does not depend on it: inbound mail is handled the same either way.
The standard wire formats are unchanged: the messages are ordinary PGP/MIME and
Autocrypt. There are no X-pEp-Version headers, no pEp 2.x message wrapping,
no trustwords or handshake, and no key synchronisation between devices, and the
preset does not change PROTECT_HEADERS.
Eager key creation. On every committed locally submitted message – the
cleartext pass-through as well as the protected route, so that the first message
already carries the new key in its Autocrypt: header – the stage generates
an OpenPGP MTA key for the From: address when all of these hold:
ENABLE_PEPis on for the message (it is per-address overridable);the
From:domain is one of[pepsi-ingress] ACCEPTED_DOMAINS: a key is only ever made for an address whose inbound mail comes through this deployment, which is what lets pepsi-stage-decrypt(1) open replies encrypted to it;[pepsi-crypto] AUTO_CREATE_IDENTITYis on – the operator’s veto, which no per-address setting can override;a
[pepsi-crypto] KEY_WRAP_SECRETis configured;the address has no
crypto_identityrow of any kind, whatever its status, protocol or custody. A revoked key counts: if somebody revoked a key, Pepsi does not quietly mint a replacement.
This covers every submission route, including mynetworks and
client-cert relays, so a Pepsi deployed as a proxy behind an MTA that
authenticates the users works the same way. A web application sending as
noreply@ a domain Pepsi does not receive mail for gets no key. Two workers
seeing the same first-time sender at once store exactly one key: the test and
the insert run under a per-address lock in the SQL function
crypto_identity_create_if_absent, and the loser discards what it generated.
An automatically created key is marked auto_created, published over the
deployment’s own WKD, and never uploaded to a key server unless its owner
asks (vks_wanted = false; see pepsi-keys(1)).
SIGN = encrypted-only (the preset’s default) signs a message only when this
stage encrypts it, and then inside the ciphertext. Cleartext mail carries the
sender’s key (the Autocrypt: header, plus the key file when
ATTACH_KEYS_AS_FILES is on) but no signature, so a recipient who does not use
cryptography never sees a signature.asc. An explicit request
(X-Pepsi-Sign: yes, a [sign] subject keyword) still signs cleartext.
Writing SIGN = opportunistic keeps sign-everything behaviour under the
preset.
The first message to a new correspondent may still wait for key discovery up to
DISCOVERY_TIMEOUT and then go out in cleartext; the preset does not change
discovery.
85.1.6.1.3.3. Two keys per user: MTA keys, MUA keys and the public face¶
An address may hold two kinds of identity:
- MTA key
custody = local: Pepsi holds the private half (wrapped incrypto_identity.private_wrapped). Pepsi signs, decrypts and advertises with it. Generated automatically, on request, or imported with its private half.- MUA key
custody = client: the user’s own key, whose private half lives only in their mail client. The row is public-only for ever. Pepsi encrypts to it and recognises inbound mail encrypted to it, but can never open, sign with or advertise it.
The public face of an address is the key a correspondent should use: the
newest active MUA key when the user has one, otherwise the primary active MTA key
that still holds its private half. The Autocrypt: header, the key file, the
deployment’s WKD and local encryption all read it through the same ordering, so
they cannot disagree:
WKD serves only the MUA key when there is one;
mail from one local user to another is encrypted to the public face;
Pepsi adds no
Autocrypt:header and attaches no key file when the public face is an MUA key. The mail client advertises its own key, and Pepsi advertising a different one on some of the same user’s messages would make correspondents’ key state flip under Autocrypt’s newest-wins rule.
The MTA key is not retired when an MUA key appears. It keeps decrypting mail from correspondents who still use it, and it still signs, inside the ciphertext, cleartext mail that Pepsi encrypts on the user’s behalf. Inbound mail encrypted to an MUA key is passed to the mail client unopened; see pepsi-stage-decrypt(1).
85.1.6.1.3.4. Registering the user’s own key¶
A locally submitted message whose From: address is at a served domain can
register the key its mail client uses as an MUA key. Registration does not
depend on ENABLE_PEP. The key is taken from:
the message’s single valid
Autocrypt:header, whoseaddr=must equal theFrom:address; or, failing that,an attached
application/pgp-keyspart whose User IDs name the address – but only for the address’s very first key. An attachment is a weaker signal than a header the mail client manages, so a second key is registered only fromAutocrypt:.
It happens only when the submitter may speak for the From: address, which
is stricter than being allowed to send as it: the message arrived from a trusted
relay (state.origin.auth_method is mynetworks or client-cert), or from
an authenticated account (sasl, peercred) for which ingress recorded
state.origin.from_bound = true – the From: is exactly one mailbox that
matched a concrete, wildcard-free USERNAME_MAP entry of the account, or its
default login@HOSTNAME. An account granted a wildcard (*@example.org,
*) may send as its colleagues but never registers a key for them.
A fingerprint the address already has, retired or not, is not registered again,
so a key the user retired does not come back because an old device still sends
it. A new MUA key is stored with vks_wanted = false and becomes the public
face at once. When the address already had another identity, the user is sent a
null-sender notice (Auto-Submitted: auto-generated, the
key-registered.<lang>.body template) at RESPONSE_STAGE, naming the
fingerprint and saying how to retire it – so a key added with a stolen password
does not go unnoticed. With no RESPONSE_STAGE the key is still registered but
no notice can be sent. The message itself goes on unchanged apart from the usual
header hygiene.
An address that has enrolled a second factor (otp enroll, below)
registers nothing this way: a message cannot carry a code, and this path is
exactly what a stolen password would use. Its owner registers a new key with
register CODE instead.
85.1.6.1.3.5. Mail the client already protected¶
Only the outermost layer of the submitted message is consulted:
- Already encrypted
PGP/MIME, an enveloped S/MIME
application/pkcs7-mime, an inline-----BEGIN PGP MESSAGE, a signature wrapped around ciphertext, or anything no classifier could identify. The stage encrypts, signs and attaches nothing, and adds noAutocrypt:header. Header hygiene and the MUA-key registration above still run.state.crypto.out.client_protected = trueis recorded.- Already signed (
multipart/signed, inline PGP signed) The stage may still encrypt it, as
encrypted(signed), but adds no signature of its own and no cleartext key file.
A forwarded encrypted message attached as message/rfc822 is somebody else’s
protection and does not count.
85.1.6.1.3.6. Attaching the key as a file¶
ATTACH_KEYS_AS_FILES (default no, independent of the preset) also
attaches the sender’s public key the classic OpenPGP way, in addition to the
Autocrypt: header, for correspondents whose client does not read Autocrypt.
The part is application/pgp-keys, named OpenPGP_0x<long key ID>.asc (the
last 16 hex digits of the fingerprint, the name Thunderbird uses), holding the
same minimal certificate the header carries, ASCII-armoured.
On the encrypting route it goes inside the ciphertext, placed like gossip.
On the cleartext route the committed message is wrapped in a new
multipart/mixedholding the original entity and the key part, or the key becomes one more part of an existingmultipart/mixed. The stage runs before pepsi-stage-dkim-sign(1), so the deployment’s DKIM signature covers the result.It is attached until every recipient of the row provably holds the key (the
pepsi.peer_has_own_keyrecord that pepsi-stage-autocrypt-learn(1) writes when a correspondent sends an encrypted and signed reply). The record is keyed on the fingerprint, so a new key starts attaching again. A multi-recipient cleartext row is not split for this: the key goes to all if any recipient lacks proof.Never when the public face is an MUA key, never on a message the client encrypted, and not on cleartext the client signed. A message already carrying the key’s file is not given a second one.
state.crypto.out.key_attached = true records that the file went out.
85.1.6.1.3.7. Operating your keys by e-mail¶
With RESPONSE_STAGE set and KEYS_CONTROL_LOCAL_PART not none, a
locally submitted message whose only recipient is
<KEYS_CONTROL_LOCAL_PART>@<served domain> (default pepsi-keys@) is a key
command. It is consumed – deleted, never relayed – and answered at
RESPONSE_STAGE with the keys.<lang>.body template, which lists the
address’s keys. The command is the Subject:, case-insensitive, with any
Re:/Fwd: prefixes ignored:
statusList the address’s keys: kind (server-managed or the user’s own), status, key-server state, and which one is given to correspondents.
generateCreate a server-managed OpenPGP MTA key. Always allowed on request, even next to the user’s own key or a revoked one. Its key-server upload is requested when
[pepsi-keys] VKS_PUBLISHis on.registerRegister the key in this message’s
Autocrypt:header, or its attached key file, as the user’s own MUA key.publish[FPR]Ask for an upload to the key server (and publish over WKD), by default of the public face. Only an active OpenPGP key can be uploaded. The upload itself is made by the next run of
pepsi-keys identity publish --retry, and cannot be undone.retireFPRRevoke a key; also how a wrongly registered MUA key is removed.
otp enrollEnrol (or replace) a second factor for these commands: a TOTP secret (RFC 6238, SHA-1, six digits, 30-second steps). The reply carries it once – as an
otpauth://URI, in base32, and as an attached QR code (second-factor.png) for an authenticator app – and should be deleted once the secret is in the app. Replacing an existing one needs its current code. Being the only copy of the secret, the reply is committed in one transaction with the secret it carries, and rendered before either: if it cannot be built or queued, nothing is stored, the previous second factor stays as it was, and the command is retried.otp removeRemove the second factor; needs its current code.
otpalone (orotp status) isstatus.
With a second factor enrolled, every command but status must carry the
app’s current code: a word of exactly six digits anywhere in the Subject:
(register 123456; otp=123456 is accepted too). A command without one is
refused and not counted. A wrong code, or a code already used (a code is valid
once), counts as a failure and a right one resets the count; after ten failures
in a row the second factor is locked and every command but status is refused
until the operator resets it (pepsi-keys otp reset). The secret is wrapped
under the key store’s key-encryption key and readable by the pepsi-crypto
role only; see pepsi-keys(1). The reply to status says whether the
address has one and whether it is locked.
A command is answered once. A command message whose reply is already queued (an earlier pass carried it out and was interrupted before consuming it) is only consumed, so a retry never repeats a command – in particular never mints a second secret behind the reply that carries the first. A command that fails for a reason of this server is answered with a generic “try again later”; the error itself is in the log, not in the mail.
FPR is the full fingerprint or at least its last 16 hex digits, written as one
word (a 0x prefix is accepted); being at least 16 digits, it is never
mistaken for a code. list, upload and revoke are accepted for
status, publish and retire, and 2fa/totp for otp; any other subject is
answered with the key list and the valid commands. The command acts on the From: address,
and only when the submitter may speak for it (see Registering the user’s own
key); otherwise, and for an address at a domain the deployment does not serve,
the reply says the request was refused. With no RESPONSE_STAGE there is
nowhere to answer, so the surface is off and such a message is sent on like any
other.
85.1.6.1.3.8. Choosing the recipient’s key¶
Normally the key comes from the pepsi.peer_key cache, best-ranked source
first and subject to MIN_TRUST — see pepsi.conf(5) and
pepsi-keydisc(1).
There is one exception, and it is a security boundary rather than an
optimisation. If this deployment holds an ``active`` identity for the
recipient, that key is used and no ``peer_key`` row for the address is consulted
at all — whatever its rank, and however it arrived. state.crypto records
the source as own.
The reason is that peer_key is fed in part by harvesting inbound mail, and
harvesting keys on the sender-written From: header. Inbound authentication
is deliberately fail-open, so a message claiming to be from one of this
deployment’s own users can seed a peer key for that user with a stranger’s key
in it; without the exception, mail from one local user to another would then be
encrypted to the planted key and would look exactly like success.
The rule is conditional on holding a key, not on serving the address. A user who runs their own OpenPGP client and never uploaded a key here has no identity, and their real key is one this host can only learn by discovery, harvesting or gossip — so the exception does not apply to them. See the manual’s Our own users’ keys are not discovered for the trade-off in full, including its cost: a signature made with a user’s separate personal key stops verifying once an identity is held for that address.
The same preference applies to the gossip fields below: a subject of ours is introduced with the key we issued them, never with one somebody planted.
85.1.6.1.3.9. Recipient outcomes¶
Per recipient, exactly one of:
encryptedA usable key was found (subject to
MIN_TRUST) and the message was encrypted to it. The container actually emitted is recorded, together with whether it was a downgrade from the AES-256-GCM default.signed-onlyNo usable recipient key, policy allows ordinary mail, and the author’s signature was applied.
cleartextNeither signed nor encrypted.
secure-linkRouted to
SECURE_LINK_STAGEinstead of being sent.bouncedEncryption was required and could not be applied; routed to
BOUNCE_STAGEwith an RFC 34635.7.0enhanced status. With noBOUNCE_STAGEthe message is failed rather than sent unprotected.oversizeThe message was larger than
MAX_SIZEand took theON_OVERSIZEroute.
When the recipients of one message do not all share an outcome, the stage splits
them one per row (each row’s state.dsn.rcpt sliced in lockstep with its
rcpt_to) and hands the message back to the queue; each row then re-enters
this stage alone. A message whose recipients do all share an outcome — the
common case of “nobody has a key, send it as usual” — is never split.
Every outcome that puts unprotected content on the wire while encryption was
wanted is logged at WARN and recorded per recipient in state. Sending
cleartext is never a silent outcome.
85.1.6.1.3.10. Autocrypt key advertisement¶
Unless AUTOCRYPT is turned off, every locally submitted message leaves this
stage carrying an Autocrypt: header advertising the From: author’s own
OpenPGP key:
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
keydata=xjMEZ8kAABYJKwYBBAHaRw8BAQdA…
This happens on every message — signed, encrypted, routed to the secure link or plain cleartext. The pass-through case is the one that matters: a correspondent has to be able to learn the key from ordinary mail, because until they have it there is nothing to encrypt with. Advertising only on already protected mail would tell the key to exactly the people who already know it.
The advertised key is the minimal transferable public key of Autocrypt Level 1 §2.1.1 — primary key, primary User ID, its self-signature, one transport-encryption subkey and its binding signature, and nothing else. Extra User IDs, the signing subkey and any third-party certifications are stripped: the header rides on every message, so its size is paid over and over, and republishing certifications would publish the sender’s social graph as a side effect of sending mail.
Five rules bound it:
OpenPGP only. Autocrypt is defined over an OpenPGP transferable public key and has no S/MIME form, so a sender holding only an X.509 identity advertises nothing.
Locally submitted mail only, like everything else this stage does.
Never over an existing header. A real client (Thunderbird, K-9, Delta Chat) may have set its own
Autocrypt:field naming the key it holds the secret half of. That one is better than ours and is left alone — and a second field would be worse than useless, because Autocrypt Level 1 discards a message carrying two of them outright.The key must still be openable here. Only an identity that still holds its private material is advertised, so ciphertext sent to an advertised key is always ciphertext pepsi-stage-decrypt(1) can open.
Only the public face. When the user registered their own MUA key, it is the public face and Pepsi advertises nothing; the mail client advertises its key itself. A message the client already encrypted gets no header either.
prefer-encrypt=mutual is emitted only when the stage’s effective ENCRYPT
is required — including when that comes from a per-address pepsi.settings
override, so one user can advertise it without the deployment doing so. Under
Autocrypt Level 1 §2.4 a correspondent’s client enables encryption by default
only when both ends say mutual; claiming it under opportunistic, where
ON_NO_KEY = cleartext says cleartext is an acceptable outcome, would announce
a preference this deployment does not enforce. The attribute is then omitted,
which Level 1 reads as nopreference: the correspondent still records the key
and is still offered encryption, only not by default. The value follows the
configured mode rather than the one resolved for a single message, so a
[secure] subject keyword does not make a correspondent’s key state oscillate
with our users’ subject lines.
Because this stage runs before pepsi-stage-dkim-sign(1), the header is covered by the deployment’s own DKIM signature.
85.1.6.1.3.11. Autocrypt key gossip¶
AUTOCRYPT_GOSSIP (default NO) turns on the other half of Autocrypt Level
1: Autocrypt-Gossip: fields advertising the other recipients’ keys. It
answers the case the header above cannot. Alice writes to Bob and Carol, who
have never corresponded. Both learn Alice’s key from her Autocrypt: header,
but neither learns the other’s — so Bob’s reply-all cannot be encrypted to Carol
until some later message happens to introduce them. One gossiped message closes
that gap:
Autocrypt-Gossip: addr=carol@example.org;
keydata=xjMEZ8kAABYJKwYBBAHaRw8BAQdA…
It is off by default because it is a different kind of act from AUTOCRYPT:
that one publishes a key its owner asked this deployment to hold, while this one
redistributes third parties’ key material — cached here for our own use — to
correspondents who never asked for it. That is an operator’s decision.
Three rules follow from the specification, and one from this stage’s own shape.
Only inside a ciphertext. Level 1 §5.3 puts these fields in the header section of the encrypted part, and not as a nicety: an outer
Autocrypt-Gossip:field would publish the recipient set of an encrypted message to every relay on the path, which is most of what the encryption was for. So there is no gossip on the cleartext pass-through, none on a message that was only signed, and none on an S/MIME container — Autocrypt is defined over OpenPGP and has no S/MIME form, so a gossip field there would be redistribution into a place no client looks.Several are expected. One field per introduced recipient, in
To:/Cc:order — the opposite ofAutocrypt:, where a second field makes a Level 1 parser discard the message outright.No ``prefer-encrypt``. The attribute states the sender’s policy, and these fields speak for somebody else. Asserting one on a correspondent’s behalf is a claim this deployment has no standing to make, and Level 1 leaves it out of the gossip form for the same reason.
A cached key or nothing. A visible recipient this deployment holds no usable OpenPGP key for is left out of the set. The stage performs no discovery on their behalf and never pauses a message to fetch a third party’s key: an introduction is a courtesy, and delaying the sender’s mail to perform one would not be. The floor is the stricter of
MIN_TRUSTandGOSSIP_MIN_TRUST(defaultowner-confirmed): a key that would not have been good enough to encrypt to is not good enough to pass on either, and by default a key this deployment merely harvested from inbound mail — or was itself gossiped — is not republished at all. Introducing a correspondent to a key nobody vouched for is a stronger claim than using it oneself.
Warning
A blind carbon copy is never gossiped. The subject addresses come from the
message’s parsed To: and Cc: fields and from nowhere else. The
envelope recipient list — which does contain the Bcc recipients, that
being the entire meaning of a blind copy — is consulted only to remove an
address from the set (this copy’s own recipient, and the From: author).
A Bcc recipient therefore cannot appear in any copy of the message, and
the guarantee does not depend on a Bcc: field having been stripped
upstream: the field is not read at all. Level 1 §5.3 requires the same of a
receiving client, which ignores a gossip header whose addr is not a
To:/Cc: recipient — so such a header would leak the address and
achieve nothing.
The converse is deliberate. A blind recipient’s own copy does carry gossip
about the To:/Cc: recipients: they received those header fields like
everybody else, so it tells them nothing they cannot already read, and
withholding it would break reply-all for exactly the recipient least likely to
have corresponded with the others before.
A message with more than 50 visible recipients gets no gossip at all. Each introduced key is repeated in every recipient’s own ciphertext, so the cost grows with the square of the address count, and at that size the message is a broadcast where nobody will reply-all. Introducing an arbitrary fifty of them would be a silent choice with no defensible rule behind it, so the stage introduces none and says so in the log.
85.1.6.1.3.12. Content-algorithm negotiation¶
For OpenPGP the recipient’s certificate states whether it can read RFC 9580
SEIPDv2/AEAD; most of the installed base (GnuPG 2.4 and older) cannot. Under
[pepsi] CRYPTO_ALLOW_DOWNGRADE = negotiate (the default) the stage emits
SEIPDv1+MDC only when the recipient’s own key proves it cannot do better,
logs it at INFO, and records it — a downgrade is a fact about the
correspondent, not a failure. Under no the same recipient is refused and
falls into ON_NO_KEY; see the caution under that option.
S/MIME has nothing to negotiate against — an X.509 certificate advertises no
content-encryption capability — so it emits RFC 5083 AuthEnvelopedData
unless CRYPTO_ALLOW_DOWNGRADE = yes forces AES-256-CBC. On an installed
Pepsi that is the shipped default: ${DATADIR}/config.d/thunderbird.conf
sets yes, because Thunderbird 140.12.0esr cannot read
AuthEnvelopedData and fails silently when it tries. Deleting that file
restores the compiled-in negotiate, and with it AEAD. The OpenPGP behaviour
above is the same under either value — the container request is auto, which
both resolve per recipient — so the S/MIME default costs OpenPGP nothing. See
pepsi.conf(5).
85.1.6.1.4. Configuration¶
The stage reads its own [stage-<name>] section (PROGRAM =
pepsi-stage-encrypt, a required NEXT_STAGE — every outcome this stage
has is an onward hop — and BOUNCE_STAGE, which pepsi-setup(1) in turn
requires whenever ON_NO_KEY/ON_OVERSIZE resolves to bounce, 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_*, STRIP_REQUEST_HEADERS, PROTECT_HEADERS,
AUTOCRYPT, AUTOCRYPT_GOSSIP, GOSSIP_MIN_TRUST, MAX_SIZE,
SECURE_LINK_STAGE, DISCOVERY, DISCOVERY_TIMEOUT), all documented in
pepsi.conf(5). Those are
behavioural options and are per-address overridable through the
pepsi.settings table.
The cryptographic policy — algorithm choice, downgrade permission, weak-digest
acceptance, minimum key sizes — lives in [pepsi]’s CRYPTO_* options and
[pepsi-crypto]. Those are system-administrator-only and are deliberately
not reachable from pepsi.settings or pepsi-stage-edit-settings(1).
85.1.6.1.5. Privilege¶
The binary is installed setuid pepsi-crypto, mode 4750
pepsi-crypto:pepsi, and is therefore not folded into the unified pepsi
multi-call 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 lives in secrets.d/pepsi-crypto.secret, mode 0640
owned by the same account. A setgid binary would gain the group — enough to
read the fragment — and would still authenticate to the database as pepsi,
precisely the role that is denied the column.
The group in the mode restricts execution to the dispatcher’s account and root;
the program also refuses at run time unless its real uid is root or one of
the pepsi, pepsi-crypto and pepsi-owner accounts. That run-time check
applies only when the binary actually carries the setuid bit — a build
installed without it (a developer tree, cargo test) lets anyone run it, on the
grounds that such a caller reaches the database as themselves and PostgreSQL’s own
grants are then the authority. -c
is accepted from those callers (the dispatcher’s documented spawn shape is
PROGRAM -c cfg worker) and the environment is sanitised before the
configuration is loaded, because the configuration layer is steerable through
XDG_CONFIG_HOME, HOME, PATH and the PG* family.
85.1.6.1.6. State¶
Inputs: state.local_origin (the gate), state.dsn (honoured and
sliced in lockstep with any recipient split), state.origin.auth_method and
state.origin.from_bound (whether the submitter may speak for the From:
address, for key registration and the e-mail key commands).
Outputs: state.crypto.out — whether the message was signed, by which
identity, and one entry per recipient carrying the outcome, protocol,
fingerprint, key source, trust rank, content algorithm and downgrade flag;
key_attached = true when the sender’s key went out as a file, and
client_protected = true when the submitting client had already encrypted the
message and the stage left it alone. Also
state.encrypted = true when a recipient’s message was encrypted, which
pepsi-stage-auto-whitelist(1) reads. On a bounce, state.bounce. The
state layout is described in pepsi.state(7).
Transitions: NEXT_STAGE for a sent message (encrypted, signed-only or
cleartext); SECURE_LINK_STAGE or BOUNCE_STAGE for a recipient that could
not be protected; paused while waiting for key discovery; pending at this
same stage after a recipient split. A key command to the control address is
deleted (finish) after its reply has been injected at RESPONSE_STAGE;
the key-registered notices are injected there too, as new null-sender messages.
85.1.6.1.7. Commands¶
- worker
Run as a persistent dispatcher worker, reading message ids on standard input and writing one status line per message. This is the only mode; there is no one-shot form. To run a single message by hand, pipe its id:
echo 1234 | pepsi-stage-encrypt -c /etc/pepsi/pepsi.conf worker
85.1.6.1.8. Exit Status¶
- 0
The worker exited cleanly (standard input closed).
- 1
An error occurred (misconfiguration, an unreadable key store, or a database error). The reason is written to the log.
- 78
The worker refused to start: its configuration or key ring does not work. The dispatcher requeues what it handed over and retries the stage later.
85.1.6.1.9. See Also¶
pepsi-stage-dkim-sign(1), pepsi-stage-srs(1), pepsi-keys(1), pepsi-keydisc(1), pepsi-stage-auto-whitelist(1), pepsi-stage-bounce(1), pepsi-dispatch(1), pepsi-setup(1), pepsi.conf(5), pepsi.state(7)
85.1.6.1.10. Bugs¶
Report bugs to the Pepsi issue tracker.