From 2d6bc0328c0139c772f65b9c17a10eb4435bd16c Mon Sep 17 00:00:00 2001 From: Ayoub Zaki Date: Sat, 12 Sep 2026 12:03:31 +0200 Subject: [PATCH] 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 --- src/resolve/meson.build | 4 + src/resolve/resolved-dns-dnssec.c | 79 ++++++++++ src/resolve/test-dnssec-mldsa.c | 219 +++++++++++++++++++++++++++ src/shared/dns-rr.c | 1 + src/shared/dns-rr.h | 1 + test/test-resolve/mldsa44-dnskey.b64 | 30 ++++ test/test-resolve/mldsa44-rrsig.b64 | 54 +++++++ 7 files changed, 388 insertions(+) create mode 100644 src/resolve/test-dnssec-mldsa.c create mode 100644 test/test-resolve/mldsa44-dnskey.b64 create mode 100644 test/test-resolve/mldsa44-rrsig.b64 diff --git a/src/resolve/meson.build b/src/resolve/meson.build index b246c0c402..bcf7bf4833 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -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'), }, diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c index 0e234111d3..693cbf672e 100644 --- a/src/resolve/resolved-dns-dnssec.c +++ b/src/resolve/resolved-dns-dnssec.c @@ -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); diff --git a/src/resolve/test-dnssec-mldsa.c b/src/resolve/test-dnssec-mldsa.c new file mode 100644 index 0000000000..d19dea3423 --- /dev/null +++ b/src/resolve/test-dnssec-mldsa.c @@ -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); diff --git a/src/shared/dns-rr.c b/src/shared/dns-rr.c index 26d2e3934a..5e3983b554 100644 --- a/src/shared/dns-rr.c +++ b/src/shared/dns-rr.c @@ -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", diff --git a/src/shared/dns-rr.h b/src/shared/dns-rr.h index a8199cdc44..63f60e1f66 100644 --- a/src/shared/dns-rr.h +++ b/src/shared/dns-rr.h @@ -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, diff --git a/test/test-resolve/mldsa44-dnskey.b64 b/test/test-resolve/mldsa44-dnskey.b64 new file mode 100644 index 0000000000..2bf62b4e4d --- /dev/null +++ b/test/test-resolve/mldsa44-dnskey.b64 @@ -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== diff --git a/test/test-resolve/mldsa44-rrsig.b64 b/test/test-resolve/mldsa44-rrsig.b64 new file mode 100644 index 0000000000..0ec289665b --- /dev/null +++ b/test/test-resolve/mldsa44-rrsig.b64 @@ -0,0 +1,54 @@ +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