You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

arc.lua 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]] --
  13. local rspamd_logger = require "rspamd_logger"
  14. local lua_util = require "lua_util"
  15. local dkim_sign_tools = require "lua_dkim_tools"
  16. local rspamd_util = require "rspamd_util"
  17. local rspamd_rsa_privkey = require "rspamd_rsa_privkey"
  18. local rspamd_rsa = require "rspamd_rsa"
  19. local fun = require "fun"
  20. local lua_auth_results = require "lua_auth_results"
  21. local hash = require "rspamd_cryptobox_hash"
  22. local lua_mime = require "lua_mime"
  23. if confighelp then
  24. return
  25. end
  26. local N = 'arc'
  27. local AR_TRUSTED_CACHE_KEY = 'arc_trusted_aar'
  28. if not rspamd_plugins.dkim then
  29. rspamd_logger.errx(rspamd_config, "cannot enable arc plugin: dkim is disabled")
  30. return
  31. end
  32. local dkim_verify = rspamd_plugins.dkim.verify
  33. local dkim_sign = rspamd_plugins.dkim.sign
  34. local dkim_canonicalize = rspamd_plugins.dkim.canon_header_relaxed
  35. local redis_params
  36. if not dkim_verify or not dkim_sign or not dkim_canonicalize then
  37. rspamd_logger.errx(rspamd_config, "cannot enable arc plugin: dkim is disabled")
  38. return
  39. end
  40. local arc_symbols = {
  41. allow = 'ARC_ALLOW',
  42. invalid = 'ARC_INVALID',
  43. dnsfail = 'ARC_DNSFAIL',
  44. na = 'ARC_NA',
  45. reject = 'ARC_REJECT',
  46. }
  47. local settings = {
  48. allow_envfrom_empty = true,
  49. allow_hdrfrom_mismatch = false,
  50. allow_hdrfrom_mismatch_local = false,
  51. allow_hdrfrom_mismatch_sign_networks = false,
  52. allow_hdrfrom_multiple = false,
  53. allow_username_mismatch = false,
  54. sign_authenticated = true,
  55. domain = {},
  56. path = string.format('%s/%s/%s', rspamd_paths['DBDIR'], 'arc', '$domain.$selector.key'),
  57. sign_local = true,
  58. selector = 'arc',
  59. sign_symbol = 'ARC_SIGNED',
  60. try_fallback = true,
  61. use_domain = 'header',
  62. use_esld = true,
  63. use_redis = false,
  64. key_prefix = 'arc_keys', -- default hash name
  65. reuse_auth_results = false, -- Reuse the existing authentication results
  66. whitelisted_signers_map = nil, -- Trusted signers domains
  67. adjust_dmarc = true, -- Adjust DMARC rejected policy for trusted forwarders
  68. allowed_ids = nil, -- Allowed settings id
  69. forbidden_ids = nil, -- Banned settings id
  70. }
  71. -- To match normal AR
  72. local ar_settings = lua_auth_results.default_settings
  73. local function parse_arc_header(hdr, target, is_aar)
  74. -- Split elements by ';' and trim spaces
  75. local arr = fun.totable(fun.map(
  76. function(val)
  77. return fun.totable(fun.map(lua_util.rspamd_str_trim,
  78. fun.filter(function(v)
  79. return v and #v > 0
  80. end,
  81. lua_util.rspamd_str_split(val.decoded, ';')
  82. )
  83. ))
  84. end, hdr
  85. ))
  86. -- v[1] is the key and v[2] is the value
  87. local function fill_arc_header_table(v, t)
  88. if v[1] and v[2] then
  89. local key = lua_util.rspamd_str_trim(v[1])
  90. local value = lua_util.rspamd_str_trim(v[2])
  91. t[key] = value
  92. end
  93. end
  94. -- Now we have two tables in format:
  95. -- [arc_header] -> [{arc_header1_elts}, {arc_header2_elts}...]
  96. for i, elts in ipairs(arr) do
  97. if not target[i] then
  98. target[i] = {}
  99. end
  100. if not is_aar then
  101. -- For normal ARC headers we split by kv pair, like k=v
  102. fun.each(function(v)
  103. fill_arc_header_table(v, target[i])
  104. end,
  105. fun.map(function(elt)
  106. return lua_util.rspamd_str_split(elt, '=')
  107. end, elts)
  108. )
  109. else
  110. -- For AAR we check special case of i=%d and pass everything else to
  111. -- AAR specific parser
  112. for _, elt in ipairs(elts) do
  113. if string.match(elt, "%s*i%s*=%s*%d+%s*") then
  114. local pair = lua_util.rspamd_str_split(elt, '=')
  115. fill_arc_header_table(pair, target[i])
  116. else
  117. -- Normal element
  118. local ar_elt = lua_auth_results.parse_ar_element(elt)
  119. if ar_elt then
  120. if not target[i].ar then
  121. target[i].ar = {}
  122. end
  123. table.insert(target[i].ar, ar_elt)
  124. end
  125. end
  126. end
  127. end
  128. target[i].header = hdr[i].decoded
  129. target[i].raw_header = hdr[i].value
  130. end
  131. -- sort by i= attribute
  132. table.sort(target, function(a, b)
  133. return (a.i or 0) < (b.i or 0)
  134. end)
  135. end
  136. local function arc_validate_seals(task, seals, sigs, seal_headers, sig_headers)
  137. local fail_reason
  138. for i = 1, #seals do
  139. if (sigs[i].i or 0) ~= i then
  140. fail_reason = string.format('bad i for signature: %d, expected %d; d=%s',
  141. sigs[i].i, i, sigs[i].d)
  142. rspamd_logger.infox(task, fail_reason)
  143. task:insert_result(arc_symbols['invalid'], 1.0, fail_reason)
  144. return false, fail_reason
  145. end
  146. if (seals[i].i or 0) ~= i then
  147. fail_reason = string.format('bad i for seal: %d, expected %d; d=%s',
  148. seals[i].i, i, seals[i].d)
  149. rspamd_logger.infox(task, fail_reason)
  150. task:insert_result(arc_symbols['invalid'], 1.0, fail_reason)
  151. return false, fail_reason
  152. end
  153. if not seals[i].cv then
  154. fail_reason = string.format('no cv on i=%d', i)
  155. task:insert_result(arc_symbols['invalid'], 1.0, fail_reason)
  156. return false, fail_reason
  157. end
  158. if i == 1 then
  159. -- We need to ensure that cv of seal is equal to 'none'
  160. if seals[i].cv ~= 'none' then
  161. fail_reason = 'cv is not "none" for i=1'
  162. task:insert_result(arc_symbols['invalid'], 1.0, fail_reason)
  163. return false, fail_reason
  164. end
  165. else
  166. if seals[i].cv ~= 'pass' then
  167. fail_reason = string.format('cv is %s on i=%d', seals[i].cv, i)
  168. task:insert_result(arc_symbols['reject'], 1.0, fail_reason)
  169. return true, fail_reason
  170. end
  171. end
  172. end
  173. return true, nil
  174. end
  175. local function arc_callback(task)
  176. local arc_sig_headers = task:get_header_full('ARC-Message-Signature')
  177. local arc_seal_headers = task:get_header_full('ARC-Seal')
  178. local arc_ar_headers = task:get_header_full('ARC-Authentication-Results')
  179. if not arc_sig_headers or not arc_seal_headers then
  180. task:insert_result(arc_symbols['na'], 1.0)
  181. return
  182. end
  183. if #arc_sig_headers ~= #arc_seal_headers then
  184. -- We mandate that count of seals is equal to count of signatures
  185. rspamd_logger.infox(task, 'number of seals (%s) is not equal to number of signatures (%s)',
  186. #arc_seal_headers, #arc_sig_headers)
  187. task:insert_result(arc_symbols['invalid'], 1.0, 'invalid count of seals and signatures')
  188. return
  189. end
  190. local cbdata = {
  191. seals = {},
  192. sigs = {},
  193. ars = {},
  194. res = 'success',
  195. errors = {},
  196. allowed_by_trusted = false
  197. }
  198. parse_arc_header(arc_seal_headers, cbdata.seals, false)
  199. parse_arc_header(arc_sig_headers, cbdata.sigs, false)
  200. if arc_ar_headers then
  201. parse_arc_header(arc_ar_headers, cbdata.ars, true)
  202. end
  203. -- Fix i type
  204. fun.each(function(hdr)
  205. hdr.i = tonumber(hdr.i) or 0
  206. end, cbdata.seals)
  207. fun.each(function(hdr)
  208. hdr.i = tonumber(hdr.i) or 0
  209. end, cbdata.sigs)
  210. -- Now we need to sort elements according to their [i] value
  211. table.sort(cbdata.seals, function(e1, e2)
  212. return (e1.i or 0) < (e2.i or 0)
  213. end)
  214. table.sort(cbdata.sigs, function(e1, e2)
  215. return (e1.i or 0) < (e2.i or 0)
  216. end)
  217. lua_util.debugm(N, task, 'got %s arc sections', #cbdata.seals)
  218. -- Now check sanity of what we have
  219. local valid, validation_error = arc_validate_seals(task, cbdata.seals, cbdata.sigs,
  220. arc_seal_headers, arc_sig_headers)
  221. if not valid then
  222. task:cache_set('arc-failure', validation_error)
  223. return
  224. end
  225. task:cache_set('arc-sigs', cbdata.sigs)
  226. task:cache_set('arc-seals', cbdata.seals)
  227. task:cache_set('arc-authres', cbdata.ars)
  228. if validation_error then
  229. -- ARC rejection but no strong failure for signing
  230. return
  231. end
  232. local function gen_arc_seal_cb(index, sig)
  233. return function(_, res, err, domain)
  234. lua_util.debugm(N, task, 'checked arc seal: %s(%s), %s processed',
  235. res, err, index)
  236. if not res then
  237. cbdata.res = 'fail'
  238. if err and domain then
  239. table.insert(cbdata.errors, string.format('sig:%s:%s', domain, err))
  240. end
  241. end
  242. if settings.whitelisted_signers_map and cbdata.res == 'success' then
  243. if settings.whitelisted_signers_map:get_key(sig.d) then
  244. -- Whitelisted signer has been found in a valid chain
  245. local mult = 1.0
  246. local cur_aar = cbdata.ars[index]
  247. if not cur_aar then
  248. rspamd_logger.warnx(task, "cannot find Arc-Authentication-Results for trusted " ..
  249. "forwarder %s on i=%s", domain, cbdata.index)
  250. else
  251. task:cache_set(AR_TRUSTED_CACHE_KEY, cur_aar)
  252. local seen_dmarc
  253. for _, ar in ipairs(cur_aar.ar) do
  254. if ar.dmarc then
  255. local dmarc_fwd = ar.dmarc
  256. seen_dmarc = true
  257. if dmarc_fwd == 'reject' or dmarc_fwd == 'fail' or dmarc_fwd == 'quarantine' then
  258. lua_util.debugm(N, "found rejected dmarc on forwarding")
  259. mult = 0.0
  260. elseif dmarc_fwd == 'pass' then
  261. mult = 1.0
  262. end
  263. elseif ar.spf then
  264. local spf_fwd = ar.spf
  265. if spf_fwd == 'reject' or spf_fwd == 'fail' or spf_fwd == 'quarantine' then
  266. lua_util.debugm(N, "found rejected spf on forwarding")
  267. if not seen_dmarc then
  268. mult = mult * 0.5
  269. end
  270. end
  271. end
  272. end
  273. end
  274. task:insert_result(arc_symbols.trusted_allow, mult,
  275. string.format('%s:s=%s:i=%d', domain, sig.s, index))
  276. end
  277. end
  278. if index == #arc_sig_headers then
  279. if cbdata.res == 'success' then
  280. local arc_allow_result = string.format('%s:s=%s:i=%d',
  281. domain, sig.s, index)
  282. task:insert_result(arc_symbols.allow, 1.0, arc_allow_result)
  283. task:cache_set('arc-allow', arc_allow_result)
  284. else
  285. task:insert_result(arc_symbols.reject, 1.0,
  286. rspamd_logger.slog('seal check failed: %s, %s', cbdata.res,
  287. cbdata.errors))
  288. end
  289. end
  290. end
  291. end
  292. local function arc_signature_cb(_, res, err, domain)
  293. lua_util.debugm(N, task, 'checked arc signature %s: %s(%s)',
  294. domain, res, err)
  295. if not res then
  296. cbdata.res = 'fail'
  297. if err and domain then
  298. table.insert(cbdata.errors, string.format('sig:%s:%s', domain, err))
  299. end
  300. end
  301. if cbdata.res == 'success' then
  302. -- Verify seals
  303. for i, sig in ipairs(cbdata.seals) do
  304. local ret, lerr = dkim_verify(task, sig.header, gen_arc_seal_cb(i, sig), 'arc-seal')
  305. if not ret then
  306. cbdata.res = 'fail'
  307. table.insert(cbdata.errors, string.format('seal:%s:s=%s:i=%s:%s',
  308. sig.d or '', sig.s or '', sig.i or '', lerr))
  309. lua_util.debugm(N, task, 'checked arc seal %s: %s(%s), %s processed',
  310. sig.d, ret, lerr, i)
  311. end
  312. end
  313. else
  314. task:insert_result(arc_symbols['reject'], 1.0,
  315. rspamd_logger.slog('signature check failed: %s, %s', cbdata.res,
  316. cbdata.errors))
  317. end
  318. end
  319. --[[
  320. 1. Collect all ARC Sets currently attached to the message. If there
  321. are none, the Chain Validation Status is "none" and the algorithm
  322. stops here. The maximum number of ARC Sets that can be attached
  323. to a message is 50. If more than the maximum number exist the
  324. Chain Validation Status is "fail" and the algorithm stops here.
  325. In the following algorithm, the maximum ARC instance value is
  326. referred to as "N".
  327. 2. If the Chain Validation Status of the highest instance value ARC
  328. Set is "fail", then the Chain Validation status is "fail" and the
  329. algorithm stops here.
  330. 3. Validate the structure of the Authenticated Received Chain. A
  331. valid ARC has the following conditions:
  332. 1. Each ARC Set MUST contain exactly one each of the three ARC
  333. header fields (AAR, AMS, and AS).
  334. 2. The instance values of the ARC Sets MUST form a continuous
  335. sequence from 1..N with no gaps or repetition.
  336. 3. The "cv" value for all ARC-Seal header fields must be non-
  337. failing. For instance values > 1, the value must be "pass".
  338. For instance value = 1, the value must be "none".
  339. * If any of these conditions are not met, the Chain Validation
  340. Status is "fail" and the algorithm stops here.
  341. 4. Validate the AMS with the greatest instance value (most recent).
  342. If validation fails, then the Chain Validation Status is "fail"
  343. and the algorithm stops here.
  344. 5 - 7. Optional, not implemented
  345. 8. Validate each AS beginning with the greatest instance value and
  346. proceeding in decreasing order to the AS with the instance value
  347. of 1. If any AS fails to validate, the Chain Validation Status
  348. is "fail" and the algorithm stops here.
  349. 9. If the algorithm reaches this step, then the Chain Validation
  350. Status is "pass", and the algorithm is complete.
  351. ]]--
  352. local processed = 0
  353. local sig = cbdata.sigs[#cbdata.sigs] -- last AMS
  354. local ret, err = dkim_verify(task, sig.header, arc_signature_cb, 'arc-sign')
  355. if not ret then
  356. cbdata.res = 'fail'
  357. table.insert(cbdata.errors, string.format('sig:%s:%s', sig.d or '', err))
  358. else
  359. processed = processed + 1
  360. lua_util.debugm(N, task, 'processed arc signature %s[%s]: %s(%s), %s total',
  361. sig.d, sig.i, ret, err, #cbdata.seals)
  362. end
  363. if processed == 0 then
  364. task:insert_result(arc_symbols['reject'], 1.0,
  365. rspamd_logger.slog('cannot verify %s of %s signatures: %s',
  366. #arc_sig_headers - processed, #arc_sig_headers, cbdata.errors))
  367. end
  368. end
  369. local opts = rspamd_config:get_all_opt('arc')
  370. if not opts or type(opts) ~= 'table' then
  371. return
  372. end
  373. if opts['symbols'] then
  374. for k, _ in pairs(arc_symbols) do
  375. if opts['symbols'][k] then
  376. arc_symbols[k] = opts['symbols'][k]
  377. end
  378. end
  379. end
  380. local id = rspamd_config:register_symbol({
  381. name = 'ARC_CHECK',
  382. type = 'callback',
  383. group = 'policies',
  384. groups = { 'arc' },
  385. callback = arc_callback,
  386. augmentations = { lua_util.dns_timeout_augmentation(rspamd_config) },
  387. })
  388. rspamd_config:register_symbol({
  389. name = 'ARC_CALLBACK', -- compatibility symbol
  390. type = 'virtual,skip',
  391. parent = id,
  392. })
  393. rspamd_config:register_symbol({
  394. name = arc_symbols['allow'],
  395. parent = id,
  396. type = 'virtual',
  397. score = -1.0,
  398. group = 'policies',
  399. groups = { 'arc' },
  400. })
  401. rspamd_config:register_symbol({
  402. name = arc_symbols['reject'],
  403. parent = id,
  404. type = 'virtual',
  405. score = 2.0,
  406. group = 'policies',
  407. groups = { 'arc' },
  408. })
  409. rspamd_config:register_symbol({
  410. name = arc_symbols['invalid'],
  411. parent = id,
  412. type = 'virtual',
  413. score = 1.0,
  414. group = 'policies',
  415. groups = { 'arc' },
  416. })
  417. rspamd_config:register_symbol({
  418. name = arc_symbols['dnsfail'],
  419. parent = id,
  420. type = 'virtual',
  421. score = 0.0,
  422. group = 'policies',
  423. groups = { 'arc' },
  424. })
  425. rspamd_config:register_symbol({
  426. name = arc_symbols['na'],
  427. parent = id,
  428. type = 'virtual',
  429. score = 0.0,
  430. group = 'policies',
  431. groups = { 'arc' },
  432. })
  433. rspamd_config:register_dependency('ARC_CHECK', 'SPF_CHECK')
  434. rspamd_config:register_dependency('ARC_CHECK', 'DKIM_CHECK')
  435. local function arc_sign_seal(task, params, header)
  436. local arc_sigs = task:cache_get('arc-sigs')
  437. local arc_seals = task:cache_get('arc-seals')
  438. local arc_auth_results = task:cache_get('arc-authres')
  439. local cur_auth_results
  440. local privkey
  441. if params.rawkey then
  442. -- Distinguish between pem and base64
  443. if string.match(params.rawkey, '^-----BEGIN') then
  444. privkey = rspamd_rsa_privkey.load_pem(params.rawkey)
  445. else
  446. privkey = rspamd_rsa_privkey.load_base64(params.rawkey)
  447. end
  448. elseif params.key then
  449. privkey = rspamd_rsa_privkey.load_file(params.key)
  450. end
  451. if not privkey then
  452. rspamd_logger.errx(task, 'cannot load private key for signing')
  453. return
  454. end
  455. if settings.reuse_auth_results then
  456. local ar_header = task:get_header('Authentication-Results')
  457. if ar_header then
  458. rspamd_logger.debugm(N, task, 'reuse authentication results header for ARC')
  459. cur_auth_results = ar_header
  460. else
  461. rspamd_logger.debugm(N, task, 'cannot reuse authentication results, header is missing')
  462. cur_auth_results = lua_auth_results.gen_auth_results(task, ar_settings) or ''
  463. end
  464. else
  465. cur_auth_results = lua_auth_results.gen_auth_results(task, ar_settings) or ''
  466. end
  467. local sha_ctx = hash.create_specific('sha256')
  468. -- Update using previous seals + sigs + AAR
  469. local cur_idx = 1
  470. if arc_seals then
  471. cur_idx = #arc_seals + 1
  472. -- We use the cached version per each ARC-* header field individually, already sorted by instance
  473. -- value in ascending order
  474. for i = 1, #arc_seals, 1 do
  475. if arc_auth_results[i] then
  476. local s = dkim_canonicalize('ARC-Authentication-Results',
  477. arc_auth_results[i].raw_header)
  478. sha_ctx:update(s)
  479. lua_util.debugm(N, task, 'update signature with header: %s', s)
  480. end
  481. if arc_sigs[i] then
  482. local s = dkim_canonicalize('ARC-Message-Signature',
  483. arc_sigs[i].raw_header)
  484. sha_ctx:update(s)
  485. lua_util.debugm(N, task, 'update signature with header: %s', s)
  486. end
  487. if arc_seals[i] then
  488. local s = dkim_canonicalize('ARC-Seal', arc_seals[i].raw_header)
  489. sha_ctx:update(s)
  490. lua_util.debugm(N, task, 'update signature with header: %s', s)
  491. end
  492. end
  493. end
  494. header = lua_util.fold_header(task,
  495. 'ARC-Message-Signature',
  496. header)
  497. cur_auth_results = string.format('i=%d; %s', cur_idx, cur_auth_results)
  498. cur_auth_results = lua_util.fold_header(task,
  499. 'ARC-Authentication-Results',
  500. cur_auth_results, ';')
  501. local s = dkim_canonicalize('ARC-Authentication-Results',
  502. cur_auth_results)
  503. sha_ctx:update(s)
  504. lua_util.debugm(N, task, 'update signature with header: %s', s)
  505. s = dkim_canonicalize('ARC-Message-Signature', header)
  506. sha_ctx:update(s)
  507. lua_util.debugm(N, task, 'update signature with header: %s', s)
  508. local cur_arc_seal = string.format('i=%d; s=%s; d=%s; t=%d; a=rsa-sha256; cv=%s; b=',
  509. cur_idx,
  510. params.selector,
  511. params.domain,
  512. math.floor(rspamd_util.get_time()), params.arc_cv)
  513. s = string.format('%s:%s', 'arc-seal', cur_arc_seal)
  514. sha_ctx:update(s)
  515. lua_util.debugm(N, task, 'initial update signature with header: %s', s)
  516. local nl_type
  517. if task:has_flag("milter") then
  518. nl_type = "lf"
  519. else
  520. nl_type = task:get_newlines_type()
  521. end
  522. local sig = rspamd_rsa.sign_memory(privkey, sha_ctx:bin())
  523. cur_arc_seal = string.format('%s%s', cur_arc_seal,
  524. sig:base64(70, nl_type))
  525. lua_mime.modify_headers(task, {
  526. add = {
  527. ['ARC-Authentication-Results'] = { order = 1, value = cur_auth_results },
  528. ['ARC-Message-Signature'] = { order = 1, value = header },
  529. ['ARC-Seal'] = { order = 1, value = lua_util.fold_header(task,
  530. 'ARC-Seal', cur_arc_seal) }
  531. },
  532. -- RFC requires a strict order for these headers to be inserted
  533. order = { 'ARC-Authentication-Results', 'ARC-Message-Signature', 'ARC-Seal' },
  534. })
  535. task:insert_result(settings.sign_symbol, 1.0,
  536. string.format('%s:s=%s:i=%d', params.domain, params.selector, cur_idx))
  537. end
  538. local function prepare_arc_selector(task, sel)
  539. local arc_seals = task:cache_get('arc-seals')
  540. if not arc_seals then
  541. -- Check if our arc is broken
  542. local failure_reason = task:cache_get('arc-failure')
  543. if failure_reason then
  544. rspamd_logger.infox(task, 'skip ARC as the existing chain is broken: %s', failure_reason)
  545. return false
  546. end
  547. end
  548. sel.arc_cv = 'none'
  549. sel.arc_idx = 1
  550. sel.no_cache = true
  551. sel.sign_type = 'arc-sign'
  552. if arc_seals then
  553. sel.arc_idx = #arc_seals + 1
  554. local function default_arc_cv()
  555. if task:cache_get('arc-allow') then
  556. sel.arc_cv = 'pass'
  557. else
  558. sel.arc_cv = 'fail'
  559. end
  560. end
  561. if settings.reuse_auth_results then
  562. local ar_header = task:get_header('Authentication-Results')
  563. if ar_header then
  564. local arc_match = string.match(ar_header, 'arc=(%w+)')
  565. if arc_match then
  566. if arc_match == 'none' or arc_match == 'pass' then
  567. -- none should be converted to `pass`
  568. sel.arc_cv = 'pass'
  569. else
  570. sel.arc_cv = 'fail'
  571. end
  572. else
  573. default_arc_cv()
  574. end
  575. else
  576. -- Cannot reuse, use normal path
  577. default_arc_cv()
  578. end
  579. else
  580. default_arc_cv()
  581. end
  582. end
  583. return true
  584. end
  585. local function do_sign(task, sign_params)
  586. if sign_params.alg and sign_params.alg ~= 'rsa' then
  587. -- No support for ed25519 keys
  588. return
  589. end
  590. if not prepare_arc_selector(task, sign_params) then
  591. -- Broken arc
  592. return
  593. end
  594. if settings.check_pubkey then
  595. local resolve_name = sign_params.selector .. "._domainkey." .. sign_params.domain
  596. task:get_resolver():resolve_txt({
  597. task = task,
  598. name = resolve_name,
  599. callback = function(_, _, results, err)
  600. if not err and results and results[1] then
  601. sign_params.pubkey = results[1]
  602. sign_params.strict_pubkey_check = not settings.allow_pubkey_mismatch
  603. elseif not settings.allow_pubkey_mismatch then
  604. rspamd_logger.errx('public key for domain %s/%s is not found: %s, skip signing',
  605. sign_params.domain, sign_params.selector, err)
  606. return
  607. else
  608. rspamd_logger.infox('public key for domain %s/%s is not found: %s',
  609. sign_params.domain, sign_params.selector, err)
  610. end
  611. local dret, hdr = dkim_sign(task, sign_params)
  612. if dret then
  613. arc_sign_seal(task, sign_params, hdr)
  614. end
  615. end,
  616. forced = true
  617. })
  618. else
  619. local dret, hdr = dkim_sign(task, sign_params)
  620. if dret then
  621. arc_sign_seal(task, sign_params, hdr)
  622. end
  623. end
  624. end
  625. local function sign_error(task, msg)
  626. rspamd_logger.errx(task, 'signing failure: %s', msg)
  627. end
  628. local function arc_signing_cb(task)
  629. local ret, selectors = dkim_sign_tools.prepare_dkim_signing(N, task, settings)
  630. if not ret then
  631. return
  632. end
  633. if settings.use_redis then
  634. dkim_sign_tools.sign_using_redis(N, task, settings, selectors, do_sign, sign_error)
  635. else
  636. if selectors.vault then
  637. dkim_sign_tools.sign_using_vault(N, task, settings, selectors, do_sign, sign_error)
  638. else
  639. -- TODO: no support for multiple sigs
  640. local cur_selector = selectors[1]
  641. prepare_arc_selector(task, cur_selector)
  642. if ((cur_selector.key or cur_selector.rawkey) and cur_selector.selector) then
  643. if cur_selector.key then
  644. cur_selector.key = lua_util.template(cur_selector.key, {
  645. domain = cur_selector.domain,
  646. selector = cur_selector.selector
  647. })
  648. local exists, err = rspamd_util.file_exists(cur_selector.key)
  649. if not exists then
  650. if err and err == 'No such file or directory' then
  651. lua_util.debugm(N, task, 'cannot read key from %s: %s', cur_selector.key, err)
  652. else
  653. rspamd_logger.warnx(task, 'cannot read key from %s: %s', cur_selector.key, err)
  654. end
  655. return false
  656. end
  657. end
  658. do_sign(task, cur_selector)
  659. else
  660. rspamd_logger.infox(task, 'key path or dkim selector unconfigured; no signing')
  661. return false
  662. end
  663. end
  664. end
  665. end
  666. dkim_sign_tools.process_signing_settings(N, settings, opts)
  667. if not dkim_sign_tools.validate_signing_settings(settings) then
  668. rspamd_logger.infox(rspamd_config, 'mandatory parameters missing, disable arc signing')
  669. return
  670. end
  671. local ar_opts = rspamd_config:get_all_opt('milter_headers')
  672. if ar_opts and ar_opts.routines then
  673. local routines = ar_opts.routines
  674. if routines['authentication-results'] then
  675. ar_settings = lua_util.override_defaults(ar_settings,
  676. routines['authentication-results'])
  677. end
  678. end
  679. if settings.use_redis then
  680. redis_params = rspamd_parse_redis_server('arc')
  681. if not redis_params then
  682. rspamd_logger.errx(rspamd_config, 'no servers are specified, ' ..
  683. 'but module is configured to load keys from redis, disable arc signing')
  684. return
  685. end
  686. settings.redis_params = redis_params
  687. end
  688. local sym_reg_tbl = {
  689. name = settings['sign_symbol'],
  690. callback = arc_signing_cb,
  691. groups = { "policies", "arc" },
  692. flags = 'ignore_passthrough',
  693. score = 0.0,
  694. }
  695. if type(settings.allowed_ids) == 'table' then
  696. sym_reg_tbl.allowed_ids = settings.allowed_ids
  697. end
  698. if type(settings.forbidden_ids) == 'table' then
  699. sym_reg_tbl.forbidden_ids = settings.forbidden_ids
  700. end
  701. if settings.whitelisted_signers_map then
  702. arc_symbols.trusted_allow = arc_symbols.trusted_allow or 'ARC_ALLOW_TRUSTED'
  703. rspamd_config:register_symbol({
  704. name = arc_symbols.trusted_allow,
  705. parent = id,
  706. type = 'virtual',
  707. score = -2.0,
  708. group = 'policies',
  709. groups = { 'arc' },
  710. })
  711. end
  712. rspamd_config:register_symbol(sym_reg_tbl)
  713. -- Do not sign unless checked
  714. rspamd_config:register_dependency(settings['sign_symbol'], 'ARC_CHECK')
  715. -- We need to check dmarc before signing as we have to produce valid AAR header
  716. -- see #3613
  717. rspamd_config:register_dependency(settings['sign_symbol'], 'DMARC_CHECK')
  718. if settings.adjust_dmarc and settings.whitelisted_signers_map then
  719. local function arc_dmarc_adjust_cb(task)
  720. local trusted_arc_ar = task:cache_get(AR_TRUSTED_CACHE_KEY)
  721. local sym_to_adjust
  722. if task:has_symbol(ar_settings.dmarc_symbols.reject) then
  723. sym_to_adjust = ar_settings.dmarc_symbols.reject
  724. elseif task:has_symbol(ar_settings.dmarc_symbols.quarantine) then
  725. sym_to_adjust = ar_settings.dmarc_symbols.quarantine
  726. end
  727. if sym_to_adjust and trusted_arc_ar and trusted_arc_ar.ar then
  728. for _, ar in ipairs(trusted_arc_ar.ar) do
  729. if ar.dmarc then
  730. local dmarc_fwd = ar.dmarc
  731. if dmarc_fwd == 'pass' then
  732. rspamd_logger.infox(task, "adjust dmarc reject score as trusted forwarder "
  733. .. "proved DMARC validity for %s", ar['header.from'])
  734. task:adjust_result(sym_to_adjust, 0.1,
  735. 'ARC trusted')
  736. end
  737. end
  738. end
  739. end
  740. end
  741. rspamd_config:register_symbol({
  742. name = 'ARC_DMARC_ADJUSTMENT',
  743. callback = arc_dmarc_adjust_cb,
  744. type = 'callback',
  745. })
  746. rspamd_config:register_dependency('ARC_DMARC_ADJUSTMENT', 'DMARC_CHECK')
  747. rspamd_config:register_dependency('ARC_DMARC_ADJUSTMENT', 'ARC_CHECK')
  748. end