From 6fa40bd780f73e64c8041f42d894575ee272d2fe Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Tue, 13 Dec 2022 02:28:00 +0100 Subject: DNSSEC signer (#866) * 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 | 419 ++++++++++++++++++++++++++++++++++++++++++++++----- tests/keys.py | 160 ++++++++++++++++++++ tests/test_dnssec.py | 145 ++++++++++++++++++ 3 files changed, 685 insertions(+), 39 deletions(-) create mode 100644 tests/keys.py diff --git a/dns/dnssec.py b/dns/dnssec.py index 13415bd..c4aff95 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -20,9 +20,11 @@ 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 index 0000000..9c0d47e --- /dev/null +++ b/tests/keys.py @@ -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, + ), +] diff --git a/tests/test_dnssec.py b/tests/test_dnssec.py index d51f770..9aed879 100644 --- a/tests/test_dnssec.py +++ b/tests/test_dnssec.py @@ -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() -- cgit v1.2.1