]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] Add ED25519 support for DKIM signing with OpenSSL version checks
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 4 Oct 2025 14:06:27 +0000 (15:06 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sat, 4 Oct 2025 14:06:27 +0000 (15:06 +0100)
This commit adds support for ED25519 DKIM signatures when OpenSSL 1.1.1+ is available.
Key changes:

- Added HAVE_ED25519 detection in CMake to check for EVP_PKEY_ED25519 support
- All ED25519-specific code is conditionally compiled based on HAVE_ED25519
- When ED25519 is not supported, informative error messages are returned
ED25519 keys loaded from PEM files are extracted and converted to libsodium format
- Fixed union handling to prevent double-free issues
- Updated tests to dynamically select key type based on request header
- Removed unused dkim-ed25519-pem.conf (cannot be passed via rspamc)

The implementation gracefully degrades on older OpenSSL versions while maintaining
full functionality when ED25519 support is available.

cmake/CheckSystemFeatures.cmake
config.h.in
src/libserver/dkim.c
test/functional/cases/116_dkim.robot
test/functional/configs/dkim-ed25519-pem.conf [deleted file]
test/functional/configs/dkim.conf

index 0a15749fd2741f04259f2fc77f4fa6750fc65cb4..0ff7cbb21ebd2e2189689408f1fca725d475b728 100644 (file)
@@ -58,6 +58,7 @@ function(CheckSystemFeatures)
 
     check_symbol_exists(SSL_set_tlsext_host_name "openssl/ssl.h" HAVE_SSL_TLSEXT_HOSTNAME)
     check_symbol_exists(FIPS_mode "openssl/crypto.h" HAVE_FIPS_MODE)
+    check_symbol_exists(EVP_PKEY_ED25519 "openssl/evp.h" HAVE_ED25519)
 
     # Directory and file path operations
     check_symbol_exists(dirfd "sys/types.h;unistd.h;dirent.h" HAVE_DIRFD)
index f9d910d68155fc3edf4062692e5e5efa4f93e6b1..bad7f1f2214909687fa1544b702a4cd0607b6fa3 100644 (file)
@@ -19,6 +19,7 @@
 #cmakedefine HAVE_CTYPE_H        1
 #cmakedefine HAVE_DIRENT_H       1
 #cmakedefine HAVE_DIRFD          1
+#cmakedefine HAVE_ED25519        1
 #cmakedefine HAVE_ENDIAN_H       1
 #cmakedefine HAVE_FALLOCATE      1
 #cmakedefine HAVE_FCNTL_H        1
index b1bcda099a1644095f6e05d8266e7282ba4f0bf9..9587176ce295536b8566b08dbb983c1f83050e24 100644 (file)
@@ -320,8 +320,16 @@ rspamd_dkim_parse_signalg(rspamd_dkim_context_t *ctx,
        }
        else if (len == 14) {
                if (memcmp(param, "ed25519-sha256", len) == 0) {
+#ifdef HAVE_ED25519
                        ctx->sig_alg = DKIM_SIGN_EDDSASHA256;
                        return true;
+#else
+                       g_set_error(err,
+                                               DKIM_ERROR,
+                                               DKIM_SIGERROR_BADSIG,
+                                               "ed25519 signatures are not supported (OpenSSL 1.1.1+ required)");
+                       return false;
+#endif
                }
        }
 
@@ -1285,8 +1293,11 @@ rspamd_create_dkim_context(const char *sig,
                md_alg = EVP_sha1();
        }
        else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 ||
-                        ctx->sig_alg == DKIM_SIGN_ECDSASHA256 ||
-                        ctx->sig_alg == DKIM_SIGN_EDDSASHA256) {
+                        ctx->sig_alg == DKIM_SIGN_ECDSASHA256
+#ifdef HAVE_ED25519
+                        || ctx->sig_alg == DKIM_SIGN_EDDSASHA256
+#endif
+       ) {
                md_alg = EVP_sha256();
        }
        else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 ||
@@ -2976,8 +2987,11 @@ rspamd_dkim_check(rspamd_dkim_context_t *ctx,
                nid = NID_sha1;
        }
        else if (ctx->sig_alg == DKIM_SIGN_RSASHA256 ||
-                        ctx->sig_alg == DKIM_SIGN_ECDSASHA256 ||
-                        ctx->sig_alg == DKIM_SIGN_EDDSASHA256) {
+                        ctx->sig_alg == DKIM_SIGN_ECDSASHA256
+#ifdef HAVE_ED25519
+                        || ctx->sig_alg == DKIM_SIGN_EDDSASHA256
+#endif
+       ) {
                nid = NID_sha256;
        }
        else if (ctx->sig_alg == DKIM_SIGN_RSASHA512 ||
@@ -2993,7 +3007,9 @@ rspamd_dkim_check(rspamd_dkim_context_t *ctx,
                GError *err = NULL;
 
                if (ctx->sig_alg == DKIM_SIGN_ECDSASHA256 ||
+#ifdef HAVE_ED25519
                        ctx->sig_alg == DKIM_SIGN_EDDSASHA256 ||
+#endif
                        ctx->sig_alg == DKIM_SIGN_ECDSASHA512) {
                        /* RSA key provided for ECDSA/EDDSA signature */
                        res->rcode = DKIM_PERM_ERROR;
@@ -3086,6 +3102,7 @@ rspamd_dkim_check(rspamd_dkim_context_t *ctx,
                break;
 
        case RSPAMD_DKIM_KEY_EDDSA:
+#ifdef HAVE_ED25519
                if (ctx->sig_alg != DKIM_SIGN_EDDSASHA256) {
                        /* EDDSA key provided for RSA/ECDSA signature */
                        res->rcode = DKIM_PERM_ERROR;
@@ -3117,6 +3134,20 @@ rspamd_dkim_check(rspamd_dkim_context_t *ctx,
                                res->fail_reason = "headers eddsa verify failed";
                        }
                }
+#else
+               /* ED25519 not supported in this OpenSSL version */
+               res->rcode = DKIM_PERM_ERROR;
+               res->fail_reason = "ed25519 signatures are not supported (OpenSSL 1.1.1+ required)";
+               msg_info_dkim(
+                       "%s: ed25519 signatures not supported (OpenSSL 1.1.1+ required); "
+                       "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),
+                       (int) (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);
+#endif
                break;
        }
 
@@ -3359,6 +3390,7 @@ rspamd_dkim_sign_key_load(const char *key, size_t len,
                        nkey->type = RSPAMD_DKIM_KEY_ECDSA;
                        nkey->keylen = EVP_PKEY_size(nkey->specific.key_ssl.key_evp);
                        break;
+#ifdef HAVE_ED25519
                case EVP_PKEY_ED25519:
                        /* For Ed25519, extract the raw key and store it in the eddsa field */
                        nkey->type = RSPAMD_DKIM_KEY_EDDSA;
@@ -3392,18 +3424,31 @@ rspamd_dkim_sign_key_load(const char *key, size_t len,
                        /* 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;
+                       /* Note: we don't zero out the pointers because 'specific' is a union,
+                        * and zeroing key_ssl would overwrite key_eddsa. The cleanup function
+                        * checks the key type and won't touch key_ssl for EDDSA keys. */
                        break;
-               default:
-                       g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
-                                               "unsupported key type: %s",
-                                               OBJ_nid2sn(key_type));
+#endif /* HAVE_ED25519 */
+               default: {
+                       const char *key_type_str = OBJ_nid2sn(key_type);
+#ifndef HAVE_ED25519
+                       /* Check if this is an ED25519 key without support */
+                       if (key_type_str && strcmp(key_type_str, "ED25519") == 0) {
+                               g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+                                                       "ed25519 keys are not supported (OpenSSL 1.1.1+ required)");
+                       }
+                       else
+#endif
+                       {
+                               g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYFAIL,
+                                                       "unsupported key type: %s",
+                                                       key_type_str ? key_type_str : "unknown");
+                       }
                        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"),
@@ -3766,12 +3811,14 @@ rspamd_dkim_sign(struct rspamd_task *task, const char *selector,
                        return NULL;
                }
        }
+#ifdef HAVE_ED25519
        else if (ctx->key->type == RSPAMD_DKIM_KEY_EDDSA) {
                sig_len = crypto_sign_bytes();
                sig_buf = g_alloca(sig_len);
 
                rspamd_cryptobox_sign(sig_buf, NULL, raw_digest, dlen, ctx->key->specific.key_eddsa);
        }
+#endif
        else {
                g_string_free(hdr, true);
                msg_err_task("unsupported key type for signing");
@@ -3809,6 +3856,7 @@ bool rspamd_dkim_match_keys(rspamd_dkim_key_t *pk,
                return false;
        }
 
+#ifdef HAVE_ED25519
        if (pk->type == RSPAMD_DKIM_KEY_EDDSA) {
                if (memcmp(sk->specific.key_eddsa + 32, pk->specific.key_eddsa, 32) != 0) {
                        g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH,
@@ -3816,19 +3864,20 @@ bool rspamd_dkim_match_keys(rspamd_dkim_key_t *pk,
                        return false;
                }
        }
+       else
+#endif
+       {
 #if OPENSSL_VERSION_MAJOR >= 3
-       else if (EVP_PKEY_eq(pk->specific.key_ssl.key_evp, sk->specific.key_ssl.key_evp) != 1) {
-               g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH,
-                                       "pubkey does not match private key");
-               return false;
-       }
+               if (EVP_PKEY_eq(pk->specific.key_ssl.key_evp, sk->specific.key_ssl.key_evp) != 1)
 #else
-       else if (EVP_PKEY_cmp(pk->specific.key_ssl.key_evp, sk->specific.key_ssl.key_evp) != 1) {
-               g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH,
-                                       "pubkey does not match private key");
-               return false;
-       }
+               if (EVP_PKEY_cmp(pk->specific.key_ssl.key_evp, sk->specific.key_ssl.key_evp) != 1)
 #endif
+               {
+                       g_set_error(err, dkim_error_quark(), DKIM_SIGERROR_KEYHASHMISMATCH,
+                                               "pubkey does not match private key");
+                       return false;
+               }
+       }
 
        return true;
 }
index c182f177117d90088bc3b0d70b7223b36d139b20..a8177d7277f61bc2df118b1793ee8af95f6a54be 100644 (file)
@@ -59,7 +59,7 @@ DKIM Verify ED25519 REJECT
   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
+  ${result} =  Scan Message With Rspamc  ${RSPAMD_TESTDIR}/messages/spam_message.eml  --mime  --header=dodkim=ed25519
   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
deleted file mode 100644 (file)
index 72fc517..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-.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";
index 50712d17b27807a2695036480e73b78ad9b73bf5..3e2692460760a675e524f77d9f61a9a36c23c325 100644 (file)
@@ -46,8 +46,17 @@ sign_condition =<<EOD
 return function(task)
   local dodkim = task:get_request_header('dodkim')
   if not dodkim then return end
+
+  dodkim = tostring(dodkim)
+  local key_file = "{= env.TESTDIR =}/configs/dkim.key"
+
+  -- Check if we should use ed25519 key
+  if dodkim == "ed25519" then
+    key_file = "{= env.TESTDIR =}/configs/dkim-eddsa-pem.key"
+  end
+
   return {
-    key = "{= env.TESTDIR =}/configs/dkim.key",
+    key = key_file,
     domain = "cacophony.za.org",
     selector = "dkim"
   }