Skip to content

Commit 4beced1

Browse files
author
Dan Mahoney
committed
opendkim: add AddCanonicalizedData option for RFC 9991 forensic reporting
Adds an opt-in AddCanonicalizedData directive that emits X-DKIM-Canonicalized-Header/-Body milter headers, tagged with the signature's d=/s= values, for every verified signature. Reuses the canonicalization tmp-file capture and base64 encoding already built for the unrelated SendReports feature, so a downstream DMARC filter (e.g. OpenDMARC) can populate the RFC 6591 ARF fields required by RFC 9991 forensic reports without verifying DKIM itself.
1 parent fc8e13b commit 4beced1

9 files changed

Lines changed: 391 additions & 2 deletions

CHANGES-202605.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ A systematic audit of memory and resource leaks (issue #272) produced fixes acro
105105
- **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)
106106
- **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)
107107
- **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)
108+
- **`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.
108109

109110
---
110111

opendkim/opendkim-config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
/* config definition */
2525
struct configdef dkimf_config[] =
2626
{
27+
{ "AddCanonicalizedData", CONFIG_TYPE_BOOLEAN, FALSE },
2728
{ "AllowSHA1Only", CONFIG_TYPE_BOOLEAN, FALSE },
2829
{ "AlwaysAddARHeader", CONFIG_TYPE_BOOLEAN, FALSE },
2930
#ifdef _FFR_ATPS

opendkim/opendkim.c

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ struct dkimf_config
236236
_Bool conf_alwaysaddar; /* always add Auth-Results:? */
237237
_Bool conf_reqreports; /* request reports */
238238
_Bool conf_sendreports; /* signature failure reports */
239+
_Bool conf_addcanondata; /* add canonicalized data headers */
239240
_Bool conf_reqhdrs; /* required header checks */
240241
_Bool conf_authservidwithjobid; /* use jobids in A-R headers */
241242
_Bool conf_subdomains; /* sign subdomains */
@@ -740,6 +741,7 @@ void dkimf_sendprogress (const void *);
740741
sfsistat dkimf_setpriv (SMFICTX *, void *);
741742
sfsistat dkimf_setreply (SMFICTX *, char *, char *, char *);
742743
static void dkimf_sigreport (connctx, struct dkimf_config *, char *);
744+
static void dkimf_add_canon_headers (connctx, struct dkimf_config *, SMFICTX *);
743745
static void dkimf_log(struct dkimf_config *conf, int priority, const char *format, ...);
744746

745747
/* GLOBALS */
@@ -4200,6 +4202,169 @@ dkimf_add_ar_fields(struct msgctx *dfc, struct dkimf_config *conf,
42004202
}
42014203
}
42024204

4205+
/*
4206+
** DKIMF_CANON_HEADER_VALUE -- base64-encode a canonicalization tmp file
4207+
** into a malloc'd, NUL-terminated, folded
4208+
** header value
4209+
**
4210+
** Parameters:
4211+
** fd -- descriptor of the canonicalization tmp file (header or body)
4212+
** prefix -- tag prefix ("d=...; s=...; b=") to prepend on the same
4213+
** line before the base64 data starts
4214+
**
4215+
** Return value:
4216+
** A malloc'd C string ready to hand to dkimf_insheader(), or NULL on
4217+
** failure. Caller must free() the result.
4218+
*/
4219+
4220+
static char *
4221+
dkimf_canon_header_value(int fd, const char *prefix)
4222+
{
4223+
long len;
4224+
char *buf;
4225+
FILE *tmp;
4226+
4227+
if (fd < 0)
4228+
return NULL;
4229+
4230+
tmp = tmpfile();
4231+
if (tmp == NULL)
4232+
return NULL;
4233+
4234+
fputs(prefix, tmp);
4235+
4236+
dkimf_base64_encode_file(fd, tmp, 8, DKIM_HDRMARGIN,
4237+
(int) strlen(prefix));
4238+
4239+
len = ftell(tmp);
4240+
if (len <= 0)
4241+
{
4242+
fclose(tmp);
4243+
return NULL;
4244+
}
4245+
4246+
buf = (char *) malloc((size_t) len + 1);
4247+
if (buf == NULL)
4248+
{
4249+
fclose(tmp);
4250+
return NULL;
4251+
}
4252+
4253+
rewind(tmp);
4254+
if (fread(buf, 1, (size_t) len, tmp) != (size_t) len)
4255+
{
4256+
free(buf);
4257+
fclose(tmp);
4258+
return NULL;
4259+
}
4260+
buf[len] = '\0';
4261+
4262+
fclose(tmp);
4263+
4264+
return buf;
4265+
}
4266+
4267+
/*
4268+
** DKIMF_ADD_CANON_HEADERS -- add canonicalized header/body data as milter
4269+
** headers, for downstream DMARC forensic
4270+
** reporting (RFC 9991 / RFC 6591)
4271+
**
4272+
** Parameters:
4273+
** cc -- connection context
4274+
** conf -- configuration handle
4275+
** ctx -- milter context
4276+
**
4277+
** Return value:
4278+
** None.
4279+
**
4280+
** Notes:
4281+
** The canonicalized bytes come from the same tmp files the
4282+
** SendReports feature uses (see dkimf_sigreport()); they're
4283+
** populated for every verified signature regardless of whether it
4284+
** requested reporting ("r=y") or ultimately passed. This emits one
4285+
** X-DKIM-Canonicalized-Header/-Body pair per signature, tagged with
4286+
** its domain/selector so a downstream consumer (e.g. OpenDMARC) can
4287+
** match them against whichever signature it cares about without
4288+
** needing a numeric index.
4289+
*/
4290+
4291+
static void
4292+
dkimf_add_canon_headers(connctx cc, struct dkimf_config *conf, SMFICTX *ctx)
4293+
{
4294+
int c;
4295+
int nsigs = 0;
4296+
msgctx dfc;
4297+
DKIM_SIGINFO **sigs = NULL;
4298+
4299+
assert(cc != NULL);
4300+
assert(conf != NULL);
4301+
assert(ctx != NULL);
4302+
4303+
dfc = cc->cctx_msg;
4304+
4305+
assert(dfc->mctx_dkimv != NULL);
4306+
4307+
if (dkim_getsiglist(dfc->mctx_dkimv, &sigs, &nsigs) != DKIM_STAT_OK)
4308+
return;
4309+
4310+
for (c = 0; c < nsigs; c++)
4311+
{
4312+
int bfd = -1;
4313+
int hfd = -1;
4314+
char *domain;
4315+
char *selector;
4316+
char prefix[BUFRSZ];
4317+
char *hval;
4318+
4319+
if (dkim_sig_getreportinfo(dfc->mctx_dkimv, sigs[c],
4320+
&hfd, &bfd,
4321+
NULL, 0, NULL, 0,
4322+
NULL, 0, NULL) != DKIM_STAT_OK)
4323+
continue;
4324+
4325+
domain = (char *) dkim_sig_getdomain(sigs[c]);
4326+
selector = (char *) dkim_sig_getselector(sigs[c]);
4327+
4328+
snprintf(prefix, sizeof prefix, "d=%s; s=%s; b=",
4329+
domain != NULL ? domain : "",
4330+
selector != NULL ? selector : "");
4331+
4332+
if (hfd != -1)
4333+
{
4334+
hval = dkimf_canon_header_value(hfd, prefix);
4335+
if (hval != NULL)
4336+
{
4337+
if (dkimf_insheader(ctx, 0,
4338+
"X-DKIM-Canonicalized-Header",
4339+
hval) == MI_FAILURE)
4340+
{
4341+
dkimf_log(conf, LOG_ERR,
4342+
"%s: X-DKIM-Canonicalized-Header header add failed",
4343+
dfc->mctx_jobid);
4344+
}
4345+
free(hval);
4346+
}
4347+
}
4348+
4349+
if (bfd != -1)
4350+
{
4351+
hval = dkimf_canon_header_value(bfd, prefix);
4352+
if (hval != NULL)
4353+
{
4354+
if (dkimf_insheader(ctx, 0,
4355+
"X-DKIM-Canonicalized-Body",
4356+
hval) == MI_FAILURE)
4357+
{
4358+
dkimf_log(conf, LOG_ERR,
4359+
"%s: X-DKIM-Canonicalized-Body header add failed",
4360+
dfc->mctx_jobid);
4361+
}
4362+
free(hval);
4363+
}
4364+
}
4365+
}
4366+
}
4367+
42034368
/*
42044369
** DKIMF_DB_ERROR -- syslog errors related to db retrieval
42054370
**
@@ -6521,6 +6686,13 @@ dkimf_config_load(struct config *data, struct dkimf_config *conf,
65216686
&conf->conf_sendreports,
65226687
sizeof conf->conf_sendreports);
65236688
}
6689+
6690+
if (!conf->conf_addcanondata)
6691+
{
6692+
(void) config_get(data, "AddCanonicalizedData",
6693+
&conf->conf_addcanondata,
6694+
sizeof conf->conf_addcanondata);
6695+
}
65246696
(void) config_get(data, "MTACommand",
65256697
&conf->conf_mtacommand,
65266698
sizeof conf->conf_mtacommand);
@@ -8829,6 +9001,7 @@ dkimf_config_setlib(struct dkimf_config *conf, char **err)
88299001
}
88309002

88319003
if (conf->conf_sendreports || conf->conf_keeptmpfiles ||
9004+
conf->conf_addcanondata ||
88329005
conf->conf_stricthdrs || conf->conf_blen || conf->conf_ztags ||
88339006
conf->conf_fixcrlf)
88349007
{
@@ -8844,7 +9017,8 @@ dkimf_config_setlib(struct dkimf_config *conf, char **err)
88449017
return FALSE;
88459018
}
88469019

8847-
if (conf->conf_sendreports || conf->conf_keeptmpfiles)
9020+
if (conf->conf_sendreports || conf->conf_keeptmpfiles ||
9021+
conf->conf_addcanondata)
88489022
opts |= DKIM_LIBFLAGS_TMPFILES;
88499023
if (conf->conf_keeptmpfiles)
88509024
opts |= DKIM_LIBFLAGS_KEEPFILES;
@@ -15102,6 +15276,10 @@ mlfi_eom(SMFICTX *ctx)
1510215276
conf->conf_sendreports)
1510315277
dkimf_sigreport(cc, conf, hostname);
1510415278

15279+
/* expose canonicalized data for downstream DMARC reporting? */
15280+
if (conf->conf_addcanondata)
15281+
dkimf_add_canon_headers(cc, conf, ctx);
15282+
1510515283
#ifdef _FFR_VBR
1510615284
if (dkimf_valid_vbr(dfc))
1510715285
{

opendkim/opendkim.conf.5.in

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ Unless otherwise stated, Boolean values default to "false", integer values
4848
default to 0, and string and dataset values default to being undefined.
4949

5050
.SH PARAMETERS
51+
.TP
52+
.I AddCanonicalizedData (Boolean)
53+
If true, for every verified signature, adds
54+
.I X-DKIM-Canonicalized-Header
55+
and
56+
.I X-DKIM-Canonicalized-Body
57+
header fields containing the base64-encoded canonicalized header block and
58+
body used to verify the signature, tagged with the signature's "d=" and
59+
"s=" values. This is independent of the signature's own "r=" reporting
60+
request and of whether it ultimately passed; it is intended for use by a
61+
downstream DMARC filter needing the raw bytes for RFC6591 forensic report
62+
fields defined by RFC9991, since this filter does not otherwise expose
63+
them. Turning this on implies the same in-memory temporary file usage as
64+
.I SendReports
65+
and
66+
.I KeepTemporaryFiles,
67+
but never persists files to disk the way
68+
.I KeepTemporaryFiles
69+
does. Default "false".
70+
5171
.TP
5272
.I AllowSHA1Only (Boolean)
5373
Permit verify mode when only SHA1 support is available. RFC6376 requires

opendkim/opendkim.conf.sample

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@
2929

3030
## CONFIGURATION OPTIONS
3131

32+
## AddCanonicalizedData { yes | no }
33+
## default "no"
34+
##
35+
## If set, adds "X-DKIM-Canonicalized-Header" and "X-DKIM-Canonicalized-Body"
36+
## headers for every verified signature, containing the base64-encoded
37+
## canonicalized header block and body used to verify it, tagged with the
38+
## signature's "d=" and "s=" values. Intended for use by a downstream
39+
## DMARC filter that needs these bytes for RFC9991 forensic report fields.
40+
## See opendkim.conf(5) for details.
41+
42+
# AddCanonicalizedData no
43+
3244
## AllowSHA1Only { yes | no }
3345
## default "no"
3446
##

opendkim/tests/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ check_SCRIPTS = t-sign-ss t-sign-rs t-sign-rs-tables t-sign-rs-tables-bad \
1616
t-verify-ss-ar-bad t-verify-ss-ar-admd-less \
1717
t-dontsign t-peer \
1818
t-lua-verify-tests t-sign-ss-macro t-sign-ss-macro-value \
19-
t-sign-ss-macro-value-file t-verify-report \
19+
t-sign-ss-macro-value-file t-verify-report t-verify-canondata \
2020
t-sign-report t-conf-check t-verify-double-from
2121

2222
if LIVE_TESTS
@@ -94,6 +94,7 @@ EXTRA_DIST = \
9494
t-peer t-peer.conf t-peer.list t-peer.lua \
9595
t-verify-report t-verify-report.conf t-verify-report.txt \
9696
t-verify-report.lua \
97+
t-verify-canondata t-verify-canondata.conf t-verify-canondata.lua \
9798
t-sign-atps t-sign-atps.conf t-sign-atps.lua \
9899
t-verify-ss-atps t-verify-ss-atps.conf t-verify-ss-atps.lua \
99100
t-conf-check t-conf-check.conf t-conf-check.keytable t-conf-check.lua \

opendkim/tests/t-verify-canondata

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
#
3+
#
4+
# simple/simple verifying test with AddCanonicalizedData (should pass)
5+
6+
if [ x"$srcdir" = x"" ]
7+
then
8+
srcdir=`pwd`
9+
fi
10+
11+
../../miltertest/miltertest $MILTERTESTFLAGS -s $srcdir/t-verify-canondata.lua
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# simple/simple verifying test with AddCanonicalizedData (should pass)
2+
3+
TestPublicKeys pubkeys
4+
Mode v
5+
On-BadSignature reject
6+
Background No
7+
AddCanonicalizedData yes

0 commit comments

Comments
 (0)