diff options
Diffstat (limited to 'src/plugins/lua/arc.lua')
-rw-r--r-- | src/plugins/lua/arc.lua | 853 |
1 files changed, 853 insertions, 0 deletions
diff --git a/src/plugins/lua/arc.lua b/src/plugins/lua/arc.lua new file mode 100644 index 0000000..ff19aef --- /dev/null +++ b/src/plugins/lua/arc.lua @@ -0,0 +1,853 @@ +--[[ +Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +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 rspamd_logger = require "rspamd_logger" +local lua_util = require "lua_util" +local dkim_sign_tools = require "lua_dkim_tools" +local rspamd_util = require "rspamd_util" +local rspamd_rsa_privkey = require "rspamd_rsa_privkey" +local rspamd_rsa = require "rspamd_rsa" +local fun = require "fun" +local lua_auth_results = require "lua_auth_results" +local hash = require "rspamd_cryptobox_hash" +local lua_mime = require "lua_mime" + +if confighelp then + return +end + +local N = 'arc' +local AR_TRUSTED_CACHE_KEY = 'arc_trusted_aar' + +if not rspamd_plugins.dkim then + rspamd_logger.errx(rspamd_config, "cannot enable arc plugin: dkim is disabled") + return +end + +local dkim_verify = rspamd_plugins.dkim.verify +local dkim_sign = rspamd_plugins.dkim.sign +local dkim_canonicalize = rspamd_plugins.dkim.canon_header_relaxed +local redis_params + +if not dkim_verify or not dkim_sign or not dkim_canonicalize then + rspamd_logger.errx(rspamd_config, "cannot enable arc plugin: dkim is disabled") + return +end + +local arc_symbols = { + allow = 'ARC_ALLOW', + invalid = 'ARC_INVALID', + dnsfail = 'ARC_DNSFAIL', + na = 'ARC_NA', + reject = 'ARC_REJECT', +} + +local settings = { + allow_envfrom_empty = true, + allow_hdrfrom_mismatch = false, + allow_hdrfrom_mismatch_local = false, + allow_hdrfrom_mismatch_sign_networks = false, + allow_hdrfrom_multiple = false, + allow_username_mismatch = false, + sign_authenticated = true, + domain = {}, + path = string.format('%s/%s/%s', rspamd_paths['DBDIR'], 'arc', '$domain.$selector.key'), + sign_local = true, + selector = 'arc', + sign_symbol = 'ARC_SIGNED', + try_fallback = true, + 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 + 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 +} + +-- To match normal AR +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 + )) + + -- v[1] is the key and v[2] is the value + local function fill_arc_header_table(v, t) + if v[1] and v[2] then + local key = lua_util.rspamd_str_trim(v[1]) + local value = lua_util.rspamd_str_trim(v[2]) + t[key] = value + end + end + + -- Now we have two tables in format: + -- [arc_header] -> [{arc_header1_elts}, {arc_header2_elts}...] + for i, elts in ipairs(arr) do + if not target[i] then + target[i] = {} + end + 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) + ) + else + -- For AAR we check special case of i=%d and pass everything else to + -- AAR specific parser + for _, elt in ipairs(elts) do + if string.match(elt, "%s*i%s*=%s*%d+%s*") then + local pair = lua_util.rspamd_str_split(elt, '=') + fill_arc_header_table(pair, target[i]) + else + -- Normal element + local ar_elt = lua_auth_results.parse_ar_element(elt) + + if ar_elt then + if not target[i].ar then + target[i].ar = {} + end + table.insert(target[i].ar, ar_elt) + end + end + end + end + target[i].header = hdr[i].decoded + target[i].raw_header = hdr[i].value + end + + -- sort by i= attribute + table.sort(target, function(a, b) + return (a.i or 0) < (b.i or 0) + end) +end + +local function arc_validate_seals(task, seals, sigs, seal_headers, sig_headers) + local fail_reason + 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) + 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) + rspamd_logger.infox(task, fail_reason) + task:insert_result(arc_symbols['invalid'], 1.0, fail_reason) + return false, fail_reason + end + + if not seals[i].cv then + fail_reason = string.format('no cv on i=%d', i) + task:insert_result(arc_symbols['invalid'], 1.0, fail_reason) + return false, fail_reason + end + + if i == 1 then + -- We need to ensure that cv of seal is equal to 'none' + if seals[i].cv ~= 'none' then + fail_reason = 'cv is not "none" for i=1' + task:insert_result(arc_symbols['invalid'], 1.0, fail_reason) + return false, fail_reason + end + else + if seals[i].cv ~= 'pass' then + fail_reason = string.format('cv is %s on i=%d', seals[i].cv, i) + task:insert_result(arc_symbols['reject'], 1.0, fail_reason) + return true, fail_reason + end + end + end + + return true, nil +end + +local function arc_callback(task) + local arc_sig_headers = task:get_header_full('ARC-Message-Signature') + local arc_seal_headers = task:get_header_full('ARC-Seal') + local arc_ar_headers = task:get_header_full('ARC-Authentication-Results') + + if not arc_sig_headers or not arc_seal_headers then + task:insert_result(arc_symbols['na'], 1.0) + return + end + + 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) + task:insert_result(arc_symbols['invalid'], 1.0, 'invalid count of seals and signatures') + return + end + + local cbdata = { + seals = {}, + sigs = {}, + ars = {}, + res = 'success', + errors = {}, + allowed_by_trusted = false + } + + parse_arc_header(arc_seal_headers, cbdata.seals, false) + parse_arc_header(arc_sig_headers, cbdata.sigs, false) + + if arc_ar_headers then + parse_arc_header(arc_ar_headers, cbdata.ars, true) + end + + -- Fix i type + fun.each(function(hdr) + hdr.i = tonumber(hdr.i) or 0 + end, cbdata.seals) + + fun.each(function(hdr) + hdr.i = tonumber(hdr.i) or 0 + end, cbdata.sigs) + + -- Now we need to sort elements according to their [i] value + table.sort(cbdata.seals, function(e1, e2) + return (e1.i or 0) < (e2.i or 0) + end) + table.sort(cbdata.sigs, function(e1, e2) + return (e1.i or 0) < (e2.i or 0) + end) + + lua_util.debugm(N, task, 'got %s arc sections', #cbdata.seals) + + -- 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) + if not valid then + task:cache_set('arc-failure', validation_error) + return + end + + task:cache_set('arc-sigs', cbdata.sigs) + task:cache_set('arc-seals', cbdata.seals) + task:cache_set('arc-authres', cbdata.ars) + + if validation_error then + -- ARC rejection but no strong failure for signing + return + end + + 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) + + if not res then + cbdata.res = 'fail' + if err and domain then + table.insert(cbdata.errors, string.format('sig:%s:%s', domain, err)) + end + end + + if settings.whitelisted_signers_map and cbdata.res == 'success' then + if settings.whitelisted_signers_map:get_key(sig.d) then + -- Whitelisted signer has been found in a valid chain + local mult = 1.0 + 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) + else + task:cache_set(AR_TRUSTED_CACHE_KEY, cur_aar) + local seen_dmarc + for _, ar in ipairs(cur_aar.ar) do + if ar.dmarc then + local dmarc_fwd = ar.dmarc + seen_dmarc = true + if dmarc_fwd == 'reject' or dmarc_fwd == 'fail' or dmarc_fwd == 'quarantine' then + lua_util.debugm(N, "found rejected dmarc on forwarding") + mult = 0.0 + elseif dmarc_fwd == 'pass' then + mult = 1.0 + end + elseif ar.spf then + local spf_fwd = ar.spf + if spf_fwd == 'reject' or spf_fwd == 'fail' or spf_fwd == 'quarantine' then + lua_util.debugm(N, "found rejected spf on forwarding") + if not seen_dmarc then + mult = mult * 0.5 + end + end + end + end + end + task:insert_result(arc_symbols.trusted_allow, mult, + 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) + 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)) + end + end + end + end + + local function arc_signature_cb(_, res, err, domain) + lua_util.debugm(N, task, 'checked arc signature %s: %s(%s)', + 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)) + end + end + if cbdata.res == 'success' then + -- Verify seals + 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', + sig.d or '', sig.s or '', sig.i or '', lerr)) + lua_util.debugm(N, task, 'checked arc seal %s: %s(%s), %s processed', + sig.d, ret, lerr, i) + end + end + else + task:insert_result(arc_symbols['reject'], 1.0, + rspamd_logger.slog('signature check failed: %s, %s', cbdata.res, + cbdata.errors)) + end + end + + --[[ + 1. Collect all ARC Sets currently attached to the message. If there + are none, the Chain Validation Status is "none" and the algorithm + stops here. The maximum number of ARC Sets that can be attached + to a message is 50. If more than the maximum number exist the + Chain Validation Status is "fail" and the algorithm stops here. + In the following algorithm, the maximum ARC instance value is + referred to as "N". + + 2. If the Chain Validation Status of the highest instance value ARC + Set is "fail", then the Chain Validation status is "fail" and the + algorithm stops here. + + 3. Validate the structure of the Authenticated Received Chain. A + valid ARC has the following conditions: + + 1. Each ARC Set MUST contain exactly one each of the three ARC + header fields (AAR, AMS, and AS). + + 2. The instance values of the ARC Sets MUST form a continuous + sequence from 1..N with no gaps or repetition. + + 3. The "cv" value for all ARC-Seal header fields must be non- + failing. For instance values > 1, the value must be "pass". + For instance value = 1, the value must be "none". + + * If any of these conditions are not met, the Chain Validation + Status is "fail" and the algorithm stops here. + + 4. Validate the AMS with the greatest instance value (most recent). + If validation fails, then the Chain Validation Status is "fail" + and the algorithm stops here. + + 5 - 7. Optional, not implemented + 8. Validate each AS beginning with the greatest instance value and + proceeding in decreasing order to the AS with the instance value + of 1. If any AS fails to validate, the Chain Validation Status + 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)) + 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) + 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)) + end +end + +local opts = rspamd_config:get_all_opt('arc') +if not opts or type(opts) ~= 'table' then + return +end + +if opts['symbols'] then + for k, _ in pairs(arc_symbols) do + if opts['symbols'][k] then + arc_symbols[k] = opts['symbols'][k] + end + end +end + +local id = rspamd_config:register_symbol({ + name = 'ARC_CHECK', + type = 'callback', + group = 'policies', + groups = { 'arc' }, + callback = arc_callback, + augmentations = { lua_util.dns_timeout_augmentation(rspamd_config) }, +}) +rspamd_config:register_symbol({ + name = 'ARC_CALLBACK', -- compatibility symbol + type = 'virtual,skip', + parent = id, +}) + +rspamd_config:register_symbol({ + name = arc_symbols['allow'], + parent = id, + type = 'virtual', + score = -1.0, + group = 'policies', + groups = { 'arc' }, +}) +rspamd_config:register_symbol({ + name = arc_symbols['reject'], + parent = id, + type = 'virtual', + score = 2.0, + group = 'policies', + groups = { 'arc' }, +}) +rspamd_config:register_symbol({ + name = arc_symbols['invalid'], + parent = id, + type = 'virtual', + score = 1.0, + group = 'policies', + groups = { 'arc' }, +}) +rspamd_config:register_symbol({ + name = arc_symbols['dnsfail'], + parent = id, + type = 'virtual', + score = 0.0, + group = 'policies', + groups = { 'arc' }, +}) +rspamd_config:register_symbol({ + name = arc_symbols['na'], + parent = id, + type = 'virtual', + score = 0.0, + group = 'policies', + groups = { 'arc' }, +}) + +rspamd_config:register_dependency('ARC_CHECK', 'SPF_CHECK') +rspamd_config:register_dependency('ARC_CHECK', 'DKIM_CHECK') + +local function arc_sign_seal(task, params, header) + local arc_sigs = task:cache_get('arc-sigs') + local arc_seals = task:cache_get('arc-seals') + local arc_auth_results = task:cache_get('arc-authres') + local cur_auth_results + local privkey + + if params.rawkey then + -- Distinguish between pem and base64 + if string.match(params.rawkey, '^-----BEGIN') then + privkey = rspamd_rsa_privkey.load_pem(params.rawkey) + else + privkey = rspamd_rsa_privkey.load_base64(params.rawkey) + end + elseif params.key then + privkey = rspamd_rsa_privkey.load_file(params.key) + end + + if not privkey then + rspamd_logger.errx(task, 'cannot load private key for signing') + return + end + + if settings.reuse_auth_results then + local ar_header = task:get_header('Authentication-Results') + + if ar_header then + rspamd_logger.debugm(N, task, 'reuse authentication results header for ARC') + cur_auth_results = ar_header + else + rspamd_logger.debugm(N, task, 'cannot reuse authentication results, header is missing') + cur_auth_results = lua_auth_results.gen_auth_results(task, ar_settings) or '' + end + else + cur_auth_results = lua_auth_results.gen_auth_results(task, ar_settings) or '' + end + + local sha_ctx = hash.create_specific('sha256') + + -- Update using previous seals + sigs + AAR + local cur_idx = 1 + if arc_seals then + cur_idx = #arc_seals + 1 + -- We use the cached version per each ARC-* header field individually, already sorted by instance + -- value in ascending order + 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) + 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) + sha_ctx:update(s) + lua_util.debugm(N, task, 'update signature with header: %s', s) + end + if arc_seals[i] then + local s = dkim_canonicalize('ARC-Seal', arc_seals[i].raw_header) + sha_ctx:update(s) + lua_util.debugm(N, task, 'update signature with header: %s', s) + end + end + end + + header = lua_util.fold_header(task, + '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, ';') + + local s = dkim_canonicalize('ARC-Authentication-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) + sha_ctx:update(s) + 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) + 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) + + local nl_type + if task:has_flag("milter") then + nl_type = "lf" + else + nl_type = task:get_newlines_type() + end + + 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)) + + 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) } + }, + -- 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)) +end + +local function prepare_arc_selector(task, sel) + local arc_seals = task:cache_get('arc-seals') + + if not arc_seals then + -- Check if our arc is broken + local failure_reason = task:cache_get('arc-failure') + if failure_reason then + rspamd_logger.infox(task, 'skip ARC as the existing chain is broken: %s', failure_reason) + return false + end + end + + sel.arc_cv = 'none' + sel.arc_idx = 1 + sel.no_cache = true + sel.sign_type = 'arc-sign' + + if arc_seals then + sel.arc_idx = #arc_seals + 1 + + local function default_arc_cv() + if task:cache_get('arc-allow') then + sel.arc_cv = 'pass' + else + sel.arc_cv = 'fail' + end + end + + if settings.reuse_auth_results then + local ar_header = task:get_header('Authentication-Results') + + if ar_header then + local arc_match = string.match(ar_header, 'arc=(%w+)') + + if arc_match then + if arc_match == 'none' or arc_match == 'pass' then + -- none should be converted to `pass` + sel.arc_cv = 'pass' + else + sel.arc_cv = 'fail' + end + else + default_arc_cv() + end + else + -- Cannot reuse, use normal path + default_arc_cv() + end + else + default_arc_cv() + end + + end + + return true +end + +local function do_sign(task, sign_params) + if sign_params.alg and sign_params.alg ~= 'rsa' then + -- No support for ed25519 keys + return + end + + if not prepare_arc_selector(task, sign_params) then + -- Broken arc + return + end + + if settings.check_pubkey then + local resolve_name = sign_params.selector .. "._domainkey." .. sign_params.domain + task:get_resolver():resolve_txt({ + task = task, + name = resolve_name, + callback = function(_, _, results, err) + if not err and results and results[1] then + sign_params.pubkey = results[1] + sign_params.strict_pubkey_check = not settings.allow_pubkey_mismatch + elseif not settings.allow_pubkey_mismatch then + rspamd_logger.errx('public key for domain %s/%s is not found: %s, skip signing', + sign_params.domain, sign_params.selector, err) + return + else + rspamd_logger.infox('public key for domain %s/%s is not found: %s', + 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 + }) + else + local dret, hdr = dkim_sign(task, sign_params) + if dret then + arc_sign_seal(task, sign_params, hdr) + end + end +end + +local function sign_error(task, msg) + rspamd_logger.errx(task, 'signing failure: %s', msg) +end + +local function arc_signing_cb(task) + local ret, selectors = dkim_sign_tools.prepare_dkim_signing(N, task, settings) + + if not ret then + return + end + + if settings.use_redis then + dkim_sign_tools.sign_using_redis(N, task, settings, selectors, do_sign, sign_error) + else + if selectors.vault then + dkim_sign_tools.sign_using_vault(N, task, settings, selectors, do_sign, sign_error) + else + -- TODO: no support for multiple sigs + local cur_selector = selectors[1] + prepare_arc_selector(task, cur_selector) + if ((cur_selector.key or cur_selector.rawkey) and cur_selector.selector) then + if cur_selector.key then + cur_selector.key = lua_util.template(cur_selector.key, { + domain = cur_selector.domain, + selector = cur_selector.selector + }) + + local exists, err = rspamd_util.file_exists(cur_selector.key) + if not exists then + if err and err == 'No such file or directory' then + lua_util.debugm(N, task, 'cannot read key from %s: %s', cur_selector.key, err) + else + rspamd_logger.warnx(task, 'cannot read key from %s: %s', cur_selector.key, err) + end + return false + end + end + + do_sign(task, cur_selector) + else + rspamd_logger.infox(task, 'key path or dkim selector unconfigured; no signing') + return false + end + end + end +end + +dkim_sign_tools.process_signing_settings(N, settings, opts) + +if not dkim_sign_tools.validate_signing_settings(settings) then + rspamd_logger.infox(rspamd_config, 'mandatory parameters missing, disable arc signing') + return +end + +local ar_opts = rspamd_config:get_all_opt('milter_headers') + +if ar_opts and ar_opts.routines then + local routines = ar_opts.routines + + if routines['authentication-results'] then + ar_settings = lua_util.override_defaults(ar_settings, + routines['authentication-results']) + end +end + +if settings.use_redis then + redis_params = rspamd_parse_redis_server('arc') + + 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') + return + end + + settings.redis_params = redis_params +end + +local sym_reg_tbl = { + name = settings['sign_symbol'], + callback = arc_signing_cb, + groups = { "policies", "arc" }, + flags = 'ignore_passthrough', + score = 0.0, +} +if type(settings.allowed_ids) == 'table' then + sym_reg_tbl.allowed_ids = settings.allowed_ids +end +if type(settings.forbidden_ids) == 'table' then + sym_reg_tbl.forbidden_ids = settings.forbidden_ids +end + +if settings.whitelisted_signers_map then + arc_symbols.trusted_allow = arc_symbols.trusted_allow or 'ARC_ALLOW_TRUSTED' + rspamd_config:register_symbol({ + name = arc_symbols.trusted_allow, + parent = id, + type = 'virtual', + score = -2.0, + group = 'policies', + groups = { 'arc' }, + }) +end + +rspamd_config:register_symbol(sym_reg_tbl) + +-- Do not sign unless checked +rspamd_config:register_dependency(settings['sign_symbol'], 'ARC_CHECK') +-- We need to check dmarc before signing as we have to produce valid AAR header +-- see #3613 +rspamd_config:register_dependency(settings['sign_symbol'], 'DMARC_CHECK') + +if settings.adjust_dmarc and settings.whitelisted_signers_map then + local function arc_dmarc_adjust_cb(task) + local trusted_arc_ar = task:cache_get(AR_TRUSTED_CACHE_KEY) + local sym_to_adjust + if task:has_symbol(ar_settings.dmarc_symbols.reject) then + sym_to_adjust = ar_settings.dmarc_symbols.reject + elseif task:has_symbol(ar_settings.dmarc_symbols.quarantine) then + sym_to_adjust = ar_settings.dmarc_symbols.quarantine + end + if sym_to_adjust and trusted_arc_ar and trusted_arc_ar.ar then + for _, ar in ipairs(trusted_arc_ar.ar) do + if ar.dmarc 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']) + task:adjust_result(sym_to_adjust, 0.1, + 'ARC trusted') + end + end + end + end + end + rspamd_config:register_symbol({ + name = 'ARC_DMARC_ADJUSTMENT', + callback = arc_dmarc_adjust_cb, + type = 'callback', + }) + rspamd_config:register_dependency('ARC_DMARC_ADJUSTMENT', 'DMARC_CHECK') + rspamd_config:register_dependency('ARC_DMARC_ADJUSTMENT', 'ARC_CHECK') +end |