From eccece67e64096eb77f743a96c3b98405874bb5c Mon Sep 17 00:00:00 2001 From: Vsevolod Stakhov Date: Mon, 28 May 2012 21:31:56 +0400 Subject: [PATCH] * Add signing and simple canonization support (not finished yet, work in progress). --- CMakeLists.txt | 10 ++ config.h.in | 2 + src/dkim.c | 343 +++++++++++++++++++++++++++++++++++++++++++++++-- src/dkim.h | 26 +++- 4 files changed, 372 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 025b6ff0f..6d9ad74fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ INCLUDE(CheckLibraryExists) INCLUDE(FindPkgConfig) INCLUDE(CheckCCompilerFlag) INCLUDE(FindPerl) +INCLUDE(FindOpenSSL) ############################# MODULES SECTION ############################################# @@ -393,6 +394,12 @@ IF(SQLITE_LIBRARY_DIRS) LINK_DIRECTORIES("${SQLITE_LIBRARY_DIRS}") ENDIF(SQLITE_LIBRARY_DIRS) +#Check for openssl (required for dkim) +IF(OPENSSL_FOUND) + SET(HAVE_OPENSSL 1) + INCLUDE_DIRECTORIES("${OPENSSL_INCLUDE_DIR}") +ENDIF(OPENSSL_FOUND) + IF(ENABLE_STATIC MATCHES "ON") pkg_check_modules(GLIB2 REQUIRED glib-2.0>=2.12) ELSE(ENABLE_STATIC MATCHES "ON") @@ -948,6 +955,9 @@ IF(GMIME24) ELSE(GMIME24) TARGET_LINK_LIBRARIES(rspamd ${GMIME2_LIBRARIES}) ENDIF(GMIME24) +IF(OPENSSL_FOUND) + TARGET_LINK_LIBRARIES(rspamd ${OPENSSL_LIBRARIES}) +ENDIF(OPENSSL_FOUND) IF(ENABLE_STATIC MATCHES "ON") TARGET_LINK_LIBRARIES(rspamd ${PCRE_LIBRARIES}) diff --git a/config.h.in b/config.h.in index 186021446..f33a1b766 100644 --- a/config.h.in +++ b/config.h.in @@ -185,6 +185,8 @@ #cmakedefine HAVE_CLOCK_GETTIME 1 +#cmakedefine HAVE_OPENSSL 1 + #cmakedefine GLIB_COMPAT 1 #cmakedefine GLIB_RE_COMPAT 1 #cmakedefine GLIB_UNISCRIPT_COMPAT 1 diff --git a/src/dkim.c b/src/dkim.c index 97fd8db7e..4c597ec40 100644 --- a/src/dkim.c +++ b/src/dkim.c @@ -74,6 +74,7 @@ rspamd_dkim_parse_signature (rspamd_dkim_context_t* ctx, const gchar *param, gsi ctx->b = memory_pool_alloc (ctx->pool, len + 1); rspamd_strlcpy (ctx->b, param, len + 1); g_base64_decode_inplace (ctx->b, &len); + ctx->blen = len; return TRUE; } @@ -273,6 +274,7 @@ rspamd_dkim_parse_bodyhash (rspamd_dkim_context_t* ctx, const gchar *param, gsiz ctx->bh = memory_pool_alloc (ctx->pool, len + 1); rspamd_strlcpy (ctx->bh, param, len + 1); g_base64_decode_inplace (ctx->bh, &len); + ctx->bhlen = len; return TRUE; } @@ -501,6 +503,20 @@ rspamd_create_dkim_context (const gchar *sig, memory_pool_t *pool, GError **err) g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_EMPTY_S, "s parameter missing"); return NULL; } + if (new->sig_alg == DKIM_SIGN_RSASHA1) { + /* Check bh length */ + if (new->bhlen != g_checksum_type_get_length (G_CHECKSUM_SHA1)) { + g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_BADSIG, "signature has incorrect length: %ud", new->bhlen); + return NULL; + } + + } + else if (new->sig_alg == DKIM_SIGN_RSASHA256) { + if (new->bhlen != g_checksum_type_get_length (G_CHECKSUM_SHA256)) { + g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_BADSIG, "signature has incorrect length: %ud", new->bhlen); + return NULL; + } + } /* Check expiration */ now = time (NULL); if (new->timestamp && new->timestamp > now) { @@ -517,6 +533,23 @@ rspamd_create_dkim_context (const gchar *sig, memory_pool_t *pool, GError **err) new->dns_key = memory_pool_alloc (new->pool, taglen); rspamd_snprintf (new->dns_key, taglen, "%s.%s.%s", new->selector, DKIM_DNSKEYNAME, new->domain); + /* Create checksums for further operations */ + if (new->sig_alg == DKIM_SIGN_RSASHA1) { + new->body_hash = g_checksum_new (G_CHECKSUM_SHA1); + new->headers_hash = g_checksum_new (G_CHECKSUM_SHA1); + } + else if (new->sig_alg == DKIM_SIGN_RSASHA256) { + new->body_hash = g_checksum_new (G_CHECKSUM_SHA256); + new->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; + } + + memory_pool_add_destructor (new->pool, (pool_destruct_func)g_checksum_free, new->body_hash); + memory_pool_add_destructor (new->pool, (pool_destruct_func)g_checksum_free, new->headers_hash); + return new; } @@ -526,13 +559,69 @@ struct rspamd_dkim_key_cbdata { gpointer ud; }; +static rspamd_dkim_key_t* +rspamd_dkim_make_key (const gchar *keydata, guint keylen, GError *err) +{ + rspamd_dkim_key_t *key = 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; + g_base64_decode_inplace (key->keydata, &key->decoded_len); +#ifdef HAVE_OPENSSL + key->key_bio = BIO_new_mem_buf (key->keydata, decoded_len); + if (key->key_bio == NULL) { + g_set_error (err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, "cannot make ssl bio from key"); + rspamd_dkim_key_free (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"); + rspamd_dkim_key_free (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"); + rspamd_dkim_key_free (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_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 (const gchar *txt, gsize *keylen, GError **err) { const gchar *c, *p, *end; gint state = 0; gsize len; - rspamd_dkim_key_t *key = NULL; c = txt; p = txt; @@ -556,12 +645,7 @@ rspamd_dkim_parse_key (const gchar *txt, gsize *keylen, GError **err) /* State when we got p= and looking for some public key */ if ((*p == ';' || p == end) && p > c) { len = (p == end) ? p - c : p - c - 1; - key = g_slice_alloc (len + 1); - /* For free data */ - *keylen = len + 1; - rspamd_strlcpy (key, c, len + 1); - g_base64_decode_inplace (key, &len); - return key; + return rspamd_dkim_make_key (c, len, err); } break; } @@ -635,6 +719,146 @@ rspamd_get_dkim_key (rspamd_dkim_context_t *ctx, struct rspamd_dns_resolver *res return make_dns_request (resolver, s, ctx->pool, rspamd_dkim_dns_cb, cbdata, DNS_REQUEST_TXT, ctx->dns_key); } +static gboolean +rspamd_dkim_canonize_body (rspamd_dkim_context_t *ctx, const gchar *start, const gchar *end) +{ + if (ctx->body_canon_type == DKIM_CANON_SIMPLE) { + /* Perform simple canonization */ + if (start == NULL) { + /* Empty body */ + g_checksum_update (ctx->body_hash, CRLF, sizeof (CRLF) - 2); + } + else { + while (end > start + 2) { + if (*end == '\n' && *(end - 1) == '\r' && *(end - 2) == '\n') { + end -= 2; + } + } + if (end == start || end == start + 2) { + /* Empty body */ + g_checksum_update (ctx->body_hash, CRLF, sizeof (CRLF) - 2); + } + else { + g_checksum_update (ctx->body_hash, start, end - start); + } + } + return TRUE; + } + + /* TODO: Implement relaxed algorithm */ + return FALSE; +} + +/* 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; + tag = TRUE; + skip = FALSE; + + while (p >= end) { + if (tag && p[0] == 'b' && p[1] == '=') { + /* Add to signature */ + g_checksum_update (ctx->headers_hash, c, p - c + 2); + skip = TRUE; + } + else if (skip && *p == ';') { + skip = FALSE; + c = p; + } + else if (!tag && *p == ';') { + tag = TRUE; + } + else if (tag && *p == '=') { + tag = FALSE; + } + p ++; + } + + /* Skip \r\n at the end */ + while ((*p == '\r' || *p == '\n') && p > c) { + p --; + } + g_checksum_update (ctx->headers_hash, c, p - c); +} + +static gboolean +rspamd_dkim_canonize_header_simple (rspamd_dkim_context_t *ctx, const gchar *headers, const gchar *header_name, gboolean is_sign) +{ + const gchar *p, *c; + gint state = 0, hlen; + gboolean found = FALSE; + + /* This process is very similar to raw headers processing */ + p = headers; + c = p; + hlen = strlen (header_name); + + while (*p) { + switch (state) { + case 0: + /* Compare state */ + if (*p == ':') { + /* Compare header's name with desired one */ + if (p - c - 1 == hlen) { + if (g_ascii_strncasecmp (c, header_name, hlen) == 0) { + /* Get value */ + state = 2; + } + } + 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')) { + if (!is_sign) { + g_checksum_update (ctx->headers_hash, c, p - c); + } + else { + rspamd_dkim_signature_update (ctx, c, p - c); + } + c = p + 1; + state = 0; + found = TRUE; + } + p ++; + break; + } + } + + return found; +} + +static gboolean +rspamd_dkim_canonize_header (rspamd_dkim_context_t *ctx, struct worker_task *task, const gchar *header_name, gboolean is_sig) +{ + if (ctx->header_canon_type == DKIM_CANON_SIMPLE) { + return rspamd_dkim_canonize_header_simple (ctx, task->raw_headers_str, header_name, is_sig); + } + + /* TODO: Implement relaxed algorithm */ + return FALSE; +} + /** * Check task for dkim context using dkim key * @param ctx dkim verify context @@ -645,6 +869,109 @@ rspamd_get_dkim_key (rspamd_dkim_context_t *ctx, struct rspamd_dns_resolver *res gint rspamd_dkim_check (rspamd_dkim_context_t *ctx, rspamd_dkim_key_t *key, struct worker_task *task) { - /* TODO: this check must be implemented */ + const gchar *p, *headers_end = NULL, *end; + gboolean got_cr = FALSE, got_crlf = FALSE, got_lf = FALSE; + GList *cur; + gchar *digest; + gsize dlen; +#ifdef HAVE_OPENSSL + RSA *rsa; +#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 != NULL, 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 { + /* Set got crlf flag */ + got_crlf = TRUE; + got_cr = 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') { + /* \n\r case */ + headers_end = p + 1; + break; + } + else { + got_cr = TRUE; + } + } + } + + /* Start canonization of body part */ + if (!rspamd_dkim_canonize_body (ctx, headers_end, end)) { + return DKIM_ERROR; + } + /* Now canonize headers */ + cur = ctx->hlist; + while (cur) { + if (!rspamd_dkim_canonize_header (ctx, task, cur->data, FALSE)) { + return DKIM_ERROR; + } + cur = g_list_next (cur); + } + + /* Canonize dkim signature */ + rspamd_dkim_canonize_header (ctx, task, DKIM_SIGNHEADER, 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) { + return DKIM_ERROR; + } + +#ifdef HAVE_OPENSSL + /* Check headers signature */ + rsa = RSA_new (); + + rsa->rsa_rsa = key->rsa_key; + rsa->rsa_keysize = RSA_size (rsa->rsa_rsa); + rsa->rsa_pad = RSA_PKCS1_PADDING; + + + RSA_free (rsa); +#endif return DKIM_CONTINUE; } diff --git a/src/dkim.h b/src/dkim.h index 6bb26887d..bea6f4042 100644 --- a/src/dkim.h +++ b/src/dkim.h @@ -28,6 +28,10 @@ #include "config.h" #include "event.h" #include "dns.h" +#ifdef HAVE_OPENSSL +#include +#include +#endif /* Main types and definitions */ @@ -138,12 +142,26 @@ typedef struct rspamd_dkim_context_s { time_t expiration; gint8 *b; gint8 *bh; + guint bhlen; + guint blen; GList *hlist; guint ver; gchar *dns_key; + GChecksum *headers_hash; + GChecksum *body_hash; } rspamd_dkim_context_t; -typedef guint8 rspamd_dkim_key_t; +typedef struct rspamd_dkim_key_s { + guint8 *keydata; + guint keylen; + gsize decoded_len; +#ifdef HAVE_OPENSSL + RSA *rsa_key; + BIO *key_bio; + EVP_PKEY *key_evp; +#endif +} +rspamd_dkim_key_t; struct worker_task; @@ -178,4 +196,10 @@ gboolean rspamd_get_dkim_key (rspamd_dkim_context_t *ctx, struct rspamd_dns_reso */ gint rspamd_dkim_check (rspamd_dkim_context_t *ctx, rspamd_dkim_key_t *key, struct worker_task *task); +/** + * Free DKIM key + * @param key + */ +void rspamd_dkim_key_free (rspamd_dkim_key_t *key); + #endif /* DKIM_H_ */ -- 2.39.5