]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
DNSSEC signer (#866)
authorJakob Schlyter <jakob@kirei.se>
Tue, 13 Dec 2022 01:28:00 +0000 (02:28 +0100)
committerGitHub <noreply@github.com>
Tue, 13 Dec 2022 01:28:00 +0000 (17:28 -0800)
* first cut at key_to_dnskey

* update docs

* typo

* use real test vectors for DNSKEY

* comment

* split

* add test for large exponent size

* rename to make_dnskey

* no default algorithm

* rename and add comment

* split out function to create rrsig signature data

* docs

* add type for public key

* more typing

* make RSA exponent key test easier to read

* work in progress for dns.dnssec.sign

* better docs

* docs

* simplify

* add test with RSASHA1

* initial support for DSA

* update docs

* clean up DSA, t still not clear

* allow inception/expiration to be specified as datetime, string, float or in

* allow rrset to be specified as a tuple

* calculate dsa_t

* reformat

* more rrset tuple fixes

* support DSA

* improve exception handling

* fix return type error

* fix typing issue to silence mypy

* make test case more verbose

* ensure UTC and use sigtime_to_posixtime to convert text to timestamp

dns/dnssec.py
tests/keys.py [new file with mode: 0644]
tests/test_dnssec.py

index 13415bdf306b17f1ce60bb9fdb6e49b74cf2fd13..c4aff9575e715746e6008522dd8b712fa79eeb82 100644 (file)
 from typing import Any, cast, Dict, List, Optional, Tuple, Union
 
 import hashlib
+import math
 import struct
 import time
 import base64
+from datetime import datetime, timezone
 
 from dns.dnssectypes import Algorithm, DSDigest, NSEC3Hash
 
@@ -36,17 +38,37 @@ import dns.rdataclass
 import dns.rrset
 from dns.rdtypes.ANY.DNSKEY import DNSKEY
 from dns.rdtypes.ANY.DS import DS
-from dns.rdtypes.ANY.RRSIG import RRSIG
+from dns.rdtypes.ANY.RRSIG import RRSIG, sigtime_to_posixtime
+from dns.rdtypes.dnskeybase import Flag
 
 
 class UnsupportedAlgorithm(dns.exception.DNSException):
     """The DNSSEC algorithm is not supported."""
 
 
+class AlgorithmKeyMismatch(UnsupportedAlgorithm):
+    """The DNSSEC algorithm is not supported for the given key type."""
+
+
 class ValidationFailure(dns.exception.DNSException):
     """The DNSSEC signature is invalid."""
 
 
+PublicKey = Union[
+    "rsa.RSAPublicKey",
+    "ec.EllipticCurvePublicKey",
+    "ed25519.Ed25519PublicKey",
+    "ed448.Ed448PublicKey",
+]
+
+PrivateKey = Union[
+    "rsa.RSAPrivateKey",
+    "ec.EllipticCurvePrivateKey",
+    "ed25519.Ed25519PrivateKey",
+    "ed448.Ed448PrivateKey",
+]
+
+
 def algorithm_from_text(text: str) -> Algorithm:
     """Convert text into a DNSSEC algorithm value.
 
@@ -69,6 +91,20 @@ def algorithm_to_text(value: Union[Algorithm, int]) -> str:
     return Algorithm.to_text(value)
 
 
+def to_timestamp(value: Union[datetime, str, float, int]) -> int:
+    """Convert various format to a timestamp"""
+    if isinstance(value, datetime):
+        return int(value.timestamp())
+    elif isinstance(value, str):
+        return sigtime_to_posixtime(value)
+    elif isinstance(value, float):
+        return int(value)
+    elif isinstance(value, int):
+        return value
+    else:
+        raise TypeError("Unsupported timestamp type")
+
+
 def key_id(key: DNSKEY) -> int:
     """Return the key id (a 16-bit number) for the specified key.
 
@@ -213,6 +249,35 @@ def _is_sha512(algorithm: int) -> bool:
     return algorithm == Algorithm.RSASHA512
 
 
+def _ensure_algorithm_key_combination(algorithm: int, key: PublicKey) -> None:
+    """Ensure algorithm is valid for key type, throwing an exception on
+    mismatch."""
+    if isinstance(key, rsa.RSAPublicKey):
+        if _is_rsa(algorithm):
+            return
+        raise AlgorithmKeyMismatch('algorithm "%s" not valid for RSA key' % algorithm)
+    if isinstance(key, dsa.DSAPublicKey):
+        if _is_dsa(algorithm):
+            return
+        raise AlgorithmKeyMismatch('algorithm "%s" not valid for DSA key' % algorithm)
+    if isinstance(key, ec.EllipticCurvePublicKey):
+        if _is_ecdsa(algorithm):
+            return
+        raise AlgorithmKeyMismatch('algorithm "%s" not valid for ECDSA key' % algorithm)
+    if isinstance(key, ed25519.Ed25519PublicKey):
+        if algorithm == Algorithm.ED25519:
+            return
+        raise AlgorithmKeyMismatch(
+            'algorithm "%s" not valid for ED25519 key' % algorithm
+        )
+    if isinstance(key, ed448.Ed448PublicKey):
+        if algorithm == Algorithm.ED448:
+            return
+        raise AlgorithmKeyMismatch('algorithm "%s" not valid for ED448 key' % algorithm)
+
+    raise TypeError("unsupported key type")
+
+
 def _make_hash(algorithm: int) -> Any:
     if _is_md5(algorithm):
         return hashes.MD5()
@@ -353,22 +418,10 @@ def _validate_rrsig(
     dnspython but not implemented.
     """
 
-    if isinstance(origin, str):
-        origin = dns.name.from_text(origin, dns.name.root)
-
     candidate_keys = _find_candidate_keys(keys, rrsig)
     if candidate_keys is None:
         raise ValidationFailure("unknown key")
 
-    # For convenience, allow the rrset to be specified as a (name,
-    # rdataset) tuple as well as a proper rrset
-    if isinstance(rrset, tuple):
-        rrname = rrset[0]
-        rdataset = rrset[1]
-    else:
-        rrname = rrset.name
-        rdataset = rrset
-
     if now is None:
         now = time.time()
     if rrsig.expiration < now:
@@ -391,31 +444,7 @@ def _validate_rrsig(
     else:
         sig = rrsig.signature
 
-    data = b""
-    data += rrsig.to_wire(origin=origin)[:18]
-    data += rrsig.signer.to_digestable(origin)
-
-    # Derelativize the name before considering labels.
-    if not rrname.is_absolute():
-        if origin is None:
-            raise ValidationFailure("relative RR name without an origin specified")
-        rrname = rrname.derelativize(origin)
-
-    if len(rrname) - 1 < rrsig.labels:
-        raise ValidationFailure("owner name longer than RRSIG labels")
-    elif rrsig.labels < len(rrname) - 1:
-        suffix = rrname.split(rrsig.labels + 1)[1]
-        rrname = dns.name.from_text("*", suffix)
-    rrnamebuf = rrname.to_digestable()
-    rrfixed = struct.pack("!HHI", rdataset.rdtype, rdataset.rdclass, rrsig.original_ttl)
-    rdatas = [rdata.to_digestable(origin) for rdata in rdataset]
-    for rdata in sorted(rdatas):
-        data += rrnamebuf
-        data += rrfixed
-        rrlen = struct.pack("!H", len(rdata))
-        data += rrlen
-        data += rdata
-
+    data = _make_rrsig_signature_data(rrset, rrsig, origin)
     chosen_hash = _make_hash(rrsig.algorithm)
 
     for candidate_key in candidate_keys:
@@ -495,6 +524,314 @@ def _validate(
     raise ValidationFailure("no RRSIGs validated")
 
 
+def _sign(
+    rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+    private_key: PrivateKey,
+    signer: dns.name.Name,
+    dnskey: DNSKEY,
+    inception: Optional[Union[datetime, str, float]] = None,
+    expiration: Optional[Union[datetime, str, float]] = None,
+    lifetime: Optional[int] = None,
+    verify: bool = False,
+) -> RRSIG:
+    """Sign RRset using private key.
+
+    *rrset*, the RRset to validate.  This can be a
+    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+    tuple.
+
+    *private_key*, the private key to use for signing, a
+    ``cryptography.hazmat.primitives.asymmetric`` private key class applicable
+    for DNSSEC.
+
+    *signer*, a ``dns.name.Name``, the Signer's name.
+
+    *dnskey*, a ``DNSKEY`` matching ``private_key``.
+
+    *inception*, a ``datetime``, ``str``, or ``float``, signature inception; defaults to now.
+
+    *expiration*, a ``datetime``, ``str`` or ``float``, signature expiration. May be specified as lifetime.
+
+    *lifetime*, an ``int`` specifiying the signature lifetime in seconds.
+
+    *verify*, a ``bool`` set to ``True`` if the signer should verify issued signaures.
+    """
+
+    if isinstance(rrset, tuple):
+        rdclass = rrset[1].rdclass
+        rdtype = rrset[1].rdtype
+        rrname = rrset[0]
+        original_ttl = rrset[1].ttl
+    else:
+        rdclass = rrset.rdclass
+        rdtype = rrset.rdtype
+        rrname = rrset.name
+        original_ttl = rrset.ttl
+
+    if inception is not None:
+        rrsig_inception = to_timestamp(inception)
+    else:
+        rrsig_inception = int(time.time())
+
+    if expiration is not None:
+        rrsig_expiration = to_timestamp(expiration)
+    elif lifetime is not None:
+        rrsig_expiration = int(time.time()) + lifetime
+    else:
+        raise ValueError("expiration or lifetime must be specified")
+
+    rrsig_template = RRSIG(
+        rdclass=rdclass,
+        rdtype=dns.rdatatype.RRSIG,
+        type_covered=rdtype,
+        algorithm=dnskey.algorithm,
+        labels=len(rrname) - 1,
+        original_ttl=original_ttl,
+        expiration=rrsig_expiration,
+        inception=rrsig_inception,
+        key_tag=key_id(dnskey),
+        signer=signer,
+        signature=b"",
+    )
+
+    data = dns.dnssec._make_rrsig_signature_data(rrset, rrsig_template)
+    chosen_hash = _make_hash(rrsig_template.algorithm)
+    signature = None
+
+    if isinstance(private_key, rsa.RSAPrivateKey):
+        if not _is_rsa(dnskey.algorithm):
+            raise ValueError("Invalid DNSKEY algorithm for RSA key")
+        signature = private_key.sign(data, padding.PKCS1v15(), chosen_hash)
+        if verify:
+            private_key.public_key().verify(
+                signature, data, padding.PKCS1v15(), chosen_hash
+            )
+    elif isinstance(private_key, dsa.DSAPrivateKey):
+        if not _is_dsa(dnskey.algorithm):
+            raise ValueError("Invalid DNSKEY algorithm for DSA key")
+        public_dsa_key = private_key.public_key()
+        if public_dsa_key.key_size > 1024:
+            raise ValueError("DSA key size overflow")
+        der_signature = private_key.sign(data, chosen_hash)
+        if verify:
+            public_dsa_key.verify(der_signature, data, chosen_hash)
+        dsa_r, dsa_s = utils.decode_dss_signature(der_signature)
+        dsa_t = (public_dsa_key.key_size // 8 - 64) // 8
+        octets = 20
+        signature = (
+            struct.pack("!B", dsa_t)
+            + int.to_bytes(dsa_r, length=octets, byteorder="big")
+            + int.to_bytes(dsa_s, length=octets, byteorder="big")
+        )
+    elif isinstance(private_key, ec.EllipticCurvePrivateKey):
+        if not _is_ecdsa(dnskey.algorithm):
+            raise ValueError("Invalid DNSKEY algorithm for EC key")
+        der_signature = private_key.sign(data, ec.ECDSA(chosen_hash))
+        if verify:
+            private_key.public_key().verify(der_signature, data, ec.ECDSA(chosen_hash))
+        if dnskey.algorithm == Algorithm.ECDSAP256SHA256:
+            octets = 32
+        else:
+            octets = 48
+        dsa_r, dsa_s = utils.decode_dss_signature(der_signature)
+        signature = int.to_bytes(dsa_r, length=octets, byteorder="big") + int.to_bytes(
+            dsa_s, length=octets, byteorder="big"
+        )
+    elif isinstance(private_key, ed25519.Ed25519PrivateKey):
+        if dnskey.algorithm != Algorithm.ED25519:
+            raise ValueError("Invalid DNSKEY algorithm for ED25519 key")
+        signature = private_key.sign(data)
+        if verify:
+            private_key.public_key().verify(signature, data)
+    elif isinstance(private_key, ed448.Ed448PrivateKey):
+        if dnskey.algorithm != Algorithm.ED448:
+            raise ValueError("Invalid DNSKEY algorithm for ED448 key")
+        signature = private_key.sign(data)
+        if verify:
+            private_key.public_key().verify(signature, data)
+    else:
+        raise TypeError("Unsupported key algorithm")
+
+    return RRSIG(
+        rdclass=rrsig_template.rdclass,
+        rdtype=rrsig_template.rdtype,
+        type_covered=rrsig_template.type_covered,
+        algorithm=rrsig_template.algorithm,
+        labels=rrsig_template.labels,
+        original_ttl=rrsig_template.original_ttl,
+        expiration=rrsig_template.expiration,
+        inception=rrsig_template.inception,
+        key_tag=rrsig_template.key_tag,
+        signer=rrsig_template.signer,
+        signature=signature,
+    )
+
+
+def _make_rrsig_signature_data(
+    rrset: Union[dns.rrset.RRset, Tuple[dns.name.Name, dns.rdataset.Rdataset]],
+    rrsig: RRSIG,
+    origin: Optional[dns.name.Name] = None,
+) -> bytes:
+    """Create signature rdata.
+
+    *rrset*, the RRset to sign/validate.  This can be a
+    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+    tuple.
+
+    *rrsig*, a ``dns.rdata.Rdata``, the signature to validate, or the
+    signature template used when signing.
+
+    *origin*, a ``dns.name.Name`` or ``None``, the origin to use for relative
+    names.
+
+    Raises ``UnsupportedAlgorithm`` if the algorithm is recognized by
+    dnspython but not implemented.
+    """
+
+    if isinstance(origin, str):
+        origin = dns.name.from_text(origin, dns.name.root)
+
+    signer = rrsig.signer
+    if not signer.is_absolute():
+        if origin is None:
+            raise ValidationFailure("relative RR name without an origin specified")
+        signer = signer.derelativize(origin)
+
+    # For convenience, allow the rrset to be specified as a (name,
+    # rdataset) tuple as well as a proper rrset
+    if isinstance(rrset, tuple):
+        rrname = rrset[0]
+        rdataset = rrset[1]
+    else:
+        rrname = rrset.name
+        rdataset = rrset
+
+    data = b""
+    data += rrsig.to_wire(origin=signer)[:18]
+    data += rrsig.signer.to_digestable(signer)
+
+    # Derelativize the name before considering labels.
+    if not rrname.is_absolute():
+        if origin is None:
+            raise ValidationFailure("relative RR name without an origin specified")
+        rrname = rrname.derelativize(origin)
+
+    if len(rrname) - 1 < rrsig.labels:
+        raise ValidationFailure("owner name longer than RRSIG labels")
+    elif rrsig.labels < len(rrname) - 1:
+        suffix = rrname.split(rrsig.labels + 1)[1]
+        rrname = dns.name.from_text("*", suffix)
+    rrnamebuf = rrname.to_digestable()
+    rrfixed = struct.pack("!HHI", rdataset.rdtype, rdataset.rdclass, rrsig.original_ttl)
+    rdatas = [rdata.to_digestable(origin) for rdata in rdataset]
+    for rdata in sorted(rdatas):
+        data += rrnamebuf
+        data += rrfixed
+        rrlen = struct.pack("!H", len(rdata))
+        data += rrlen
+        data += rdata
+
+    return data
+
+
+def _make_dnskey(
+    public_key: PublicKey,
+    algorithm: Union[int, str],
+    flags: int = Flag.ZONE,
+    protocol: int = 3,
+) -> DNSKEY:
+    """Convert a public key to DNSKEY Rdata
+
+    *public_key*, the public key to convert, a
+    ``cryptography.hazmat.primitives.asymmetric`` public key class applicable
+    for DNSSEC.
+
+    *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm.
+
+    *flags: DNSKEY flags field as an integer.
+
+    *protocol*: DNSKEY protocol field as an integer.
+
+    Raises ``ValueError`` if the specified key algorithm parameters are not
+    unsupported, ``TypeError`` if the key type is unsupported,
+    `UnsupportedAlgorithm` if the algorithm is unknown and
+    `AlgorithmKeyMismatch` if the algorithm does not match the key type.
+
+    Return DNSKEY ``Rdata``.
+    """
+
+    def encode_rsa_public_key(public_key: "rsa.RSAPublicKey") -> bytes:
+        """Encode a public key per RFC 3110, section 2."""
+        pn = public_key.public_numbers()
+        _exp_len = math.ceil(int.bit_length(pn.e) / 8)
+        exp = int.to_bytes(pn.e, length=_exp_len, byteorder="big")
+        if _exp_len > 255:
+            exp_header = b"\0" + struct.pack("!H", _exp_len)
+        else:
+            exp_header = struct.pack("!B", _exp_len)
+        if pn.n.bit_length() < 512 or pn.n.bit_length() > 4096:
+            raise ValueError("unsupported RSA key length")
+        return exp_header + exp + pn.n.to_bytes((pn.n.bit_length() + 7) // 8, "big")
+
+    def encode_dsa_public_key(public_key: "dsa.DSAPublicKey") -> bytes:
+        """Encode a public key per RFC 2536, section 2."""
+        pn = public_key.public_numbers()
+        dsa_t = (public_key.key_size // 8 - 64) // 8
+        if dsa_t > 8:
+            raise ValueError("unsupported DSA key size")
+        octets = 64 + dsa_t * 8
+        res = struct.pack("!B", dsa_t)
+        res += pn.parameter_numbers.q.to_bytes(20, "big")
+        res += pn.parameter_numbers.p.to_bytes(octets, "big")
+        res += pn.parameter_numbers.g.to_bytes(octets, "big")
+        res += pn.y.to_bytes(octets, "big")
+        return res
+
+    def encode_ecdsa_public_key(public_key: "ec.EllipticCurvePublicKey") -> bytes:
+        """Encode a public key per RFC 6605, section 4."""
+        pn = public_key.public_numbers()
+        if isinstance(public_key.curve, ec.SECP256R1):
+            return pn.x.to_bytes(32, "big") + pn.y.to_bytes(32, "big")
+        elif isinstance(public_key.curve, ec.SECP384R1):
+            return pn.x.to_bytes(48, "big") + pn.y.to_bytes(48, "big")
+        else:
+            raise ValueError("unsupported ECDSA curve")
+
+    try:
+        if isinstance(algorithm, str):
+            algorithm = Algorithm[algorithm.upper()]
+    except Exception:
+        raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm)
+
+    _ensure_algorithm_key_combination(algorithm, public_key)
+
+    if isinstance(public_key, rsa.RSAPublicKey):
+        key_bytes = encode_rsa_public_key(public_key)
+    elif isinstance(public_key, dsa.DSAPublicKey):
+        key_bytes = encode_dsa_public_key(public_key)
+    elif isinstance(public_key, ec.EllipticCurvePublicKey):
+        key_bytes = encode_ecdsa_public_key(public_key)
+    elif isinstance(public_key, ed25519.Ed25519PublicKey):
+        key_bytes = public_key.public_bytes(
+            encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
+        )
+    elif isinstance(public_key, ed448.Ed448PublicKey):
+        key_bytes = public_key.public_bytes(
+            encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
+        )
+    else:
+        raise TypeError("unsupported key algorithm")
+
+    return DNSKEY(
+        rdclass=dns.rdataclass.IN,
+        rdtype=dns.rdatatype.DNSKEY,
+        flags=flags,
+        protocol=protocol,
+        algorithm=algorithm,
+        key=key_bytes,
+    )
+
+
 def nsec3_hash(
     domain: Union[dns.name.Name, str],
     salt: Optional[Union[str, bytes]],
@@ -565,7 +902,7 @@ def _need_pyca(*args, **kwargs):
 try:
     from cryptography.exceptions import InvalidSignature
     from cryptography.hazmat.backends import default_backend
-    from cryptography.hazmat.primitives import hashes
+    from cryptography.hazmat.primitives import hashes, serialization
     from cryptography.hazmat.primitives.asymmetric import padding
     from cryptography.hazmat.primitives.asymmetric import utils
     from cryptography.hazmat.primitives.asymmetric import dsa
@@ -576,10 +913,14 @@ try:
 except ImportError:  # pragma: no cover
     validate = _need_pyca
     validate_rrsig = _need_pyca
+    sign = _need_pyca
+    make_dnskey = _need_pyca
     _have_pyca = False
 else:
     validate = _validate  # type: ignore
     validate_rrsig = _validate_rrsig  # type: ignore
+    sign = _sign
+    make_dnskey = _make_dnskey
     _have_pyca = True
 
 ### BEGIN generated Algorithm constants
diff --git a/tests/keys.py b/tests/keys.py
new file mode 100644 (file)
index 0000000..9c0d47e
--- /dev/null
@@ -0,0 +1,160 @@
+# DNSKEY test vectors
+#
+# private keys generate by OpenSSL
+# DNSKEY rdata generated by Knot DNS (after PEM import)
+
+from dataclasses import dataclass
+
+from dns.dnssectypes import Algorithm
+
+
+@dataclass(frozen=True)
+class TestKey:
+    command: str
+    private_pem: str
+    dnskey: str
+    algorithm: int
+
+
+test_dnskeys = [
+    TestKey(
+        command="openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:2048",
+        private_pem="""
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHve8aGCaof3lX
+Cc6QREh9gFvtc0pIm8iZAayiRu1KNS6EH2mN27+9jbfKRETywsxGN86XH/LZEEXH
+C0El2YMJGwRbg7OqjUp14zEI33X/34jZZsqlHWbzJ2WBLY49K9mBengDLdQu5Ve9
+8YWl+QYDoyRrTxqfEDgL7JZ0gECQuFjV//cIiovIaoKcffCGmWDY0QknPtHzn8X4
+LQVx/S21uGNPZM8JcSw6fgbJ/hv+cct4x3JtrSktf2XDBH8HZZ/fbxHqSSBuQ/Y+
+Jvx6twptxbY0LFALDZhidd1HZxsIf8uPkf4kfswSGEYeZQDDtQamG1q4IbRb/PZM
+PHtCXydrAgMBAAECggEBAK9f/r3EkrzDIADh5XIZ4iP/Pbeg0Ior7dcZ9z+MUvAi
+/bKX+g/J7/I4qjR3+KnFi6HjggqCzLD1bq6zHQJkln66L/tCCdAnukcDsZv+yBZf
+aEKp1CdhR3EbGC5xlz/ybkkXBKSV6oU6bO2jUBtIKJWs+l8V12Pt06f0lK25pfbp
+uCDbBDA7uIMJIFaQ1jqejaFpCROTuFyJVS5QbyMJlWBhx+TvvQbpgFltqPHji+/R
+0V1CY4TI89VB/phPQJdf0bwUbvd7pOp8WL/W0NB+TzOWhOsqlmy13D30D7/IrbOu
+OlDOPcfOs+g+dSiloO5hnSw1+mAd8vlkFvohEZz0vhECgYEA6QxXxHwCwSZ1n4i/
+h5O0QfQbZSi8piDknzgyVvZp9cH9/WFhBOErvfbm4m2XLSaCsTrtlPEeEfefv73v
+nMyY8dE/yPr64NZrMjLv/NfM6+fH5oyGmXcARrQD/KG703IRlq1NbzoClFcsMhuc
+qbgY8I1CbvlQ8iaxiKvFGD3aFz8CgYEA22nd2MpxK33DAirmUDKJr24iQ2xQM33x
+39gzbPPRQKU55OqpdXk9QcMB7q6mz7i9Phqia1JqqP3qc38be14mG3R0sT6glBPg
+i8FUO+eTAHL6XYzd8w0daTnYmHo1xuV8+h4srsdoYrqwcESLBt3mJ2wE8eAlNk9s
+Qnil9ZLyMNUCgYEA3Fp2Vmtnc1g5GXqElt37L+1vRcwp6+7oHQBW4NEnyV7/GGjO
+An4iDQF6uBglPGTQaGGuqQj/hL+dxgACo0D1UJio9hERzCwRuapeLrWhpmFHK2Au
+GMdjdHbb2jDW1wxhQxZkREoWjEqMmGhxTiyrMDBw41tLxVr+vJqlxtEc+KMCgYEA
+n3tv+WgMomQjHqw4BAr38T/IP+G22fatnNr1ZjhC3Q476px22CBr2iT4fpkMPug1
+BbMuY3vgcz088P5u51kjsckQGNVAuuFH0c2QgIpuW2E3glAl88iQnC+jtBEAjbW5
+BcRxDgl7Ymf4X2Iy+6bG59ioL3eRFMzeD+LKHpnU2JECgYA7kJn1MJHeB7LYkLpS
+lJ9PrYW3gfGRMoeEifhTs0f4FJDqbuiT8tsrEWUOJhsBebpXR9bfMD+F8aJ6Re3d
+sZio5F16RuyuhwHv7agNfIcrCCXIs2xERN+q8D0Gi6LzwrtGxeaRPQnQFXo7kEOQ
+HzK7xZItz01yelD1En+o4m2/Dg==
+-----END PRIVATE KEY-----
+""",
+        dnskey="256 3 8 AwEAAce97xoYJqh/eVcJzpBESH2AW+1zSkibyJkBrKJG7Uo1LoQfaY3bv72Nt8pERPLCzEY3zpcf8tkQRccLQSXZgwkbBFuDs6qNSnXjMQjfdf/fiNlmyqUdZvMnZYEtjj0r2YF6eAMt1C7lV73xhaX5BgOjJGtPGp8QOAvslnSAQJC4WNX/9wiKi8hqgpx98IaZYNjRCSc+0fOfxfgtBXH9LbW4Y09kzwlxLDp+Bsn+G/5xy3jHcm2tKS1/ZcMEfwdln99vEepJIG5D9j4m/Hq3Cm3FtjQsUAsNmGJ13UdnGwh/y4+R/iR+zBIYRh5lAMO1BqYbWrghtFv89kw8e0JfJ2s=",
+        algorithm=Algorithm.RSASHA256,
+    ),
+    TestKey(
+        command="openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096",
+        private_pem="""
+-----BEGIN PRIVATE KEY-----
+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
+bVQXjmugND3w6dydVZp/DrvEqfE1Ib0=
+-----END PRIVATE KEY-----
+""",
+        dnskey="256 3 8 AwEAAcj+jhGMD2D+pZINqBY5g/61jl/cRXk/yQo/c4zH5FEL+zgmfNBcqDL/JYzd4Kc9ydjLVqa2vgsKlCvBl37wNXlO6n9Zms8dC7lihGAtv6gqe/DY0YwxsZDqOV6EDxN9+4XVtFB/VM8sFFospEnv1J6a+LIdxm6eq2OK/yZsOYWQyZQAVVAWcIat2/S+TyZuTGn3xQSTPAEqTgR3bCJBkCXUVYylZSG0B5m4kLhzW0dTXE2M3hzZeZiiRG8rSPf3qXoHSKH+CL3A5h/qN5OXmrSVK1v43Q9p5C+WPI4KOjvleoyRzdxVJpn/16SSmtIVnJXlR9LVqOPmsSSHg4f/0jIhMzx4WisyVnlGqGemfvG8uHXahnUBmSSLbHdWriHw7je5Uga0pdIaTaSse6xuN69WxrnOLNsdN3ygFpSu9SE5jUvw5KbQyI+tJAZFpXJDUDIF3eoSTACqC5m85RhYTfQMrxOiISaOORvV0hawlrgN4SS6H0iiHYceMthqfc+ilmqRGtjhkB0vcgscL+oiHjw2/fcS39DkkTlo9ZKWo9k3Rn82COAAfGyxZW/9L1yYCs0cTGesaKMQzE/xTaZcvxUplM2w0jXPEg61GO65W8bKLUU68Pye0zh5Dn2KES/+zKv4b14llnAs6cVCqoKRegHONbCUWsAP+RcgmzQxC8uT",
+        algorithm=Algorithm.RSASHA256,
+    ),
+    TestKey(
+        command="openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve",
+        private_pem="""
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJFyT16nmjmDgEF2v
+1iTperYVGR52zVT8ej6A9eTmmSChRANCAASfsKTiVq2KNEKSUoYtPAXiZbDG6EEP
+8TwdLumK8ge2F9AtE0Q343bnnZBCFpCxuvxtuWmS8QQwAWh8PizqKrDu
+-----END PRIVATE KEY-----
+""",
+        dnskey="256 3 13 n7Ck4latijRCklKGLTwF4mWwxuhBD/E8HS7pivIHthfQLRNEN+N2552QQhaQsbr8bblpkvEEMAFofD4s6iqw7g==",
+        algorithm=Algorithm.ECDSAP256SHA256,
+    ),
+    TestKey(
+        command="openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve",
+        private_pem="""
+-----BEGIN PRIVATE KEY-----
+MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCNSZ3SrRmdh8wcUVPO
+h9ea2zw9Jyc3P1XuP2nOYZR/aQMHfScCtWA3AsMCcsseEmihZANiAATv2H3Q3jrI
+aH/Vmit9RefIpnh+iZzpyk29/m1EJKgkkwbA0OHClk8Nt7RL/4CO4CUpzaOcqamN
+6B48G68LN4yZByMKt3z751qB86Z7rYc7SuOR0m7bPlXyUsO48+8o/hU=
+-----END PRIVATE KEY-----
+""",
+        dnskey="256 3 14 79h90N46yGh/1ZorfUXnyKZ4fomc6cpNvf5tRCSoJJMGwNDhwpZPDbe0S/+AjuAlKc2jnKmpjegePBuvCzeMmQcjCrd8++dagfOme62HO0rjkdJu2z5V8lLDuPPvKP4V",
+        algorithm=Algorithm.ECDSAP384SHA384,
+    ),
+    TestKey(
+        command="openssl genpkey -algorithm ED25519",
+        private_pem="""
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIKGelcdVWlxU5YlLE5/LAEfqhZq7P9s0NHlQqxOjBvcS
+-----END PRIVATE KEY-----
+""",
+        dnskey="256 3 15 iHaBu3tWzJxuuMSzk1WMwCGF3LD60n0fkOdaCCqsL0A=",
+        algorithm=Algorithm.ED25519,
+    ),
+    TestKey(
+        command="openssl genpkey -algorithm ED448",
+        private_pem="""
+-----BEGIN PRIVATE KEY-----
+MEcCAQAwBQYDK2VxBDsEOfGENbZhfMbspoQV1c3/vljWPMFsIzef7M111gU0QTva
+dUd0khisgJ/gk+I1DWLtf/6M4wxXje5FLg==
+-----END PRIVATE KEY-----
+""",
+        dnskey="256 3 16 ziFYQq6fEXyNKPGzq2GErJxCl9979MKNdW46r4Bqn/waS+iIAmAbaTG3klpwqJtl+Qvdj2xGqJwA",
+        algorithm=Algorithm.ED448,
+    ),
+]
index d51f7707440778558c23adc31d1204964950b8c6..9aed87975edaae363ecfe807a441c5fac865e2bd 100644 (file)
@@ -15,6 +15,7 @@
 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
 # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
+from datetime import datetime, timedelta, timezone
 from typing import Any
 
 import unittest
@@ -28,6 +29,15 @@ import dns.rdtypes.ANY.CDS
 import dns.rdtypes.ANY.DS
 import dns.rrset
 
+from .keys import test_dnskeys
+
+try:
+    from cryptography.hazmat.backends import default_backend
+    from cryptography.hazmat.primitives.serialization import load_pem_private_key
+    from cryptography.hazmat.primitives.asymmetric import dsa, ec, ed25519, ed448, rsa
+except ImportError:
+    pass  # Cryptography ImportError already handled in dns.dnssec
+
 # pylint: disable=line-too-long
 
 abs_dnspython_org = dns.name.from_text("dnspython.org")
@@ -814,6 +824,23 @@ class DNSSECMiscTestCase(unittest.TestCase):
         with self.assertRaises(dns.dnssec.ValidationFailure):
             dns.dnssec._make_hash(100)
 
+    def testToTimestamp(self):
+        REFERENCE_TIMESTAMP = 441812220
+
+        ts = dns.dnssec.to_timestamp(
+            datetime(year=1984, month=1, day=1, hour=13, minute=37, tzinfo=timezone.utc)
+        )
+        self.assertEqual(ts, REFERENCE_TIMESTAMP)
+
+        ts = dns.dnssec.to_timestamp("19840101133700")
+        self.assertEqual(ts, REFERENCE_TIMESTAMP)
+
+        ts = dns.dnssec.to_timestamp(441812220.0)
+        self.assertEqual(ts, REFERENCE_TIMESTAMP)
+
+        ts = dns.dnssec.to_timestamp(441812220)
+        self.assertEqual(ts, REFERENCE_TIMESTAMP)
+
 
 class DNSSECMakeDSTestCase(unittest.TestCase):
     def testMnemonicParser(self):
@@ -919,5 +946,123 @@ class DNSSECMakeDSTestCase(unittest.TestCase):
                 self.assertEqual(msg, str(cm.exception))
 
 
+@unittest.skipUnless(dns.dnssec._have_pyca, "Python Cryptography cannot be imported")
+class DNSSECMakeDNSKEYTestCase(unittest.TestCase):
+    def testKnownDNSKEYs(self):  # type: () -> None
+        for tk in test_dnskeys:
+            print(tk.command)
+            key = load_pem_private_key(tk.private_pem.encode(), password=None)
+            rdata1 = str(dns.dnssec.make_dnskey(key.public_key(), tk.algorithm))
+            rdata2 = str(
+                dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNSKEY, tk.dnskey)
+            )
+            self.assertEqual(rdata1, rdata2)
+
+    def testInvalidMakeDNSKEY(self):  # type: () -> None
+        key = rsa.generate_private_key(
+            public_exponent=65537,
+            key_size=1024,
+            backend=default_backend(),
+        )
+        with self.assertRaises(dns.dnssec.AlgorithmKeyMismatch):
+            dns.dnssec.make_dnskey(key.public_key(), dns.dnssec.Algorithm.ED448)
+
+        with self.assertRaises(TypeError):
+            dns.dnssec.make_dnskey("xyzzy", dns.dnssec.Algorithm.ED448)
+
+        key = dsa.generate_private_key(2048)
+        with self.assertRaises(ValueError):
+            dns.dnssec.make_dnskey(key.public_key(), dns.dnssec.Algorithm.DSA)
+
+    def testRSALargeExponent(self):  # type: () -> None
+        for key_size, public_exponent, dnskey_key_length in [
+            (1024, 3, 130),
+            (1024, 65537, 132),
+            (2048, 3, 258),
+            (2048, 65537, 260),
+            (4096, 3, 514),
+            (4096, 65537, 516),
+        ]:
+            key = rsa.generate_private_key(
+                public_exponent=public_exponent,
+                key_size=key_size,
+                backend=default_backend(),
+            )
+            dnskey = dns.dnssec.make_dnskey(
+                key.public_key(), algorithm=dns.dnssec.Algorithm.RSASHA256
+            )
+            self.assertEqual(len(dnskey.key), dnskey_key_length)
+
+
+@unittest.skipUnless(dns.dnssec._have_pyca, "Python Cryptography cannot be imported")
+class DNSSECSignatureTestCase(unittest.TestCase):
+    def testSignatureData(self):  # type: () -> None
+        rrsig_template = abs_soa_rrsig[0]
+        data = dns.dnssec._make_rrsig_signature_data(abs_soa, rrsig_template)
+
+    def testSignatureRSASHA1(self):  # type: () -> None
+        key = rsa.generate_private_key(
+            public_exponent=65537, key_size=2048, backend=default_backend()
+        )
+        self._test_signature(key, dns.dnssec.Algorithm.RSASHA1, abs_soa)
+
+    def testSignatureRSASHA256(self):  # type: () -> None
+        key = rsa.generate_private_key(
+            public_exponent=65537, key_size=2048, backend=default_backend()
+        )
+        self._test_signature(key, dns.dnssec.Algorithm.RSASHA256, abs_soa)
+
+    def testSignatureDSA(self):  # type: () -> None
+        key = dsa.generate_private_key(key_size=1024)
+        self._test_signature(key, dns.dnssec.Algorithm.DSA, abs_soa)
+
+    def testSignatureECDSAP256SHA256(self):  # type: () -> None
+        key = ec.generate_private_key(curve=ec.SECP256R1, backend=default_backend())
+        self._test_signature(key, dns.dnssec.Algorithm.ECDSAP256SHA256, abs_soa)
+
+    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)
+
+    def testSignatureED25519(self):  # type: () -> None
+        key = ed25519.Ed25519PrivateKey.generate()
+        self._test_signature(key, dns.dnssec.Algorithm.ED25519, abs_soa)
+
+    def testSignatureED448(self):  # type: () -> None
+        key = ed448.Ed448PrivateKey.generate()
+        self._test_signature(key, dns.dnssec.Algorithm.ED448, abs_soa)
+
+    def testSignRdataset(self):  # type: () -> None
+        key = ed448.Ed448PrivateKey.generate()
+        name = dns.name.from_text("example.com")
+        rdataset = dns.rdataset.from_text_list("in", "a", 30, ["10.0.0.1", "10.0.0.2"])
+        rrset = (name, rdataset)
+        self._test_signature(key, dns.dnssec.Algorithm.ED448, rrset)
+
+    def _test_signature(self, key, algorithm, rrset, signer=None):  # type: () -> None
+        ttl = 60
+        lifetime = 3600
+        if isinstance(rrset, tuple):
+            rrname = rrset[0]
+        else:
+            rrname = rrset.name
+        signer = signer or rrname
+        dnskey = dns.dnssec.make_dnskey(
+            public_key=key.public_key(), algorithm=algorithm
+        )
+        dnskey_rrset = dns.rrset.from_rdata(signer, ttl, dnskey)
+        rrsig = dns.dnssec.sign(
+            rrset=rrset,
+            private_key=key,
+            dnskey=dnskey,
+            lifetime=lifetime,
+            signer=signer,
+            verify=True,
+        )
+        keys = {signer: dnskey_rrset}
+        rrsigset = dns.rrset.from_rdata(rrname, ttl, rrsig)
+        dns.dnssec.validate(rrset=rrset, rrsigset=rrsigset, keys=keys)
+
+
 if __name__ == "__main__":
     unittest.main()