diff --git a/APIDOCS.md b/APIDOCS.md
index 058f75112..54383cbf3 100644
--- a/APIDOCS.md
+++ b/APIDOCS.md
@@ -2673,13 +2673,13 @@ WHERE:
- `token`: The session token generated by the `login` or the `createToken` call.
- `node` (optional): The node domain name for which the this API call is intended. When unspecified, the current node is used. This parameter can be used only when Clustering is initialized.
- `zone`: The name of the primary zone to sign.
-- `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`].
+- `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`, `MLDSA`].
- `pemKskPrivateKey` (optional): The user specified private key in PEM format for Key Signing Key (KSK). When this parameter is specified, the private key specified is used instead of automatically generating it.
- `pemZskPrivateKey` (optional): The user specified private key in PEM format for Zone Signing Key (ZSK). When this parameter is specified, the private key specified is used instead of automatically generating it.
- `hashAlgorithm` (optional): The hash algorithm to be used when using `RSA` algorithm. Valid values are [`MD5`, `SHA1`, `SHA256`, `SHA512`]. This optional parameter is required when using `RSA` algorithm.
- `kskKeySize` (optional): The size of the Key Signing Key (KSK) in bits to be used when using `RSA` algorithm. This optional parameter is required when using `RSA` algorithm.
- `zskKeySize` (optional): The size of the Zone Signing Key (ZSK) in bits to be used when using `RSA` algorithm. This optional parameter is required when using `RSA` algorithm.
-- `curve` (optional): The name of the curve to be used when using `ECDSA` or `EDDSA` algorithm. Valid values are [`P256`, `P384`] for `ECDSA` algorithm and [`ED25519`, `ED448`] for `EDDSA` algorithm. This optional parameter is required when using `ECDSA` or `EDDSA` algorithm.
+- `curve` (optional): The name of the curve to be used when using `ECDSA` or `EDDSA` algorithm. Valid values are [`P256`, `P384`] for `ECDSA` algorithm and [`ED25519`, `ED448`] for `EDDSA` algorithm. This optional parameter is required when using `ECDSA` or `EDDSA` algorithm. The `MLDSA` algorithm does not use this parameter; it always generates an ML-DSA-44 key.
- `dnsKeyTtl` (optional): The TTL value to be used for DNSKEY records. Default value is `86400` when not specified.
- `zskRolloverDays` (optional): The frequency in days that the DNS server must automatically rollover the Zone Signing Keys (ZSK) in the zone. Valid range is 0-365 days where 0 disables rollover. Default value is `30` when not specified.
- `nxProof` (optional): The type of proof of non-existence that must be used for signing the zone. Valid values are [`NSEC`, `NSEC3`]. Default value is `NSEC` when not specified.
@@ -2975,11 +2975,11 @@ WHERE:
- `zone`: The name of the primary zone.
- `keyType`: The type of key for which the private key is to be generated. Valid values are [`KeySigningKey`, `ZoneSigningKey`].
- `rolloverDays` (optional): The frequency in days that the DNS server must automatically rollover the private key in the zone. Valid range is 0-365 days where 0 disables rollover. Default value is 90 days for Zone Signing Key (ZSK) and 0 days for Key Signing Key (KSK).
-- `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`].
+- `algorithm`: The algorithm to be used for signing. Valid values are [`RSA`, `ECDSA`, `EDDSA`, `MLDSA`].
- `pemPrivateKey` (optional): Specifies a user generated private key in PEM format to add. When not specified a private key will be automatically generated.
- `hashAlgorithm` (optional): The hash algorithm to be used when using `RSA` algorithm. Valid values are [`MD5`, `SHA1`, `SHA256`, `SHA512`]. This optional parameter is required when using `RSA` algorithm.
- `keySize` (optional): The size of the generated private key in bits to be used when using `RSA` algorithm. This optional parameter is required when using `RSA` algorithm.
-- `curve` (optional): The name of the curve to be used when using `ECDSA` or `EDDSA` algorithm. Valid values are [`P256`, `P384`] for `ECDSA` algorithm and [`ED25519`, `ED448`] for `EDDSA` algorithm. This optional parameter is required when using `ECDSA` or `EDDSA` algorithm.
+- `curve` (optional): The name of the curve to be used when using `ECDSA` or `EDDSA` algorithm. Valid values are [`P256`, `P384`] for `ECDSA` algorithm and [`ED25519`, `ED448`] for `EDDSA` algorithm. This optional parameter is required when using `ECDSA` or `EDDSA` algorithm. The `MLDSA` algorithm does not use this parameter; it always generates an ML-DSA-44 key.
RESPONSE:
```
@@ -3193,7 +3193,7 @@ WHERE:
- `naptrReplacement` (optional): This parameter is required for adding the `NAPTR` record.
- `dname` (optional): The DNAME domain name. This option is required for adding `DNAME` record.
- `keyTag` (optional): This parameter is required for adding `DS` record.
-- `algorithm` (optional): Valid values are [`RSAMD5`, `DSA`, `RSASHA1`, `DSA-NSEC3-SHA1`, `RSASHA1-NSEC3-SHA1`, `RSASHA256`, `RSASHA512`, `ECC-GOST`, `ECDSAP256SHA256`, `ECDSAP384SHA384`, `ED25519`, `ED448`]. This parameter is required for adding `DS` record.
+- `algorithm` (optional): Valid values are [`RSAMD5`, `DSA`, `RSASHA1`, `DSA-NSEC3-SHA1`, `RSASHA1-NSEC3-SHA1`, `RSASHA256`, `RSASHA512`, `ECC-GOST`, `ECDSAP256SHA256`, `ECDSAP384SHA384`, `ED25519`, `ED448`, `MLDSA44`]. This parameter is required for adding `DS` record.
- `digestType` (optional): Valid values are [`SHA1`, `SHA256`, `GOST-R-34-11-94`, `SHA384`]. This parameter is required for adding `DS` record.
- `digest` (optional): A hex string value. This parameter is required for adding `DS` record.
- `sshfpAlgorithm` (optional): Valid values are [`RSA`, `DSA`, `ECDSA`, `Ed25519`, `Ed448`]. This parameter is required for adding `SSHFP` record.
diff --git a/DnsServerCore/Dns/Dnssec/DnssecMldsaPrivateKey.cs b/DnsServerCore/Dns/Dnssec/DnssecMldsaPrivateKey.cs
new file mode 100644
index 000000000..03015dbda
--- /dev/null
+++ b/DnsServerCore/Dns/Dnssec/DnssecMldsaPrivateKey.cs
@@ -0,0 +1,96 @@
+/*
+Technitium DNS Server
+Copyright (C) 2026 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Crypto.Signers;
+using System.IO;
+using TechnitiumLibrary.IO;
+using TechnitiumLibrary.Net.Dns.Dnssec;
+using TechnitiumLibrary.Net.Dns.ResourceRecords;
+
+namespace DnsServerCore.Dns.Dnssec
+{
+ class DnssecMldsaPrivateKey : DnssecPrivateKey
+ {
+ #region variables
+
+ MLDsaPrivateKeyParameters _privateKey;
+
+ #endregion
+
+ #region constructor
+
+ public DnssecMldsaPrivateKey(DnssecPrivateKeyType keyType, MLDsaPrivateKeyParameters privateKey)
+ : base(DnssecAlgorithm.MLDSA44, keyType)
+ {
+ _privateKey = privateKey;
+
+ InitDnsKey();
+ }
+
+ public DnssecMldsaPrivateKey(DnssecAlgorithm algorithm, BinaryReader bR, int version)
+ : base(algorithm, bR, version)
+ {
+ InitDnsKey();
+ }
+
+ #endregion
+
+ #region private
+
+ private void InitDnsKey()
+ {
+ InitDnsKey(new DnssecMldsaPublicKey(_privateKey.GetPublicKeyEncoded()));
+ }
+
+ #endregion
+
+ #region protected
+
+ protected override byte[] SignHash(byte[] hash)
+ {
+ //pure ML-DSA-44 signing with an empty context; must match the verifier's signer construction
+ MLDsaSigner signer = new MLDsaSigner(MLDsaParameters.ml_dsa_44, false);
+ signer.Init(true, _privateKey);
+ signer.BlockUpdate(hash, 0, hash.Length);
+
+ return signer.GenerateSignature();
+ }
+
+ protected override void ReadPrivateKeyFrom(BinaryReader bR)
+ {
+ switch (Algorithm)
+ {
+ case DnssecAlgorithm.MLDSA44:
+ _privateKey = MLDsaPrivateKeyParameters.FromSeed(MLDsaParameters.ml_dsa_44, bR.ReadBuffer());
+ break;
+
+ default:
+ throw new InvalidDataException();
+ }
+ }
+
+ protected override void WritePrivateKeyTo(BinaryWriter bW)
+ {
+ bW.WriteBuffer(_privateKey.GetSeed());
+ }
+
+ #endregion
+ }
+}
diff --git a/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs b/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs
index e3a891604..82286b8c2 100644
--- a/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs
+++ b/DnsServerCore/Dns/Dnssec/DnssecPrivateKey.cs
@@ -211,6 +211,9 @@ public static DnssecPrivateKey Create(DnssecAlgorithm algorithm, DnssecPrivateKe
case DnssecAlgorithm.ED448:
return new DnssecEddsaPrivateKey(keyType, new Ed448PrivateKeyParameters(RandomNumberGenerator.GetBytes(57)));
+ case DnssecAlgorithm.MLDSA44:
+ return new DnssecMldsaPrivateKey(keyType, MLDsaPrivateKeyParameters.FromSeed(MLDsaParameters.ml_dsa_44, RandomNumberGenerator.GetBytes(32)));
+
default:
throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString());
}
@@ -275,6 +278,18 @@ public static DnssecPrivateKey Create(DnssecAlgorithm algorithm, DnssecPrivateKe
return new DnssecEddsaPrivateKey(keyType, privateKey);
}
+ case DnssecAlgorithm.MLDSA44:
+ using (PemReader pemReader = new PemReader(new StringReader(pemPrivateKey)))
+ {
+ if ((pemReader.ReadObject() is not MLDsaPrivateKeyParameters privateKey) || (privateKey.Parameters != MLDsaParameters.ml_dsa_44))
+ throw new ArgumentException($"The ML-DSA ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key must be for ML-DSA-44.", nameof(pemPrivateKey));
+
+ if (privateKey.GetSeed() is null)
+ throw new ArgumentException($"The ML-DSA-44 ({(keyType == DnssecPrivateKeyType.KeySigningKey ? "KSK" : "ZSK")}) private key must use the seed encoding.", nameof(pemPrivateKey));
+
+ return new DnssecMldsaPrivateKey(keyType, privateKey);
+ }
+
default:
throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString());
}
@@ -308,6 +323,9 @@ public static DnssecPrivateKey ReadFrom(BinaryReader bR)
case DnssecAlgorithm.ED448:
return new DnssecEddsaPrivateKey(algorithm, bR, version);
+ case DnssecAlgorithm.MLDSA44:
+ return new DnssecMldsaPrivateKey(algorithm, bR, version);
+
default:
throw new NotSupportedException("DNSSEC algorithm is not supported: " + algorithm.ToString());
}
diff --git a/DnsServerCore/Dns/Zones/PrimaryZone.cs b/DnsServerCore/Dns/Zones/PrimaryZone.cs
index dfbc4485b..d182ce4a6 100644
--- a/DnsServerCore/Dns/Zones/PrimaryZone.cs
+++ b/DnsServerCore/Dns/Zones/PrimaryZone.cs
@@ -1280,6 +1280,7 @@ public void RolloverDnsKey(ushort keyTag)
case DnssecAlgorithm.ECDSAP384SHA384:
case DnssecAlgorithm.ED25519:
case DnssecAlgorithm.ED448:
+ case DnssecAlgorithm.MLDSA44:
GenerateAndAddPrivateKey(privateKey.KeyType, privateKey.Algorithm, privateKey.RolloverDays);
break;
diff --git a/DnsServerCore/WebServiceZonesApi.cs b/DnsServerCore/WebServiceZonesApi.cs
index 6f31ae7ae..c99d47181 100644
--- a/DnsServerCore/WebServiceZonesApi.cs
+++ b/DnsServerCore/WebServiceZonesApi.cs
@@ -2285,6 +2285,22 @@ public void SignPrimaryZone(HttpContext context)
}
break;
+ case "MLDSA":
+ {
+ DnssecAlgorithm dnssecAlgorithm = DnssecAlgorithm.MLDSA44;
+
+ if (pemKskPrivateKey is null)
+ kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey);
+ else
+ kskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.KeySigningKey, pemKskPrivateKey);
+
+ if (pemZskPrivateKey is null)
+ zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey);
+ else
+ zskPrivateKey = DnssecPrivateKey.Create(dnssecAlgorithm, DnssecPrivateKeyType.ZoneSigningKey, pemZskPrivateKey);
+ }
+ break;
+
default:
throw new NotSupportedException("Algorithm is not supported: " + algorithm);
}
@@ -2718,6 +2734,24 @@ public void AddPrimaryZoneDnssecPrivateKey(HttpContext context)
}
break;
+ case "MLDSA":
+ {
+ DnssecAlgorithm dnssecAlgorithm = DnssecAlgorithm.MLDSA44;
+
+ if (pemPrivateKey is null)
+ {
+ privateKey = _dnsWebService._dnsServer.AuthZoneManager.GenerateAndAddPrimaryZoneDnssecPrivateKey(zoneName, keyType, dnssecAlgorithm, rolloverDays);
+ }
+ else
+ {
+ privateKey = DnssecPrivateKey.Create(dnssecAlgorithm, keyType, pemPrivateKey);
+ privateKey.RolloverDays = rolloverDays;
+
+ _dnsWebService._dnsServer.AuthZoneManager.AddPrimaryZoneDnssecPrivateKey(zoneName, privateKey);
+ }
+ }
+ break;
+
default:
throw new NotSupportedException("Algorithm is not supported: " + algorithm);
}
diff --git a/DnsServerCore/www/index.html b/DnsServerCore/www/index.html
index d6b819de5..946d6d6ef 100644
--- a/DnsServerCore/www/index.html
+++ b/DnsServerCore/www/index.html
@@ -4539,6 +4539,7 @@