Browse Source

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 <v@rspamd.com>
pull/5664/head
Vsevolod Stakhov 2 weeks ago
parent
commit
0da40ce40b
No known key found for this signature in database GPG Key ID: 7647B6790081437
  1. 67
      src/libserver/dkim.c
  2. 6
      test/functional/cases/116_dkim.robot
  3. 66
      test/functional/configs/dkim-ed25519-pem.conf
  4. 3
      test/functional/configs/dkim-eddsa-pem.key

67
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);

6
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}

66
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 =<<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";

3
test/functional/configs/dkim-eddsa-pem.key

@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIPwynGQ23nIwwKJYgLULTADkL+L4fAPEtSWjNYA6PPSo
-----END PRIVATE KEY-----
Loading…
Cancel
Save