resolved: add ML-DSA-44 DNSSEC verification

Classical DNSSEC algorithms such as RSA and ECDSA are vulnerable to
quantum computers. Add support for post-quantum ML-DSA-44, DNSSEC
algorithm 18.

Verify pure ML-DSA through OpenSSL. Report the algorithm as unsupported
only when it is unavailable. Treat key import and verification failures
as hard errors.

Add the draft test vector and negative tests.

https://datatracker.ietf.org/doc/draft-westerbaan-dnssec-mldsa/

Signed-off-by: Ayoub Zaki <ayoub.zaki@embetrix.com>
This commit is contained in:
Ayoub Zaki
2026-09-24 09:23:52 +02:00
committed by Luca Boccassi
parent bc5de99fd0
commit 2d6bc0328c
7 changed files with 388 additions and 0 deletions
+4
View File
@@ -144,6 +144,10 @@ executables += [
'sources' : files('test-dnssec-crypto.c'),
'conditions' : ['HAVE_OPENSSL'],
},
resolve_test_template + {
'sources' : files('test-dnssec-mldsa.c'),
'conditions' : ['HAVE_OPENSSL'],
},
resolve_test_template + {
'sources' : files('test-dns-search-domain.c'),
},
+79
View File
@@ -20,6 +20,8 @@
#define VERIFY_RRS_MAX 256
#define MAX_KEY_SIZE (32*1024)
#define MLDSA44_PUBLIC_KEY_SIZE 1312
#define MLDSA44_SIGNATURE_SIZE 2420
/* Permit a maximum clock skew of 1h 10min. This should be enough to deal with DST confusion */
#define SKEW_MAX (1*USEC_PER_HOUR + 10*USEC_PER_MINUTE)
@@ -399,6 +401,81 @@ static int dnssec_eddsa_verify(
dnskey->dnskey.key, key_size);
}
static int dnssec_mldsa_verify(
const void *data,
size_t data_size,
DnsResourceRecord *rrsig,
DnsResourceRecord *dnskey) {
int r;
assert(data);
assert(data_size > 0);
assert(rrsig);
assert(dnskey);
if (dnskey->dnskey.key_size != MLDSA44_PUBLIC_KEY_SIZE)
return -EINVAL;
if (rrsig->rrsig.signature_size != MLDSA44_SIGNATURE_SIZE)
return -EINVAL;
/* Capability detection must only consider errors from this operation. */
sym_ERR_clear_error();
_cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *kctx = sym_EVP_PKEY_CTX_new_from_name(
/* libctx= */ NULL,
"ML-DSA-44",
/* propquery= */ NULL);
if (!kctx)
return log_openssl_errors(LOG_DEBUG, "Failed to create ML-DSA-44 key context");
if (sym_EVP_PKEY_fromdata_init(kctx) <= 0)
return dnssec_verify_errno(
log_openssl_errors(LOG_DEBUG, "Failed to initialize ML-DSA-44 key creation"));
OSSL_PARAM params[] = {
sym_OSSL_PARAM_construct_octet_string(
OSSL_PKEY_PARAM_PUB_KEY, dnskey->dnskey.key, dnskey->dnskey.key_size),
sym_OSSL_PARAM_construct_end(),
};
_cleanup_(EVP_PKEY_freep) EVP_PKEY *evkey = NULL;
if (sym_EVP_PKEY_fromdata(kctx, &evkey, EVP_PKEY_PUBLIC_KEY, params) <= 0)
return dnssec_verify_errno(
log_openssl_errors(LOG_DEBUG, "Failed to load ML-DSA-44 public key from raw data"));
_cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *ctx = sym_EVP_MD_CTX_new();
if (!ctx)
return -ENOMEM;
/* ML-DSA DNSSEC uses pure ML-DSA with the default empty context, not HashML-DSA */
if (sym_EVP_DigestVerifyInit(
ctx,
/* pctx= */ NULL,
/* type= */ NULL,
/* engine= */ NULL,
evkey) <= 0)
return dnssec_verify_errno(
log_openssl_errors(LOG_DEBUG, "Failed to initialize ML-DSA-44 verification"));
r = sym_EVP_DigestVerify(
ctx,
rrsig->rrsig.signature,
rrsig->rrsig.signature_size,
data,
data_size);
if (r < 0)
return dnssec_verify_errno(
log_openssl_errors(LOG_DEBUG, "ML-DSA-44 signature verification failed"));
if (r == 0)
/* An invalid signature is an expected outcome but leaves entries in the thread-local
* OpenSSL error queue. Clear them so a later failure translates its own error */
sym_ERR_clear_error();
return r;
}
static int md_add_uint8(EVP_MD_CTX *ctx, uint8_t v) {
return sym_EVP_DigestUpdate(ctx, &v, sizeof(v));
}
@@ -664,6 +741,8 @@ static int dnssec_rrset_verify_sig(
dnskey));
case DNSSEC_ALGORITHM_ED448:
return -EOPNOTSUPP;
case DNSSEC_ALGORITHM_MLDSA44:
return dnssec_mldsa_verify(sig_data, sig_size, rrsig, dnskey);
default:
/* OK, the RRs are now in canonical order. Let's calculate the digest */
md_algorithm = algorithm_to_implementation_id(rrsig->rrsig.algorithm);
+219
View File
@@ -0,0 +1,219 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "crypto-util.h"
#include "dns-answer.h"
#include "dns-rr.h"
#include "fileio.h"
#include "hexdecoct.h"
#include "resolved-dns-dnssec.h"
#include "tests.h"
#include "time-util.h"
static void* load_base64_test_vector(const char *name, size_t *ret_size) {
_cleanup_free_ char *encoded = NULL, *path = NULL;
void *decoded = NULL;
assert(name);
assert(ret_size);
assert_se(get_testdata_dir(name, &path) >= 0);
assert_se(read_full_file(path, &encoded, /* ret_size= */ NULL) >= 0);
assert_se(unbase64mem(encoded, &decoded, ret_size) >= 0);
return decoded;
}
static void mldsa44_fixture_new(
DnsResourceRecord **ret_mx,
DnsResourceRecord **ret_rrsig,
DnsResourceRecord **ret_dnskey,
DnsAnswer **ret_answer) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *mx = NULL, *rrsig = NULL;
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
assert(ret_mx);
assert(ret_rrsig);
assert(ret_dnskey);
assert(ret_answer);
/* Deterministic example from Section 6 of draft-westerbaan-dnssec-mldsa-04. */
assert_se(dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "example.com."));
dnskey->dnskey.flags = 257;
dnskey->dnskey.protocol = 3;
dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_MLDSA44;
dnskey->dnskey.key = load_base64_test_vector(
"test-resolve/mldsa44-dnskey.b64", &dnskey->dnskey.key_size);
assert_se(dnskey->dnskey.key_size == 1312);
assert_se(mx = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_MX, "example.com."));
mx->ttl = 3600;
mx->mx.priority = 10;
assert_se(mx->mx.exchange = strdup("mail.example.com."));
assert_se(rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "example.com."));
rrsig->ttl = 3600;
rrsig->rrsig.type_covered = DNS_TYPE_MX;
rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_MLDSA44;
rrsig->rrsig.labels = 2;
rrsig->rrsig.original_ttl = 3600;
rrsig->rrsig.expiration = 1440021600;
rrsig->rrsig.inception = 1438207200;
rrsig->rrsig.key_tag = 59829;
assert_se(rrsig->rrsig.signer = strdup("example.com."));
rrsig->rrsig.signature = load_base64_test_vector(
"test-resolve/mldsa44-rrsig.b64", &rrsig->rrsig.signature_size);
assert_se(rrsig->rrsig.signature_size == 2420);
assert_se(dnssec_keytag(dnskey, false) == rrsig->rrsig.key_tag);
assert_se(dnssec_key_match_rrsig(mx->key, rrsig) > 0);
assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0);
assert_se(answer = dns_answer_new(1));
assert_se(dns_answer_add(answer, mx, 0, DNS_ANSWER_AUTHENTICATED, /* rrsig= */ NULL) >= 0);
*ret_mx = TAKE_PTR(mx);
*ret_rrsig = TAKE_PTR(rrsig);
*ret_dnskey = TAKE_PTR(dnskey);
*ret_answer = TAKE_PTR(answer);
}
static bool mldsa44_available(void) {
sym_ERR_clear_error();
_cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = sym_EVP_PKEY_CTX_new_from_name(
/* libctx= */ NULL, "ML-DSA-44", /* propquery= */ NULL);
if (ctx)
return true;
ASSERT_ERROR(log_openssl_errors(LOG_DEBUG, "ML-DSA-44 capability check failed"), EOPNOTSUPP);
return false;
}
TEST(dnssec_mldsa44_verify) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *mx = NULL, *rrsig = NULL;
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_free_ char *algorithm = NULL;
DnssecResult result;
ASSERT_OK(dnssec_algorithm_to_string_alloc(DNSSEC_ALGORITHM_MLDSA44, &algorithm));
ASSERT_STREQ(algorithm, "MLDSA44");
mldsa44_fixture_new(&mx, &rrsig, &dnskey, &answer);
bool available = mldsa44_available();
ASSERT_OK(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey, rrsig->rrsig.inception * USEC_PER_SEC, &result));
if (!available) {
ASSERT_EQ(result, DNSSEC_UNSUPPORTED_ALGORITHM);
return (void) log_tests_skipped("OpenSSL does not provide ML-DSA-44");
}
ASSERT_EQ(result, DNSSEC_VALIDATED);
mx->mx.priority++;
dns_resource_record_clear_wire_format(mx);
ASSERT_OK(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey, rrsig->rrsig.inception * USEC_PER_SEC, &result));
ASSERT_EQ(result, DNSSEC_INVALID);
mx->mx.priority--;
dns_resource_record_clear_wire_format(mx);
((uint8_t*) rrsig->rrsig.signature)[0] ^= 1;
ASSERT_OK(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey, rrsig->rrsig.inception * USEC_PER_SEC, &result));
ASSERT_EQ(result, DNSSEC_INVALID);
}
TEST(dnssec_mldsa44_corrupted_key) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *mx = NULL, *rrsig = NULL;
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
DnssecResult result;
mldsa44_fixture_new(&mx, &rrsig, &dnskey, &answer);
if (!mldsa44_available())
return (void) log_tests_skipped("OpenSSL does not provide ML-DSA-44");
ASSERT_OK(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey, rrsig->rrsig.inception * USEC_PER_SEC, &result));
ASSERT_EQ(result, DNSSEC_VALIDATED);
((uint8_t*) dnskey->dnskey.key)[0] ^= 1;
ASSERT_EQ(dnskey->dnskey.key_size, (size_t) 1312);
ASSERT_OK(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey, rrsig->rrsig.inception * USEC_PER_SEC, &result));
ASSERT_EQ(result, DNSSEC_INVALID);
}
TEST(dnssec_mldsa44_sizes) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *mx = NULL, *rrsig = NULL;
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
DnssecResult result;
mldsa44_fixture_new(&mx, &rrsig, &dnskey, &answer);
dnskey->dnskey.key_size--;
ASSERT_ERROR(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey,
rrsig->rrsig.inception * USEC_PER_SEC, &result), EINVAL);
dnskey->dnskey.key_size++;
rrsig->rrsig.signature_size--;
ASSERT_ERROR(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey,
rrsig->rrsig.inception * USEC_PER_SEC, &result), EINVAL);
}
static int fail_key_import(EVP_PKEY_CTX *ctx, EVP_PKEY **pkey, int selection, OSSL_PARAM params[]) {
/* Generate a real unsupported-algorithm error after capability detection has succeeded. */
_cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *unsupported = sym_EVP_PKEY_CTX_new_from_name(
/* libctx= */ NULL, "systemd-test-nonexistent-algorithm", /* propquery= */ NULL);
ASSERT_NULL(unsupported);
return 0;
}
TEST(dnssec_mldsa44_import_failure) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *mx = NULL, *rrsig = NULL;
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
DnssecResult result;
int r;
if (!mldsa44_available())
return (void) log_tests_skipped("OpenSSL does not provide ML-DSA-44");
mldsa44_fixture_new(&mx, &rrsig, &dnskey, &answer);
typeof(sym_EVP_PKEY_fromdata) saved_import = sym_EVP_PKEY_fromdata;
sym_EVP_PKEY_fromdata = fail_key_import;
r = dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey, rrsig->rrsig.inception * USEC_PER_SEC, &result);
sym_EVP_PKEY_fromdata = saved_import;
ASSERT_ERROR(r, EIO);
}
TEST(dnssec_mldsa44_stale_errors) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *mx = NULL, *rrsig = NULL;
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
DnssecResult result;
mldsa44_fixture_new(&mx, &rrsig, &dnskey, &answer);
bool available = mldsa44_available();
/* Leave an unrelated key-import error in OpenSSL's queue. */
_cleanup_(EVP_PKEY_freep) EVP_PKEY *bad = sym_EVP_PKEY_new_raw_public_key(
EVP_PKEY_ED25519, /* engine= */ NULL, dnskey->dnskey.key, 1);
ASSERT_NULL(bad);
ASSERT_OK(dnssec_verify_rrset(
answer, mx->key, rrsig, dnskey, rrsig->rrsig.inception * USEC_PER_SEC, &result));
ASSERT_EQ(result, available ? DNSSEC_VALIDATED : DNSSEC_UNSUPPORTED_ALGORITHM);
ASSERT_EQ(sym_ERR_get_error(), 0UL);
}
static int intro(void) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return EXIT_TEST_SKIP;
return EXIT_SUCCESS;
}
DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
+1
View File
@@ -2643,6 +2643,7 @@ static const char* const dnssec_algorithm_table[_DNSSEC_ALGORITHM_MAX_DEFINED] =
[DNSSEC_ALGORITHM_ECDSAP384SHA384] = "ECDSAP384SHA384",
[DNSSEC_ALGORITHM_ED25519] = "ED25519",
[DNSSEC_ALGORITHM_ED448] = "ED448",
[DNSSEC_ALGORITHM_MLDSA44] = "MLDSA44",
[DNSSEC_ALGORITHM_INDIRECT] = "INDIRECT",
[DNSSEC_ALGORITHM_PRIVATEDNS] = "PRIVATEDNS",
[DNSSEC_ALGORITHM_PRIVATEOID] = "PRIVATEOID",
+1
View File
@@ -34,6 +34,7 @@ enum {
DNSSEC_ALGORITHM_ECDSAP384SHA384 = 14, /* RFC 6605 */
DNSSEC_ALGORITHM_ED25519 = 15, /* RFC 8080 */
DNSSEC_ALGORITHM_ED448 = 16, /* RFC 8080 */
DNSSEC_ALGORITHM_MLDSA44 = 18, /* draft-westerbaan-dnssec-mldsa */
DNSSEC_ALGORITHM_INDIRECT = 252,
DNSSEC_ALGORITHM_PRIVATEDNS,
DNSSEC_ALGORITHM_PRIVATEOID,
+30
View File
@@ -0,0 +1,30 @@
17K0clSq4NtF55MNSpjSyX2PE5fReJ2voXAksxbpvslPyZRtQvGbeadBO7qj
PnFJy0LtURVpOsBB+suYit61/g4dhjEYSZW1ksOX0ilOLhT5CqQUujgmiZrE
P0zMrLwm6agyuVEY1ctDPL75ZgsAE44IF/YediyidMNq1VTrIqrBFi5KsBrL
oeOMTv2PgLZbMz0PcuVd/nHOnB67mInnxWEGwP1zgDoq7P6v3teqPLLO2lTR
K9jNNqeM+XWUO0er0l6ICsRS5XQu0ejRqCr6huWQx1jBWuTShA2SvKGlCQ9A
SWWX/KfYuVE/GhvabpUKqpjeRnUH1KT1pPBZkhZYLDVy9i7aiQWrNYFnDEoC
d3oz4Mpylf2PT/bRoKOnaD1l9fX3/GDaAj6CbF+SFEwC99G6EHWYdVPqk2f8
122ZC3+pnNRa/biDbUPkWfUYffBYR5cJoB6mg1k1+nBGCZDNPcG6QBupS6sd
3kGsZ6szGdysoGBI1MTu8n7hOpwX0FOPQw8tZC3CQVZg3niHfY2KvHJSOXjA
QuQoX0MZhGxEEmJCl2hEwQ5Va6IVtacZ5Z0MayqW05hZBx/cws3nUkp77a5U
6FsxjoVOj+Ky8+36yXGRKCcKr9HlBEw6T9r9n/MfkHhLjo5FlhRKDa9YZRHT
2ZYrnqla8Ze05fxg8rHtFd46W+9fib3HnZEFHZsoFudPpUUx79wcvnTUSIV/
R2vNWPIcC2U7O3ak4HamVZowJxhVXMY/dIWaq6uSXwI4YcqM0Pe62yhx9n1V
Mm10URNa1F9KG6aRGPuyyKMO7JOS7z+XcGbJrdXHEMxkexUU0hfZWMcBfD6Q
/SDATmdLkEhuk3CjGgAdMvRzl55JBnSefkd/oLdFCPil8jeDErg8Jb04jKCw
//dHi69CtxZn7arJfEaxKWQ+WG5bBVoMIRlG1PNuZ1vtWGD6BCoxXZgmFk1q
kjfDWl+/SVSQpb1N8ki5XEqud4S2BWcxZqxCRbW0sIKgnpMj5i8geMW3Z4NE
be/XNq06NwLUmwiYRJAKYYMzl7xEGbMNepegs4fBkRR0xNQbU+Mql3rLbw6n
XbZbs55Z5wHnaVfe9vLURVnDGncSK1IE47XCGfFoixTtC8C4AbPm6C3NQ+nA
6fQXRM2YFb0byIINi7Ej8E+s0bG2hd1aKxuNu/PtkzZw8JWhgLTxktCLELj6
u9/MKyRRjjLuoKXgyQTKhEeACD87DNLQuLavZ7w1W5SUAl3HsKePqA46Lb/r
UTKIUdYHgZjpSTZRrnh+wCUfkiujDp9R32Km1yeEzz3SBTkxdt+jJKUSvZSX
CjbdNKUUqGeR8Os28BRbCatkZRtKAxOymWEaKhxIiRYnWYdooxFAYLpEQ0ht
9RUioc6IswmFwhb45u0XjdVnswSg1Mr7qIKig0LxepqiauWNtjAIPSw1j99W
bD9dYqQoVnvJ6ozpXKoPNUdLC/qPM5olCrTfzyCDvo7vvBBV4Y/hU3DuyyYF
Ztg/8GshGq7EPKKbVMzQD4gVokZe8LRlFcx+QfMSTwnv/3OTCatYspoUWaAL
zlA46TjJZ49y6w5O5f2q5m2fhXP8l/xCtJWfS/i2HXhDPoawM11ukZHE2L9I
ezkFwQjP1qwksM633LfPUfhNDtaHuV6uscUzwG8NlwI9kqcIJYN7Wbpst9Tl
awqHwgOGKujzFbpZJejt76Z5NpoiAnZhUfFqll+fgeznbMBwtVhp5NuXhM8F
yDCzJCyDEg==
+54
View File
@@ -0,0 +1,54 @@
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