@@ -3,7 +3,9 @@ Open questions and implementation notes for RFC 9989 DNS Tree Walk
33
44This file tracks uncertainties identified while reading RFC 9989 and
55designing the walk-mode refactor in libopendmarc. Each item references
6- the RFC section where the ambiguity was found.
6+ the RFC section where the ambiguity was found. Several items below were
7+ resolved by direct correspondence with an RFC 9989 co-author; see
8+ DMARCBIS-EDITOR-EMAIL.txt for the full question/answer thread.
79
810------------------------------------------------------------------------
9111. Walk exhaustion without psd= (SS 4.10, 4.10.1)
@@ -27,8 +29,15 @@ This could be read to imply that records-without-psd= found during the
2729walk are applicable as org-domain records when the walk exhausts, but
2830this is not stated explicitly.
2931
30- Status: deferred. Current implementation treats walk exhaustion without
31- psd= as no-record-found.
32+ Status: resolved. Per John Levine (RFC 9989 co-author), dmarc@ietf.org,
33+ 2026-06-20 (see DMARCBIS-EDITOR-EMAIL.txt): psd= is OPTIONAL (S 4.7) and
34+ defaults to psd=u; a record with no explicit psd= tag is a valid DMARC
35+ Policy Record, not an absent one. The status note previously here,
36+ describing the implementation as treating walk exhaustion without psd=
37+ as no-record-found, was inaccurate: query_dmarc_rfc9989_walk() already
38+ returns the last accumulated record (best_domain) when the walk exhausts
39+ without an explicit psd=y/psd=n, and only reports no-record when zero
40+ valid records were found at any level (found_any == 0).
3241
3342------------------------------------------------------------------------
34432. "might" in secondary alignment walks (S 4.10.2)
@@ -48,24 +57,60 @@ tree walks for each authenticated identifier to determine its
4857organizational domain for relaxed alignment comparison. A strict reading
4958permits using only the org domain found during policy discovery.
5059
51- Status: secondary alignment walks not implemented in this pass. Relaxed
52- alignment continues to compare org domains using the single walk
53- performed during policy discovery.
60+ Status: resolved. Per John Levine (RFC 9989 co-author), dmarc@ietf.org,
61+ 2026-06-20 (see DMARCBIS-EDITOR-EMAIL.txt): the tree walk for Identifier
62+ Alignment Evaluation starts at whichever of the Author Domain, the
63+ SPF-Authenticated Identifier, or the DKIM-Authenticated Identifier failed
64+ strict alignment -- it is definitely one of those three, not an optional
65+ refinement. Reusing the single policy-discovery walk's org domain for
66+ all three identifiers was confirmed non-compliant.
67+
68+ Fixed: opendmarc_policy_check_alignment() now reduces the SPF/DKIM
69+ identifier (the 'tld' argument) to its own organizational domain via
70+ reduce_to_org_domain_for_alignment(), which dispatches on the configured
71+ walk_mode/walk_mode_fallback exactly like policy discovery does (PSL and
72+ AUTO keep the pre-existing direct opendmarc_get_tld() lookup; RFC7489 and
73+ RFC9989 run the same DNS walk used for policy discovery, started at the
74+ identifier instead of the Author Domain, via a throwaway DMARC_POLICY_T
75+ so the caller's real policy context is untouched). Covered by the
76+ secondary-alignment-walk cases in test_dmarc_walk.c.
5477
5578------------------------------------------------------------------------
56- 3. Multiple DMARC records at one level (SS 4.10 steps 2 and 6)
79+ 3. Multiple DMARC records at one level (SS 4.10 steps 2 and 6; RFC 7489
80+ S 6.6.3 step 5)
5781------------------------------------------------------------------------
5882
59- The walk algorithm says: "if multiple DMARC Policy Records are returned
60- for a single target, they are all discarded."
61-
62- The current dmarc_dns_get_record() implementation returns only the first
63- DMARC TXT record found at a name; it cannot detect the presence of
64- additional records. Full compliance with this requirement would need a
65- DNS-layer change to count matching records before returning.
66-
67- Status: known gap. Current implementation returns the first record;
68- multi-record discard is not enforced.
83+ Both RFC 7489 and RFC 9989 require that when multiple valid DMARC records
84+ are found at a single DNS name, they are all discarded and policy
85+ discovery terminates without applying DMARC.
86+
87+ RFC 7489 S 6.6.3 step 5: "If the remaining set contains multiple records
88+ or no records, policy discovery terminates and DMARC processing is not
89+ applied to this message."
90+
91+ RFC 9989 SS 4.10 steps 2 and 6: "If multiple DMARC Policy Records are
92+ returned for a single target, they are all discarded."
93+
94+ Status: fixed. dmarc_dns_get_record() scans all TXT records in the
95+ answer (and the fake-DNS test table) before returning, counts those
96+ containing "v=DMARC", and returns the record only when exactly one is
97+ found; zero or more than one yields NO_DATA / DMARC_DNS_ERROR_NO_RECORD.
98+ Covered by a fake-DNS test case (_dmarc.multi.example, two v=DMARC1
99+ entries) in test_subdomain_fallback.c.
100+
101+ Note on wording: RFC 7489 step 5 says discovery "terminates" on multiple
102+ records, while RFC 9989 steps 2/6 say the records are "discarded" and
103+ (per step 7) the walk continues to the next label. This is not a
104+ behavioral conflict requiring different handling: RFC 7489's algorithm
105+ has only two levels (From domain, then Organizational Domain), so its
106+ "terminate" is just "no more levels to try" -- the same outcome the 9989
107+ walk reaches when it runs out of labels. query_dmarc_rfc9989_walk()
108+ treats any NULL return from dmarc_query_at() (whether caused by zero
109+ records or by the multiple-records discard) identically: strip the
110+ leftmost label and continue, since that "no usable record at this level,
111+ try the next one" loop already existed for the plain-absent case. The
112+ fix at the DNS layer therefore satisfies both RFCs' wording without any
113+ walk-level changes.
69114
70115------------------------------------------------------------------------
711164. "One label below" psd=y with the 8-label skip (SS 4.10, 4.10.2)
@@ -81,9 +126,13 @@ the level immediately below the psd=y domain. "One label below in the
81126DNS hierarchy" must therefore be reconstructed from the original starting
82127domain, not inferred from the walk's previous step.
83128
84- Status: tracked. Implementation computes "one label below" by finding
85- the child of the psd=y domain that is an ancestor of the original
86- starting domain.
129+ Status: confirmed correct. Per John Levine (RFC 9989 co-author),
130+ dmarc@ietf.org, 2026-06-20 (see DMARCBIS-EDITOR-EMAIL.txt): reconstructing
131+ "one label below" from the original starting domain is the intended
132+ approach; divergence from the walk's literal previous step in
133+ 8-label-skip cases is expected to be rare to the point of contrived.
134+ Implementation computes "one label below" by finding the child of the
135+ psd=y domain that is an ancestor of the original starting domain.
87136
88137------------------------------------------------------------------------
891385. Fate of records-without-psd= collected during the walk (S 4.10.2)
@@ -104,10 +153,14 @@ the name with the fewest number of labels") only makes sense if multiple
104153records were collected. This implies the walk accumulates all valid
105154single records encountered, stopping only when psd= is hit.
106155
107- Status: walk implementation accumulates records. The selection process
108- from 4.10.2 is applied post-walk. Policy discovery (4.10.1) uses the
109- psd= stopping record when found; falls back to fewest-labels selection
110- when the walk exhausts without psd=.
156+ Status: confirmed correct. Per John Levine (RFC 9989 co-author),
157+ dmarc@ietf.org, 2026-06-20 (see DMARCBIS-EDITOR-EMAIL.txt): the walk
158+ accumulates every valid record it sees regardless of an explicit psd=
159+ tag, confirming the S 4.10.2 fewest-labels selection should run over all
160+ accumulated records. Walk implementation accumulates records; the
161+ selection process from 4.10.2 is applied post-walk. Policy discovery
162+ (4.10.1) uses the psd= stopping record when found; falls back to
163+ fewest-labels selection when the walk exhausts without psd=.
111164
112165------------------------------------------------------------------------
1131666. AUTO mode fallback scope when a PSL boundary is found but unqueryable
@@ -126,3 +179,32 @@ identified and queried unsuccessfully.
126179
127180Status: intentional. AUTO favors finding a record over strict adherence
128181to the PSL boundary when one was identified but had nothing there.
182+
183+ ------------------------------------------------------------------------
184+ 7. Configurable default/fallback walk strategy (not an RFC ambiguity --
185+ an operational requirement raised independently of the items above)
186+ ------------------------------------------------------------------------
187+
188+ RFC 9989 deprecates PSL-based discovery as the recommended mechanism, but
189+ RFC 9990's aggregate-report schema still defines policy_published's
190+ discovery_method as exactly { psl, treewalk } -- meaning reports must be
191+ able to truthfully declare PSL as the discovery method, so PSL has to
192+ remain a live, selectable strategy rather than something to delete once
193+ the tree walk works. Operators transitioning between the two also need
194+ to choose, independently, which strategy is the default and which (if
195+ any) is the fallback when the default finds nothing, rather than being
196+ stuck with AUTO's fixed PSL-then-RFC7489 combinator.
197+
198+ Status: implemented. walk_mode_fallback (OPENDMARC_LIB_T) /
199+ DMARCbisWalkModeFallback (opendmarc.conf) lets any one of PSL, RFC7489,
200+ or RFC9989 serve as either the primary (DMARCbisWalkMode) or fallback
201+ strategy. Consulted by opendmarc_policy_query_dmarc() for policy
202+ discovery and by opendmarc_policy_check_alignment() (via
203+ reduce_to_org_domain_for_alignment(), item 2 above) for the secondary
204+ alignment walk, so both code paths agree on strategy. AUTO's own
205+ PSL-then-RFC7489 combinator (item 6 above) is unaffected: walk_mode_fallback
206+ is only consulted when DMARCbisWalkMode names a concrete strategy, not
207+ when it is Auto. Defaults to no fallback (OPENDMARC_WALK_MODE_NONE,
208+ which aliases AUTO's zero value so a zero-initialized OPENDMARC_LIB_T --
209+ the common case -- never silently enables a fallback nobody configured).
210+ Covered by test_dmarc_walk.c.
0 commit comments