| 1 | # Copyright (C) 2003-2007, 2009 Nominum, Inc.
|
|---|
| 2 | #
|
|---|
| 3 | # Permission to use, copy, modify, and distribute this software and its
|
|---|
| 4 | # documentation for any purpose with or without fee is hereby granted,
|
|---|
| 5 | # provided that the above copyright notice and this permission notice
|
|---|
| 6 | # appear in all copies.
|
|---|
| 7 | #
|
|---|
| 8 | # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
|
|---|
| 9 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|---|
| 10 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
|
|---|
| 11 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|---|
| 12 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|---|
| 13 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|---|
| 14 | # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|---|
| 15 |
|
|---|
| 16 | """Common DNSSEC-related functions and constants."""
|
|---|
| 17 |
|
|---|
| 18 | import cStringIO
|
|---|
| 19 | import struct
|
|---|
| 20 | import time
|
|---|
| 21 |
|
|---|
| 22 | import dns.exception
|
|---|
| 23 | import dns.hash
|
|---|
| 24 | import dns.name
|
|---|
| 25 | import dns.node
|
|---|
| 26 | import dns.rdataset
|
|---|
| 27 | import dns.rdata
|
|---|
| 28 | import dns.rdatatype
|
|---|
| 29 | import dns.rdataclass
|
|---|
| 30 |
|
|---|
| 31 | class UnsupportedAlgorithm(dns.exception.DNSException):
|
|---|
| 32 | """Raised if an algorithm is not supported."""
|
|---|
| 33 | pass
|
|---|
| 34 |
|
|---|
| 35 | class ValidationFailure(dns.exception.DNSException):
|
|---|
| 36 | """The DNSSEC signature is invalid."""
|
|---|
| 37 | pass
|
|---|
| 38 |
|
|---|
| 39 | RSAMD5 = 1
|
|---|
| 40 | DH = 2
|
|---|
| 41 | DSA = 3
|
|---|
| 42 | ECC = 4
|
|---|
| 43 | RSASHA1 = 5
|
|---|
| 44 | DSANSEC3SHA1 = 6
|
|---|
| 45 | RSASHA1NSEC3SHA1 = 7
|
|---|
| 46 | RSASHA256 = 8
|
|---|
| 47 | RSASHA512 = 10
|
|---|
| 48 | INDIRECT = 252
|
|---|
| 49 | PRIVATEDNS = 253
|
|---|
| 50 | PRIVATEOID = 254
|
|---|
| 51 |
|
|---|
| 52 | _algorithm_by_text = {
|
|---|
| 53 | 'RSAMD5' : RSAMD5,
|
|---|
| 54 | 'DH' : DH,
|
|---|
| 55 | 'DSA' : DSA,
|
|---|
| 56 | 'ECC' : ECC,
|
|---|
| 57 | 'RSASHA1' : RSASHA1,
|
|---|
| 58 | 'DSANSEC3SHA1' : DSANSEC3SHA1,
|
|---|
| 59 | 'RSASHA1NSEC3SHA1' : RSASHA1NSEC3SHA1,
|
|---|
| 60 | 'RSASHA256' : RSASHA256,
|
|---|
| 61 | 'RSASHA512' : RSASHA512,
|
|---|
| 62 | 'INDIRECT' : INDIRECT,
|
|---|
| 63 | 'PRIVATEDNS' : PRIVATEDNS,
|
|---|
| 64 | 'PRIVATEOID' : PRIVATEOID,
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | # We construct the inverse mapping programmatically to ensure that we
|
|---|
| 68 | # cannot make any mistakes (e.g. omissions, cut-and-paste errors) that
|
|---|
| 69 | # would cause the mapping not to be true inverse.
|
|---|
| 70 |
|
|---|
| 71 | _algorithm_by_value = dict([(y, x) for x, y in _algorithm_by_text.iteritems()])
|
|---|
| 72 |
|
|---|
| 73 | def algorithm_from_text(text):
|
|---|
| 74 | """Convert text into a DNSSEC algorithm value
|
|---|
| 75 | @rtype: int"""
|
|---|
| 76 |
|
|---|
| 77 | value = _algorithm_by_text.get(text.upper())
|
|---|
| 78 | if value is None:
|
|---|
| 79 | value = int(text)
|
|---|
| 80 | return value
|
|---|
| 81 |
|
|---|
| 82 | def algorithm_to_text(value):
|
|---|
| 83 | """Convert a DNSSEC algorithm value to text
|
|---|
| 84 | @rtype: string"""
|
|---|
| 85 |
|
|---|
| 86 | text = _algorithm_by_value.get(value)
|
|---|
| 87 | if text is None:
|
|---|
| 88 | text = str(value)
|
|---|
| 89 | return text
|
|---|
| 90 |
|
|---|
| 91 | def _to_rdata(record, origin):
|
|---|
| 92 | s = cStringIO.StringIO()
|
|---|
| 93 | record.to_wire(s, origin=origin)
|
|---|
| 94 | return s.getvalue()
|
|---|
| 95 |
|
|---|
| 96 | def key_id(key, origin=None):
|
|---|
| 97 | rdata = _to_rdata(key, origin)
|
|---|
| 98 | if key.algorithm == RSAMD5:
|
|---|
| 99 | return (ord(rdata[-3]) << 8) + ord(rdata[-2])
|
|---|
| 100 | else:
|
|---|
| 101 | total = 0
|
|---|
| 102 | for i in range(len(rdata) / 2):
|
|---|
| 103 | total += (ord(rdata[2 * i]) << 8) + ord(rdata[2 * i + 1])
|
|---|
| 104 | if len(rdata) % 2 != 0:
|
|---|
| 105 | total += ord(rdata[len(rdata) - 1]) << 8
|
|---|
| 106 | total += ((total >> 16) & 0xffff);
|
|---|
| 107 | return total & 0xffff
|
|---|
| 108 |
|
|---|
| 109 | def make_ds(name, key, algorithm, origin=None):
|
|---|
| 110 | if algorithm.upper() == 'SHA1':
|
|---|
| 111 | dsalg = 1
|
|---|
| 112 | hash = dns.hash.get('SHA1')()
|
|---|
| 113 | elif algorithm.upper() == 'SHA256':
|
|---|
| 114 | dsalg = 2
|
|---|
| 115 | hash = dns.hash.get('SHA256')()
|
|---|
| 116 | else:
|
|---|
| 117 | raise UnsupportedAlgorithm, 'unsupported algorithm "%s"' % algorithm
|
|---|
| 118 |
|
|---|
| 119 | if isinstance(name, (str, unicode)):
|
|---|
| 120 | name = dns.name.from_text(name, origin)
|
|---|
| 121 | hash.update(name.canonicalize().to_wire())
|
|---|
| 122 | hash.update(_to_rdata(key, origin))
|
|---|
| 123 | digest = hash.digest()
|
|---|
| 124 |
|
|---|
| 125 | dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, dsalg) + digest
|
|---|
| 126 | return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0,
|
|---|
| 127 | len(dsrdata))
|
|---|
| 128 |
|
|---|
| 129 | def _find_key(keys, rrsig):
|
|---|
| 130 | value = keys.get(rrsig.signer)
|
|---|
| 131 | if value is None:
|
|---|
| 132 | return None
|
|---|
| 133 | if isinstance(value, dns.node.Node):
|
|---|
| 134 | try:
|
|---|
| 135 | rdataset = node.find_rdataset(dns.rdataclass.IN,
|
|---|
| 136 | dns.rdatatype.DNSKEY)
|
|---|
| 137 | except KeyError:
|
|---|
| 138 | return None
|
|---|
| 139 | else:
|
|---|
| 140 | rdataset = value
|
|---|
| 141 | for rdata in rdataset:
|
|---|
| 142 | if rdata.algorithm == rrsig.algorithm and \
|
|---|
| 143 | key_id(rdata) == rrsig.key_tag:
|
|---|
| 144 | return rdata
|
|---|
| 145 | return None
|
|---|
| 146 |
|
|---|
| 147 | def _is_rsa(algorithm):
|
|---|
| 148 | return algorithm in (RSAMD5, RSASHA1,
|
|---|
| 149 | RSASHA1NSEC3SHA1, RSASHA256,
|
|---|
| 150 | RSASHA512)
|
|---|
| 151 |
|
|---|
| 152 | def _is_dsa(algorithm):
|
|---|
| 153 | return algorithm in (DSA, DSANSEC3SHA1)
|
|---|
| 154 |
|
|---|
| 155 | def _is_md5(algorithm):
|
|---|
| 156 | return algorithm == RSAMD5
|
|---|
| 157 |
|
|---|
| 158 | def _is_sha1(algorithm):
|
|---|
| 159 | return algorithm in (DSA, RSASHA1,
|
|---|
| 160 | DSANSEC3SHA1, RSASHA1NSEC3SHA1)
|
|---|
| 161 |
|
|---|
| 162 | def _is_sha256(algorithm):
|
|---|
| 163 | return algorithm == RSASHA256
|
|---|
| 164 |
|
|---|
| 165 | def _is_sha512(algorithm):
|
|---|
| 166 | return algorithm == RSASHA512
|
|---|
| 167 |
|
|---|
| 168 | def _make_hash(algorithm):
|
|---|
| 169 | if _is_md5(algorithm):
|
|---|
| 170 | return dns.hash.get('MD5')()
|
|---|
| 171 | if _is_sha1(algorithm):
|
|---|
| 172 | return dns.hash.get('SHA1')()
|
|---|
| 173 | if _is_sha256(algorithm):
|
|---|
| 174 | return dns.hash.get('SHA256')()
|
|---|
| 175 | if _is_sha512(algorithm):
|
|---|
| 176 | return dns.hash.get('SHA512')()
|
|---|
| 177 | raise ValidationFailure, 'unknown hash for algorithm %u' % algorithm
|
|---|
| 178 |
|
|---|
| 179 | def _make_algorithm_id(algorithm):
|
|---|
| 180 | if _is_md5(algorithm):
|
|---|
| 181 | oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05]
|
|---|
| 182 | elif _is_sha1(algorithm):
|
|---|
| 183 | oid = [0x2b, 0x0e, 0x03, 0x02, 0x1a]
|
|---|
| 184 | elif _is_sha256(algorithm):
|
|---|
| 185 | oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]
|
|---|
| 186 | elif _is_sha512(algorithm):
|
|---|
| 187 | oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]
|
|---|
| 188 | else:
|
|---|
| 189 | raise ValidationFailure, 'unknown algorithm %u' % algorithm
|
|---|
| 190 | olen = len(oid)
|
|---|
| 191 | dlen = _make_hash(algorithm).digest_size
|
|---|
| 192 | idbytes = [0x30] + [8 + olen + dlen] + \
|
|---|
| 193 | [0x30, olen + 4] + [0x06, olen] + oid + \
|
|---|
| 194 | [0x05, 0x00] + [0x04, dlen]
|
|---|
| 195 | return ''.join(map(chr, idbytes))
|
|---|
| 196 |
|
|---|
| 197 | def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
|
|---|
| 198 | """Validate an RRset against a single signature rdata
|
|---|
| 199 |
|
|---|
| 200 | The owner name of the rrsig is assumed to be the same as the owner name
|
|---|
| 201 | of the rrset.
|
|---|
| 202 |
|
|---|
| 203 | @param rrset: The RRset to validate
|
|---|
| 204 | @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset)
|
|---|
| 205 | tuple
|
|---|
| 206 | @param rrsig: The signature rdata
|
|---|
| 207 | @type rrsig: dns.rrset.Rdata
|
|---|
| 208 | @param keys: The key dictionary.
|
|---|
| 209 | @type keys: a dictionary keyed by dns.name.Name with node or rdataset values
|
|---|
| 210 | @param origin: The origin to use for relative names
|
|---|
| 211 | @type origin: dns.name.Name or None
|
|---|
| 212 | @param now: The time to use when validating the signatures. The default
|
|---|
| 213 | is the current time.
|
|---|
| 214 | @type now: int
|
|---|
| 215 | """
|
|---|
| 216 |
|
|---|
| 217 | if isinstance(origin, (str, unicode)):
|
|---|
| 218 | origin = dns.name.from_text(origin, dns.name.root)
|
|---|
| 219 |
|
|---|
| 220 | key = _find_key(keys, rrsig)
|
|---|
| 221 | if not key:
|
|---|
| 222 | raise ValidationFailure, 'unknown key'
|
|---|
| 223 |
|
|---|
| 224 | # For convenience, allow the rrset to be specified as a (name, rdataset)
|
|---|
| 225 | # tuple as well as a proper rrset
|
|---|
| 226 | if isinstance(rrset, tuple):
|
|---|
| 227 | rrname = rrset[0]
|
|---|
| 228 | rdataset = rrset[1]
|
|---|
| 229 | else:
|
|---|
| 230 | rrname = rrset.name
|
|---|
| 231 | rdataset = rrset
|
|---|
| 232 |
|
|---|
| 233 | if now is None:
|
|---|
| 234 | now = time.time()
|
|---|
| 235 | if rrsig.expiration < now:
|
|---|
| 236 | raise ValidationFailure, 'expired'
|
|---|
| 237 | if rrsig.inception > now:
|
|---|
| 238 | raise ValidationFailure, 'not yet valid'
|
|---|
| 239 |
|
|---|
| 240 | hash = _make_hash(rrsig.algorithm)
|
|---|
| 241 |
|
|---|
| 242 | if _is_rsa(rrsig.algorithm):
|
|---|
| 243 | keyptr = key.key
|
|---|
| 244 | (bytes,) = struct.unpack('!B', keyptr[0:1])
|
|---|
| 245 | keyptr = keyptr[1:]
|
|---|
| 246 | if bytes == 0:
|
|---|
| 247 | (bytes,) = struct.unpack('!H', keyptr[0:2])
|
|---|
| 248 | keyptr = keyptr[2:]
|
|---|
| 249 | rsa_e = keyptr[0:bytes]
|
|---|
| 250 | rsa_n = keyptr[bytes:]
|
|---|
| 251 | keylen = len(rsa_n) * 8
|
|---|
| 252 | pubkey = Crypto.PublicKey.RSA.construct(
|
|---|
| 253 | (Crypto.Util.number.bytes_to_long(rsa_n),
|
|---|
| 254 | Crypto.Util.number.bytes_to_long(rsa_e)))
|
|---|
| 255 | sig = (Crypto.Util.number.bytes_to_long(rrsig.signature),)
|
|---|
| 256 | elif _is_dsa(rrsig.algorithm):
|
|---|
| 257 | keyptr = key.key
|
|---|
| 258 | (t,) = struct.unpack('!B', keyptr[0:1])
|
|---|
| 259 | keyptr = keyptr[1:]
|
|---|
| 260 | octets = 64 + t * 8
|
|---|
| 261 | dsa_q = keyptr[0:20]
|
|---|
| 262 | keyptr = keyptr[20:]
|
|---|
| 263 | dsa_p = keyptr[0:octets]
|
|---|
| 264 | keyptr = keyptr[octets:]
|
|---|
| 265 | dsa_g = keyptr[0:octets]
|
|---|
| 266 | keyptr = keyptr[octets:]
|
|---|
| 267 | dsa_y = keyptr[0:octets]
|
|---|
| 268 | pubkey = Crypto.PublicKey.DSA.construct(
|
|---|
| 269 | (Crypto.Util.number.bytes_to_long(dsa_y),
|
|---|
| 270 | Crypto.Util.number.bytes_to_long(dsa_g),
|
|---|
| 271 | Crypto.Util.number.bytes_to_long(dsa_p),
|
|---|
| 272 | Crypto.Util.number.bytes_to_long(dsa_q)))
|
|---|
| 273 | (dsa_r, dsa_s) = struct.unpack('!20s20s', rrsig.signature[1:])
|
|---|
| 274 | sig = (Crypto.Util.number.bytes_to_long(dsa_r),
|
|---|
| 275 | Crypto.Util.number.bytes_to_long(dsa_s))
|
|---|
| 276 | else:
|
|---|
| 277 | raise ValidationFailure, 'unknown algorithm %u' % rrsig.algorithm
|
|---|
| 278 |
|
|---|
| 279 | hash.update(_to_rdata(rrsig, origin)[:18])
|
|---|
| 280 | hash.update(rrsig.signer.to_digestable(origin))
|
|---|
| 281 |
|
|---|
| 282 | if rrsig.labels < len(rrname) - 1:
|
|---|
| 283 | suffix = rrname.split(rrsig.labels + 1)[1]
|
|---|
| 284 | rrname = dns.name.from_text('*', suffix)
|
|---|
| 285 | rrnamebuf = rrname.to_digestable(origin)
|
|---|
| 286 | rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
|
|---|
| 287 | rrsig.original_ttl)
|
|---|
| 288 | rrlist = sorted(rdataset);
|
|---|
| 289 | for rr in rrlist:
|
|---|
| 290 | hash.update(rrnamebuf)
|
|---|
| 291 | hash.update(rrfixed)
|
|---|
| 292 | rrdata = rr.to_digestable(origin)
|
|---|
| 293 | rrlen = struct.pack('!H', len(rrdata))
|
|---|
| 294 | hash.update(rrlen)
|
|---|
| 295 | hash.update(rrdata)
|
|---|
| 296 |
|
|---|
| 297 | digest = hash.digest()
|
|---|
| 298 |
|
|---|
| 299 | if _is_rsa(rrsig.algorithm):
|
|---|
| 300 | # PKCS1 algorithm identifier goop
|
|---|
| 301 | digest = _make_algorithm_id(rrsig.algorithm) + digest
|
|---|
| 302 | padlen = keylen / 8 - len(digest) - 3
|
|---|
| 303 | digest = chr(0) + chr(1) + chr(0xFF) * padlen + chr(0) + digest
|
|---|
| 304 | elif _is_dsa(rrsig.algorithm):
|
|---|
| 305 | pass
|
|---|
| 306 | else:
|
|---|
| 307 | # Raise here for code clarity; this won't actually ever happen
|
|---|
| 308 | # since if the algorithm is really unknown we'd already have
|
|---|
| 309 | # raised an exception above
|
|---|
| 310 | raise ValidationFailure, 'unknown algorithm %u' % rrsig.algorithm
|
|---|
| 311 |
|
|---|
| 312 | if not pubkey.verify(digest, sig):
|
|---|
| 313 | raise ValidationFailure, 'verify failure'
|
|---|
| 314 |
|
|---|
| 315 | def _validate(rrset, rrsigset, keys, origin=None, now=None):
|
|---|
| 316 | """Validate an RRset
|
|---|
| 317 |
|
|---|
| 318 | @param rrset: The RRset to validate
|
|---|
| 319 | @type rrset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset)
|
|---|
| 320 | tuple
|
|---|
| 321 | @param rrsigset: The signature RRset
|
|---|
| 322 | @type rrsigset: dns.rrset.RRset or (dns.name.Name, dns.rdataset.Rdataset)
|
|---|
| 323 | tuple
|
|---|
| 324 | @param keys: The key dictionary.
|
|---|
| 325 | @type keys: a dictionary keyed by dns.name.Name with node or rdataset values
|
|---|
| 326 | @param origin: The origin to use for relative names
|
|---|
| 327 | @type origin: dns.name.Name or None
|
|---|
| 328 | @param now: The time to use when validating the signatures. The default
|
|---|
| 329 | is the current time.
|
|---|
| 330 | @type now: int
|
|---|
| 331 | """
|
|---|
| 332 |
|
|---|
| 333 | if isinstance(origin, (str, unicode)):
|
|---|
| 334 | origin = dns.name.from_text(origin, dns.name.root)
|
|---|
| 335 |
|
|---|
| 336 | if isinstance(rrset, tuple):
|
|---|
| 337 | rrname = rrset[0]
|
|---|
| 338 | else:
|
|---|
| 339 | rrname = rrset.name
|
|---|
| 340 |
|
|---|
| 341 | if isinstance(rrsigset, tuple):
|
|---|
| 342 | rrsigname = rrsigset[0]
|
|---|
| 343 | rrsigrdataset = rrsigset[1]
|
|---|
| 344 | else:
|
|---|
| 345 | rrsigname = rrsigset.name
|
|---|
| 346 | rrsigrdataset = rrsigset
|
|---|
| 347 |
|
|---|
| 348 | rrname = rrname.choose_relativity(origin)
|
|---|
| 349 | rrsigname = rrname.choose_relativity(origin)
|
|---|
| 350 | if rrname != rrsigname:
|
|---|
| 351 | raise ValidationFailure, "owner names do not match"
|
|---|
| 352 |
|
|---|
| 353 | for rrsig in rrsigrdataset:
|
|---|
| 354 | try:
|
|---|
| 355 | _validate_rrsig(rrset, rrsig, keys, origin, now)
|
|---|
| 356 | return
|
|---|
| 357 | except ValidationFailure, e:
|
|---|
| 358 | pass
|
|---|
| 359 | raise ValidationFailure, "no RRSIGs validated"
|
|---|
| 360 |
|
|---|
| 361 | def _need_pycrypto(*args, **kwargs):
|
|---|
| 362 | raise NotImplementedError, "DNSSEC validation requires pycrypto"
|
|---|
| 363 |
|
|---|
| 364 | try:
|
|---|
| 365 | import Crypto.PublicKey.RSA
|
|---|
| 366 | import Crypto.PublicKey.DSA
|
|---|
| 367 | import Crypto.Util.number
|
|---|
| 368 | validate = _validate
|
|---|
| 369 | validate_rrsig = _validate_rrsig
|
|---|
| 370 | except ImportError:
|
|---|
| 371 | validate = _need_pycrypto
|
|---|
| 372 | validate_rrsig = _need_pycrypto
|
|---|