.. This file is part of PEPSI. Copyright (C) 2026 GNUnet e.V. PEPSI is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. ===================== pepsi-stage-dkim-sign ===================== *Prepend DKIM signatures before delivery.* Role ==== ``pepsi-stage-dkim-sign`` DKIM-signs the message and advances it to a delivery stage. Separating signing from message construction lets :doc:`pepsi-stage-bounce` build an *unsigned* DSN and point its ``NEXT_STAGE`` here, so a generated bounce is signed before it is sent. Reference: :manpage:`pepsi-stage-dkim-sign(1)`. Features ======== * **Dual-signature DKIM** (RFC 6376): prepends both an **RSA-2048** and an **Ed25519** (RFC 8463) ``DKIM-Signature`` header by default; ``[pepsi] DKIM_ALGORITHMS`` narrows that to one (the largest receivers do not verify Ed25519 and report it as ``fail`` in DMARC aggregate reports). * **Signing domain selection:** ``SIGNING_DOMAIN`` if set, otherwise the domain of the message's ``From:`` header. * **Full body coverage, always:** RFC 6376's ``l=`` body-length tag is never emitted and cannot be configured. Strict verifiers (mail-auth, Stalwart, and Pepsi's own) reject an ``l > 0`` signature outright, and RFC 6376 §8.2 warns that appended content can replace the original in the reader's eyes. * **Prepend-only / single write:** signatures are added at the top, so existing signatures lower in the message are undisturbed; only the ``headers`` column is rewritten. * **Fail-closed:** a message is never advanced unsigned. If no signing domain can be determined, or there is no key directory for it, the message is **failed** (terminal ``failed``, with the reason in ``state.last_error``), and :doc:`pepsi-failure-bouncer` or the operator decides what happens to it. If the keys exist but cannot be read or used (a half-done rotation, a changed permission), the host is at fault: the message is paused and retried, with the reason in ``state.last_error``, until the stage's ``MAX_LIFETIME`` (default 120 hours) — fix the key and the queue drains by itself. Configuration ============= ``[stage-]``: ``PROGRAM = pepsi-stage-dkim-sign``, ``NEXT_STAGE`` *(required)* and ``SIGNING_DOMAIN``, plus the signature parameters ``HEADER_CANONICALIZATION`` / ``BODY_CANONICALIZATION`` (``relaxed`` — the default — or ``simple``, choosable independently), ``SIGNED_HEADERS`` (the ``h=`` set; must include ``From``) and ``SIGNATURE_EXPIRATION_DAYS`` (the ``x=`` tag; none by default). Key material and selectors come from the shared ``[pepsi]`` section (``KEY_DIR``, ``DKIM_SELECTOR``; the Ed25519 selector is that name with ``-ed25519`` appended; ``DKIM_ALGORITHMS``). See :manpage:`pepsi-stage-dkim-sign(1)`. State ===== * **Inputs:** none from ``state`` (domain from ``SIGNING_DOMAIN`` or the ``from_header`` column; body hash from the loaded message). * **Outputs:** none on the signing path — the whole ``state``, including ``state.dsn``, is preserved. Only a failure or a retry writes anything (``state.last_error``, and ``state.temporary_failures`` for a retry). See also ======== :doc:`pepsi-stage-bounce`, :doc:`pepsi-stage-relay-to-internet`, :doc:`../features`, :manpage:`pepsi-stage-dkim-sign(1)`.