]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
feat: Add ED25519 support for DKIM signing and verification
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 4 Oct 2025 13:28:19 +0000 (14:28 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 4 Oct 2025 13:28:19 +0000 (14:28 +0100)
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 <v@rspamd.com>
src/libserver/dkim.c
test/functional/cases/116_dkim.robot
test/functional/configs/dkim-ed25519-pem.conf [new file with mode: 0644]
test/functional/configs/dkim-eddsa-pem.key [new file with mode: 0644]

index e9c86125b7b3091211106316635a9f70376f44c2..b1bcda099a1644095f6e05d8266e7282ba4f0bf9 100644 (file)
@@ -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);
index 5c1005c28109caf0495940b37e873a19c4a5db65..c182f177117d90088bc3b0d70b7223b36d139b20 100644 (file)
@@ -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 (file)
index 0000000..72fc517
--- /dev/null
@@ -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 =<<EOD
+return function(task)
+  local dodkim = task:get_request_header('dodkim')
+  if not dodkim then return end
+  return {
+    key = "{= env.TESTDIR =}/configs/dkim-eddsa-pem.key",
+    domain = "cacophony.za.org",
+    selector = "dkim"
+  }
+end
+EOD;
+
+  dkim_cache_size = 2k;
+  dkim_cache_expire = 1d;
+  time_jitter = 6h;
+  trusted_only = false;
+  skip_multi = false;
+}
+modules {
+    path = "{= env.TESTDIR =}/../../src/plugins/lua/"
+}
+lua = "{= env.TESTDIR =}/lua/test_coverage.lua";
diff --git a/test/functional/configs/dkim-eddsa-pem.key b/test/functional/configs/dkim-eddsa-pem.key
new file mode 100644 (file)
index 0000000..66d4a40
--- /dev/null
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIPwynGQ23nIwwKJYgLULTADkL+L4fAPEtSWjNYA6PPSo
+-----END PRIVATE KEY-----