]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] Fix whitelist options in the arc module 5559/head
authorVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 1 Aug 2025 11:41:57 +0000 (12:41 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 1 Aug 2025 11:41:57 +0000 (12:41 +0100)
Issue: #5558

lualib/lua_dkim_tools.lua
src/plugins/lua/arc.lua

index b7f520fae6d65309410093bed6d7857b36edf6a7..69c9462b56e3a5266116e86eb1a1bf94e636639b 100644 (file)
@@ -13,7 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-]]--
+]] --
 
 local exports = {}
 
@@ -33,7 +33,7 @@ local function check_violation(N, task, domain)
   if task:has_symbol(sym_check) then
     local sym = task:get_symbol(sym_check)[1]
     logger.infox(task, 'skip signing for %s: violation %s found: %s',
-        domain, sym_check, sym.options)
+      domain, sym_check, sym.options)
     return false
   end
 
@@ -92,7 +92,6 @@ local function parse_dkim_http_headers(N, task, settings)
     local key = task:get_request_header(headers.key_header)
 
     if not (domain and selector and key) then
-
       logger.errx(task, 'missing required headers to sign email')
       return false, {}
     end
@@ -258,14 +257,14 @@ local function prepare_dkim_signing(N, task, settings)
     -- OpenDKIM style
     if is_skip_sign() then
       lua_util.debugm(N, task,
-          'skip signing: is_sign_network: %s, is_authed: %s, is_local: %s',
-          is_sign_networks, is_authed, is_local)
+        'skip signing: is_sign_network: %s, is_authed: %s, is_local: %s',
+        is_sign_networks, is_authed, is_local)
       return false, {}
     end
 
     if not hfrom or not hfrom[1] or not hfrom[1].addr then
       lua_util.debugm(N, task,
-          'signing_table: cannot get data when no header from is presented')
+        'signing_table: cannot get data when no header from is presented')
       return false, {}
     end
     local sign_entry = settings.signing_table:get_key(hfrom[1].addr:lower())
@@ -273,7 +272,7 @@ local function prepare_dkim_signing(N, task, settings)
     if sign_entry then
       -- Check opendkim style entries
       lua_util.debugm(N, task,
-          'signing_table: found entry for %s: %s', hfrom[1].addr, sign_entry)
+        'signing_table: found entry for %s: %s', hfrom[1].addr, sign_entry)
       if sign_entry == '%' then
         sign_entry = hdom
       end
@@ -291,7 +290,7 @@ local function prepare_dkim_signing(N, task, settings)
 
             if not selector then
               logger.errx(task, 'no selector defined for sign_entry %s, key_entry %s',
-                  sign_entry, key_entry)
+                sign_entry, key_entry)
               return false, {}
             end
 
@@ -305,11 +304,11 @@ local function prepare_dkim_signing(N, task, settings)
             if st:sub(1, 1) == '/' or st == './' or st == '..' then
               res.key = parts[2]:gsub('%%', hdom)
               lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, key file=%s',
-                  hdom, selector, res.domain, res.key)
+                hdom, selector, res.domain, res.key)
             else
               res.rawkey = parts[2] -- No sanity check here
               lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, raw key used',
-                  hdom, selector, res.domain)
+                hdom, selector, res.domain)
             end
 
             return true, { res }
@@ -327,56 +326,56 @@ local function prepare_dkim_signing(N, task, settings)
             if st:sub(1, 1) == '/' or st == './' or st == '..' then
               res.key = parts[3]:gsub('%%', hdom)
               lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, key file=%s',
-                  hdom, selector, res.domain, res.key)
+                hdom, selector, res.domain, res.key)
             else
               res.rawkey = parts[3] -- No sanity check here
               lua_util.debugm(N, task, 'perform dkim signing for %s, selector=%s, domain=%s, raw key used',
-                  hdom, selector, res.domain)
+                hdom, selector, res.domain)
             end
 
             return true, { res }
           else
             logger.errx(task, 'invalid key entry for sign entry %s: %s; when signing %s domain',
-                sign_entry, key_entry, hdom)
+              sign_entry, key_entry, hdom)
             return false, {}
           end
         elseif settings.use_vault then
           -- Sign table is presented, the rest is covered by vault
           lua_util.debugm(N, task, 'check vault for %s, by sign entry %s, key entry is missing',
-              hdom, sign_entry)
+            hdom, sign_entry)
           return true, {
             domain = sign_entry,
             vault = true
           }
         else
           logger.errx(task, 'missing key entry for sign entry %s; when signing %s domain',
-              sign_entry, hdom)
+            sign_entry, hdom)
           return false, {}
         end
       else
         logger.errx(task, 'cannot get key entry for signing entry %s, when signing %s domain',
-            sign_entry, hdom)
+          sign_entry, hdom)
         return false, {}
       end
     else
       lua_util.debugm(N, task,
-          'signing_table: no entry for %s', hfrom[1].addr)
+        'signing_table: no entry for %s', hfrom[1].addr)
       return false, {}
     end
   else
     if settings.use_domain_sign_networks and is_sign_networks then
       dkim_domain = get_dkim_domain('use_domain_sign_networks')
       lua_util.debugm(N, task,
-          'sign_networks: use domain(%s) for signature: %s',
-          settings.use_domain_sign_networks, dkim_domain)
+        'sign_networks: use domain(%s) for signature: %s',
+        settings.use_domain_sign_networks, dkim_domain)
     elseif settings.use_domain_sign_local and is_local then
       dkim_domain = get_dkim_domain('use_domain_sign_local')
       lua_util.debugm(N, task, 'local: use domain(%s) for signature: %s',
-          settings.use_domain_sign_local, dkim_domain)
+        settings.use_domain_sign_local, dkim_domain)
     elseif settings.use_domain_sign_inbound and not is_local and not auser then
       dkim_domain = get_dkim_domain('use_domain_sign_inbound')
       lua_util.debugm(N, task, 'inbound: use domain(%s) for signature: %s',
-          settings.use_domain_sign_inbound, dkim_domain)
+        settings.use_domain_sign_inbound, dkim_domain)
     elseif settings.use_domain_custom then
       if type(settings.use_domain_custom) == 'string' then
         -- Load custom function
@@ -387,10 +386,10 @@ local function prepare_dkim_signing(N, task, settings)
             settings.use_domain_custom = res_or_err
             dkim_domain = settings.use_domain_custom(task)
             lua_util.debugm(N, task, 'use custom domain for signing: %s',
-                dkim_domain)
+              dkim_domain)
           else
             logger.errx(task, 'cannot load dkim domain custom script: invalid type: %s, expected function',
-                type(res_or_err))
+              type(res_or_err))
             settings.use_domain_custom = nil
           end
         else
@@ -400,12 +399,12 @@ local function prepare_dkim_signing(N, task, settings)
       else
         dkim_domain = settings.use_domain_custom(task)
         lua_util.debugm(N, task, 'use custom domain for signing: %s',
-            dkim_domain)
+          dkim_domain)
       end
     else
       dkim_domain = get_dkim_domain('use_domain')
       lua_util.debugm(N, task, 'use domain(%s) for signature: %s',
-          settings.use_domain, dkim_domain)
+        settings.use_domain, dkim_domain)
     end
   end
 
@@ -467,7 +466,7 @@ local function prepare_dkim_signing(N, task, settings)
         })
       else
         lua_util.debugm(N, task, 'domain %s is not designated for vault',
-            dkim_domain)
+          dkim_domain)
       end
     else
       -- TODO: try every domain in the vault
@@ -501,7 +500,7 @@ local function prepare_dkim_signing(N, task, settings)
     if ret then
       table.insert(p, k)
       lua_util.debugm(N, task, 'using mempool selector %s with key %s',
-          k.selector, k.key)
+        k.selector, k.key)
     end
   end
 
@@ -530,11 +529,11 @@ local function prepare_dkim_signing(N, task, settings)
 
   if not settings.use_redis then
     insert_or_update_prop(N, task, p, 'key',
-        'default path', settings.path)
+      'default path', settings.path)
   end
 
   insert_or_update_prop(N, task, p, 'selector',
-      'default selector', settings.selector)
+    'default selector', settings.selector)
 
   if settings.check_violation then
     if not check_violation(N, task, p.domain) then
@@ -543,7 +542,7 @@ local function prepare_dkim_signing(N, task, settings)
   end
 
   insert_or_update_prop(N, task, p, 'domain', 'dkim_domain',
-      dkim_domain)
+    dkim_domain)
 
   return #p > 0 and true or false, p
 end
@@ -560,53 +559,53 @@ exports.sign_using_redis = function(N, task, settings, selectors, sign_func, err
     local function redis_key_cb(err, data)
       if err then
         err_func(string.format("cannot make request to load DKIM key for %s: %s",
-            rk, err))
+          rk, err))
       elseif type(data) ~= 'string' then
         lua_util.debugm(N, task, "missing DKIM key for %s", rk)
       else
         p.rawkey = data
         lua_util.debugm(N, task, 'found and parsed key for %s:%s in Redis',
-            p.domain, p.selector)
+          p.domain, p.selector)
         sign_func(task, p)
       end
     end
     local rret = lua_redis.redis_make_request(task,
-        settings.redis_params, -- connect params
-        rk, -- hash key
-        false, -- is write
-        redis_key_cb, --callback
-        'HGET', -- command
-        { settings.key_prefix, rk } -- arguments
+      settings.redis_params,      -- connect params
+      rk,                         -- hash key
+      false,                      -- is write
+      redis_key_cb,               --callback
+      'HGET',                     -- command
+      { settings.key_prefix, rk } -- arguments
     )
     if not rret then
       err_func(task,
-          string.format("cannot make request to load DKIM key for %s", rk))
+        string.format("cannot make request to load DKIM key for %s", rk))
     end
   end
 
   for _, p in ipairs(selectors) do
     if settings.selector_prefix then
       logger.infox(task, "using selector prefix '%s' for domain '%s'",
-          settings.selector_prefix, p.domain);
+        settings.selector_prefix, p.domain);
       local function redis_selector_cb(err, data)
         if err or type(data) ~= 'string' then
           err_func(task, string.format("cannot make request to load DKIM selector for domain %s: %s",
-              p.domain, err))
+            p.domain, err))
         else
           try_redis_key(data, p)
         end
       end
       local rret = lua_redis.redis_make_request(task,
-          settings.redis_params, -- connect params
-          p.domain, -- hash key
-          false, -- is write
-          redis_selector_cb, --callback
-          'HGET', -- command
-          { settings.selector_prefix, p.domain } -- arguments
+        settings.redis_params,                 -- connect params
+        p.domain,                              -- hash key
+        false,                                 -- is write
+        redis_selector_cb,                     --callback
+        'HGET',                                -- command
+        { settings.selector_prefix, p.domain } -- arguments
       )
       if not rret then
         err_func(task, string.format("cannot make Redis request to load DKIM selector for domain %s",
-            p.domain))
+          p.domain))
       end
     else
       try_redis_key(p.selector, p)
@@ -619,25 +618,25 @@ exports.sign_using_vault = function(N, task, settings, selector, sign_func, err_
   local ucl = require "ucl"
 
   local full_url = string.format('%s/v1/%s/%s',
-      settings.vault_url, settings.vault_path or 'dkim', selector.domain)
+    settings.vault_url, settings.vault_path or 'dkim', selector.domain)
   local upstream_list = lua_util.http_upstreams_by_url(rspamd_config:get_mempool(), settings.vault_url)
 
   local function vault_callback(err, code, body, _)
     if code ~= 200 then
       err_func(task, string.format('cannot request data from the vault url: %s; %s (%s)',
-          full_url, err, body))
+        full_url, err, body))
     else
       local parser = ucl.parser()
       local res, parser_err = parser:parse_string(body)
       if not res then
         err_func(task, string.format('vault reply for %s (data=%s) cannot be parsed: %s',
-            full_url, body, parser_err))
+          full_url, body, parser_err))
       else
         local obj = parser:get_object()
 
         if not obj or not obj.data then
           err_func(task, string.format('vault reply for %s (data=%s) is invalid, no data',
-              full_url, body))
+            full_url, body))
         else
           local elts = obj.data.selectors or {}
           local errs = {}
@@ -675,13 +674,13 @@ exports.sign_using_vault = function(N, task, settings, selector, sign_func, err_
               alg = p.alg,
             }
             lua_util.debugm(N, task, 'found and parsed key for %s:%s in Vault',
-                dkim_sign_data.domain, dkim_sign_data.selector)
+              dkim_sign_data.domain, dkim_sign_data.selector)
             nvalid = nvalid + 1
             sign_func(task, dkim_sign_data)
           end, fun.filter(is_selector_valid, elts))
           for _, e in errs do
             lua_util.debugm(N, task, 'error found during processing Vault selectors: %s:%s',
-                e[1], e[2])
+              e[1], e[2])
           end
 
           if nvalid == 0 then
@@ -707,7 +706,7 @@ exports.sign_using_vault = function(N, task, settings, selector, sign_func, err_
 
   if not ret then
     err_func(task, string.format("cannot make HTTP request to load DKIM data domain %s",
-        selector.domain))
+      selector.domain))
   end
 end
 
@@ -732,8 +731,7 @@ exports.process_signing_settings = function(N, settings, opts)
     selector_map = { 'map', 'DKIM selectors' },
     signing_table = { 'glob', 'DKIM signing table' },
     key_table = { 'glob', 'DKIM keys table' },
-    vault_domains = { 'glob', 'DKIM signing domains in vault' },
-    whitelisted_signers_map = { 'set', 'ARC trusted signers domains' }
+    vault_domains = { 'glob', 'DKIM signing domains in vault' }
   }
   for k, v in pairs(opts) do
     local maybe_map = maps_opts[k]
index 45da1f5a265e5a2ca6a2df45b079a5c7e8e94ecd..954583ed06242fbb48eac42daaa5e75569222cca 100644 (file)
@@ -72,12 +72,13 @@ local settings = {
   use_domain = 'header',
   use_esld = true,
   use_redis = false,
-  key_prefix = 'arc_keys', -- default hash name
-  reuse_auth_results = false, -- Reuse the existing authentication results
+  key_prefix = 'arc_keys',       -- default hash name
+  reuse_auth_results = false,    -- Reuse the existing authentication results
   whitelisted_signers_map = nil, -- Trusted signers domains
-  adjust_dmarc = true, -- Adjust DMARC rejected policy for trusted forwarders
-  allowed_ids = nil, -- Allowed settings id
-  forbidden_ids = nil, -- Banned settings id
+  whitelist = nil,               -- Domains with broken ARC implementations to trust despite validation failures
+  adjust_dmarc = true,           -- Adjust DMARC rejected policy for trusted forwarders
+  allowed_ids = nil,             -- Allowed settings id
+  forbidden_ids = nil,           -- Banned settings id
 }
 
 -- To match normal AR
@@ -86,15 +87,15 @@ local ar_settings = lua_auth_results.default_settings
 local function parse_arc_header(hdr, target, is_aar)
   -- Split elements by ';' and trim spaces
   local arr = fun.totable(fun.map(
-      function(val)
-        return fun.totable(fun.map(lua_util.rspamd_str_trim,
-            fun.filter(function(v)
-              return v and #v > 0
-            end,
-                lua_util.rspamd_str_split(val.decoded, ';')
-            )
-        ))
-      end, hdr
+    function(val)
+      return fun.totable(fun.map(lua_util.rspamd_str_trim,
+        fun.filter(function(v)
+            return v and #v > 0
+          end,
+          lua_util.rspamd_str_split(val.decoded, ';')
+        )
+      ))
+    end, hdr
   ))
 
   -- v[1] is the key and v[2] is the value
@@ -115,11 +116,11 @@ local function parse_arc_header(hdr, target, is_aar)
     if not is_aar then
       -- For normal ARC headers we split by kv pair, like k=v
       fun.each(function(v)
-        fill_arc_header_table(v, target[i])
-      end,
-          fun.map(function(elt)
-            return lua_util.rspamd_str_split(elt, '=')
-          end, elts)
+          fill_arc_header_table(v, target[i])
+        end,
+        fun.map(function(elt)
+          return lua_util.rspamd_str_split(elt, '=')
+        end, elts)
       )
     else
       -- For AAR we check special case of i=%d and pass everything else to
@@ -156,14 +157,14 @@ local function arc_validate_seals(task, seals, sigs, seal_headers, sig_headers)
   for i = 1, #seals do
     if (sigs[i].i or 0) ~= i then
       fail_reason = string.format('bad i for signature: %d, expected %d; d=%s',
-          sigs[i].i, i, sigs[i].d)
+        sigs[i].i, i, sigs[i].d)
       rspamd_logger.infox(task, fail_reason)
       task:insert_result(arc_symbols['invalid'], 1.0, fail_reason)
       return false, fail_reason
     end
     if (seals[i].i or 0) ~= i then
       fail_reason = string.format('bad i for seal: %d, expected %d; d=%s',
-          seals[i].i, i, seals[i].d)
+        seals[i].i, i, seals[i].d)
       rspamd_logger.infox(task, fail_reason)
       task:insert_result(arc_symbols['invalid'], 1.0, fail_reason)
       return false, fail_reason
@@ -207,7 +208,7 @@ local function arc_callback(task)
   if #arc_sig_headers ~= #arc_seal_headers then
     -- We mandate that count of seals is equal to count of signatures
     rspamd_logger.infox(task, 'number of seals (%s) is not equal to number of signatures (%s)',
-        #arc_seal_headers, #arc_sig_headers)
+      #arc_seal_headers, #arc_sig_headers)
     task:insert_result(arc_symbols['invalid'], 1.0, 'invalid count of seals and signatures')
     return
   end
@@ -249,7 +250,7 @@ local function arc_callback(task)
 
   -- Now check sanity of what we have
   local valid, validation_error = arc_validate_seals(task, cbdata.seals, cbdata.sigs,
-      arc_seal_headers, arc_sig_headers)
+    arc_seal_headers, arc_sig_headers)
   if not valid then
     task:cache_set('arc-failure', validation_error)
     return
@@ -267,12 +268,20 @@ local function arc_callback(task)
   local function gen_arc_seal_cb(index, sig)
     return function(_, res, err, domain)
       lua_util.debugm(N, task, 'checked arc seal: %s(%s), %s processed',
-          res, err, index)
+        res, err, index)
 
       if not res then
-        cbdata.res = 'fail'
-        if err and domain then
-          table.insert(cbdata.errors, string.format('sig:%s:%s', domain, err))
+        -- Check if this domain is whitelisted for broken ARC implementations
+        if settings.whitelist and domain and settings.whitelist:get_key(domain) then
+          rspamd_logger.infox(task, 'ARC seal validation failed for whitelisted domain %s, treating as valid: %s',
+            domain, err)
+          lua_util.debugm(N, task, 'whitelisted domain %s ARC seal failure ignored', domain)
+          res = true -- Treat as valid to continue the chain
+        else
+          cbdata.res = 'fail'
+          if err and domain then
+            table.insert(cbdata.errors, string.format('sig:%s:%s', domain, err))
+          end
         end
       end
 
@@ -283,7 +292,7 @@ local function arc_callback(task)
           local cur_aar = cbdata.ars[index]
           if not cur_aar then
             rspamd_logger.warnx(task, "cannot find Arc-Authentication-Results for trusted " ..
-                "forwarder %s on i=%s", domain, cbdata.index)
+              "forwarder %s on i=%s", domain, cbdata.index)
           else
             task:cache_set(AR_TRUSTED_CACHE_KEY, cur_aar)
             local seen_dmarc
@@ -309,20 +318,20 @@ local function arc_callback(task)
             end
           end
           task:insert_result(arc_symbols.trusted_allow, mult,
-              string.format('%s:s=%s:i=%d', domain, sig.s, index))
+            string.format('%s:s=%s:i=%d', domain, sig.s, index))
         end
       end
 
       if index == #arc_sig_headers then
         if cbdata.res == 'success' then
           local arc_allow_result = string.format('%s:s=%s:i=%d',
-              domain, sig.s, index)
+            domain, sig.s, index)
           task:insert_result(arc_symbols.allow, 1.0, arc_allow_result)
           task:cache_set('arc-allow', arc_allow_result)
         else
           task:insert_result(arc_symbols.reject, 1.0,
-              rspamd_logger.slog('seal check failed: %s, %s', cbdata.res,
-                  cbdata.errors))
+            rspamd_logger.slog('seal check failed: %s, %s', cbdata.res,
+              cbdata.errors))
         end
       end
     end
@@ -330,12 +339,20 @@ local function arc_callback(task)
 
   local function arc_signature_cb(_, res, err, domain)
     lua_util.debugm(N, task, 'checked arc signature %s: %s(%s)',
-        domain, res, err)
+      domain, res, err)
 
     if not res then
-      cbdata.res = 'fail'
-      if err and domain then
-        table.insert(cbdata.errors, string.format('sig:%s:%s', domain, err))
+      -- Check if this domain is whitelisted for broken ARC implementations
+      if settings.whitelist and domain and settings.whitelist:get_key(domain) then
+        rspamd_logger.infox(task, 'ARC signature validation failed for whitelisted domain %s, treating as valid: %s',
+          domain, err)
+        lua_util.debugm(N, task, 'whitelisted domain %s ARC signature failure ignored', domain)
+        res = true -- Treat as valid to continue the chain
+      else
+        cbdata.res = 'fail'
+        if err and domain then
+          table.insert(cbdata.errors, string.format('sig:%s:%s', domain, err))
+        end
       end
     end
     if cbdata.res == 'success' then
@@ -343,17 +360,24 @@ local function arc_callback(task)
       for i, sig in ipairs(cbdata.seals) do
         local ret, lerr = dkim_verify(task, sig.header, gen_arc_seal_cb(i, sig), 'arc-seal')
         if not ret then
-          cbdata.res = 'fail'
-          table.insert(cbdata.errors, string.format('seal:%s:s=%s:i=%s:%s',
+          -- Check if this domain is whitelisted for broken ARC implementations
+          if settings.whitelist and sig.d and settings.whitelist:get_key(sig.d) then
+            rspamd_logger.infox(task, 'ARC seal dkim_verify failed for whitelisted domain %s, treating as valid: %s',
+              sig.d, lerr)
+            lua_util.debugm(N, task, 'whitelisted domain %s ARC seal dkim_verify failure ignored', sig.d)
+          else
+            cbdata.res = 'fail'
+            table.insert(cbdata.errors, string.format('seal:%s:s=%s:i=%s:%s',
               sig.d or '', sig.s or '', sig.i or '', lerr))
-          lua_util.debugm(N, task, 'checked arc seal %s: %s(%s), %s processed',
+            lua_util.debugm(N, task, 'checked arc seal %s: %s(%s), %s processed',
               sig.d, ret, lerr, i)
+          end
         end
       end
     else
       task:insert_result(arc_symbols['reject'], 1.0,
-          rspamd_logger.slog('signature check failed: %s, %s', cbdata.res,
-              cbdata.errors))
+        rspamd_logger.slog('signature check failed: %s, %s', cbdata.res,
+          cbdata.errors))
     end
   end
 
@@ -397,25 +421,33 @@ local function arc_callback(task)
        is "fail" and the algorithm stops here.
    9.  If the algorithm reaches this step, then the Chain Validation
        Status is "pass", and the algorithm is complete.
-  ]]--
+  ]] --
 
   local processed = 0
   local sig = cbdata.sigs[#cbdata.sigs] -- last AMS
   local ret, err = dkim_verify(task, sig.header, arc_signature_cb, 'arc-sign')
 
   if not ret then
-    cbdata.res = 'fail'
-    table.insert(cbdata.errors, string.format('sig:%s:%s', sig.d or '', err))
+    -- Check if this domain is whitelisted for broken ARC implementations
+    if settings.whitelist and sig.d and settings.whitelist:get_key(sig.d) then
+      rspamd_logger.infox(task, 'ARC signature dkim_verify failed for whitelisted domain %s, treating as valid: %s',
+        sig.d, err)
+      lua_util.debugm(N, task, 'whitelisted domain %s ARC signature dkim_verify failure ignored', sig.d)
+      processed = processed + 1
+    else
+      cbdata.res = 'fail'
+      table.insert(cbdata.errors, string.format('sig:%s:%s', sig.d or '', err))
+    end
   else
     processed = processed + 1
     lua_util.debugm(N, task, 'processed arc signature %s[%s]: %s(%s), %s total',
-        sig.d, sig.i, ret, err, #cbdata.seals)
+      sig.d, sig.i, ret, err, #cbdata.seals)
   end
 
   if processed == 0 then
     task:insert_result(arc_symbols['reject'], 1.0,
-        rspamd_logger.slog('cannot verify %s of %s signatures: %s',
-            #arc_sig_headers - processed, #arc_sig_headers, cbdata.errors))
+      rspamd_logger.slog('cannot verify %s of %s signatures: %s',
+        #arc_sig_headers - processed, #arc_sig_headers, cbdata.errors))
   end
 end
 
@@ -538,13 +570,13 @@ local function arc_sign_seal(task, params, header)
     for i = 1, #arc_seals, 1 do
       if arc_auth_results[i] then
         local s = dkim_canonicalize('ARC-Authentication-Results',
-            arc_auth_results[i].raw_header)
+          arc_auth_results[i].raw_header)
         sha_ctx:update(s)
         lua_util.debugm(N, task, 'update signature with header: %s', s)
       end
       if arc_sigs[i] then
         local s = dkim_canonicalize('ARC-Message-Signature',
-            arc_sigs[i].raw_header)
+          arc_sigs[i].raw_header)
         sha_ctx:update(s)
         lua_util.debugm(N, task, 'update signature with header: %s', s)
       end
@@ -557,16 +589,16 @@ local function arc_sign_seal(task, params, header)
   end
 
   header = lua_util.fold_header(task,
-      'ARC-Message-Signature',
-      header)
+    'ARC-Message-Signature',
+    header)
 
   cur_auth_results = string.format('i=%d; %s', cur_idx, cur_auth_results)
   cur_auth_results = lua_util.fold_header(task,
-      'ARC-Authentication-Results',
-      cur_auth_results, ';')
+    'ARC-Authentication-Results',
+    cur_auth_results, ';')
 
   local s = dkim_canonicalize('ARC-Authentication-Results',
-      cur_auth_results)
+    cur_auth_results)
   sha_ctx:update(s)
   lua_util.debugm(N, task, 'update signature with header: %s', s)
   s = dkim_canonicalize('ARC-Message-Signature', header)
@@ -574,10 +606,10 @@ local function arc_sign_seal(task, params, header)
   lua_util.debugm(N, task, 'update signature with header: %s', s)
 
   local cur_arc_seal = string.format('i=%d; s=%s; d=%s; t=%d; a=rsa-sha256; cv=%s; b=',
-      cur_idx,
-      params.selector,
-      params.domain,
-      math.floor(rspamd_util.get_time()), params.arc_cv)
+    cur_idx,
+    params.selector,
+    params.domain,
+    math.floor(rspamd_util.get_time()), params.arc_cv)
   s = string.format('%s:%s', 'arc-seal', cur_arc_seal)
   sha_ctx:update(s)
   lua_util.debugm(N, task, 'initial update signature with header: %s', s)
@@ -591,20 +623,23 @@ local function arc_sign_seal(task, params, header)
 
   local sig = rspamd_rsa.sign_memory(privkey, sha_ctx:bin())
   cur_arc_seal = string.format('%s%s', cur_arc_seal,
-      sig:base64(70, nl_type))
+    sig:base64(70, nl_type))
 
   lua_mime.modify_headers(task, {
     add = {
       ['ARC-Authentication-Results'] = { order = 1, value = cur_auth_results },
       ['ARC-Message-Signature'] = { order = 1, value = header },
-      ['ARC-Seal'] = { order = 1, value = lua_util.fold_header(task,
-          'ARC-Seal', cur_arc_seal) }
+      ['ARC-Seal'] = {
+        order = 1,
+        value = lua_util.fold_header(task,
+          'ARC-Seal', cur_arc_seal)
+      }
     },
     -- RFC requires a strict order for these headers to be inserted
     order = { 'ARC-Authentication-Results', 'ARC-Message-Signature', 'ARC-Seal' },
   })
   task:insert_result(settings.sign_symbol, 1.0,
-      string.format('%s:s=%s:i=%d', params.domain, params.selector, cur_idx))
+    string.format('%s:s=%s:i=%d', params.domain, params.selector, cur_idx))
 end
 
 local function prepare_arc_selector(task, sel)
@@ -668,7 +703,6 @@ local function prepare_arc_selector(task, sel)
     else
       default_arc_cv()
     end
-
   end
 
   return true
@@ -696,18 +730,17 @@ local function do_sign(task, sign_params)
           sign_params.strict_pubkey_check = not settings.allow_pubkey_mismatch
         elseif not settings.allow_pubkey_mismatch then
           rspamd_logger.errx(task, 'public key for domain %s/%s is not found: %s, skip signing',
-              sign_params.domain, sign_params.selector, err)
+            sign_params.domain, sign_params.selector, err)
           return
         else
           rspamd_logger.infox(task, 'public key for domain %s/%s is not found: %s',
-              sign_params.domain, sign_params.selector, err)
+            sign_params.domain, sign_params.selector, err)
         end
 
         local dret, hdr = dkim_sign(task, sign_params)
         if dret then
           arc_sign_seal(task, sign_params, hdr)
         end
-
       end,
       forced = true
     })
@@ -768,6 +801,31 @@ end
 
 dkim_sign_tools.process_signing_settings(N, settings, opts)
 
+-- Process ARC-specific maps that aren't handled by dkim_sign_tools
+local lua_maps = require "lua_maps"
+
+if opts.whitelisted_signers_map then
+  settings.whitelisted_signers_map = lua_maps.map_add_from_ucl(opts.whitelisted_signers_map, 'set',
+    'ARC trusted signers domains')
+  if not settings.whitelisted_signers_map then
+    rspamd_logger.errx(rspamd_config, 'cannot load whitelisted_signers_map')
+    settings.whitelisted_signers_map = nil
+  else
+    rspamd_logger.infox(rspamd_config, 'loaded ARC whitelisted signers map')
+  end
+end
+
+if opts.whitelist then
+  settings.whitelist = lua_maps.map_add_from_ucl(opts.whitelist, 'set',
+    'ARC domains with broken implementations')
+  if not settings.whitelist then
+    rspamd_logger.errx(rspamd_config, 'cannot load ARC whitelist map')
+    settings.whitelist = nil
+  else
+    rspamd_logger.infox(rspamd_config, 'loaded ARC whitelist map')
+  end
+end
+
 if not dkim_sign_tools.validate_signing_settings(settings) then
   rspamd_logger.infox(rspamd_config, 'mandatory parameters missing, disable arc signing')
   return
@@ -780,7 +838,7 @@ if ar_opts and ar_opts.routines then
 
   if routines['authentication-results'] then
     ar_settings = lua_util.override_defaults(ar_settings,
-        routines['authentication-results'])
+      routines['authentication-results'])
   end
 end
 
@@ -789,7 +847,7 @@ if settings.use_redis then
 
   if not redis_params then
     rspamd_logger.errx(rspamd_config, 'no servers are specified, ' ..
-        'but module is configured to load keys from redis, disable arc signing')
+      'but module is configured to load keys from redis, disable arc signing')
     return
   end
 
@@ -845,9 +903,9 @@ if settings.adjust_dmarc and settings.whitelisted_signers_map then
           local dmarc_fwd = ar.dmarc
           if dmarc_fwd == 'pass' then
             rspamd_logger.infox(task, "adjust dmarc reject score as trusted forwarder "
-                .. "proved DMARC validity for %s", ar['header.from'])
+              .. "proved DMARC validity for %s", ar['header.from'])
             task:adjust_result(sym_to_adjust, 0.1,
-                'ARC trusted')
+              'ARC trusted')
           end
         end
       end