Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dns/dnssec.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"ec.EllipticCurvePublicKey",
"ed25519.Ed25519PublicKey",
"ed448.Ed448PublicKey",
"mldsa.MLDSA44PublicKey",
]

PrivateKey = Union[
Expand All @@ -65,6 +66,7 @@
"ec.EllipticCurvePrivateKey",
"ed25519.Ed25519PrivateKey",
"ed448.Ed448PrivateKey",
"mldsa.MLDSA44PrivateKey",
]

RRsetSigner = Callable[[dns.transaction.Transaction, dns.rrset.RRset], None]
Expand Down Expand Up @@ -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
)

Expand Down
2 changes: 2 additions & 0 deletions dns/dnssecalgs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -41,6 +42,7 @@
(Algorithm.ECDSAP384SHA384, None): PrivateECDSAP384SHA384,
(Algorithm.ED25519, None): PrivateED25519,
(Algorithm.ED448, None): PrivateED448,
(Algorithm.MLDSA44, None): PrivateMLDSA44,
}
)

Expand Down
58 changes: 58 additions & 0 deletions dns/dnssecalgs/mldsa.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions dns/dnssectypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Algorithm(dns.enum.IntEnum):
ECDSAP384SHA384 = 14
ED25519 = 15
ED448 = 16
MLDSA44 = 18
INDIRECT = 252
PRIVATEDNS = 253
PRIVATEOID = 254
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
135 changes: 134 additions & 1 deletion tests/test_dnssec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions tests/test_dnssecalgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down