summaryrefslogtreecommitdiffstats
path: root/src/plugins/dkim_check.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/dkim_check.c')
-rw-r--r--src/plugins/dkim_check.c1620
1 files changed, 1620 insertions, 0 deletions
diff --git a/src/plugins/dkim_check.c b/src/plugins/dkim_check.c
new file mode 100644
index 0000000..29ab34d
--- /dev/null
+++ b/src/plugins/dkim_check.c
@@ -0,0 +1,1620 @@
+/*-
+ * Copyright 2016 Vsevolod Stakhov
+ *
+ * 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.
+ */
+/***MODULE:dkim
+ * rspamd module that checks dkim records of incoming email
+ *
+ * Allowed options:
+ * - symbol_allow (string): symbol to insert in case of allow (default: 'R_DKIM_ALLOW')
+ * - symbol_reject (string): symbol to insert (default: 'R_DKIM_REJECT')
+ * - symbol_tempfail (string): symbol to insert in case of temporary fail (default: 'R_DKIM_TEMPFAIL')
+ * - symbol_permfail (string): symbol to insert in case of permanent failure (default: 'R_DKIM_PERMFAIL')
+ * - symbol_na (string): symbol to insert in case of no signing (default: 'R_DKIM_NA')
+ * - whitelist (map): map of whitelisted networks
+ * - domains (map): map of domains to check
+ * - strict_multiplier (number): multiplier for strict domains
+ * - time_jitter (number): jitter in seconds to allow time diff while checking
+ * - trusted_only (flag): check signatures only for domains in 'domains' map
+ */
+
+
+#include "config.h"
+#include "libmime/message.h"
+#include "libserver/dkim.h"
+#include "libutil/hash.h"
+#include "libserver/maps/map.h"
+#include "libserver/maps/map_helpers.h"
+#include "rspamd.h"
+#include "utlist.h"
+#include "unix-std.h"
+#include "lua/lua_common.h"
+#include "libserver/mempool_vars_internal.h"
+
+#define DEFAULT_SYMBOL_REJECT "R_DKIM_REJECT"
+#define DEFAULT_SYMBOL_TEMPFAIL "R_DKIM_TEMPFAIL"
+#define DEFAULT_SYMBOL_ALLOW "R_DKIM_ALLOW"
+#define DEFAULT_SYMBOL_NA "R_DKIM_NA"
+#define DEFAULT_SYMBOL_PERMFAIL "R_DKIM_PERMFAIL"
+#define DEFAULT_CACHE_SIZE 2048
+#define DEFAULT_TIME_JITTER 60
+#define DEFAULT_MAX_SIGS 5
+
+static const gchar *M = "rspamd dkim plugin";
+
+static const gchar default_sign_headers[] = ""
+ "(o)from:(x)sender:(o)reply-to:(o)subject:(x)date:(x)message-id:"
+ "(o)to:(o)cc:(x)mime-version:(x)content-type:(x)content-transfer-encoding:"
+ "resent-to:resent-cc:resent-from:resent-sender:resent-message-id:"
+ "(x)in-reply-to:(x)references:list-id:list-help:list-owner:list-unsubscribe:"
+ "list-unsubscribe-post:list-subscribe:list-post:(x)openpgp:(x)autocrypt";
+static const gchar default_arc_sign_headers[] = ""
+ "(o)from:(x)sender:(o)reply-to:(o)subject:(x)date:(x)message-id:"
+ "(o)to:(o)cc:(x)mime-version:(x)content-type:(x)content-transfer-encoding:"
+ "resent-to:resent-cc:resent-from:resent-sender:resent-message-id:"
+ "(x)in-reply-to:(x)references:list-id:list-help:list-owner:list-unsubscribe:"
+ "list-unsubscribe-post:list-subscribe:list-post:dkim-signature:(x)openpgp:"
+ "(x)autocrypt";
+
+struct dkim_ctx {
+ struct module_ctx ctx;
+ const gchar *symbol_reject;
+ const gchar *symbol_tempfail;
+ const gchar *symbol_allow;
+ const gchar *symbol_na;
+ const gchar *symbol_permfail;
+
+ struct rspamd_radix_map_helper *whitelist_ip;
+ struct rspamd_hash_map_helper *dkim_domains;
+ guint strict_multiplier;
+ guint time_jitter;
+ rspamd_lru_hash_t *dkim_hash;
+ rspamd_lru_hash_t *dkim_sign_hash;
+ const gchar *sign_headers;
+ const gchar *arc_sign_headers;
+ guint max_sigs;
+ gboolean trusted_only;
+ gboolean check_local;
+ gboolean check_authed;
+};
+
+struct dkim_check_result {
+ rspamd_dkim_context_t *ctx;
+ rspamd_dkim_key_t *key;
+ struct rspamd_task *task;
+ struct rspamd_dkim_check_result *res;
+ gdouble mult_allow;
+ gdouble mult_deny;
+ struct rspamd_symcache_dynamic_item *item;
+ struct dkim_check_result *next, *prev, *first;
+};
+
+static void dkim_symbol_callback(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ void *unused);
+
+static gint lua_dkim_sign_handler(lua_State *L);
+static gint lua_dkim_verify_handler(lua_State *L);
+static gint lua_dkim_canonicalize_handler(lua_State *L);
+
+/* Initialization */
+gint dkim_module_init(struct rspamd_config *cfg, struct module_ctx **ctx);
+gint dkim_module_config(struct rspamd_config *cfg, bool validate);
+gint dkim_module_reconfig(struct rspamd_config *cfg);
+
+module_t dkim_module = {
+ "dkim",
+ dkim_module_init,
+ dkim_module_config,
+ dkim_module_reconfig,
+ NULL,
+ RSPAMD_MODULE_VER,
+ (guint) -1,
+};
+
+static inline struct dkim_ctx *
+dkim_get_context(struct rspamd_config *cfg)
+{
+ return (struct dkim_ctx *) g_ptr_array_index(cfg->c_modules,
+ dkim_module.ctx_offset);
+}
+
+static void
+dkim_module_key_dtor(gpointer k)
+{
+ rspamd_dkim_key_t *key = k;
+
+ rspamd_dkim_key_unref(key);
+}
+
+static void
+dkim_module_free_list(gpointer k)
+{
+ g_list_free_full((GList *) k, rspamd_gstring_free_hard);
+}
+
+gint dkim_module_init(struct rspamd_config *cfg, struct module_ctx **ctx)
+{
+ struct dkim_ctx *dkim_module_ctx;
+
+ dkim_module_ctx = rspamd_mempool_alloc0(cfg->cfg_pool,
+ sizeof(*dkim_module_ctx));
+ dkim_module_ctx->sign_headers = default_sign_headers;
+ dkim_module_ctx->arc_sign_headers = default_arc_sign_headers;
+ dkim_module_ctx->max_sigs = DEFAULT_MAX_SIGS;
+
+ *ctx = (struct module_ctx *) dkim_module_ctx;
+
+ rspamd_rcl_add_doc_by_path(cfg,
+ NULL,
+ "DKIM check plugin",
+ "dkim",
+ UCL_OBJECT,
+ NULL,
+ 0,
+ NULL,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Map of IP addresses that should be excluded from DKIM checks",
+ "whitelist",
+ UCL_STRING,
+ NULL,
+ 0,
+ NULL,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Symbol that is added if DKIM check is successful",
+ "symbol_allow",
+ UCL_STRING,
+ NULL,
+ 0,
+ DEFAULT_SYMBOL_ALLOW,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Symbol that is added if DKIM check is unsuccessful",
+ "symbol_reject",
+ UCL_STRING,
+ NULL,
+ 0,
+ DEFAULT_SYMBOL_REJECT,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Symbol that is added if DKIM check can't be completed (e.g. DNS failure)",
+ "symbol_tempfail",
+ UCL_STRING,
+ NULL,
+ 0,
+ DEFAULT_SYMBOL_TEMPFAIL,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Symbol that is added if mail is not signed",
+ "symbol_na",
+ UCL_STRING,
+ NULL,
+ 0,
+ DEFAULT_SYMBOL_NA,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Symbol that is added if permanent failure encountered",
+ "symbol_permfail",
+ UCL_STRING,
+ NULL,
+ 0,
+ DEFAULT_SYMBOL_PERMFAIL,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Size of DKIM keys cache",
+ "dkim_cache_size",
+ UCL_INT,
+ NULL,
+ 0,
+ G_STRINGIFY(DEFAULT_CACHE_SIZE),
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Allow this time difference when checking DKIM signature time validity",
+ "time_jitter",
+ UCL_TIME,
+ NULL,
+ 0,
+ G_STRINGIFY(DEFAULT_TIME_JITTER),
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Domains to check DKIM for (check all domains if this option is empty)",
+ "domains",
+ UCL_STRING,
+ NULL,
+ 0,
+ "empty",
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Map of domains that are treated as 'trusted' meaning that DKIM policy failure has more significant score",
+ "trusted_domains",
+ UCL_STRING,
+ NULL,
+ 0,
+ "empty",
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Multiply dkim score by this factor for trusted domains",
+ "strict_multiplier",
+ UCL_FLOAT,
+ NULL,
+ 0,
+ NULL,
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Check DKIM policies merely for `trusted_domains`",
+ "trusted_only",
+ UCL_BOOLEAN,
+ NULL,
+ 0,
+ "false",
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Lua script that tells if a message should be signed and with what params (obsoleted)",
+ "sign_condition",
+ UCL_STRING,
+ NULL,
+ 0,
+ "empty",
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Obsoleted: maximum number of DKIM signatures to check",
+ "max_sigs",
+ UCL_INT,
+ NULL,
+ 0,
+ "n/a",
+ 0);
+ rspamd_rcl_add_doc_by_path(cfg,
+ "dkim",
+ "Headers used in signing",
+ "sign_headers",
+ UCL_STRING,
+ NULL,
+ 0,
+ default_sign_headers,
+ 0);
+
+ return 0;
+}
+
+gint dkim_module_config(struct rspamd_config *cfg, bool validate)
+{
+ const ucl_object_t *value;
+ gint res = TRUE, cb_id = -1;
+ guint cache_size, sign_cache_size;
+ gboolean got_trusted = FALSE;
+ struct dkim_ctx *dkim_module_ctx = dkim_get_context(cfg);
+
+ /* Register global methods */
+ lua_getglobal(cfg->lua_state, "rspamd_plugins");
+
+ if (lua_type(cfg->lua_state, -1) == LUA_TTABLE) {
+ lua_pushstring(cfg->lua_state, "dkim");
+ lua_createtable(cfg->lua_state, 0, 1);
+ /* Set methods */
+ lua_pushstring(cfg->lua_state, "sign");
+ lua_pushcfunction(cfg->lua_state, lua_dkim_sign_handler);
+ lua_settable(cfg->lua_state, -3);
+ lua_pushstring(cfg->lua_state, "verify");
+ lua_pushcfunction(cfg->lua_state, lua_dkim_verify_handler);
+ lua_settable(cfg->lua_state, -3);
+ lua_pushstring(cfg->lua_state, "canon_header_relaxed");
+ lua_pushcfunction(cfg->lua_state, lua_dkim_canonicalize_handler);
+ lua_settable(cfg->lua_state, -3);
+ /* Finish dkim key */
+ lua_settable(cfg->lua_state, -3);
+ }
+
+ lua_pop(cfg->lua_state, 1); /* Remove global function */
+ dkim_module_ctx->whitelist_ip = NULL;
+
+ value = rspamd_config_get_module_opt(cfg, "dkim", "check_local");
+
+ if (value == NULL) {
+ value = rspamd_config_get_module_opt(cfg, "options", "check_local");
+ }
+
+ if (value != NULL) {
+ dkim_module_ctx->check_local = ucl_object_toboolean(value);
+ }
+ else {
+ dkim_module_ctx->check_local = FALSE;
+ }
+
+ value = rspamd_config_get_module_opt(cfg, "dkim",
+ "check_authed");
+
+ if (value == NULL) {
+ value = rspamd_config_get_module_opt(cfg, "options",
+ "check_authed");
+ }
+
+ if (value != NULL) {
+ dkim_module_ctx->check_authed = ucl_object_toboolean(value);
+ }
+ else {
+ dkim_module_ctx->check_authed = FALSE;
+ }
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "symbol_reject")) != NULL) {
+ dkim_module_ctx->symbol_reject = ucl_object_tostring(value);
+ }
+ else {
+ dkim_module_ctx->symbol_reject = DEFAULT_SYMBOL_REJECT;
+ }
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim",
+ "symbol_tempfail")) != NULL) {
+ dkim_module_ctx->symbol_tempfail = ucl_object_tostring(value);
+ }
+ else {
+ dkim_module_ctx->symbol_tempfail = DEFAULT_SYMBOL_TEMPFAIL;
+ }
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "symbol_allow")) != NULL) {
+ dkim_module_ctx->symbol_allow = ucl_object_tostring(value);
+ }
+ else {
+ dkim_module_ctx->symbol_allow = DEFAULT_SYMBOL_ALLOW;
+ }
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "symbol_na")) != NULL) {
+ dkim_module_ctx->symbol_na = ucl_object_tostring(value);
+ }
+ else {
+ dkim_module_ctx->symbol_na = DEFAULT_SYMBOL_NA;
+ }
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "symbol_permfail")) != NULL) {
+ dkim_module_ctx->symbol_permfail = ucl_object_tostring(value);
+ }
+ else {
+ dkim_module_ctx->symbol_permfail = DEFAULT_SYMBOL_PERMFAIL;
+ }
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim",
+ "dkim_cache_size")) != NULL) {
+ cache_size = ucl_object_toint(value);
+ }
+ else {
+ cache_size = DEFAULT_CACHE_SIZE;
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim",
+ "sign_cache_size")) != NULL) {
+ sign_cache_size = ucl_object_toint(value);
+ }
+ else {
+ sign_cache_size = 128;
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "time_jitter")) != NULL) {
+ dkim_module_ctx->time_jitter = ucl_object_todouble(value);
+ }
+ else {
+ dkim_module_ctx->time_jitter = DEFAULT_TIME_JITTER;
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "max_sigs")) != NULL) {
+ dkim_module_ctx->max_sigs = ucl_object_toint(value);
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "whitelist")) != NULL) {
+
+ rspamd_config_radix_from_ucl(cfg, value, "DKIM whitelist",
+ &dkim_module_ctx->whitelist_ip, NULL, NULL, "dkim whitelist");
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "domains")) != NULL) {
+ if (!rspamd_map_add_from_ucl(cfg, value,
+ "DKIM domains",
+ rspamd_kv_list_read,
+ rspamd_kv_list_fin,
+ rspamd_kv_list_dtor,
+ (void **) &dkim_module_ctx->dkim_domains,
+ NULL, RSPAMD_MAP_DEFAULT)) {
+ msg_warn_config("cannot load dkim domains list from %s",
+ ucl_object_tostring(value));
+ }
+ else {
+ got_trusted = TRUE;
+ }
+ }
+
+ if (!got_trusted && (value =
+ rspamd_config_get_module_opt(cfg, "dkim", "trusted_domains")) != NULL) {
+ if (!rspamd_map_add_from_ucl(cfg, value,
+ "DKIM domains",
+ rspamd_kv_list_read,
+ rspamd_kv_list_fin,
+ rspamd_kv_list_dtor,
+ (void **) &dkim_module_ctx->dkim_domains,
+ NULL, RSPAMD_MAP_DEFAULT)) {
+ msg_warn_config("cannot load dkim domains list from %s",
+ ucl_object_tostring(value));
+
+ if (validate) {
+ return FALSE;
+ }
+ }
+ else {
+ got_trusted = TRUE;
+ }
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim",
+ "strict_multiplier")) != NULL) {
+ dkim_module_ctx->strict_multiplier = ucl_object_toint(value);
+ }
+ else {
+ dkim_module_ctx->strict_multiplier = 1;
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "trusted_only")) != NULL) {
+ dkim_module_ctx->trusted_only = ucl_object_toboolean(value);
+ }
+ else {
+ dkim_module_ctx->trusted_only = FALSE;
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "dkim", "sign_headers")) != NULL) {
+ dkim_module_ctx->sign_headers = ucl_object_tostring(value);
+ }
+
+ if ((value =
+ rspamd_config_get_module_opt(cfg, "arc", "sign_headers")) != NULL) {
+ dkim_module_ctx->arc_sign_headers = ucl_object_tostring(value);
+ }
+
+ if (cache_size > 0) {
+ dkim_module_ctx->dkim_hash = rspamd_lru_hash_new(
+ cache_size,
+ g_free,
+ dkim_module_key_dtor);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) rspamd_lru_hash_destroy,
+ dkim_module_ctx->dkim_hash);
+ }
+
+ if (sign_cache_size > 0) {
+ dkim_module_ctx->dkim_sign_hash = rspamd_lru_hash_new(
+ sign_cache_size,
+ g_free,
+ (GDestroyNotify) rspamd_dkim_sign_key_unref);
+ rspamd_mempool_add_destructor(cfg->cfg_pool,
+ (rspamd_mempool_destruct_t) rspamd_lru_hash_destroy,
+ dkim_module_ctx->dkim_sign_hash);
+ }
+
+ if (dkim_module_ctx->trusted_only && !got_trusted) {
+ msg_err_config("trusted_only option is set and no trusted domains are defined");
+ if (validate) {
+ return FALSE;
+ }
+ }
+ else {
+ if (!rspamd_config_is_module_enabled(cfg, "dkim")) {
+ return TRUE;
+ }
+
+ cb_id = rspamd_symcache_add_symbol(cfg->cache,
+ "DKIM_CHECK",
+ 0,
+ dkim_symbol_callback,
+ NULL,
+ SYMBOL_TYPE_CALLBACK,
+ -1);
+ rspamd_config_add_symbol(cfg,
+ "DKIM_CHECK",
+ 0.0,
+ "DKIM check callback",
+ "policies",
+ RSPAMD_SYMBOL_FLAG_IGNORE_METRIC,
+ 1,
+ 1);
+ rspamd_config_add_symbol_group(cfg, "DKIM_CHECK", "dkim");
+ rspamd_symcache_add_symbol(cfg->cache,
+ dkim_module_ctx->symbol_reject,
+ 0,
+ NULL,
+ NULL,
+ SYMBOL_TYPE_VIRTUAL | SYMBOL_TYPE_FINE,
+ cb_id);
+ rspamd_symcache_add_symbol(cfg->cache,
+ dkim_module_ctx->symbol_na,
+ 0,
+ NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL | SYMBOL_TYPE_FINE,
+ cb_id);
+ rspamd_symcache_add_symbol(cfg->cache,
+ dkim_module_ctx->symbol_permfail,
+ 0,
+ NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL | SYMBOL_TYPE_FINE,
+ cb_id);
+ rspamd_symcache_add_symbol(cfg->cache,
+ dkim_module_ctx->symbol_tempfail,
+ 0,
+ NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL | SYMBOL_TYPE_FINE,
+ cb_id);
+ rspamd_symcache_add_symbol(cfg->cache,
+ dkim_module_ctx->symbol_allow,
+ 0,
+ NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL | SYMBOL_TYPE_FINE,
+ cb_id);
+
+ rspamd_symcache_add_symbol(cfg->cache,
+ "DKIM_TRACE",
+ 0,
+ NULL, NULL,
+ SYMBOL_TYPE_VIRTUAL | SYMBOL_TYPE_NOSTAT,
+ cb_id);
+ rspamd_config_add_symbol(cfg,
+ "DKIM_TRACE",
+ 0.0,
+ "DKIM trace symbol",
+ "policies",
+ RSPAMD_SYMBOL_FLAG_IGNORE_METRIC,
+ 1,
+ 1);
+ rspamd_config_add_symbol_group(cfg, "DKIM_TRACE", "dkim");
+
+ msg_info_config("init internal dkim module");
+#ifndef HAVE_OPENSSL
+ msg_warn_config(
+ "openssl is not found so dkim rsa check is disabled, only check body hash, it is NOT safe to trust these results");
+#endif
+ }
+
+ return res;
+}
+
+
+/**
+ * Grab a private key from the cache
+ * or from the key content provided
+ */
+rspamd_dkim_sign_key_t *
+dkim_module_load_key_format(struct rspamd_task *task,
+ struct dkim_ctx *dkim_module_ctx,
+ const gchar *key, gsize keylen,
+ enum rspamd_dkim_key_format key_format)
+
+{
+ guchar h[rspamd_cryptobox_HASHBYTES],
+ hex_hash[rspamd_cryptobox_HASHBYTES * 2 + 1];
+ rspamd_dkim_sign_key_t *ret = NULL;
+ GError *err = NULL;
+ struct stat st;
+
+ memset(hex_hash, 0, sizeof(hex_hash));
+ rspamd_cryptobox_hash(h, key, keylen, NULL, 0);
+ rspamd_encode_hex_buf(h, sizeof(h), hex_hash, sizeof(hex_hash));
+
+ if (dkim_module_ctx->dkim_sign_hash) {
+ ret = rspamd_lru_hash_lookup(dkim_module_ctx->dkim_sign_hash,
+ hex_hash, time(NULL));
+ }
+
+ /*
+ * This fails for paths that are also valid base64.
+ * Maybe the caller should have specified a format.
+ */
+ if (key_format == RSPAMD_DKIM_KEY_UNKNOWN) {
+ if (key[0] == '.' || key[0] == '/') {
+ if (!rspamd_cryptobox_base64_is_valid(key, keylen)) {
+ key_format = RSPAMD_DKIM_KEY_FILE;
+ }
+ }
+ else if (rspamd_cryptobox_base64_is_valid(key, keylen)) {
+ key_format = RSPAMD_DKIM_KEY_BASE64;
+ }
+ }
+
+
+ if (ret != NULL && key_format == RSPAMD_DKIM_KEY_FILE) {
+ msg_debug_task("checking for stale file key");
+
+ if (stat(key, &st) != 0) {
+ msg_err_task("cannot stat key file: %s", strerror(errno));
+ return NULL;
+ }
+
+ if (rspamd_dkim_sign_key_maybe_invalidate(ret, st.st_mtime)) {
+ msg_debug_task("removing stale file key");
+ /*
+ * Invalidate DKIM key
+ * removal from lru cache also cleanup the key and value
+ */
+ if (dkim_module_ctx->dkim_sign_hash) {
+ rspamd_lru_hash_remove(dkim_module_ctx->dkim_sign_hash,
+ hex_hash);
+ }
+ ret = NULL;
+ }
+ }
+
+ /* found key; done */
+ if (ret != NULL) {
+ return ret;
+ }
+
+ ret = rspamd_dkim_sign_key_load(key, keylen, key_format, &err);
+
+ if (ret == NULL) {
+ msg_err_task("cannot load dkim key %s: %e",
+ key, err);
+ g_error_free(err);
+ }
+ else if (dkim_module_ctx->dkim_sign_hash) {
+ rspamd_lru_hash_insert(dkim_module_ctx->dkim_sign_hash,
+ g_strdup(hex_hash), ret, time(NULL), 0);
+ }
+
+ return ret;
+}
+
+static gint
+lua_dkim_sign_handler(lua_State *L)
+{
+ struct rspamd_task *task = lua_check_task(L, 1);
+ gint64 arc_idx = 0, expire = 0;
+ enum rspamd_dkim_type sign_type = RSPAMD_DKIM_NORMAL;
+ GError *err = NULL;
+ GString *hdr;
+ GList *sigs = NULL;
+ const gchar *selector = NULL, *domain = NULL, *key = NULL, *rawkey = NULL,
+ *headers = NULL, *sign_type_str = NULL, *arc_cv = NULL,
+ *pubkey = NULL;
+ rspamd_dkim_sign_context_t *ctx;
+ rspamd_dkim_sign_key_t *dkim_key;
+ gsize rawlen = 0, keylen = 0;
+ gboolean no_cache = FALSE, strict_pubkey_check = FALSE;
+ struct dkim_ctx *dkim_module_ctx;
+
+ luaL_argcheck(L, lua_type(L, 2) == LUA_TTABLE, 2, "'table' expected");
+ /*
+ * Get the following elements:
+ * - selector
+ * - domain
+ * - key
+ */
+ if (!rspamd_lua_parse_table_arguments(L, 2, &err,
+ RSPAMD_LUA_PARSE_ARGUMENTS_DEFAULT,
+ "key=V;rawkey=V;*domain=S;*selector=S;no_cache=B;headers=S;"
+ "sign_type=S;arc_idx=I;arc_cv=S;expire=I;pubkey=S;"
+ "strict_pubkey_check=B",
+ &keylen, &key, &rawlen, &rawkey, &domain,
+ &selector, &no_cache, &headers,
+ &sign_type_str, &arc_idx, &arc_cv, &expire, &pubkey,
+ &strict_pubkey_check)) {
+ msg_err_task("cannot parse table arguments: %e",
+ err);
+ g_error_free(err);
+
+ lua_pushboolean(L, FALSE);
+ return 1;
+ }
+
+ dkim_module_ctx = dkim_get_context(task->cfg);
+
+ if (key) {
+ dkim_key = dkim_module_load_key_format(task, dkim_module_ctx, key,
+ keylen, RSPAMD_DKIM_KEY_UNKNOWN);
+ }
+ else if (rawkey) {
+ dkim_key = dkim_module_load_key_format(task, dkim_module_ctx, rawkey,
+ rawlen, RSPAMD_DKIM_KEY_UNKNOWN);
+ }
+ else {
+ msg_err_task("neither key nor rawkey are specified");
+ lua_pushboolean(L, FALSE);
+
+ return 1;
+ }
+
+ if (dkim_key == NULL) {
+ lua_pushboolean(L, FALSE);
+ return 1;
+ }
+
+ if (sign_type_str) {
+ if (strcmp(sign_type_str, "dkim") == 0) {
+ sign_type = RSPAMD_DKIM_NORMAL;
+
+ if (headers == NULL) {
+ headers = dkim_module_ctx->sign_headers;
+ }
+ }
+ else if (strcmp(sign_type_str, "arc-sign") == 0) {
+ sign_type = RSPAMD_DKIM_ARC_SIG;
+
+ if (headers == NULL) {
+ headers = dkim_module_ctx->arc_sign_headers;
+ }
+
+ if (arc_idx == 0) {
+ lua_settop(L, 0);
+ return luaL_error(L, "no arc idx specified");
+ }
+ }
+ else if (strcmp(sign_type_str, "arc-seal") == 0) {
+ sign_type = RSPAMD_DKIM_ARC_SEAL;
+ if (arc_cv == NULL) {
+ lua_settop(L, 0);
+ return luaL_error(L, "no arc cv specified");
+ }
+ if (arc_idx == 0) {
+ lua_settop(L, 0);
+ return luaL_error(L, "no arc idx specified");
+ }
+ }
+ else {
+ lua_settop(L, 0);
+ return luaL_error(L, "unknown sign type: %s",
+ sign_type_str);
+ }
+ }
+ else {
+ /* Unspecified sign type, assume plain dkim */
+ if (headers == NULL) {
+ headers = dkim_module_ctx->sign_headers;
+ }
+ }
+
+ if (pubkey != NULL) {
+ /* Also check if private and public keys match */
+ rspamd_dkim_key_t *pk;
+ keylen = strlen(pubkey);
+
+ pk = rspamd_dkim_parse_key(pubkey, &keylen, NULL);
+
+ if (pk == NULL) {
+ if (strict_pubkey_check) {
+ msg_err_task("cannot parse pubkey from string: %s, skip signing",
+ pubkey);
+ lua_pushboolean(L, FALSE);
+
+ return 1;
+ }
+ else {
+ msg_warn_task("cannot parse pubkey from string: %s",
+ pubkey);
+ }
+ }
+ else {
+ GError *te = NULL;
+
+ /* We have parsed the key, so try to check keys */
+ if (!rspamd_dkim_match_keys(pk, dkim_key, &te)) {
+ if (strict_pubkey_check) {
+ msg_err_task("public key for %s/%s does not match private "
+ "key: %e, skip signing",
+ domain, selector, te);
+ g_error_free(te);
+ lua_pushboolean(L, FALSE);
+ rspamd_dkim_key_unref(pk);
+
+ return 1;
+ }
+ else {
+ msg_warn_task("public key for %s/%s does not match private "
+ "key: %e",
+ domain, selector, te);
+ g_error_free(te);
+ }
+ }
+
+ rspamd_dkim_key_unref(pk);
+ }
+ }
+
+ ctx = rspamd_create_dkim_sign_context(task, dkim_key,
+ DKIM_CANON_RELAXED, DKIM_CANON_RELAXED,
+ headers, sign_type, &err);
+
+ if (ctx == NULL) {
+ msg_err_task("cannot create sign context: %e",
+ err);
+ g_error_free(err);
+
+ lua_pushboolean(L, FALSE);
+ return 1;
+ }
+
+ hdr = rspamd_dkim_sign(task, selector, domain, 0,
+ expire, arc_idx, arc_cv, ctx);
+
+ if (hdr) {
+
+ if (!no_cache) {
+ sigs = rspamd_mempool_get_variable(task->task_pool, "dkim-signature");
+
+ if (sigs == NULL) {
+ sigs = g_list_append(sigs, hdr);
+ rspamd_mempool_set_variable(task->task_pool, "dkim-signature",
+ sigs, dkim_module_free_list);
+ }
+ else {
+ sigs = g_list_append(sigs, hdr);
+ (void) sigs;
+ }
+ }
+
+ lua_pushboolean(L, TRUE);
+ lua_pushlstring(L, hdr->str, hdr->len);
+
+ if (no_cache) {
+ g_string_free(hdr, TRUE);
+ }
+
+ return 2;
+ }
+
+
+ lua_pushboolean(L, FALSE);
+ lua_pushnil(L);
+
+ return 2;
+}
+
+gint dkim_module_reconfig(struct rspamd_config *cfg)
+{
+ return dkim_module_config(cfg, false);
+}
+
+/*
+ * Parse strict value for domain in format: 'reject_multiplier:deny_multiplier'
+ */
+static gboolean
+dkim_module_parse_strict(const gchar *value, gdouble *allow, gdouble *deny)
+{
+ const gchar *colon;
+ gchar *err = NULL;
+ gdouble val;
+ gchar numbuf[64];
+
+ colon = strchr(value, ':');
+ if (colon) {
+ rspamd_strlcpy(numbuf, value,
+ MIN(sizeof(numbuf), (colon - value) + 1));
+ val = strtod(numbuf, &err);
+
+ if (err == NULL || *err == '\0') {
+ *deny = val;
+ colon++;
+ rspamd_strlcpy(numbuf, colon, sizeof(numbuf));
+ err = NULL;
+ val = strtod(numbuf, &err);
+
+ if (err == NULL || *err == '\0') {
+ *allow = val;
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+}
+
+static void
+dkim_module_check(struct dkim_check_result *res)
+{
+ gboolean all_done = TRUE;
+ const gchar *strict_value;
+ struct dkim_check_result *first, *cur = NULL;
+ struct dkim_ctx *dkim_module_ctx = dkim_get_context(res->task->cfg);
+ struct rspamd_task *task = res->task;
+
+ first = res->first;
+
+ DL_FOREACH(first, cur)
+ {
+ if (cur->ctx == NULL) {
+ continue;
+ }
+
+ if (cur->key != NULL && cur->res == NULL) {
+ cur->res = rspamd_dkim_check(cur->ctx, cur->key, task);
+
+ if (dkim_module_ctx->dkim_domains != NULL) {
+ /* Perform strict check */
+ const gchar *domain = rspamd_dkim_get_domain(cur->ctx);
+
+ if ((strict_value =
+ rspamd_match_hash_map(dkim_module_ctx->dkim_domains,
+ domain,
+ strlen(domain))) != NULL) {
+ if (!dkim_module_parse_strict(strict_value, &cur->mult_allow,
+ &cur->mult_deny)) {
+ cur->mult_allow = dkim_module_ctx->strict_multiplier;
+ cur->mult_deny = dkim_module_ctx->strict_multiplier;
+ }
+ }
+ }
+ }
+ }
+
+ DL_FOREACH(first, cur)
+ {
+ if (cur->ctx == NULL) {
+ continue;
+ }
+ if (cur->res == NULL) {
+ /* Still need a key */
+ all_done = FALSE;
+ }
+ }
+
+ if (all_done) {
+ /* Create zero terminated array of results */
+ struct rspamd_dkim_check_result **pres;
+ guint nres = 0, i = 0;
+
+ DL_FOREACH(first, cur)
+ {
+ if (cur->ctx == NULL || cur->res == NULL) {
+ continue;
+ }
+
+ nres++;
+ }
+
+ pres = rspamd_mempool_alloc(task->task_pool, sizeof(*pres) * (nres + 1));
+ pres[nres] = NULL;
+
+ DL_FOREACH(first, cur)
+ {
+ const gchar *symbol = NULL, *trace = NULL;
+ gdouble symbol_weight = 1.0;
+
+ if (cur->ctx == NULL || cur->res == NULL) {
+ continue;
+ }
+
+ pres[i++] = cur->res;
+
+ if (cur->res->rcode == DKIM_REJECT) {
+ symbol = dkim_module_ctx->symbol_reject;
+ trace = "-";
+ symbol_weight = cur->mult_deny * 1.0;
+ }
+ else if (cur->res->rcode == DKIM_CONTINUE) {
+ symbol = dkim_module_ctx->symbol_allow;
+ trace = "+";
+ symbol_weight = cur->mult_allow * 1.0;
+ }
+ else if (cur->res->rcode == DKIM_PERM_ERROR) {
+ trace = "~";
+ symbol = dkim_module_ctx->symbol_permfail;
+ }
+ else if (cur->res->rcode == DKIM_TRYAGAIN) {
+ trace = "?";
+ symbol = dkim_module_ctx->symbol_tempfail;
+ }
+
+ if (symbol != NULL) {
+ const gchar *domain = rspamd_dkim_get_domain(cur->ctx);
+ const gchar *selector = rspamd_dkim_get_selector(cur->ctx);
+ gsize tracelen;
+ gchar *tracebuf;
+
+ tracelen = strlen(domain) + strlen(selector) + 4;
+ tracebuf = rspamd_mempool_alloc(task->task_pool,
+ tracelen);
+ rspamd_snprintf(tracebuf, tracelen, "%s:%s", domain, trace);
+
+ rspamd_task_insert_result(cur->task,
+ "DKIM_TRACE",
+ 0.0,
+ tracebuf);
+
+ rspamd_snprintf(tracebuf, tracelen, "%s:s=%s", domain, selector);
+ rspamd_task_insert_result(task,
+ symbol,
+ symbol_weight,
+ tracebuf);
+ }
+ }
+
+ rspamd_mempool_set_variable(task->task_pool,
+ RSPAMD_MEMPOOL_DKIM_CHECK_RESULTS,
+ pres, NULL);
+ }
+}
+
+static void
+dkim_module_key_handler(rspamd_dkim_key_t *key,
+ gsize keylen,
+ rspamd_dkim_context_t *ctx,
+ gpointer ud,
+ GError *err)
+{
+ struct dkim_check_result *res = ud;
+ struct rspamd_task *task;
+ struct dkim_ctx *dkim_module_ctx;
+
+ task = res->task;
+ dkim_module_ctx = dkim_get_context(task->cfg);
+
+ if (key != NULL) {
+ /* Another ref belongs to the check context */
+ res->key = rspamd_dkim_key_ref(key);
+ /*
+ * We actually receive key with refcount = 1, so we just assume that
+ * lru hash owns this object now
+ */
+ /* Release key when task is processed */
+ rspamd_mempool_add_destructor(res->task->task_pool,
+ dkim_module_key_dtor, res->key);
+
+ if (dkim_module_ctx->dkim_hash) {
+ rspamd_lru_hash_insert(dkim_module_ctx->dkim_hash,
+ g_strdup(rspamd_dkim_get_dns_key(ctx)),
+ key, res->task->task_timestamp, rspamd_dkim_key_get_ttl(key));
+
+ msg_info_task("stored DKIM key for %s in LRU cache for %d seconds, "
+ "%d/%d elements in the cache",
+ rspamd_dkim_get_dns_key(ctx),
+ rspamd_dkim_key_get_ttl(key),
+ rspamd_lru_hash_size(dkim_module_ctx->dkim_hash),
+ rspamd_lru_hash_capacity(dkim_module_ctx->dkim_hash));
+ }
+ }
+ else {
+ /* Insert tempfail symbol */
+ msg_info_task("cannot get key for domain %s: %e",
+ rspamd_dkim_get_dns_key(ctx), err);
+
+ if (err != NULL) {
+ if (err->code == DKIM_SIGERROR_NOKEY) {
+ res->res = rspamd_dkim_create_result(ctx, DKIM_TRYAGAIN, task);
+ res->res->fail_reason = "DNS error when getting key";
+ }
+ else {
+ res->res = rspamd_dkim_create_result(ctx, DKIM_PERM_ERROR, task);
+ res->res->fail_reason = "invalid DKIM record";
+ }
+ }
+ }
+
+ if (err) {
+ g_error_free(err);
+ }
+
+ dkim_module_check(res);
+}
+
+static void
+dkim_symbol_callback(struct rspamd_task *task,
+ struct rspamd_symcache_dynamic_item *item,
+ void *unused)
+{
+ rspamd_dkim_context_t *ctx;
+ rspamd_dkim_key_t *key;
+ GError *err = NULL;
+ struct rspamd_mime_header *rh, *rh_cur;
+ struct dkim_check_result *res = NULL, *cur;
+ guint checked = 0;
+ gdouble *dmarc_checks;
+ struct dkim_ctx *dkim_module_ctx = dkim_get_context(task->cfg);
+
+ /* Allow dmarc */
+ dmarc_checks = rspamd_mempool_get_variable(task->task_pool,
+ RSPAMD_MEMPOOL_DMARC_CHECKS);
+
+ if (dmarc_checks) {
+ (*dmarc_checks)++;
+ }
+ else {
+ dmarc_checks = rspamd_mempool_alloc(task->task_pool,
+ sizeof(*dmarc_checks));
+ *dmarc_checks = 1;
+ rspamd_mempool_set_variable(task->task_pool,
+ RSPAMD_MEMPOOL_DMARC_CHECKS,
+ dmarc_checks, NULL);
+ }
+
+ /* First check if plugin should be enabled */
+ if ((!dkim_module_ctx->check_authed && task->auth_user != NULL) || (!dkim_module_ctx->check_local &&
+ rspamd_ip_is_local_cfg(task->cfg, task->from_addr))) {
+ msg_info_task("skip DKIM checks for local networks and authorized users");
+ rspamd_symcache_finalize_item(task, item);
+
+ return;
+ }
+ /* Check whitelist */
+ if (rspamd_match_radix_map_addr(dkim_module_ctx->whitelist_ip,
+ task->from_addr) != NULL) {
+ msg_info_task("skip DKIM checks for whitelisted address");
+ rspamd_symcache_finalize_item(task, item);
+
+ return;
+ }
+
+ rspamd_symcache_item_async_inc(task, item, M);
+
+ /* Now check if a message has its signature */
+ rh = rspamd_message_get_header_array(task, RSPAMD_DKIM_SIGNHEADER, FALSE);
+ if (rh) {
+ msg_debug_task("dkim signature found");
+
+ DL_FOREACH(rh, rh_cur)
+ {
+ if (rh_cur->decoded == NULL || rh_cur->decoded[0] == '\0') {
+ msg_info_task("cannot load empty DKIM signature");
+ continue;
+ }
+
+ cur = rspamd_mempool_alloc0(task->task_pool, sizeof(*cur));
+ cur->first = res;
+ cur->res = NULL;
+ cur->task = task;
+ cur->mult_allow = 1.0;
+ cur->mult_deny = 1.0;
+ cur->item = item;
+
+ ctx = rspamd_create_dkim_context(rh_cur->decoded,
+ task->task_pool,
+ task->resolver,
+ dkim_module_ctx->time_jitter,
+ RSPAMD_DKIM_NORMAL,
+ &err);
+
+ if (res == NULL) {
+ res = cur;
+ res->first = res;
+ res->prev = res;
+ }
+ else {
+ DL_APPEND(res, cur);
+ }
+
+ if (ctx == NULL) {
+ if (err != NULL) {
+ msg_info_task("cannot parse DKIM signature: %e",
+ err);
+ g_error_free(err);
+ err = NULL;
+ }
+ else {
+ msg_info_task("cannot parse DKIM signature: "
+ "unknown error");
+ }
+
+ continue;
+ }
+ else {
+ /* Get key */
+ cur->ctx = ctx;
+ const gchar *domain = rspamd_dkim_get_domain(cur->ctx);
+
+ if (dkim_module_ctx->trusted_only &&
+ (dkim_module_ctx->dkim_domains == NULL ||
+ rspamd_match_hash_map(dkim_module_ctx->dkim_domains,
+ domain, strlen(domain)) == NULL)) {
+ msg_debug_task("skip dkim check for %s domain",
+ rspamd_dkim_get_domain(ctx));
+
+ continue;
+ }
+
+ if (dkim_module_ctx->dkim_hash) {
+ key = rspamd_lru_hash_lookup(dkim_module_ctx->dkim_hash,
+ rspamd_dkim_get_dns_key(ctx),
+ task->task_timestamp);
+ }
+ else {
+ key = NULL;
+ }
+
+ if (key != NULL) {
+ cur->key = rspamd_dkim_key_ref(key);
+ /* Release key when task is processed */
+ rspamd_mempool_add_destructor(task->task_pool,
+ dkim_module_key_dtor, cur->key);
+ }
+ else {
+ if (!rspamd_get_dkim_key(ctx,
+ task,
+ dkim_module_key_handler,
+ cur)) {
+ continue;
+ }
+ }
+ }
+
+ checked++;
+
+ if (checked > dkim_module_ctx->max_sigs) {
+ msg_info_task("message has multiple signatures but we"
+ " stopped after %d checked signatures as limit"
+ " is reached",
+ checked);
+ break;
+ }
+ }
+ }
+ else {
+ rspamd_task_insert_result(task,
+ dkim_module_ctx->symbol_na,
+ 1.0,
+ NULL);
+ }
+
+ if (res != NULL) {
+ dkim_module_check(res);
+ }
+
+ rspamd_symcache_item_async_dec_check(task, item, M);
+}
+
+struct rspamd_dkim_lua_verify_cbdata {
+ rspamd_dkim_context_t *ctx;
+ struct rspamd_task *task;
+ lua_State *L;
+ rspamd_dkim_key_t *key;
+ gint cbref;
+};
+
+static void
+dkim_module_lua_push_verify_result(struct rspamd_dkim_lua_verify_cbdata *cbd,
+ struct rspamd_dkim_check_result *res, GError *err)
+{
+ struct rspamd_task **ptask, *task;
+ const gchar *error_str = "unknown error";
+ gboolean success = FALSE;
+
+ task = cbd->task;
+
+ switch (res->rcode) {
+ case DKIM_CONTINUE:
+ error_str = NULL;
+ success = TRUE;
+ break;
+ case DKIM_REJECT:
+ if (err) {
+ error_str = err->message;
+ }
+ else {
+ error_str = "reject";
+ }
+ break;
+ case DKIM_TRYAGAIN:
+ if (err) {
+ error_str = err->message;
+ }
+ else {
+ error_str = "tempfail";
+ }
+ break;
+ case DKIM_NOTFOUND:
+ if (err) {
+ error_str = err->message;
+ }
+ else {
+ error_str = "not found";
+ }
+ break;
+ case DKIM_RECORD_ERROR:
+ if (err) {
+ error_str = err->message;
+ }
+ else {
+ error_str = "bad record";
+ }
+ break;
+ case DKIM_PERM_ERROR:
+ if (err) {
+ error_str = err->message;
+ }
+ else {
+ error_str = "permanent error";
+ }
+ break;
+ default:
+ break;
+ }
+
+ lua_rawgeti(cbd->L, LUA_REGISTRYINDEX, cbd->cbref);
+ ptask = lua_newuserdata(cbd->L, sizeof(*ptask));
+ *ptask = task;
+ lua_pushboolean(cbd->L, success);
+
+ if (error_str) {
+ lua_pushstring(cbd->L, error_str);
+ }
+ else {
+ lua_pushnil(cbd->L);
+ }
+
+ if (cbd->ctx) {
+ if (res->domain) {
+ lua_pushstring(cbd->L, res->domain);
+ }
+ else {
+ lua_pushnil(cbd->L);
+ }
+
+ if (res->selector) {
+ lua_pushstring(cbd->L, res->selector);
+ }
+ else {
+ lua_pushnil(cbd->L);
+ }
+
+ if (res->short_b) {
+ lua_pushstring(cbd->L, res->short_b);
+ }
+ else {
+ lua_pushnil(cbd->L);
+ }
+
+ if (res->fail_reason) {
+ lua_pushstring(cbd->L, res->fail_reason);
+ }
+ else {
+ lua_pushnil(cbd->L);
+ }
+ }
+ else {
+ lua_pushnil(cbd->L);
+ lua_pushnil(cbd->L);
+ lua_pushnil(cbd->L);
+ lua_pushnil(cbd->L);
+ }
+
+ if (lua_pcall(cbd->L, 7, 0, 0) != 0) {
+ msg_err_task("call to verify callback failed: %s",
+ lua_tostring(cbd->L, -1));
+ lua_pop(cbd->L, 1);
+ }
+
+ luaL_unref(cbd->L, LUA_REGISTRYINDEX, cbd->cbref);
+}
+
+static void
+dkim_module_lua_on_key(rspamd_dkim_key_t *key,
+ gsize keylen,
+ rspamd_dkim_context_t *ctx,
+ gpointer ud,
+ GError *err)
+{
+ struct rspamd_dkim_lua_verify_cbdata *cbd = ud;
+ struct rspamd_task *task;
+ struct rspamd_dkim_check_result *res;
+ struct dkim_ctx *dkim_module_ctx;
+
+ task = cbd->task;
+ dkim_module_ctx = dkim_get_context(task->cfg);
+
+ if (key != NULL) {
+ /* Another ref belongs to the check context */
+ cbd->key = rspamd_dkim_key_ref(key);
+ /*
+ * We actually receive key with refcount = 1, so we just assume that
+ * lru hash owns this object now
+ */
+
+ if (dkim_module_ctx->dkim_hash) {
+ rspamd_lru_hash_insert(dkim_module_ctx->dkim_hash,
+ g_strdup(rspamd_dkim_get_dns_key(ctx)),
+ key, cbd->task->task_timestamp, rspamd_dkim_key_get_ttl(key));
+ }
+ /* Release key when task is processed */
+ rspamd_mempool_add_destructor(cbd->task->task_pool,
+ dkim_module_key_dtor, cbd->key);
+ }
+ else {
+ /* Insert tempfail symbol */
+ msg_info_task("cannot get key for domain %s: %e",
+ rspamd_dkim_get_dns_key(ctx), err);
+
+ if (err != NULL) {
+ if (err->code == DKIM_SIGERROR_NOKEY) {
+ res = rspamd_dkim_create_result(ctx, DKIM_TRYAGAIN, task);
+ res->fail_reason = "DNS error when getting key";
+ }
+ else {
+ res = rspamd_dkim_create_result(ctx, DKIM_PERM_ERROR, task);
+ res->fail_reason = "invalid DKIM record";
+ }
+ }
+ else {
+ res = rspamd_dkim_create_result(ctx, DKIM_TRYAGAIN, task);
+ res->fail_reason = "DNS error when getting key";
+ }
+
+ dkim_module_lua_push_verify_result(cbd, res, err);
+
+ if (err) {
+ g_error_free(err);
+ }
+
+ return;
+ }
+
+ res = rspamd_dkim_check(cbd->ctx, cbd->key, cbd->task);
+ dkim_module_lua_push_verify_result(cbd, res, NULL);
+}
+
+static gint
+lua_dkim_verify_handler(lua_State *L)
+{
+ struct rspamd_task *task = lua_check_task(L, 1);
+ const gchar *sig = luaL_checkstring(L, 2);
+ rspamd_dkim_context_t *ctx;
+ struct rspamd_dkim_lua_verify_cbdata *cbd;
+ rspamd_dkim_key_t *key;
+ struct rspamd_dkim_check_result *ret;
+ GError *err = NULL;
+ const gchar *type_str = NULL;
+ enum rspamd_dkim_type type = RSPAMD_DKIM_NORMAL;
+ struct dkim_ctx *dkim_module_ctx;
+
+ if (task && sig && lua_isfunction(L, 3)) {
+ if (lua_isstring(L, 4)) {
+ type_str = lua_tostring(L, 4);
+
+ if (type_str) {
+ if (strcmp(type_str, "dkim") == 0) {
+ type = RSPAMD_DKIM_NORMAL;
+ }
+ else if (strcmp(type_str, "arc-sign") == 0) {
+ type = RSPAMD_DKIM_ARC_SIG;
+ }
+ else if (strcmp(type_str, "arc-seal") == 0) {
+ type = RSPAMD_DKIM_ARC_SEAL;
+ }
+ else {
+ lua_settop(L, 0);
+ return luaL_error(L, "unknown sign type: %s",
+ type_str);
+ }
+ }
+ }
+
+ dkim_module_ctx = dkim_get_context(task->cfg);
+
+ ctx = rspamd_create_dkim_context(sig,
+ task->task_pool,
+ task->resolver,
+ dkim_module_ctx->time_jitter,
+ type,
+ &err);
+
+ if (ctx == NULL) {
+ lua_pushboolean(L, false);
+
+ if (err) {
+ lua_pushstring(L, err->message);
+ g_error_free(err);
+ }
+ else {
+ lua_pushstring(L, "unknown error");
+ }
+
+ return 2;
+ }
+
+ cbd = rspamd_mempool_alloc(task->task_pool, sizeof(*cbd));
+ cbd->L = L;
+ cbd->task = task;
+ lua_pushvalue(L, 3);
+ cbd->cbref = luaL_ref(L, LUA_REGISTRYINDEX);
+ cbd->ctx = ctx;
+ cbd->key = NULL;
+
+ if (dkim_module_ctx->dkim_hash) {
+ key = rspamd_lru_hash_lookup(dkim_module_ctx->dkim_hash,
+ rspamd_dkim_get_dns_key(ctx),
+ task->task_timestamp);
+ }
+ else {
+ key = NULL;
+ }
+
+ if (key != NULL) {
+ cbd->key = rspamd_dkim_key_ref(key);
+ /* Release key when task is processed */
+ rspamd_mempool_add_destructor(task->task_pool,
+ dkim_module_key_dtor, cbd->key);
+ ret = rspamd_dkim_check(cbd->ctx, cbd->key, cbd->task);
+ dkim_module_lua_push_verify_result(cbd, ret, NULL);
+ }
+ else {
+ rspamd_get_dkim_key(ctx,
+ task,
+ dkim_module_lua_on_key,
+ cbd);
+ }
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ lua_pushboolean(L, TRUE);
+ lua_pushnil(L);
+
+ return 2;
+}
+
+static gint
+lua_dkim_canonicalize_handler(lua_State *L)
+{
+ gsize nlen, vlen;
+ const gchar *hname = luaL_checklstring(L, 1, &nlen),
+ *hvalue = luaL_checklstring(L, 2, &vlen);
+ static gchar st_buf[8192];
+ gchar *buf;
+ guint inlen;
+ gboolean allocated = FALSE;
+ goffset r;
+
+ if (hname && hvalue && nlen > 0) {
+ inlen = nlen + vlen + sizeof(":" CRLF);
+
+ if (inlen > sizeof(st_buf)) {
+ buf = g_malloc(inlen);
+ allocated = TRUE;
+ }
+ else {
+ /* Faster */
+ buf = st_buf;
+ }
+
+ r = rspamd_dkim_canonize_header_relaxed_str(hname, hvalue, buf, inlen);
+
+ if (r == -1) {
+ lua_pushnil(L);
+ }
+ else {
+ lua_pushlstring(L, buf, r);
+ }
+
+ if (allocated) {
+ g_free(buf);
+ }
+ }
+ else {
+ return luaL_error(L, "invalid arguments");
+ }
+
+ return 1;
+}