From: Vsevolod Stakhov Date: Sat, 4 Oct 2025 14:06:27 +0000 (+0100) Subject: [Feature] Add ED25519 support for DKIM signing with OpenSSL version checks X-Git-Tag: 3.13.2~4^2~1 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=9e45beec7cd3bff905ba207a8b4e2a4ff5f22ca7;p=thirdparty%2Frspamd.git [Feature] Add ED25519 support for DKIM signing with OpenSSL version checks 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. --- diff --git a/cmake/CheckSystemFeatures.cmake b/cmake/CheckSystemFeatures.cmake index 0a15749fd2..0ff7cbb21e 100644 --- a/cmake/CheckSystemFeatures.cmake +++ b/cmake/CheckSystemFeatures.cmake @@ -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) diff --git a/config.h.in b/config.h.in index f9d910d681..bad7f1f221 100644 --- a/config.h.in +++ b/config.h.in @@ -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 diff --git a/src/libserver/dkim.c b/src/libserver/dkim.c index b1bcda099a..9587176ce2 100644 --- a/src/libserver/dkim.c +++ b/src/libserver/dkim.c @@ -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; } diff --git a/test/functional/cases/116_dkim.robot b/test/functional/cases/116_dkim.robot index c182f17711..a8177d7277 100644 --- a/test/functional/cases/116_dkim.robot +++ b/test/functional/cases/116_dkim.robot @@ -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 index 72fc517224..0000000000 --- a/test/functional/configs/dkim-ed25519-pem.conf +++ /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 =<