From: Jakob Schlyter Date: Wed, 24 Jul 2024 00:58:13 +0000 (+0200) Subject: Add support for deterministic signatures (#1104) X-Git-Tag: v2.7.0rc1~39 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=83ad949528bae6d1640155f6b439c17a7a3b927a;p=thirdparty%2Fdnspython.git Add support for deterministic signatures (#1104) Add support for deterministic signatures and make them by default for ECDSA. --- diff --git a/dns/dnssec.py b/dns/dnssec.py index 6a7b78ea..b6d146dc 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -484,6 +484,7 @@ def _sign( verify: bool = False, policy: Optional[Policy] = None, origin: Optional[dns.name.Name] = None, + deterministic: bool = True, ) -> RRSIG: """Sign RRset using private key. @@ -523,6 +524,10 @@ def _sign( names in the rrset (including its owner name) must be absolute; otherwise the specified origin will be used to make names absolute when signing. + *deterministic*, a ``bool``. If ``True``, the default, use deterministic + (reproducible) signatures when supported by the algorithm used for signing. + Currently, this only affects ECDSA. + Raises ``DeniedByPolicy`` if the signature is denied by policy. """ @@ -589,7 +594,7 @@ def _sign( except UnsupportedAlgorithm: raise TypeError("Unsupported key algorithm") - signature = signing_key.sign(data, verify) + signature = signing_key.sign(data, verify, deterministic) return cast(RRSIG, rrsig_template.replace(signature=signature)) @@ -950,6 +955,7 @@ def default_rrset_signer( lifetime: Optional[int] = None, policy: Optional[Policy] = None, origin: Optional[dns.name.Name] = None, + deterministic: bool = True, ) -> None: """Default RRset signer""" @@ -975,6 +981,7 @@ def default_rrset_signer( signer=signer, policy=policy, origin=origin, + deterministic=deterministic, ) txn.add(rrset.name, rrset.ttl, rrsig) @@ -991,6 +998,7 @@ def sign_zone( nsec3: Optional[NSEC3PARAM] = None, rrset_signer: Optional[RRsetSigner] = None, policy: Optional[Policy] = None, + deterministic: bool = True, ) -> None: """Sign zone. @@ -1030,6 +1038,10 @@ def sign_zone( function requires two arguments: transaction and RRset. If the not specified, ``dns.dnssec.default_rrset_signer`` will be used. + *deterministic*, a ``bool``. If ``True``, the default, use deterministic + (reproducible) signatures when supported by the algorithm used for signing. + Currently, this only affects ECDSA. + Returns ``None``. """ @@ -1084,6 +1096,7 @@ def sign_zone( lifetime=lifetime, policy=policy, origin=zone.origin, + deterministic=deterministic, ) return _sign_zone_nsec(zone, _txn, _rrset_signer) diff --git a/dns/dnssecalgs/base.py b/dns/dnssecalgs/base.py index e990575a..752ee480 100644 --- a/dns/dnssecalgs/base.py +++ b/dns/dnssecalgs/base.py @@ -65,7 +65,12 @@ class GenericPrivateKey(ABC): pass @abstractmethod - def sign(self, data: bytes, verify: bool = False) -> bytes: + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: """Sign DNSSEC data""" @abstractmethod diff --git a/dns/dnssecalgs/dsa.py b/dns/dnssecalgs/dsa.py index 0fe4690d..1c84f49f 100644 --- a/dns/dnssecalgs/dsa.py +++ b/dns/dnssecalgs/dsa.py @@ -1,4 +1,5 @@ import struct +from typing import Optional from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes @@ -68,7 +69,12 @@ class PrivateDSA(CryptographyPrivateKey): key_cls = dsa.DSAPrivateKey public_cls = PublicDSA - def sign(self, data: bytes, verify: bool = False) -> bytes: + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: """Sign using a private key per RFC 2536, section 3.""" public_dsa_key = self.key.public_key() if public_dsa_key.key_size > 1024: diff --git a/dns/dnssecalgs/ecdsa.py b/dns/dnssecalgs/ecdsa.py index a31d79f2..6845a039 100644 --- a/dns/dnssecalgs/ecdsa.py +++ b/dns/dnssecalgs/ecdsa.py @@ -1,3 +1,5 @@ +from typing import Optional + from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, utils @@ -47,9 +49,17 @@ class PrivateECDSA(CryptographyPrivateKey): key_cls = ec.EllipticCurvePrivateKey public_cls = PublicECDSA - def sign(self, data: bytes, verify: bool = False) -> bytes: + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: """Sign using a private key per RFC 6605, section 4.""" - der_signature = self.key.sign(data, ec.ECDSA(self.public_cls.chosen_hash)) + algorithm = ec.ECDSA( + self.public_cls.chosen_hash, deterministic_signing=deterministic + ) + der_signature = self.key.sign(data, algorithm) dsa_r, dsa_s = utils.decode_dss_signature(der_signature) signature = int.to_bytes( dsa_r, length=self.public_cls.octets, byteorder="big" diff --git a/dns/dnssecalgs/eddsa.py b/dns/dnssecalgs/eddsa.py index 70505342..d70923fc 100644 --- a/dns/dnssecalgs/eddsa.py +++ b/dns/dnssecalgs/eddsa.py @@ -1,4 +1,4 @@ -from typing import Type +from typing import Optional, Type from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed448, ed25519 @@ -29,7 +29,12 @@ class PublicEDDSA(CryptographyPublicKey): class PrivateEDDSA(CryptographyPrivateKey): public_cls: Type[PublicEDDSA] - def sign(self, data: bytes, verify: bool = False) -> bytes: + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: """Sign using a private key per RFC 8080, section 4.""" signature = self.key.sign(data) if verify: diff --git a/dns/dnssecalgs/rsa.py b/dns/dnssecalgs/rsa.py index e95dcf1d..935edadc 100644 --- a/dns/dnssecalgs/rsa.py +++ b/dns/dnssecalgs/rsa.py @@ -1,5 +1,6 @@ import math import struct +from typing import Optional from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes @@ -56,7 +57,12 @@ class PrivateRSA(CryptographyPrivateKey): public_cls = PublicRSA default_public_exponent = 65537 - def sign(self, data: bytes, verify: bool = False) -> bytes: + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: """Sign using a private key per RFC 3110, section 3.""" signature = self.key.sign(data, padding.PKCS1v15(), self.public_cls.chosen_hash) if verify: diff --git a/tests/test_dnssec.py b/tests/test_dnssec.py index c2cdb8ec..ce468d70 100644 --- a/tests/test_dnssec.py +++ b/tests/test_dnssec.py @@ -1401,6 +1401,44 @@ class DNSSECSignatureTestCase(unittest.TestCase): key = ec.generate_private_key(curve=ec.SECP256R1(), backend=default_backend()) self._test_signature(key, dns.dnssec.Algorithm.ECDSAP256SHA256, abs_soa) + def testDeterministicSignatureECDSAP256SHA256(self): # type: () -> None + key = ec.generate_private_key(curve=ec.SECP256R1(), backend=default_backend()) + inception = time.time() + rrsigset1 = self._test_signature( + key, + dns.dnssec.Algorithm.ECDSAP256SHA256, + abs_soa, + inception=inception, + deterministic=True, + ) + rrsigset2 = self._test_signature( + key, + dns.dnssec.Algorithm.ECDSAP256SHA256, + abs_soa, + inception=inception, + deterministic=True, + ) + assert rrsigset1 == rrsigset2 + + def testNonDeterministicSignatureECDSAP256SHA256(self): # type: () -> None + key = ec.generate_private_key(curve=ec.SECP256R1(), backend=default_backend()) + inception = time.time() + rrsigset1 = self._test_signature( + key, + dns.dnssec.Algorithm.ECDSAP256SHA256, + abs_soa, + inception=inception, + deterministic=False, + ) + rrsigset2 = self._test_signature( + key, + dns.dnssec.Algorithm.ECDSAP256SHA256, + abs_soa, + inception=inception, + deterministic=False, + ) + assert rrsigset1 != rrsigset2 + def testSignatureECDSAP384SHA384(self): # type: () -> None key = ec.generate_private_key(curve=ec.SECP384R1(), backend=default_backend()) self._test_signature(key, dns.dnssec.Algorithm.ECDSAP384SHA384, abs_soa) @@ -1428,7 +1466,16 @@ class DNSSECSignatureTestCase(unittest.TestCase): rrsigset = self._test_signature(key, dns.dnssec.Algorithm.ED448, rrset) self.assertEqual(rrsigset[0].labels, 2) - def _test_signature(self, key, algorithm, rrset, signer=None, policy=None): + def _test_signature( + self, + key, + algorithm, + rrset, + signer=None, + policy=None, + inception=None, + deterministic=True, + ): ttl = 60 lifetime = 3600 if isinstance(rrset, tuple): @@ -1444,9 +1491,11 @@ class DNSSECSignatureTestCase(unittest.TestCase): rrset=rrset, private_key=key, dnskey=dnskey, + inception=inception, lifetime=lifetime, signer=signer, verify=True, + deterministic=deterministic, policy=policy, ) keys = {signer: dnskey_rrset}