diff --git a/dns/dnssec.py b/dns/dnssec.py index a6949c02..09e8d254 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -57,6 +57,7 @@ "ec.EllipticCurvePublicKey", "ed25519.Ed25519PublicKey", "ed448.Ed448PublicKey", + "mldsa.MLDSA44PublicKey", ] PrivateKey = Union[ @@ -65,6 +66,7 @@ "ec.EllipticCurvePrivateKey", "ed25519.Ed25519PrivateKey", "ed448.Ed448PrivateKey", + "mldsa.MLDSA44PrivateKey", ] RRsetSigner = Callable[[dns.transaction.Transaction, dns.rrset.RRset], None] @@ -1133,6 +1135,7 @@ def _need_pyca(*args, **kwargs): ec, # pylint: disable=W0611 ed448, # pylint: disable=W0611 ed25519, + mldsa, # pylint: disable=W0611 rsa, # pylint: disable=W0611 ) diff --git a/dns/dnssecalgs/__init__.py b/dns/dnssecalgs/__init__.py index 7f4e0879..8574572c 100644 --- a/dns/dnssecalgs/__init__.py +++ b/dns/dnssecalgs/__init__.py @@ -11,6 +11,7 @@ from dns.dnssecalgs.dsa import PrivateDSA, PrivateDSANSEC3SHA1 from dns.dnssecalgs.ecdsa import PrivateECDSAP256SHA256, PrivateECDSAP384SHA384 from dns.dnssecalgs.eddsa import PrivateED448, PrivateED25519 + from dns.dnssecalgs.mldsa import PrivateMLDSA44 from dns.dnssecalgs.rsa import ( PrivateRSAMD5, PrivateRSASHA1, @@ -41,6 +42,7 @@ (Algorithm.ECDSAP384SHA384, None): PrivateECDSAP384SHA384, (Algorithm.ED25519, None): PrivateED25519, (Algorithm.ED448, None): PrivateED448, + (Algorithm.MLDSA44, None): PrivateMLDSA44, } ) diff --git a/dns/dnssecalgs/mldsa.py b/dns/dnssecalgs/mldsa.py new file mode 100644 index 00000000..7d2e94b2 --- /dev/null +++ b/dns/dnssecalgs/mldsa.py @@ -0,0 +1,58 @@ +from typing import Type + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import mldsa + +from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey +from dns.dnssectypes import Algorithm +from dns.rdtypes.ANY.DNSKEY import DNSKEY + + +class PublicMLDSA(CryptographyPublicKey): + def verify(self, signature: bytes, data: bytes) -> None: + self.key.verify(signature, data) + + def encode_key_bytes(self) -> bytes: + """Encode a public key per draft-westerbaan-dnssec-mldsa, section 3.""" + return self.key.public_bytes( + encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw + ) + + @classmethod + def from_dnskey(cls, key: DNSKEY) -> "PublicMLDSA": + cls._ensure_algorithm_key_combination(key) + return cls( + key=cls.key_cls.from_public_bytes(key.key), + ) + + +class PrivateMLDSA(CryptographyPrivateKey): + public_cls: Type[PublicMLDSA] # pyright: ignore + + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: + """Sign using a private key per draft-westerbaan-dnssec-mldsa, section 4.""" + signature = self.key.sign(data) + if verify: + self.public_key().verify(signature, data) + return signature + + @classmethod + def generate(cls) -> "PrivateMLDSA": + return cls(key=cls.key_cls.generate()) + + +class PublicMLDSA44(PublicMLDSA): + key: mldsa.MLDSA44PublicKey + key_cls = mldsa.MLDSA44PublicKey + algorithm = Algorithm.MLDSA44 + + +class PrivateMLDSA44(PrivateMLDSA): + key: mldsa.MLDSA44PrivateKey + key_cls = mldsa.MLDSA44PrivateKey + public_cls = PublicMLDSA44 diff --git a/dns/dnssectypes.py b/dns/dnssectypes.py index 02131e0a..6e15ce9c 100644 --- a/dns/dnssectypes.py +++ b/dns/dnssectypes.py @@ -38,6 +38,7 @@ class Algorithm(dns.enum.IntEnum): ECDSAP384SHA384 = 14 ED25519 = 15 ED448 = 16 + MLDSA44 = 18 INDIRECT = 252 PRIVATEDNS = 253 PRIVATEOID = 254 diff --git a/pyproject.toml b/pyproject.toml index 1982228a..44f7eb05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dev = [ "sphinx-rtd-theme>=3.1.0 ; python_full_version >= '3.12'", "ty>=0.0.63", ] -dnssec = ["cryptography>=49"] +dnssec = ["cryptography>=50"] doh = ["httpcore2>=2.9", "httpx2>=2.9", "h2>=4.4"] doq = ["aioquic>=1.3.0"] idna = ["idna>=3.18"] diff --git a/tests/test_dnssec.py b/tests/test_dnssec.py index dd2b44ea..9902d9fd 100644 --- a/tests/test_dnssec.py +++ b/tests/test_dnssec.py @@ -39,7 +39,14 @@ try: from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives.asymmetric import dsa, ec, ed448, ed25519, rsa + from cryptography.hazmat.primitives.asymmetric import ( + dsa, + ec, + ed448, + ed25519, + mldsa, + rsa, + ) from cryptography.hazmat.primitives.serialization import load_pem_private_key except ImportError: pass # Cryptography ImportError already handled in dns.dnssec @@ -448,6 +455,113 @@ def default_backend(): "sQ4f7qI1gVnML8vWWiyW2KXhT9kuAICUSxv5OWbf81Rq7Yu60npabODB0QFPb/rkW3kUZmQ0YQUA", ) +abs_mldsa44_mx = dns.rrset.from_text( + "example.com.", 3600, "IN", "MX", "10 mail.example.com." +) +abs_other_mldsa44_mx = dns.rrset.from_text( + "example.com.", 3600, "IN", "MX", "11 mail.example.com." +) +abs_mldsa44_keys_1 = { + abs_example_com: dns.rrset.from_text( + "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==", + ) +} +abs_mldsa44_mx_rrsig_1 = dns.rrset.from_text( + "example.com.", + 3600, + "IN", + "RRSIG", + "MX 18 2 3600 1440021600 1438207200 59829 example.com. " + "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", +) + when5 = 1440021600 when5_start = 1438207200 @@ -802,6 +916,21 @@ def testAbsoluteED448Bad(self): # type: () -> None abs_other_ed448_mx, abs_ed448_mx_rrsig_2, abs_ed448_keys_2, None, when5 ) + def testAbsoluteMLDSA44Good(self): # type: () -> None + dns.dnssec.validate( + abs_mldsa44_mx, abs_mldsa44_mx_rrsig_1, abs_mldsa44_keys_1, None, when5 + ) + + def testAbsoluteMLDSA44Bad(self): # type: () -> None + with self.assertRaises(dns.dnssec.ValidationFailure): + dns.dnssec.validate( + abs_other_mldsa44_mx, + abs_mldsa44_mx_rrsig_1, + abs_mldsa44_keys_1, + None, + when5, + ) + def testAbsoluteRSASHA512Good(self): dns.dnssec.validate( rsasha512_ns, rsasha512_ns_rrsig, rsasha512_keys, None, rsasha512_when @@ -1478,6 +1607,10 @@ def testSignatureED448(self): # type: () -> None key = ed448.Ed448PrivateKey.generate() self._test_signature(key, dns.dnssec.Algorithm.ED448, abs_soa) + def testSignatureMLDSA44(self): # type: () -> None + key = mldsa.MLDSA44PrivateKey.generate() + self._test_signature(key, dns.dnssec.Algorithm.MLDSA44, abs_soa) + def testSignRdataset(self): # type: () -> None key = ed448.Ed448PrivateKey.generate() name = dns.name.from_text("example.com") diff --git a/tests/test_dnssecalgs.py b/tests/test_dnssecalgs.py index 9982f0d1..a8d58172 100644 --- a/tests/test_dnssecalgs.py +++ b/tests/test_dnssecalgs.py @@ -32,6 +32,7 @@ from dns.dnssecalgs.dsa import PrivateDSA, PrivateDSANSEC3SHA1 from dns.dnssecalgs.ecdsa import PrivateECDSAP256SHA256, PrivateECDSAP384SHA384 from dns.dnssecalgs.eddsa import PrivateED448, PrivateED25519, PublicED25519 + from dns.dnssecalgs.mldsa import PrivateMLDSA44, PublicMLDSA44 from dns.dnssecalgs.rsa import ( PrivateRSAMD5, PrivateRSASHA1, @@ -115,6 +116,9 @@ def test_eddsa(self): self._test_dnssec_alg(PrivateED25519) self._test_dnssec_alg(PrivateED448) + def test_mldsa(self): + self._test_dnssec_alg(PrivateMLDSA44) + def test_algorithm_mismatch(self): private_key_ed448 = PrivateED448.generate() dnskey_ed448 = private_key_ed448.public_key().to_dnskey()