]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Add support for deterministic signatures (#1104)
authorJakob Schlyter <jakob@kirei.se>
Wed, 24 Jul 2024 00:58:13 +0000 (02:58 +0200)
committerGitHub <noreply@github.com>
Wed, 24 Jul 2024 00:58:13 +0000 (17:58 -0700)
Add support for deterministic signatures and make them by default for ECDSA.

dns/dnssec.py
dns/dnssecalgs/base.py
dns/dnssecalgs/dsa.py
dns/dnssecalgs/ecdsa.py
dns/dnssecalgs/eddsa.py
dns/dnssecalgs/rsa.py
tests/test_dnssec.py

index 6a7b78eabd0d6b0bc04a8731213dad9f1ed4540a..b6d146dc0fd613772fef5ddd6cb88dd1058250c9 100644 (file)
@@ -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)
 
index e990575a30ca19a9679ff2e3125f039d4c245b3e..752ee48069b4c2d366369d620eda8cff1f37160e 100644 (file)
@@ -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
index 0fe4690d39ec9f26caf1221146cf5309676e0173..1c84f49fd1ff184a319fc670bbeacefd6a1f9452 100644 (file)
@@ -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:
index a31d79f2b8ee461bc6ae736c13372b2013abd3c6..6845a039938d077ad380125f7b3d43705073d10f 100644 (file)
@@ -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"
index 705053423998b0df1944f42617b0ed85655d94e1..d70923fc4fb48ff7459b4f3c3e0c33bb41718002 100644 (file)
@@ -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:
index e95dcf1ddc45ad7c2731b258f5edd3abd34e5248..935edadc1a2d5c31af22ad641e13fba8718444ae 100644 (file)
@@ -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:
index c2cdb8ecbece8695c7f97f173cce2f81ee77afe6..ce468d7083bea79e7e783b54355ab5d0b595a33b 100644 (file)
@@ -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}