1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
2
3# Copyright (C) 2003-2017 Nominum, Inc.
4#
5# Permission to use, copy, modify, and distribute this software and its
6# documentation for any purpose with or without fee is hereby granted,
7# provided that the above copyright notice and this permission notice
8# appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
18"""Common DNSSEC-related functions and constants."""
19
20import hashlib
21import struct
22import time
23import base64
24
25import dns.enum
26import dns.exception
27import dns.name
28import dns.node
29import dns.rdataset
30import dns.rdata
31import dns.rdatatype
32import dns.rdataclass
33
34
35class UnsupportedAlgorithm(dns.exception.DNSException):
36    """The DNSSEC algorithm is not supported."""
37
38
39class ValidationFailure(dns.exception.DNSException):
40    """The DNSSEC signature is invalid."""
41
42
43class Algorithm(dns.enum.IntEnum):
44    RSAMD5 = 1
45    DH = 2
46    DSA = 3
47    ECC = 4
48    RSASHA1 = 5
49    DSANSEC3SHA1 = 6
50    RSASHA1NSEC3SHA1 = 7
51    RSASHA256 = 8
52    RSASHA512 = 10
53    ECCGOST = 12
54    ECDSAP256SHA256 = 13
55    ECDSAP384SHA384 = 14
56    ED25519 = 15
57    ED448 = 16
58    INDIRECT = 252
59    PRIVATEDNS = 253
60    PRIVATEOID = 254
61
62    @classmethod
63    def _maximum(cls):
64        return 255
65
66
67globals().update(Algorithm.__members__)
68
69
70def algorithm_from_text(text):
71    """Convert text into a DNSSEC algorithm value.
72
73    *text*, a ``str``, the text to convert to into an algorithm value.
74
75    Returns an ``int``.
76    """
77
78    return Algorithm.from_text(text)
79
80
81def algorithm_to_text(value):
82    """Convert a DNSSEC algorithm value to text
83
84    *value*, an ``int`` a DNSSEC algorithm.
85
86    Returns a ``str``, the name of a DNSSEC algorithm.
87    """
88
89    return Algorithm.to_text(value)
90
91
92def key_id(key):
93    """Return the key id (a 16-bit number) for the specified key.
94
95    *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY``
96
97    Returns an ``int`` between 0 and 65535
98    """
99
100    rdata = key.to_wire()
101    if key.algorithm == Algorithm.RSAMD5:
102        return (rdata[-3] << 8) + rdata[-2]
103    else:
104        total = 0
105        for i in range(len(rdata) // 2):
106            total += (rdata[2 * i] << 8) + \
107                rdata[2 * i + 1]
108        if len(rdata) % 2 != 0:
109            total += rdata[len(rdata) - 1] << 8
110        total += ((total >> 16) & 0xffff)
111        return total & 0xffff
112
113class DSDigest(dns.enum.IntEnum):
114    """DNSSEC Delgation Signer Digest Algorithm"""
115
116    SHA1 = 1
117    SHA256 = 2
118    SHA384 = 4
119
120    @classmethod
121    def _maximum(cls):
122        return 255
123
124
125def make_ds(name, key, algorithm, origin=None):
126    """Create a DS record for a DNSSEC key.
127
128    *name*, a ``dns.name.Name`` or ``str``, the owner name of the DS record.
129
130    *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY``, the key the DS is about.
131
132    *algorithm*, a ``str`` or ``int`` specifying the hash algorithm.
133    The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case
134    does not matter for these strings.
135
136    *origin*, a ``dns.name.Name`` or ``None``.  If `key` is a relative name,
137    then it will be made absolute using the specified origin.
138
139    Raises ``UnsupportedAlgorithm`` if the algorithm is unknown.
140
141    Returns a ``dns.rdtypes.ANY.DS.DS``
142    """
143
144    try:
145        if isinstance(algorithm, str):
146            algorithm = DSDigest[algorithm.upper()]
147    except Exception:
148        raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm)
149
150    if algorithm == DSDigest.SHA1:
151        dshash = hashlib.sha1()
152    elif algorithm == DSDigest.SHA256:
153        dshash = hashlib.sha256()
154    elif algorithm == DSDigest.SHA384:
155        dshash = hashlib.sha384()
156    else:
157        raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm)
158
159    if isinstance(name, str):
160        name = dns.name.from_text(name, origin)
161    dshash.update(name.canonicalize().to_wire())
162    dshash.update(key.to_wire(origin=origin))
163    digest = dshash.digest()
164
165    dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, algorithm) + \
166        digest
167    return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0,
168                               len(dsrdata))
169
170
171def _find_candidate_keys(keys, rrsig):
172    candidate_keys = []
173    value = keys.get(rrsig.signer)
174    if value is None:
175        return None
176    if isinstance(value, dns.node.Node):
177        try:
178            rdataset = value.find_rdataset(dns.rdataclass.IN,
179                                           dns.rdatatype.DNSKEY)
180        except KeyError:
181            return None
182    else:
183        rdataset = value
184    for rdata in rdataset:
185        if rdata.algorithm == rrsig.algorithm and \
186                key_id(rdata) == rrsig.key_tag:
187            candidate_keys.append(rdata)
188    return candidate_keys
189
190
191def _is_rsa(algorithm):
192    return algorithm in (Algorithm.RSAMD5, Algorithm.RSASHA1,
193                         Algorithm.RSASHA1NSEC3SHA1, Algorithm.RSASHA256,
194                         Algorithm.RSASHA512)
195
196
197def _is_dsa(algorithm):
198    return algorithm in (Algorithm.DSA, Algorithm.DSANSEC3SHA1)
199
200
201def _is_ecdsa(algorithm):
202    return algorithm in (Algorithm.ECDSAP256SHA256, Algorithm.ECDSAP384SHA384)
203
204
205def _is_eddsa(algorithm):
206    return algorithm in (Algorithm.ED25519, Algorithm.ED448)
207
208
209def _is_gost(algorithm):
210    return algorithm == Algorithm.ECCGOST
211
212
213def _is_md5(algorithm):
214    return algorithm == Algorithm.RSAMD5
215
216
217def _is_sha1(algorithm):
218    return algorithm in (Algorithm.DSA, Algorithm.RSASHA1,
219                         Algorithm.DSANSEC3SHA1, Algorithm.RSASHA1NSEC3SHA1)
220
221
222def _is_sha256(algorithm):
223    return algorithm in (Algorithm.RSASHA256, Algorithm.ECDSAP256SHA256)
224
225
226def _is_sha384(algorithm):
227    return algorithm == Algorithm.ECDSAP384SHA384
228
229
230def _is_sha512(algorithm):
231    return algorithm == Algorithm.RSASHA512
232
233
234def _make_hash(algorithm):
235    if _is_md5(algorithm):
236        return hashes.MD5()
237    if _is_sha1(algorithm):
238        return hashes.SHA1()
239    if _is_sha256(algorithm):
240        return hashes.SHA256()
241    if _is_sha384(algorithm):
242        return hashes.SHA384()
243    if _is_sha512(algorithm):
244        return hashes.SHA512()
245    if algorithm == Algorithm.ED25519:
246        return hashes.SHA512()
247    if algorithm == Algorithm.ED448:
248        return hashes.SHAKE256(114)
249
250    raise ValidationFailure('unknown hash for algorithm %u' % algorithm)
251
252
253def _bytes_to_long(b):
254    return int.from_bytes(b, 'big')
255
256
257def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
258    """Validate an RRset against a single signature rdata, throwing an
259    exception if validation is not successful.
260
261    *rrset*, the RRset to validate.  This can be a
262    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
263    tuple.
264
265    *rrsig*, a ``dns.rdata.Rdata``, the signature to validate.
266
267    *keys*, the key dictionary, used to find the DNSKEY associated
268    with a given name.  The dictionary is keyed by a
269    ``dns.name.Name``, and has ``dns.node.Node`` or
270    ``dns.rdataset.Rdataset`` values.
271
272    *origin*, a ``dns.name.Name`` or ``None``, the origin to use for relative
273    names.
274
275    *now*, an ``int`` or ``None``, the time, in seconds since the epoch, to
276    use as the current time when validating.  If ``None``, the actual current
277    time is used.
278
279    Raises ``ValidationFailure`` if the signature is expired, not yet valid,
280    the public key is invalid, the algorithm is unknown, the verification
281    fails, etc.
282
283    Raises ``UnsupportedAlgorithm`` if the algorithm is recognized by
284    dnspython but not implemented.
285    """
286
287    if isinstance(origin, str):
288        origin = dns.name.from_text(origin, dns.name.root)
289
290    candidate_keys = _find_candidate_keys(keys, rrsig)
291    if candidate_keys is None:
292        raise ValidationFailure('unknown key')
293
294    for candidate_key in candidate_keys:
295        # For convenience, allow the rrset to be specified as a (name,
296        # rdataset) tuple as well as a proper rrset
297        if isinstance(rrset, tuple):
298            rrname = rrset[0]
299            rdataset = rrset[1]
300        else:
301            rrname = rrset.name
302            rdataset = rrset
303
304        if now is None:
305            now = time.time()
306        if rrsig.expiration < now:
307            raise ValidationFailure('expired')
308        if rrsig.inception > now:
309            raise ValidationFailure('not yet valid')
310
311        if _is_rsa(rrsig.algorithm):
312            keyptr = candidate_key.key
313            (bytes_,) = struct.unpack('!B', keyptr[0:1])
314            keyptr = keyptr[1:]
315            if bytes_ == 0:
316                (bytes_,) = struct.unpack('!H', keyptr[0:2])
317                keyptr = keyptr[2:]
318            rsa_e = keyptr[0:bytes_]
319            rsa_n = keyptr[bytes_:]
320            try:
321                public_key = rsa.RSAPublicNumbers(
322                    _bytes_to_long(rsa_e),
323                    _bytes_to_long(rsa_n)).public_key(default_backend())
324            except ValueError:
325                raise ValidationFailure('invalid public key')
326            sig = rrsig.signature
327        elif _is_dsa(rrsig.algorithm):
328            keyptr = candidate_key.key
329            (t,) = struct.unpack('!B', keyptr[0:1])
330            keyptr = keyptr[1:]
331            octets = 64 + t * 8
332            dsa_q = keyptr[0:20]
333            keyptr = keyptr[20:]
334            dsa_p = keyptr[0:octets]
335            keyptr = keyptr[octets:]
336            dsa_g = keyptr[0:octets]
337            keyptr = keyptr[octets:]
338            dsa_y = keyptr[0:octets]
339            try:
340                public_key = dsa.DSAPublicNumbers(
341                    _bytes_to_long(dsa_y),
342                    dsa.DSAParameterNumbers(
343                        _bytes_to_long(dsa_p),
344                        _bytes_to_long(dsa_q),
345                        _bytes_to_long(dsa_g))).public_key(default_backend())
346            except ValueError:
347                raise ValidationFailure('invalid public key')
348            sig_r = rrsig.signature[1:21]
349            sig_s = rrsig.signature[21:]
350            sig = utils.encode_dss_signature(_bytes_to_long(sig_r),
351                                             _bytes_to_long(sig_s))
352        elif _is_ecdsa(rrsig.algorithm):
353            keyptr = candidate_key.key
354            if rrsig.algorithm == Algorithm.ECDSAP256SHA256:
355                curve = ec.SECP256R1()
356                octets = 32
357            else:
358                curve = ec.SECP384R1()
359                octets = 48
360            ecdsa_x = keyptr[0:octets]
361            ecdsa_y = keyptr[octets:octets * 2]
362            try:
363                public_key = ec.EllipticCurvePublicNumbers(
364                    curve=curve,
365                    x=_bytes_to_long(ecdsa_x),
366                    y=_bytes_to_long(ecdsa_y)).public_key(default_backend())
367            except ValueError:
368                raise ValidationFailure('invalid public key')
369            sig_r = rrsig.signature[0:octets]
370            sig_s = rrsig.signature[octets:]
371            sig = utils.encode_dss_signature(_bytes_to_long(sig_r),
372                                             _bytes_to_long(sig_s))
373
374        elif _is_eddsa(rrsig.algorithm):
375            keyptr = candidate_key.key
376            if rrsig.algorithm == Algorithm.ED25519:
377                loader = ed25519.Ed25519PublicKey
378            else:
379                loader = ed448.Ed448PublicKey
380            try:
381                public_key = loader.from_public_bytes(keyptr)
382            except ValueError:
383                raise ValidationFailure('invalid public key')
384            sig = rrsig.signature
385        elif _is_gost(rrsig.algorithm):
386            raise UnsupportedAlgorithm(
387                'algorithm "%s" not supported by dnspython' %
388                algorithm_to_text(rrsig.algorithm))
389        else:
390            raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)
391
392        data = b''
393        data += rrsig.to_wire(origin=origin)[:18]
394        data += rrsig.signer.to_digestable(origin)
395
396        if rrsig.labels < len(rrname) - 1:
397            suffix = rrname.split(rrsig.labels + 1)[1]
398            rrname = dns.name.from_text('*', suffix)
399        rrnamebuf = rrname.to_digestable(origin)
400        rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
401                              rrsig.original_ttl)
402        rrlist = sorted(rdataset)
403        for rr in rrlist:
404            data += rrnamebuf
405            data += rrfixed
406            rrdata = rr.to_digestable(origin)
407            rrlen = struct.pack('!H', len(rrdata))
408            data += rrlen
409            data += rrdata
410
411        chosen_hash = _make_hash(rrsig.algorithm)
412        try:
413            if _is_rsa(rrsig.algorithm):
414                public_key.verify(sig, data, padding.PKCS1v15(), chosen_hash)
415            elif _is_dsa(rrsig.algorithm):
416                public_key.verify(sig, data, chosen_hash)
417            elif _is_ecdsa(rrsig.algorithm):
418                public_key.verify(sig, data, ec.ECDSA(chosen_hash))
419            elif _is_eddsa(rrsig.algorithm):
420                public_key.verify(sig, data)
421            else:
422                # Raise here for code clarity; this won't actually ever happen
423                # since if the algorithm is really unknown we'd already have
424                # raised an exception above
425                raise ValidationFailure('unknown algorithm %u' %
426                                        rrsig.algorithm)  # pragma: no cover
427            # If we got here, we successfully verified so we can return
428            # without error
429            return
430        except InvalidSignature:
431            # this happens on an individual validation failure
432            continue
433    # nothing verified -- raise failure:
434    raise ValidationFailure('verify failure')
435
436
437def _validate(rrset, rrsigset, keys, origin=None, now=None):
438    """Validate an RRset against a signature RRset, throwing an exception
439    if none of the signatures validate.
440
441    *rrset*, the RRset to validate.  This can be a
442    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
443    tuple.
444
445    *rrsigset*, the signature RRset.  This can be a
446    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
447    tuple.
448
449    *keys*, the key dictionary, used to find the DNSKEY associated
450    with a given name.  The dictionary is keyed by a
451    ``dns.name.Name``, and has ``dns.node.Node`` or
452    ``dns.rdataset.Rdataset`` values.
453
454    *origin*, a ``dns.name.Name``, the origin to use for relative names;
455    defaults to None.
456
457    *now*, an ``int`` or ``None``, the time, in seconds since the epoch, to
458    use as the current time when validating.  If ``None``, the actual current
459    time is used.
460
461    Raises ``ValidationFailure`` if the signature is expired, not yet valid,
462    the public key is invalid, the algorithm is unknown, the verification
463    fails, etc.
464    """
465
466    if isinstance(origin, str):
467        origin = dns.name.from_text(origin, dns.name.root)
468
469    if isinstance(rrset, tuple):
470        rrname = rrset[0]
471    else:
472        rrname = rrset.name
473
474    if isinstance(rrsigset, tuple):
475        rrsigname = rrsigset[0]
476        rrsigrdataset = rrsigset[1]
477    else:
478        rrsigname = rrsigset.name
479        rrsigrdataset = rrsigset
480
481    rrname = rrname.choose_relativity(origin)
482    rrsigname = rrsigname.choose_relativity(origin)
483    if rrname != rrsigname:
484        raise ValidationFailure("owner names do not match")
485
486    for rrsig in rrsigrdataset:
487        try:
488            _validate_rrsig(rrset, rrsig, keys, origin, now)
489            return
490        except (ValidationFailure, UnsupportedAlgorithm):
491            pass
492    raise ValidationFailure("no RRSIGs validated")
493
494
495class NSEC3Hash(dns.enum.IntEnum):
496    """NSEC3 hash algorithm"""
497
498    SHA1 = 1
499
500    @classmethod
501    def _maximum(cls):
502        return 255
503
504def nsec3_hash(domain, salt, iterations, algorithm):
505    """
506    Calculate the NSEC3 hash, according to
507    https://tools.ietf.org/html/rfc5155#section-5
508
509    *domain*, a ``dns.name.Name`` or ``str``, the name to hash.
510
511    *salt*, a ``str``, ``bytes``, or ``None``, the hash salt.  If a
512    string, it is decoded as a hex string.
513
514    *iterations*, an ``int``, the number of iterations.
515
516    *algorithm*, a ``str`` or ``int``, the hash algorithm.
517    The only defined algorithm is SHA1.
518
519    Returns a ``str``, the encoded NSEC3 hash.
520    """
521
522    b32_conversion = str.maketrans(
523        "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", "0123456789ABCDEFGHIJKLMNOPQRSTUV"
524    )
525
526    try:
527        if isinstance(algorithm, str):
528            algorithm = NSEC3Hash[algorithm.upper()]
529    except Exception:
530        raise ValueError("Wrong hash algorithm (only SHA1 is supported)")
531
532    if algorithm != NSEC3Hash.SHA1:
533        raise ValueError("Wrong hash algorithm (only SHA1 is supported)")
534
535    salt_encoded = salt
536    if salt is None:
537        salt_encoded = b''
538    elif isinstance(salt, str):
539        if len(salt) % 2 == 0:
540            salt_encoded = bytes.fromhex(salt)
541        else:
542            raise ValueError("Invalid salt length")
543
544    if not isinstance(domain, dns.name.Name):
545        domain = dns.name.from_text(domain)
546    domain_encoded = domain.canonicalize().to_wire()
547
548    digest = hashlib.sha1(domain_encoded + salt_encoded).digest()
549    for i in range(iterations):
550        digest = hashlib.sha1(digest + salt_encoded).digest()
551
552    output = base64.b32encode(digest).decode("utf-8")
553    output = output.translate(b32_conversion)
554
555    return output
556
557
558def _need_pyca(*args, **kwargs):
559    raise ImportError("DNSSEC validation requires " +
560                      "python cryptography")  # pragma: no cover
561
562
563try:
564    from cryptography.exceptions import InvalidSignature
565    from cryptography.hazmat.backends import default_backend
566    from cryptography.hazmat.primitives import hashes
567    from cryptography.hazmat.primitives.asymmetric import padding
568    from cryptography.hazmat.primitives.asymmetric import utils
569    from cryptography.hazmat.primitives.asymmetric import dsa
570    from cryptography.hazmat.primitives.asymmetric import ec
571    from cryptography.hazmat.primitives.asymmetric import ed25519
572    from cryptography.hazmat.primitives.asymmetric import ed448
573    from cryptography.hazmat.primitives.asymmetric import rsa
574except ImportError:  # pragma: no cover
575    validate = _need_pyca
576    validate_rrsig = _need_pyca
577    _have_pyca = False
578else:
579    validate = _validate                # type: ignore
580    validate_rrsig = _validate_rrsig    # type: ignore
581    _have_pyca = True
582