summaryrefslogtreecommitdiffstats
path: root/src/dkim.c
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@rambler-co.ru>2012-05-24 21:35:03 +0400
committerVsevolod Stakhov <vsevolod@rambler-co.ru>2012-05-24 21:35:03 +0400
commit3f333be9d531f0edecf3f31a241a8692e79a86bd (patch)
tree16b8bc275fc739df81fc25b06cdbb93d63f0d424 /src/dkim.c
parent1b171ea80eb559fe4f4529a20bc68e1fb0ea2226 (diff)
downloadrspamd-3f333be9d531f0edecf3f31a241a8692e79a86bd.tar.gz
rspamd-3f333be9d531f0edecf3f31a241a8692e79a86bd.zip
* Start dkim support implementation.
Diffstat (limited to 'src/dkim.c')
-rw-r--r--src/dkim.c536
1 files changed, 536 insertions, 0 deletions
diff --git a/src/dkim.c b/src/dkim.c
new file mode 100644
index 000000000..8a25a08c2
--- /dev/null
+++ b/src/dkim.c
@@ -0,0 +1,536 @@
+/* Copyright (c) 2010-2011, Vsevolod Stakhov
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "main.h"
+#include "dkim.h"
+
+/* 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
+};
+
+#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 = memory_pool_alloc (ctx->pool, len + 1);
+ rspamd_strlcpy (ctx->b, param, len + 1);
+ g_base64_decode_inplace (ctx->b, &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 = memory_pool_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", len) == 0) {
+ ctx->header_canon_type = DKIM_CANON_SIMPLE;
+ }
+ else if (sl == 7 && memcmp (param, "relaxed", len) == 0) {
+ ctx->header_canon_type = DKIM_CANON_RELAXED;
+ }
+ else {
+ goto err;
+ }
+ /* Check body */
+ len = 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 sign 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 = memory_pool_alloc (ctx->pool, len + 1);
+ rspamd_strlcpy (ctx->selector, param, len + 1);
+ return 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;
+
+ c = param;
+ p = param;
+ while (p <= end) {
+ if ((*p == ':' || p == end) && p - c > 0) {
+ /* Insert new header to the list */
+ if (p == end) {
+ h = memory_pool_alloc (ctx->pool, p - c + 1);
+ rspamd_strlcpy (h, c, p - c + 1);
+ }
+ else {
+ h = memory_pool_alloc (ctx->pool, p - c);
+ rspamd_strlcpy (h, c, p - c);
+ }
+ /* Check mandatory from */
+ if (!from_found && g_ascii_strcasecmp (h, "from") == 0) {
+ from_found = TRUE;
+ }
+ ctx->hlist = g_list_prepend (ctx->hlist, h);
+ 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_list_free (ctx->hlist);
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_INVALID_H, "invalid dkim header list, from header is missing");
+ return FALSE;
+ }
+ /* Reverse list */
+ ctx->hlist = g_list_reverse (ctx->hlist);
+ memory_pool_add_destructor (ctx->pool, (pool_destruct_func)g_list_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 = memory_pool_alloc (ctx->pool, len + 1);
+ rspamd_strlcpy (ctx->bh, param, len + 1);
+ g_base64_decode_inplace (ctx->bh, &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, memory_pool_t *pool, GError **err)
+{
+ const gchar *p, *c, *tag;
+ gsize taglen;
+ gint param = DKIM_PARAM_UNKNOWN;
+ time_t now;
+ rspamd_dkim_context_t *new;
+ enum {
+ DKIM_STATE_TAG = 0,
+ DKIM_STATE_AFTER_TAG,
+ DKIM_STATE_VALUE,
+ DKIM_STATE_SKIP_SPACES = 99,
+ DKIM_STATE_ERROR = 100
+ } state, next_state;
+
+
+ new = memory_pool_alloc0 (pool, sizeof (rspamd_dkim_context_t));
+ new->pool = pool;
+ new->header_canon_type = DKIM_CANON_DEFAULT;
+ new->body_canon_type = DKIM_CANON_DEFAULT;
+ new->sig_alg = DKIM_SIGN_UNKNOWN;
+ /* A simple state machine of parsing tags */
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_TAG;
+ taglen = 0;
+ while (*p) {
+ 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 {
+ p ++;
+ taglen ++;
+ }
+ 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 lenght: %zd", taglen);
+ state = DKIM_STATE_ERROR;
+ break;
+ }
+ if (state != DKIM_STATE_ERROR) {
+ /* Skip spaces */
+ p ++;
+ state = DKIM_STATE_SKIP_SPACES;
+ next_state = DKIM_STATE_VALUE;
+ }
+ break;
+ case DKIM_STATE_VALUE:
+ if (*p == ';') {
+ if (param == DKIM_PARAM_UNKNOWN || !parser_funcs[param](new, c, p - c - 1, err)) {
+ state = DKIM_STATE_ERROR;
+ }
+ }
+ 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) {
+ msg_info ("dkim parse failed: %s", (*err)->message);
+ return NULL;
+ }
+ else {
+ msg_info ("dkim parse failed: unknown error");
+ return NULL;
+ }
+ break;
+ }
+ }
+
+ /* Now check validity of signature */
+ if (new->b == NULL) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_B, "b parameter missing");
+ return NULL;
+ }
+ if (new->bh == NULL) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_BH, "bh parameter missing");
+ return NULL;
+ }
+ if (new->domain == NULL) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_D, "domain parameter missing");
+ return NULL;
+ }
+ if (new->selector == NULL) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_S, "selector parameter missing");
+ return NULL;
+ }
+ if (new->ver == 0) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_V, "v parameter missing");
+ return NULL;
+ }
+ if (new->hlist == NULL) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_H, "h parameter missing");
+ return NULL;
+ }
+ if (new->sig_alg == DKIM_SIGN_UNKNOWN) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_S, "s parameter missing");
+ return NULL;
+ }
+ /* Check expiration */
+ now = time (NULL);
+ if (new->timestamp && new->timestamp > now) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_FUTURE, "signature was made in future, ignoring");
+ return NULL;
+ }
+ if (new->expiration && new->expiration < now) {
+ g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EXPIRED, "signature has expired");
+ return NULL;
+ }
+
+ return new;
+}
+
+/**
+ * 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
+ */
+rspamd_dkim_key_t*
+rspamd_get_dkim_key (rspamd_dkim_context_t *ctx, struct rspamd_dns_resolver *resolver,
+ struct rspamd_async_session *s)
+{
+ /* TODO: add this parser as well */
+ return NULL;
+}
+
+/**
+ * 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 worker_task *task)
+{
+ /* TODO: this check must be implemented */
+ return DKIM_CONTINUE;
+}