/*- * 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. */ #include "config.h" #include "rspamd.h" #include "message.h" #include "dkim.h" #include "dns.h" #include "utlist.h" #define msg_err_dkim(...) rspamd_default_log_function (G_LOG_LEVEL_CRITICAL, \ "dkim", ctx->pool->tag.uid, \ G_STRFUNC, \ __VA_ARGS__) #define msg_warn_dkim(...) rspamd_default_log_function (G_LOG_LEVEL_WARNING, \ "dkim", ctx->pool->tag.uid, \ G_STRFUNC, \ __VA_ARGS__) #define msg_info_dkim(...) rspamd_default_log_function (G_LOG_LEVEL_INFO, \ "dkim", ctx->pool->tag.uid, \ G_STRFUNC, \ __VA_ARGS__) #define msg_debug_dkim(...) rspamd_default_log_function (G_LOG_LEVEL_DEBUG, \ "dkim", ctx->pool->tag.uid, \ G_STRFUNC, \ __VA_ARGS__) /* Parser of dkim params */ typedef gboolean (*dkim_parse_param_f) (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_signature (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_signalg (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_domain (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_canonalg (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_ignore (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_selector (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_hdrlist (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_version (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_timestamp (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_expiration (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_bodyhash (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static gboolean rspamd_dkim_parse_bodylength (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err); static const dkim_parse_param_f parser_funcs[] = { [DKIM_PARAM_SIGNATURE] = rspamd_dkim_parse_signature, [DKIM_PARAM_SIGNALG] = rspamd_dkim_parse_signalg, [DKIM_PARAM_DOMAIN] = rspamd_dkim_parse_domain, [DKIM_PARAM_CANONALG] = rspamd_dkim_parse_canonalg, [DKIM_PARAM_QUERYMETHOD] = rspamd_dkim_parse_ignore, [DKIM_PARAM_SELECTOR] = rspamd_dkim_parse_selector, [DKIM_PARAM_HDRLIST] = rspamd_dkim_parse_hdrlist, [DKIM_PARAM_VERSION] = rspamd_dkim_parse_version, [DKIM_PARAM_IDENTITY] = rspamd_dkim_parse_ignore, [DKIM_PARAM_TIMESTAMP] = rspamd_dkim_parse_timestamp, [DKIM_PARAM_EXPIRATION] = rspamd_dkim_parse_expiration, [DKIM_PARAM_COPIEDHDRS] = rspamd_dkim_parse_ignore, [DKIM_PARAM_BODYHASH] = rspamd_dkim_parse_bodyhash, [DKIM_PARAM_BODYLENGTH] = rspamd_dkim_parse_bodylength }; struct rspamd_dkim_header { gchar *name; guint count; }; #define DKIM_ERROR dkim_error_quark () GQuark dkim_error_quark (void) { return g_quark_from_static_string ("dkim-error-quark"); } /* Parsers implementation */ static gboolean rspamd_dkim_parse_signature (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { ctx->b = rspamd_mempool_alloc (ctx->pool, len + 1); rspamd_strlcpy (ctx->b, param, len + 1); #if ((GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION < 20)) gchar *tmp; gsize tmp_len = len; tmp = g_base64_decode (ctx->b, &tmp_len); rspamd_strlcpy (ctx->b, tmp, tmp_len + 1); g_free (tmp); #else g_base64_decode_inplace (ctx->b, &len); #endif ctx->blen = len; return TRUE; } static gboolean rspamd_dkim_parse_signalg (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { if (len == 8) { if (memcmp (param, "rsa-sha1", len) == 0) { ctx->sig_alg = DKIM_SIGN_RSASHA1; return TRUE; } } else if (len == 10) { if (memcmp (param, "rsa-sha256", len) == 0) { ctx->sig_alg = DKIM_SIGN_RSASHA256; return TRUE; } } g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_INVALID_A, "invalid dkim sign algorithm"); return FALSE; } static gboolean rspamd_dkim_parse_domain (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { ctx->domain = rspamd_mempool_alloc (ctx->pool, len + 1); rspamd_strlcpy (ctx->domain, param, len + 1); return TRUE; } static gboolean rspamd_dkim_parse_canonalg (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { const gchar *p, *slash = NULL, *end = param + len; gsize sl = 0; p = param; while (p != end) { if (*p == '/') { slash = p; break; } p++; sl++; } if (slash == NULL) { /* Only check header */ if (len == 6 && memcmp (param, "simple", len) == 0) { ctx->header_canon_type = DKIM_CANON_SIMPLE; return TRUE; } else if (len == 7 && memcmp (param, "relaxed", len) == 0) { ctx->header_canon_type = DKIM_CANON_RELAXED; return TRUE; } } else { /* First check header */ if (sl == 6 && memcmp (param, "simple", sl) == 0) { ctx->header_canon_type = DKIM_CANON_SIMPLE; } else if (sl == 7 && memcmp (param, "relaxed", sl) == 0) { ctx->header_canon_type = DKIM_CANON_RELAXED; } else { goto err; } /* Check body */ len -= sl + 1; slash++; if (len == 6 && memcmp (slash, "simple", len) == 0) { ctx->body_canon_type = DKIM_CANON_SIMPLE; return TRUE; } else if (len == 7 && memcmp (slash, "relaxed", len) == 0) { ctx->body_canon_type = DKIM_CANON_RELAXED; return TRUE; } } err: g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_INVALID_A, "invalid dkim canonization algorithm"); return FALSE; } static gboolean rspamd_dkim_parse_ignore (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { /* Just ignore unused params */ return TRUE; } static gboolean rspamd_dkim_parse_selector (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { ctx->selector = rspamd_mempool_alloc (ctx->pool, len + 1); rspamd_strlcpy (ctx->selector, param, len + 1); return TRUE; } static struct rspamd_dkim_header * rspamd_dkim_find_header (GPtrArray *arr, const gchar *name, gsize len) { guint i; struct rspamd_dkim_header *h; for (i = 0; i < arr->len; i++) { h = g_ptr_array_index (arr, i); if (g_ascii_strncasecmp (h->name, name, len) == 0) { return h; } } return NULL; } static void rspamd_dkim_hlist_free (void *ud) { GPtrArray *a = ud; g_ptr_array_free (a, TRUE); } static gboolean rspamd_dkim_parse_hdrlist (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { const gchar *c, *p, *end = param + len; gchar *h; gboolean from_found = FALSE; guint count = 0; struct rspamd_dkim_header *new; p = param; while (p <= end) { if ((p == end || *p == ':')) { count++; } p++; } if (count > 0) { ctx->hlist = g_ptr_array_sized_new (count); } else { return FALSE; } c = param; p = param; while (p <= end) { if ((p == end || *p == ':') && p - c > 0) { if ((new = rspamd_dkim_find_header (ctx->hlist, c, p - c)) != NULL) { new->count++; } else { /* Insert new header to the list */ new = rspamd_mempool_alloc (ctx->pool, sizeof (struct rspamd_dkim_header)); h = rspamd_mempool_alloc (ctx->pool, p - c + 1); rspamd_strlcpy (h, c, p - c + 1); g_strstrip (h); new->name = h; new->count = 1; /* Check mandatory from */ if (!from_found && g_ascii_strcasecmp (h, "from") == 0) { from_found = TRUE; } g_ptr_array_add (ctx->hlist, new); } c = p + 1; p++; } else { p++; } } if (!ctx->hlist) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_INVALID_H, "invalid dkim header list"); return FALSE; } else { if (!from_found) { g_ptr_array_free (ctx->hlist, TRUE); g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_INVALID_H, "invalid dkim header list, from header is missing"); return FALSE; } /* Reverse list */ rspamd_mempool_add_destructor (ctx->pool, (rspamd_mempool_destruct_t)rspamd_dkim_hlist_free, ctx->hlist); } return TRUE; } static gboolean rspamd_dkim_parse_version (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { if (len != 1 || *param != '1') { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_VERSION, "invalid dkim version"); return FALSE; } ctx->ver = 1; return TRUE; } static gboolean rspamd_dkim_parse_timestamp (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { gulong val; if (!rspamd_strtoul (param, len, &val)) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_UNKNOWN, "invalid dkim timestamp"); return FALSE; } ctx->timestamp = val; return TRUE; } static gboolean rspamd_dkim_parse_expiration (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { gulong val; if (!rspamd_strtoul (param, len, &val)) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_UNKNOWN, "invalid dkim expiration"); return FALSE; } ctx->expiration = val; return TRUE; } static gboolean rspamd_dkim_parse_bodyhash (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { ctx->bh = rspamd_mempool_alloc (ctx->pool, len + 1); rspamd_strlcpy (ctx->bh, param, len + 1); #if ((GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION < 20)) gchar *tmp; gsize tmp_len = len; tmp = g_base64_decode (ctx->bh, &tmp_len); rspamd_strlcpy (ctx->bh, tmp, tmp_len + 1); g_free (tmp); #else g_base64_decode_inplace (ctx->bh, &len); #endif ctx->bhlen = len; return TRUE; } static gboolean rspamd_dkim_parse_bodylength (rspamd_dkim_context_t * ctx, const gchar *param, gsize len, GError **err) { gulong val; if (!rspamd_strtoul (param, len, &val)) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_INVALID_L, "invalid dkim body length"); return FALSE; } ctx->len = val; return TRUE; } /** * Create new dkim context from signature * @param sig message's signature * @param pool pool to allocate memory from * @param err pointer to error object * @return new context or NULL */ rspamd_dkim_context_t * rspamd_create_dkim_context (const gchar *sig, rspamd_mempool_t *pool, guint time_jitter, GError **err) { const gchar *p, *c, *tag = NULL, *end; gsize taglen; gint param = DKIM_PARAM_UNKNOWN; time_t now; rspamd_dkim_context_t *ctx; enum { DKIM_STATE_TAG = 0, DKIM_STATE_AFTER_TAG, DKIM_STATE_VALUE, DKIM_STATE_SKIP_SPACES = 99, DKIM_STATE_ERROR = 100 } state, next_state; ctx = rspamd_mempool_alloc0 (pool, sizeof (rspamd_dkim_context_t)); ctx->pool = pool; ctx->header_canon_type = DKIM_CANON_DEFAULT; ctx->body_canon_type = DKIM_CANON_DEFAULT; ctx->sig_alg = DKIM_SIGN_UNKNOWN; /* A simple state machine of parsing tags */ state = DKIM_STATE_SKIP_SPACES; next_state = DKIM_STATE_TAG; taglen = 0; p = sig; c = sig; end = p + strlen (p); while (p <= end) { switch (state) { case DKIM_STATE_TAG: if (g_ascii_isspace (*p)) { taglen = p - c; while (*p && g_ascii_isspace (*p)) { /* Skip spaces before '=' sign */ p++; } if (*p != '=') { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_UNKNOWN, "invalid dkim param"); state = DKIM_STATE_ERROR; } else { state = DKIM_STATE_SKIP_SPACES; next_state = DKIM_STATE_AFTER_TAG; param = DKIM_PARAM_UNKNOWN; p++; tag = c; } } else if (*p == '=') { state = DKIM_STATE_SKIP_SPACES; next_state = DKIM_STATE_AFTER_TAG; param = DKIM_PARAM_UNKNOWN; p++; tag = c; } else { taglen++; p++; } break; case DKIM_STATE_AFTER_TAG: /* We got tag at tag and len at taglen */ switch (taglen) { case 0: g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_UNKNOWN, "zero length dkim param"); state = DKIM_STATE_ERROR; break; case 1: /* Simple tags */ switch (*tag) { case 'v': param = DKIM_PARAM_VERSION; break; case 'a': param = DKIM_PARAM_SIGNALG; break; case 'b': param = DKIM_PARAM_SIGNATURE; break; case 'c': param = DKIM_PARAM_CANONALG; break; case 'd': param = DKIM_PARAM_DOMAIN; break; case 'h': param = DKIM_PARAM_HDRLIST; break; case 'i': param = DKIM_PARAM_IDENTITY; break; case 'l': param = DKIM_PARAM_BODYLENGTH; break; case 'q': param = DKIM_PARAM_QUERYMETHOD; break; case 's': param = DKIM_PARAM_SELECTOR; break; case 't': param = DKIM_PARAM_TIMESTAMP; break; case 'x': param = DKIM_PARAM_EXPIRATION; break; case 'z': param = DKIM_PARAM_COPIEDHDRS; break; default: g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_UNKNOWN, "invalid dkim param: %c", *tag); state = DKIM_STATE_ERROR; break; } break; case 2: if (tag[0] == 'b' && tag[1] == 'h') { param = DKIM_PARAM_BODYHASH; } else { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_UNKNOWN, "invalid dkim param: %c%c", tag[0], tag[1]); state = DKIM_STATE_ERROR; } break; default: g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_UNKNOWN, "invalid dkim param length: %zd", taglen); state = DKIM_STATE_ERROR; break; } if (state != DKIM_STATE_ERROR) { /* Skip spaces */ state = DKIM_STATE_SKIP_SPACES; next_state = DKIM_STATE_VALUE; } break; case DKIM_STATE_VALUE: if (*p == ';') { if (param == DKIM_PARAM_UNKNOWN || p - c == 0 || !parser_funcs[param](ctx, c, p - c, err)) { state = DKIM_STATE_ERROR; } else { state = DKIM_STATE_SKIP_SPACES; next_state = DKIM_STATE_TAG; p++; taglen = 0; } } else if (p == end) { if (param == DKIM_PARAM_UNKNOWN || !parser_funcs[param](ctx, c, p - c + 1, err)) { state = DKIM_STATE_ERROR; } else { /* Finish processing */ p++; } } else { p++; } break; case DKIM_STATE_SKIP_SPACES: if (g_ascii_isspace (*p)) { p++; } else { c = p; state = next_state; } break; case DKIM_STATE_ERROR: if (err && *err) { msg_info_dkim ("dkim parse failed: %s", (*err)->message); return NULL; } else { msg_info_dkim ("dkim parse failed: unknown error"); return NULL; } break; } } /* Now check validity of signature */ if (ctx->b == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_B, "b parameter missing"); return NULL; } if (ctx->bh == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_BH, "bh parameter missing"); return NULL; } if (ctx->domain == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_D, "domain parameter missing"); return NULL; } if (ctx->selector == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_S, "selector parameter missing"); return NULL; } if (ctx->ver == 0) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_V, "v parameter missing"); return NULL; } if (ctx->hlist == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_H, "h parameter missing"); return NULL; } if (ctx->sig_alg == DKIM_SIGN_UNKNOWN) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_S, "s parameter missing"); return NULL; } if (ctx->sig_alg == DKIM_SIGN_RSASHA1) { /* Check bh length */ if (ctx->bhlen != (guint)g_checksum_type_get_length (G_CHECKSUM_SHA1)) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_BADSIG, "signature has incorrect length: %ud", ctx->bhlen); return NULL; } } else if (ctx->sig_alg == DKIM_SIGN_RSASHA256) { if (ctx->bhlen != (guint)g_checksum_type_get_length (G_CHECKSUM_SHA256)) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_BADSIG, "signature has incorrect length: %ud", ctx->bhlen); return NULL; } } /* Check expiration */ now = time (NULL); if (ctx->timestamp && now < ctx->timestamp && ctx->timestamp - now > (gint)time_jitter) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_FUTURE, "signature was made in future, ignoring"); return NULL; } if (ctx->expiration && ctx->expiration < now) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EXPIRED, "signature has expired"); return NULL; } /* Now create dns key to request further */ taglen = strlen (ctx->domain) + strlen (ctx->selector) + sizeof (DKIM_DNSKEYNAME) + 2; ctx->dns_key = rspamd_mempool_alloc (ctx->pool, taglen); rspamd_snprintf (ctx->dns_key, taglen, "%s.%s.%s", ctx->selector, DKIM_DNSKEYNAME, ctx->domain); /* Create checksums for further operations */ if (ctx->sig_alg == DKIM_SIGN_RSASHA1) { ctx->body_hash = g_checksum_new (G_CHECKSUM_SHA1); ctx->headers_hash = g_checksum_new (G_CHECKSUM_SHA1); } else if (ctx->sig_alg == DKIM_SIGN_RSASHA256) { ctx->body_hash = g_checksum_new (G_CHECKSUM_SHA256); ctx->headers_hash = g_checksum_new (G_CHECKSUM_SHA256); } else { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_BADSIG, "signature has unsupported signature algorithm"); return NULL; } rspamd_mempool_add_destructor (ctx->pool, (rspamd_mempool_destruct_t)g_checksum_free, ctx->body_hash); rspamd_mempool_add_destructor (ctx->pool, (rspamd_mempool_destruct_t)g_checksum_free, ctx->headers_hash); ctx->dkim_header = sig; return ctx; } struct rspamd_dkim_key_cbdata { rspamd_dkim_context_t *ctx; dkim_key_handler_f handler; gpointer ud; }; static rspamd_dkim_key_t * rspamd_dkim_make_key (rspamd_dkim_context_t *ctx, const gchar *keydata, guint keylen, GError **err) { rspamd_dkim_key_t *key = NULL; if (keylen < 3) { msg_err_dkim ("DKIM key is too short to be valid"); return NULL; } key = g_slice_alloc0 (sizeof (rspamd_dkim_key_t)); key->keydata = g_slice_alloc (keylen + 1); rspamd_strlcpy (key->keydata, keydata, keylen + 1); key->keylen = keylen + 1; key->decoded_len = keylen + 1; #if ((GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION < 20)) gchar *tmp; gsize tmp_len = keylen; tmp = g_base64_decode (key->keydata, &tmp_len); rspamd_strlcpy (key->keydata, tmp, tmp_len + 1); g_free (tmp); key->decoded_len = tmp_len; #else g_base64_decode_inplace (key->keydata, &key->decoded_len); #endif REF_INIT_RETAIN (key, rspamd_dkim_key_free); #ifdef HAVE_OPENSSL key->key_bio = BIO_new_mem_buf (key->keydata, key->decoded_len); if (key->key_bio == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, "cannot make ssl bio from key"); REF_RELEASE (key); return NULL; } key->key_evp = d2i_PUBKEY_bio (key->key_bio, NULL); if (key->key_evp == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, "cannot extract pubkey from bio"); REF_RELEASE (key); return NULL; } key->key_rsa = EVP_PKEY_get1_RSA (key->key_evp); if (key->key_rsa == NULL) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, "cannot extract rsa key from evp key"); REF_RELEASE (key); return NULL; } #endif return key; } /** * Free DKIM key * @param key */ void rspamd_dkim_key_free (rspamd_dkim_key_t *key) { #ifdef HAVE_OPENSSL if (key->key_evp) { EVP_PKEY_free (key->key_evp); } if (key->key_rsa) { RSA_free (key->key_rsa); } if (key->key_bio) { BIO_free (key->key_bio); } #endif g_slice_free1 (key->keylen, key->keydata); g_slice_free1 (sizeof (rspamd_dkim_key_t), key); } static rspamd_dkim_key_t * rspamd_dkim_parse_key (rspamd_dkim_context_t *ctx, const gchar *txt, gsize *keylen, GError **err) { const gchar *c, *p, *end; gint state = 0; gsize len; c = txt; p = txt; end = txt + strlen (txt); while (p <= end) { switch (state) { case 0: if (p != end && p[0] == 'p' && p[1] == '=') { /* We got something like public key */ c = p + 2; p = c; state = 1; } else { /* Ignore everything */ p++; } break; case 1: /* State when we got p= and looking for some public key */ if ((*p == ';' || p == end) && p > c) { len = p - c; if (keylen) { *keylen = len; } return rspamd_dkim_make_key (ctx, c, len, err); } else { p++; } break; } } if (p - c == 0) { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_KEYREVOKED, "key was revoked"); } else { g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, "key was not found"); } return NULL; } /* Get TXT request data and parse it */ static void rspamd_dkim_dns_cb (struct rdns_reply *reply, gpointer arg) { struct rspamd_dkim_key_cbdata *cbdata = arg; rspamd_dkim_key_t *key = NULL; GError *err = NULL; struct rdns_reply_entry *elt; gsize keylen = 0; if (reply->code != RDNS_RC_NOERROR) { g_set_error (&err, DKIM_ERROR, DKIM_SIGERROR_NOKEY, "dns request to %s failed: %s", cbdata->ctx->dns_key, rdns_strerror (reply->code)); cbdata->handler (NULL, 0, cbdata->ctx, cbdata->ud, err); } else { LL_FOREACH (reply->entries, elt) { if (elt->type == RDNS_REQUEST_TXT) { key = rspamd_dkim_parse_key (cbdata->ctx, elt->content.txt.data, &keylen, &err); if (key) { key->ttl = elt->ttl; break; } } } if (key != NULL && err != NULL) { /* Free error as it is insignificant */ g_error_free (err); err = NULL; } cbdata->handler (key, keylen, cbdata->ctx, cbdata->ud, err); } } /** * Make DNS request for specified context and obtain and parse key * @param ctx dkim context from signature * @param resolver dns resolver object * @param s async session to make request * @return */ gboolean rspamd_get_dkim_key (rspamd_dkim_context_t *ctx, struct rspamd_task *task, dkim_key_handler_f handler, gpointer ud) { struct rspamd_dkim_key_cbdata *cbdata; g_return_val_if_fail (ctx != NULL, FALSE); g_return_val_if_fail (ctx->dns_key != NULL, FALSE); cbdata = rspamd_mempool_alloc (ctx->pool, sizeof (struct rspamd_dkim_key_cbdata)); cbdata->ctx = ctx; cbdata->handler = handler; cbdata->ud = ud; return make_dns_request_task (task, rspamd_dkim_dns_cb, cbdata, RDNS_REQUEST_TXT, ctx->dns_key); } static gboolean rspamd_dkim_relaxed_body_step (rspamd_dkim_context_t *ctx, GChecksum *ck, const gchar **start, guint size, guint *remain) { const gchar *h; static gchar buf[BUFSIZ]; gchar *t; guint len, inlen, added = 0; gboolean got_sp; len = size; inlen = sizeof (buf) - 1; h = *start; t = buf; got_sp = FALSE; while (len && inlen) { if (*h == '\r' || *h == '\n') { if (got_sp) { /* Ignore spaces at the end of line */ t --; } *t++ = '\r'; *t++ = '\n'; if (len > 1 && (*h == '\r' && h[1] == '\n')) { h += 2; len -= 2; } else { h ++; len --; added ++; } break; } else if (g_ascii_isspace (*h)) { if (got_sp) { /* Ignore multiply spaces */ h++; len--; continue; } else { *t++ = ' '; h++; inlen--; len--; got_sp = TRUE; continue; } } else { got_sp = FALSE; } *t++ = *h++; inlen--; len--; } *start = h; if (*remain > 0) { size_t cklen = MIN(t - buf, *remain + added); g_checksum_update (ck, buf, cklen); *remain = *remain - (cklen - added); #if 0 msg_debug_dkim ("update signature with buffer (%ud size, %ud remain, %ud added): %*s", cklen, *remain, added, cklen, buf); #else msg_debug_dkim ("update signature with body buffer " "(%ud size, %ud remain, %ud added)", cklen, *remain, added); #endif } return (len != 0); } static gboolean rspamd_dkim_simple_body_step (rspamd_dkim_context_t *ctx, GChecksum *ck, const gchar **start, guint size, guint *remain) { const gchar *h; static gchar buf[BUFSIZ]; gchar *t; guint len, inlen, added = 0; len = size; inlen = sizeof (buf) - 1; h = *start; t = &buf[0]; while (len && inlen) { if (*h == '\r' || *h == '\n') { *t++ = '\r'; *t++ = '\n'; if (len > 1 && (*h == '\r' && h[1] == '\n')) { h += 2; len -= 2; } else { h ++; len --; added ++; } break; } *t++ = *h++; inlen--; len--; } *start = h; if (*remain > 0) { size_t cklen = MIN(t - buf, *remain + added); g_checksum_update (ck, buf, cklen); *remain = *remain - (cklen - added); msg_debug_dkim ("update signature with body buffer " "(%ud size, %ud remain, %ud added)", cklen, *remain, added); } return (len != 0); } static gboolean rspamd_dkim_canonize_body (rspamd_dkim_context_t *ctx, const gchar *start, const gchar *end) { const gchar *p; guint remain = ctx->len ? ctx->len : (guint)(end - start); if (start == NULL) { /* Empty body */ if (ctx->body_canon_type == DKIM_CANON_SIMPLE) { g_checksum_update (ctx->body_hash, CRLF, sizeof (CRLF) - 1); } else { g_checksum_update (ctx->body_hash, "", 0); } } else { /* Strip extra ending CRLF */ p = end - 1; while (p >= start + 2) { if (*p == '\n' && *(p - 1) == '\r' && *(p - 2) == '\n') { p -= 2; } else if (*p == '\n' && *(p - 1) == '\n') { p--; } else if (*p == '\r' && *(p - 1) == '\r') { p--; } else { break; } } end = p + 1; if (end == start) { /* Empty body */ if (ctx->body_canon_type == DKIM_CANON_SIMPLE) { g_checksum_update (ctx->body_hash, CRLF, sizeof (CRLF) - 1); } else { g_checksum_update (ctx->body_hash, "", 0); } } else { if (ctx->body_canon_type == DKIM_CANON_SIMPLE) { /* Simple canonization */ while (rspamd_dkim_simple_body_step (ctx, ctx->body_hash, &start, end - start, &remain)) ; } else { while (rspamd_dkim_relaxed_body_step (ctx, ctx->body_hash, &start, end - start, &remain)) ; } } return TRUE; } /* TODO: Implement relaxed algorithm */ return FALSE; } /* Update hash converting all CR and LF to CRLF */ static void rspamd_dkim_hash_update (GChecksum *ck, const gchar *begin, gsize len) { const gchar *p, *c, *end; end = begin + len; p = begin; c = p; while (p != end) { if (*p == '\r') { g_checksum_update (ck, c, p - c); g_checksum_update (ck, CRLF, sizeof (CRLF) - 1); p++; if (*p == '\n') { p++; } c = p; } else if (*p == '\n') { g_checksum_update (ck, c, p - c); g_checksum_update (ck, CRLF, sizeof (CRLF) - 1); p++; c = p; } else { p++; } } if (p != c) { g_checksum_update (ck, c, p - c); } } /* Update hash by signature value (ignoring b= tag) */ static void rspamd_dkim_signature_update (rspamd_dkim_context_t *ctx, const gchar *begin, guint len) { const gchar *p, *c, *end; gboolean tag, skip; end = begin + len; p = begin; c = begin; tag = TRUE; skip = FALSE; while (p < end) { if (tag && p[0] == 'b' && p[1] == '=') { /* Add to signature */ msg_debug_dkim ("initial update hash with signature part: %*s", p - c + 2, c); rspamd_dkim_hash_update (ctx->headers_hash, c, p - c + 2); skip = TRUE; } else if (skip && (*p == ';' || p == end - 1)) { skip = FALSE; c = p; } else if (!tag && *p == ';') { tag = TRUE; } else if (tag && *p == '=') { tag = FALSE; } p++; } p--; /* Skip \r\n at the end */ while ((*p == '\r' || *p == '\n') && p >= c) { p--; } if (p - c + 1 > 0) { msg_debug_dkim ("final update hash with signature part: %*s", p - c + 1, c); rspamd_dkim_hash_update (ctx->headers_hash, c, p - c + 1); } } static gboolean rspamd_dkim_canonize_header_relaxed (rspamd_dkim_context_t *ctx, const gchar *header, const gchar *header_name, gboolean is_sign) { const gchar *h; gchar *t, *buf; guint inlen; gboolean got_sp, allocated = FALSE; inlen = strlen (header) + strlen (header_name) + sizeof (":" CRLF); if (inlen > BUFSIZ) { buf = g_malloc (inlen); allocated = TRUE; } else { /* Faster */ buf = g_alloca (inlen); } /* Name part */ t = buf; h = header_name; while (*h) { *t++ = g_ascii_tolower (*h++); } *t++ = ':'; /* Value part */ h = header; /* Skip spaces at the beginning */ while (g_ascii_isspace (*h)) { h++; } got_sp = FALSE; while (*h) { if (g_ascii_isspace (*h)) { if (got_sp) { h++; continue; } else { got_sp = TRUE; *t++ = ' '; h++; continue; } } else { got_sp = FALSE; } *t++ = *h++; } if (g_ascii_isspace (*(t - 1))) { t--; } *t++ = '\r'; *t++ = '\n'; *t = '\0'; if (!is_sign) { msg_debug_dkim ("update signature with header: %s", buf); g_checksum_update (ctx->headers_hash, buf, t - buf); } else { rspamd_dkim_signature_update (ctx, buf, t - buf); } if (allocated) { g_free (buf); } return TRUE; } struct rspamd_dkim_sign_chunk { const gchar *begin; gsize len; gboolean append_crlf; }; static gboolean rspamd_dkim_canonize_header_simple (rspamd_dkim_context_t *ctx, const gchar *headers, gsize headers_length, const gchar *header_name, guint count, gboolean is_sign) { const gchar *p, *c, *end; gint state = 0, hlen; gboolean found = FALSE; GArray *to_sign; struct rspamd_dkim_sign_chunk chunk, *elt = NULL; gint i; /* This process is very similar to raw headers processing */ to_sign = g_array_sized_new (FALSE, FALSE, sizeof (struct rspamd_dkim_sign_chunk), count); p = headers; end = p + headers_length; c = p; hlen = strlen (header_name); while (p < end) { switch (state) { case 0: /* Compare state */ if (*p == ':') { /* Compare header's name with desired one */ if (p - c == hlen) { if (g_ascii_strncasecmp (c, header_name, hlen) == 0) { /* Get value */ state = 2; } else { /* Skip the whole header */ state = 1; } } else { /* Skip the whole header */ state = 1; } } p++; break; case 1: /* Skip header state */ if (*p == '\n' && !g_ascii_isspace (p[1])) { /* Header is skipped */ state = 0; c = p + 1; } p++; break; case 2: /* c contains the beginning of header */ if (*p == '\n' && (!g_ascii_isspace (p[1]) || p[1] == '\0')) { chunk.begin = c; if (*(p - 1) == '\r') { chunk.len = p - c + 1; chunk.append_crlf = FALSE; } else { /* Need append CRLF as linefeed is not proper */ chunk.len = p - c; chunk.append_crlf = TRUE; } g_array_append_val (to_sign, chunk); c = p + 1; state = 0; found = TRUE; } p++; break; } } if (found) { if (!is_sign) { for (i = to_sign->len - 1; i >= 0 && count > 0; i--, count--) { elt = &g_array_index (to_sign, struct rspamd_dkim_sign_chunk, i); if (!chunk.append_crlf) { msg_debug_dkim ("update signature with header: %*s", elt->len, elt->begin); rspamd_dkim_hash_update (ctx->headers_hash, elt->begin, elt->len); } else { msg_debug_dkim ("update signature with header: %*s", elt->len + 1, elt->begin); rspamd_dkim_hash_update (ctx->headers_hash, elt->begin, elt->len + 1); } } } else { /* Try to find the proper header by domain */ for (i = to_sign->len - 1; i >= 0; i--) { elt = &g_array_index (to_sign, struct rspamd_dkim_sign_chunk, i); if (rspamd_substring_search (elt->begin, elt->len, ctx->domain, strlen (ctx->domain)) != -1) { break; } } if (elt && elt->append_crlf) { rspamd_dkim_signature_update (ctx, elt->begin, elt->len + 1); } else { rspamd_dkim_signature_update (ctx, elt->begin, elt->len); } } } g_array_free (to_sign, TRUE); return found; } static gboolean rspamd_dkim_canonize_header (rspamd_dkim_context_t *ctx, struct rspamd_task *task, const gchar *header_name, guint count, gboolean is_sig) { struct raw_header *rh, *rh_iter; guint rh_num = 0; GList *nh = NULL, *cur; if (ctx->header_canon_type == DKIM_CANON_SIMPLE) { return rspamd_dkim_canonize_header_simple (ctx, task->raw_headers_content.begin, task->raw_headers_content.len, header_name, count, is_sig); } else { rh = g_hash_table_lookup (task->raw_headers, header_name); if (rh) { if (!is_sig) { rh_iter = rh; while (rh_iter) { rh_num++; rh_iter = rh_iter->next; } if (rh_num > count) { /* Set skip count */ rh_num -= count; } else { rh_num = 0; } rh_iter = rh; while (rh_num) { rh_iter = rh_iter->next; rh_num--; } /* Now insert required headers */ while (rh_iter) { nh = g_list_prepend (nh, rh_iter); rh_iter = rh_iter->next; } cur = nh; while (cur) { rh = cur->data; if (!rspamd_dkim_canonize_header_relaxed (ctx, rh->value, header_name, is_sig)) { g_list_free (nh); return FALSE; } cur = g_list_next (cur); } if (nh != NULL) { g_list_free (nh); } } else { /* For signature check just use the saved dkim header */ rspamd_dkim_canonize_header_relaxed (ctx, ctx->dkim_header, header_name, is_sig); } return TRUE; } } /* TODO: Implement relaxed algorithm */ return FALSE; } /** * Check task for dkim context using dkim key * @param ctx dkim verify context * @param key dkim key (from cache or from dns request) * @param task task to check * @return */ gint rspamd_dkim_check (rspamd_dkim_context_t *ctx, rspamd_dkim_key_t *key, struct rspamd_task *task) { const gchar *p, *headers_end = NULL, *end, *body_end; gboolean got_cr = FALSE, got_crlf = FALSE, got_lf = FALSE; gchar *digest; gsize dlen; gint res = DKIM_CONTINUE; guint i; struct rspamd_dkim_header *dh; #ifdef HAVE_OPENSSL gint nid; #endif g_return_val_if_fail (ctx != NULL, DKIM_ERROR); g_return_val_if_fail (key != NULL, DKIM_ERROR); g_return_val_if_fail (task->msg.len > 0, DKIM_ERROR); /* First of all find place of body */ p = task->msg.begin; end = task->msg.begin + task->msg.len; while (p <= end) { /* Search for \r\n\r\n at the end of headers */ if (*p == '\n') { if (got_cr && *(p - 1) == '\r') { if (got_crlf) { /* \r\n\r\n */ headers_end = p + 1; break; } else if (got_lf) { /* \n\r\n */ headers_end = p + 1; break; } else { /* Set got crlf flag */ got_crlf = TRUE; got_cr = FALSE; got_lf = FALSE; } } else if (got_cr && *(p - 1) != '\r') { /* We got CR somewhere but not right before */ got_cr = FALSE; if (*(p - 1) == '\n') { /* \r\n\n case */ headers_end = p + 1; break; } got_lf = TRUE; } else if (got_lf && *(p - 1) == '\n') { /* \n\n case */ headers_end = p + 1; break; } else { got_lf = TRUE; } } else if (*p == '\r') { if (got_cr && *(p - 1) == '\r') { /* \r\r case */ headers_end = p + 1; break; } else if (got_lf && *(p - 1) != '\n') { /* Sequence is broken */ got_lf = FALSE; got_cr = TRUE; } else { got_cr = TRUE; } } else { got_cr = FALSE; got_crlf = FALSE; } p++; } /* Start canonization of body part */ body_end = end; if (!rspamd_dkim_canonize_body (ctx, headers_end, body_end)) { return DKIM_RECORD_ERROR; } /* Now canonize headers */ for (i = 0; i < ctx->hlist->len; i++) { dh = g_ptr_array_index (ctx->hlist, i); rspamd_dkim_canonize_header (ctx, task, dh->name, dh->count, FALSE); } /* Canonize dkim signature */ rspamd_dkim_canonize_header (ctx, task, DKIM_SIGNHEADER, 1, TRUE); dlen = ctx->bhlen; digest = g_alloca (dlen); g_checksum_get_digest (ctx->body_hash, digest, &dlen); /* Check bh field */ if (memcmp (ctx->bh, digest, dlen) != 0) { msg_debug_dkim ("bh value missmatch: %*xs versus %*xs", dlen, ctx->bh, dlen, digest); return DKIM_REJECT; } g_checksum_get_digest (ctx->headers_hash, digest, &dlen); #ifdef HAVE_OPENSSL /* Check headers signature */ if (ctx->sig_alg == DKIM_SIGN_RSASHA1) { nid = NID_sha1; } else if (ctx->sig_alg == DKIM_SIGN_RSASHA256) { nid = NID_sha256; } else { /* Not reached */ nid = NID_sha1; } if (RSA_verify (nid, digest, dlen, ctx->b, ctx->blen, key->key_rsa) != 1) { msg_debug_dkim ("rsa verify failed"); res = DKIM_REJECT; } #endif return res; }