Commit 20378a1980 for bind
commit 20378a19800954d609873bc5a076ec6540cfec5f
Author: OndÅ™ej Surý <ondrej@isc.org>
Date: Wed Dec 31 12:06:11 2025 +0100
Add ML-DSA-44 support for DNSSEC
Implement draft-westerbaan-dnssec-mldsa-04 with OpenSSL so MLDSA44 keys
and signatures interoperate using the assigned algorithm number and
pure ML-DSA with an empty context. Exercise the published example and
signing, validation, and malformed inputs in unit and system tests.
diff --git a/REUSE.toml b/REUSE.toml
index d5a03a653b..7aa1ff9a25 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -210,3 +210,14 @@ path = [
precedence = "aggregate"
SPDX-FileCopyrightText = "Internet Systems Consortium, Inc. (\"ISC\")"
SPDX-License-Identifier = "CC0-1.0"
+
+[[annotations]]
+path = [
+ "tests/dns/testdata/dst/Kexample.com.+018+59829.key",
+ "tests/dns/testdata/dst/Kexample.com.+018+59829.private",
+ "tests/dns/testdata/dst/mldsa.data",
+ "tests/dns/testdata/dst/mldsa.sig"
+]
+precedence = "override"
+SPDX-FileCopyrightText = "2026 IETF Trust and the persons identified as the document authors"
+SPDX-License-Identifier = "BSD-3-Clause"
diff --git a/bin/dnssec/dnssec-keygen.c b/bin/dnssec/dnssec-keygen.c
index e2cb16d4e7..d029e66d91 100644
--- a/bin/dnssec/dnssec-keygen.c
+++ b/bin/dnssec/dnssec-keygen.c
@@ -146,7 +146,8 @@ usage(int ret) {
}
fprintf(stderr, " RSASHA256 | RSASHA512 |\n");
fprintf(stderr, " ECDSAP256SHA256 | ECDSAP384SHA384 |\n");
- fprintf(stderr, " ED25519 | ED448\n");
+ fprintf(stderr, " ED25519 | ED448 |\n");
+ fprintf(stderr, " MLDSA44\n");
fprintf(stderr, " -3: use NSEC3-capable algorithm\n");
fprintf(stderr, " -b <key size in bits>:\n");
if (!isc_crypto_fips_mode()) {
@@ -162,6 +163,7 @@ usage(int ret) {
fprintf(stderr, " ECDSAP384SHA384:\tignored\n");
fprintf(stderr, " ED25519:\tignored\n");
fprintf(stderr, " ED448:\tignored\n");
+ fprintf(stderr, " MLDSA44:\tignored\n");
fprintf(stderr, " (key size defaults are set according to\n"
" algorithm and usage (ZSK or KSK)\n");
fprintf(stderr, " -c <class>: (default: IN)\n");
@@ -288,6 +290,7 @@ keygen(keygen_ctx_t *ctx, const char *keyname) {
case DST_ALG_ECDSA384:
case DST_ALG_ED25519:
case DST_ALG_ED448:
+ case DST_ALG_MLDSA44:
break;
default:
fatal("algorithm %s is incompatible with NSEC3"
@@ -322,6 +325,7 @@ keygen(keygen_ctx_t *ctx, const char *keyname) {
case DST_ALG_ECDSA384:
case DST_ALG_ED25519:
case DST_ALG_ED448:
+ case DST_ALG_MLDSA44:
break;
default:
fatal("key size not specified (-b option)");
@@ -492,6 +496,9 @@ keygen(keygen_ctx_t *ctx, const char *keyname) {
case DST_ALG_ED448:
ctx->size = 456;
break;
+ case DST_ALG_MLDSA44:
+ ctx->size = DNS_KEY_MLDSA44SIZE * 8;
+ break;
default:
fatal("not a dnskey algorithm %u\n", ctx->alg);
}
@@ -512,6 +519,7 @@ keygen(keygen_ctx_t *ctx, const char *keyname) {
case DST_ALG_ECDSA384:
case DST_ALG_ED25519:
case DST_ALG_ED448:
+ case DST_ALG_MLDSA44:
show_progress = true;
break;
default:
diff --git a/bin/dnssec/dnssec-keygen.rst b/bin/dnssec/dnssec-keygen.rst
index 19b3e93f1f..e6486dc4e5 100644
--- a/bin/dnssec/dnssec-keygen.rst
+++ b/bin/dnssec/dnssec-keygen.rst
@@ -49,7 +49,7 @@ Options
This option selects the cryptographic algorithm. For DNSSEC keys,
the value of ``algorithm`` must be one of RSASHA1 (deprecated),
NSEC3RSASHA1 (deprecated), RSASHA256, RSASHA512, ECDSAP256SHA256,
- ECDSAP384SHA384, ED25519, or ED448.
+ ECDSAP384SHA384, ED25519, ED448, or MLDSA44.
These values are case-insensitive. In some cases, abbreviations
are supported, such as ECDSA256 for ECDSAP256SHA256 and ECDSA384
diff --git a/bin/dnssec/dnssec-ksr.c b/bin/dnssec/dnssec-ksr.c
index 6e3f15a2b3..66c9c43a35 100644
--- a/bin/dnssec/dnssec-ksr.c
+++ b/bin/dnssec/dnssec-ksr.c
@@ -353,6 +353,9 @@ create_key(ksr_ctx_t *ksr, dns_kasp_t *kasp, dns_kasp_key_t *kaspkey,
case DST_ALG_ED448:
ksr->size = 456;
break;
+ case DST_ALG_MLDSA44:
+ ksr->size = DNS_KEY_MLDSA44SIZE * 8;
+ break;
default:
show_progress = false;
break;
@@ -512,7 +515,7 @@ print_rdata(dns_rdataset_t *rrset) {
isc_buffer_t target;
isc_region_t r;
isc_result_t result;
- char buf[4096];
+ char buf[65536];
isc_buffer_init(&target, buf, sizeof(buf));
result = dns_rdataset_totext(rrset, name, false, false, &target);
diff --git a/bin/tests/system/isctest/algorithms.py b/bin/tests/system/isctest/algorithms.py
index d833e39734..104b91b224 100644
--- a/bin/tests/system/isctest/algorithms.py
+++ b/bin/tests/system/isctest/algorithms.py
@@ -38,6 +38,7 @@ ECDSAP256SHA256 = Algorithm("ECDSAP256SHA256", 13, 13, 256)
ECDSAP384SHA384 = Algorithm("ECDSAP384SHA384", 14, 14, 384)
ED25519 = Algorithm("ED25519", 15, 15, 256)
ED448 = Algorithm("ED448", 16, 16, 456)
+MLDSA44 = Algorithm("MLDSA44", 18, 18, 1312 * 8)
RSASHA256OID = Algorithm("RSASHA256OID", 254, 256, 2048)
RSASHA512OID = Algorithm("RSASHA512OID", 254, 257, 2048)
@@ -50,6 +51,7 @@ ALL_ALGORITHMS = [
ECDSAP384SHA384,
ED25519,
ED448,
+ MLDSA44,
RSASHA256OID,
RSASHA512OID,
]
diff --git a/bin/tests/system/isctest/vars/algorithms.py b/bin/tests/system/isctest/vars/algorithms.py
index 6b6e9b3c7d..e061c27e2d 100644
--- a/bin/tests/system/isctest/vars/algorithms.py
+++ b/bin/tests/system/isctest/vars/algorithms.py
@@ -141,6 +141,7 @@ CRYPTO_SUPPORTED_VARS = {
"ECDSAP384SHA384_SUPPORTED": "0",
"ED25519_SUPPORTED": "0",
"ED448_SUPPORTED": "0",
+ "MLDSA44_SUPPORTED": "0",
"RSASHA256OID_SUPPORTED": "0",
"RSASHA512OID_SUPPORTED": "0",
}
diff --git a/bin/tests/system/mldsa/ns1/named.conf.j2 b/bin/tests/system/mldsa/ns1/named.conf.j2
new file mode 100644
index 0000000000..2febbc8596
--- /dev/null
+++ b/bin/tests/system/mldsa/ns1/named.conf.j2
@@ -0,0 +1,8 @@
+options {
+ {% include_indented "_common/options.conf.j2" %}
+ recursion no;
+ dnssec-validation no;
+};
+
+{% include "_common/controls.conf.j2" %}
+{% include "_common/zones.conf.j2" %}
diff --git a/bin/tests/system/mldsa/ns2/named.conf.j2 b/bin/tests/system/mldsa/ns2/named.conf.j2
new file mode 100644
index 0000000000..4d958e5bc5
--- /dev/null
+++ b/bin/tests/system/mldsa/ns2/named.conf.j2
@@ -0,0 +1,9 @@
+options {
+ {% include_indented "_common/options.conf.j2" %}
+ recursion yes;
+ dnssec-validation yes;
+};
+
+{% include "_common/controls.conf.j2" %}
+{% include "_common/trusted.conf.j2" %}
+{% include "_common/root.hint.conf" %}
diff --git a/bin/tests/system/mldsa/tests_mldsa.py b/bin/tests/system/mldsa/tests_mldsa.py
new file mode 100644
index 0000000000..91bf34784b
--- /dev/null
+++ b/bin/tests/system/mldsa/tests_mldsa.py
@@ -0,0 +1,164 @@
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+from pathlib import Path
+
+import base64
+import os
+import shutil
+
+import dns.flags
+import dns.rdata
+import dns.rdatatype
+import pytest
+
+from isctest.algorithms import MLDSA44
+from isctest.template import NS1, zones
+from isctest.zone import FileZoneKey, Zone
+
+import isctest
+import isctest.mark
+
+pytestmark = [
+ isctest.mark.with_algorithm("MLDSA44"),
+ pytest.mark.extra_artifacts(
+ [
+ "ns*/dsset-*",
+ "ns*/zones/*.db",
+ "ns*/zones/*.db.signed",
+ ]
+ ),
+]
+
+
+def bootstrap():
+ # configure_root() signs with the default algorithm, so drive the steps
+ # here to sign the root zone with ML-DSA-44 keys
+ root = Zone(".", NS1, signed=True)
+ root.keys = [
+ FileZoneKey.generate(root, "-f KSK", alg=MLDSA44),
+ FileZoneKey.generate(root, alg=MLDSA44),
+ ]
+ root.render(template="_common/zones/root.db.j2.manual")
+ root.sign()
+
+ return {
+ "trust_anchors": root.trust_anchors(),
+ "zones": zones([root]),
+ }
+
+
+def test_mldsa_keygen_seed():
+ """
+ dnssec-keygen stores the 32-byte seed as the ML-DSA-44 private key.
+ """
+ privates = list(Path("ns1/keys").glob("K*.private"))
+ assert len(privates) == 2
+ for private in privates:
+ seed = next(
+ line.split()[1]
+ for line in private.read_text(encoding="ascii").splitlines()
+ if line.startswith("PrivateKey:")
+ )
+ assert len(base64.b64decode(seed)) == 32
+
+
+def test_mldsa_verify():
+ isctest.run.EnvCmd("VERIFY")("-o . zones/root.db.signed", cwd="ns1")
+
+
+def test_mldsa_authoritative_and_resolver(ns1, ns2):
+ msg = isctest.query.create(".", "SOA")
+ authoritative = isctest.query.tcp(msg, ns1.ip)
+ validated = isctest.query.tcp(msg, ns2.ip)
+ isctest.check.noerror(authoritative)
+ isctest.check.same_answer(authoritative, validated)
+ isctest.check.adflag(validated)
+ rrsigs = [
+ rrset for rrset in validated.answer if rrset.rdtype == dns.rdatatype.RRSIG
+ ]
+ assert rrsigs
+ for rrset in rrsigs:
+ for signature in rrset:
+ assert signature.algorithm == MLDSA44.number
+ assert len(signature.signature) == 2420
+
+
+def test_mldsa_udp_truncation(ns1, ns2):
+ msg = isctest.query.create(".", "DNSKEY")
+ response = isctest.query.udp(msg, ns1.ip)
+ assert response.flags & dns.flags.TC
+ response = isctest.query.tcp(msg, ns2.ip)
+ isctest.check.noerror(response)
+ isctest.check.adflag(response)
+ keys = response.find_rrset(response.answer, ".", "IN", "DNSKEY")
+ assert len(keys) == 2
+ assert all(key.algorithm == MLDSA44.number and len(key.key) == 1312 for key in keys)
+
+
+def draft_key():
+ return (
+ Path(os.environ["TOP_SRCDIR"])
+ / "tests/dns/testdata/dst"
+ / "Kexample.com.+018+59829"
+ )
+
+
+def test_mldsa_draft_ds():
+ ds = isctest.run.cmd(
+ [os.environ["DSFROMKEY"], "-a", "sha-256", f"{draft_key()}.key"]
+ ).out.split()
+ assert ds[:6] == ["example.com.", "IN", "DS", "59829", "18", "2"]
+ assert "".join(ds[6:]).lower() == (
+ "812cb1a22af04380e2f72d91c06c14eb1a918cf30037a8a9c67497e9264b4bfa"
+ )
+
+
+@pytest.mark.parametrize(
+ "seed",
+ [None, bytes(31), bytes(33), bytes(32)],
+ ids=["missing", "short", "long", "mismatch"],
+)
+def test_mldsa_bad_private_seed(tmp_path, seed):
+ """
+ Reject a missing seed, wrong seed lengths, and a seed that doesn't
+ match the DNSKEY.
+ """
+ key = draft_key()
+ shutil.copyfile(f"{key}.key", tmp_path / f"{key.name}.key")
+ private = "Private-key-format: v1.3\nAlgorithm: 18 (MLDSA44)\n"
+ if seed is not None:
+ private += f"PrivateKey: {base64.b64encode(seed).decode('ascii')}\n"
+ (tmp_path / f"{key.name}.private").write_text(private, encoding="ascii")
+ result = isctest.run.cmd(
+ [os.environ["SETTIME"], "-p", "all", key.name],
+ cwd=tmp_path,
+ raise_on_exception=False,
+ )
+ assert result.rc != 0
+ assert "private key is invalid" in result.err
+
+
+def test_mldsa_policy_keygen(tmp_path):
+ config = tmp_path / "policy.conf"
+ config.write_text(
+ 'dnssec-policy "mldsa" { keys { csk lifetime unlimited algorithm MLDSA44; }; };\n',
+ encoding="ascii",
+ )
+ key = isctest.run.cmd(
+ [os.environ["KEYGEN"], "-k", "mldsa", "-l", str(config), "policy.example."],
+ cwd=tmp_path,
+ ).out.strip()
+ text = (tmp_path / f"{key}.key").read_text(encoding="ascii")
+ record = next(line for line in text.splitlines() if not line.startswith(";"))
+ key = dns.rdata.from_text("IN", "DNSKEY", record.split("DNSKEY", 1)[1])
+ assert key.algorithm == MLDSA44.number
+ assert len(key.key) == 1312
diff --git a/lib/dns/dst_api.c b/lib/dns/dst_api.c
index c1cb110240..669b0ed9aa 100644
--- a/lib/dns/dst_api.c
+++ b/lib/dns/dst_api.c
@@ -217,6 +217,9 @@ dst__lib_initialize(void) {
#ifdef HAVE_OPENSSL_ED448
dst__openssleddsa_init(&dst_t_func[DST_ALG_ED448], DST_ALG_ED448);
#endif /* ifdef HAVE_OPENSSL_ED448 */
+#ifdef HAVE_OPENSSL_MLDSA44
+ dst__opensslmldsa_init(&dst_t_func[DST_ALG_MLDSA44]);
+#endif /* ifdef HAVE_OPENSSL_MLDSA44 */
#if HAVE_GSSAPI
dst__gssapi_init(&dst_t_func[DST_ALG_GSSAPI]);
@@ -1291,6 +1294,9 @@ dst_key_sigsize(const dst_key_t *key, unsigned int *n) {
case DST_ALG_ED448:
*n = DNS_SIG_ED448SIZE;
break;
+ case DST_ALG_MLDSA44:
+ *n = DNS_SIG_MLDSA44SIZE;
+ break;
case DST_ALG_HMACMD5:
*n = ISC_MD5_DIGESTLENGTH;
break;
@@ -1754,6 +1760,7 @@ issymmetric(const dst_key_t *key) {
case DST_ALG_ECDSA384:
case DST_ALG_ED25519:
case DST_ALG_ED448:
+ case DST_ALG_MLDSA44:
return false;
case DST_ALG_HMACMD5:
case DST_ALG_HMACSHA1:
diff --git a/lib/dns/dst_internal.h b/lib/dns/dst_internal.h
index 550951e984..fa1fd1009f 100644
--- a/lib/dns/dst_internal.h
+++ b/lib/dns/dst_internal.h
@@ -95,6 +95,7 @@ struct dst_key {
struct {
EVP_PKEY *pub;
EVP_PKEY *priv;
+ uint8_t *seed; /*%< ML-DSA private key */
} pkeypair;
} keydata; /*%< pointer to key in crypto pkg fmt */
@@ -197,6 +198,10 @@ void
dst__opensslecdsa_init(struct dst_func **funcp);
void
dst__openssleddsa_init(struct dst_func **funcp, unsigned char algorithm);
+#ifdef HAVE_OPENSSL_MLDSA44
+void
+dst__opensslmldsa_init(struct dst_func **funcp);
+#endif /* HAVE_OPENSSL_MLDSA44 */
#if HAVE_GSSAPI
void
dst__gssapi_init(struct dst_func **funcp);
diff --git a/lib/dns/dst_parse.c b/lib/dns/dst_parse.c
index bef7de650c..4da1020bb2 100644
--- a/lib/dns/dst_parse.c
+++ b/lib/dns/dst_parse.c
@@ -42,6 +42,7 @@
#include <isc/string.h>
#include <isc/util.h>
+#include <dns/keyvalues.h>
#include <dns/time.h>
#include "dst_internal.h"
@@ -106,6 +107,8 @@ static struct parse_map map[] = { { TAG_RSA_MODULUS, "Modulus:" },
{ TAG_EDDSA_ENGINE, "Engine:" },
{ TAG_EDDSA_LABEL, "Label:" },
+ { TAG_MLDSA_PRIVATEKEY, "PrivateKey:" },
+
{ TAG_HMACMD5_KEY, "Key:" },
{ TAG_HMACMD5_BITS, "Bits:" },
@@ -290,6 +293,27 @@ check_eddsa(const dst_private_t *priv, bool external) {
return ok ? ISC_R_SUCCESS : DST_R_INVALIDPRIVATEKEY;
}
+static isc_result_t
+check_mldsa(const dst_private_t *priv, bool external) {
+ if (external) {
+ return priv->nelements == 0 ? ISC_R_SUCCESS
+ : DST_R_INVALIDPRIVATEKEY;
+ }
+
+ /*
+ * The private key is the ML-DSA seed and nothing else;
+ * opensslmldsa_parse() relies on this.
+ */
+ if (priv->nelements != MLDSA_NTAGS ||
+ priv->elements[0].tag != TAG_MLDSA_PRIVATEKEY ||
+ priv->elements[0].length != DNS_KEY_MLDSA44SEEDSIZE)
+ {
+ return DST_R_INVALIDPRIVATEKEY;
+ }
+
+ return ISC_R_SUCCESS;
+}
+
static isc_result_t
check_hmac_md5(const dst_private_t *priv, bool old) {
int i, j;
@@ -360,6 +384,8 @@ check_data(const dst_private_t *priv, const unsigned int alg, bool old,
case DST_ALG_ED25519:
case DST_ALG_ED448:
return check_eddsa(priv, external);
+ case DST_ALG_MLDSA44:
+ return check_mldsa(priv, external);
case DST_ALG_HMACMD5:
return check_hmac_md5(priv, old);
case DST_ALG_HMACSHA1:
@@ -676,6 +702,9 @@ dst__privstruct_writefile(const dst_key_t *key, const dst_private_t *priv,
case DST_ALG_ED448:
fprintf(fp, "(ED448)\n");
break;
+ case DST_ALG_MLDSA44:
+ fprintf(fp, "(MLDSA44)\n");
+ break;
case DST_ALG_HMACMD5:
fprintf(fp, "(HMAC_MD5)\n");
break;
diff --git a/lib/dns/dst_parse.h b/lib/dns/dst_parse.h
index ddf707854a..3587c29943 100644
--- a/lib/dns/dst_parse.h
+++ b/lib/dns/dst_parse.h
@@ -67,6 +67,9 @@
#define TAG_EDDSA_ENGINE ((DST_ALG_ED25519 << TAG_SHIFT) + 1)
#define TAG_EDDSA_LABEL ((DST_ALG_ED25519 << TAG_SHIFT) + 2)
+#define MLDSA_NTAGS 1
+#define TAG_MLDSA_PRIVATEKEY ((DST_ALG_MLDSA44 << TAG_SHIFT) + 0)
+
#define OLD_HMACMD5_NTAGS 1
#define HMACMD5_NTAGS 2
#define TAG_HMACMD5_KEY ((DST_ALG_HMACMD5 << TAG_SHIFT) + 0)
diff --git a/lib/dns/include/dns/keyvalues.h b/lib/dns/include/dns/keyvalues.h
index ef3ded5d2e..437b090c13 100644
--- a/lib/dns/include/dns/keyvalues.h
+++ b/lib/dns/include/dns/keyvalues.h
@@ -57,6 +57,7 @@ enum {
DNS_KEYALG_ECDSA384 = 14,
DNS_KEYALG_ED25519 = 15,
DNS_KEYALG_ED448 = 16,
+ DNS_KEYALG_MLDSA44 = 18,
DNS_KEYALG_INDIRECT = 252,
DNS_KEYALG_PRIVATEDNS = 253,
DNS_KEYALG_PRIVATEOID = 254, /*%< Key begins with OID giving alg */
@@ -82,3 +83,8 @@ enum {
#define DNS_KEY_ED448SIZE 57
#define DNS_SIG_ED448SIZE 114
+
+/* draft-westerbaan-dnssec-mldsa */
+#define DNS_KEY_MLDSA44SIZE 1312
+#define DNS_SIG_MLDSA44SIZE 2420
+#define DNS_KEY_MLDSA44SEEDSIZE 32
diff --git a/lib/dns/include/dst/dst.h b/lib/dns/include/dst/dst.h
index 4abff90182..0ad87b2acc 100644
--- a/lib/dns/include/dst/dst.h
+++ b/lib/dns/include/dst/dst.h
@@ -93,6 +93,7 @@ typedef enum dst_algorithm {
DST_ALG_ECDSA384 = 14,
DST_ALG_ED25519 = 15,
DST_ALG_ED448 = 16,
+ DST_ALG_MLDSA44 = 18,
/*
* Do not renumber HMAC algorithms as they are used externally to named
diff --git a/lib/dns/kasp.c b/lib/dns/kasp.c
index 72b51be2c2..49087d3922 100644
--- a/lib/dns/kasp.c
+++ b/lib/dns/kasp.c
@@ -456,6 +456,9 @@ dns_kasp_key_size(dns_kasp_key_t *key) {
case DST_ALG_ED448:
size = 456;
break;
+ case DST_ALG_MLDSA44:
+ size = DNS_KEY_MLDSA44SIZE * 8;
+ break;
default:
/* unsupported */
break;
diff --git a/lib/dns/meson.build b/lib/dns/meson.build
index 2248b0588a..6e5676e410 100644
--- a/lib/dns/meson.build
+++ b/lib/dns/meson.build
@@ -30,6 +30,10 @@ dns_header_gen = executable(
subdir('include')
+if config.has('HAVE_OPENSSL_MLDSA44')
+ dns_srcset.add(files('opensslmldsa_link.c'))
+endif
+
dns_srcset.add(dns_gen_headers)
dns_srcset.add(when: dnstap_dep, if_true: files('dnstap.c'))
dns_srcset.add(when: maxminddb_dep, if_true: files('geoip2.c'))
diff --git a/lib/dns/opensslmldsa_link.c b/lib/dns/opensslmldsa_link.c
new file mode 100644
index 0000000000..15305f20fb
--- /dev/null
+++ b/lib/dns/opensslmldsa_link.c
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+/*! \file */
+
+#include <openssl/core_names.h>
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/params.h>
+#include <openssl/rand.h>
+
+#include <isc/buffer.h>
+#include <isc/mem.h>
+#include <isc/result.h>
+#include <isc/safe.h>
+#include <isc/util.h>
+
+#include <dns/keyvalues.h>
+
+#include "dst_internal.h"
+#include "dst_openssl.h"
+#include "dst_parse.h"
+
+/*
+ * draft-westerbaan-dnssec-mldsa uses pure ML-DSA-44 with an empty context.
+ * ML-DSA hashes the message with SHAKE256 before signing it (FIPS 204
+ * Algorithm 7 step 6, Algorithm 8 step 7):
+ *
+ * tr = H(pk, 64)
+ * mu = H(tr || M', 64), where M' = 0 || |ctx| || ctx || M
+ *
+ * so mu is computed incrementally as the data is added, and the provider
+ * is handed mu instead of the message. Hedged signing is left at the
+ * OpenSSL default.
+ */
+#define MLDSA_TR_SIZE 64
+#define MLDSA_MU_SIZE 64
+
+static const unsigned char mldsa_prefix[] = { 0x00, 0x00 };
+
+static void
+opensslmldsa__setkey(dst_key_t *key, EVP_PKEY *pk, EVP_PKEY *sk,
+ uint8_t *seed) {
+ key->key_size = DNS_KEY_MLDSA44SIZE * 8;
+ key->keydata.pkeypair.pub = pk;
+ key->keydata.pkeypair.priv = sk;
+ key->keydata.pkeypair.seed = seed;
+}
+
+static isc_result_t
+opensslmldsa__getpk(dst_key_t *key, unsigned char *pk, size_t prlen) {
+ size_t len = prlen;
+ int r = EVP_PKEY_get_raw_public_key(key->keydata.pkeypair.pub, pk,
+ &len);
+ if (r != 1) {
+ return dst__openssl_toresult(DST_R_INVALIDPUBLICKEY);
+ }
+ INSIST(len == prlen);
+
+ return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+opensslmldsa__addpk(dst_context_t *dctx, EVP_MD_CTX *ctx, unsigned char *pk,
+ size_t pklen, unsigned char *tr, size_t trlen) {
+ int r = EVP_DigestInit_ex(ctx, isc__crypto_md[ISC_MD_SHAKE256], NULL);
+ if (r != 1) {
+ goto cleanup;
+ }
+
+ r = EVP_DigestUpdate(ctx, pk, pklen);
+ if (r != 1) {
+ goto cleanup;
+ }
+
+ r = EVP_DigestFinalXOF(ctx, tr, trlen);
+ if (r != 1) {
+ goto cleanup;
+ }
+
+ return ISC_R_SUCCESS;
+
+cleanup:
+ return dst__openssl_toresult3(dctx->category, "SHAKE256(pk)",
+ ISC_R_FAILURE);
+}
+
+static isc_result_t
+opensslmldsa__addtr(dst_context_t *dctx, EVP_MD_CTX *ctx, unsigned char *tr,
+ size_t trlen) {
+ int r = EVP_DigestInit_ex(ctx, isc__crypto_md[ISC_MD_SHAKE256], NULL);
+ if (r != 1) {
+ goto cleanup;
+ }
+ r = EVP_DigestUpdate(ctx, tr, trlen);
+ if (r != 1) {
+ goto cleanup;
+ }
+ r = EVP_DigestUpdate(ctx, mldsa_prefix, sizeof(mldsa_prefix));
+ if (r != 1) {
+ goto cleanup;
+ }
+
+ return ISC_R_SUCCESS;
+cleanup:
+ return dst__openssl_toresult3(dctx->category, "SHAKE256(tr)",
+ ISC_R_FAILURE);
+}
+
+static isc_result_t
+opensslmldsa_createctx(dst_key_t *key, dst_context_t *dctx) {
+ REQUIRE(key->key_alg == DST_ALG_MLDSA44);
+
+ isc_result_t result = ISC_R_SUCCESS;
+ EVP_MD_CTX *ctx = NULL;
+ unsigned char pk[DNS_KEY_MLDSA44SIZE];
+ unsigned char tr[MLDSA_TR_SIZE];
+
+ ctx = EVP_MD_CTX_new();
+ if (ctx == NULL) {
+ return dst__openssl_toresult(ISC_R_NOMEMORY);
+ }
+
+ CHECK(opensslmldsa__getpk(key, pk, sizeof(pk)));
+ CHECK(opensslmldsa__addpk(dctx, ctx, pk, sizeof(pk), tr, sizeof(tr)));
+ CHECK(opensslmldsa__addtr(dctx, ctx, tr, sizeof(tr)));
+
+ dctx->ctxdata.evp_md_ctx = MOVE_OWNERSHIP(ctx);
+
+cleanup:
+ EVP_MD_CTX_free(ctx);
+ return result;
+}
+
+static void
+opensslmldsa_destroyctx(dst_context_t *dctx) {
+ EVP_MD_CTX *ctx = MOVE_OWNERSHIP(dctx->ctxdata.evp_md_ctx);
+ EVP_MD_CTX_free(ctx);
+}
+
+static isc_result_t
+opensslmldsa_adddata(dst_context_t *dctx, const isc_region_t *data) {
+ int r = EVP_DigestUpdate(dctx->ctxdata.evp_md_ctx, data->base,
+ data->length);
+ if (r != 1) {
+ return dst__openssl_toresult3(
+ dctx->category, "EVP_DigestUpdate", ISC_R_FAILURE);
+ }
+ return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+opensslmldsa_sign(dst_context_t *dctx, isc_buffer_t *sig) {
+ isc_result_t result = ISC_R_SUCCESS;
+ isc_region_t target;
+ EVP_MD_CTX *ctx = NULL;
+ EVP_PKEY *pkey = dctx->key->keydata.pkeypair.priv;
+ unsigned char mu[MLDSA_MU_SIZE];
+ size_t siglen = DNS_SIG_MLDSA44SIZE;
+ int external_mu = 1;
+ const OSSL_PARAM params[] = {
+ OSSL_PARAM_int(OSSL_SIGNATURE_PARAM_MU, &external_mu),
+ OSSL_PARAM_END,
+ };
+
+ if (pkey == NULL) {
+ return DST_R_NULLKEY;
+ }
+ isc_buffer_availableregion(sig, &target);
+ if (target.length < siglen) {
+ return ISC_R_NOSPACE;
+ }
+
+ ctx = EVP_MD_CTX_new();
+ if (ctx == NULL) {
+ return dst__openssl_toresult(ISC_R_NOMEMORY);
+ }
+
+ int r = EVP_DigestFinalXOF(dctx->ctxdata.evp_md_ctx, mu, sizeof(mu));
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult3(dctx->category,
+ "EVP_DigestFinalXOF",
+ DST_R_SIGNFAILURE));
+ }
+
+ r = EVP_DigestSignInit_ex(ctx, NULL, NULL, NULL, NULL, pkey, params);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult3(dctx->category,
+ "EVP_DigestSignInit_ex",
+ DST_R_SIGNFAILURE));
+ }
+
+ r = EVP_DigestSign(ctx, target.base, &siglen, mu, sizeof(mu));
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult3(dctx->category, "EVP_DigestSign",
+ DST_R_SIGNFAILURE));
+ }
+ INSIST(siglen == DNS_SIG_MLDSA44SIZE);
+ isc_buffer_add(sig, siglen);
+
+cleanup:
+ EVP_MD_CTX_free(ctx);
+ return result;
+}
+
+static isc_result_t
+opensslmldsa_verify(dst_context_t *dctx, const isc_region_t *sig) {
+ isc_result_t result = ISC_R_SUCCESS;
+ EVP_MD_CTX *ctx = NULL;
+ EVP_PKEY *pkey = dctx->key->keydata.pkeypair.pub;
+ unsigned char mu[MLDSA_MU_SIZE];
+ int external_mu = 1;
+ const OSSL_PARAM params[] = {
+ OSSL_PARAM_int(OSSL_SIGNATURE_PARAM_MU, &external_mu),
+ OSSL_PARAM_END,
+ };
+
+ if (sig->length != DNS_SIG_MLDSA44SIZE) {
+ return DST_R_VERIFYFAILURE;
+ }
+
+ ctx = EVP_MD_CTX_new();
+ if (ctx == NULL) {
+ return dst__openssl_toresult(ISC_R_NOMEMORY);
+ }
+
+ int r = EVP_DigestFinalXOF(dctx->ctxdata.evp_md_ctx, mu, sizeof(mu));
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult3(dctx->category,
+ "EVP_DigestFinalXOF",
+ DST_R_VERIFYFAILURE));
+ }
+
+ r = EVP_DigestVerifyInit_ex(ctx, NULL, NULL, NULL, NULL, pkey, params);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult3(dctx->category,
+ "EVP_DigestVerifyInit_ex",
+ DST_R_VERIFYFAILURE));
+ }
+
+ r = EVP_DigestVerify(ctx, sig->base, sig->length, mu, sizeof(mu));
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult(DST_R_VERIFYFAILURE));
+ }
+
+cleanup:
+ EVP_MD_CTX_free(ctx);
+ return result;
+}
+
+static void
+opensslmldsa_destroy(dst_key_t *key) {
+ if (key->keydata.pkeypair.seed != NULL) {
+ isc_safe_memwipe(key->keydata.pkeypair.seed,
+ DNS_KEY_MLDSA44SEEDSIZE);
+ isc_mem_put(key->mctx, key->keydata.pkeypair.seed,
+ DNS_KEY_MLDSA44SEEDSIZE);
+ }
+ dst__openssl_keypair_destroy(key);
+}
+
+static isc_result_t
+opensslmldsa_generate(dst_key_t *key, int unused ISC_ATTR_UNUSED,
+ void (*callback ISC_ATTR_UNUSED)(int)) {
+ isc_result_t result = ISC_R_SUCCESS;
+ EVP_PKEY_CTX *ctx = NULL;
+ EVP_PKEY *pkey = NULL;
+ uint8_t *seed = NULL;
+ OSSL_PARAM params[2];
+
+ if (key->label != NULL) {
+ return ISC_R_NOTIMPLEMENTED;
+ }
+
+ ctx = EVP_PKEY_CTX_new_from_name(NULL, "ML-DSA-44", NULL);
+ if (ctx == NULL) {
+ return dst__openssl_toresult2("EVP_PKEY_CTX_new_from_name",
+ DST_R_OPENSSLFAILURE);
+ }
+ int r = EVP_PKEY_keygen_init(ctx);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult2("EVP_PKEY_keygen_init",
+ DST_R_OPENSSLFAILURE));
+ }
+
+ /*
+ * Draw the seed from the same DRBG the provider would use and let
+ * the provider generate the key from it, so that we know the seed.
+ */
+ seed = isc_mem_get(key->mctx, DNS_KEY_MLDSA44SEEDSIZE);
+ r = RAND_priv_bytes_ex(NULL, seed, DNS_KEY_MLDSA44SEEDSIZE, 0);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult2("RAND_priv_bytes_ex",
+ DST_R_OPENSSLFAILURE));
+ }
+ params[0] = OSSL_PARAM_construct_octet_string(
+ OSSL_PKEY_PARAM_ML_DSA_SEED, seed, DNS_KEY_MLDSA44SEEDSIZE);
+ params[1] = OSSL_PARAM_construct_end();
+ r = EVP_PKEY_CTX_set_params(ctx, params);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult2("EVP_PKEY_CTX_set_params",
+ DST_R_OPENSSLFAILURE));
+ }
+
+ r = EVP_PKEY_generate(ctx, &pkey);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult2("EVP_PKEY_generate",
+ DST_R_OPENSSLFAILURE));
+ }
+
+ opensslmldsa__setkey(key, pkey, pkey, seed);
+ (void)MOVE_OWNERSHIP(pkey);
+ (void)MOVE_OWNERSHIP(seed);
+
+cleanup:
+ if (seed != NULL) {
+ isc_safe_memwipe(seed, DNS_KEY_MLDSA44SEEDSIZE);
+ isc_mem_put(key->mctx, seed, DNS_KEY_MLDSA44SEEDSIZE);
+ }
+ EVP_PKEY_CTX_free(ctx);
+ return result;
+}
+
+static isc_result_t
+opensslmldsa_todns(const dst_key_t *key, isc_buffer_t *data) {
+ isc_region_t target;
+ size_t len = DNS_KEY_MLDSA44SIZE;
+
+ isc_buffer_availableregion(data, &target);
+ if (target.length < len) {
+ return ISC_R_NOSPACE;
+ }
+ if (EVP_PKEY_get_raw_public_key(key->keydata.pkeypair.pub, target.base,
+ &len) != 1)
+ {
+ return dst__openssl_toresult(DST_R_INVALIDPUBLICKEY);
+ }
+ INSIST(len == DNS_KEY_MLDSA44SIZE);
+ isc_buffer_add(data, len);
+ return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+opensslmldsa_fromdns(dst_key_t *key, isc_buffer_t *data) {
+ isc_region_t source;
+ EVP_PKEY *pkey = NULL;
+
+ isc_buffer_remainingregion(data, &source);
+ if (source.length != DNS_KEY_MLDSA44SIZE) {
+ return DST_R_INVALIDPUBLICKEY;
+ }
+ pkey = EVP_PKEY_new_raw_public_key_ex(NULL, "ML-DSA-44", NULL,
+ source.base, source.length);
+ if (pkey == NULL) {
+ return dst__openssl_toresult(DST_R_INVALIDPUBLICKEY);
+ }
+ isc_buffer_forward(data, source.length);
+ opensslmldsa__setkey(key, pkey, NULL, NULL);
+ return ISC_R_SUCCESS;
+}
+
+static isc_result_t
+opensslmldsa_tofile(const dst_key_t *key, const char *directory) {
+ dst_private_t priv = { 0 };
+
+ if (key->external) {
+ return dst__privstruct_writefile(key, &priv, directory);
+ }
+ if (key->keydata.pkeypair.seed == NULL) {
+ return DST_R_NULLKEY;
+ }
+
+ priv.nelements = 1;
+ priv.elements[0].tag = TAG_MLDSA_PRIVATEKEY;
+ priv.elements[0].length = DNS_KEY_MLDSA44SEEDSIZE;
+ priv.elements[0].data = key->keydata.pkeypair.seed;
+ return dst__privstruct_writefile(key, &priv, directory);
+}
+
+static isc_result_t
+opensslmldsa_parse(dst_key_t *key, isc_lex_t *lexer, dst_key_t *pub) {
+ isc_result_t result = ISC_R_SUCCESS;
+ dst_private_t priv;
+ EVP_PKEY_CTX *ctx = NULL;
+ EVP_PKEY *pkey = NULL;
+ OSSL_PARAM params[2];
+ uint8_t *seed = NULL;
+
+ CHECK(dst__privstruct_parse(key, DST_ALG_MLDSA44, lexer, key->mctx,
+ &priv));
+ if (key->external) {
+ if (pub == NULL) {
+ CLEANUP(DST_R_INVALIDPRIVATEKEY);
+ }
+ key->keydata.pkeypair = pub->keydata.pkeypair;
+ pub->keydata.pkeypair.priv = NULL;
+ pub->keydata.pkeypair.pub = NULL;
+ pub->keydata.pkeypair.seed = NULL;
+ CLEANUP(ISC_R_SUCCESS);
+ }
+
+ /* check_mldsa() requires exactly one 32-byte seed. */
+ if (priv.elements[0].length != DNS_KEY_MLDSA44SEEDSIZE) {
+ CLEANUP(DST_R_INVALIDPRIVATEKEY);
+ }
+ seed = isc_mem_get(key->mctx, DNS_KEY_MLDSA44SEEDSIZE);
+ memmove(seed, priv.elements[0].data, priv.elements[0].length);
+ params[0] = OSSL_PARAM_construct_octet_string(
+ OSSL_PKEY_PARAM_ML_DSA_SEED, seed, priv.elements[0].length);
+ params[1] = OSSL_PARAM_construct_end();
+ ctx = EVP_PKEY_CTX_new_from_name(NULL, "ML-DSA-44", NULL);
+ if (ctx == NULL) {
+ CLEANUP(dst__openssl_toresult2("EVP_PKEY_CTX_new_from_name",
+ DST_R_INVALIDPRIVATEKEY));
+ };
+
+ int r = EVP_PKEY_fromdata_init(ctx);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult2("EVP_PKEY_fromdata_init",
+ DST_R_INVALIDPRIVATEKEY));
+ }
+
+ r = EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params);
+ if (r != 1) {
+ CLEANUP(dst__openssl_toresult2("EVP_PKEY_fromdata",
+ DST_R_INVALIDPRIVATEKEY));
+ }
+
+ if (pub != NULL) {
+ r = EVP_PKEY_eq(pkey, pub->keydata.pkeypair.pub);
+ if (r != 1) {
+ CLEANUP(DST_R_INVALIDPRIVATEKEY);
+ }
+ }
+
+ opensslmldsa__setkey(key, pkey, pkey, seed);
+ (void)MOVE_OWNERSHIP(pkey);
+ (void)MOVE_OWNERSHIP(seed);
+
+cleanup:
+ if (seed != NULL) {
+ isc_safe_memwipe(seed, DNS_KEY_MLDSA44SEEDSIZE);
+ isc_mem_put(key->mctx, seed, DNS_KEY_MLDSA44SEEDSIZE);
+ }
+ EVP_PKEY_free(pkey);
+
+ EVP_PKEY_CTX_free(ctx);
+ dst__privstruct_free(&priv, key->mctx);
+ isc_safe_memwipe(&priv, sizeof(priv));
+ return result;
+}
+
+static dst_func_t opensslmldsa_functions = {
+ .createctx = opensslmldsa_createctx,
+ .destroyctx = opensslmldsa_destroyctx,
+ .adddata = opensslmldsa_adddata,
+ .sign = opensslmldsa_sign,
+ .verify = opensslmldsa_verify,
+ .compare = dst__openssl_keypair_compare,
+ .generate = opensslmldsa_generate,
+ .isprivate = dst__openssl_keypair_isprivate,
+ .destroy = opensslmldsa_destroy,
+ .todns = opensslmldsa_todns,
+ .fromdns = opensslmldsa_fromdns,
+ .tofile = opensslmldsa_tofile,
+ .parse = opensslmldsa_parse,
+};
+
+void
+dst__opensslmldsa_init(dst_func_t **funcp) {
+ REQUIRE(funcp != NULL);
+
+ /* Respect the configured providers and default property query. */
+ EVP_KEYMGMT *keymgmt = EVP_KEYMGMT_fetch(NULL, "ML-DSA-44", NULL);
+ EVP_SIGNATURE *signature = EVP_SIGNATURE_fetch(NULL, "ML-DSA-44", NULL);
+ if (keymgmt != NULL && signature != NULL &&
+ isc__crypto_md[ISC_MD_SHAKE256] != NULL)
+ {
+ *funcp = &opensslmldsa_functions;
+ }
+ EVP_KEYMGMT_free(keymgmt);
+ EVP_SIGNATURE_free(signature);
+ ERR_clear_error();
+}
diff --git a/lib/dns/rcode.c b/lib/dns/rcode.c
index b58b330d75..adce491774 100644
--- a/lib/dns/rcode.c
+++ b/lib/dns/rcode.c
@@ -107,6 +107,7 @@
{ DNS_KEYALG_ECDSA384, "ECDSA384", 0 }, \
{ DNS_KEYALG_ED25519, "ED25519", 0 }, \
{ DNS_KEYALG_ED448, "ED448", 0 }, \
+ { DNS_KEYALG_MLDSA44, "MLDSA44", 0 }, \
{ DNS_KEYALG_INDIRECT, "INDIRECT", 0 }, \
{ DNS_KEYALG_PRIVATEDNS, "PRIVATEDNS", 0 }, \
{ DNS_KEYALG_PRIVATEOID, "PRIVATEOID", 0 }, SENTINEL
diff --git a/lib/isc/crypto/ossl3.c b/lib/isc/crypto/ossl3.c
index 51d94b304f..a8641e4454 100644
--- a/lib/isc/crypto/ossl3.c
+++ b/lib/isc/crypto/ossl3.c
@@ -95,6 +95,8 @@ static isc_constregion_t md_to_name[ISC_MD_MAX] = {
.length = sizeof("SHA2-384") - 1 },
[ISC_MD_SHA512] = { .base = "SHA2-512",
.length = sizeof("SHA2-512") - 1 },
+ [ISC_MD_SHAKE256] = { .base = "SHAKE-256",
+ .length = sizeof("SHAKE-256") - 1 },
};
static OSSL_PARAM md_to_hmac_params[ISC_MD_MAX][2] = {
@@ -123,6 +125,8 @@ static OSSL_PARAM md_to_hmac_params[ISC_MD_MAX][2] = {
OSSL_PARAM_utf8_string(OSSL_MAC_PARAM_DIGEST, UNCONST("SHA2-512"), sizeof("SHA2-512") - 1),
OSSL_PARAM_END,
},
+ [ISC_MD_SHAKE256] = {
+ OSSL_PARAM_utf8_string(OSSL_MAC_PARAM_DIGEST, UNCONST("SHAKE-256"), sizeof("SHAKE-256") - 1), OSSL_PARAM_END, },
};
#define md_register_algorithm(alg) \
@@ -152,6 +156,9 @@ register_algorithms(void) {
md_register_algorithm(SHA256);
md_register_algorithm(SHA384);
md_register_algorithm(SHA512);
+#if HAVE_OPENSSL_MLDSA44
+ md_register_algorithm(SHAKE256);
+#endif
INSIST(evp_aes_128_gcm == NULL);
evp_aes_128_gcm = EVP_CIPHER_fetch(NULL, "AES-128-GCM", NULL);
diff --git a/lib/isc/crypto/ossl_common.c b/lib/isc/crypto/ossl_common.c
index 965bbb7fab..e5752385e1 100644
--- a/lib/isc/crypto/ossl_common.c
+++ b/lib/isc/crypto/ossl_common.c
@@ -20,7 +20,8 @@
#include <isc/util.h>
EVP_MD *isc__crypto_md[] = {
- [ISC_MD_UNKNOWN] = NULL, [ISC_MD_MD5] = NULL, [ISC_MD_SHA1] = NULL,
- [ISC_MD_SHA224] = NULL, [ISC_MD_SHA256] = NULL, [ISC_MD_SHA384] = NULL,
- [ISC_MD_SHA512] = NULL,
+ [ISC_MD_UNKNOWN] = NULL, [ISC_MD_MD5] = NULL,
+ [ISC_MD_SHA1] = NULL, [ISC_MD_SHA224] = NULL,
+ [ISC_MD_SHA256] = NULL, [ISC_MD_SHA384] = NULL,
+ [ISC_MD_SHA512] = NULL, [ISC_MD_SHAKE256] = NULL,
};
diff --git a/lib/isc/include/isc/md.h b/lib/isc/include/isc/md.h
index f8f89bba68..8fb7c01e9a 100644
--- a/lib/isc/include/isc/md.h
+++ b/lib/isc/include/isc/md.h
@@ -42,24 +42,26 @@ typedef enum isc_md_type {
ISC_MD_SHA256 = 0x04,
ISC_MD_SHA384 = 0x05,
ISC_MD_SHA512 = 0x06,
- ISC_MD_MAX = 0x07,
+ ISC_MD_SHAKE256 = 0x07,
+ ISC_MD_MAX = 0x08,
} isc_md_type_t;
-#define ISC_MD5_DIGESTLENGTH 16
-#define ISC_MD5_BLOCK_LENGTH 64
-#define ISC_SHA1_DIGESTLENGTH 20
-#define ISC_SHA1_BLOCK_LENGTH 64
-#define ISC_SHA224_DIGESTLENGTH 28
-#define ISC_SHA224_BLOCK_LENGTH 64
-#define ISC_SHA256_DIGESTLENGTH 32
-#define ISC_SHA256_BLOCK_LENGTH 64
-#define ISC_SHA384_DIGESTLENGTH 48
-#define ISC_SHA384_BLOCK_LENGTH 128
-#define ISC_SHA512_DIGESTLENGTH 64
-#define ISC_SHA512_BLOCK_LENGTH 128
+#define ISC_MD5_DIGESTLENGTH 16
+#define ISC_MD5_BLOCK_LENGTH 64
+#define ISC_SHA1_DIGESTLENGTH 20
+#define ISC_SHA1_BLOCK_LENGTH 64
+#define ISC_SHA224_DIGESTLENGTH 28
+#define ISC_SHA224_BLOCK_LENGTH 64
+#define ISC_SHA256_DIGESTLENGTH 32
+#define ISC_SHA256_BLOCK_LENGTH 64
+#define ISC_SHA384_DIGESTLENGTH 48
+#define ISC_SHA384_BLOCK_LENGTH 128
+#define ISC_SHA512_DIGESTLENGTH 64
+#define ISC_SHA512_BLOCK_LENGTH 128
+#define ISC_SHAKE256_BLOCK_LENGTH 136
#define ISC_MAX_MD_SIZE 64U /* EVP_MAX_MD_SIZE */
-#define ISC_MAX_BLOCK_SIZE 128U /* ISC_SHA512_BLOCK_LENGTH */
+#define ISC_MAX_BLOCK_SIZE 136U /* ISC_SHAKE256_BLOCK_LENGTH */
/**
* isc_md:
diff --git a/lib/isccfg/kaspconf.c b/lib/isccfg/kaspconf.c
index 3b1913d83c..5008a6db1a 100644
--- a/lib/isccfg/kaspconf.c
+++ b/lib/isccfg/kaspconf.c
@@ -349,6 +349,7 @@ cfg_kaspkey_fromconfig(const cfg_obj_t *config, dns_kasp_t *kasp,
case DST_ALG_ECDSA384:
case DST_ALG_ED25519:
case DST_ALG_ED448:
+ case DST_ALG_MLDSA44:
if (log_errors) {
kaspcfg_log(
obj, ISC_LOG_WARNING,
diff --git a/meson.build b/meson.build
index 7c70fd6c3a..d45aedfff8 100644
--- a/meson.build
+++ b/meson.build
@@ -727,6 +727,11 @@ if cc.has_header_symbol('openssl/evp.h', 'NID_ED448', dependencies: openssl_dep)
config.set('HAVE_OPENSSL_ED448', 1)
endif
+# Check for ML-DSA support (requires OpenSSL 3.5+ with ML-DSA support)
+if cc.has_header_symbol('openssl/evp.h', 'EVP_PKEY_ML_DSA_44', dependencies: openssl_dep)
+ config.set('HAVE_OPENSSL_MLDSA44', 1)
+endif
+
foreach fn, header : {
'ERR_get_error_all': '#include <openssl/err.h>',
'BIO_read_ex': '#include <openssl/bio.h>',
diff --git a/tests/dns/dst_test.c b/tests/dns/dst_test.c
index 0c97dfba75..3dbb8464a7 100644
--- a/tests/dns/dst_test.c
+++ b/tests/dns/dst_test.c
@@ -26,6 +26,13 @@
* redefined malloc in cmocka.h.
*/
#include <openssl/err.h>
+#ifdef HAVE_OPENSSL_MLDSA44
+#include <limits.h>
+
+#include <openssl/core_names.h>
+#include <openssl/params.h>
+#include <openssl/provider.h>
+#endif
#define UNIT_TESTING
#include <cmocka.h>
@@ -259,7 +266,7 @@ check_sig(const char *datapath, const char *sigpath, const char *keyname,
size_t rval, len;
FILE *fp;
dst_key_t *key = NULL;
- unsigned char sig[512];
+ unsigned char sig[DNS_SIG_MLDSA44SIZE + 1];
unsigned char *p;
unsigned char *data;
off_t size;
@@ -357,11 +364,12 @@ check_sig(const char *datapath, const char *sigpath, const char *keyname,
isc_region_t r;
isc_buffer_usedregion(&sigb, &r);
- char hexbuf[4096] = { 0 };
+ char hexbuf[sizeof(sigbuf2) * 2 + 1] = { 0 };
isc_buffer_t hb;
isc_buffer_init(&hb, hexbuf, sizeof(hexbuf));
- isc_hex_totext(&r, 0, "", &hb);
+ result2 = isc_hex_totext(&r, 0, "", &hb);
+ assert_int_equal(result2, ISC_R_SUCCESS);
fprintf(stderr, "# %s:\n# %s\n", sigpath, hexbuf);
}
@@ -385,6 +393,13 @@ ISC_RUN_TEST_IMPL(sig_test) {
dns_secalg_t alg;
bool expect;
} testcases[] = {
+ /* Published DNSSEC example, including the private seed. */
+ { TESTS_DIR "/testdata/dst/mldsa.data",
+ TESTS_DIR "/testdata/dst/mldsa.sig", "example.com.", 59829,
+ DST_ALG_MLDSA44, true },
+ { TESTS_DIR "/testdata/dst/test1.data",
+ TESTS_DIR "/testdata/dst/mldsa.sig", "example.com.", 59829,
+ DST_ALG_MLDSA44, false },
{ TESTS_DIR "/testdata/dst/test1.data",
TESTS_DIR "/testdata/dst/test1.ecdsa256sig", "test.", 49130,
DST_ALG_ECDSA256, true },
@@ -621,12 +636,230 @@ ISC_RUN_TEST_IMPL(ecdsa_determinism_test) {
dst_key_free(&key);
}
+static isc_result_t
+mldsa_verify(dst_key_t *key, isc_region_t *data, isc_region_t *sig) {
+ dst_context_t *ctx = NULL;
+ isc_result_t result;
+
+ result = dst_context_create(key, isc_g_mctx, DNS_LOGCATEGORY_GENERAL,
+ false, &ctx);
+ assert_int_equal(result, ISC_R_SUCCESS);
+ assert_int_equal(dst_context_adddata(ctx, data), ISC_R_SUCCESS);
+ result = dst_context_verify(ctx, sig);
+ dst_context_destroy(&ctx);
+ return result;
+}
+
+ISC_RUN_TEST_IMPL(mldsa_wire) {
+ dns_fixedname_t fname;
+ dns_name_t *name = dns_fixedname_initname(&fname);
+ dst_key_t *key = NULL, *pub = NULL;
+ dst_context_t *ctx = NULL;
+ unsigned char wire[4 + DNS_KEY_MLDSA44SIZE + 1] = { 0 };
+ unsigned char signature[DNS_SIG_MLDSA44SIZE + 1] = { 0 };
+ unsigned char message[4096] = { 1, 2, 3, 4 };
+ isc_region_t data = { .base = message, .length = sizeof(message) };
+ isc_region_t sig;
+ isc_buffer_t buf;
+ unsigned int sigsize;
+
+ if (!dst_algorithm_supported(DST_ALG_MLDSA44)) {
+ skip();
+ }
+ assert_int_equal(dns_name_fromstring(name, "example.com.", dns_rootname,
+ 0, isc_g_mctx),
+ ISC_R_SUCCESS);
+ assert_int_equal(dst_key_fromfile(name, 59829, DST_ALG_MLDSA44,
+ DST_TYPE_PUBLIC | DST_TYPE_PRIVATE,
+ TESTS_DIR "/testdata/dst", isc_g_mctx,
+ &key),
+ ISC_R_SUCCESS);
+ assert_int_equal(dst_key_size(key), 1312 * 8);
+ assert_int_equal(dst_key_sigsize(key, &sigsize), ISC_R_SUCCESS);
+ assert_int_equal(sigsize, 2420);
+
+ isc_buffer_init(&buf, wire, sizeof(wire));
+ assert_int_equal(dst_key_todns(key, &buf), ISC_R_SUCCESS);
+ assert_int_equal(isc_buffer_usedlength(&buf), 4 + 1312);
+ assert_int_equal(wire[3], 18);
+ assert_int_equal(dst_key_fromdns(name, dns_rdataclass_in, &buf,
+ isc_g_mctx, &pub),
+ ISC_R_SUCCESS);
+ assert_true(dst_key_pubcompare(key, pub, false));
+
+ /* Truncated and oversized public keys must not be accepted. */
+ unsigned int lengths[] = { 5, 4 + 1311, 4 + 1313 };
+ for (size_t i = 0; i < ARRAY_SIZE(lengths); i++) {
+ dst_key_t *bad = NULL;
+ isc_buffer_init(&buf, wire, sizeof(wire));
+ isc_buffer_add(&buf, lengths[i]);
+ assert_int_equal(dst_key_fromdns(name, dns_rdataclass_in, &buf,
+ isc_g_mctx, &bad),
+ DST_R_INVALIDPUBLICKEY);
+ assert_null(bad);
+ }
+
+ assert_int_equal(dst_context_create(key, isc_g_mctx,
+ DNS_LOGCATEGORY_GENERAL, true,
+ &ctx),
+ ISC_R_SUCCESS);
+ /* Exercise hashing of multiple input regions into mu. */
+ isc_region_t first = { .base = message, .length = 20 };
+ isc_region_t rest = { .base = message + 20,
+ .length = sizeof(message) - 20 };
+ assert_int_equal(dst_context_adddata(ctx, &first), ISC_R_SUCCESS);
+ assert_int_equal(dst_context_adddata(ctx, &rest), ISC_R_SUCCESS);
+ isc_buffer_init(&buf, signature, DNS_SIG_MLDSA44SIZE - 1);
+ assert_int_equal(dst_context_sign(ctx, &buf), ISC_R_NOSPACE);
+ isc_buffer_init(&buf, signature, sizeof(signature));
+ assert_int_equal(dst_context_sign(ctx, &buf), ISC_R_SUCCESS);
+ assert_int_equal(isc_buffer_usedlength(&buf), 2420);
+ dst_context_destroy(&ctx);
+ isc_buffer_usedregion(&buf, &sig);
+#ifdef HAVE_OPENSSL_MLDSA44
+ /* Signing over mu must match OpenSSL's one-shot pure ML-DSA. */
+ EVP_MD_CTX *oneshot = EVP_MD_CTX_new();
+ assert_non_null(oneshot);
+ assert_int_equal(
+ EVP_DigestVerifyInit_ex(oneshot, NULL, NULL, NULL, NULL,
+ pub->keydata.pkeypair.pub, NULL),
+ 1);
+ assert_int_equal(EVP_DigestVerify(oneshot, sig.base, sig.length,
+ data.base, data.length),
+ 1);
+ EVP_MD_CTX_free(oneshot);
+#endif
+ assert_int_equal(mldsa_verify(pub, &data, &sig), ISC_R_SUCCESS);
+ sig.length--;
+ assert_int_equal(mldsa_verify(pub, &data, &sig), DST_R_VERIFYFAILURE);
+ sig.length += 2;
+ assert_int_equal(mldsa_verify(pub, &data, &sig), DST_R_VERIFYFAILURE);
+ sig.length--;
+ signature[0] ^= 1;
+ assert_int_equal(mldsa_verify(pub, &data, &sig), DST_R_VERIFYFAILURE);
+
+#ifdef HAVE_OPENSSL_MLDSA44
+ /* Neither a nonempty context nor unencoded ML-DSA is DNSSEC MLDSA44. */
+ int encoding = 0;
+ char context[] = "DNSSEC";
+ OSSL_PARAM contexts[] = {
+ OSSL_PARAM_octet_string(OSSL_SIGNATURE_PARAM_CONTEXT_STRING,
+ context, sizeof(context) - 1),
+ OSSL_PARAM_END,
+ };
+ OSSL_PARAM unencoded[] = {
+ OSSL_PARAM_int(OSSL_SIGNATURE_PARAM_MESSAGE_ENCODING,
+ &encoding),
+ OSSL_PARAM_END,
+ };
+ const OSSL_PARAM *variants[] = { contexts, unencoded };
+ for (size_t i = 0; i < ARRAY_SIZE(variants); i++) {
+ EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
+ size_t len = DNS_SIG_MLDSA44SIZE;
+ assert_non_null(mdctx);
+ assert_int_equal(
+ EVP_DigestSignInit_ex(mdctx, NULL, NULL, NULL, NULL,
+ key->keydata.pkeypair.priv,
+ variants[i]),
+ 1);
+ assert_int_equal(EVP_DigestSign(mdctx, signature, &len,
+ data.base, data.length),
+ 1);
+ assert_int_equal(mldsa_verify(pub, &data, &sig),
+ DST_R_VERIFYFAILURE);
+ EVP_MD_CTX_free(mdctx);
+ }
+#endif
+ dst_key_free(&pub);
+ dst_key_free(&key);
+}
+
+#ifdef HAVE_OPENSSL_MLDSA44
+/*
+ * The private key file holds the seed, so generated and loaded keys must
+ * be writable even when the provider is configured to discard the seed.
+ */
+ISC_RUN_TEST_IMPL(mldsa_noretain) {
+ dns_fixedname_t fname;
+ dns_name_t *name = dns_fixedname_initname(&fname);
+ dst_key_t *key = NULL, *loaded = NULL;
+ EVP_KEYMGMT *keymgmt = NULL;
+ OSSL_PROVIDER *prov = NULL;
+ char keydir[] = BUILDDIR "/mldsa_noretain.XXXXXX";
+ char filename[PATH_MAX] = { 0 };
+ isc_buffer_t buf;
+
+ if (!dst_algorithm_supported(DST_ALG_MLDSA44)) {
+ skip();
+ }
+
+ keymgmt = EVP_KEYMGMT_fetch(NULL, "ML-DSA-44", NULL);
+ assert_non_null(keymgmt);
+ prov = UNCONST(EVP_KEYMGMT_get0_provider(keymgmt));
+ /* OpenSSL 4.0 keeps the seed of generated keys regardless. */
+ assert_int_equal(OSSL_PROVIDER_add_conf_parameter(
+ prov, "ml-dsa.retain_seed", "no"),
+ 1);
+
+ assert_non_null(mkdtemp(keydir));
+ assert_int_equal(dns_name_fromstring(name, "example.", dns_rootname, 0,
+ isc_g_mctx),
+ ISC_R_SUCCESS);
+ assert_int_equal(
+ dst_key_generate(name, DST_ALG_MLDSA44, 0, 0, DNS_KEYOWNER_ZONE,
+ DNS_KEYPROTO_DNSSEC, dns_rdataclass_in, NULL,
+ isc_g_mctx, &key, NULL),
+ ISC_R_SUCCESS);
+
+ /* Write a generated key, as dnssec-keygen does. */
+ assert_int_equal(
+ dst_key_tofile(key, DST_TYPE_PUBLIC | DST_TYPE_PRIVATE, keydir),
+ ISC_R_SUCCESS);
+
+ /* Read it back and rewrite it twice, as dnssec-settime does. */
+ for (size_t i = 0; i < 2; i++) {
+ assert_int_equal(
+ dst_key_fromfile(name, dst_key_id(key), DST_ALG_MLDSA44,
+ DST_TYPE_PUBLIC | DST_TYPE_PRIVATE,
+ keydir, isc_g_mctx, &loaded),
+ ISC_R_SUCCESS);
+ assert_true(dst_key_compare(key, loaded));
+ assert_int_equal(
+ dst_key_tofile(loaded,
+ DST_TYPE_PUBLIC | DST_TYPE_PRIVATE,
+ keydir),
+ ISC_R_SUCCESS);
+ dst_key_free(&loaded);
+ }
+
+ int types[] = { DST_TYPE_PUBLIC, DST_TYPE_PRIVATE };
+ for (size_t i = 0; i < ARRAY_SIZE(types); i++) {
+ isc_buffer_init(&buf, filename, sizeof(filename));
+ assert_int_equal(
+ dst_key_buildfilename(key, types[i], keydir, &buf),
+ ISC_R_SUCCESS);
+ assert_int_equal(remove(filename), 0);
+ }
+ assert_int_equal(rmdir(keydir), 0);
+
+ assert_int_equal(OSSL_PROVIDER_add_conf_parameter(
+ prov, "ml-dsa.retain_seed", "yes"),
+ 1);
+ dst_key_free(&key);
+ EVP_KEYMGMT_free(keymgmt);
+}
+#endif /* HAVE_OPENSSL_MLDSA44 */
+
ISC_TEST_LIST_START
ISC_TEST_ENTRY(algorithm_fromdata)
ISC_TEST_ENTRY(algorithm_format)
ISC_TEST_ENTRY(algorithm_totext)
ISC_TEST_ENTRY(key_format)
ISC_TEST_ENTRY(sig_test)
+ISC_TEST_ENTRY(mldsa_wire)
+#ifdef HAVE_OPENSSL_MLDSA44
+ISC_TEST_ENTRY(mldsa_noretain)
+#endif /* HAVE_OPENSSL_MLDSA44 */
ISC_TEST_ENTRY(cmp_test)
ISC_TEST_ENTRY(ecdsa_determinism_test)
ISC_TEST_LIST_END
diff --git a/tests/dns/testdata/dst/Kexample.com.+018+59829.key b/tests/dns/testdata/dst/Kexample.com.+018+59829.key
new file mode 100644
index 0000000000..7ae4b038bf
--- /dev/null
+++ b/tests/dns/testdata/dst/Kexample.com.+018+59829.key
@@ -0,0 +1,32 @@
+; draft-westerbaan-dnssec-mldsa-04, Section 6
+example.com. 3600 IN DNSKEY 257 3 18 (
+ 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/tests/dns/testdata/dst/Kexample.com.+018+59829.private b/tests/dns/testdata/dst/Kexample.com.+018+59829.private
new file mode 100644
index 0000000000..7547d0b49b
--- /dev/null
+++ b/tests/dns/testdata/dst/Kexample.com.+018+59829.private
@@ -0,0 +1,3 @@
+Private-key-format: v1.3
+Algorithm: 18 (MLDSA44)
+PrivateKey: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
diff --git a/tests/dns/testdata/dst/mldsa.data b/tests/dns/testdata/dst/mldsa.data
new file mode 100644
index 0000000000..863f0a5343
Binary files /dev/null and b/tests/dns/testdata/dst/mldsa.data differ
diff --git a/tests/dns/testdata/dst/mldsa.md b/tests/dns/testdata/dst/mldsa.md
new file mode 100644
index 0000000000..a7ed00b833
--- /dev/null
+++ b/tests/dns/testdata/dst/mldsa.md
@@ -0,0 +1,10 @@
+The ML-DSA-44 key and signature are the example from Section 6 of
+[draft-westerbaan-dnssec-mldsa-04](https://www.ietf.org/archive/id/draft-westerbaan-dnssec-mldsa-04.html#section-6).
+The private key is the 32-byte seed 00, 01, ..., 1f. The public key's key tag
+is 59829. `mldsa.sig` is the hex encoding of the published signature, and
+`mldsa.data` is the RFC 4034 Section 3.1.8.1 wire encoding of the example's
+RRSIG fields and MX RRset. No signature is regenerated for this fixture.
+
+These four test-vector files are Copyright (c) 2026 IETF Trust and the
+persons identified as the document authors. All rights reserved. They are
+provided under the [Revised BSD License](../../../../LICENSES/BSD-3-Clause.txt).
diff --git a/tests/dns/testdata/dst/mldsa.sig b/tests/dns/testdata/dst/mldsa.sig
new file mode 100644
index 0000000000..8e95e91c87
--- /dev/null
+++ b/tests/dns/testdata/dst/mldsa.sig
@@ -0,0 +1,76 @@
+91DC921F3C01ECD7ED8D048017BB2709E29ABB7368AA92CD835EA1FDE1D957C2
+F76698B7D25911CA24B814B30C66A4E3CDB7F503F6D2860E2A6696259DFA9737
+CBAA15617D1841093A0B5ED4215D178182D39804054F3C2ABC9F9C601CA5729A
+7F0F61B43C59AEA9AA55258DFB582E5BB1F2514168D48B964E764ABA1669B432
+64ABE325FB9647CB8A7ED9377093C307EED2654D893F51568CD702173C1D9880
+9FBE903D33872B39737F2BCED71A3A6317B7A7A3C80118DFB0DCD59FCAB2B450
+656BFE5D9FC24E58AE87E746247A06B939B92B04B4BACF3CE2E916342FF0214F
+D27651A8043557B13D7ADD3AFA9537FCA49E318B170A20DF27B0A485C4FF63F8
+06DA3794A87C802FA782C62052E030AB1157F72A798A3C3DE8150F6121010DBF
+1D54CEDEE3BCBE5A8B2BB9D65552B6FB66982882F612ACE385160A5390E13304
+BD6606C6D278ACCC65E9BD970D70EC80057CAC755B9A0E39D4B9A99854008E52
+B57BF3A2CD9ADF085839D75A83CF304ACF939BCDC87ABAF991E0EFD51E6694B3
+C7401EFF56887E2D9612D6BD3ABE6C188B419FABC55A2F64251BA1BA83085DFF
+422C157F2C72FF3C87A70715D29EBD8EC20E7802E48DDAD9A4DA50F2F017CACF
+3BF14CEA0A5F1E1E768F7A5EFE90580480C4EF499B212F86717748123A1C6DDF
+9F6FA4F2B8092C81E9B2BE29661321E3F49059AA23C6DE759C42058B56E5ECFE
+AC02609D30FF352D2371D3984890A70F78EDF7840D954833BB1B3384B2704ECD
+B8545FD81B16BDDA355FD6ADBB3F7193002F523420A1B6D2392611F67D3DE28D
+D215C2C47268E213C52F69A6F5C886A6E5D2EE837C7FB62E6E5353C1163C6F8A
+6549C5480A9C814F95033A07B347DFFE0E79252F47F320E44C4D19E90F586AE0
+1A9932034CC2F733160DC3AD22D03FF70188A2E7DEDA09AE44AE53D697A1D12A
+5B796145D32F0D4A93B4780C29AB39BDE96669BA443406E2C6207C9F9CE810D9
+CC2D5EB0FF5DDAFB23934F6594BC44A01EA17DD857D031D425DFA9E8C9EC268D
+4159A298AB08657A02479B755B13636EC3B5890659A98B5DC9B7C3677A01857F
+2ECB2FC8AFC9967C3F13B61125690EFBC3EF1C0DADBDF412AFC7CD0B8965FCA1
+DDC2BF1DD10F2CA4F70E1CC32E8B6D170D87A9ACF9A9EB8852B78201BA242EB1
+D9B7490B168D0BDA52C83CB32919776E12FF0350886B305DAA06EA859860266E
+A5FBC6734B206FEF6AC595397D20182B0C792E361A8EBBAF4397E579B02DAD47
+4BB8F6C631BD88ED9F1CE08BC399B03A963A8C2BAD30D5723EC14EE3496CE811
+A888780511D5A4FDDB9285304866146DE44EBE593C8E7BC0CEE9108F6304C13C
+D25BBB0F77F8CFEFA27121D227960D6BFFA4A7B792213F789B84E3EE77A598DD
+169CA160CC664A9EE8831897B0E451677C1D2A7EFA943543EDD6B55A22BEEBCC
+104BE23F16407A8EA35EC0FB4A6722A2937B567C3311673F03F57961DADD3FFB
+25E10F483E4914EA445B1B35DADC314C2E3606B4C27244A6ADF9A4B1FC6625AD
+267DF95A38F1E48DA853A6BAC8B49CC6D5E7C7602BB99CFE6BB1C95917484339
+1D4E66B80AD888671B5568B483C1E3EFFEEF4FAE0F4A9CA98DCBDB2A27C45950
+4BCB0498C7178BDC504F17451B834E93F4D01B9D0E972D6D469F54A0B8F04C3B
+6522387BD4BCFD95A8DB01A6D5E16B48BDDEDCC1A4443B490502DCC881F25154
+C6AC5AF528E9EF77A90053873BE408DB3C9E0B1AD235C92F51EB1113FEAA4894
+2C20CCAF5F45562996988CCB0A447DA60F565A3AEE63992B81D55B20D52A8ECC
+B21AB88F121CB626778D59D39D168030916B466C8A50847B366CB1E91BDC4F3C
+5F8496A2E68CD6BBE81857BF871ABAB1D89FB2B49B834C5600F2C065E8874386
+7BF3E85D0E201EDEC46C781B0903BD57CFBFD7E316C3D66B53A6ED3862762555
+DEC915D8C896AB9FE7273CBD6758E96AEDB303DE66B33933593DCE5410D4AF0C
+BE75866777583EF58B6BBD6EDA88CE39E64B18203C746C55C39A1971CCDDA509
+9EB0EDC22D4463B4C79496AD4BE22D8D640C08387C83FE0B17D4B6516185736D
+4C8A6B305AF821D6307FE21608F42F0F482A40795DBA757A00367B51FE52E6B3
+8DB8F3C755275C323468DA34D5F58AD8AA158B0D527DCBC93DD375552007091F
+CCED48E149E10C86EC1C959BB3B0E5FF9D848B831BA4F2579CBD6030D73A483D
+44913D42798F5F7AC1C5DB4C2BF2D6FB57EA8F3B64C2136EACC81F65678D4E48
+E620A8F60AFCE1F2A0B79D4264CE165B3440C8F61746E4B37A499BF4C30CC12E
+51D86E94F6AD9D904D2177B1A8D2264141728A9709F56E4A9C3E96AF9FA3C942
+F2541411A563F38A97CF14009BE5DE6558FB6BBE1E24BAB62DDEC49D532F9624
+AB986F5AE8C574D926A3F440382754460BE67267C6A4910FD7F1EC73111D7383
+9158A8045992520C0FE570658E7FD8A48806F08C78DA1C8A302B62CAFD60218D
+FF9BC71F1F28F923AC5C0C7513647C8CF7E29265E8A97FAC149E782866E6333F
+D377368CE8F0B23EC37328EACFD58E19AC00B39872EB3BDB4B34310639A6B864
+719614BE0BFD60BF51FB5166ECAC72DDC8E99C6AB25B0C4DE9898DC3F7FA0BED
+B50E57B1EFED3DA36CA08B4335344659D0585D6819B1571B528DB066DEE2AEF6
+D696D333B9DF81D79627C59317FA1A16E8C5B19D9B5C9E9152230CB9C2FA2A1D
+4322F0222D2FBDCE37E5793819106CF03E4FC5FDEF4F7B603F2E6C0D76C9DE54
+F9F0C7A429D4FF1E86EE80339DBB496D19E9C58E728585D26E76F43F1458EA26
+4D476278F1C5D3F74182A54873AFD800ED46A808D151CB2661023A27D8AFDD2E
+5A6975BB3694CCAD174EE966F1F9FA95FA45DC4589612B2AD625D05AE891C3DB
+BA771892D36F9CE19F0DCEE9A8878F16FB3D06172FAA0A5CA1A9D79D03A93759
+92098BDD7D7B96F6E268F9AEACDC82B03A28CAA74C90CA87672EA0A0B57C8F3C
+C3BEF8728D173C3CA0FAFFD10F6113D5FAAE0C61E3795C9506C927359439970B
+CB780CBFB87F90979A84090F5608824FC9D659841F74EA190767EBDC8757C6FA
+F86032DA1786D678517CD0C1B2412FFE30C6D39ABB72205869091089893CADCA
+13EEE985F9F96261928D50A9F28D6DD90C69C09D593C1B93AD45B2D7982BB883
+FC7B8D797F450F5198C51BEA7A26AB14CDA073819BC083D9D8FEF1C40B4B17B8
+2EE59EA983C6BF0B53D24C0FBED7AC171C5CC3F5F7D77FD746C95BC4C7FB2CEA
+138B328DD50F11A9509F03FF7E1F6EB3DD9E49DBF9D2FAF4FCA3E1CDF5B3711C
+16171A1F4B5E739497B7C4D8FE0405111C20292A30648797A6B8BCBEC3DCE7EA
+F9FAFD0B1B5D6A7A85959CA0B3BACAD2DFEBF1FE0E182E373B3F54555B839497
+A3ADB0CED3E8000000000000000000000D233446