]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add private_key support to all ZoneKey implementations
authorNicki Křížek <nicki@isc.org>
Tue, 16 Jun 2026 14:38:53 +0000 (16:38 +0200)
committerNicki Křížek <nicki@isc.org>
Thu, 2 Jul 2026 12:51:41 +0000 (14:51 +0200)
Extend the ZoneKeyFile to read the file-backed private key and return it
in a format suitable for use with dnspython. Add ZoneKey.private_key
property to unify the interface.

bin/tests/system/isctest/zone.py

index a7ba22db90a0732d8e59aceeabb0db2859a258ef..8f5a88662778e72d0dfa7e12a87771a12f1d7858 100644 (file)
@@ -16,6 +16,7 @@ from collections.abc import Callable
 from pathlib import Path
 from typing import TypeAlias
 
+import base64
 import shutil
 
 from cryptography.hazmat.primitives import serialization
@@ -32,7 +33,14 @@ import dns.zonefile
 from .log import debug
 from .run import EnvCmd
 from .template import NS1, Nameserver, TemplateEngine, TrustAnchor
-from .vars.algorithms import Algorithm
+from .vars.algorithms import (
+    ALL_ALGORITHMS_BY_NUM,
+    ECDSAP256SHA256,
+    ECDSAP384SHA384,
+    ED448,
+    ED25519,
+    Algorithm,
+)
 
 KEYDIR = "keys"
 DNSKEY_TTL = 3600
@@ -61,7 +69,16 @@ class ZoneKey(ABC):
     @property
     @abstractmethod
     def dnskey(self) -> dns.rrset.RRset:
-        """The DNSKEY RRset for this key (single-record, with TTL)."""
+        """
+        The DNSKEY RRset for this key (single-record, with TTL).
+        """
+
+    @property
+    @abstractmethod
+    def private_key(self) -> PrivateKey:
+        """
+        The dnspython private-key object, for use with dns.dnssec signing.
+        """
 
     def is_ksk(self) -> bool:
         return bool(self.dnskey[0].flags & int(Flag.SEP))
@@ -142,6 +159,53 @@ class FileZoneKey(ZoneKey):
         ), f"DNSKEY not found in {self.keyfile}"
         return dnskey_rr
 
+    @property
+    def private_key(self) -> PrivateKey:
+        """
+        Read the .private file and build the corresponding cryptography
+        private-key object, dispatching on the DNSKEY algorithm.
+
+        Supports the RSA, ECDSA and EdDSA families; other algorithms raise
+        NotImplementedError.
+        """
+        alg = ALL_ALGORITHMS_BY_NUM[self.dnskey[0].algorithm]
+
+        fields = {}
+        with open(self.privatefile, "r", encoding="utf-8") as file:
+            for line in file:
+                name, sep, value = line.partition(":")
+                if sep:
+                    fields[name.strip()] = value.strip()
+
+        def field(name: str) -> bytes:
+            return base64.b64decode(fields[name])
+
+        if "Modulus" in fields:  # RSA family, including the private-OID variants
+            public = rsa.RSAPublicNumbers(
+                int.from_bytes(field("PublicExponent"), "big"),
+                int.from_bytes(field("Modulus"), "big"),
+            )
+            return rsa.RSAPrivateNumbers(
+                p=int.from_bytes(field("Prime1"), "big"),
+                q=int.from_bytes(field("Prime2"), "big"),
+                d=int.from_bytes(field("PrivateExponent"), "big"),
+                dmp1=int.from_bytes(field("Exponent1"), "big"),
+                dmq1=int.from_bytes(field("Exponent2"), "big"),
+                iqmp=int.from_bytes(field("Coefficient"), "big"),
+                public_numbers=public,
+            ).private_key()
+
+        secret = field("PrivateKey")
+        if alg == ECDSAP256SHA256:
+            return ec.derive_private_key(int.from_bytes(secret, "big"), ec.SECP256R1())
+        if alg == ECDSAP384SHA384:
+            return ec.derive_private_key(int.from_bytes(secret, "big"), ec.SECP384R1())
+        if alg == ED25519:
+            return ed25519.Ed25519PrivateKey.from_private_bytes(secret)
+        if alg == ED448:
+            return ed448.Ed448PrivateKey.from_private_bytes(secret)
+        raise NotImplementedError(f"dnskey algorithm {alg.name} not implemented")
+
     def write_dsset(
         self,
         target_dir: Path | str,
@@ -215,10 +279,14 @@ class PythonZoneKey(ZoneKey):
         ttl: int = DNSKEY_TTL,
     ) -> None:
         self.zone = zone
-        self.private_key = private_key
+        self._private_key = private_key
         self._dnskey_rdata = dnskey_rdata
         self.ttl = ttl
 
+    @property
+    def private_key(self) -> PrivateKey:
+        return self._private_key
+
     @property
     def dnskey(self) -> dns.rrset.RRset:
         rrset = dns.rrset.RRset(