From: Vsevolod Stakhov Date: Sat, 4 Oct 2025 13:28:19 +0000 (+0100) Subject: feat: Add ED25519 support for DKIM signing and verification X-Git-Tag: 3.13.2~4^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0da40ce40b4f302bf23dab2a47400781017ad8b6;p=thirdparty%2Frspamd.git feat: Add ED25519 support for DKIM signing and verification This commit introduces support for ED25519 keys in DKIM signing and verification. It includes changes to the DKIM library to handle ED25519 keys, along with new test cases and configuration files to demonstrate and test this functionality. Co-authored-by: Vsevolod Stakhov --- diff --git a/src/libserver/dkim.c b/src/libserver/dkim.c index e9c86125b7..b1bcda099a 100644 --- a/src/libserver/dkim.c +++ b/src/libserver/dkim.c @@ -1479,8 +1479,10 @@ void rspamd_dkim_sign_key_free(rspamd_dkim_sign_key_t *key) } } else { - rspamd_explicit_memzero(key->specific.key_eddsa, key->keylen); - g_free(key->keydata); + if (key->specific.key_eddsa) { + rspamd_explicit_memzero(key->specific.key_eddsa, key->keylen); + g_free(key->specific.key_eddsa); + } } g_free(key); @@ -3345,6 +3347,67 @@ rspamd_dkim_sign_key_load(const char *key, size_t len, goto end; } } + + /* Detect the key type from the loaded EVP_PKEY */ + int key_type = EVP_PKEY_base_id(nkey->specific.key_ssl.key_evp); + switch (key_type) { + case EVP_PKEY_RSA: + nkey->type = RSPAMD_DKIM_KEY_RSA; + nkey->keylen = EVP_PKEY_size(nkey->specific.key_ssl.key_evp); + break; + case EVP_PKEY_EC: + nkey->type = RSPAMD_DKIM_KEY_ECDSA; + nkey->keylen = EVP_PKEY_size(nkey->specific.key_ssl.key_evp); + break; + case EVP_PKEY_ED25519: + /* For Ed25519, extract the raw key and store it in the eddsa field */ + nkey->type = RSPAMD_DKIM_KEY_EDDSA; + nkey->keylen = crypto_sign_secretkeybytes(); + nkey->specific.key_eddsa = g_malloc(nkey->keylen); + + /* Extract raw private key from EVP_PKEY */ + size_t extracted_len = nkey->keylen; + if (EVP_PKEY_get_raw_private_key(nkey->specific.key_ssl.key_evp, + nkey->specific.key_eddsa, &extracted_len) != 1) { + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "cannot extract ed25519 raw key: %s", + ERR_error_string(ERR_get_error(), NULL)); + EVP_PKEY_free(nkey->specific.key_ssl.key_evp); + rspamd_dkim_sign_key_free(nkey); + nkey = NULL; + goto end; + } + + /* ED25519 raw private key is 32 bytes (the seed), but we need the full 64-byte key */ + if (extracted_len == 32) { + /* OpenSSL gives us the 32-byte seed, we need to derive the full key */ + unsigned char pk[32]; + unsigned char *full_key = g_malloc(crypto_sign_secretkeybytes()); + crypto_sign_ed25519_seed_keypair(pk, full_key, nkey->specific.key_eddsa); + rspamd_explicit_memzero(nkey->specific.key_eddsa, extracted_len); + g_free(nkey->specific.key_eddsa); + nkey->specific.key_eddsa = full_key; + } + + /* Clean up the EVP_PKEY and BIO as we have the raw key now */ + EVP_PKEY_free(nkey->specific.key_ssl.key_evp); + BIO_free(nkey->specific.key_ssl.key_bio); + /* Zero out the pointers to avoid double-free in cleanup */ + nkey->specific.key_ssl.key_evp = NULL; + nkey->specific.key_ssl.key_bio = NULL; + break; + default: + g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL, + "unsupported key type: %s", + OBJ_nid2sn(key_type)); + rspamd_dkim_sign_key_free(nkey); + nkey = NULL; + goto end; + } + + msg_debug_dkim_taskless("loaded %s private key from %s", + nkey->type == RSPAMD_DKIM_KEY_RSA ? "RSA" : (nkey->type == RSPAMD_DKIM_KEY_ECDSA ? "ECDSA" : "Ed25519"), + type == RSPAMD_DKIM_KEY_PEM ? "PEM" : "DER"); } REF_INIT_RETAIN(nkey, rspamd_dkim_sign_key_free); diff --git a/test/functional/cases/116_dkim.robot b/test/functional/cases/116_dkim.robot index 5c1005c281..c182f17711 100644 --- a/test/functional/cases/116_dkim.robot +++ b/test/functional/cases/116_dkim.robot @@ -57,3 +57,9 @@ DKIM Verify ED25519 PASS DKIM Verify ED25519 REJECT Scan File ${RSPAMD_TESTDIR}/messages/ed25519-broken.eml Expect Symbol R_DKIM_REJECT + +DKIM Sign ED25519 PEM + ${result} = Scan Message With Rspamc ${RSPAMD_TESTDIR}/messages/spam_message.eml --mime --header=dodkim=1 -c ${RSPAMD_TESTDIR}/configs/dkim-ed25519-pem.conf + Check Rspamc ${result} ed25519-sha256 + Set Suite Variable ${SIGNED_ED25519_MESSAGE} ${RSPAMD_TMPDIR}/dkim_sign_ed25519_pem_test.eml + Create File ${SIGNED_ED25519_MESSAGE} ${result.stdout} diff --git a/test/functional/configs/dkim-ed25519-pem.conf b/test/functional/configs/dkim-ed25519-pem.conf new file mode 100644 index 0000000000..72fc517224 --- /dev/null +++ b/test/functional/configs/dkim-ed25519-pem.conf @@ -0,0 +1,66 @@ +.include(duplicate=append,priority=0) "{= env.TESTDIR =}/configs/plugins.conf" + +options = { + filters = ["dkim"] + pidfile = "{= env.TMPDIR =}/rspamd.pid" + dns { + retransmits = 10; + timeout = 2s; + } +} +logging = { + type = "file", + level = "debug" + filename = "{= env.TMPDIR =}/rspamd.log" +} +metric = { + name = "default", + actions = { + reject = 100500, + } + unknown_weight = 1 +} + +worker { + type = normal + bind_socket = "{= env.LOCAL_ADDR =}:{= env.PORT_NORMAL =}" + count = 1 + keypair { + pubkey = "{= env.KEY_PUB1 =}"; + privkey = "{= env.KEY_PVT1 =}"; + } + task_timeout = 60s; +} + +worker { + type = controller + bind_socket = "{= env.LOCAL_ADDR =}:{= env.PORT_CONTROLLER =}" + count = 1 + secure_ip = ["127.0.0.1", "::1"]; + stats_path = "{= env.TMPDIR =}/stats.ucl" +} + +dkim { + +sign_condition =<