From: Vsevolod Stakhov Date: Wed, 15 Oct 2025 15:30:53 +0000 (+0100) Subject: [Feature] Add DKIM signing key API for flexible ARC signing X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=0b5968b23ccee5f10de47ca14733af7abab9a8aa;p=thirdparty%2Frspamd.git [Feature] Add DKIM signing key API for flexible ARC signing Implements new C API for DKIM signing operations: - rspamd_plugins.dkim.load_sign_key() - loads signing key - rspamd_plugins.dkim.sign_key_get_alg() - detects key algorithm - rspamd_plugins.dkim.sign_digest() - signs digest with loaded key Updates ARC module to use new API for proper ed25519 and RSA support. Adds comprehensive tests and improved signing eligibility diagnostics. --- diff --git a/lualib/lua_dkim_tools.lua b/lualib/lua_dkim_tools.lua index fe13c0c502..1942f4c2d8 100644 --- a/lualib/lua_dkim_tools.lua +++ b/lualib/lua_dkim_tools.lua @@ -191,7 +191,12 @@ local function prepare_dkim_signing(N, task, settings) elseif settings.sign_inbound and not is_local and not auser then lua_util.debugm(N, task, 'mail was sent to us') else - lua_util.debugm(N, task, 'mail is ineligible for signing') + lua_util.debugm(N, task, + 'mail is ineligible for signing: auser=%s, ip=%s, is_local=%s, ' .. + 'sign_authenticated=%s, sign_networks=%s, sign_local=%s, sign_inbound=%s', + auser, tostring(ip), is_local, + settings.sign_authenticated, settings.sign_networks ~= nil, + settings.sign_local, settings.sign_inbound) return false, {} end diff --git a/src/libserver/dkim.c b/src/libserver/dkim.c index 7084fe02ad..acb9d41900 100644 --- a/src/libserver/dkim.c +++ b/src/libserver/dkim.c @@ -3220,6 +3220,87 @@ void rspamd_dkim_sign_key_unref(rspamd_dkim_sign_key_t *k) REF_RELEASE(k); } +enum rspamd_dkim_key_type +rspamd_dkim_sign_key_get_type(rspamd_dkim_sign_key_t *key) +{ + if (key) { + return key->type; + } + return RSPAMD_DKIM_KEY_UNKNOWN; +} + +gboolean +rspamd_dkim_sign_digest(rspamd_dkim_sign_key_t *key, + const unsigned char *digest, gsize dlen, + char **sig_out, GError **err) +{ + unsigned char *sig_buf; + unsigned int sig_len; + + if (!key || !digest || dlen != 32 || !sig_out) { + g_set_error(err, DKIM_ERROR, DKIM_SIGERROR_INVALID_HC, + "invalid arguments"); + return FALSE; + } + +#ifdef HAVE_OPENSSL + if (key->type == RSPAMD_DKIM_KEY_RSA) { + sig_len = EVP_PKEY_size(key->specific.key_ssl.key_evp); + sig_buf = g_alloca(sig_len); + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(key->specific.key_ssl.key_evp, NULL); + if (EVP_PKEY_sign_init(pctx) <= 0) { + g_set_error(err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, + "rsa sign error: %s", + ERR_error_string(ERR_get_error(), NULL)); + EVP_PKEY_CTX_free(pctx); + return FALSE; + } + if (EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PADDING) <= 0) { + g_set_error(err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, + "rsa sign error: %s", + ERR_error_string(ERR_get_error(), NULL)); + EVP_PKEY_CTX_free(pctx); + return FALSE; + } + if (EVP_PKEY_CTX_set_signature_md(pctx, EVP_sha256()) <= 0) { + g_set_error(err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, + "rsa sign error: %s", + ERR_error_string(ERR_get_error(), NULL)); + EVP_PKEY_CTX_free(pctx); + return FALSE; + } + size_t sig_len_size_t = sig_len; + if (EVP_PKEY_sign(pctx, sig_buf, &sig_len_size_t, digest, dlen) <= 0) { + g_set_error(err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, + "rsa sign error: %s", + ERR_error_string(ERR_get_error(), NULL)); + EVP_PKEY_CTX_free(pctx); + return FALSE; + } + sig_len = sig_len_size_t; + EVP_PKEY_CTX_free(pctx); + } + else +#endif +#ifdef HAVE_ED25519 + if (key->type == RSPAMD_DKIM_KEY_EDDSA) { + sig_len = crypto_sign_bytes(); + sig_buf = g_alloca(sig_len); + rspamd_cryptobox_sign(sig_buf, NULL, digest, dlen, key->specific.key_eddsa); + } + else +#endif + { + g_set_error(err, DKIM_ERROR, DKIM_SIGERROR_KEYFAIL, + "unsupported key type"); + return FALSE; + } + + /* Encode signature as base64 */ + *sig_out = rspamd_encode_base64(sig_buf, sig_len, 0, NULL); + return TRUE; +} + const char * rspamd_dkim_get_domain(rspamd_dkim_context_t *ctx) { diff --git a/src/libserver/dkim.h b/src/libserver/dkim.h index 0c3f6edebb..de3ba0ff0b 100644 --- a/src/libserver/dkim.h +++ b/src/libserver/dkim.h @@ -224,6 +224,26 @@ rspamd_dkim_sign_key_t *rspamd_dkim_sign_key_ref(rspamd_dkim_sign_key_t *k); void rspamd_dkim_sign_key_unref(rspamd_dkim_sign_key_t *k); +/** + * Get the type of a signing key + * @param key signing key + * @return key type (RSA, EDDSA, etc) + */ +enum rspamd_dkim_key_type rspamd_dkim_sign_key_get_type(rspamd_dkim_sign_key_t *key); + +/** + * Sign a digest with a DKIM signing key + * @param key signing key + * @param digest SHA256 digest (32 bytes) + * @param dlen digest length (must be 32) + * @param sig_out output signature buffer (base64 encoded), must be freed by caller + * @param err error pointer + * @return TRUE if successful + */ +gboolean rspamd_dkim_sign_digest(rspamd_dkim_sign_key_t *key, + const unsigned char *digest, gsize dlen, + char **sig_out, GError **err); + const char *rspamd_dkim_get_domain(rspamd_dkim_context_t *ctx); const char *rspamd_dkim_get_selector(rspamd_dkim_context_t *ctx); diff --git a/src/plugins/dkim_check.c b/src/plugins/dkim_check.c index 1fbe8eff39..bb0af61752 100644 --- a/src/plugins/dkim_check.c +++ b/src/plugins/dkim_check.c @@ -107,6 +107,30 @@ static void dkim_symbol_callback(struct rspamd_task *task, static int lua_dkim_sign_handler(lua_State *L); static int lua_dkim_verify_handler(lua_State *L); static int lua_dkim_canonicalize_handler(lua_State *L); +static int lua_dkim_load_sign_key_handler(lua_State *L); +static int lua_dkim_sign_key_get_alg_handler(lua_State *L); +static int lua_dkim_sign_digest_handler(lua_State *L); + +/* Lua userdata classname for DKIM signing keys */ +#define rspamd_dkim_sign_key_classname "rspamd{dkim_sign_key}" + +static rspamd_dkim_sign_key_t * +lua_check_dkim_sign_key(lua_State *L, int pos) +{ + void *ud = rspamd_lua_check_udata(L, pos, rspamd_dkim_sign_key_classname); + luaL_argcheck(L, ud != NULL, pos, "'dkim_sign_key' expected"); + return ud ? *((rspamd_dkim_sign_key_t **) ud) : NULL; +} + +static int +lua_dkim_sign_key_gc(lua_State *L) +{ + rspamd_dkim_sign_key_t *key = lua_check_dkim_sign_key(L, 1); + if (key) { + rspamd_dkim_sign_key_unref(key); + } + return 0; +} /* Initialization */ int dkim_module_init(struct rspamd_config *cfg, struct module_ctx **ctx); @@ -328,10 +352,26 @@ int dkim_module_config(struct rspamd_config *cfg, bool validate) lua_pushstring(cfg->lua_state, "canon_header_relaxed"); lua_pushcfunction(cfg->lua_state, lua_dkim_canonicalize_handler); lua_settable(cfg->lua_state, -3); + lua_pushstring(cfg->lua_state, "load_sign_key"); + lua_pushcfunction(cfg->lua_state, lua_dkim_load_sign_key_handler); + lua_settable(cfg->lua_state, -3); + lua_pushstring(cfg->lua_state, "sign_key_get_alg"); + lua_pushcfunction(cfg->lua_state, lua_dkim_sign_key_get_alg_handler); + lua_settable(cfg->lua_state, -3); + lua_pushstring(cfg->lua_state, "sign_digest"); + lua_pushcfunction(cfg->lua_state, lua_dkim_sign_digest_handler); + lua_settable(cfg->lua_state, -3); /* Finish dkim key */ lua_settable(cfg->lua_state, -3); } + /* Register metatable for dkim_sign_key userdata */ + luaL_Reg dkim_sign_key_mt[] = { + {"__gc", lua_dkim_sign_key_gc}, + {NULL, NULL}}; + rspamd_lua_new_class(cfg->lua_state, rspamd_dkim_sign_key_classname, dkim_sign_key_mt); + lua_pop(cfg->lua_state, 1); + lua_pop(cfg->lua_state, 1); /* Remove global function */ dkim_module_ctx->whitelist_ip = NULL; @@ -904,6 +944,157 @@ lua_dkim_sign_handler(lua_State *L) return 2; } +/* Load a DKIM signing key and return it as userdata */ +static int +lua_dkim_load_sign_key_handler(lua_State *L) +{ + struct rspamd_task *task = lua_check_task(L, 1); + GError *err = NULL; + const char *key = NULL, *rawkey = NULL; + rspamd_dkim_sign_key_t *dkim_key; + gsize rawlen = 0, keylen = 0; + struct dkim_ctx *dkim_module_ctx; + rspamd_dkim_sign_key_t **pkey; + + luaL_argcheck(L, lua_type(L, 2) == LUA_TTABLE, 2, "'table' expected"); + + if (!rspamd_lua_parse_table_arguments(L, 2, &err, + RSPAMD_LUA_PARSE_ARGUMENTS_DEFAULT, + "key=V;rawkey=V", + &keylen, &key, &rawlen, &rawkey)) { + msg_err_task("cannot parse table arguments: %e", err); + g_error_free(err); + lua_pushnil(L); + return 1; + } + + dkim_module_ctx = dkim_get_context(task->cfg); + + if (key) { + dkim_key = dkim_module_load_key_format(task, dkim_module_ctx, key, + keylen, RSPAMD_DKIM_KEY_UNKNOWN); + } + else if (rawkey) { + dkim_key = dkim_module_load_key_format(task, dkim_module_ctx, rawkey, + rawlen, RSPAMD_DKIM_KEY_UNKNOWN); + } + else { + msg_err_task("neither key nor rawkey are specified"); + lua_pushnil(L); + return 1; + } + + if (dkim_key == NULL) { + lua_pushnil(L); + return 1; + } + + /* Store key in userdata */ + pkey = lua_newuserdata(L, sizeof(rspamd_dkim_sign_key_t *)); + *pkey = rspamd_dkim_sign_key_ref(dkim_key); + rspamd_lua_setclass(L, rspamd_dkim_sign_key_classname, -1); + + return 1; +} + +/* Get algorithm name from a loaded signing key */ +static int +lua_dkim_sign_key_get_alg_handler(lua_State *L) +{ + rspamd_dkim_sign_key_t *dkim_key = lua_check_dkim_sign_key(L, 1); + enum rspamd_dkim_key_type key_type; + + if (dkim_key == NULL) { + return luaL_error(L, "invalid arguments"); + } + + key_type = rspamd_dkim_sign_key_get_type(dkim_key); + + if (key_type == RSPAMD_DKIM_KEY_RSA) { + lua_pushstring(L, "rsa-sha256"); + } + else if (key_type == RSPAMD_DKIM_KEY_EDDSA) { + lua_pushstring(L, "ed25519-sha256"); + } + else { + lua_pushstring(L, "unknown"); + } + + return 1; +} + +static int +lua_dkim_sign_digest_handler(lua_State *L) +{ + struct rspamd_task *task = lua_check_task(L, 1); + GError *err = NULL; + rspamd_dkim_sign_key_t *dkim_key; + const char *digest_str = NULL; + gsize digest_len = 0; + struct rspamd_lua_text *digest_text = NULL; + const unsigned char *digest_data; + char *sig_b64 = NULL; + + luaL_argcheck(L, lua_type(L, 2) == LUA_TTABLE, 2, "'table' expected"); + + /* Get sign_key from table */ + lua_pushstring(L, "sign_key"); + lua_gettable(L, 2); + dkim_key = lua_check_dkim_sign_key(L, -1); + lua_pop(L, 1); + + if (dkim_key == NULL) { + msg_err_task("sign_key is not specified or invalid"); + lua_pushboolean(L, FALSE); + return 1; + } + + /* Get digest from table - can be string or rspamd_text */ + lua_pushstring(L, "digest"); + lua_gettable(L, 2); + + if (lua_type(L, -1) == LUA_TSTRING) { + digest_str = lua_tolstring(L, -1, &digest_len); + digest_data = (const unsigned char *) digest_str; + } + else { + digest_text = lua_check_text(L, -1); + if (digest_text) { + digest_data = (const unsigned char *) digest_text->start; + digest_len = digest_text->len; + } + else { + lua_pop(L, 1); + msg_err_task("digest is not specified or invalid"); + lua_pushboolean(L, FALSE); + return 1; + } + } + lua_pop(L, 1); + + if (digest_len != 32) { + msg_err_task("digest must be exactly 32 bytes (SHA256), got %zu", digest_len); + lua_pushboolean(L, FALSE); + return 1; + } + + /* Call C function to sign the digest */ + if (!rspamd_dkim_sign_digest(dkim_key, digest_data, digest_len, + &sig_b64, &err)) { + msg_err_task("cannot sign digest: %e", err); + g_error_free(err); + lua_pushboolean(L, FALSE); + return 1; + } + + lua_pushboolean(L, TRUE); + lua_pushstring(L, sig_b64); + + g_free(sig_b64); + + return 2; +} + int dkim_module_reconfig(struct rspamd_config *cfg) { return dkim_module_config(cfg, false); diff --git a/src/plugins/lua/arc.lua b/src/plugins/lua/arc.lua index b681572ada..39fb874fd1 100644 --- a/src/plugins/lua/arc.lua +++ b/src/plugins/lua/arc.lua @@ -21,6 +21,7 @@ local rspamd_util = require "rspamd_util" local fun = require "fun" local lua_auth_results = require "lua_auth_results" local lua_mime = require "lua_mime" +local hash = require "rspamd_cryptobox_hash" if confighelp then return @@ -36,6 +37,9 @@ end local dkim_verify = rspamd_plugins.dkim.verify local dkim_sign = rspamd_plugins.dkim.sign +local dkim_load_sign_key = rspamd_plugins.dkim.load_sign_key +local dkim_sign_key_get_alg = rspamd_plugins.dkim.sign_key_get_alg +local dkim_sign_digest = rspamd_plugins.dkim.sign_digest local dkim_canonicalize = rspamd_plugins.dkim.canon_header_relaxed local redis_params @@ -197,7 +201,31 @@ local function arc_callback(task) local arc_seal_headers = task:get_header_full('ARC-Seal') local arc_ar_headers = task:get_header_full('ARC-Authentication-Results') + lua_util.debugm(N, task, 'ARC verification: found %s ARC-MS, %s ARC-Seal, %s ARC-AR headers', + arc_sig_headers and #arc_sig_headers or 0, + arc_seal_headers and #arc_seal_headers or 0, + arc_ar_headers and #arc_ar_headers or 0) + + if arc_sig_headers then + for i, hdr in ipairs(arc_sig_headers) do + lua_util.debugm(N, task, 'ARC-Message-Signature[%s]: %s', i, hdr.decoded) + end + end + + if arc_seal_headers then + for i, hdr in ipairs(arc_seal_headers) do + lua_util.debugm(N, task, 'ARC-Seal[%s]: %s', i, hdr.decoded) + end + end + + if arc_ar_headers then + for i, hdr in ipairs(arc_ar_headers) do + lua_util.debugm(N, task, 'ARC-Authentication-Results[%s]: %s', i, hdr.decoded) + end + end + if not arc_sig_headers or not arc_seal_headers then + lua_util.debugm(N, task, 'ARC verification failed: missing required headers') task:insert_result(arc_symbols['na'], 1.0) return end @@ -553,7 +581,10 @@ local function arc_sign_seal(task, params, header) cur_auth_results, { stop_chars = ';', structured = true, encode = false }) - -- Temporarily add AAR and AMS headers so they can be signed by the seal + -- Add AAR and AMS headers first + lua_util.debugm(N, task, 'adding ARC-Authentication-Results: %s', cur_auth_results) + lua_util.debugm(N, task, 'adding ARC-Message-Signature: %s', header) + lua_mime.modify_headers(task, { add = { ['ARC-Authentication-Results'] = { order = 1, value = cur_auth_results }, @@ -561,37 +592,106 @@ local function arc_sign_seal(task, params, header) }, }) - -- Create ARC-Seal using the native C dkim_sign function - -- This supports both RSA and ed25519 keys automatically - local seal_params = { - domain = params.domain, - selector = params.selector, + -- Create ARC-Seal signature manually using SHA256 hash + -- We must canonicalize all ARC headers in order and sign them + local sha_ctx = hash.create_specific('sha256') + + -- Canonicalize previous ARC sets if they exist (arc_seals already retrieved above) + local arc_sigs = task:cache_get('arc-sigs') + local arc_auth_results = task:cache_get('arc-authres') + + if arc_seals then + for i = 1, #arc_seals do + if arc_auth_results[i] then + local s = dkim_canonicalize('ARC-Authentication-Results', arc_auth_results[i].raw_header) + sha_ctx:update(s) + lua_util.debugm(N, task, 'canonicalized previous AAR[%d]: %s', i, s) + end + if arc_sigs[i] then + local s = dkim_canonicalize('ARC-Message-Signature', arc_sigs[i].raw_header) + sha_ctx:update(s) + lua_util.debugm(N, task, 'canonicalized previous AMS[%d]: %s', i, s) + end + if arc_seals[i] then + local s = dkim_canonicalize('ARC-Seal', arc_seals[i].raw_header) + sha_ctx:update(s) + lua_util.debugm(N, task, 'canonicalized previous AS[%d]: %s', i, s) + end + end + end + + -- Canonicalize the new ARC-Authentication-Results header + local s = dkim_canonicalize('ARC-Authentication-Results', cur_auth_results) + sha_ctx:update(s) + lua_util.debugm(N, task, 'canonicalized new AAR: %s', s) + + -- Canonicalize the new ARC-Message-Signature header + s = dkim_canonicalize('ARC-Message-Signature', header) + sha_ctx:update(s) + lua_util.debugm(N, task, 'canonicalized new AMS: %s', s) + + -- Load the signing key + local sign_key = dkim_load_sign_key(task, { key = params.key, rawkey = params.rawkey, - sign_type = 'arc-seal', - arc_idx = cur_idx, - arc_cv = params.arc_cv, - headers = 'arc-authentication-results:arc-message-signature' .. - string.rep(':arc-seal', cur_idx - 1), -- Include previous seals - } + }) + + if not sign_key then + rspamd_logger.errx(task, 'cannot load signing key') + return + end + + -- Get the algorithm from the key + local algorithm = dkim_sign_key_get_alg(sign_key) + if not algorithm or algorithm == 'unknown' then + rspamd_logger.errx(task, 'cannot determine key algorithm') + return + end + + -- Construct partial ARC-Seal header (without signature) using detected algorithm + local cur_arc_seal = string.format('i=%d; a=%s; d=%s; s=%s; cv=%s; t=%d; b=', + cur_idx, algorithm, params.domain, params.selector, params.arc_cv, + math.floor(rspamd_util.get_time())) + + -- Canonicalize the partial ARC-Seal and add to hash + local seal_canon = string.format('%s:%s', 'arc-seal', cur_arc_seal) + sha_ctx:update(seal_canon) + lua_util.debugm(N, task, 'canonicalized partial seal: %s', seal_canon) + + -- Now sign the complete digest + local dret, sig_b64 = dkim_sign_digest(task, { + sign_key = sign_key, + digest = sha_ctx:bin(), + }) - local dret, seal_header = dkim_sign(task, seal_params) - if not dret then + if not dret or not sig_b64 then rspamd_logger.errx(task, 'cannot create ARC seal signature') return end + -- Get appropriate line ending + local nl_type + if task:has_flag("milter") then + nl_type = "lf" + else + nl_type = task:get_newlines_type() + end + + -- Fold the signature to 70 chars per line + local folded_sig = rspamd_util.encode_base64(rspamd_util.decode_base64(sig_b64), 70, nl_type) + cur_arc_seal = cur_arc_seal .. folded_sig + + lua_util.debugm(N, task, 'adding ARC-Seal: %s', cur_arc_seal) + lua_mime.modify_headers(task, { add = { ['ARC-Seal'] = { order = 1, value = lua_util.fold_header_with_encoding(task, - 'ARC-Seal', seal_header, + 'ARC-Seal', cur_arc_seal, { structured = true, encode = false }) } }, - -- RFC requires a strict order for these headers to be inserted - order = { 'ARC-Authentication-Results', 'ARC-Message-Signature', 'ARC-Seal' }, }) task:insert_result(settings.sign_symbol, 1.0, string.format('%s:s=%s:i=%d', params.domain, params.selector, cur_idx)) diff --git a/test/functional/cases/320_arc_signing/003_roundtrip.robot b/test/functional/cases/320_arc_signing/003_roundtrip.robot new file mode 100644 index 0000000000..cd8472a1e1 --- /dev/null +++ b/test/functional/cases/320_arc_signing/003_roundtrip.robot @@ -0,0 +1,114 @@ +*** Settings *** +Suite Setup Rspamd Setup +Suite Teardown Rspamd Teardown +Library ${RSPAMD_TESTDIR}/lib/rspamd.py +Library String +Resource ${RSPAMD_TESTDIR}/lib/rspamd.robot +Variables ${RSPAMD_TESTDIR}/lib/vars.py + +*** Variables *** +${CONFIG} ${RSPAMD_TESTDIR}/configs/arc_signing/roundtrip.conf +${MESSAGE_RSA} ${RSPAMD_TESTDIR}/messages/dmarc/fail_none.eml +${MESSAGE_ED25519} ${RSPAMD_TESTDIR}/messages/dmarc/ed25519_from.eml +${REDIS_SCOPE} Suite +${RSPAMD_SCOPE} Suite +${RSPAMD_URL_TLD} ${RSPAMD_TESTDIR}/../lua/unit/test_tld.dat + +*** Test Cases *** +ARC ROUNDTRIP RSA SIGN AND VERIFY + # First pass: Sign the message with RSA key and check for ARC_SIGNED + ${result} = Scan Message With Rspamc ${MESSAGE_RSA} -u bob@cacophony.za.org --mime + Should Contain ${result.stdout} ARC_SIGNED + + # Write signed message to robot-save directory for debugging + ${signed_file} = Write Mime Message To File ${result} rsa_signed.eml + + # Second pass: Verify the ARC signature we just created + ${verify_result} = Scan Message With Rspamc ${signed_file} --header=Settings-Id:arc_verify + Should Contain ${verify_result.stdout} ARC_ALLOW + Should Not Contain ${verify_result.stdout} ARC_INVALID + Should Not Contain ${verify_result.stdout} ARC_REJECT + + # Cleanup + Remove File ${signed_file} + +ARC ROUNDTRIP ED25519 SIGN AND VERIFY + # First pass: Sign the message with ed25519 key and check for ARC_SIGNED + ${result} = Scan Message With Rspamc ${MESSAGE_ED25519} -u bob@ed25519.za.org --mime + Should Contain ${result.stdout} ARC_SIGNED + + # Write signed message to robot-save directory for debugging + ${signed_file} = Write Mime Message To File ${result} ed25519_signed.eml + + # Second pass: Verify the ARC signature we just created + ${verify_result} = Scan Message With Rspamc ${signed_file} --header=Settings-Id:arc_verify + Should Contain ${verify_result.stdout} ARC_ALLOW + Should Not Contain ${verify_result.stdout} ARC_INVALID + Should Not Contain ${verify_result.stdout} ARC_REJECT + + # Cleanup + Remove File ${signed_file} + +ARC ED25519 ALGORITHM CHECK + # Sign with ed25519 and verify the algorithm is correctly set + ${result} = Scan Message With Rspamc ${MESSAGE_ED25519} -u bob@ed25519.za.org --mime + Should Contain ${result.stdout} ARC_SIGNED + Should Contain ${result.stdout} a=ed25519-sha256 + Should Not Contain ${result.stdout} a=rsa-sha256 + +ARC RSA ALGORITHM CHECK + # Sign with RSA and verify the algorithm is correctly set + ${result} = Scan Message With Rspamc ${MESSAGE_RSA} -u bob@cacophony.za.org --mime + Should Contain ${result.stdout} ARC_SIGNED + Should Contain ${result.stdout} a=rsa-sha256 + Should Not Contain ${result.stdout} a=ed25519-sha256 + +*** Keywords *** +Write Mime Message To File + [Arguments] ${mime_result} ${filename} + # Save to robot-save directory for artifact preservation + ${artifact_file} = Set Variable ${EXECDIR}/robot-save/${filename} + Create File ${artifact_file} ${mime_result.stdout} + Log Saved signed message to ${artifact_file} + + # Also save to temp directory for test use + ${temp_file} = Set Variable ${RSPAMD_TMPDIR}/${filename} + Create File ${temp_file} ${mime_result.stdout} + + # Count ARC headers for debugging + ${lines} = Split To Lines ${mime_result.stdout} + ${arc_count} = Set Variable ${0} + FOR ${line} IN @{lines} + ${is_arc_header} = Run Keyword And Return Status Should Match Regexp ${line} ^ARC-.*: + IF ${is_arc_header} + ${arc_count} = Evaluate ${arc_count} + 1 + END + END + Log Total ARC headers found in mime output: ${arc_count} + + # Log first few lines of the created file for debugging + ${lines} = Split To Lines ${mime_result.stdout} + ${first_10_lines} = Get Slice From List ${lines} 0 10 + Log First 10 lines of signed message file: + FOR ${line} IN @{first_10_lines} + Log ${line} + END + + # Log any ARC headers found + FOR ${line} IN @{lines} + ${is_arc_header} = Run Keyword And Return Status Should Match Regexp ${line} ^ARC-.*: + IF ${is_arc_header} + Log Found ARC header: ${line} + END + END + + [Return] ${temp_file} + +Write Signed Message To File + [Arguments] ${scan_result} ${filename} + # DEPRECATED: This method doesn't work with -p flag + # Use Write Mime Message To File instead + Log WARNING: Write Signed Message To File is deprecated, use Write Mime Message To File + ${temp_file} = Set Variable ${RSPAMD_TMPDIR}/${filename} + Create File ${temp_file} ${scan_result.stdout} + [Return] ${temp_file} diff --git a/test/functional/cases/320_arc_signing/004_ed25519.robot b/test/functional/cases/320_arc_signing/004_ed25519.robot new file mode 100644 index 0000000000..1d04b95883 --- /dev/null +++ b/test/functional/cases/320_arc_signing/004_ed25519.robot @@ -0,0 +1,25 @@ +*** Settings *** +Suite Setup Rspamd Setup +Suite Teardown Rspamd Teardown +Library ${RSPAMD_TESTDIR}/lib/rspamd.py +Resource ${RSPAMD_TESTDIR}/lib/rspamd.robot +Variables ${RSPAMD_TESTDIR}/lib/vars.py + +*** Variables *** +${CONFIG} ${RSPAMD_TESTDIR}/configs/arc_signing/ed25519.conf +${MESSAGE_RSA} ${RSPAMD_TESTDIR}/messages/dmarc/fail_none.eml +${MESSAGE_ED25519} ${RSPAMD_TESTDIR}/messages/dmarc/ed25519_from.eml +${REDIS_SCOPE} Suite +${RSPAMD_SCOPE} Suite +${RSPAMD_URL_TLD} ${RSPAMD_TESTDIR}/../lua/unit/test_tld.dat + +*** Test Cases *** +ARC ED25519 BASIC SIGNING + ${result} = Scan Message With Rspamc ${MESSAGE_ED25519} -u bob@ed25519.za.org --mime + Should Contain ${result.stdout} ARC_SIGNED + Should Contain ${result.stdout} a=ed25519-sha256 + +ARC RSA BASIC SIGNING + ${result} = Scan Message With Rspamc ${MESSAGE_RSA} -u bob@cacophony.za.org --mime + Should Contain ${result.stdout} ARC_SIGNED + Should Contain ${result.stdout} a=rsa-sha256 diff --git a/test/functional/configs/arc_signing/ed25519.conf b/test/functional/configs/arc_signing/ed25519.conf new file mode 100644 index 0000000000..44e545818c --- /dev/null +++ b/test/functional/configs/arc_signing/ed25519.conf @@ -0,0 +1,25 @@ +.include(duplicate=append,priority=0) "{= env.TESTDIR =}/configs/plugins.conf" + +# ARC signing configuration with both RSA and ed25519 keys +arc { + sign_authenticated = true; + use_http_headers = true; + allow_headers_fallback = true; + + domain { + # RSA key for cacophony.za.org + cacophony.za.org { + path = "{= env.TESTDIR =}/configs/dkim.key"; + selector = "dkim"; + } + # ed25519 key for ed25519.za.org + ed25519.za.org { + path = "{= env.TESTDIR =}/configs/dkim_ed25519.key"; + selector = "dkim"; + } + } + + check_pubkey = false; # Disable pubkey checking for tests + allow_pubkey_mismatch = true; + allow_username_mismatch = true; # Allow user domain to differ from signing domain +} diff --git a/test/functional/configs/arc_signing/roundtrip.conf b/test/functional/configs/arc_signing/roundtrip.conf new file mode 100644 index 0000000000..06efd2fe2f --- /dev/null +++ b/test/functional/configs/arc_signing/roundtrip.conf @@ -0,0 +1,36 @@ +.include(duplicate=append,priority=0) "{= env.TESTDIR =}/configs/plugins.conf" + +# ARC signing configuration with both RSA and ed25519 keys +arc { + sign_authenticated = true; + use_http_headers = true; + allow_headers_fallback = true; + + domain { + # RSA key for cacophony.za.org + cacophony.za.org { + path = "{= env.TESTDIR =}/configs/dkim.key"; + selector = "dkim"; + } + # ed25519 key for ed25519.za.org + ed25519.za.org { + path = "{= env.TESTDIR =}/configs/dkim_ed25519.key"; + selector = "dkim"; + } + } + + check_pubkey = false; # Disable pubkey checking for tests + allow_pubkey_mismatch = true; + allow_username_mismatch = true; # Allow user domain to differ from signing domain +} + +# Settings for ARC verification phase +settings { + arc_verify { + id = "arc_verify"; + apply { + symbols_enabled = ["ARC_CHECK", "ARC_ALLOW", "ARC_REJECT", "ARC_INVALID", "ARC_DNSFAIL", "ARC_NA"]; + symbols_disabled = ["ARC_SIGNED"]; + } + } +} diff --git a/test/functional/configs/dkim_ed25519.key b/test/functional/configs/dkim_ed25519.key new file mode 100644 index 0000000000..f9ef600de9 --- /dev/null +++ b/test/functional/configs/dkim_ed25519.key @@ -0,0 +1 @@ +eG7PwSiukhUE4CUIfaEBw50VVLcgNRMAq85k4tqKT7StW8cdiXqvj5pSkB6MD0LKPYRsx7FReXnkur8MCiKlyQ== \ No newline at end of file diff --git a/test/functional/configs/plugins.conf b/test/functional/configs/plugins.conf index 22190a37c4..2e5dee93dc 100644 --- a/test/functional/configs/plugins.conf +++ b/test/functional/configs/plugins.conf @@ -28,6 +28,11 @@ options = { type = "txt"; replies = ["v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDXtxBE5IiNRMcq2/lc2zErfdCvDFyQNBnMjbOjBQrPST2k4fdGbtpe5Iu5uS01Met+dAEf94XL8I0hwmYw+n70PP834zfJGi2egwGqrakpaWsCDPvIJZLkxJCJKQRA/zrQ622uEXdvYixVbsEGVw7U4wAGSmT5rU2eU1y63AlOlQIDAQAB"]; }, + { + name = "dkim._domainkey.ed25519.za.org", + type = "txt"; + replies = ["v=DKIM1; k=ed25519; p=rVvHHYl6r4+aUpAejA9Cyj2EbMexUXl55Lq/DAoipck="]; + }, { name = "eddsa._domainkey.cacophony.za.org", type = "txt"; diff --git a/test/functional/messages/dmarc/ed25519_from.eml b/test/functional/messages/dmarc/ed25519_from.eml new file mode 100644 index 0000000000..eb21e80267 --- /dev/null +++ b/test/functional/messages/dmarc/ed25519_from.eml @@ -0,0 +1,3 @@ +From: Rspamd + +hello