Skip to content
Open
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
25 changes: 18 additions & 7 deletions opendmarc/opendmarc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2344,6 +2344,7 @@ mlfi_eom(SMFICTX *ctx)
u_char *bang;
u_char **ruv;
unsigned char header[MAXHEADER + 1];
unsigned char authservid_hdr[MAXHOSTNAMELEN + BUFRSZ + 4];
unsigned char addrbuf[BUFRSZ + 1];
unsigned char replybuf[BUFRSZ + 1];
unsigned char pdomain[MAXHOSTNAMELEN + 1];
Expand Down Expand Up @@ -2401,6 +2402,18 @@ mlfi_eom(SMFICTX *ctx)
}
}

/*
* Build the authserv-id string for A-R headers. Per RFC 8601 + RFC 2045,
* "/" is a tspecial and invalid in an unquoted token, so quote the whole
* value when a job ID is appended.
*/
if (conf->conf_authservidwithjobid && dfc->mctx_jobid[0] != '\0')
snprintf(authservid_hdr, sizeof authservid_hdr,
"\"%s/%s\"", authservid, dfc->mctx_jobid);
else
snprintf(authservid_hdr, sizeof authservid_hdr,
"%s", authservid);

/* ensure there was a From field */
from = dmarcf_findheader(dfc, "From", 0);

Expand Down Expand Up @@ -3129,13 +3142,13 @@ mlfi_eom(SMFICTX *ctx)
{
snprintf(header, sizeof header,
"%s; spf=%s smtp.helo=%s",
authservid, pass_fail, use_domain);
authservid_hdr, pass_fail, use_domain);
}
else
{
snprintf(header, sizeof header,
"%s; spf=%s smtp.mailfrom=%s",
authservid, pass_fail, use_domain);
authservid_hdr, pass_fail, use_domain);
}

if (dmarcf_insheader(ctx, 1, AUTHRESULTSHDR,
Expand Down Expand Up @@ -3201,7 +3214,7 @@ mlfi_eom(SMFICTX *ctx)

snprintf(header, sizeof header,
"%s; dmarc=permerror header.from=%s",
authservid, dfc->mctx_fromdomain);
authservid_hdr, dfc->mctx_fromdomain);

if (dmarcf_insheader(ctx, 1, AUTHRESULTSHDR,
header) == MI_FAILURE)
Expand Down Expand Up @@ -3746,10 +3759,8 @@ mlfi_eom(SMFICTX *ctx)
if (ret != SMFIS_TEMPFAIL && ret != SMFIS_REJECT)
{
snprintf(header, sizeof header,
"%s%s%s; dmarc=%s (p=%s dis=%s) header.from=%s",
authservid,
conf->conf_authservidwithjobid ? "/" : "",
conf->conf_authservidwithjobid ? dfc->mctx_jobid : "",
"%s; dmarc=%s (p=%s dis=%s) header.from=%s",
authservid_hdr,
aresult, apolicy, adisposition, dfc->mctx_fromdomain);

if (dmarcf_insheader(ctx, 1, AUTHRESULTSHDR,
Expand Down
6 changes: 4 additions & 2 deletions opendmarc/tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ if LIVE_TESTS
check_SCRIPTS += t-verify-multi-from-reject t-verify-multi-from-malformed \
t-verify-nodata t-verify-nodata-reject \
t-verify-unspec t-verify-received-spf-good t-verify-received-spf-bad \
t-verify-self-spf
t-verify-self-spf t-verify-authservid-jobid
endif

TESTS = $(check_SCRIPTS)
Expand All @@ -24,6 +24,8 @@ EXTRA_DIST = \
t-verify-multi-from-reject t-verify-multi-from-reject.conf \
t-verify-multi-from-reject.lua \
t-verify-multi-from-malformed t-verify-multi-from-malformed.conf \
t-verify-multi-from-malformed.lua
t-verify-multi-from-malformed.lua \
t-verify-authservid-jobid t-verify-authservid-jobid.conf \
t-verify-authservid-jobid.lua

MOSTLYCLEANFILES=
11 changes: 11 additions & 0 deletions opendmarc/tests/t-verify-authservid-jobid
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
#
# Verify that AuthservIDWithJobID produces a properly RFC 8601-compliant
# quoted authserv-id in all Authentication-Results headers.

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

miltertest -s $srcdir/t-verify-authservid-jobid.lua
4 changes: 4 additions & 0 deletions opendmarc/tests/t-verify-authservid-jobid.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Background No
AuthservID testhost
AuthservIDWithJobID Yes
SPFSelfValidate Yes
111 changes: 111 additions & 0 deletions opendmarc/tests/t-verify-authservid-jobid.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
-- Copyright (c) 2026, The Trusted Domain Project. All rights reserved.

-- Verifies that when AuthservIDWithJobID is set, the authserv-id in both
-- the spf= and dmarc= Authentication-Results headers is a properly quoted
-- RFC 8601 / RFC 2045 token of the form "authservid/jobid" rather than
-- the invalid unquoted form authservid/jobid (where "/" is a tspecial).

mt.echo("*** authserv-id with job ID quoting test")

-- setup
sock = "unix:" .. mt.getcwd() .. "/t-verify-authservid-jobid.sock"
binpath = mt.getcwd() .. "/.."
if os.getenv("srcdir") ~= nil then
mt.chdir(os.getenv("srcdir"))
end

mt.startfilter(binpath .. "/opendmarc", "-l", "-c",
"t-verify-authservid-jobid.conf", "-p", sock)

conn = mt.connect(sock, 40, 0.05)
if conn == nil then
error("mt.connect() failed")
end

if mt.conninfo(conn, "localhost2", "66.220.149.251") ~= nil then
error("mt.conninfo() failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.conninfo() unexpected reply")
end

mt.macro(conn, SMFIC_MAIL, "i", "testjobid")
if mt.mailfrom(conn, "user@trusteddomain.org") ~= nil then
error("mt.mailfrom() failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.mailfrom() unexpected reply")
end

if mt.header(conn, "From", "user@trusteddomain.org") ~= nil then
error("mt.header(From) failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.header(From) unexpected reply")
end
if mt.header(conn, "To", "user@example.com") ~= nil then
error("mt.header(To) failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.header(To) unexpected reply")
end
if mt.header(conn, "Date", "Tue, 22 Dec 2009 13:04:12 -0800") ~= nil then
error("mt.header(Date) failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.header(Date) unexpected reply")
end
if mt.header(conn, "Subject", "DMARC test") ~= nil then
error("mt.header(Subject) failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.header(Subject) unexpected reply")
end

if mt.eoh(conn) ~= nil then
error("mt.eoh() failed")
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error("mt.eoh() unexpected reply")
end

if mt.eom(conn) ~= nil then
error("mt.eom() failed")
end
if mt.getreply(conn) ~= SMFIR_ACCEPT then
error("mt.eom() unexpected reply")
end

if not mt.eom_check(conn, MT_HDRINSERT, "Authentication-Results") and
not mt.eom_check(conn, MT_HDRADD, "Authentication-Results") then
error("no Authentication-Results added")
end

-- every A-R header must use the quoted authserv-id form;
-- the unquoted form with a bare slash is an RFC violation
n = 0
unquoted_found = 0
quoted_found = 0
while true do
ar = mt.getheader(conn, "Authentication-Results", n)
if ar == nil then
break
end
if string.find(ar, '"testhost/testjobid"', 1, true) ~= nil then
quoted_found = quoted_found + 1
end
if string.find(ar, "testhost/testjobid", 1, true) ~= nil and
string.find(ar, '"testhost/testjobid"', 1, true) == nil then
unquoted_found = unquoted_found + 1
end
n = n + 1
end

if quoted_found == 0 then
error("authserv-id with job ID not properly quoted (expected \"testhost/testjobid\")")
end
if unquoted_found > 0 then
error("authserv-id with job ID present in invalid unquoted form (RFC 8601 violation)")
end

mt.disconnect(conn)