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
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)
{
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);
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);
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;
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);
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
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
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
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 },
},
})
- -- 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))
--- /dev/null
+*** 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}
--- /dev/null
+*** 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
--- /dev/null
+.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
+}
--- /dev/null
+.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"];
+ }
+ }
+}
--- /dev/null
+eG7PwSiukhUE4CUIfaEBw50VVLcgNRMAq85k4tqKT7StW8cdiXqvj5pSkB6MD0LKPYRsx7FReXnkur8MCiKlyQ==
\ No newline at end of file
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";
--- /dev/null
+From: Rspamd <foo@ed25519.za.org>
+
+hello