diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:30:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:30:40 +0000 |
commit | 133a45c109da5310add55824db21af5239951f93 (patch) | |
tree | ba6ac4c0a950a0dda56451944315d66409923918 /src/libserver/dkim.c | |
parent | Initial commit. (diff) | |
download | rspamd-upstream.tar.xz rspamd-upstream.zip |
Adding upstream version 3.8.1.upstream/3.8.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/libserver/dkim.c')
-rw-r--r-- | src/libserver/dkim.c | 3588 |
1 files changed, 3588 insertions, 0 deletions
diff --git a/src/libserver/dkim.c b/src/libserver/dkim.c new file mode 100644 index 0000000..4318e87 --- /dev/null +++ b/src/libserver/dkim.c @@ -0,0 +1,3588 @@ +/*- + * 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" +#include "unix-std.h" +#include "mempool_vars_internal.h" + +#include <openssl/evp.h> +#include <openssl/rsa.h> +#include <openssl/engine.h> + +/* special DNS tokens */ +#define DKIM_DNSKEYNAME "_domainkey" + +/* ed25519 key lengths */ +#define ED25519_B64_BYTES 45 +#define ED25519_BYTES 32 + +/* Canonization methods */ +#define DKIM_CANON_UNKNOWN (-1) /* unknown method */ +#define DKIM_CANON_SIMPLE 0 /* as specified in DKIM spec */ +#define DKIM_CANON_RELAXED 1 /* as specified in DKIM spec */ + +#define DKIM_CANON_DEFAULT DKIM_CANON_SIMPLE + +#define RSPAMD_SHORT_BH_LEN 8 + +/* Params */ +enum rspamd_dkim_param_type { + DKIM_PARAM_UNKNOWN = -1, + DKIM_PARAM_SIGNATURE = 0, + DKIM_PARAM_SIGNALG, + DKIM_PARAM_DOMAIN, + DKIM_PARAM_CANONALG, + DKIM_PARAM_QUERYMETHOD, + DKIM_PARAM_SELECTOR, + DKIM_PARAM_HDRLIST, + DKIM_PARAM_VERSION, + DKIM_PARAM_IDENTITY, + DKIM_PARAM_TIMESTAMP, + DKIM_PARAM_EXPIRATION, + DKIM_PARAM_COPIEDHDRS, + DKIM_PARAM_BODYHASH, + DKIM_PARAM_BODYLENGTH, + DKIM_PARAM_IDX, + DKIM_PARAM_CV, + DKIM_PARAM_IGNORE +}; + +#define RSPAMD_DKIM_MAX_ARC_IDX 10 + +#define msg_err_dkim(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL, \ + "dkim", ctx->pool->tag.uid, \ + RSPAMD_LOG_FUNC, \ + __VA_ARGS__) +#define msg_warn_dkim(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING, \ + "dkim", ctx->pool->tag.uid, \ + RSPAMD_LOG_FUNC, \ + __VA_ARGS__) +#define msg_info_dkim(...) rspamd_default_log_function(G_LOG_LEVEL_INFO, \ + "dkim", ctx->pool->tag.uid, \ + RSPAMD_LOG_FUNC, \ + __VA_ARGS__) +#define msg_debug_dkim(...) rspamd_conditional_debug_fast(NULL, NULL, \ + rspamd_dkim_log_id, "dkim", ctx->pool->tag.uid, \ + RSPAMD_LOG_FUNC, \ + __VA_ARGS__) +#define msg_debug_dkim_taskless(...) rspamd_conditional_debug_fast(NULL, NULL, \ + rspamd_dkim_log_id, "dkim", "", \ + RSPAMD_LOG_FUNC, \ + __VA_ARGS__) + +INIT_LOG_MODULE(dkim) + +#define RSPAMD_DKIM_FLAG_OVERSIGN (1u << 0u) +#define RSPAMD_DKIM_FLAG_OVERSIGN_EXISTING (1u << 1u) + +union rspamd_dkim_header_stat { + struct _st { + guint16 count; + guint16 flags; + } s; + guint32 n; +}; + +struct rspamd_dkim_common_ctx { + rspamd_mempool_t *pool; + guint64 sig_hash; + gsize len; + GPtrArray *hlist; + GHashTable *htable; /* header -> count mapping */ + EVP_MD_CTX *headers_hash; + EVP_MD_CTX *body_hash; + enum rspamd_dkim_type type; + guint idx; + gint header_canon_type; + gint body_canon_type; + guint body_canonicalised; + guint headers_canonicalised; + gboolean is_sign; +}; + +enum rspamd_arc_seal_cv { + RSPAMD_ARC_UNKNOWN = 0, + RSPAMD_ARC_NONE, + RSPAMD_ARC_INVALID, + RSPAMD_ARC_FAIL, + RSPAMD_ARC_PASS +}; + + +struct rspamd_dkim_context_s { + struct rspamd_dkim_common_ctx common; + rspamd_mempool_t *pool; + struct rspamd_dns_resolver *resolver; + gsize blen; + gsize bhlen; + gint sig_alg; + guint ver; + time_t timestamp; + time_t expiration; + gchar *domain; + gchar *selector; + gint8 *b; + gchar *short_b; + gint8 *bh; + gchar *dns_key; + enum rspamd_arc_seal_cv cv; + const gchar *dkim_header; +}; + +#define RSPAMD_DKIM_KEY_ID_LEN 16 + +struct rspamd_dkim_key_s { + guint8 *keydata; + guint8 *raw_key; + gsize keylen; + gsize decoded_len; + gchar key_id[RSPAMD_DKIM_KEY_ID_LEN]; + union { + RSA *key_rsa; + EC_KEY *key_ecdsa; + guchar *key_eddsa; + } key; + BIO *key_bio; + EVP_PKEY *key_evp; + time_t mtime; + guint ttl; + enum rspamd_dkim_key_type type; + ref_entry_t ref; +}; + +struct rspamd_dkim_sign_context_s { + struct rspamd_dkim_common_ctx common; + rspamd_dkim_sign_key_t *key; +}; + +struct rspamd_dkim_header { + const gchar *name; + gint count; +}; + +/* 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 gboolean rspamd_dkim_parse_idx(rspamd_dkim_context_t *ctx, + const gchar *param, + gsize len, + GError **err); +static gboolean rspamd_dkim_parse_cv(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, + [DKIM_PARAM_IDX] = rspamd_dkim_parse_idx, + [DKIM_PARAM_CV] = rspamd_dkim_parse_cv, + [DKIM_PARAM_IGNORE] = rspamd_dkim_parse_ignore, +}; + +#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_alloc0(ctx->pool, len); + ctx->short_b = rspamd_mempool_alloc0(ctx->pool, RSPAMD_SHORT_BH_LEN + 1); + rspamd_strlcpy(ctx->short_b, param, MIN(len, RSPAMD_SHORT_BH_LEN + 1)); + (void) rspamd_cryptobox_base64_decode(param, len, ctx->b, &ctx->blen); + + return TRUE; +} + +static gboolean +rspamd_dkim_parse_signalg(rspamd_dkim_context_t *ctx, + const gchar *param, + gsize len, + GError **err) +{ + /* XXX: ugly size comparison, improve this code style some day */ + 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; + } + else if (memcmp(param, "rsa-sha512", len) == 0) { + ctx->sig_alg = DKIM_SIGN_RSASHA512; + return TRUE; + } + } + else if (len == 15) { + if (memcmp(param, "ecdsa256-sha256", len) == 0) { + ctx->sig_alg = DKIM_SIGN_ECDSASHA256; + return TRUE; + } + else if (memcmp(param, "ecdsa256-sha512", len) == 0) { + ctx->sig_alg = DKIM_SIGN_ECDSASHA512; + return TRUE; + } + } + else if (len == 14) { + if (memcmp(param, "ed25519-sha256", len) == 0) { + ctx->sig_alg = DKIM_SIGN_EDDSASHA256; + 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) +{ + if (!rspamd_str_has_8bit(param, len)) { + ctx->domain = rspamd_mempool_alloc(ctx->pool, len + 1); + rspamd_strlcpy(ctx->domain, param, len + 1); + } + else { + ctx->domain = rspamd_dns_resolver_idna_convert_utf8(ctx->resolver, + ctx->pool, param, len, NULL); + + if (!ctx->domain) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_INVALID_H, + "invalid dkim domain tag %.*s: idna failed", + (int) len, param); + + return FALSE; + } + } + + 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->common.header_canon_type = DKIM_CANON_SIMPLE; + return TRUE; + } + else if (len == 7 && memcmp(param, "relaxed", len) == 0) { + ctx->common.header_canon_type = DKIM_CANON_RELAXED; + return TRUE; + } + } + else { + /* First check header */ + if (sl == 6 && memcmp(param, "simple", sl) == 0) { + ctx->common.header_canon_type = DKIM_CANON_SIMPLE; + } + else if (sl == 7 && memcmp(param, "relaxed", sl) == 0) { + ctx->common.header_canon_type = DKIM_CANON_RELAXED; + } + else { + goto err; + } + /* Check body */ + len -= sl + 1; + slash++; + if (len == 6 && memcmp(slash, "simple", len) == 0) { + ctx->common.body_canon_type = DKIM_CANON_SIMPLE; + return TRUE; + } + else if (len == 7 && memcmp(slash, "relaxed", len) == 0) { + ctx->common.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) +{ + + if (!rspamd_str_has_8bit(param, len)) { + ctx->selector = rspamd_mempool_alloc(ctx->pool, len + 1); + rspamd_strlcpy(ctx->selector, param, len + 1); + } + else { + ctx->selector = rspamd_dns_resolver_idna_convert_utf8(ctx->resolver, + ctx->pool, param, len, NULL); + + if (!ctx->selector) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_INVALID_H, + "invalid dkim selector tag %.*s: idna failed", + (int) len, param); + + return FALSE; + } + } + + return TRUE; +} + +static void +rspamd_dkim_hlist_free(void *ud) +{ + GPtrArray *a = ud; + + g_ptr_array_free(a, TRUE); +} + +static gboolean +rspamd_dkim_parse_hdrlist_common(struct rspamd_dkim_common_ctx *ctx, + const gchar *param, + gsize len, + gboolean sign, + GError **err) +{ + const gchar *c, *p, *end = param + len; + gchar *h; + gboolean from_found = FALSE, oversign, existing; + guint count = 0; + struct rspamd_dkim_header *new; + gpointer found; + union rspamd_dkim_header_stat u; + + 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; + ctx->htable = g_hash_table_new(rspamd_strcase_hash, rspamd_strcase_equal); + + while (p <= end) { + if ((p == end || *p == ':') && p - c > 0) { + oversign = FALSE; + existing = FALSE; + h = rspamd_mempool_alloc(ctx->pool, p - c + 1); + rspamd_strlcpy(h, c, p - c + 1); + + g_strstrip(h); + + if (sign) { + if (rspamd_lc_cmp(h, "(o)", 3) == 0) { + oversign = TRUE; + h += 3; + msg_debug_dkim("oversign header: %s", h); + } + else if (rspamd_lc_cmp(h, "(x)", 3) == 0) { + oversign = TRUE; + existing = TRUE; + h += 3; + msg_debug_dkim("oversign existing header: %s", h); + } + } + + /* Check mandatory from */ + if (!from_found && g_ascii_strcasecmp(h, "from") == 0) { + from_found = TRUE; + } + + new = rspamd_mempool_alloc(ctx->pool, + sizeof(struct rspamd_dkim_header)); + new->name = h; + new->count = 0; + u.n = 0; + + g_ptr_array_add(ctx->hlist, new); + found = g_hash_table_lookup(ctx->htable, h); + + if (oversign) { + if (found) { + msg_err_dkim("specified oversigned header more than once: %s", + h); + } + + u.s.flags |= RSPAMD_DKIM_FLAG_OVERSIGN; + + if (existing) { + u.s.flags |= RSPAMD_DKIM_FLAG_OVERSIGN_EXISTING; + } + + u.s.count = 0; + } + else { + if (found != NULL) { + u.n = GPOINTER_TO_UINT(found); + new->count = u.s.count; + u.s.count++; + } + else { + /* Insert new header order to the list */ + u.s.count = new->count + 1; + } + } + + g_hash_table_insert(ctx->htable, h, GUINT_TO_POINTER(u.n)); + + 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; + } + + rspamd_mempool_add_destructor(ctx->pool, + (rspamd_mempool_destruct_t) rspamd_dkim_hlist_free, + ctx->hlist); + rspamd_mempool_add_destructor(ctx->pool, + (rspamd_mempool_destruct_t) g_hash_table_unref, + ctx->htable); + } + + return TRUE; +} + +static gboolean +rspamd_dkim_parse_hdrlist(rspamd_dkim_context_t *ctx, + const gchar *param, + gsize len, + GError **err) +{ + return rspamd_dkim_parse_hdrlist_common(&ctx->common, param, len, FALSE, err); +} + +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_alloc0(ctx->pool, len); + (void) rspamd_cryptobox_base64_decode(param, len, ctx->bh, &ctx->bhlen); + + 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->common.len = val; + + return TRUE; +} + +static gboolean +rspamd_dkim_parse_idx(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 ARC idx"); + return FALSE; + } + ctx->common.idx = val; + + return TRUE; +} + +static gboolean +rspamd_dkim_parse_cv(rspamd_dkim_context_t *ctx, + const gchar *param, + gsize len, + GError **err) +{ + + /* Only check header */ + if (len == 4 && memcmp(param, "fail", len) == 0) { + ctx->cv = RSPAMD_ARC_FAIL; + return TRUE; + } + else if (len == 4 && memcmp(param, "pass", len) == 0) { + ctx->cv = RSPAMD_ARC_PASS; + return TRUE; + } + else if (len == 4 && memcmp(param, "none", len) == 0) { + ctx->cv = RSPAMD_ARC_NONE; + return TRUE; + } + else if (len == 7 && memcmp(param, "invalid", len) == 0) { + ctx->cv = RSPAMD_ARC_INVALID; + return TRUE; + } + + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "invalid arc seal verification result"); + + return FALSE; +} + + +static void +rspamd_dkim_add_arc_seal_headers(rspamd_mempool_t *pool, + struct rspamd_dkim_common_ctx *ctx) +{ + struct rspamd_dkim_header *hdr; + gint count = ctx->idx, i; + + ctx->hlist = g_ptr_array_sized_new(count * 3 - 1); + + for (i = 0; i < count; i++) { + /* Authentication results */ + hdr = rspamd_mempool_alloc(pool, sizeof(*hdr)); + hdr->name = RSPAMD_DKIM_ARC_AUTHHEADER; + hdr->count = -(i + 1); + g_ptr_array_add(ctx->hlist, hdr); + + /* Arc signature */ + hdr = rspamd_mempool_alloc(pool, sizeof(*hdr)); + hdr->name = RSPAMD_DKIM_ARC_SIGNHEADER; + hdr->count = -(i + 1); + g_ptr_array_add(ctx->hlist, hdr); + + /* Arc seal (except last one) */ + if (i != count - 1) { + hdr = rspamd_mempool_alloc(pool, sizeof(*hdr)); + hdr->name = RSPAMD_DKIM_ARC_SEALHEADER; + hdr->count = -(i + 1); + g_ptr_array_add(ctx->hlist, hdr); + } + } + + rspamd_mempool_add_destructor(ctx->pool, + (rspamd_mempool_destruct_t) rspamd_dkim_hlist_free, + ctx->hlist); +} + +/** + * 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, + struct rspamd_dns_resolver *resolver, + guint time_jitter, + enum rspamd_dkim_type type, + GError **err) +{ + const gchar *p, *c, *tag = NULL, *end; + gint taglen; + gint param = DKIM_PARAM_UNKNOWN; + const EVP_MD *md_alg; + 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; + + + if (sig == NULL) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_EMPTY_B, + "empty signature"); + return NULL; + } + + ctx = rspamd_mempool_alloc0(pool, sizeof(rspamd_dkim_context_t)); + ctx->pool = pool; + ctx->resolver = resolver; + + if (type == RSPAMD_DKIM_ARC_SEAL) { + ctx->common.header_canon_type = DKIM_CANON_RELAXED; + ctx->common.body_canon_type = DKIM_CANON_RELAXED; + } + else { + ctx->common.header_canon_type = DKIM_CANON_DEFAULT; + ctx->common.body_canon_type = DKIM_CANON_DEFAULT; + } + + ctx->sig_alg = DKIM_SIGN_UNKNOWN; + ctx->common.pool = pool; + ctx->common.type = type; + /* 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); + ctx->common.sig_hash = rspamd_cryptobox_fast_hash(sig, end - sig, + rspamd_hash_seed()); + + msg_debug_dkim("create dkim context sig = %L", ctx->common.sig_hash); + + while (p <= end) { + switch (state) { + case DKIM_STATE_TAG: + if (g_ascii_isspace(*p)) { + taglen = (int) (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++; + + if (taglen > G_MAXINT8) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "too long dkim tag"); + state = DKIM_STATE_ERROR; + } + else { + 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: + /* 1 character tags */ + switch (*tag) { + case 'v': + if (type == RSPAMD_DKIM_NORMAL) { + param = DKIM_PARAM_VERSION; + } + else { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "invalid ARC v param"); + state = DKIM_STATE_ERROR; + break; + } + 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': + if (type == RSPAMD_DKIM_ARC_SEAL) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "ARC seal must NOT have h= tag"); + state = DKIM_STATE_ERROR; + break; + } + else { + param = DKIM_PARAM_HDRLIST; + } + break; + case 'i': + if (type == RSPAMD_DKIM_NORMAL) { + param = DKIM_PARAM_IDENTITY; + } + else { + param = DKIM_PARAM_IDX; + } + 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; + case 'r': + param = DKIM_PARAM_IGNORE; + break; + default: + param = DKIM_PARAM_UNKNOWN; + msg_debug_dkim("unknown DKIM param %c, ignoring it", *tag); + break; + } + break; + case 2: + /* Two characters tags, e.g. `bh` */ + if (tag[0] == 'b' && tag[1] == 'h') { + if (type == RSPAMD_DKIM_ARC_SEAL) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "ARC seal must NOT have bh= tag"); + state = DKIM_STATE_ERROR; + } + else { + param = DKIM_PARAM_BODYHASH; + } + } + else if (tag[0] == 'c' && tag[1] == 'v') { + if (type != RSPAMD_DKIM_ARC_SEAL) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "cv tag is valid for ARC-Seal only"); + state = DKIM_STATE_ERROR; + } + else { + param = DKIM_PARAM_CV; + } + } + else { + param = DKIM_PARAM_UNKNOWN; + msg_debug_dkim("unknown DKIM param %*s, ignoring it", taglen, tag); + } + break; + default: + /* Long and unknown (yet) DKIM tag */ + param = DKIM_PARAM_UNKNOWN; + msg_debug_dkim("unknown DKIM param %*s, ignoring it", taglen, tag); + 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 (p - c == 0 || c > p) { + state = DKIM_STATE_ERROR; + } + else { + /* Cut trailing spaces for value */ + gint tlen = p - c; + const gchar *tmp = p - 1; + + while (tlen > 0) { + if (!g_ascii_isspace(*tmp)) { + break; + } + tlen--; + tmp--; + } + + if (param != DKIM_PARAM_UNKNOWN) { + if (!parser_funcs[param](ctx, c, tlen, err)) { + state = DKIM_STATE_ERROR; + } + else { + state = DKIM_STATE_SKIP_SPACES; + next_state = DKIM_STATE_TAG; + p++; + taglen = 0; + } + } + else { + /* Unknown param has been ignored */ + msg_debug_dkim("ignored unknown tag parameter value: %*s = %*s", + taglen, tag, tlen, c); + state = DKIM_STATE_SKIP_SPACES; + next_state = DKIM_STATE_TAG; + p++; + taglen = 0; + } + } + } + else if (p == end) { + /* Last parameter with no `;` character */ + gint tlen = p - c; + const gchar *tmp = p - 1; + + while (tlen > 0) { + if (!g_ascii_isspace(*tmp)) { + break; + } + tlen--; + tmp--; + } + + if (param != DKIM_PARAM_UNKNOWN) { + if (!parser_funcs[param](ctx, c, tlen, err)) { + state = DKIM_STATE_ERROR; + } + } + else { + msg_debug_dkim("ignored unknown tag parameter value: %*s: %*s", + taglen, tag, tlen, c); + } + + if (state == DKIM_STATE_ERROR) { + /* + * We need to return from here as state machine won't + * do any more steps after p == end + */ + if (err) { + msg_info_dkim("dkim parse failed: %e", *err); + } + + return NULL; + } + /* 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 when parsing %c tag", + tag ? *tag : '?'); + return NULL; + } + break; + } + } + + if (type == RSPAMD_DKIM_ARC_SEAL) { + rspamd_dkim_add_arc_seal_headers(pool, &ctx->common); + } + + /* 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->common.type != RSPAMD_DKIM_ARC_SEAL && 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->common.type == RSPAMD_DKIM_NORMAL && ctx->ver == 0) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_EMPTY_V, + "v parameter missing"); + return NULL; + } + if (ctx->common.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 (type != RSPAMD_DKIM_ARC_SEAL) { + if (ctx->sig_alg == DKIM_SIGN_RSASHA1) { + /* Check bh length */ + if (ctx->bhlen != (guint) EVP_MD_size(EVP_sha1())) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_BADSIG, + "signature has incorrect length: %zu", + ctx->bhlen); + return NULL; + } + } + else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 || + ctx->sig_alg == DKIM_SIGN_ECDSASHA256) { + if (ctx->bhlen != + (guint) EVP_MD_size(EVP_sha256())) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_BADSIG, + "signature has incorrect length: %zu", + ctx->bhlen); + return NULL; + } + } + else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 || + ctx->sig_alg == DKIM_SIGN_ECDSASHA512) { + if (ctx->bhlen != + (guint) EVP_MD_size(EVP_sha512())) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_BADSIG, + "signature has incorrect length: %zu", + 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; + } + + if (ctx->common.type != RSPAMD_DKIM_NORMAL && (ctx->common.idx == 0 || + ctx->common.idx > RSPAMD_DKIM_MAX_ARC_IDX)) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "i parameter missing or invalid for ARC"); + return NULL; + } + + if (ctx->common.type == RSPAMD_DKIM_ARC_SEAL) { + if (ctx->cv == RSPAMD_ARC_UNKNOWN) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_UNKNOWN, + "cv parameter missing or invalid for ARC"); + return NULL; + } + } + + /* Now create dns key to request further */ + gsize dnslen = strlen(ctx->domain) + strlen(ctx->selector) + + sizeof(DKIM_DNSKEYNAME) + 2; + ctx->dns_key = rspamd_mempool_alloc(ctx->pool, dnslen); + rspamd_snprintf(ctx->dns_key, + dnslen, + "%s.%s.%s", + ctx->selector, + DKIM_DNSKEYNAME, + ctx->domain); + + /* Create checksums for further operations */ + if (ctx->sig_alg == DKIM_SIGN_RSASHA1) { + md_alg = EVP_sha1(); + } + else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 || + ctx->sig_alg == DKIM_SIGN_ECDSASHA256 || + ctx->sig_alg == DKIM_SIGN_EDDSASHA256) { + md_alg = EVP_sha256(); + } + else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 || + ctx->sig_alg == DKIM_SIGN_ECDSASHA512) { + md_alg = EVP_sha512(); + } + else { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_BADSIG, + "signature has unsupported signature algorithm"); + + return NULL; + } +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + ctx->common.body_hash = EVP_MD_CTX_create(); + EVP_DigestInit_ex(ctx->common.body_hash, md_alg, NULL); + ctx->common.headers_hash = EVP_MD_CTX_create(); + EVP_DigestInit_ex(ctx->common.headers_hash, md_alg, NULL); + rspamd_mempool_add_destructor(pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, ctx->common.body_hash); + rspamd_mempool_add_destructor(pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, ctx->common.headers_hash); +#else + ctx->common.body_hash = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx->common.body_hash, md_alg, NULL); + ctx->common.headers_hash = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx->common.headers_hash, md_alg, NULL); + rspamd_mempool_add_destructor(pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_free, ctx->common.body_hash); + rspamd_mempool_add_destructor(pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_free, ctx->common.headers_hash); +#endif + ctx->dkim_header = sig; + + return ctx; +} + +struct rspamd_dkim_key_cbdata { + rspamd_dkim_context_t *ctx; + dkim_key_handler_f handler; + gpointer ud; +}; + +rspamd_dkim_key_t * +rspamd_dkim_make_key(const gchar *keydata, + guint keylen, enum rspamd_dkim_key_type type, GError **err) +{ + rspamd_dkim_key_t *key = NULL; + + if (keylen < 3) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_KEYFAIL, + "DKIM key is too short to be valid"); + return NULL; + } + + key = g_malloc0(sizeof(rspamd_dkim_key_t)); + REF_INIT_RETAIN(key, rspamd_dkim_key_free); + key->keydata = g_malloc0(keylen + 1); + key->raw_key = g_malloc(keylen); + key->decoded_len = keylen; + key->type = type; + + /* Copy key skipping all spaces and newlines */ + const char *h = keydata; + guint8 *t = key->raw_key; + + while (h - keydata < keylen) { + if (!g_ascii_isspace(*h)) { + *t++ = *h++; + } + else { + h++; + } + } + + key->keylen = t - key->raw_key; + + if (!rspamd_cryptobox_base64_decode(key->raw_key, key->keylen, key->keydata, + &key->decoded_len)) { + REF_RELEASE(key); + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_KEYFAIL, + "DKIM key is not a valid base64 string"); + + return NULL; + } + + /* Calculate ID -> md5 */ + EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); + +#ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW + EVP_MD_CTX_set_flags(mdctx, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); +#endif + + if (EVP_DigestInit_ex(mdctx, EVP_md5(), NULL) == 1) { + guint dlen = sizeof(key->key_id); + + EVP_DigestUpdate(mdctx, key->keydata, key->decoded_len); + EVP_DigestFinal_ex(mdctx, key->key_id, &dlen); + } + + EVP_MD_CTX_destroy(mdctx); + + if (key->type == RSPAMD_DKIM_KEY_EDDSA) { + key->key.key_eddsa = key->keydata; + + if (key->decoded_len != rspamd_cryptobox_pk_sig_bytes( + RSPAMD_CRYPTOBOX_MODE_25519)) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_KEYFAIL, + "DKIM key is has invalid length %d for eddsa; expected %d", + (gint) key->decoded_len, + rspamd_cryptobox_pk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519)); + REF_RELEASE(key); + + return NULL; + } + } + else { + 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; + } + + if (type == RSPAMD_DKIM_KEY_RSA) { + key->key.key_rsa = EVP_PKEY_get1_RSA(key->key_evp); + + if (key->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; + } + } + else { + key->key.key_ecdsa = EVP_PKEY_get1_EC_KEY(key->key_evp); + + if (key->key.key_ecdsa == NULL) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_KEYFAIL, + "cannot extract ecdsa key from evp key"); + REF_RELEASE(key); + + return NULL; + } + } + } + + return key; +} + +const guchar * +rspamd_dkim_key_id(rspamd_dkim_key_t *key) +{ + if (key) { + return key->key_id; + } + + return NULL; +} + +/** + * Free DKIM key + * @param key + */ +void rspamd_dkim_key_free(rspamd_dkim_key_t *key) +{ + if (key->key_evp) { + EVP_PKEY_free(key->key_evp); + } + + if (key->type == RSPAMD_DKIM_KEY_RSA) { + if (key->key.key_rsa) { + RSA_free(key->key.key_rsa); + } + } + else if (key->type == RSPAMD_DKIM_KEY_ECDSA) { + if (key->key.key_ecdsa) { + EC_KEY_free(key->key.key_ecdsa); + } + } + /* Nothing in case of eddsa key */ + if (key->key_bio) { + BIO_free(key->key_bio); + } + + g_free(key->raw_key); + g_free(key->keydata); + g_free(key); +} + +void rspamd_dkim_sign_key_free(rspamd_dkim_sign_key_t *key) +{ + if (key->key_evp) { + EVP_PKEY_free(key->key_evp); + } + if (key->type == RSPAMD_DKIM_KEY_RSA) { + if (key->key.key_rsa) { + RSA_free(key->key.key_rsa); + } + } + if (key->key_bio) { + BIO_free(key->key_bio); + } + + if (key->type == RSPAMD_DKIM_KEY_EDDSA) { + rspamd_explicit_memzero(key->key.key_eddsa, key->keylen); + g_free(key->keydata); + } + + g_free(key); +} + +rspamd_dkim_key_t * +rspamd_dkim_parse_key(const gchar *txt, gsize *keylen, GError **err) +{ + const gchar *c, *p, *end, *key = NULL, *alg = "rsa"; + enum { + read_tag = 0, + read_tag_before_eqsign, + read_eqsign, + read_p_tag, + read_k_tag, + ignore_value, + skip_spaces, + } state = read_tag, + next_state; + gchar tag = '\0'; + gsize klen = 0, alglen = 0; + + c = txt; + p = txt; + end = txt + strlen(txt); + + while (p < end) { + switch (state) { + case read_tag: + if (*p == '=') { + state = read_eqsign; + } + else if (g_ascii_isspace(*p)) { + state = skip_spaces; + + if (tag != '\0') { + /* We had tag letter */ + next_state = read_tag_before_eqsign; + } + else { + /* We had no tag letter, so we ignore empty tag */ + next_state = read_tag; + } + } + else { + tag = *p; + } + p++; + break; + case read_tag_before_eqsign: + /* Input: spaces before eqsign + * Output: either read a next tag (previous had no value), or read value + * p is moved forward + */ + if (*p == '=') { + state = read_eqsign; + } + else { + tag = *p; + state = read_tag; + } + p++; + break; + case read_eqsign: + /* Always switch to skip spaces state and do not advance p */ + state = skip_spaces; + + if (tag == 'p') { + next_state = read_p_tag; + } + else if (tag == 'k') { + next_state = read_k_tag; + } + else { + /* Unknown tag, ignore */ + next_state = ignore_value; + tag = '\0'; + } + break; + case read_p_tag: + if (*p == ';') { + klen = p - c; + key = c; + state = read_tag; + tag = '\0'; + p++; + } + else { + p++; + } + break; + case read_k_tag: + if (*p == ';') { + alglen = p - c; + alg = c; + state = read_tag; + tag = '\0'; + p++; + } + else if (g_ascii_isspace(*p)) { + alglen = p - c; + alg = c; + state = skip_spaces; + next_state = read_tag; + tag = '\0'; + } + else { + p++; + } + break; + case ignore_value: + if (*p == ';') { + state = read_tag; + tag = '\0'; + p++; + } + else if (g_ascii_isspace(*p)) { + state = skip_spaces; + next_state = read_tag; + tag = '\0'; + } + else { + p++; + } + break; + case skip_spaces: + /* Skip spaces and switch to the next state if needed */ + if (g_ascii_isspace(*p)) { + p++; + } + else { + c = p; + state = next_state; + } + break; + default: + break; + } + } + + /* Leftover */ + switch (state) { + case read_p_tag: + klen = p - c; + key = c; + break; + case read_k_tag: + alglen = p - c; + alg = c; + break; + default: + break; + } + + if (klen == 0 || key == NULL) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_KEYFAIL, + "key is missing"); + + return NULL; + } + + if (alglen == 0 || alg == NULL) { + alg = "rsa"; /* Implicit */ + alglen = 3; + } + + if (keylen) { + *keylen = klen; + } + + if (alglen == 8 && rspamd_lc_cmp(alg, "ecdsa256", alglen) == 0) { + return rspamd_dkim_make_key(key, klen, + RSPAMD_DKIM_KEY_ECDSA, err); + } + else if (alglen == 7 && rspamd_lc_cmp(alg, "ed25519", alglen) == 0) { + return rspamd_dkim_make_key(key, klen, + RSPAMD_DKIM_KEY_EDDSA, err); + } + else { + /* We assume RSA default in all cases */ + return rspamd_dkim_make_key(key, klen, + RSPAMD_DKIM_KEY_RSA, err); + } + + g_assert_not_reached(); + + 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) { + gint err_code = DKIM_SIGERROR_NOKEY; + if (reply->code == RDNS_RC_NOREC) { + err_code = DKIM_SIGERROR_NOREC; + } + else if (reply->code == RDNS_RC_NXDOMAIN) { + err_code = DKIM_SIGERROR_NOREC; + } + g_set_error(&err, + DKIM_ERROR, + err_code, + "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) { + if (err != NULL) { + /* Free error as it is insignificant */ + g_error_free(err); + err = NULL; + } + key = rspamd_dkim_parse_key(elt->content.txt.data, + &keylen, + &err); + if (key) { + key->ttl = elt->ttl; + break; + } + } + } + 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 rspamd_dns_resolver_request_task_forced(task, + rspamd_dkim_dns_cb, + cbdata, + RDNS_REQUEST_TXT, + ctx->dns_key); +} + +static gboolean +rspamd_dkim_relaxed_body_step(struct rspamd_dkim_common_ctx *ctx, EVP_MD_CTX *ck, + const gchar **start, guint size, + gssize *remain) +{ + const gchar *h; + gchar *t; + guint len, inlen; + gssize octets_remain; + gboolean got_sp, ret = TRUE; + gchar buf[1024]; + + len = size; + inlen = sizeof(buf) - 1; + h = *start; + t = buf; + got_sp = FALSE; + octets_remain = *remain; + + while (len > 0 && inlen > 0 && (octets_remain > 0)) { + + 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; + octets_remain -= 2; + } + else { + h++; + len--; + if (octets_remain >= 2) { + octets_remain -= 2; /* Input has just \n or \r so we actually add more octets */ + } + else { + octets_remain--; + break; + } + } + break; + } + else if (g_ascii_isspace(*h)) { + if (got_sp) { + /* Ignore multiply spaces */ + h++; + len--; + continue; + } + else { + *t++ = ' '; + h++; + inlen--; + len--; + octets_remain--; + got_sp = TRUE; + continue; + } + } + else { + got_sp = FALSE; + } + + *t++ = *h++; + inlen--; + len--; + octets_remain--; + } + + if (octets_remain < 0) { + /* Absurdic l tag value, but we still need to rewind the t pointer back */ + while (t > buf && octets_remain < 0) { + t--; + octets_remain++; + } + + ret = FALSE; + } + + *start = h; + + if (t - buf > 0) { + gsize cklen = t - buf; + + EVP_DigestUpdate(ck, buf, cklen); + ctx->body_canonicalised += cklen; + msg_debug_dkim("relaxed update signature with body buffer " + "(%z size, %z -> %z remain)", + cklen, *remain, octets_remain); + *remain = octets_remain; + } + + return ret && ((len > 0) && (octets_remain > 0)); +} + +static gboolean +rspamd_dkim_simple_body_step(struct rspamd_dkim_common_ctx *ctx, + EVP_MD_CTX *ck, const gchar **start, guint size, + gssize *remain) +{ + const gchar *h; + gchar *t; + guint len, inlen; + gssize octets_remain; + gchar buf[1024]; + + len = size; + inlen = sizeof(buf) - 1; + h = *start; + t = &buf[0]; + octets_remain = *remain; + + while (len > 0 && inlen > 0 && (octets_remain != 0)) { + if (*h == '\r' || *h == '\n') { + *t++ = '\r'; + *t++ = '\n'; + + if (len > 1 && (*h == '\r' && h[1] == '\n')) { + h += 2; + len -= 2; + + if (octets_remain >= 2) { + octets_remain -= 2; /* Input has just \n or \r so we actually add more octets */ + } + else { + octets_remain--; + } + } + else { + h++; + len--; + + if (octets_remain >= 2) { + octets_remain -= 2; /* Input has just \n or \r so we actually add more octets */ + } + else { + octets_remain--; + } + } + break; + } + + *t++ = *h++; + octets_remain--; + inlen--; + len--; + } + + *start = h; + + if (t - buf > 0) { + gsize cklen = t - buf; + + EVP_DigestUpdate(ck, buf, cklen); + ctx->body_canonicalised += cklen; + msg_debug_dkim("simple update signature with body buffer " + "(%z size, %z -> %z remain)", + cklen, *remain, octets_remain); + *remain = octets_remain; + } + + return ((len != 0) && (octets_remain != 0)); +} + +static const gchar * +rspamd_dkim_skip_empty_lines(const gchar *start, const gchar *end, + guint type, gboolean sign, gboolean *need_crlf) +{ + const gchar *p = end - 1, *t; + enum { + init = 0, + init_2, + got_cr, + got_lf, + got_crlf, + test_spaces, + } state = init; + guint skip = 0; + + while (p >= start) { + switch (state) { + case init: + if (*p == '\r') { + state = got_cr; + } + else if (*p == '\n') { + state = got_lf; + } + else if (type == DKIM_CANON_RELAXED && *p == ' ') { + skip = 0; + state = test_spaces; + } + else { + if (sign || type != DKIM_CANON_RELAXED) { + *need_crlf = TRUE; + } + + goto end; + } + break; + case init_2: + if (*p == '\r') { + state = got_cr; + } + else if (*p == '\n') { + state = got_lf; + } + else if (type == DKIM_CANON_RELAXED && (*p == ' ' || *p == '\t')) { + skip = 0; + state = test_spaces; + } + else { + goto end; + } + break; + case got_cr: + if (p >= start + 1) { + if (*(p - 1) == '\r') { + p--; + state = got_cr; + } + else if (*(p - 1) == '\n') { + if ((*p - 2) == '\r') { + /* \r\n\r -> we know about one line */ + p -= 1; + state = got_crlf; + } + else { + /* \n\r -> we know about one line */ + p -= 1; + state = got_lf; + } + } + else if (type == DKIM_CANON_RELAXED && (*(p - 1) == ' ' || + *(p - 1) == '\t')) { + skip = 1; + state = test_spaces; + } + else { + goto end; + } + } + else { + if (g_ascii_isspace(*(p - 1))) { + if (type == DKIM_CANON_RELAXED) { + p -= 1; + } + } + goto end; + } + break; + case got_lf: + if (p >= start + 1) { + if (*(p - 1) == '\r') { + state = got_crlf; + } + else if (*(p - 1) == '\n') { + /* We know about one line */ + p--; + state = got_lf; + } + else if (type == DKIM_CANON_RELAXED && (*(p - 1) == ' ' || + *(p - 1) == '\t')) { + skip = 1; + state = test_spaces; + } + else { + goto end; + } + } + else { + if (g_ascii_isspace(*(p - 1))) { + if (type == DKIM_CANON_RELAXED) { + p -= 1; + } + } + goto end; + } + break; + case got_crlf: + if (p >= start + 2) { + if (*(p - 2) == '\r') { + p -= 2; + state = got_cr; + } + else if (*(p - 2) == '\n') { + p -= 2; + state = got_lf; + } + else if (type == DKIM_CANON_RELAXED && (*(p - 2) == ' ' || + *(p - 2) == '\t')) { + skip = 2; + state = test_spaces; + } + else { + goto end; + } + } + else { + if (g_ascii_isspace(*(p - 2))) { + if (type == DKIM_CANON_RELAXED) { + p -= 2; + } + } + goto end; + } + break; + case test_spaces: + t = p - skip; + + while (t >= start + 2 && (*t == ' ' || *t == '\t')) { + t--; + } + + if (*t == '\r') { + p = t; + state = got_cr; + } + else if (*t == '\n') { + p = t; + state = got_lf; + } + else { + goto end; + } + break; + } + } + +end: + return p; +} + +static gboolean +rspamd_dkim_canonize_body(struct rspamd_dkim_common_ctx *ctx, + const gchar *start, + const gchar *end, + gboolean sign) +{ + const gchar *p; + gssize remain = ctx->len ? ctx->len : G_MAXSSIZE; + guint total_len = end - start; + gboolean need_crlf = FALSE; + + if (start == NULL) { + /* Empty body */ + if (ctx->body_canon_type == DKIM_CANON_SIMPLE) { + EVP_DigestUpdate(ctx->body_hash, CRLF, sizeof(CRLF) - 1); + ctx->body_canonicalised += sizeof(CRLF) - 1; + } + else { + EVP_DigestUpdate(ctx->body_hash, "", 0); + } + } + else { + /* Strip extra ending CRLF */ + p = rspamd_dkim_skip_empty_lines(start, end, ctx->body_canon_type, + sign, &need_crlf); + end = p + 1; + + if (end == start) { + /* Empty body */ + if (ctx->body_canon_type == DKIM_CANON_SIMPLE) { + EVP_DigestUpdate(ctx->body_hash, CRLF, sizeof(CRLF) - 1); + ctx->body_canonicalised += sizeof(CRLF) - 1; + } + else { + EVP_DigestUpdate(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)) + ; + + /* + * If we have l= tag then we cannot add crlf... + */ + if (need_crlf) { + /* l is evil... */ + if (ctx->len == 0) { + remain = 2; + } + else { + if (ctx->len <= total_len) { + /* We don't have enough l to add \r\n */ + remain = 0; + } + else { + if (ctx->len - total_len >= 2) { + remain = 2; + } + else { + remain = ctx->len - total_len; + } + } + } + + start = "\r\n"; + end = start + 2; + + 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)) + ; + if (need_crlf) { + start = "\r\n"; + end = start + 2; + remain = 2; + 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(EVP_MD_CTX *ck, const gchar *begin, gsize len) +{ + const gchar *p, *c, *end; + + end = begin + len; + p = begin; + c = p; + + while (p < end) { + if (*p == '\r') { + EVP_DigestUpdate(ck, c, p - c); + EVP_DigestUpdate(ck, CRLF, sizeof(CRLF) - 1); + p++; + + if (p < end && *p == '\n') { + p++; + } + c = p; + } + else if (*p == '\n') { + EVP_DigestUpdate(ck, c, p - c); + EVP_DigestUpdate(ck, CRLF, sizeof(CRLF) - 1); + p++; + c = p; + } + else { + p++; + } + } + + if (p > c) { + EVP_DigestUpdate(ck, c, p - c); + } +} + +/* Update hash by signature value (ignoring b= tag) */ +static void +rspamd_dkim_signature_update(struct rspamd_dkim_common_ctx *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", + (gint) (p - c + 2), + c); + ctx->headers_canonicalised += p - c + 2; + 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", + (gint) (p - c + 1), c); + ctx->headers_canonicalised += p - c + 1; + rspamd_dkim_hash_update(ctx->headers_hash, c, p - c + 1); + } +} + +goffset +rspamd_dkim_canonize_header_relaxed_str(const gchar *hname, + const gchar *hvalue, + gchar *out, + gsize outlen) +{ + gchar *t; + const guchar *h; + gboolean got_sp; + + /* Name part */ + t = out; + h = hname; + + while (*h && t - out < outlen) { + *t++ = lc_map[*h++]; + } + + if (t - out >= outlen) { + return -1; + } + + *t++ = ':'; + + /* Value part */ + h = hvalue; + /* Skip spaces at the beginning */ + while (g_ascii_isspace(*h)) { + h++; + } + + got_sp = FALSE; + + while (*h && (t - out < outlen)) { + 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--; + } + + if (t - out >= outlen - 2) { + return -1; + } + + *t++ = '\r'; + *t++ = '\n'; + *t = '\0'; + + return t - out; +} + +static gboolean +rspamd_dkim_canonize_header_relaxed(struct rspamd_dkim_common_ctx *ctx, + const gchar *header, + const gchar *header_name, + gboolean is_sign, + guint count, + bool is_seal) +{ + static gchar st_buf[8192]; + gchar *buf; + guint inlen; + goffset r; + gboolean allocated = FALSE; + + inlen = strlen(header) + strlen(header_name) + 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(header_name, header, buf, inlen); + + g_assert(r != -1); + + if (!is_sign) { + msg_debug_dkim("update %s with header (idx=%d): %s", + is_seal ? "seal" : "signature", count, buf); + EVP_DigestUpdate(ctx->headers_hash, buf, r); + } + else { + rspamd_dkim_signature_update(ctx, buf, r); + } + + if (allocated) { + g_free(buf); + } + + return TRUE; +} + + +static gboolean +rspamd_dkim_canonize_header(struct rspamd_dkim_common_ctx *ctx, + struct rspamd_task *task, + const gchar *header_name, + gint count, + const gchar *dkim_header, + const gchar *dkim_domain) +{ + struct rspamd_mime_header *rh, *cur, *sel = NULL; + gint hdr_cnt = 0; + bool use_idx = false, is_sign = ctx->is_sign; + + /* + * TODO: + * Temporary hack to prevent linked list being misused until refactored + */ + const guint max_list_iters = 1000; + + if (count < 0) { + use_idx = true; + count = -(count); /* use i= in header content as it is arc stuff */ + } + + if (dkim_header == NULL) { + rh = rspamd_message_get_header_array(task, header_name, + is_sign); + + if (rh) { + /* Check uniqueness of the header but we count from the bottom to top */ + if (!use_idx) { + for (cur = rh->prev;; cur = cur->prev) { + if (hdr_cnt == count) { + sel = cur; + } + + hdr_cnt++; + + if (cur == rh || hdr_cnt >= max_list_iters) { + /* Cycle */ + break; + } + } + + if ((rh->flags & RSPAMD_HEADER_UNIQUE) && hdr_cnt > 1) { + guint64 random_cookie = ottery_rand_uint64(); + + msg_warn_dkim("header %s is intended to be unique by" + " email standards, but we have %d headers of this" + " type, artificially break DKIM check", + header_name, + hdr_cnt); + rspamd_dkim_hash_update(ctx->headers_hash, + (const gchar *) &random_cookie, + sizeof(random_cookie)); + ctx->headers_canonicalised += sizeof(random_cookie); + + return FALSE; + } + + if (hdr_cnt <= count) { + /* + * If DKIM has less headers requested than there are in a + * message, then it's fine, it allows adding extra headers + */ + return TRUE; + } + } + else { + /* + * This branch is used for ARC headers, and it orders them based on + * i=<number> string and not their real order in the list of headers + */ + gchar idx_buf[16]; + gint id_len, i; + + id_len = rspamd_snprintf(idx_buf, sizeof(idx_buf), "i=%d;", + count); + + for (cur = rh->prev, i = 0; i < max_list_iters; cur = cur->prev, i++) { + if (cur->decoded && + rspamd_substring_search(cur->decoded, strlen(cur->decoded), + idx_buf, id_len) != -1) { + sel = cur; + break; + } + + if (cur == rh) { + /* Cycle */ + break; + } + } + + if (sel == NULL) { + return FALSE; + } + } + + /* Selected header must be non-null if previous condition is false */ + g_assert(sel != NULL); + + if (ctx->header_canon_type == DKIM_CANON_SIMPLE) { + rspamd_dkim_hash_update(ctx->headers_hash, sel->raw_value, + sel->raw_len); + ctx->headers_canonicalised += sel->raw_len; + msg_debug_dkim("update %s with header (idx=%d): %*s", + (use_idx ? "seal" : "signature"), + count, (gint) sel->raw_len, sel->raw_value); + } + else { + if (is_sign && (sel->flags & RSPAMD_HEADER_FROM)) { + /* Special handling of the From handling when rewrite is done */ + gboolean has_rewrite = FALSE; + guint i; + struct rspamd_email_address *addr; + + PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, from_mime), i, addr) + { + if ((addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL) && !(addr->flags & RSPAMD_EMAIL_ADDR_ALIASED)) { + has_rewrite = TRUE; + } + } + + if (has_rewrite) { + PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, from_mime), i, addr) + { + if (!(addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL)) { + if (!rspamd_dkim_canonize_header_relaxed(ctx, addr->raw, + header_name, FALSE, i, use_idx)) { + return FALSE; + } + + return TRUE; + } + } + } + } + + if (!rspamd_dkim_canonize_header_relaxed(ctx, sel->value, + header_name, FALSE, count, use_idx)) { + return FALSE; + } + } + } + } + else { + /* For signature check just use the saved dkim header */ + if (ctx->header_canon_type == DKIM_CANON_SIMPLE) { + /* We need to find our own signature and use it */ + rh = rspamd_message_get_header_array(task, header_name, is_sign); + + if (rh) { + /* We need to find our own signature */ + if (!dkim_domain) { + msg_err_dkim("cannot verify dkim as we have no dkim domain!"); + return FALSE; + } + + gboolean found = FALSE; + + DL_FOREACH(rh, cur) + { + guint64 th = rspamd_cryptobox_fast_hash(cur->decoded, + strlen(cur->decoded), rspamd_hash_seed()); + + if (th == ctx->sig_hash) { + rspamd_dkim_signature_update(ctx, cur->raw_value, + cur->raw_len); + found = TRUE; + break; + } + } + if (!found) { + msg_err_dkim("BUGON: cannot verify dkim as we have lost our signature" + " during simple canonicalisation, expected hash=%L", + ctx->sig_hash); + return FALSE; + } + } + else { + return FALSE; + } + } + else { + if (!rspamd_dkim_canonize_header_relaxed(ctx, + dkim_header, + header_name, + TRUE, 0, use_idx)) { + return FALSE; + } + } + } + + return TRUE; +} + +struct rspamd_dkim_cached_hash { + guchar *digest_normal; + guchar *digest_cr; + guchar *digest_crlf; + gchar *type; +}; + +static struct rspamd_dkim_cached_hash * +rspamd_dkim_check_bh_cached(struct rspamd_dkim_common_ctx *ctx, + struct rspamd_task *task, gsize bhlen, gboolean is_sign) +{ + gchar typebuf[64]; + struct rspamd_dkim_cached_hash *res; + + rspamd_snprintf(typebuf, sizeof(typebuf), + RSPAMD_MEMPOOL_DKIM_BH_CACHE "%z_%s_%d_%z", + bhlen, + ctx->body_canon_type == DKIM_CANON_RELAXED ? "1" : "0", + !!is_sign, + ctx->len); + + res = rspamd_mempool_get_variable(task->task_pool, + typebuf); + + if (!res) { + res = rspamd_mempool_alloc0(task->task_pool, sizeof(*res)); + res->type = rspamd_mempool_strdup(task->task_pool, typebuf); + rspamd_mempool_set_variable(task->task_pool, + res->type, res, NULL); + } + + return res; +} + +static const char * +rspamd_dkim_type_to_string(enum rspamd_dkim_type t) +{ + switch (t) { + case RSPAMD_DKIM_NORMAL: + return "dkim"; + case RSPAMD_DKIM_ARC_SIG: + return "arc_sig"; + case RSPAMD_DKIM_ARC_SEAL: + default: + return "arc_seal"; + } +} + +/** + * 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 + */ +struct rspamd_dkim_check_result * +rspamd_dkim_check(rspamd_dkim_context_t *ctx, + rspamd_dkim_key_t *key, + struct rspamd_task *task) +{ + const gchar *body_end, *body_start; + guchar raw_digest[EVP_MAX_MD_SIZE]; + struct rspamd_dkim_cached_hash *cached_bh = NULL; + EVP_MD_CTX *cpy_ctx = NULL; + gsize dlen = 0; + struct rspamd_dkim_check_result *res; + guint i; + struct rspamd_dkim_header *dh; + gint nid; + + g_return_val_if_fail(ctx != NULL, NULL); + g_return_val_if_fail(key != NULL, NULL); + g_return_val_if_fail(task->msg.len > 0, NULL); + + /* First of all find place of body */ + body_end = task->msg.begin + task->msg.len; + + body_start = MESSAGE_FIELD(task, raw_headers_content).body_start; + + res = rspamd_mempool_alloc0(task->task_pool, sizeof(*res)); + res->ctx = ctx; + res->selector = ctx->selector; + res->domain = ctx->domain; + res->fail_reason = NULL; + res->short_b = ctx->short_b; + res->rcode = DKIM_CONTINUE; + + if (!body_start) { + res->rcode = DKIM_ERROR; + return res; + } + + if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) { + dlen = EVP_MD_CTX_size(ctx->common.body_hash); + cached_bh = rspamd_dkim_check_bh_cached(&ctx->common, task, + dlen, FALSE); + + if (!cached_bh->digest_normal) { + /* Start canonization of body part */ + if (!rspamd_dkim_canonize_body(&ctx->common, body_start, body_end, + FALSE)) { + res->rcode = DKIM_RECORD_ERROR; + return res; + } + } + } + + /* Now canonize headers */ + for (i = 0; i < ctx->common.hlist->len; i++) { + dh = g_ptr_array_index(ctx->common.hlist, i); + rspamd_dkim_canonize_header(&ctx->common, task, dh->name, dh->count, + NULL, NULL); + } + + /* Canonize dkim signature */ + switch (ctx->common.type) { + case RSPAMD_DKIM_NORMAL: + rspamd_dkim_canonize_header(&ctx->common, task, RSPAMD_DKIM_SIGNHEADER, 0, + ctx->dkim_header, ctx->domain); + break; + case RSPAMD_DKIM_ARC_SIG: + rspamd_dkim_canonize_header(&ctx->common, task, RSPAMD_DKIM_ARC_SIGNHEADER, 0, + ctx->dkim_header, ctx->domain); + break; + case RSPAMD_DKIM_ARC_SEAL: + rspamd_dkim_canonize_header(&ctx->common, task, RSPAMD_DKIM_ARC_SEALHEADER, 0, + ctx->dkim_header, ctx->domain); + break; + } + + + /* Use cached BH for all but arc seal, if it is not NULL we are not in arc seal mode */ + if (cached_bh != NULL) { + if (!cached_bh->digest_normal) { + /* Copy md_ctx to deal with broken CRLF at the end */ + cpy_ctx = EVP_MD_CTX_create(); + EVP_MD_CTX_copy(cpy_ctx, ctx->common.body_hash); + EVP_DigestFinal_ex(cpy_ctx, raw_digest, NULL); + + cached_bh->digest_normal = rspamd_mempool_alloc(task->task_pool, + sizeof(raw_digest)); + memcpy(cached_bh->digest_normal, raw_digest, sizeof(raw_digest)); + } + + /* Check bh field */ + if (memcmp(ctx->bh, cached_bh->digest_normal, ctx->bhlen) != 0) { + msg_debug_dkim( + "bh value mismatch: %*xs versus %*xs, try add LF; try adding CRLF", + (gint) dlen, ctx->bh, + (gint) dlen, raw_digest); + + if (cpy_ctx) { + /* Try add CRLF */ +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + EVP_MD_CTX_cleanup(cpy_ctx); +#else + EVP_MD_CTX_reset(cpy_ctx); +#endif + EVP_MD_CTX_copy(cpy_ctx, ctx->common.body_hash); + EVP_DigestUpdate(cpy_ctx, "\r\n", 2); + EVP_DigestFinal_ex(cpy_ctx, raw_digest, NULL); + cached_bh->digest_crlf = rspamd_mempool_alloc(task->task_pool, + sizeof(raw_digest)); + memcpy(cached_bh->digest_crlf, raw_digest, sizeof(raw_digest)); + + if (memcmp(ctx->bh, raw_digest, ctx->bhlen) != 0) { + msg_debug_dkim( + "bh value mismatch after added CRLF: %*xs versus %*xs, try add LF", + (gint) dlen, ctx->bh, + (gint) dlen, raw_digest); + + /* Try add LF */ +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + EVP_MD_CTX_cleanup(cpy_ctx); +#else + EVP_MD_CTX_reset(cpy_ctx); +#endif + EVP_MD_CTX_copy(cpy_ctx, ctx->common.body_hash); + EVP_DigestUpdate(cpy_ctx, "\n", 1); + EVP_DigestFinal_ex(cpy_ctx, raw_digest, NULL); + cached_bh->digest_cr = rspamd_mempool_alloc(task->task_pool, + sizeof(raw_digest)); + memcpy(cached_bh->digest_cr, raw_digest, sizeof(raw_digest)); + + if (memcmp(ctx->bh, raw_digest, ctx->bhlen) != 0) { + msg_debug_dkim("bh value mismatch after added LF: %*xs versus %*xs", + (gint) dlen, ctx->bh, + (gint) dlen, raw_digest); + res->fail_reason = "body hash did not verify"; + res->rcode = DKIM_REJECT; + } + } + } + else if (cached_bh->digest_crlf) { + if (memcmp(ctx->bh, cached_bh->digest_crlf, ctx->bhlen) != 0) { + msg_debug_dkim("bh value mismatch after added CRLF: %*xs versus %*xs", + (gint) dlen, ctx->bh, + (gint) dlen, cached_bh->digest_crlf); + + if (cached_bh->digest_cr) { + if (memcmp(ctx->bh, cached_bh->digest_cr, ctx->bhlen) != 0) { + msg_debug_dkim( + "bh value mismatch after added LF: %*xs versus %*xs", + (gint) dlen, ctx->bh, + (gint) dlen, cached_bh->digest_cr); + + res->fail_reason = "body hash did not verify"; + res->rcode = DKIM_REJECT; + } + } + else { + + res->fail_reason = "body hash did not verify"; + res->rcode = DKIM_REJECT; + } + } + } + else { + msg_debug_dkim( + "bh value mismatch: %*xs versus %*xs", + (gint) dlen, ctx->bh, + (gint) dlen, cached_bh->digest_normal); + res->fail_reason = "body hash did not verify"; + res->rcode = DKIM_REJECT; + } + } + + if (cpy_ctx) { +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + EVP_MD_CTX_cleanup(cpy_ctx); +#else + EVP_MD_CTX_reset(cpy_ctx); +#endif + EVP_MD_CTX_destroy(cpy_ctx); + } + + if (res->rcode == DKIM_REJECT) { + msg_info_dkim( + "%s: bh value mismatch: got %*Bs, expected %*Bs; " + "body length %d->%d; d=%s; s=%s", + rspamd_dkim_type_to_string(ctx->common.type), + (gint) dlen, cached_bh->digest_normal, + (gint) dlen, ctx->bh, + (gint) (body_end - body_start), ctx->common.body_canonicalised, + ctx->domain, ctx->selector); + + return res; + } + } + + dlen = EVP_MD_CTX_size(ctx->common.headers_hash); + EVP_DigestFinal_ex(ctx->common.headers_hash, raw_digest, NULL); + /* Check headers signature */ + + if (ctx->sig_alg == DKIM_SIGN_RSASHA1) { + nid = NID_sha1; + } + else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 || + ctx->sig_alg == DKIM_SIGN_ECDSASHA256 || + ctx->sig_alg == DKIM_SIGN_EDDSASHA256) { + nid = NID_sha256; + } + else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 || + ctx->sig_alg == DKIM_SIGN_ECDSASHA512) { + nid = NID_sha512; + } + else { + /* Not reached */ + nid = NID_sha1; + } + + switch (key->type) { + case RSPAMD_DKIM_KEY_RSA: + if (RSA_verify(nid, raw_digest, dlen, ctx->b, ctx->blen, + key->key.key_rsa) != 1) { + msg_debug_dkim("headers rsa verify failed"); + ERR_clear_error(); + res->rcode = DKIM_REJECT; + res->fail_reason = "headers rsa verify failed"; + + msg_info_dkim( + "%s: headers RSA verification failure; " + "body length %d->%d; headers length %d; d=%s; s=%s; key_md5=%*xs; orig header: %s", + rspamd_dkim_type_to_string(ctx->common.type), + (gint) (body_end - body_start), ctx->common.body_canonicalised, + ctx->common.headers_canonicalised, + ctx->domain, ctx->selector, + RSPAMD_DKIM_KEY_ID_LEN, rspamd_dkim_key_id(key), + ctx->dkim_header); + } + break; + case RSPAMD_DKIM_KEY_ECDSA: + if (ECDSA_verify(nid, raw_digest, dlen, ctx->b, ctx->blen, + key->key.key_ecdsa) != 1) { + msg_info_dkim( + "%s: headers ECDSA verification failure; " + "body length %d->%d; headers length %d; d=%s; s=%s; key_md5=%*xs; orig header: %s", + rspamd_dkim_type_to_string(ctx->common.type), + (gint) (body_end - body_start), ctx->common.body_canonicalised, + ctx->common.headers_canonicalised, + ctx->domain, ctx->selector, + RSPAMD_DKIM_KEY_ID_LEN, rspamd_dkim_key_id(key), + ctx->dkim_header); + msg_debug_dkim("headers ecdsa verify failed"); + ERR_clear_error(); + res->rcode = DKIM_REJECT; + res->fail_reason = "headers ecdsa verify failed"; + } + break; + case RSPAMD_DKIM_KEY_EDDSA: + if (!rspamd_cryptobox_verify(ctx->b, ctx->blen, raw_digest, dlen, + key->key.key_eddsa, RSPAMD_CRYPTOBOX_MODE_25519)) { + msg_info_dkim( + "%s: headers EDDSA verification failure; " + "body length %d->%d; headers length %d; d=%s; s=%s; key_md5=%*xs; orig header: %s", + rspamd_dkim_type_to_string(ctx->common.type), + (gint) (body_end - body_start), ctx->common.body_canonicalised, + ctx->common.headers_canonicalised, + ctx->domain, ctx->selector, + RSPAMD_DKIM_KEY_ID_LEN, rspamd_dkim_key_id(key), + ctx->dkim_header); + msg_debug_dkim("headers eddsa verify failed"); + res->rcode = DKIM_REJECT; + res->fail_reason = "headers eddsa verify failed"; + } + break; + } + + + if (ctx->common.type == RSPAMD_DKIM_ARC_SEAL && res->rcode == DKIM_CONTINUE) { + switch (ctx->cv) { + case RSPAMD_ARC_INVALID: + msg_info_dkim("arc seal is invalid i=%d", ctx->common.idx); + res->rcode = DKIM_PERM_ERROR; + res->fail_reason = "arc seal is invalid"; + break; + case RSPAMD_ARC_FAIL: + msg_info_dkim("arc seal failed i=%d", ctx->common.idx); + res->rcode = DKIM_REJECT; + res->fail_reason = "arc seal failed"; + break; + default: + break; + } + } + + return res; +} + +struct rspamd_dkim_check_result * +rspamd_dkim_create_result(rspamd_dkim_context_t *ctx, + enum rspamd_dkim_check_rcode rcode, + struct rspamd_task *task) +{ + struct rspamd_dkim_check_result *res; + + res = rspamd_mempool_alloc0(task->task_pool, sizeof(*res)); + res->ctx = ctx; + res->selector = ctx->selector; + res->domain = ctx->domain; + res->fail_reason = NULL; + res->short_b = ctx->short_b; + res->rcode = rcode; + + return res; +} + +rspamd_dkim_key_t * +rspamd_dkim_key_ref(rspamd_dkim_key_t *k) +{ + REF_RETAIN(k); + + return k; +} + +void rspamd_dkim_key_unref(rspamd_dkim_key_t *k) +{ + REF_RELEASE(k); +} + +rspamd_dkim_sign_key_t * +rspamd_dkim_sign_key_ref(rspamd_dkim_sign_key_t *k) +{ + REF_RETAIN(k); + + return k; +} + +void rspamd_dkim_sign_key_unref(rspamd_dkim_sign_key_t *k) +{ + REF_RELEASE(k); +} + +const gchar * +rspamd_dkim_get_domain(rspamd_dkim_context_t *ctx) +{ + if (ctx) { + return ctx->domain; + } + + return NULL; +} + +const gchar * +rspamd_dkim_get_selector(rspamd_dkim_context_t *ctx) +{ + if (ctx) { + return ctx->selector; + } + + return NULL; +} + +guint rspamd_dkim_key_get_ttl(rspamd_dkim_key_t *k) +{ + if (k) { + return k->ttl; + } + + return 0; +} + +const gchar * +rspamd_dkim_get_dns_key(rspamd_dkim_context_t *ctx) +{ + if (ctx) { + return ctx->dns_key; + } + + return NULL; +} + +#define PEM_SIG "-----BEGIN" + +rspamd_dkim_sign_key_t * +rspamd_dkim_sign_key_load(const gchar *key, gsize len, + enum rspamd_dkim_key_format type, + GError **err) +{ + guchar *map = NULL, *tmp = NULL; + gsize maplen; + rspamd_dkim_sign_key_t *nkey; + time_t mtime = time(NULL); + + if (type < 0 || type > RSPAMD_DKIM_KEY_UNKNOWN || len == 0 || key == NULL) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "invalid key type to load: %d", type); + return NULL; + } + + nkey = g_malloc0(sizeof(*nkey)); + nkey->mtime = mtime; + + msg_debug_dkim_taskless("got public key with length %z and type %d", + len, type); + + /* Load key file if needed */ + if (type == RSPAMD_DKIM_KEY_FILE) { + struct stat st; + + if (stat(key, &st) != 0) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "cannot stat key file: '%s' %s", key, strerror(errno)); + g_free(nkey); + + return NULL; + } + + nkey->mtime = st.st_mtime; + map = rspamd_file_xmap(key, PROT_READ, &maplen, TRUE); + + if (map == NULL) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "cannot map key file: '%s' %s", key, strerror(errno)); + g_free(nkey); + + return NULL; + } + + key = map; + len = maplen; + + if (maplen > sizeof(PEM_SIG) && + strncmp(map, PEM_SIG, sizeof(PEM_SIG) - 1) == 0) { + type = RSPAMD_DKIM_KEY_PEM; + } + else if (rspamd_cryptobox_base64_is_valid(map, maplen)) { + type = RSPAMD_DKIM_KEY_BASE64; + } + else { + type = RSPAMD_DKIM_KEY_RAW; + } + } + + if (type == RSPAMD_DKIM_KEY_UNKNOWN) { + if (len > sizeof(PEM_SIG) && + memcmp(key, PEM_SIG, sizeof(PEM_SIG) - 1) == 0) { + type = RSPAMD_DKIM_KEY_PEM; + } + else { + type = RSPAMD_DKIM_KEY_RAW; + } + } + + if (type == RSPAMD_DKIM_KEY_BASE64) { + type = RSPAMD_DKIM_KEY_RAW; + tmp = g_malloc(len); + rspamd_cryptobox_base64_decode(key, len, tmp, &len); + key = tmp; + } + + if (type == RSPAMD_DKIM_KEY_RAW && (len == 32 || + len == rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519))) { + if (len == 32) { + /* Seeded key, need scalarmult */ + unsigned char pk[32]; + nkey->type = RSPAMD_DKIM_KEY_EDDSA; + nkey->key.key_eddsa = g_malloc( + rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519)); + crypto_sign_ed25519_seed_keypair(pk, nkey->key.key_eddsa, key); + nkey->keylen = rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519); + } + else { + /* Full ed25519 key */ + unsigned klen = rspamd_cryptobox_sk_sig_bytes(RSPAMD_CRYPTOBOX_MODE_25519); + nkey->type = RSPAMD_DKIM_KEY_EDDSA; + nkey->key.key_eddsa = g_malloc(klen); + memcpy(nkey->key.key_eddsa, key, klen); + nkey->keylen = klen; + } + } + else { + nkey->key_bio = BIO_new_mem_buf(key, len); + + if (type == RSPAMD_DKIM_KEY_RAW) { + if (d2i_PrivateKey_bio(nkey->key_bio, &nkey->key_evp) == NULL) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "cannot parse raw private key: %s", + ERR_error_string(ERR_get_error(), NULL)); + + rspamd_dkim_sign_key_free(nkey); + nkey = NULL; + + goto end; + } + } + else { + if (!PEM_read_bio_PrivateKey(nkey->key_bio, &nkey->key_evp, NULL, NULL)) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "cannot parse pem private key: %s", + ERR_error_string(ERR_get_error(), NULL)); + rspamd_dkim_sign_key_free(nkey); + nkey = NULL; + + goto end; + } + } + nkey->key.key_rsa = EVP_PKEY_get1_RSA(nkey->key_evp); + if (nkey->key.key_rsa == NULL) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_KEYFAIL, + "cannot extract rsa key from evp key"); + rspamd_dkim_sign_key_free(nkey); + nkey = NULL; + + goto end; + } + nkey->type = RSPAMD_DKIM_KEY_RSA; + } + + REF_INIT_RETAIN(nkey, rspamd_dkim_sign_key_free); + +end: + + if (map != NULL) { + munmap(map, maplen); + } + + if (tmp != NULL) { + rspamd_explicit_memzero(tmp, len); + g_free(tmp); + } + + return nkey; +} + +#undef PEM_SIG + +gboolean +rspamd_dkim_sign_key_maybe_invalidate(rspamd_dkim_sign_key_t *key, time_t mtime) +{ + if (mtime > key->mtime) { + return TRUE; + } + return FALSE; +} + +rspamd_dkim_sign_context_t * +rspamd_create_dkim_sign_context(struct rspamd_task *task, + rspamd_dkim_sign_key_t *priv_key, + gint headers_canon, + gint body_canon, + const gchar *headers, + enum rspamd_dkim_type type, + GError **err) +{ + rspamd_dkim_sign_context_t *nctx; + + if (headers_canon != DKIM_CANON_SIMPLE && headers_canon != DKIM_CANON_RELAXED) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_INVALID_HC, + "bad headers canonicalisation"); + + return NULL; + } + if (body_canon != DKIM_CANON_SIMPLE && body_canon != DKIM_CANON_RELAXED) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_INVALID_BC, + "bad body canonicalisation"); + + return NULL; + } + + if (!priv_key || (!priv_key->key.key_rsa && !priv_key->key.key_eddsa)) { + g_set_error(err, + DKIM_ERROR, + DKIM_SIGERROR_KEYFAIL, + "bad key to sign"); + + return NULL; + } + + nctx = rspamd_mempool_alloc0(task->task_pool, sizeof(*nctx)); + nctx->common.pool = task->task_pool; + nctx->common.header_canon_type = headers_canon; + nctx->common.body_canon_type = body_canon; + nctx->common.type = type; + nctx->common.is_sign = TRUE; + + if (type != RSPAMD_DKIM_ARC_SEAL) { + if (!rspamd_dkim_parse_hdrlist_common(&nctx->common, headers, + strlen(headers), TRUE, + err)) { + return NULL; + } + } + else { + rspamd_dkim_add_arc_seal_headers(task->task_pool, &nctx->common); + } + + nctx->key = rspamd_dkim_sign_key_ref(priv_key); + + rspamd_mempool_add_destructor(task->task_pool, + (rspamd_mempool_destruct_t) rspamd_dkim_sign_key_unref, priv_key); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + nctx->common.body_hash = EVP_MD_CTX_create(); + EVP_DigestInit_ex(nctx->common.body_hash, EVP_sha256(), NULL); + nctx->common.headers_hash = EVP_MD_CTX_create(); + EVP_DigestInit_ex(nctx->common.headers_hash, EVP_sha256(), NULL); + rspamd_mempool_add_destructor(task->task_pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, nctx->common.body_hash); + rspamd_mempool_add_destructor(task->task_pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_destroy, nctx->common.headers_hash); +#else + nctx->common.body_hash = EVP_MD_CTX_new(); + EVP_DigestInit_ex(nctx->common.body_hash, EVP_sha256(), NULL); + nctx->common.headers_hash = EVP_MD_CTX_new(); + EVP_DigestInit_ex(nctx->common.headers_hash, EVP_sha256(), NULL); + rspamd_mempool_add_destructor(task->task_pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_free, nctx->common.body_hash); + rspamd_mempool_add_destructor(task->task_pool, + (rspamd_mempool_destruct_t) EVP_MD_CTX_free, nctx->common.headers_hash); +#endif + + return nctx; +} + + +GString * +rspamd_dkim_sign(struct rspamd_task *task, const gchar *selector, + const gchar *domain, time_t expire, gsize len, guint idx, + const gchar *arc_cv, rspamd_dkim_sign_context_t *ctx) +{ + GString *hdr; + struct rspamd_dkim_header *dh; + const gchar *body_end, *body_start, *hname; + guchar raw_digest[EVP_MAX_MD_SIZE]; + struct rspamd_dkim_cached_hash *cached_bh = NULL; + gsize dlen = 0; + guint i, j; + gchar *b64_data; + guchar *sig_buf; + guint sig_len; + guint headers_len = 0, cur_len = 0; + union rspamd_dkim_header_stat hstat; + + g_assert(ctx != NULL); + + /* First of all find place of body */ + body_end = task->msg.begin + task->msg.len; + body_start = MESSAGE_FIELD(task, raw_headers_content).body_start; + + if (len > 0) { + ctx->common.len = len; + } + + if (!body_start) { + return NULL; + } + + /* Start canonization of body part */ + if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) { + dlen = EVP_MD_CTX_size(ctx->common.body_hash); + cached_bh = rspamd_dkim_check_bh_cached(&ctx->common, task, + dlen, TRUE); + + if (!cached_bh->digest_normal) { + /* Start canonization of body part */ + if (!rspamd_dkim_canonize_body(&ctx->common, body_start, body_end, + TRUE)) { + return NULL; + } + } + } + + hdr = g_string_sized_new(255); + + if (ctx->common.type == RSPAMD_DKIM_NORMAL) { + rspamd_printf_gstring(hdr, "v=1; a=%s; c=%s/%s; d=%s; s=%s; ", + ctx->key->type == RSPAMD_DKIM_KEY_RSA ? "rsa-sha256" : "ed25519-sha256", + ctx->common.header_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple", + ctx->common.body_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple", + domain, selector); + } + else if (ctx->common.type == RSPAMD_DKIM_ARC_SIG) { + rspamd_printf_gstring(hdr, "i=%d; a=%s; c=%s/%s; d=%s; s=%s; ", + idx, + ctx->key->type == RSPAMD_DKIM_KEY_RSA ? "rsa-sha256" : "ed25519-sha256", + ctx->common.header_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple", + ctx->common.body_canon_type == DKIM_CANON_RELAXED ? "relaxed" : "simple", + domain, selector); + } + else { + g_assert(arc_cv != NULL); + rspamd_printf_gstring(hdr, "i=%d; a=%s; d=%s; s=%s; cv=%s; ", + idx, + ctx->key->type == RSPAMD_DKIM_KEY_RSA ? "rsa-sha256" : "ed25519-sha256", + domain, + selector, + arc_cv); + } + + if (expire > 0) { + rspamd_printf_gstring(hdr, "x=%t; ", expire); + } + + if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) { + if (len > 0) { + rspamd_printf_gstring(hdr, "l=%z; ", len); + } + } + + rspamd_printf_gstring(hdr, "t=%t; h=", time(NULL)); + + /* Now canonize headers */ + for (i = 0; i < ctx->common.hlist->len; i++) { + struct rspamd_mime_header *rh, *cur; + + dh = g_ptr_array_index(ctx->common.hlist, i); + + /* We allow oversigning if dh->count > number of headers with this name */ + hstat.n = GPOINTER_TO_UINT(g_hash_table_lookup(ctx->common.htable, dh->name)); + + if (hstat.s.flags & RSPAMD_DKIM_FLAG_OVERSIGN) { + /* Do oversigning */ + guint count = 0; + + rh = rspamd_message_get_header_array(task, dh->name, FALSE); + + if (rh) { + DL_FOREACH(rh, cur) + { + /* Sign all existing headers */ + rspamd_dkim_canonize_header(&ctx->common, task, dh->name, + count, + NULL, NULL); + count++; + } + } + + /* Now add one more entry to oversign */ + if (count > 0 || !(hstat.s.flags & RSPAMD_DKIM_FLAG_OVERSIGN_EXISTING)) { + cur_len = (strlen(dh->name) + 1) * (count + 1); + headers_len += cur_len; + + if (headers_len > 70 && i > 0 && i < ctx->common.hlist->len - 1) { + rspamd_printf_gstring(hdr, " "); + headers_len = cur_len; + } + + for (j = 0; j < count + 1; j++) { + rspamd_printf_gstring(hdr, "%s:", dh->name); + } + } + } + else { + rh = rspamd_message_get_header_array(task, dh->name, FALSE); + + if (rh) { + if (hstat.s.count > 0) { + + cur_len = (strlen(dh->name) + 1) * (hstat.s.count); + headers_len += cur_len; + if (headers_len > 70 && i > 0 && i < ctx->common.hlist->len - 1) { + rspamd_printf_gstring(hdr, " "); + headers_len = cur_len; + } + + for (j = 0; j < hstat.s.count; j++) { + rspamd_printf_gstring(hdr, "%s:", dh->name); + } + } + + + rspamd_dkim_canonize_header(&ctx->common, task, + dh->name, dh->count, + NULL, NULL); + } + } + + g_hash_table_remove(ctx->common.htable, dh->name); + } + + /* Replace the last ':' with ';' */ + hdr->str[hdr->len - 1] = ';'; + + if (ctx->common.type != RSPAMD_DKIM_ARC_SEAL) { + if (!cached_bh->digest_normal) { + EVP_DigestFinal_ex(ctx->common.body_hash, raw_digest, NULL); + cached_bh->digest_normal = rspamd_mempool_alloc(task->task_pool, + sizeof(raw_digest)); + memcpy(cached_bh->digest_normal, raw_digest, sizeof(raw_digest)); + } + + + b64_data = rspamd_encode_base64(cached_bh->digest_normal, dlen, 0, NULL); + rspamd_printf_gstring(hdr, " bh=%s; b=", b64_data); + g_free(b64_data); + } + else { + rspamd_printf_gstring(hdr, " b="); + } + + switch (ctx->common.type) { + case RSPAMD_DKIM_NORMAL: + default: + hname = RSPAMD_DKIM_SIGNHEADER; + break; + case RSPAMD_DKIM_ARC_SIG: + hname = RSPAMD_DKIM_ARC_SIGNHEADER; + break; + case RSPAMD_DKIM_ARC_SEAL: + hname = RSPAMD_DKIM_ARC_SEALHEADER; + break; + } + + if (ctx->common.header_canon_type == DKIM_CANON_RELAXED) { + if (!rspamd_dkim_canonize_header_relaxed(&ctx->common, + hdr->str, + hname, + TRUE, + 0, + ctx->common.type == RSPAMD_DKIM_ARC_SEAL)) { + + g_string_free(hdr, TRUE); + return NULL; + } + } + else { + /* Will likely have issues with folding */ + rspamd_dkim_hash_update(ctx->common.headers_hash, hdr->str, + hdr->len); + ctx->common.headers_canonicalised += hdr->len; + msg_debug_task("update signature with header: %*s", + (gint) hdr->len, hdr->str); + } + + dlen = EVP_MD_CTX_size(ctx->common.headers_hash); + EVP_DigestFinal_ex(ctx->common.headers_hash, raw_digest, NULL); + if (ctx->key->type == RSPAMD_DKIM_KEY_RSA) { + sig_len = RSA_size(ctx->key->key.key_rsa); + sig_buf = g_alloca(sig_len); + + if (RSA_sign(NID_sha256, raw_digest, dlen, sig_buf, &sig_len, + ctx->key->key.key_rsa) != 1) { + g_string_free(hdr, TRUE); + msg_err_task("rsa sign error: %s", + ERR_error_string(ERR_get_error(), NULL)); + + return NULL; + } + } + else if (ctx->key->type == RSPAMD_DKIM_KEY_EDDSA) { + sig_len = rspamd_cryptobox_signature_bytes(RSPAMD_CRYPTOBOX_MODE_25519); + sig_buf = g_alloca(sig_len); + + rspamd_cryptobox_sign(sig_buf, NULL, raw_digest, dlen, + ctx->key->key.key_eddsa, RSPAMD_CRYPTOBOX_MODE_25519); + } + else { + g_string_free(hdr, TRUE); + msg_err_task("unsupported key type for signing"); + + return NULL; + } + + if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_MILTER) { + b64_data = rspamd_encode_base64_fold(sig_buf, sig_len, 70, NULL, + RSPAMD_TASK_NEWLINES_LF); + } + else { + b64_data = rspamd_encode_base64_fold(sig_buf, sig_len, 70, NULL, + MESSAGE_FIELD(task, nlines_type)); + } + + rspamd_printf_gstring(hdr, "%s", b64_data); + g_free(b64_data); + + return hdr; +} + +gboolean +rspamd_dkim_match_keys(rspamd_dkim_key_t *pk, + rspamd_dkim_sign_key_t *sk, + GError **err) +{ + if (pk == NULL || sk == NULL) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "missing public or private key"); + return FALSE; + } + if (pk->type != sk->type) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "public and private key types do not match"); + return FALSE; + } + + if (pk->type == RSPAMD_DKIM_KEY_EDDSA) { + if (memcmp(sk->key.key_eddsa + 32, pk->key.key_eddsa, 32) != 0) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH, + "pubkey does not match private key"); + return FALSE; + } + } + else if (EVP_PKEY_cmp(pk->key_evp, sk->key_evp) != 1) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH, + "pubkey does not match private key"); + return FALSE; + } + + return TRUE; +} |