Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES-202605.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ A systematic audit of memory and resource leaks (issue #272) produced fixes acro
- **Inline IPv6 CIDR entries in dataset lists**: Bracketed IPv6 addresses (e.g. `[fd00:368::]/40`) can now be used directly in inline dataset values for `PeerList`, `InternalHosts`, and similar options, without requiring a file reference. The colons in an IPv6 address were previously misidentified as a `type:` prefix. (#368, issue #319)
- **MySQL DSN special characters in passwords**: `=XY` escape sequences in DSN credential fields were never decoded. A typo also caused lowercase hex digits `a`-`e` to decode incorrectly. Both are fixed; passwords containing `=`, `%`, and other special characters now round-trip correctly through the DSN parser. (#369, issue #248)
- **Auto-detect `SignatureAlgorithm` from key type**: When `SignatureAlgorithm` is not explicitly set in the config, opendkim now inspects the loaded `KeyFile` and automatically selects `ed25519-sha256` for ed25519 keys. RSA keys continue to default to `rsa-sha256`. This eliminates the previously undocumented requirement to add `SignatureAlgorithm ed25519-sha256` alongside an ed25519 `KeyFile`. (#370, issue #107)
- **`AddCanonicalizedData` option**: New opt-in directive (default off) that adds `X-DKIM-Canonicalized-Header`/`X-DKIM-Canonicalized-Body` fields, base64-encoded and tagged with the signature's `d=`/`s=` values, for every verified signature regardless of pass/fail. This exposes canonicalized bytes OpenDKIM already computes during verification -- previously only surfaced via the unrelated `SendReports` feature on failure -- so a downstream DMARC filter (e.g. OpenDMARC) can populate the `DKIM-Canonicalized-Header`/`-Body` RFC 6591 ARF fields required by RFC 9991 forensic reports, without needing to verify DKIM itself. Turning it on implies the same in-memory temporary-file usage as `SendReports`/`KeepTemporaryFiles`, but never persists files to disk. (#423)

---

Expand Down
1 change: 1 addition & 0 deletions opendkim/opendkim-config.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
/* config definition */
struct configdef dkimf_config[] =
{
{ "AddCanonicalizedData", CONFIG_TYPE_BOOLEAN, FALSE },
{ "AllowSHA1Only", CONFIG_TYPE_BOOLEAN, FALSE },
{ "AlwaysAddARHeader", CONFIG_TYPE_BOOLEAN, FALSE },
#ifdef _FFR_ATPS
Expand Down
180 changes: 179 additions & 1 deletion opendkim/opendkim.c
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ struct dkimf_config
_Bool conf_alwaysaddar; /* always add Auth-Results:? */
_Bool conf_reqreports; /* request reports */
_Bool conf_sendreports; /* signature failure reports */
_Bool conf_addcanondata; /* add canonicalized data headers */
_Bool conf_reqhdrs; /* required header checks */
_Bool conf_authservidwithjobid; /* use jobids in A-R headers */
_Bool conf_subdomains; /* sign subdomains */
Expand Down Expand Up @@ -740,6 +741,7 @@ void dkimf_sendprogress (const void *);
sfsistat dkimf_setpriv (SMFICTX *, void *);
sfsistat dkimf_setreply (SMFICTX *, char *, char *, char *);
static void dkimf_sigreport (connctx, struct dkimf_config *, char *);
static void dkimf_add_canon_headers (connctx, struct dkimf_config *, SMFICTX *);
static void dkimf_log(struct dkimf_config *conf, int priority, const char *format, ...);

/* GLOBALS */
Expand Down Expand Up @@ -4200,6 +4202,169 @@ dkimf_add_ar_fields(struct msgctx *dfc, struct dkimf_config *conf,
}
}

/*
** DKIMF_CANON_HEADER_VALUE -- base64-encode a canonicalization tmp file
** into a malloc'd, NUL-terminated, folded
** header value
**
** Parameters:
** fd -- descriptor of the canonicalization tmp file (header or body)
** prefix -- tag prefix ("d=...; s=...; b=") to prepend on the same
** line before the base64 data starts
**
** Return value:
** A malloc'd C string ready to hand to dkimf_insheader(), or NULL on
** failure. Caller must free() the result.
*/

static char *
dkimf_canon_header_value(int fd, const char *prefix)
{
long len;
char *buf;
FILE *tmp;

if (fd < 0)
return NULL;

tmp = tmpfile();
if (tmp == NULL)
return NULL;

fputs(prefix, tmp);

dkimf_base64_encode_file(fd, tmp, 8, DKIM_HDRMARGIN,
(int) strlen(prefix));

len = ftell(tmp);
if (len <= 0)
{
fclose(tmp);
return NULL;
}

buf = (char *) malloc((size_t) len + 1);
if (buf == NULL)
{
fclose(tmp);
return NULL;
}

rewind(tmp);
if (fread(buf, 1, (size_t) len, tmp) != (size_t) len)
{
free(buf);
fclose(tmp);
return NULL;
}
buf[len] = '\0';

fclose(tmp);

return buf;
}

/*
** DKIMF_ADD_CANON_HEADERS -- add canonicalized header/body data as milter
** headers, for downstream DMARC forensic
** reporting (RFC 9991 / RFC 6591)
**
** Parameters:
** cc -- connection context
** conf -- configuration handle
** ctx -- milter context
**
** Return value:
** None.
**
** Notes:
** The canonicalized bytes come from the same tmp files the
** SendReports feature uses (see dkimf_sigreport()); they're
** populated for every verified signature regardless of whether it
** requested reporting ("r=y") or ultimately passed. This emits one
** X-DKIM-Canonicalized-Header/-Body pair per signature, tagged with
** its domain/selector so a downstream consumer (e.g. OpenDMARC) can
** match them against whichever signature it cares about without
** needing a numeric index.
*/

static void
dkimf_add_canon_headers(connctx cc, struct dkimf_config *conf, SMFICTX *ctx)
{
int c;
int nsigs = 0;
msgctx dfc;
DKIM_SIGINFO **sigs = NULL;

assert(cc != NULL);
assert(conf != NULL);
assert(ctx != NULL);

dfc = cc->cctx_msg;

assert(dfc->mctx_dkimv != NULL);

if (dkim_getsiglist(dfc->mctx_dkimv, &sigs, &nsigs) != DKIM_STAT_OK)
return;

for (c = 0; c < nsigs; c++)
{
int bfd = -1;
int hfd = -1;
char *domain;
char *selector;
char prefix[BUFRSZ];
char *hval;

if (dkim_sig_getreportinfo(dfc->mctx_dkimv, sigs[c],
&hfd, &bfd,
NULL, 0, NULL, 0,
NULL, 0, NULL) != DKIM_STAT_OK)
continue;

domain = (char *) dkim_sig_getdomain(sigs[c]);
selector = (char *) dkim_sig_getselector(sigs[c]);

snprintf(prefix, sizeof prefix, "d=%s; s=%s; b=",
domain != NULL ? domain : "",
selector != NULL ? selector : "");

if (hfd != -1)
{
hval = dkimf_canon_header_value(hfd, prefix);
if (hval != NULL)
{
if (dkimf_insheader(ctx, 0,
"X-DKIM-Canonicalized-Header",
hval) == MI_FAILURE)
{
dkimf_log(conf, LOG_ERR,
"%s: X-DKIM-Canonicalized-Header header add failed",
dfc->mctx_jobid);
}
free(hval);
}
}

if (bfd != -1)
{
hval = dkimf_canon_header_value(bfd, prefix);
if (hval != NULL)
{
if (dkimf_insheader(ctx, 0,
"X-DKIM-Canonicalized-Body",
hval) == MI_FAILURE)
{
dkimf_log(conf, LOG_ERR,
"%s: X-DKIM-Canonicalized-Body header add failed",
dfc->mctx_jobid);
}
free(hval);
}
}
}
}

/*
** DKIMF_DB_ERROR -- syslog errors related to db retrieval
**
Expand Down Expand Up @@ -6521,6 +6686,13 @@ dkimf_config_load(struct config *data, struct dkimf_config *conf,
&conf->conf_sendreports,
sizeof conf->conf_sendreports);
}

if (!conf->conf_addcanondata)
{
(void) config_get(data, "AddCanonicalizedData",
&conf->conf_addcanondata,
sizeof conf->conf_addcanondata);
}
(void) config_get(data, "MTACommand",
&conf->conf_mtacommand,
sizeof conf->conf_mtacommand);
Expand Down Expand Up @@ -8829,6 +9001,7 @@ dkimf_config_setlib(struct dkimf_config *conf, char **err)
}

if (conf->conf_sendreports || conf->conf_keeptmpfiles ||
conf->conf_addcanondata ||
conf->conf_stricthdrs || conf->conf_blen || conf->conf_ztags ||
conf->conf_fixcrlf)
{
Expand All @@ -8844,7 +9017,8 @@ dkimf_config_setlib(struct dkimf_config *conf, char **err)
return FALSE;
}

if (conf->conf_sendreports || conf->conf_keeptmpfiles)
if (conf->conf_sendreports || conf->conf_keeptmpfiles ||
conf->conf_addcanondata)
opts |= DKIM_LIBFLAGS_TMPFILES;
if (conf->conf_keeptmpfiles)
opts |= DKIM_LIBFLAGS_KEEPFILES;
Expand Down Expand Up @@ -15102,6 +15276,10 @@ mlfi_eom(SMFICTX *ctx)
conf->conf_sendreports)
dkimf_sigreport(cc, conf, hostname);

/* expose canonicalized data for downstream DMARC reporting? */
if (conf->conf_addcanondata)
dkimf_add_canon_headers(cc, conf, ctx);

#ifdef _FFR_VBR
if (dkimf_valid_vbr(dfc))
{
Expand Down
20 changes: 20 additions & 0 deletions opendkim/opendkim.conf.5.in
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ Unless otherwise stated, Boolean values default to "false", integer values
default to 0, and string and dataset values default to being undefined.

.SH PARAMETERS
.TP
.I AddCanonicalizedData (Boolean)
If true, for every verified signature, adds
.I X-DKIM-Canonicalized-Header
and
.I X-DKIM-Canonicalized-Body
header fields containing the base64-encoded canonicalized header block and
body used to verify the signature, tagged with the signature's "d=" and
"s=" values. This is independent of the signature's own "r=" reporting
request and of whether it ultimately passed; it is intended for use by a
downstream DMARC filter needing the raw bytes for RFC6591 forensic report
fields defined by RFC9991, since this filter does not otherwise expose
them. Turning this on implies the same in-memory temporary file usage as
.I SendReports
and
.I KeepTemporaryFiles,
but never persists files to disk the way
.I KeepTemporaryFiles
does. Default "false".

.TP
.I AllowSHA1Only (Boolean)
Permit verify mode when only SHA1 support is available. RFC6376 requires
Expand Down
12 changes: 12 additions & 0 deletions opendkim/opendkim.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@

## CONFIGURATION OPTIONS

## AddCanonicalizedData { yes | no }
## default "no"
##
## If set, adds "X-DKIM-Canonicalized-Header" and "X-DKIM-Canonicalized-Body"
## headers for every verified signature, containing the base64-encoded
## canonicalized header block and body used to verify it, tagged with the
## signature's "d=" and "s=" values. Intended for use by a downstream
## DMARC filter that needs these bytes for RFC9991 forensic report fields.
## See opendkim.conf(5) for details.

# AddCanonicalizedData no

## AllowSHA1Only { yes | no }
## default "no"
##
Expand Down
3 changes: 2 additions & 1 deletion opendkim/tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ check_SCRIPTS = t-sign-ss t-sign-rs t-sign-rs-tables t-sign-rs-tables-bad \
t-verify-ss-ar-bad t-verify-ss-ar-admd-less \
t-dontsign t-peer \
t-lua-verify-tests t-sign-ss-macro t-sign-ss-macro-value \
t-sign-ss-macro-value-file t-verify-report \
t-sign-ss-macro-value-file t-verify-report t-verify-canondata \
t-sign-report t-conf-check t-verify-double-from

if LIVE_TESTS
Expand Down Expand Up @@ -94,6 +94,7 @@ EXTRA_DIST = \
t-peer t-peer.conf t-peer.list t-peer.lua \
t-verify-report t-verify-report.conf t-verify-report.txt \
t-verify-report.lua \
t-verify-canondata t-verify-canondata.conf t-verify-canondata.lua \
t-sign-atps t-sign-atps.conf t-sign-atps.lua \
t-verify-ss-atps t-verify-ss-atps.conf t-verify-ss-atps.lua \
t-conf-check t-conf-check.conf t-conf-check.keytable t-conf-check.lua \
Expand Down
11 changes: 11 additions & 0 deletions opendkim/tests/t-verify-canondata
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
#
#
# simple/simple verifying test with AddCanonicalizedData (should pass)

if [ x"$srcdir" = x"" ]
then
srcdir=`pwd`
fi

../../miltertest/miltertest $MILTERTESTFLAGS -s $srcdir/t-verify-canondata.lua
7 changes: 7 additions & 0 deletions opendkim/tests/t-verify-canondata.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# simple/simple verifying test with AddCanonicalizedData (should pass)

TestPublicKeys pubkeys
Mode v
On-BadSignature reject
Background No
AddCanonicalizedData yes
Loading
Loading