70.1.8. pepsi-stage-dkim-sign

DKIM-sign a message in the Pepsi pipeline

Manual section:

1

70.1.8.1.1. Name

pepsi-stage-dkim-sign - DKIM-signing stage of the Pepsi pipeline.

70.1.8.1.2. Synopsis

pepsi-stage-dkim-sign [GLOBAL-OPTIONS] worker

70.1.8.1.3. Description

pepsi-stage-dkim-sign is a stage program run by pepsi-dispatch(1) as a persistent worker reading message ids on standard input. It prepends two DKIM-Signature headers (one RSA, one Ed25519) to the message’s raw bytes and advances the row to its NEXT_STAGE (a delivery stage). Signing only prepends header lines, so any existing signatures lower in the message are undisturbed. It does not touch the row’s state, so any RFC 3461 state.dsn parameters are preserved for later stages to honour.

The signing domain (d=) is SIGNING_DOMAIN if configured, otherwise the domain of the message’s From: header. The keys are the per-domain DKIM keys under the shared [pepsi] KEY_DIR (provisioned by pepsi-setup(1)), using the selectors from the [pepsi] section.

This stage exists so that message construction and DKIM signing are separate steps. In particular pepsi-stage-bounce(1) builds an unsigned DSN and points its NEXT_STAGE here, so a generated bounce is signed as its postmaster’s domain before delivery.

Signing is fail-closed: if no signing domain can be determined (no SIGNING_DOMAIN and no usable From: header), or the domain’s keys are missing, or signing errors, the message is moved to the terminal failed state (with the reason in state.last_error) rather than being advanced unsigned. A stage that cannot sign is a misconfiguration — most often the message reached dkim-sign for a domain this deployment holds no keys for — and failing it loudly (for the operator, or pepsi-failure-bouncer(1), to see) is safer than silently forwarding it unsigned, which defeats the reason the stage was placed in the pipeline.

70.1.8.1.3.1. Body coverage

By default the signature strictly covers the whole body: any change to the body in transit invalidates it. With COVER_BODY = no the signatures instead carry a body-length (l=) tag fixed to the body as signed. A verifier then ignores anything appended after that length, so a downstream hop may add a trailing footer without breaking the signature. The signed portion of the body is still fully covered; only appended content is tolerated.

70.1.8.1.3.2. Header coverage

The headers covered by the signature (the h= tag) are the SIGNED_HEADERS list (default: the common originator/MIME headers). From and Subject are listed twice so they are over-signed (RFC 6376 §8.15): a header named once more than it actually occurs is signed one extra (empty) time, so an attacker who prepends a second From: or Subject: — the headers that change a message’s apparent origin or meaning — produces an occurrence the signature does not cover, which breaks verification. Listing any header in SIGNED_HEADERS that is absent from the message likewise over-signs it (it cannot later be added).

70.1.8.1.4. Configuration

The stage is wired and configured through its own [stage-<name>] section (PROGRAM = pepsi-stage-dkim-sign, a required NEXT_STAGE). Its options — COVER_BODY, HEADER_CANONICALIZATION/BODY_CANONICALIZATION (chosen independently), SIGNATURE_EXPIRATION_DAYS, SIGNED_HEADERS and SIGNING_DOMAIN — together with the shared [pepsi] key material (KEY_DIR, DKIM_SELECTOR) are documented in pepsi.conf(5). The hash is always SHA-256; both an RSA and an Ed25519 signature are emitted.

70.1.8.1.5. State

Inputs: none from state — the signing domain comes from SIGNING_DOMAIN or the from_header column, and the body hash from the loaded message.

Outputs: none added to state. The signatures are prepended to the headers column; the whole state — including state.dsn — is preserved unchanged. The state layout is described in pepsi.state(7).

Transitions: on success advances to NEXT_STAGE (the delivery stage), having prepended the DKIM-Signature header(s). If the signing domain cannot be determined or its keys are missing / signing errors, the message is moved to the terminal failed state (fail-closed; see above). The stage never pauses, reroutes or finishes.

70.1.8.1.6. Commands

Run as a worker, the program signs each message and advances it.

70.1.8.1.7. Global Options

-c FILE, –config FILE

Read the configuration from FILE instead of the default search path.

-L LOGLEVEL, –log LOGLEVEL

Set the logging verbosity (error, warn, info, debug, trace; default info).

-h, –help

Print a usage summary and exit.

-V, –version

Print the version and exit.

70.1.8.1.8. Exit Status

0

The message was signed and moved to its next stage, was failed (fail-closed) when it could not be signed, or the subcommand succeeded.

1

An error occurred (misconfiguration, the message is not in the expected state, or a database error). The reason is written to the log.

70.1.8.1.9. See Also

pepsi-config(1), pepsi-stage-bounce(1), pepsi-stage-relay-to-internet(1), pepsi-stage-relay-to-smarthost(1), pepsi-dispatch(1), pepsi-ingress(1), pepsi.conf(5), pepsi.state(7), pepsi-setup(1)

70.1.8.1.10. Bugs

Report bugs to the Pepsi issue tracker.