# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
|
|
|
|
# Copyright (C) 2003-2017 Nominum, Inc.
|
|
#
|
|
# Permission to use, copy, modify, and distribute this software and its
|
|
# documentation for any purpose with or without fee is hereby granted,
|
|
# provided that the above copyright notice and this permission notice
|
|
# appear in all copies.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
|
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
"""Common DNSSEC-related functions and constants."""
|
|
|
|
from io import BytesIO
|
|
import struct
|
|
import time
|
|
|
|
import dns.exception
|
|
import dns.name
|
|
import dns.node
|
|
import dns.rdataset
|
|
import dns.rdata
|
|
import dns.rdatatype
|
|
import dns.rdataclass
|
|
from ._compat import string_types
|
|
|
|
|
|
class UnsupportedAlgorithm(dns.exception.DNSException):
|
|
"""The DNSSEC algorithm is not supported."""
|
|
|
|
|
|
class ValidationFailure(dns.exception.DNSException):
|
|
"""The DNSSEC signature is invalid."""
|
|
|
|
|
|
#: RSAMD5
|
|
RSAMD5 = 1
|
|
#: DH
|
|
DH = 2
|
|
#: DSA
|
|
DSA = 3
|
|
#: ECC
|
|
ECC = 4
|
|
#: RSASHA1
|
|
RSASHA1 = 5
|
|
#: DSANSEC3SHA1
|
|
DSANSEC3SHA1 = 6
|
|
#: RSASHA1NSEC3SHA1
|
|
RSASHA1NSEC3SHA1 = 7
|
|
#: RSASHA256
|
|
RSASHA256 = 8
|
|
#: RSASHA512
|
|
RSASHA512 = 10
|
|
#: ECDSAP256SHA256
|
|
ECDSAP256SHA256 = 13
|
|
#: ECDSAP384SHA384
|
|
ECDSAP384SHA384 = 14
|
|
#: INDIRECT
|
|
INDIRECT = 252
|
|
#: PRIVATEDNS
|
|
PRIVATEDNS = 253
|
|
#: PRIVATEOID
|
|
PRIVATEOID = 254
|
|
|
|
_algorithm_by_text = {
|
|
'RSAMD5': RSAMD5,
|
|
'DH': DH,
|
|
'DSA': DSA,
|
|
'ECC': ECC,
|
|
'RSASHA1': RSASHA1,
|
|
'DSANSEC3SHA1': DSANSEC3SHA1,
|
|
'RSASHA1NSEC3SHA1': RSASHA1NSEC3SHA1,
|
|
'RSASHA256': RSASHA256,
|
|
'RSASHA512': RSASHA512,
|
|
'INDIRECT': INDIRECT,
|
|
'ECDSAP256SHA256': ECDSAP256SHA256,
|
|
'ECDSAP384SHA384': ECDSAP384SHA384,
|
|
'PRIVATEDNS': PRIVATEDNS,
|
|
'PRIVATEOID': PRIVATEOID,
|
|
}
|
|
|
|
# We construct the inverse mapping programmatically to ensure that we
|
|
# cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
|
# would cause the mapping not to be true inverse.
|
|
|
|
_algorithm_by_value = {y: x for x, y in _algorithm_by_text.items()}
|
|
|
|
|
|
def algorithm_from_text(text):
|
|
"""Convert text into a DNSSEC algorithm value.
|
|
|
|
Returns an ``int``.
|
|
"""
|
|
|
|
value = _algorithm_by_text.get(text.upper())
|
|
if value is None:
|
|
value = int(text)
|
|
return value
|
|
|
|
|
|
def algorithm_to_text(value):
|
|
"""Convert a DNSSEC algorithm value to text
|
|
|
|
Returns a ``str``.
|
|
"""
|
|
|
|
text = _algorithm_by_value.get(value)
|
|
if text is None:
|
|
text = str(value)
|
|
return text
|
|
|
|
|
|
def _to_rdata(record, origin):
|
|
s = BytesIO()
|
|
record.to_wire(s, origin=origin)
|
|
return s.getvalue()
|
|
|
|
|
|
def key_id(key, origin=None):
|
|
"""Return the key id (a 16-bit number) for the specified key.
|
|
|
|
Note the *origin* parameter of this function is historical and
|
|
is not needed.
|
|
|
|
Returns an ``int`` between 0 and 65535.
|
|
"""
|
|
|
|
rdata = _to_rdata(key, origin)
|
|
rdata = bytearray(rdata)
|
|
if key.algorithm == RSAMD5:
|
|
return (rdata[-3] << 8) + rdata[-2]
|
|
else:
|
|
total = 0
|
|
for i in range(len(rdata) // 2):
|
|
total += (rdata[2 * i] << 8) + \
|
|
rdata[2 * i + 1]
|
|
if len(rdata) % 2 != 0:
|
|
total += rdata[len(rdata) - 1] << 8
|
|
total += ((total >> 16) & 0xffff)
|
|
return total & 0xffff
|
|
|
|
|
|
def make_ds(name, key, algorithm, origin=None):
|
|
"""Create a DS record for a DNSSEC key.
|
|
|
|
*name* is the owner name of the DS record.
|
|
|
|
*key* is a ``dns.rdtypes.ANY.DNSKEY``.
|
|
|
|
*algorithm* is a string describing which hash algorithm to use. The
|
|
currently supported hashes are "SHA1" and "SHA256". Case does not
|
|
matter for these strings.
|
|
|
|
*origin* is a ``dns.name.Name`` and will be used as the origin
|
|
if *key* is a relative name.
|
|
|
|
Returns a ``dns.rdtypes.ANY.DS``.
|
|
"""
|
|
|
|
if algorithm.upper() == 'SHA1':
|
|
dsalg = 1
|
|
hash = SHA1.new()
|
|
elif algorithm.upper() == 'SHA256':
|
|
dsalg = 2
|
|
hash = SHA256.new()
|
|
else:
|
|
raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm)
|
|
|
|
if isinstance(name, string_types):
|
|
name = dns.name.from_text(name, origin)
|
|
hash.update(name.canonicalize().to_wire())
|
|
hash.update(_to_rdata(key, origin))
|
|
digest = hash.digest()
|
|
|
|
dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, dsalg) + digest
|
|
return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0,
|
|
len(dsrdata))
|
|
|
|
|
|
def _find_candidate_keys(keys, rrsig):
|
|
candidate_keys = []
|
|
value = keys.get(rrsig.signer)
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, dns.node.Node):
|
|
try:
|
|
rdataset = value.find_rdataset(dns.rdataclass.IN,
|
|
dns.rdatatype.DNSKEY)
|
|
except KeyError:
|
|
return None
|
|
else:
|
|
rdataset = value
|
|
for rdata in rdataset:
|
|
if rdata.algorithm == rrsig.algorithm and \
|
|
key_id(rdata) == rrsig.key_tag:
|
|
candidate_keys.append(rdata)
|
|
return candidate_keys
|
|
|
|
|
|
def _is_rsa(algorithm):
|
|
return algorithm in (RSAMD5, RSASHA1,
|
|
RSASHA1NSEC3SHA1, RSASHA256,
|
|
RSASHA512)
|
|
|
|
|
|
def _is_dsa(algorithm):
|
|
return algorithm in (DSA, DSANSEC3SHA1)
|
|
|
|
|
|
def _is_ecdsa(algorithm):
|
|
return _have_ecdsa and (algorithm in (ECDSAP256SHA256, ECDSAP384SHA384))
|
|
|
|
|
|
def _is_md5(algorithm):
|
|
return algorithm == RSAMD5
|
|
|
|
|
|
def _is_sha1(algorithm):
|
|
return algorithm in (DSA, RSASHA1,
|
|
DSANSEC3SHA1, RSASHA1NSEC3SHA1)
|
|
|
|
|
|
def _is_sha256(algorithm):
|
|
return algorithm in (RSASHA256, ECDSAP256SHA256)
|
|
|
|
|
|
def _is_sha384(algorithm):
|
|
return algorithm == ECDSAP384SHA384
|
|
|
|
|
|
def _is_sha512(algorithm):
|
|
return algorithm == RSASHA512
|
|
|
|
|
|
def _make_hash(algorithm):
|
|
if _is_md5(algorithm):
|
|
return MD5.new()
|
|
if _is_sha1(algorithm):
|
|
return SHA1.new()
|
|
if _is_sha256(algorithm):
|
|
return SHA256.new()
|
|
if _is_sha384(algorithm):
|
|
return SHA384.new()
|
|
if _is_sha512(algorithm):
|
|
return SHA512.new()
|
|
raise ValidationFailure('unknown hash for algorithm %u' % algorithm)
|
|
|
|
|
|
def _make_algorithm_id(algorithm):
|
|
if _is_md5(algorithm):
|
|
oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05]
|
|
elif _is_sha1(algorithm):
|
|
oid = [0x2b, 0x0e, 0x03, 0x02, 0x1a]
|
|
elif _is_sha256(algorithm):
|
|
oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]
|
|
elif _is_sha512(algorithm):
|
|
oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]
|
|
else:
|
|
raise ValidationFailure('unknown algorithm %u' % algorithm)
|
|
olen = len(oid)
|
|
dlen = _make_hash(algorithm).digest_size
|
|
idbytes = [0x30] + [8 + olen + dlen] + \
|
|
[0x30, olen + 4] + [0x06, olen] + oid + \
|
|
[0x05, 0x00] + [0x04, dlen]
|
|
return struct.pack('!%dB' % len(idbytes), *idbytes)
|
|
|
|
|
|
def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
|
|
"""Validate an RRset against a single signature rdata
|
|
|
|
The owner name of *rrsig* is assumed to be the same as the owner name
|
|
of *rrset*.
|
|
|
|
*rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or
|
|
a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.
|
|
|
|
*rrsig* is a ``dns.rdata.Rdata``, the signature to validate.
|
|
|
|
*keys* is the key dictionary, used to find the DNSKEY associated with
|
|
a given name. The dictionary is keyed by a ``dns.name.Name``, and has
|
|
``dns.node.Node`` or ``dns.rdataset.Rdataset`` values.
|
|
|
|
*origin* is a ``dns.name.Name``, the origin to use for relative names.
|
|
|
|
*now* is an ``int``, the time to use when validating the signatures,
|
|
in seconds since the UNIX epoch. The default is the current time.
|
|
"""
|
|
|
|
if isinstance(origin, string_types):
|
|
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 candidate_key in candidate_keys:
|
|
# 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:
|
|
raise ValidationFailure('expired')
|
|
if rrsig.inception > now:
|
|
raise ValidationFailure('not yet valid')
|
|
|
|
hash = _make_hash(rrsig.algorithm)
|
|
|
|
if _is_rsa(rrsig.algorithm):
|
|
keyptr = candidate_key.key
|
|
(bytes_,) = struct.unpack('!B', keyptr[0:1])
|
|
keyptr = keyptr[1:]
|
|
if bytes_ == 0:
|
|
(bytes_,) = struct.unpack('!H', keyptr[0:2])
|
|
keyptr = keyptr[2:]
|
|
rsa_e = keyptr[0:bytes_]
|
|
rsa_n = keyptr[bytes_:]
|
|
try:
|
|
pubkey = CryptoRSA.construct(
|
|
(number.bytes_to_long(rsa_n),
|
|
number.bytes_to_long(rsa_e)))
|
|
except ValueError:
|
|
raise ValidationFailure('invalid public key')
|
|
sig = rrsig.signature
|
|
elif _is_dsa(rrsig.algorithm):
|
|
keyptr = candidate_key.key
|
|
(t,) = struct.unpack('!B', keyptr[0:1])
|
|
keyptr = keyptr[1:]
|
|
octets = 64 + t * 8
|
|
dsa_q = keyptr[0:20]
|
|
keyptr = keyptr[20:]
|
|
dsa_p = keyptr[0:octets]
|
|
keyptr = keyptr[octets:]
|
|
dsa_g = keyptr[0:octets]
|
|
keyptr = keyptr[octets:]
|
|
dsa_y = keyptr[0:octets]
|
|
pubkey = CryptoDSA.construct(
|
|
(number.bytes_to_long(dsa_y),
|
|
number.bytes_to_long(dsa_g),
|
|
number.bytes_to_long(dsa_p),
|
|
number.bytes_to_long(dsa_q)))
|
|
sig = rrsig.signature[1:]
|
|
elif _is_ecdsa(rrsig.algorithm):
|
|
# use ecdsa for NIST-384p -- not currently supported by pycryptodome
|
|
|
|
keyptr = candidate_key.key
|
|
|
|
if rrsig.algorithm == ECDSAP256SHA256:
|
|
curve = ecdsa.curves.NIST256p
|
|
key_len = 32
|
|
elif rrsig.algorithm == ECDSAP384SHA384:
|
|
curve = ecdsa.curves.NIST384p
|
|
key_len = 48
|
|
|
|
x = number.bytes_to_long(keyptr[0:key_len])
|
|
y = number.bytes_to_long(keyptr[key_len:key_len * 2])
|
|
if not ecdsa.ecdsa.point_is_valid(curve.generator, x, y):
|
|
raise ValidationFailure('invalid ECDSA key')
|
|
point = ecdsa.ellipticcurve.Point(curve.curve, x, y, curve.order)
|
|
verifying_key = ecdsa.keys.VerifyingKey.from_public_point(point,
|
|
curve)
|
|
pubkey = ECKeyWrapper(verifying_key, key_len)
|
|
r = rrsig.signature[:key_len]
|
|
s = rrsig.signature[key_len:]
|
|
sig = ecdsa.ecdsa.Signature(number.bytes_to_long(r),
|
|
number.bytes_to_long(s))
|
|
|
|
else:
|
|
raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)
|
|
|
|
hash.update(_to_rdata(rrsig, origin)[:18])
|
|
hash.update(rrsig.signer.to_digestable(origin))
|
|
|
|
if rrsig.labels < len(rrname) - 1:
|
|
suffix = rrname.split(rrsig.labels + 1)[1]
|
|
rrname = dns.name.from_text('*', suffix)
|
|
rrnamebuf = rrname.to_digestable(origin)
|
|
rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
|
|
rrsig.original_ttl)
|
|
rrlist = sorted(rdataset)
|
|
for rr in rrlist:
|
|
hash.update(rrnamebuf)
|
|
hash.update(rrfixed)
|
|
rrdata = rr.to_digestable(origin)
|
|
rrlen = struct.pack('!H', len(rrdata))
|
|
hash.update(rrlen)
|
|
hash.update(rrdata)
|
|
|
|
try:
|
|
if _is_rsa(rrsig.algorithm):
|
|
verifier = pkcs1_15.new(pubkey)
|
|
# will raise ValueError if verify fails:
|
|
verifier.verify(hash, sig)
|
|
elif _is_dsa(rrsig.algorithm):
|
|
verifier = DSS.new(pubkey, 'fips-186-3')
|
|
verifier.verify(hash, sig)
|
|
elif _is_ecdsa(rrsig.algorithm):
|
|
digest = hash.digest()
|
|
if not pubkey.verify(digest, sig):
|
|
raise ValueError
|
|
else:
|
|
# Raise here for code clarity; this won't actually ever happen
|
|
# since if the algorithm is really unknown we'd already have
|
|
# raised an exception above
|
|
raise ValidationFailure('unknown algorithm %u' % rrsig.algorithm)
|
|
# If we got here, we successfully verified so we can return without error
|
|
return
|
|
except ValueError:
|
|
# this happens on an individual validation failure
|
|
continue
|
|
# nothing verified -- raise failure:
|
|
raise ValidationFailure('verify failure')
|
|
|
|
|
|
def _validate(rrset, rrsigset, keys, origin=None, now=None):
|
|
"""Validate an RRset.
|
|
|
|
*rrset* is the RRset to validate. It can be a ``dns.rrset.RRset`` or
|
|
a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.
|
|
|
|
*rrsigset* is the signature RRset to be validated. It can be a
|
|
``dns.rrset.RRset`` or a ``(dns.name.Name, dns.rdataset.Rdataset)`` tuple.
|
|
|
|
*keys* is the key dictionary, used to find the DNSKEY associated with
|
|
a given name. The dictionary is keyed by a ``dns.name.Name``, and has
|
|
``dns.node.Node`` or ``dns.rdataset.Rdataset`` values.
|
|
|
|
*origin* is a ``dns.name.Name``, the origin to use for relative names.
|
|
|
|
*now* is an ``int``, the time to use when validating the signatures,
|
|
in seconds since the UNIX epoch. The default is the current time.
|
|
"""
|
|
|
|
if isinstance(origin, string_types):
|
|
origin = dns.name.from_text(origin, dns.name.root)
|
|
|
|
if isinstance(rrset, tuple):
|
|
rrname = rrset[0]
|
|
else:
|
|
rrname = rrset.name
|
|
|
|
if isinstance(rrsigset, tuple):
|
|
rrsigname = rrsigset[0]
|
|
rrsigrdataset = rrsigset[1]
|
|
else:
|
|
rrsigname = rrsigset.name
|
|
rrsigrdataset = rrsigset
|
|
|
|
rrname = rrname.choose_relativity(origin)
|
|
rrsigname = rrsigname.choose_relativity(origin)
|
|
if rrname != rrsigname:
|
|
raise ValidationFailure("owner names do not match")
|
|
|
|
for rrsig in rrsigrdataset:
|
|
try:
|
|
_validate_rrsig(rrset, rrsig, keys, origin, now)
|
|
return
|
|
except ValidationFailure:
|
|
pass
|
|
raise ValidationFailure("no RRSIGs validated")
|
|
|
|
|
|
def _need_pycrypto(*args, **kwargs):
|
|
raise NotImplementedError("DNSSEC validation requires pycryptodome/pycryptodomex")
|
|
|
|
|
|
try:
|
|
try:
|
|
# test we're using pycryptodome, not pycrypto (which misses SHA1 for example)
|
|
from Crypto.Hash import MD5, SHA1, SHA256, SHA384, SHA512
|
|
from Crypto.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA
|
|
from Crypto.Signature import pkcs1_15, DSS
|
|
from Crypto.Util import number
|
|
except ImportError:
|
|
from Cryptodome.Hash import MD5, SHA1, SHA256, SHA384, SHA512
|
|
from Cryptodome.PublicKey import RSA as CryptoRSA, DSA as CryptoDSA
|
|
from Cryptodome.Signature import pkcs1_15, DSS
|
|
from Cryptodome.Util import number
|
|
except ImportError:
|
|
validate = _need_pycrypto
|
|
validate_rrsig = _need_pycrypto
|
|
_have_pycrypto = False
|
|
_have_ecdsa = False
|
|
else:
|
|
validate = _validate
|
|
validate_rrsig = _validate_rrsig
|
|
_have_pycrypto = True
|
|
|
|
try:
|
|
import ecdsa
|
|
import ecdsa.ecdsa
|
|
import ecdsa.ellipticcurve
|
|
import ecdsa.keys
|
|
except ImportError:
|
|
_have_ecdsa = False
|
|
else:
|
|
_have_ecdsa = True
|
|
|
|
class ECKeyWrapper(object):
|
|
|
|
def __init__(self, key, key_len):
|
|
self.key = key
|
|
self.key_len = key_len
|
|
|
|
def verify(self, digest, sig):
|
|
diglong = number.bytes_to_long(digest)
|
|
return self.key.pubkey.verifies(diglong, sig)
|