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