|
| 1 | +# DMARCbis remaining work |
| 2 | + |
| 3 | +Tracks what's left across RFC 9989 (DMARC core), RFC 9990 (aggregate |
| 4 | +reporting), and RFC 9991 (failure reporting) — the documents that obsolete |
| 5 | +RFC 7489. Source of truth for the overall gap analysis is GitHub issue |
| 6 | +[#371](https://github.com/trusteddomainproject/OpenDMARC/issues/371) |
| 7 | +(`trusteddomainproject/OpenDMARC`); this file exists because that issue |
| 8 | +predates most of the work below and doesn't get rewritten as items close. |
| 9 | +Copies of the RFCs themselves are in the repo root (`rfc7489.txt`, |
| 10 | +`rfc9989.txt`, `rfc9990.txt`, `rfc9991.txt`). `DMARCBIS-WALK-NOTES.txt` and |
| 11 | +`DMARCBIS-EDITOR-EMAIL.txt` cover the DNS Tree Walk design questions |
| 12 | +specifically (all resolved) and are not duplicated here. |
| 13 | + |
| 14 | +## Done |
| 15 | + |
| 16 | +- **RFC 9989 DNS Tree Walk**: walk-mode selection (PSL/RFC7489/RFC9989/AUTO), |
| 17 | + configurable fallback, secondary alignment walk, `opendmarc-check` |
| 18 | + comparison tooling. All open questions in `DMARCBIS-WALK-NOTES.txt` |
| 19 | + resolved. (#430, #431, #433) |
| 20 | +- **RFC 9989 `t=`/`pct=`**: `t=` parsing, fetch accessor, and enforcement |
| 21 | + step-down (reject->quarantine->none). `pct=` deliberately kept for POLA, |
| 22 | + with `DMARCbisIgnorePct` for operators who want strict compliance. (#434) |
| 23 | +- **RFC 9990 aggregate reporting**: `np`/`testing`/`discovery_method` in |
| 24 | + `policy_published`, namespace bumped to `dmarc-2.0`, `<pct>` removed |
| 25 | + (branch `feat/rfc9990-aggregate-reporting`). |
| 26 | +- **RFC 9991 failure reporting**: `Identity-Alignment` and |
| 27 | + `DKIM-Domain`/`-Identity`/`-Selector` ARF headers, `ruf=` external |
| 28 | + destination verification + rate-limiting in `opendmarc-reports |
| 29 | + --forensic`, `psd=y` excludes `ruf=` (branch |
| 30 | + `feat/rfc9991-forensic-reporting`). Also restored PR #392's `rua=` |
| 31 | + destination verification, which had been silently lost, and fixed a |
| 32 | + NOERROR-vs-NXDOMAIN bug in it found via live testing. |
| 33 | +- **RFC 9991 `DKIM-Canonicalized-Header`/`-Body` ARF fields**: new |
| 34 | + `ReadCanonicalizedData` option reads `X-DKIM-Canonicalized-Header`/`-Body` |
| 35 | + staging headers from an upstream OpenDKIM running the new |
| 36 | + `AddCanonicalizedData` directive (`trusteddomainproject/OpenDKIM` PR |
| 37 | + #423), correlates them to whichever DKIM signature is blamed for a DMARC |
| 38 | + failure, and copies the base64 payload into the failure report, |
| 39 | + re-folded to this codebase's own ARF-body convention. Staging headers |
| 40 | + are always stripped before final delivery regardless of the setting. |
| 41 | + See CHANGES-202605.md for the full writeup; cross-project design |
| 42 | + history preserved below. |
| 43 | +- **RFC 9991 `SPF-DNS` ARF field**: new opt-in `LogSPFDNS` option (only |
| 44 | + meaningful with `SPFSelfValidate`). Real new code, as anticipated below |
| 45 | + -- libspf2 has a pluggable "DNS layer" system built exactly for this |
| 46 | + kind of interception (`SPF_dns_server_t`, chainable via `layer_below`); |
| 47 | + a new logging layer (`libopendmarc/opendmarc_spf_dns_log.c`) is spliced |
| 48 | + in above libspf2's own caching layer so it sees every SPF-record lookup |
| 49 | + regardless of cache hits, including domains reached via |
| 50 | + `include:`/`redirect=`, without altering evaluation behavior. This |
| 51 | + closes RFC 9991's SPF-DNS gap entirely; both fields originally listed |
| 52 | + as blocked in this section are now done. See CHANGES-202605.md for the |
| 53 | + full writeup. |
| 54 | + |
| 55 | +## Remaining |
| 56 | + |
| 57 | +### RFC 9990 aggregate reporting |
| 58 | + |
| 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. |
| 63 | +- **`policy_test_mode` reason type** (S3.1.6): a `<reason><type>` value a |
| 64 | + report record should carry when `t=y` caused a policy step-down. Directly |
| 65 | + adjacent to work already done — `t=` enforcement and `<testing>` in |
| 66 | + `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.) |
| 71 | +- **`error` element** (S3.1.1.3/S3.1.5): describes processing errors |
| 72 | + encountered while evaluating the DMARC Policy Record. Not implemented. |
| 73 | +- **DKIM signature priority + 100-signature cap** (S3.1.3): defines which |
| 74 | + signatures to include when a message has several (strict-aligned pass |
| 75 | + first, then relaxed, then others) and caps the list. Not implemented as |
| 76 | + specified. |
| 77 | +- **Extension mechanism** (S3.2, S5): `<extension>` at file level, |
| 78 | + namespaced elements at record level. Low priority — only matters if |
| 79 | + 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 | + |
| 84 | +### RFC 9991 failure reporting |
| 85 | + |
| 86 | +Both RFC 6591 fields originally tracked here (`DKIM-Canonicalized-Header`/ |
| 87 | +`-Body` and `SPF-DNS`) have shipped -- see "Done" above. Nothing remaining |
| 88 | +in this section. |
| 89 | + |
| 90 | +### Bugs found along the way |
| 91 | + |
| 92 | +- **`t-verify-authservid-jobid` test-ordering bug, fixed**: found while |
| 93 | + live-testing the `LogSPFDNS` work (that's what prompted actually |
| 94 | + getting a real-libspf2 build going, via `--with-spf |
| 95 | + --with-spf2-include=... --with-spf2-lib=...` -- the environment used |
| 96 | + for most of this DMARCbis work had been silently building against the |
| 97 | + built-in fallback SPF evaluator instead, an easy mistake since |
| 98 | + `--with-spf2` is a *different*, unrecognized flag that `configure` |
| 99 | + silently ignores rather than erroring on). Root-caused with `ktrace` |
| 100 | + (confirmed byte-perfect wire delivery of the macro) and `lldb` |
| 101 | + (breakpoints in both OpenDMARC and, via its exported-but-internal |
| 102 | + symbols, real libmilter itself): the test sends the `i` (job ID) macro |
| 103 | + scoped to the MAIL stage, then calls `mt.mailfrom()` without ever |
| 104 | + having called `mt.helo()` -- so miltertest auto-inserts a filler HELO |
| 105 | + command at that point, per its documented "fill in skipped steps" |
| 106 | + behavior. Real libmilter's `st_helo()` handler unconditionally clears |
| 107 | + any macros already stored for *later* protocol stages |
| 108 | + (`mi_clr_macros(ctx, CI_HELO+1)`, a correct, by-design safeguard |
| 109 | + against stale macros surviving from a prior transaction on the same |
| 110 | + connection) -- which wipes the just-stored MAIL-stage macro before it's |
| 111 | + ever read back, since it arrived earlier than the (delayed) HELO it's |
| 112 | + nominally supposed to follow. Not a bug in miltertest, real libmilter, |
| 113 | + or OpenDMARC's C code -- purely a test-script ordering issue, present |
| 114 | + identically in every test in this suite that sends the `i` macro this |
| 115 | + way (all of them), just never noticed elsewhere since no other test |
| 116 | + asserts on the delivered value. Fixed by adding an explicit |
| 117 | + `mt.helo()` call before the macro send in every affected test file |
| 118 | + (all eleven `t-verify-*.lua` files that send the `i` macro), not just |
| 119 | + the one that was actually failing. |
| 120 | +- **`--with-spf2` now a hard configure error**: the flag that caused the |
| 121 | + above (silently building against the built-in fallback SPF evaluator |
| 122 | + instead of real libspf2, since `--with-spf` is the real flag and |
| 123 | + `--with-spf2` doesn't exist) now fails `configure` immediately with a |
| 124 | + message pointing at the correct flags, instead of the default |
| 125 | + autoconf behavior of a warning + silent continue. Verified both that |
| 126 | + `--with-spf2` now errors and that the legitimate |
| 127 | + `--with-spf --with-spf2-include=... --with-spf2-lib=...` combination |
| 128 | + still configures `HAVE_SPF2_H`/`WITH_SPF` correctly. |
| 129 | +- **Four config directives silently rejected at startup**: fixed in the |
| 130 | + same session (`opendmarc-config.h`'s `dmarcf_config[]` validation table |
| 131 | + was missing `DMARCbisIgnorePct`, `DMARCbisWalkModeFallback`, |
| 132 | + `ReadCanonicalizedData`, and `LogSPFDNS`) -- see CHANGES-202605.md. Worth |
| 133 | + a standing reminder: this table is hand-maintained and separate from |
| 134 | + every `config_get()` call site, so a new directive silently fails at |
| 135 | + startup instead of a compile error until someone actually parses a |
| 136 | + config file containing it, which apparently hadn't happened for three |
| 137 | + 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. |
0 commit comments