Skip to content

Commit 228594e

Browse files
authored
feat: RFC 9990 generator/pass-disposition + RFC 9989 obsolete-tag cleanup (#440)
- report_metadata now includes <generator>, identifying the report-generating software, matching the string already used for --version output and the X-Mailer header. - opendmarc-reports strips the obsolete "!NNNk" RUA size-suffix (obs-dmarc-report-size) from destination addresses but no longer enforces it as a per-destination size cap, per RFC 9989's ABNF ("reporters should ignore" it). - Removed rf=/ri= parsing, the now-dead DMARC_POLICY_T fields, and the orphaned (never publicly declared) opendmarc_policy_fetch_rf() -- both tags were removed from the DMARC record format by RFC 9989. - ActionDispositionType now emits "pass" (RFC 9990 S3.1.1.9) alongside none/quarantine/reject: new DMARC_RESULT_PASS is set when alignment passed under an enforcing (reject/quarantine) policy, keyed off the unadjusted published p/sp so t=y step-down doesn't affect it, per the existing rule that t= doesn't affect report generation.
1 parent a04f296 commit 228594e

11 files changed

Lines changed: 74 additions & 247 deletions

DMARCBIS-REMAINING-WORK.md

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,50 @@ specifically (all resolved) and are not duplicated here.
2020
- **RFC 9989 `t=`/`pct=`**: `t=` parsing, fetch accessor, and enforcement
2121
step-down (reject->quarantine->none). `pct=` deliberately kept for POLA,
2222
with `DMARCbisIgnorePct` for operators who want strict compliance. (#434)
23+
- **`!NNNk` RUA size-suffix syntax**: RFC 9989's ABNF marks this obsolete
24+
(`obs-dmarc-uri`/`obs-dmarc-report-size`) and says reporters MUST ignore
25+
it. Unlike the `pct=` POLA call above, `opendmarc-reports` now drops the
26+
enforcement PR #392 had restored -- the suffix is still stripped from
27+
the destination address so delivery isn't broken, but it no longer
28+
overrides `report_maxbytes`.
2329
- **RFC 9990 aggregate reporting**: `np`/`testing`/`discovery_method` in
2430
`policy_published`, namespace bumped to `dmarc-2.0`, `<pct>` removed
2531
(branch `feat/rfc9990-aggregate-reporting`).
32+
- **RFC 9990 `generator` element** (S3.1.1.3): `report_metadata` now
33+
includes `<generator>` identifying the report-generating software
34+
(`$progname v$version`), matching the string already used for
35+
`--version` output and the `X-Mailer` header. (Unrelated: the separate
36+
`contrib/dmarc-report-totext.pl` *consumer* tool already parses this
37+
field from other senders' reports — that's reading, not writing.)
38+
- **`rf=`/`ri=` cleanup**: both tags were removed from the DMARC record
39+
format by RFC 9989. Removed the parsing (`opendmarc_policy.c`), the
40+
now-dead `DMARC_POLICY_T` fields (`rf`, `ri`), the orphaned
41+
`opendmarc_policy_fetch_rf()` (never declared in the public header, so
42+
unreachable by consumers anyway) and its stale doc page, and the
43+
`RF=`/`RI=` lines from `opendmarc_policy_to_buf()`'s dump. Unrecognized
44+
tags are already silently ignored by the parser per spec, so `rf=`/`ri=`
45+
now fall through to that same path rather than being validated.
46+
Verified via a real `./configure && make check` on quark (this sandbox
47+
lacks milter headers) — all 10 `libopendmarc` tests pass, including
48+
`test_dmarc_parse`'s negative cases updated to expect `DMARC_PARSE_OKAY`
49+
now that these are unknown tags rather than validated ones.
50+
- **`pass` disposition value** (S3.1.1.9): `ActionDispositionType` now
51+
emits `pass` alongside `none`/`quarantine`/`reject`. New
52+
`DMARC_RESULT_PASS` (`opendmarc.h`) is set in `opendmarc.c`'s
53+
`enforce_policy` switch specifically when alignment passed *and* the
54+
unadjusted published `p`/`sp` (whichever applies) was `reject` or
55+
`quarantine` -- under `p=none`/`sp=none` the disposition stays `none`,
56+
since RFC 9990 reserves `pass` for "passing DMARC w/enforcing policy".
57+
Deliberately keyed off the unadjusted policy rather than the
58+
`t=y`-downgraded `enforce_policy`, matching the existing rule that `t=`
59+
doesn't affect report generation. Also added a `dis=pass` case to the
60+
Authentication-Results `dis=` tag mapping for consistency with the
61+
existing `dmarc=pass` `aresult`. Flows through `messages.disp` (a plain
62+
unconstrained `TINYINT`, so no schema migration) into
63+
`opendmarc-reports.in`'s disposition mapping. Verified with a clean
64+
`./configure && make check` on quark: full project builds with no
65+
warnings, `libopendmarc` suite still 10/10 (unaffected, but a good
66+
regression signal since this touches the same enforcement path).
2667
- **RFC 9991 failure reporting**: `Identity-Alignment` and
2768
`DKIM-Domain`/`-Identity`/`-Selector` ARF headers, `ruf=` external
2869
destination verification + rate-limiting in `opendmarc-reports
@@ -56,18 +97,10 @@ specifically (all resolved) and are not duplicated here.
5697

5798
### RFC 9990 aggregate reporting
5899

59-
- **`pass` disposition value** (S3.1.1.9): `ActionDispositionType` now
60-
includes `pass` (message passed DMARC under an *enforcing* policy)
61-
alongside `none`/`quarantine`/`reject`. Current code only ever emits the
62-
original three. Not touched by the `np`/`testing`/`discovery_method` work.
63100
- **`policy_test_mode` reason type** (S3.1.6): a `<reason><type>` value a
64101
report record should carry when `t=y` caused a policy step-down. Directly
65102
adjacent to work already done — `t=` enforcement and `<testing>` in
66103
`policy_published` both shipped, but this per-record annotation didn't.
67-
- **`generator` element** (S3.1.1.3): identifies the report-generating
68-
software. Not implemented in `opendmarc-reports`. (Unrelated: the
69-
separate `contrib/dmarc-report-totext.pl` *consumer* tool already parses
70-
this field from other senders' reports — that's reading, not writing.)
71104
- **`error` element** (S3.1.1.3/S3.1.5): describes processing errors
72105
encountered while evaluating the DMARC Policy Record. Not implemented.
73106
- **DKIM signature priority + 100-signature cap** (S3.1.3): defines which
@@ -77,10 +110,6 @@ specifically (all resolved) and are not duplicated here.
77110
- **Extension mechanism** (S3.2, S5): `<extension>` at file level,
78111
namespaced elements at record level. Low priority — only matters if
79112
extensions are actually adopted by report consumers.
80-
- **`rf=`/`ri=` cleanup**: both tags were removed from the DMARC record
81-
format by RFC 9989. OpenDMARC still parses them into unused
82-
`DMARC_POLICY_T` fields (`rf`, `ri`). Minor; safe to remove.
83-
84113
### RFC 9991 failure reporting
85114

86115
Both RFC 6591 fields originally tracked here (`DKIM-Canonicalized-Header`/
@@ -135,14 +164,3 @@ in this section.
135164
startup instead of a compile error until someone actually parses a
136165
config file containing it, which apparently hadn't happened for three
137166
of these four since they were introduced.
138-
139-
### Open decision, not just missing code
140-
141-
- **`!NNNk` RUA size-suffix syntax**: RFC 9990 Appendix C says this syntax
142-
is obsolete and receivers (i.e. OpenDMARC, acting as report generator)
143-
MUST ignore it. The restored PR #392 code (`check_size_restriction`)
144-
actively parses and enforces it, faithfully reproducing pre-regression
145-
behavior rather than the RFC 9990 text. Same shape of question as the
146-
`pct=` POLA decision: keep honoring it for senders who rely on it, or
147-
drop it now that it's back in front of you. Needs an explicit call, not
148-
a default.

libopendmarc/dmarc.h.in

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ extern "C" {
4747
#define DMARC_RECORD_P_NONE ('n') /* p and sp */
4848
#define DMARC_RECORD_P_QUARANTINE ('q') /* p and sp */
4949
#define DMARC_RECORD_P_REJECT ('r') /* p and sp */
50-
#define DMARC_RECORD_RF_UNSPECIFIED (0x0) /* rf, a bitmap */
51-
#define DMARC_RECORD_RF_AFRF (0x1) /* rf, a bitmap */
52-
#define DMARC_RECORD_RF_IODEF (0x2) /* rf, a bitmap */
5350
#define DMARC_RECORD_FO_UNSPECIFIED (0x0) /* fo, a bitmap */
5451
#define DMARC_RECORD_FO_0 (0x1) /* fo, a bitmap */
5552
#define DMARC_RECORD_FO_1 (0x2) /* fo, a bitmap */

libopendmarc/docs/Makefile.am

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ dist_doc_DATA = index.html \
1313
opendmarc_policy_fetch_fo.html \
1414
opendmarc_policy_fetch_p.html \
1515
opendmarc_policy_fetch_pct.html \
16-
opendmarc_policy_fetch_rf.html \
1716
opendmarc_policy_fetch_rua.html \
1817
opendmarc_policy_fetch_ruf.html \
1918
opendmarc_policy_fetch_sp.html \

libopendmarc/docs/index.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,6 @@ <h2>Functions</h2>
160160
<td> <a href="opendmarc_policy_fetch_fo.html"> <tt>opendmarc_policy_fetch_fo()</tt> </a> </td>
161161
<td> Fetch a bitmap that shows how/when that RUF reports should be sent. </td>
162162
</tr>
163-
<tr>
164-
<td> <a href="opendmarc_policy_fetch_rf.html"> <tt>opendmarc_policy_fetch_rf()</tt> </a> </td>
165-
<td> Fetch a bitmap that shows which format (arfrf versus iodef) should be used. </td>
166-
</tr>
167163
<tr>
168164
<td> <a href="opendmarc_policy_fetch_alignment.html"> <tt>opendmarc_policy_fetch_alignment()</tt> </a> </td>
169165
<td> Fetch the result of SPF and DKIM alignment checks.

libopendmarc/docs/opendmarc_policy_fetch_rf.html

Lines changed: 0 additions & 85 deletions
This file was deleted.

libopendmarc/opendmarc_internal.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,6 @@ typedef struct dmarc_policy_t {
174174
int psd; /* RFC 9989: DMARC_RECORD_PSD_* */
175175
int t; /* RFC 9989: DMARC_RECORD_T_*, test mode */
176176
int pct;
177-
int rf;
178-
uint32_t ri;
179177
int rua_cnt;
180178
u_char ** rua_list;
181179
int ruf_cnt;

libopendmarc/opendmarc_policy.c

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,7 +1336,6 @@ opendmarc_policy_parse_dmarc(DMARC_POLICY_T *pctx, u_char *domain, u_char *recor
13361336
* Set the defaults to detect missing required items.
13371337
*/
13381338
pctx->pct = -1;
1339-
pctx->ri = -1;
13401339

13411340
(void) memset((char *)copy, '\0', sizeof copy);
13421341
(void) strlcpy((char *)copy, (char *)record, sizeof copy);
@@ -1498,65 +1497,6 @@ opendmarc_policy_parse_dmarc(DMARC_POLICY_T *pctx, u_char *domain, u_char *recor
14981497
return DMARC_PARSE_ERROR_BAD_VALUE;
14991498
}
15001499
}
1501-
else if (strcasecmp((char *)cp, "ri") == 0)
1502-
{
1503-
char *xp;
1504-
1505-
for (xp = (char *)vp; *xp != '\0'; ++xp)
1506-
{
1507-
if (! isdigit((int)*xp))
1508-
return DMARC_PARSE_ERROR_BAD_VALUE;
1509-
}
1510-
errno = 0;
1511-
pctx->ri = strtoul((char *)vp, NULL, 10);
1512-
if (errno == EINVAL || errno == ERANGE)
1513-
{
1514-
return DMARC_PARSE_ERROR_BAD_VALUE;
1515-
}
1516-
}
1517-
else if (strcasecmp((char *)cp, "rf") == 0)
1518-
{
1519-
char *xp, *yp;
1520-
1521-
/*
1522-
* The list may be a comma delimilted list of choices.
1523-
*/
1524-
for (xp = (char *)vp; *xp != '\0'; )
1525-
{
1526-
u_char xbuf[32];
1527-
1528-
yp = strchr(xp, ',');
1529-
if (yp != NULL)
1530-
*yp = '\0';
1531-
1532-
xp = (char *)opendmarc_util_cleanup((u_char *)xp, xbuf, sizeof xbuf);
1533-
if (xp != NULL && strlen((char *)xp) > 0)
1534-
{
1535-
/*
1536-
* Be generous. Accept, for example, "rf=a, aspf=afrf or any
1537-
* left match of "afrf".
1538-
*/
1539-
if (strncasecmp((char *)xp, "afrf", strlen((char *)xp)) == 0)
1540-
pctx->rf |= DMARC_RECORD_RF_AFRF;
1541-
else if (strncasecmp((char *)xp, "iodef", strlen((char *)xp)) == 0)
1542-
pctx->aspf |= DMARC_RECORD_RF_IODEF;
1543-
else
1544-
{
1545-
/* A totaly unknown value */
1546-
return DMARC_PARSE_ERROR_BAD_VALUE;
1547-
}
1548-
}
1549-
else
1550-
{
1551-
return DMARC_PARSE_ERROR_BAD_VALUE;
1552-
}
1553-
1554-
if (yp != NULL)
1555-
xp = yp+1;
1556-
else
1557-
break;
1558-
}
1559-
}
15601500
else if (strcasecmp((char *)cp, "rua") == 0)
15611501
{
15621502
char *xp, *yp;
@@ -1696,10 +1636,6 @@ opendmarc_policy_parse_dmarc(DMARC_POLICY_T *pctx, u_char *domain, u_char *recor
16961636
pctx->aspf = DMARC_RECORD_A_RELAXED;
16971637
if (pctx->pct < 0)
16981638
pctx->pct = 100;
1699-
if (pctx->rf == DMARC_RECORD_RF_UNSPECIFIED)
1700-
pctx->rf = DMARC_RECORD_RF_AFRF;
1701-
if (pctx->ri == -1)
1702-
pctx->ri = 86400;
17031639
if (pctx->fo == DMARC_RECORD_FO_UNSPECIFIED)
17041640
pctx->fo = DMARC_RECORD_FO_0;
17051641

@@ -1992,20 +1928,6 @@ opendmarc_policy_fetch_fo(DMARC_POLICY_T *pctx, int *fo)
19921928
return DMARC_PARSE_OKAY;
19931929
}
19941930

1995-
OPENDMARC_STATUS_T
1996-
opendmarc_policy_fetch_rf(DMARC_POLICY_T *pctx, int *rf)
1997-
{
1998-
if (pctx == NULL)
1999-
return DMARC_PARSE_ERROR_NULL_CTX;
2000-
if (rf == NULL)
2001-
return DMARC_PARSE_ERROR_EMPTY;
2002-
if (pctx->ruf_list == NULL)
2003-
*rf = DMARC_RECORD_RF_UNSPECIFIED;
2004-
else
2005-
*rf = pctx->rf;
2006-
return DMARC_PARSE_OKAY;
2007-
}
2008-
20091931
/**************************************************************************************************
20101932
** OPENDMARC_POLICY_FETCH_UTILIZED_DOMAIN -- Return domain used to get the dmarc record
20111933
** Either the From: domain or the organizational domain
@@ -2462,31 +2384,6 @@ opendmarc_policy_to_buf(DMARC_POLICY_T *pctx, char *buf, size_t buflen)
24622384
if (strlcat(buf, nbuf, buflen) >= buflen) return E2BIG;
24632385
if (strlcat(buf, "\n", buflen) >= buflen) return E2BIG;
24642386

2465-
if (strlcat(buf, "RF=", buflen) >= buflen) return E2BIG;
2466-
if (pctx->rf == 0)
2467-
{
2468-
if (strlcat(buf, "UNSPECIFIED", buflen) >= buflen) return E2BIG;
2469-
}
2470-
if ((pctx->rf&DMARC_RECORD_RF_AFRF) != 0)
2471-
{
2472-
if (strlcat(buf, "AFRF", buflen) >= buflen) return E2BIG;
2473-
}
2474-
if ((pctx->rf&DMARC_RECORD_RF_IODEF) != 0 &&
2475-
(pctx->rf&DMARC_RECORD_RF_AFRF) != 0)
2476-
{
2477-
if (strlcat(buf, ",", buflen) >= buflen) return E2BIG;
2478-
}
2479-
if ((pctx->rf&DMARC_RECORD_RF_IODEF) != 0)
2480-
{
2481-
if (strlcat(buf, "IODEF", buflen) >= buflen) return E2BIG;
2482-
}
2483-
if (strlcat(buf, "\n", buflen) >= buflen) return E2BIG;
2484-
2485-
if (strlcat(buf, "RI=", buflen) >= buflen) return E2BIG;
2486-
(void) snprintf(nbuf, sizeof nbuf, "%d", pctx->ri);
2487-
if (strlcat(buf, nbuf, buflen) >= buflen) return E2BIG;
2488-
if (strlcat(buf, "\n", buflen) >= buflen) return E2BIG;
2489-
24902387
if (strlcat(buf, "RUA=", buflen) >= buflen) return E2BIG;
24912388
for (i = 0; i < pctx->rua_cnt; ++i)
24922389
{

libopendmarc/tests/test_dmarc_parse.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ main(int argc, char **argv)
2727
/* 9 */ {"v=DMARC1; p=none; sp=bob;", DMARC_PARSE_ERROR_BAD_VALUE},
2828
/* 10 */ {"v=DMARC1; p=none; adkim=bob;", DMARC_PARSE_ERROR_BAD_VALUE},
2929
/* 11 */ {"v=DMARC1; p=none; aspf=bob;", DMARC_PARSE_ERROR_BAD_VALUE},
30-
/* 12 */ {"v=DMARC1; p=none; rf=bob;", DMARC_PARSE_ERROR_BAD_VALUE},
31-
/* 13 */ {"v=DMARC1; p=none; ri=bob;", DMARC_PARSE_ERROR_BAD_VALUE},
30+
/* 12 */ {"v=DMARC1; p=none; rf=bob;", DMARC_PARSE_OKAY}, /* rf= removed by RFC 9989; now an ignored unknown tag */
31+
/* 13 */ {"v=DMARC1; p=none; ri=bob;", DMARC_PARSE_OKAY}, /* ri= removed by RFC 9989; now an ignored unknown tag */
3232
/* 14 */ {"v=DMARC1; p=none; pct=500;", DMARC_PARSE_ERROR_BAD_VALUE},
3333
/* 15 */ {"v=DMARC1; pct=100;", DMARC_PARSE_ERROR_NO_REQUIRED_P},
3434
/* 16 */ {"v=DMARC1; p=none; rua=ftp://abuse.com", DMARC_PARSE_OKAY},
3535
/* 17 */ {"v=DMARC1; p=none; ruf=mailto://abuse.com", DMARC_PARSE_OKAY},
3636
/* 18 */ {"v=DMARC1; p=none; ruf=mailto://abuse.com; foo=bar; buzz=happy;", DMARC_PARSE_OKAY},
37-
/* 19 */ {"v=DMARC1; p=none; rf=000000000000000000000000000000000", DMARC_PARSE_ERROR_BAD_VALUE},
37+
/* 19 */ {"v=DMARC1; p=none; rf=000000000000000000000000000000000", DMARC_PARSE_OKAY}, /* rf= removed by RFC 9989; now an ignored unknown tag */
3838
/* 20 */ {"v=DMARC1; p=reject; t=y;", DMARC_PARSE_OKAY},
3939
/* 21 */ {"v=DMARC1; p=reject; t=n;", DMARC_PARSE_OKAY},
4040
/* 22 */ {"v=DMARC1; p=reject; t=bob;", DMARC_PARSE_ERROR_BAD_VALUE},

0 commit comments

Comments
 (0)