dkim_signing.lua 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. --[[
  2. Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
  3. Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. local rspamd_logger = require "rspamd_logger"
  15. local rspamd_util = require "rspamd_util"
  16. if confighelp then
  17. return
  18. end
  19. local settings = {
  20. allow_envfrom_empty = true,
  21. allow_hdrfrom_mismatch = false,
  22. allow_hdrfrom_mismatch_local = false,
  23. allow_hdrfrom_mismatch_sign_networks = false,
  24. allow_hdrfrom_multiple = false,
  25. allow_username_mismatch = false,
  26. auth_only = true,
  27. domain = {},
  28. path = string.format('%s/%s/%s', rspamd_paths['DBDIR'], 'dkim', '$domain.$selector.key'),
  29. sign_local = true,
  30. selector = 'dkim',
  31. symbol = 'DKIM_SIGNED',
  32. try_fallback = true,
  33. use_domain = 'header',
  34. use_esld = true,
  35. use_redis = false,
  36. key_prefix = 'dkim_keys', -- default hash name
  37. }
  38. local E = {}
  39. local N = 'dkim_signing'
  40. local redis_params
  41. local function simple_template(tmpl, keys)
  42. local lpeg = require "lpeg"
  43. local var_lit = lpeg.P { lpeg.R("az") + lpeg.R("AZ") + lpeg.R("09") + "_" }
  44. local var = lpeg.P { (lpeg.P("$") / "") * ((var_lit^1) / keys) }
  45. local var_braced = lpeg.P { (lpeg.P("${") / "") * ((var_lit^1) / keys) * (lpeg.P("}") / "") }
  46. local template_grammar = lpeg.Cs((var + var_braced + 1)^0)
  47. return lpeg.match(template_grammar, tmpl)
  48. end
  49. local function dkim_signing_cb(task)
  50. local is_local, is_sign_networks
  51. local auser = task:get_user()
  52. local ip = task:get_from_ip()
  53. if ip and ip:is_local() then
  54. is_local = true
  55. end
  56. if settings.auth_only and not auser then
  57. if (settings.sign_networks and settings.sign_networks:get_key(ip)) then
  58. is_sign_networks = true
  59. rspamd_logger.debugm(N, task, 'mail is from address in sign_networks')
  60. elseif settings.sign_local and is_local then
  61. rspamd_logger.debugm(N, task, 'mail is from local address')
  62. else
  63. rspamd_logger.debugm(N, task, 'ignoring unauthenticated mail')
  64. return
  65. end
  66. end
  67. local efrom = task:get_from('smtp')
  68. if not settings.allow_envfrom_empty and
  69. #(((efrom or E)[1] or E).addr or '') == 0 then
  70. rspamd_logger.debugm(N, task, 'empty envelope from not allowed')
  71. return false
  72. end
  73. local hfrom = task:get_from('mime')
  74. if not settings.allow_hdrfrom_multiple and (hfrom or E)[2] then
  75. rspamd_logger.debugm(N, task, 'multiple header from not allowed')
  76. return false
  77. end
  78. local dkim_domain
  79. local hdom = ((hfrom or E)[1] or E).domain
  80. local edom = ((efrom or E)[1] or E).domain
  81. if hdom then
  82. hdom = hdom:lower()
  83. end
  84. if edom then
  85. edom = edom:lower()
  86. end
  87. if settings.use_domain_sign_networks and is_sign_networks then
  88. if settings.use_domain_sign_networks == 'header' then
  89. dkim_domain = hdom
  90. else
  91. dkim_domain = edom
  92. end
  93. elseif settings.use_domain_local and is_local then
  94. if settings.use_domain_local == 'header' then
  95. dkim_domain = hdom
  96. else
  97. dkim_domain = edom
  98. end
  99. else
  100. if settings.use_domain == 'header' then
  101. dkim_domain = hdom
  102. else
  103. dkim_domain = edom
  104. end
  105. end
  106. if not dkim_domain then
  107. rspamd_logger.debugm(N, task, 'could not extract dkim domain')
  108. return false
  109. end
  110. if settings.use_esld then
  111. dkim_domain = rspamd_util.get_tld(dkim_domain)
  112. if settings.use_domain == 'envelope' and hdom then
  113. hdom = rspamd_util.get_tld(hdom)
  114. elseif settings.use_domain == 'header' and edom then
  115. edom = rspamd_util.get_tld(edom)
  116. end
  117. end
  118. if edom and hdom and not settings.allow_hdrfrom_mismatch and hdom ~= edom then
  119. if settings.allow_hdrfrom_mismatch_local and is_local then
  120. rspamd_logger.debugm(N, task, 'domain mismatch allowed for local IP: %1 != %2', hdom, edom)
  121. elseif settings.allow_hdrfrom_mismatch_sign_networks and is_sign_networks then
  122. rspamd_logger.debugm(N, task, 'domain mismatch allowed for sign_networks: %1 != %2', hdom, edom)
  123. else
  124. rspamd_logger.debugm(N, task, 'domain mismatch not allowed: %1 != %2', hdom, edom)
  125. return false
  126. end
  127. end
  128. if auser and not settings.allow_username_mismatch then
  129. local udom = string.match(auser, '.*@(.*)')
  130. if not udom then
  131. rspamd_logger.debugm(N, task, 'couldnt find domain in username')
  132. return false
  133. end
  134. if settings.use_esld then
  135. udom = rspamd_util.get_tld(udom)
  136. end
  137. if udom ~= dkim_domain then
  138. rspamd_logger.debugm(N, task, 'user domain mismatch')
  139. return false
  140. end
  141. end
  142. local p = {}
  143. if settings.domain[dkim_domain] then
  144. p.selector = settings.domain[dkim_domain].selector
  145. p.key = settings.domain[dkim_domain].path
  146. end
  147. if not (p.key and p.selector) and not
  148. (settings.try_fallback or settings.use_redis or settings.selector_map or settings.path_map) then
  149. rspamd_logger.debugm(N, task, 'dkim unconfigured and fallback disabled')
  150. return false
  151. end
  152. if not p.key then
  153. if not settings.use_redis then
  154. p.key = settings.path
  155. end
  156. end
  157. if not p.selector then
  158. p.selector = settings.selector
  159. end
  160. p.domain = dkim_domain
  161. if settings.selector_map then
  162. local data = settings.selector_map:get_key(dkim_domain)
  163. if data then
  164. p.selector = data
  165. end
  166. end
  167. if settings.path_map then
  168. local data = settings.path_map:get_key(dkim_domain)
  169. if data then
  170. p.key = data
  171. end
  172. end
  173. if settings.use_redis then
  174. local function try_redis_key(selector)
  175. p.key = nil
  176. p.selector = selector
  177. local rk = string.format('%s.%s', p.selector, p.domain)
  178. local function redis_key_cb(err, data)
  179. if err or type(data) ~= 'string' then
  180. rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM key for %s: %s",
  181. rk, err)
  182. else
  183. p.rawkey = data
  184. if rspamd_plugins.dkim.sign(task, p) then
  185. task:insert_result(settings.symbol, 1.0)
  186. end
  187. end
  188. end
  189. local ret = rspamd_redis_make_request(task,
  190. redis_params, -- connect params
  191. rk, -- hash key
  192. false, -- is write
  193. redis_key_cb, --callback
  194. 'HGET', -- command
  195. {settings.key_prefix, rk} -- arguments
  196. )
  197. if not ret then
  198. rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM key for %s", rk)
  199. end
  200. end
  201. if settings.selector_prefix then
  202. rspamd_logger.infox(rspamd_config, "Using selector prefix %s for domain %s", settings.selector_prefix, p.domain);
  203. local function redis_selector_cb(err, data)
  204. if err or type(data) ~= 'string' then
  205. rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM selector for domain %s: %s", p.domain, err)
  206. else
  207. try_redis_key(data)
  208. end
  209. end
  210. local ret = rspamd_redis_make_request(task,
  211. redis_params, -- connect params
  212. p.domain, -- hash key
  213. false, -- is write
  214. redis_selector_cb, --callback
  215. 'HGET', -- command
  216. {settings.selector_prefix, p.domain} -- arguments
  217. )
  218. if not ret then
  219. rspamd_logger.infox(rspamd_config, "cannot make request to load DKIM selector for %s", p.domain)
  220. end
  221. else
  222. if not p.selector then
  223. rspamd_logger.errx(task, 'No selector specified')
  224. return false
  225. end
  226. try_redis_key(p.selector)
  227. end
  228. else
  229. if (p.key and p.selector) then
  230. p.key = simple_template(p.key, {domain = p.domain, selector = p.selector})
  231. return rspamd_plugins.dkim.sign(task, p)
  232. else
  233. rspamd_logger.infox(task, 'key path or dkim selector unconfigured; no signing')
  234. return false
  235. end
  236. end
  237. end
  238. local opts = rspamd_config:get_all_opt('dkim_signing')
  239. if not opts then return end
  240. for k,v in pairs(opts) do
  241. if k == 'sign_networks' then
  242. settings[k] = rspamd_map_add(N, k, 'radix', 'DKIM signing networks')
  243. elseif k == 'path_map' then
  244. settings[k] = rspamd_map_add(N, k, 'map', 'Paths to DKIM signing keys')
  245. elseif k == 'selector_map' then
  246. settings[k] = rspamd_map_add(N, k, 'map', 'DKIM selectors')
  247. else
  248. settings[k] = v
  249. end
  250. end
  251. if not (settings.use_redis or settings.path or settings.domain or settings.path_map or settings.selector_map) then
  252. rspamd_logger.infox(rspamd_config, 'mandatory parameters missing, disable dkim signing')
  253. return
  254. end
  255. if settings.use_redis then
  256. redis_params = rspamd_parse_redis_server('dkim_signing')
  257. if not redis_params then
  258. rspamd_logger.errx(rspamd_config, 'no servers are specified, but module is configured to load keys from redis, disable dkim signing')
  259. return
  260. end
  261. end
  262. if settings.use_domain ~= 'header' and settings.use_domain ~= 'envelope' then
  263. rspamd_logger.errx(rspamd_config, "Value for 'use_domain' is invalid")
  264. settings.use_domain = 'header'
  265. end
  266. rspamd_config:register_symbol({
  267. name = settings['symbol'],
  268. callback = dkim_signing_cb
  269. })