1#
2# This file is a part of DNSViz, a tool suite for DNS/DNSSEC monitoring,
3# analysis, and visualization.
4# Created by Casey Deccio (casey@deccio.net)
5#
6# Copyright 2012-2014 Sandia Corporation. Under the terms of Contract
7# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
8# certain rights in this software.
9#
10# Copyright 2014-2016 VeriSign, Inc.
11#
12# Copyright 2016-2021 Casey Deccio
13#
14# DNSViz is free software; you can redistribute it and/or modify
15# it under the terms of the GNU General Public License as published by
16# the Free Software Foundation; either version 2 of the License, or
17# (at your option) any later version.
18#
19# DNSViz is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License along
25# with DNSViz.  If not, see <http://www.gnu.org/licenses/>.
26#
27
28from __future__ import unicode_literals
29
30import base64
31import datetime
32import logging
33
34# minimal support for python2.6
35try:
36    from collections import OrderedDict
37except ImportError:
38    from ordereddict import OrderedDict
39
40# python3/python2 dual compatibility
41try:
42    from html import escape
43except ImportError:
44    from cgi import escape
45
46import dns.name, dns.rdatatype
47
48from dnsviz import base32
49from dnsviz import crypto
50from dnsviz import format as fmt
51from dnsviz.util import tuple_to_dict
52lb2s = fmt.latin1_binary_to_string
53
54from . import errors as Errors
55
56CLOCK_SKEW_WARNING = 300
57
58STATUS_VALID = 0
59STATUS_INDETERMINATE = 1
60STATUS_INVALID = 2
61status_mapping = {
62        STATUS_VALID: 'VALID',
63        True: 'VALID',
64        STATUS_INDETERMINATE: 'INDETERMINATE',
65        None: 'INDETERMINATE',
66        STATUS_INVALID: 'INVALID',
67        False: 'INVALID',
68}
69
70NAME_STATUS_NOERROR = 0
71NAME_STATUS_NXDOMAIN = 1
72NAME_STATUS_INDETERMINATE = 2
73name_status_mapping = {
74        NAME_STATUS_NOERROR: 'NOERROR',
75        NAME_STATUS_NXDOMAIN: 'NXDOMAIN',
76        NAME_STATUS_INDETERMINATE: 'INDETERMINATE',
77}
78
79RRSIG_STATUS_VALID = STATUS_VALID
80RRSIG_STATUS_INDETERMINATE_NO_DNSKEY = 1
81RRSIG_STATUS_INDETERMINATE_MATCH_PRE_REVOKE = 2
82RRSIG_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM = 3
83RRSIG_STATUS_ALGORITHM_IGNORED = 4
84RRSIG_STATUS_EXPIRED = 5
85RRSIG_STATUS_PREMATURE = 6
86RRSIG_STATUS_INVALID_SIG = 7
87RRSIG_STATUS_INVALID = 8
88rrsig_status_mapping = {
89        RRSIG_STATUS_VALID: 'VALID',
90        RRSIG_STATUS_INDETERMINATE_NO_DNSKEY: 'INDETERMINATE_NO_DNSKEY',
91        RRSIG_STATUS_INDETERMINATE_MATCH_PRE_REVOKE: 'INDETERMINATE_MATCH_PRE_REVOKE',
92        RRSIG_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM: 'INDETERMINATE_UNKNOWN_ALGORITHM',
93        RRSIG_STATUS_ALGORITHM_IGNORED: 'ALGORITHM_IGNORED',
94        RRSIG_STATUS_EXPIRED: 'EXPIRED',
95        RRSIG_STATUS_PREMATURE: 'PREMATURE',
96        RRSIG_STATUS_INVALID_SIG: 'INVALID_SIG',
97        RRSIG_STATUS_INVALID: 'INVALID',
98}
99
100DS_STATUS_VALID = STATUS_VALID
101DS_STATUS_INDETERMINATE_NO_DNSKEY = 1
102DS_STATUS_INDETERMINATE_MATCH_PRE_REVOKE = 2
103DS_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM = 3
104DS_STATUS_ALGORITHM_IGNORED = 4
105DS_STATUS_INVALID_DIGEST = 5
106DS_STATUS_INVALID = 6
107ds_status_mapping = {
108        DS_STATUS_VALID: 'VALID',
109        DS_STATUS_INDETERMINATE_NO_DNSKEY: 'INDETERMINATE_NO_DNSKEY',
110        DS_STATUS_INDETERMINATE_MATCH_PRE_REVOKE: 'INDETERMINATE_MATCH_PRE_REVOKE',
111        DS_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM: 'INDETERMINATE_UNKNOWN_ALGORITHM',
112        DS_STATUS_ALGORITHM_IGNORED: 'ALGORITHM_IGNORED',
113        DS_STATUS_INVALID_DIGEST: 'INVALID_DIGEST',
114        DS_STATUS_INVALID: 'INVALID',
115}
116
117DELEGATION_STATUS_SECURE = 0
118DELEGATION_STATUS_INSECURE = 1
119DELEGATION_STATUS_BOGUS = 2
120DELEGATION_STATUS_INCOMPLETE = 3
121DELEGATION_STATUS_LAME = 4
122delegation_status_mapping = {
123        DELEGATION_STATUS_SECURE: 'SECURE',
124        DELEGATION_STATUS_INSECURE: 'INSECURE',
125        DELEGATION_STATUS_BOGUS: 'BOGUS',
126        DELEGATION_STATUS_INCOMPLETE: 'INCOMPLETE',
127        DELEGATION_STATUS_LAME: 'LAME',
128}
129
130RRSET_STATUS_SECURE = 0
131RRSET_STATUS_INSECURE = 1
132RRSET_STATUS_BOGUS = 2
133RRSET_STATUS_NON_EXISTENT = 3
134rrset_status_mapping = {
135        RRSET_STATUS_SECURE: 'SECURE',
136        RRSET_STATUS_INSECURE: 'INSECURE',
137        RRSET_STATUS_BOGUS: 'BOGUS',
138        RRSET_STATUS_NON_EXISTENT: 'NON_EXISTENT',
139}
140
141NSEC_STATUS_VALID = STATUS_VALID
142NSEC_STATUS_INDETERMINATE = STATUS_INDETERMINATE
143NSEC_STATUS_INVALID = 2
144nsec_status_mapping = {
145        NSEC_STATUS_VALID: 'VALID',
146        NSEC_STATUS_INDETERMINATE: 'INDETERMINATE',
147        NSEC_STATUS_INVALID: 'INVALID',
148}
149
150DNAME_STATUS_VALID = STATUS_VALID
151DNAME_STATUS_INDETERMINATE = STATUS_INDETERMINATE
152DNAME_STATUS_INVALID_TARGET = 2
153DNAME_STATUS_INVALID = 3
154dname_status_mapping = {
155        DNAME_STATUS_VALID: 'VALID',
156        DNAME_STATUS_INDETERMINATE: 'INDETERMINATE',
157        DNAME_STATUS_INVALID_TARGET: 'INVALID_TARGET',
158        DNAME_STATUS_INVALID: 'INVALID',
159}
160
161RRSIG_SIG_LENGTHS_BY_ALGORITHM = {
162        12: 512, 13: 512, 14: 768, 15: 512, 16: 912,
163}
164RRSIG_SIG_LENGTH_ERRORS = {
165        12: Errors.RRSIGBadLengthGOST, 13: Errors.RRSIGBadLengthECDSA256,
166        14: Errors.RRSIGBadLengthECDSA384, 15: Errors.RRSIGBadLengthEd25519,
167        16: Errors.RRSIGBadLengthEd448,
168}
169DS_DIGEST_ALGS_STRONGER_THAN_SHA1 = (2, 4)
170DS_DIGEST_ALGS_IGNORING_SHA1 = (2,)
171
172# RFC 8624 Section 3.1
173DNSKEY_ALGS_NOT_RECOMMENDED = (5, 7, 10)
174DNSKEY_ALGS_PROHIBITED = (1, 3, 6, 12)
175DNSKEY_ALGS_VALIDATION_PROHIBITED = (1, 3, 6)
176
177# RFC 8624 Section 3.2
178DS_DIGEST_ALGS_NOT_RECOMMENDED = ()
179DS_DIGEST_ALGS_PROHIBITED = (0, 1, 3)
180DS_DIGEST_ALGS_VALIDATION_PROHIBITED = ()
181
182class RRSIGStatus(object):
183    def __init__(self, rrset, rrsig, dnskey, zone_name, reference_ts, supported_algs):
184        self.rrset = rrset
185        self.rrsig = rrsig
186        self.dnskey = dnskey
187        self.zone_name = zone_name
188        self.reference_ts = reference_ts
189        self.warnings = []
190        self.errors = []
191
192        if self.dnskey is None:
193            self.signature_valid = None
194        else:
195            self.signature_valid = crypto.validate_rrsig(dnskey.rdata.algorithm, rrsig.signature, rrset.message_for_rrsig(rrsig), dnskey.rdata.key)
196
197        self.validation_status = RRSIG_STATUS_VALID
198        if self.signature_valid is None or self.rrsig.algorithm not in supported_algs:
199            # Either we can't validate the cryptographic signature, or we are
200            # explicitly directed to ignore the algorithm.
201            if self.dnskey is None:
202                # In this case, there is no corresponding DNSKEY, so we make
203                # the status "INDETERMINATE".
204                if self.validation_status == RRSIG_STATUS_VALID:
205                    self.validation_status = RRSIG_STATUS_INDETERMINATE_NO_DNSKEY
206
207            else:
208                # If there is a DNSKEY, then we look at *why* we are ignoring
209                # the cryptographic signature.
210                if self.dnskey.rdata.algorithm in DNSKEY_ALGS_VALIDATION_PROHIBITED:
211                    # In this case, specification dictates that the algorithm
212                    # MUST NOT be validated, so we mark it as ignored.
213                    if self.validation_status == RRSIG_STATUS_VALID:
214                        self.validation_status = RRSIG_STATUS_ALGORITHM_IGNORED
215                else:
216                    # In this case, we can't validate this particular
217                    # algorithm, either because the code doesn't support it,
218                    # or because we have been explicitly directed to ignore it.
219                    # In either case, mark it as "UNKNOWN", and warn that it is
220                    # not supported.
221                    if self.validation_status == RRSIG_STATUS_VALID:
222                        self.validation_status = RRSIG_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM
223                    self.warnings.append(Errors.AlgorithmNotSupported(algorithm=self.rrsig.algorithm))
224
225        # Independent of whether or not we considered the cryptographic
226        # validation, issue a warning if we are using an algorithm for which
227        # validation or signing has been prohibited.
228        #
229        # Signing is prohibited
230        if self.rrsig.algorithm in DNSKEY_ALGS_VALIDATION_PROHIBITED:
231            self.warnings.append(Errors.AlgorithmValidationProhibited(algorithm=self.rrsig.algorithm))
232        # Validation is prohibited or, at least, not recommended
233        if self.rrsig.algorithm in DNSKEY_ALGS_PROHIBITED:
234            self.warnings.append(Errors.AlgorithmProhibited(algorithm=self.rrsig.algorithm))
235        elif self.rrsig.algorithm in DNSKEY_ALGS_NOT_RECOMMENDED:
236            self.warnings.append(Errors.AlgorithmNotRecommended(algorithm=self.rrsig.algorithm))
237
238        if self.rrset.ttl_cmp:
239            if self.rrset.rrset.ttl != self.rrset.rrsig_info[self.rrsig].ttl:
240                self.warnings.append(Errors.RRsetTTLMismatch(rrset_ttl=self.rrset.rrset.ttl, rrsig_ttl=self.rrset.rrsig_info[self.rrsig].ttl))
241        if self.rrset.rrsig_info[self.rrsig].ttl > self.rrsig.original_ttl:
242            self.errors.append(Errors.OriginalTTLExceeded(rrset_ttl=self.rrset.rrset.ttl, original_ttl=self.rrsig.original_ttl))
243
244        min_ttl = min(self.rrset.rrset.ttl, self.rrset.rrsig_info[self.rrsig].ttl, self.rrsig.original_ttl)
245
246        if (zone_name is not None and self.rrsig.signer != zone_name) or \
247                (zone_name is None and not self.rrset.rrset.name.is_subdomain(self.rrsig.signer)):
248            if self.validation_status == RRSIG_STATUS_VALID:
249                self.validation_status = RRSIG_STATUS_INVALID
250            if zone_name is None:
251                zn = self.rrsig.signer
252            else:
253                zn = zone_name
254            self.errors.append(Errors.SignerNotZone(zone_name=fmt.humanize_name(zn), signer_name=fmt.humanize_name(self.rrsig.signer)))
255
256        if self.dnskey is not None and \
257                self.dnskey.rdata.flags & fmt.DNSKEY_FLAGS['revoke'] and self.rrsig.covers() != dns.rdatatype.DNSKEY:
258            if self.rrsig.key_tag != self.dnskey.key_tag:
259                if self.validation_status == RRSIG_STATUS_VALID:
260                    self.validation_status = RRSIG_STATUS_INDETERMINATE_MATCH_PRE_REVOKE
261            else:
262                self.errors.append(Errors.DNSKEYRevokedRRSIG())
263                if self.validation_status == RRSIG_STATUS_VALID:
264                    self.validation_status = RRSIG_STATUS_INVALID
265
266        sig_len = len(self.rrsig.signature) << 3
267        if self.rrsig.algorithm in RRSIG_SIG_LENGTHS_BY_ALGORITHM and \
268                sig_len != RRSIG_SIG_LENGTHS_BY_ALGORITHM[self.rrsig.algorithm]:
269            self.errors.append(RRSIG_SIG_LENGTH_ERRORS[self.rrsig.algorithm](length=sig_len))
270
271        if self.reference_ts < self.rrsig.inception:
272            if self.validation_status == RRSIG_STATUS_VALID:
273                self.validation_status = RRSIG_STATUS_PREMATURE
274            self.errors.append(Errors.InceptionInFuture(inception=fmt.timestamp_to_datetime(self.rrsig.inception), reference_time=fmt.timestamp_to_datetime(self.reference_ts)))
275        elif self.reference_ts - CLOCK_SKEW_WARNING < self.rrsig.inception:
276            self.warnings.append(Errors.InceptionWithinClockSkew(inception=fmt.timestamp_to_datetime(self.rrsig.inception), reference_time=fmt.timestamp_to_datetime(self.reference_ts)))
277
278        if self.reference_ts >= self.rrsig.expiration:
279            if self.validation_status == RRSIG_STATUS_VALID:
280                self.validation_status = RRSIG_STATUS_EXPIRED
281            self.errors.append(Errors.ExpirationInPast(expiration=fmt.timestamp_to_datetime(self.rrsig.expiration), reference_time=fmt.timestamp_to_datetime(self.reference_ts)))
282        elif self.reference_ts + min_ttl >= self.rrsig.expiration:
283            self.errors.append(Errors.TTLBeyondExpiration(expiration=fmt.timestamp_to_datetime(self.rrsig.expiration), rrsig_ttl=min_ttl, reference_time=fmt.timestamp_to_datetime(self.reference_ts)))
284        elif self.reference_ts + CLOCK_SKEW_WARNING >= self.rrsig.expiration:
285            self.warnings.append(Errors.ExpirationWithinClockSkew(expiration=fmt.timestamp_to_datetime(self.rrsig.expiration), reference_time=fmt.timestamp_to_datetime(self.reference_ts)))
286
287        if self.signature_valid == False and self.dnskey.rdata.algorithm in supported_algs:
288            # only report this if we're not referring to a key revoked post-sign
289            if self.dnskey.key_tag == self.rrsig.key_tag:
290                if self.validation_status == RRSIG_STATUS_VALID:
291                    self.validation_status = RRSIG_STATUS_INVALID_SIG
292                self.errors.append(Errors.SignatureInvalid())
293
294    def __str__(self):
295        return 'RRSIG covering %s/%s' % (fmt.humanize_name(self.rrset.rrset.name), dns.rdatatype.to_text(self.rrset.rrset.rdtype))
296
297    def serialize(self, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
298        d = OrderedDict()
299
300        erroneous_status = self.validation_status not in (RRSIG_STATUS_VALID, RRSIG_STATUS_INDETERMINATE_NO_DNSKEY, RRSIG_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM)
301
302        show_id = loglevel <= logging.INFO or \
303                (self.warnings and loglevel <= logging.WARNING) or \
304                (self.errors and loglevel <= logging.ERROR) or \
305                erroneous_status
306
307        if html_format:
308            formatter = lambda x: escape(x, True)
309        else:
310            formatter = lambda x: x
311
312        if show_id:
313            d['id'] = '%s/%d/%d' % (lb2s(self.rrsig.signer.canonicalize().to_text()), self.rrsig.algorithm, self.rrsig.key_tag)
314
315        if loglevel <= logging.DEBUG:
316            d.update((
317                ('description', formatter(str(self))),
318                ('signer', formatter(lb2s(self.rrsig.signer.canonicalize().to_text()))),
319                ('algorithm', self.rrsig.algorithm),
320                ('key_tag', self.rrsig.key_tag),
321                ('original_ttl', self.rrsig.original_ttl),
322                ('labels', self.rrsig.labels),
323                ('inception', fmt.timestamp_to_str(self.rrsig.inception)),
324                ('expiration', fmt.timestamp_to_str(self.rrsig.expiration)),
325                ('signature', lb2s(base64.b64encode(self.rrsig.signature))),
326                ('ttl', self.rrset.rrsig_info[self.rrsig].ttl),
327            ))
328
329            if html_format:
330                d['algorithm'] = '%d (%s)' % (self.rrsig.algorithm, fmt.DNSKEY_ALGORITHMS.get(self.rrsig.algorithm, self.rrsig.algorithm))
331                d['original_ttl'] = '%d (%s)' % (self.rrsig.original_ttl, fmt.humanize_time(self.rrsig.original_ttl))
332                if self.rrset.is_wildcard(self.rrsig):
333                    d['labels'] = '%d (wildcard)' % (self.rrsig.labels)
334                else:
335                    d['labels'] = '%d (no wildcard)' % (self.rrsig.labels)
336                d['inception'] += ' (%s)' % (fmt.format_diff(fmt.timestamp_to_datetime(self.reference_ts), fmt.timestamp_to_datetime(self.rrsig.inception)))
337                d['expiration'] += ' (%s)' % (fmt.format_diff(fmt.timestamp_to_datetime(self.reference_ts), fmt.timestamp_to_datetime(self.rrsig.expiration)))
338                d['ttl'] = '%d (%s)' % (self.rrset.rrsig_info[self.rrsig].ttl, fmt.humanize_time(self.rrset.rrsig_info[self.rrsig].ttl))
339
340        if loglevel <= logging.INFO or erroneous_status:
341            d['status'] = rrsig_status_mapping[self.validation_status]
342
343        if loglevel <= logging.INFO:
344            servers = tuple_to_dict(self.rrset.rrsig_info[self.rrsig].servers_clients)
345            if consolidate_clients:
346                servers = list(servers)
347                servers.sort()
348            d['servers'] = servers
349
350            if map_ip_to_ns_name is not None:
351                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
352                ns_names.sort()
353                d['ns_names'] = ns_names
354
355            tags = set()
356            nsids = set()
357            for server,client in self.rrset.rrsig_info[self.rrsig].servers_clients:
358                for response in self.rrset.rrsig_info[self.rrsig].servers_clients[(server, client)]:
359                    if response is not None:
360                        tags.add(response.effective_query_tag())
361                        nsid = response.nsid_val()
362                        if nsid is not None:
363                            nsids.add(nsid)
364
365            if nsids:
366                d['nsid_values'] = list(nsids)
367                d['nsid_values'].sort()
368
369            d['query_options'] = list(tags)
370            d['query_options'].sort()
371
372        if self.warnings and loglevel <= logging.WARNING:
373            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
374
375        if self.errors and loglevel <= logging.ERROR:
376            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
377
378        return d
379
380class DSStatus(object):
381    def __init__(self, ds, ds_meta, dnskey, supported_digest_algs):
382        self.ds = ds
383        self.ds_meta = ds_meta
384        self.dnskey = dnskey
385        self.warnings = []
386        self.errors = []
387
388        if self.dnskey is None:
389            self.digest_valid = None
390        else:
391            self.digest_valid = crypto.validate_ds_digest(ds.digest_type, ds.digest, dnskey.message_for_ds())
392
393        self.validation_status = DS_STATUS_VALID
394        if self.digest_valid is None or self.ds.digest_type not in supported_digest_algs:
395            # Either we cannot reproduce a digest with this type, or we are
396            # explicitly directed to ignore the digest type.
397            if self.dnskey is None:
398                # In this case, there is no corresponding DNSKEY, so we make
399                # the status "INDETERMINATE".
400                if self.validation_status == DS_STATUS_VALID:
401                    self.validation_status = DS_STATUS_INDETERMINATE_NO_DNSKEY
402            else:
403                # If there is a DNSKEY, then we look at *why* we are ignoring
404                # the digest of the DNSKEY.
405                if self.ds.digest_type in DS_DIGEST_ALGS_VALIDATION_PROHIBITED:
406                    # In this case, specification dictates that the algorithm
407                    # MUST NOT be validated, so we mark it as ignored.
408                    if self.validation_status == DS_STATUS_VALID:
409                        self.validation_status = DS_STATUS_ALGORITHM_IGNORED
410                else:
411                    # In this case, we can't validate this particular
412                    # digest type, either because the code doesn't support it,
413                    # or because we have been explicitly directed to ignore it.
414                    # In either case, mark it as "UNKNOWN", and warn that it is
415                    # not supported.
416                    if self.validation_status == DS_STATUS_VALID:
417                        self.validation_status = DS_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM
418                    self.warnings.append(Errors.DigestAlgorithmNotSupported(algorithm=self.ds.digest_type))
419
420        # Independent of whether or not we considered the digest for
421        # validation, issue a warning if we are using a digest type for which
422        # validation or signing has been prohibited.
423        #
424        # Signing is prohibited
425        if self.ds.digest_type in DS_DIGEST_ALGS_VALIDATION_PROHIBITED:
426            self.warnings.append(Errors.DigestAlgorithmValidationProhibited(algorithm=self.ds.digest_type))
427        # Validation is prohibited or, at least, not recommended
428        if self.ds.digest_type in DS_DIGEST_ALGS_PROHIBITED:
429            self.warnings.append(Errors.DigestAlgorithmProhibited(algorithm=self.ds.digest_type))
430        elif self.ds.digest_type in DS_DIGEST_ALGS_NOT_RECOMMENDED:
431            self.warnings.append(Errors.DigestAlgorithmNotRecommended(algorithm=self.ds.digest_type))
432
433        if self.dnskey is not None and \
434                self.dnskey.rdata.flags & fmt.DNSKEY_FLAGS['revoke']:
435            if self.dnskey.key_tag != self.ds.key_tag:
436                if self.validation_status == DS_STATUS_VALID:
437                    self.validation_status = DS_STATUS_INDETERMINATE_MATCH_PRE_REVOKE
438            else:
439                self.errors.append(Errors.DNSKEYRevokedDS())
440                if self.validation_status == DS_STATUS_VALID:
441                    self.validation_status = DS_STATUS_INVALID
442
443        if self.digest_valid == False and self.ds.digest_type in supported_digest_algs:
444            # only report this if we're not referring to a key revoked post-DS
445            if self.dnskey.key_tag == self.ds.key_tag:
446                if self.validation_status == DS_STATUS_VALID:
447                    self.validation_status = DS_STATUS_INVALID_DIGEST
448                self.errors.append(Errors.DigestInvalid())
449
450        # RFC 4509
451        if self.ds.digest_type == 1:
452            stronger_algs_all_ds = set()
453            # Cycle through all other DS records in the DS RRset, and
454            # create a list of digest types that are stronger than SHA1
455            # and are being used by DS records across the *entire* DS.
456            for ds_rdata in self.ds_meta.rrset:
457                if ds_rdata.digest_type in DS_DIGEST_ALGS_STRONGER_THAN_SHA1:
458                    stronger_algs_all_ds.add(ds_rdata.digest_type)
459
460            # Consider only digest types that we actually support
461            stronger_algs_all_ds.intersection_update(supported_digest_algs)
462
463            if stronger_algs_all_ds:
464                # If there are DS records in the DS RRset with digest type
465                # stronger than SHA1, then this one MUST be ignored by
466                # validators (RFC 4509).
467                for digest_alg in stronger_algs_all_ds:
468                    if digest_alg in DS_DIGEST_ALGS_IGNORING_SHA1:
469                        if self.validation_status == DS_STATUS_VALID:
470                            self.validation_status = DS_STATUS_ALGORITHM_IGNORED
471                        self.warnings.append(Errors.DSDigestAlgorithmIgnored(algorithm=1, new_algorithm=digest_alg))
472                    else:
473                        self.warnings.append(Errors.DSDigestAlgorithmMaybeIgnored(algorithm=1, new_algorithm=digest_alg))
474
475    def __str__(self):
476        return '%s record(s) corresponding to DNSKEY for %s (algorithm %d (%s), key tag %d)' % (dns.rdatatype.to_text(self.ds_meta.rrset.rdtype), fmt.humanize_name(self.ds_meta.rrset.name), self.ds.algorithm, fmt.DNSKEY_ALGORITHMS.get(self.ds.algorithm, self.ds.algorithm), self.ds.key_tag)
477
478    def serialize(self, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=True):
479        d = OrderedDict()
480
481        erroneous_status = self.validation_status not in (DS_STATUS_VALID, DS_STATUS_INDETERMINATE_NO_DNSKEY, DS_STATUS_INDETERMINATE_UNKNOWN_ALGORITHM)
482
483        show_id = loglevel <= logging.INFO or \
484                (self.warnings and loglevel <= logging.WARNING) or \
485                (self.errors and loglevel <= logging.ERROR) or \
486                erroneous_status
487
488        if html_format:
489            formatter = lambda x: escape(x, True)
490        else:
491            formatter = lambda x: x
492
493        if show_id:
494            d['id'] = '%d/%d/%d' % (self.ds.algorithm, self.ds.key_tag, self.ds.digest_type)
495
496        if loglevel <= logging.DEBUG:
497            d.update((
498                ('description', formatter(str(self))),
499                ('algorithm', self.ds.algorithm),
500                ('key_tag', self.ds.key_tag),
501                ('digest_type', self.ds.digest_type),
502                ('digest', lb2s(base64.b64encode(self.ds.digest))),
503            ))
504
505            if html_format:
506                d['algorithm'] = '%d (%s)' % (self.ds.algorithm, fmt.DNSKEY_ALGORITHMS.get(self.ds.algorithm, self.ds.algorithm))
507                d['digest_type'] = '%d (%s)' % (self.ds.digest_type, fmt.DS_DIGEST_TYPES.get(self.ds.digest_type, self.ds.digest_type))
508
509            d['ttl'] = self.ds_meta.rrset.ttl
510            if html_format:
511                d['ttl'] = '%d (%s)' % (self.ds_meta.rrset.ttl, fmt.humanize_time(self.ds_meta.rrset.ttl))
512
513        if loglevel <= logging.INFO or erroneous_status:
514            d['status'] = ds_status_mapping[self.validation_status]
515
516        if loglevel <= logging.INFO:
517            servers = tuple_to_dict(self.ds_meta.servers_clients)
518            if consolidate_clients:
519                servers = list(servers)
520                servers.sort()
521            d['servers'] = servers
522
523            if map_ip_to_ns_name is not None:
524                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
525                ns_names.sort()
526                d['ns_names'] = ns_names
527
528            tags = set()
529            nsids = set()
530            for server,client in self.ds_meta.servers_clients:
531                for response in self.ds_meta.servers_clients[(server, client)]:
532                    if response is not None:
533                        tags.add(response.effective_query_tag())
534                        nsid = response.nsid_val()
535                        if nsid is not None:
536                            nsids.add(nsid)
537
538            if nsids:
539                d['nsid_values'] = list(nsids)
540                d['nsid_values'].sort()
541
542            d['query_options'] = list(tags)
543            d['query_options'].sort()
544
545        if self.warnings and loglevel <= logging.WARNING:
546            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
547
548        if self.errors and loglevel <= logging.ERROR:
549            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
550
551        return d
552
553class NSECStatus(object):
554    def __repr__(self):
555        return '<%s: "%s">' % (self.__class__.__name__, self.qname)
556
557    def _get_wildcard(self, qname, nsec_rrset):
558        covering_name = nsec_rrset.name
559        next_name = nsec_rrset[0].next
560        for i in range(len(qname)):
561            j = -(i + 1)
562            if i < len(covering_name) and covering_name[j].lower() == qname[j].lower():
563                continue
564            elif i < len(next_name) and next_name[j].lower() == qname[j].lower():
565                continue
566            else:
567                break
568        return dns.name.Name(('*',) + qname[-i:])
569
570class NSECStatusNXDOMAIN(NSECStatus):
571    def __init__(self, qname, rdtype, origin, is_zone, nsec_set_info):
572        self.qname = qname
573        self.origin = origin
574        self.is_zone = is_zone
575        self.warnings = []
576        self.errors = []
577
578        self.wildcard_name = None
579
580        self.nsec_names_covering_qname = {}
581        covering_names = nsec_set_info.nsec_covering_name(self.qname)
582        self.opt_out = None
583
584        if covering_names:
585            self.nsec_names_covering_qname[self.qname] = covering_names
586
587            covering_name = list(covering_names)[0]
588            self.wildcard_name = self._get_wildcard(qname, nsec_set_info.rrsets[covering_name].rrset)
589
590        self.nsec_names_covering_wildcard = {}
591        if self.wildcard_name is not None:
592            covering_names = nsec_set_info.nsec_covering_name(self.wildcard_name)
593            if covering_names:
594                self.nsec_names_covering_wildcard[self.wildcard_name] = covering_names
595
596        # check for covering of the origin
597        self.nsec_names_covering_origin = {}
598        covering_names = nsec_set_info.nsec_covering_name(self.origin)
599        if covering_names:
600            self.nsec_names_covering_origin[self.origin] = covering_names
601
602        self._set_validation_status(nsec_set_info)
603
604    def __eq__(self, other):
605        return isinstance(other, self.__class__) and \
606                self.qname == other.qname and self.origin == other.origin and self.nsec_set_info == other.nsec_set_info
607
608    def __hash__(self):
609        return hash(id(self))
610
611    def _set_validation_status(self, nsec_set_info):
612        self.validation_status = NSEC_STATUS_VALID
613        if not self.nsec_names_covering_qname:
614            self.validation_status = NSEC_STATUS_INVALID
615            self.errors.append(Errors.SnameNotCoveredNameError(sname=fmt.humanize_name(self.qname)))
616        if not self.nsec_names_covering_wildcard and self.wildcard_name is not None:
617            self.validation_status = NSEC_STATUS_INVALID
618            self.errors.append(Errors.WildcardNotCoveredNSEC(wildcard=fmt.humanize_name(self.wildcard_name)))
619        if self.nsec_names_covering_origin:
620            self.validation_status = NSEC_STATUS_INVALID
621            qname, nsec_names = list(self.nsec_names_covering_origin.items())[0]
622            nsec_rrset = nsec_set_info.rrsets[list(nsec_names)[0]].rrset
623            self.errors.append(Errors.LastNSECNextNotZone(nsec_owner=fmt.humanize_name(nsec_rrset.name), next_name=fmt.humanize_name(nsec_rrset[0].next), zone_name=fmt.humanize_name(self.origin)))
624
625        # if it validation_status, we project out just the pertinent NSEC records
626        # otherwise clone it by projecting them all
627        if self.validation_status == NSEC_STATUS_VALID:
628            covering_names = set()
629            for names in list(self.nsec_names_covering_qname.values()) + list(self.nsec_names_covering_wildcard.values()):
630                covering_names.update(names)
631            self.nsec_set_info = nsec_set_info.project(*list(covering_names))
632        else:
633            self.nsec_set_info = nsec_set_info.project(*list(nsec_set_info.rrsets))
634
635    def __str__(self):
636        return 'NSEC record(s) proving the non-existence (NXDOMAIN) of %s' % (fmt.humanize_name(self.qname))
637
638    def serialize(self, rrset_info_serializer=None, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
639        d = OrderedDict()
640
641        nsec_list = []
642        for nsec_rrset in self.nsec_set_info.rrsets.values():
643            if rrset_info_serializer is not None:
644                nsec_serialized = rrset_info_serializer(nsec_rrset, consolidate_clients=consolidate_clients, show_servers=False, loglevel=loglevel, html_format=html_format)
645                if nsec_serialized:
646                    nsec_list.append(nsec_serialized)
647            elif loglevel <= logging.DEBUG:
648                nsec_list.append(nsec_rrset.serialize(consolidate_clients=consolidate_clients, html_format=html_format))
649
650        erroneous_status = self.validation_status != STATUS_VALID
651
652        show_id = loglevel <= logging.INFO or \
653                (self.warnings and loglevel <= logging.WARNING) or \
654                (self.errors and loglevel <= logging.ERROR) or \
655                (erroneous_status or nsec_list)
656
657        if html_format:
658            formatter = lambda x: escape(x, True)
659        else:
660            formatter = lambda x: x
661
662        if show_id:
663            d['id'] = 'NSEC'
664
665        if loglevel <= logging.DEBUG:
666            d['description'] = formatter(str(self))
667
668        if nsec_list:
669            d['nsec'] = nsec_list
670
671        if loglevel <= logging.DEBUG:
672            if self.nsec_names_covering_qname:
673                qname, nsec_names = list(self.nsec_names_covering_qname.items())[0]
674                nsec_name = list(nsec_names)[0]
675                nsec_rr = self.nsec_set_info.rrsets[nsec_name].rrset[0]
676                d['sname_covering'] = OrderedDict((
677                    ('covered_name', formatter(lb2s(qname.canonicalize().to_text()))),
678                    ('nsec_owner', formatter(lb2s(nsec_name.canonicalize().to_text()))),
679                    ('nsec_next', formatter(lb2s(nsec_rr.next.canonicalize().to_text())))
680                ))
681                if self.nsec_names_covering_wildcard:
682                    wildcard, nsec_names = list(self.nsec_names_covering_wildcard.items())[0]
683                    nsec_name = list(nsec_names)[0]
684                    nsec_rr = self.nsec_set_info.rrsets[nsec_name].rrset[0]
685                    d['wildcard_covering'] = OrderedDict((
686                        ('covered_name', formatter(lb2s(wildcard.canonicalize().to_text()))),
687                        ('nsec_owner', formatter(lb2s(nsec_name.canonicalize().to_text()))),
688                        ('nsec_next', formatter(lb2s(nsec_rr.next.canonicalize().to_text())))
689                    ))
690
691        if loglevel <= logging.INFO or erroneous_status:
692            d['status'] = nsec_status_mapping[self.validation_status]
693
694        if loglevel <= logging.INFO:
695            servers = tuple_to_dict(self.nsec_set_info.servers_clients)
696            if consolidate_clients:
697                servers = list(servers)
698                servers.sort()
699            d['servers'] = servers
700
701            if map_ip_to_ns_name is not None:
702                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
703                ns_names.sort()
704                d['ns_names'] = ns_names
705
706            tags = set()
707            nsids = set()
708            for server,client in self.nsec_set_info.servers_clients:
709                for response in self.nsec_set_info.servers_clients[(server, client)]:
710                    if response is not None:
711                        tags.add(response.effective_query_tag())
712                        nsid = response.nsid_val()
713                        if nsid is not None:
714                            nsids.add(nsid)
715
716            if nsids:
717                d['nsid_values'] = list(nsids)
718                d['nsid_values'].sort()
719
720            d['query_options'] = list(tags)
721            d['query_options'].sort()
722
723        if self.warnings and loglevel <= logging.WARNING:
724            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
725
726        if self.errors and loglevel <= logging.ERROR:
727            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
728
729        return d
730
731class NSECStatusWildcard(NSECStatusNXDOMAIN):
732    def __init__(self, qname, wildcard_name, rdtype, origin, is_zone, nsec_set_info):
733        self.wildcard_name_from_rrsig = wildcard_name
734        super(NSECStatusWildcard, self).__init__(qname, rdtype, origin, is_zone, nsec_set_info)
735
736    def __eq__(self, other):
737        return isinstance(other, self.__class__) and \
738                super(NSECStatusWildcard, self).__eq__(other) and self.wildcard_name_from_rrsig == other.wildcard_name_from_rrsig
739
740    def __hash__(self):
741        return hash(id(self))
742
743    def _next_closest_encloser(self):
744        return dns.name.Name(self.qname.labels[-len(self.wildcard_name):])
745
746    def _set_validation_status(self, nsec_set_info):
747        self.validation_status = NSEC_STATUS_VALID
748        if self.nsec_names_covering_qname:
749            next_closest_encloser = self._next_closest_encloser()
750            nsec_covering_next_closest_encloser = nsec_set_info.nsec_covering_name(next_closest_encloser)
751            if not nsec_covering_next_closest_encloser:
752                self.validation_status = NSEC_STATUS_INVALID
753                self.errors.append(Errors.WildcardExpansionInvalid(sname=fmt.humanize_name(self.qname), wildcard=fmt.humanize_name(self.wildcard_name), next_closest_encloser=fmt.humanize_name(next_closest_encloser)))
754        else:
755            self.validation_status = NSEC_STATUS_INVALID
756            self.errors.append(Errors.SnameNotCoveredWildcardAnswer(sname=fmt.humanize_name(self.qname)))
757
758        if self.nsec_names_covering_wildcard:
759            self.validation_status = NSEC_STATUS_INVALID
760            self.errors.append(Errors.WildcardCoveredAnswerNSEC(wildcard=fmt.humanize_name(self.wildcard_name)))
761
762        if self.nsec_names_covering_origin:
763            self.validation_status = NSEC_STATUS_INVALID
764            qname, nsec_names = list(self.nsec_names_covering_origin.items())[0]
765            nsec_rrset = nsec_set_info.rrsets[list(nsec_names)[0]].rrset
766            self.errors.append(Errors.LastNSECNextNotZone(nsec_owner=fmt.humanize_name(nsec_rrset.name), next_name=fmt.humanize_name(nsec_rrset[0].next), zone_name=fmt.humanize_name(self.origin)))
767
768        # if it validation_status, we project out just the pertinent NSEC records
769        # otherwise clone it by projecting them all
770        if self.validation_status == NSEC_STATUS_VALID:
771            covering_names = set()
772            for names in self.nsec_names_covering_qname.values():
773                covering_names.update(names)
774            self.nsec_set_info = nsec_set_info.project(*list(covering_names))
775        else:
776            self.nsec_set_info = nsec_set_info.project(*list(nsec_set_info.rrsets))
777
778    def serialize(self, rrset_info_serializer=None, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
779        d = super(NSECStatusWildcard, self).serialize(rrset_info_serializer, consolidate_clients=consolidate_clients, loglevel=loglevel, html_format=html_format, map_ip_to_ns_name=map_ip_to_ns_name)
780        try:
781            del d['wildcard']
782        except KeyError:
783            pass
784        return d
785
786class NSECStatusNODATA(NSECStatus):
787    def __init__(self, qname, rdtype, origin, is_zone, nsec_set_info, sname_must_match=False):
788        self.qname = qname
789        self.rdtype = rdtype
790        self.origin = origin
791        self.is_zone = is_zone
792        self.referral = nsec_set_info.referral
793        self.warnings = []
794        self.errors = []
795
796        self.wildcard_name = None
797
798        try:
799            self.nsec_for_qname = nsec_set_info.rrsets[self.qname]
800            self.has_rdtype = nsec_set_info.rdtype_exists_in_bitmap(self.qname, self.rdtype)
801            self.has_ns = nsec_set_info.rdtype_exists_in_bitmap(self.qname, dns.rdatatype.NS)
802            self.has_ds = nsec_set_info.rdtype_exists_in_bitmap(self.qname, dns.rdatatype.DS)
803            self.has_soa = nsec_set_info.rdtype_exists_in_bitmap(self.qname, dns.rdatatype.SOA)
804        except KeyError:
805            self.nsec_for_qname = None
806            self.has_rdtype = False
807            self.has_ns = False
808            self.has_ds = False
809            self.has_soa = False
810
811            if not sname_must_match:
812                # If no NSEC exists for the name itself, then look for an NSEC with
813                # an (empty non-terminal) ancestor
814                for nsec_name in nsec_set_info.rrsets:
815                    next_name = nsec_set_info.rrsets[nsec_name].rrset[0].next
816                    if next_name.is_subdomain(self.qname) and next_name != self.qname:
817                        self.nsec_for_qname = nsec_set_info.rrsets[nsec_name]
818                        break
819
820        self.nsec_names_covering_qname = {}
821        covering_names = nsec_set_info.nsec_covering_name(self.qname)
822        if covering_names:
823            self.nsec_names_covering_qname[self.qname] = covering_names
824
825            covering_name = list(covering_names)[0]
826            self.wildcard_name = self._get_wildcard(qname, nsec_set_info.rrsets[covering_name].rrset)
827
828        self.nsec_for_wildcard_name = None
829        self.wildcard_has_rdtype = None
830        if self.wildcard_name is not None:
831            try:
832                self.nsec_for_wildcard_name = nsec_set_info.rrsets[self.wildcard_name]
833                self.wildcard_has_rdtype = nsec_set_info.rdtype_exists_in_bitmap(self.wildcard_name, self.rdtype)
834            except KeyError:
835                pass
836
837        # check for covering of the origin
838        self.nsec_names_covering_origin = {}
839        covering_names = nsec_set_info.nsec_covering_name(self.origin)
840        if covering_names:
841            self.nsec_names_covering_origin[self.origin] = covering_names
842
843        self.opt_out = None
844
845        self._set_validation_status(nsec_set_info)
846
847    def __str__(self):
848        return 'NSEC record(s) proving non-existence (NODATA) of %s/%s' % (fmt.humanize_name(self.qname), dns.rdatatype.to_text(self.rdtype))
849
850    def __eq__(self, other):
851        return isinstance(other, self.__class__) and \
852                self.qname == other.qname and self.rdtype == other.rdtype and self.origin == other.origin and self.referral == other.referral and self.nsec_set_info == other.nsec_set_info
853
854    def __hash__(self):
855        return hash(id(self))
856
857    def _set_validation_status(self, nsec_set_info):
858        self.validation_status = NSEC_STATUS_VALID
859        if self.nsec_for_qname is not None:
860            # RFC 4034 5.2, 6840 4.4
861            if self.rdtype == dns.rdatatype.DS or self.referral:
862                if self.is_zone and not self.has_ns:
863                    self.errors.append(Errors.ReferralWithoutNSBitNSEC(sname=fmt.humanize_name(self.qname)))
864                    self.validation_status = NSEC_STATUS_INVALID
865                if self.has_ds:
866                    self.errors.append(Errors.ReferralWithDSBitNSEC(sname=fmt.humanize_name(self.qname)))
867                    self.validation_status = NSEC_STATUS_INVALID
868                if self.has_soa:
869                    self.errors.append(Errors.ReferralWithSOABitNSEC(sname=fmt.humanize_name(self.qname)))
870                    self.validation_status = NSEC_STATUS_INVALID
871            else:
872                if self.has_rdtype:
873                    self.errors.append(Errors.StypeInBitmapNODATANSEC(sname=fmt.humanize_name(self.qname), stype=dns.rdatatype.to_text(self.rdtype)))
874                    self.validation_status = NSEC_STATUS_INVALID
875            if self.nsec_names_covering_qname:
876                self.errors.append(Errors.SnameCoveredNODATANSEC(sname=fmt.humanize_name(self.qname)))
877                self.validation_status = NSEC_STATUS_INVALID
878        elif self.nsec_for_wildcard_name: # implies wildcard_name, which implies nsec_names_covering_qname
879            if self.wildcard_has_rdtype:
880                self.validation_status = NSEC_STATUS_INVALID
881                self.errors.append(Errors.StypeInBitmapNODATANSEC(sname=fmt.humanize_name(self.wildcard_name), stype=dns.rdatatype.to_text(self.rdtype)))
882            if self.nsec_names_covering_origin:
883                self.validation_status = NSEC_STATUS_INVALID
884                qname, nsec_names = list(self.nsec_names_covering_origin.items())[0]
885                nsec_rrset = nsec_set_info.rrsets[list(nsec_names)[0]].rrset
886                self.errors.append(Errors.LastNSECNextNotZone(nsec_owner=fmt.humanize_name(nsec_rrset.name), next_name=fmt.humanize_name(nsec_rrset[0].next), zone_name=fmt.humanize_name(self.origin)))
887        else:
888            self.validation_status = NSEC_STATUS_INVALID
889            self.errors.append(Errors.NoNSECMatchingSnameNODATA(sname=fmt.humanize_name(self.qname)))
890
891        # if it validation_status, we project out just the pertinent NSEC records
892        # otherwise clone it by projecting them all
893        if self.validation_status == NSEC_STATUS_VALID:
894            covering_names = set()
895            if self.nsec_for_qname is not None:
896                covering_names.add(self.nsec_for_qname.rrset.name)
897            if self.nsec_names_covering_qname:
898                for names in self.nsec_names_covering_qname.values():
899                    covering_names.update(names)
900            if self.nsec_for_wildcard_name is not None:
901                covering_names.add(self.wildcard_name)
902            self.nsec_set_info = nsec_set_info.project(*list(covering_names))
903        else:
904            self.nsec_set_info = nsec_set_info.project(*list(nsec_set_info.rrsets))
905
906    def serialize(self, rrset_info_serializer=None, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
907        d = OrderedDict()
908
909        nsec_list = []
910        for nsec_rrset in self.nsec_set_info.rrsets.values():
911            if rrset_info_serializer is not None:
912                nsec_serialized = rrset_info_serializer(nsec_rrset, consolidate_clients=consolidate_clients, show_servers=False, loglevel=loglevel, html_format=html_format)
913                if nsec_serialized:
914                    nsec_list.append(nsec_serialized)
915            elif loglevel <= logging.DEBUG:
916                nsec_list.append(nsec_rrset.serialize(consolidate_clients=consolidate_clients, html_format=html_format))
917
918        erroneous_status = self.validation_status != STATUS_VALID
919
920        show_id = loglevel <= logging.INFO or \
921                (self.warnings and loglevel <= logging.WARNING) or \
922                (self.errors and loglevel <= logging.ERROR) or \
923                (erroneous_status or nsec_list)
924
925        if html_format:
926            formatter = lambda x: escape(x, True)
927        else:
928            formatter = lambda x: x
929
930        if show_id:
931            d['id'] = 'NSEC'
932
933        if loglevel <= logging.DEBUG:
934            d['description'] = formatter(str(self))
935
936        if nsec_list:
937            d['nsec'] = nsec_list
938
939        if loglevel <= logging.DEBUG:
940            if self.nsec_for_qname is not None:
941                d['sname_nsec_match'] = formatter(lb2s(self.nsec_for_qname.rrset.name.canonicalize().to_text()))
942
943            if self.nsec_names_covering_qname:
944                qname, nsec_names = list(self.nsec_names_covering_qname.items())[0]
945                nsec_name = list(nsec_names)[0]
946                nsec_rr = self.nsec_set_info.rrsets[nsec_name].rrset[0]
947                d['sname_covering'] = OrderedDict((
948                    ('covered_name', formatter(lb2s(qname.canonicalize().to_text()))),
949                    ('nsec_owner', formatter(lb2s(nsec_name.canonicalize().to_text()))),
950                    ('nsec_next', formatter(lb2s(nsec_rr.next.canonicalize().to_text())))
951                ))
952
953                if self.nsec_for_wildcard_name is not None:
954                    d['wildcard_nsec_match'] = formatter(lb2s(self.wildcard_name.canonicalize().to_text()))
955
956        if loglevel <= logging.INFO or erroneous_status:
957            d['status'] = nsec_status_mapping[self.validation_status]
958
959        if loglevel <= logging.INFO:
960            servers = tuple_to_dict(self.nsec_set_info.servers_clients)
961            if consolidate_clients:
962                servers = list(servers)
963                servers.sort()
964            d['servers'] = servers
965
966            if map_ip_to_ns_name is not None:
967                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
968                ns_names.sort()
969                d['ns_names'] = ns_names
970
971            tags = set()
972            nsids = set()
973            for server,client in self.nsec_set_info.servers_clients:
974                for response in self.nsec_set_info.servers_clients[(server, client)]:
975                    if response is not None:
976                        tags.add(response.effective_query_tag())
977                        nsid = response.nsid_val()
978                        if nsid is not None:
979                            nsids.add(nsid)
980
981            if nsids:
982                d['nsid_values'] = list(nsids)
983                d['nsid_values'].sort()
984
985            d['query_options'] = list(tags)
986            d['query_options'].sort()
987
988        if self.warnings and loglevel <= logging.WARNING:
989            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
990
991        if self.errors and loglevel <= logging.ERROR:
992            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
993
994        return d
995
996class NSEC3Status(object):
997    def __repr__(self):
998        return '<%s: "%s">' % (self.__class__.__name__, self.qname)
999
1000    def _get_next_closest_encloser(self, encloser):
1001        return dns.name.Name(self.qname.labels[-(len(encloser)+1):])
1002
1003    def get_next_closest_encloser(self):
1004        if self.closest_encloser:
1005            encloser_name, nsec_names = list(self.closest_encloser.items())[0]
1006            return self._get_next_closest_encloser(encloser_name)
1007        return None
1008
1009    def _get_wildcard(self, encloser):
1010        return dns.name.from_text('*', encloser)
1011
1012    def get_wildcard(self):
1013        if self.closest_encloser:
1014            encloser_name, nsec_names = list(self.closest_encloser.items())[0]
1015            return self._get_wildcard(encloser_name)
1016        return None
1017
1018class NSEC3StatusNXDOMAIN(NSEC3Status):
1019    def __init__(self, qname, rdtype, origin, is_zone, nsec_set_info):
1020        self.qname = qname
1021        self.origin = origin
1022        self.is_zone = is_zone
1023        self.warnings = []
1024        self.errors = []
1025
1026        self.name_digest_map = {}
1027
1028        self._set_closest_encloser(nsec_set_info)
1029
1030        self.nsec_names_covering_qname = {}
1031        self.nsec_names_covering_wildcard = {}
1032        self.opt_out = None
1033
1034        for (salt, alg, iterations), nsec3_names in nsec_set_info.nsec3_params.items():
1035            digest_name = nsec_set_info.get_digest_name_for_nsec3(self.qname, self.origin, salt, alg, iterations)
1036            if self.qname not in self.name_digest_map:
1037                self.name_digest_map[self.qname] = {}
1038            self.name_digest_map[self.qname][(salt, alg, iterations)] = digest_name
1039
1040        for encloser in self.closest_encloser:
1041            next_closest_encloser = self._get_next_closest_encloser(encloser)
1042            for salt, alg, iterations in nsec_set_info.nsec3_params:
1043                try:
1044                    digest_name = self.name_digest_map[next_closest_encloser][(salt, alg, iterations)]
1045                except KeyError:
1046                    digest_name = nsec_set_info.get_digest_name_for_nsec3(next_closest_encloser, self.origin, salt, alg, iterations)
1047
1048                if digest_name is not None:
1049                    covering_names = nsec_set_info.nsec3_covering_name(digest_name, salt, alg, iterations)
1050                    if covering_names:
1051                        self.nsec_names_covering_qname[digest_name] = covering_names
1052                        self.opt_out = False
1053                        for nsec_name in covering_names:
1054                            if nsec_set_info.rrsets[nsec_name].rrset[0].flags & 0x01:
1055                                self.opt_out = True
1056
1057                if next_closest_encloser not in self.name_digest_map:
1058                    self.name_digest_map[next_closest_encloser] = {}
1059                self.name_digest_map[next_closest_encloser][(salt, alg, iterations)] = digest_name
1060
1061                wildcard_name = self._get_wildcard(encloser)
1062                digest_name = nsec_set_info.get_digest_name_for_nsec3(wildcard_name, self.origin, salt, alg, iterations)
1063
1064                if digest_name is not None:
1065                    covering_names = nsec_set_info.nsec3_covering_name(digest_name, salt, alg, iterations)
1066                    if covering_names:
1067                        self.nsec_names_covering_wildcard[digest_name] = covering_names
1068
1069                if wildcard_name not in self.name_digest_map:
1070                    self.name_digest_map[wildcard_name] = {}
1071                self.name_digest_map[wildcard_name][(salt, alg, iterations)] = digest_name
1072
1073        self._set_validation_status(nsec_set_info)
1074
1075    def __str__(self):
1076        return 'NSEC3 record(s) proving the non-existence (NXDOMAIN) of %s' % (fmt.humanize_name(self.qname))
1077
1078    def __eq__(self, other):
1079        return isinstance(other, self.__class__) and \
1080                self.qname == other.qname and self.origin == other.origin and self.nsec_set_info == other.nsec_set_info
1081
1082    def __hash__(self):
1083        return hash(id(self))
1084
1085    def _set_closest_encloser(self, nsec_set_info):
1086        self.closest_encloser = nsec_set_info.get_closest_encloser(self.qname, self.origin)
1087
1088    def _set_validation_status(self, nsec_set_info):
1089        self.validation_status = NSEC_STATUS_VALID
1090        valid_algs, invalid_algs = nsec_set_info.get_algorithm_support()
1091        if invalid_algs:
1092            invalid_alg_err = Errors.UnsupportedNSEC3Algorithm(algorithm=list(invalid_algs)[0])
1093        else:
1094            invalid_alg_err = None
1095        if not self.closest_encloser:
1096            self.validation_status = NSEC_STATUS_INVALID
1097            if valid_algs:
1098                self.errors.append(Errors.NoClosestEncloserNameError(sname=fmt.humanize_name(self.qname)))
1099            if invalid_algs:
1100                self.errors.append(invalid_alg_err)
1101        else:
1102            if not self.nsec_names_covering_qname:
1103                self.validation_status = NSEC_STATUS_INVALID
1104                if valid_algs:
1105                    next_closest_encloser = self.get_next_closest_encloser()
1106                    self.errors.append(Errors.NextClosestEncloserNotCoveredNameError(next_closest_encloser=fmt.humanize_name(next_closest_encloser)))
1107                if invalid_algs:
1108                    self.errors.append(invalid_alg_err)
1109            if not self.nsec_names_covering_wildcard:
1110                self.validation_status = NSEC_STATUS_INVALID
1111                if valid_algs:
1112                    wildcard_name = self.get_wildcard()
1113                    self.errors.append(Errors.WildcardNotCoveredNSEC3(wildcard=fmt.humanize_name(wildcard_name)))
1114                if invalid_algs and invalid_alg_err not in self.errors:
1115                    self.errors.append(invalid_alg_err)
1116
1117        # if it validation_status, we project out just the pertinent NSEC records
1118        # otherwise clone it by projecting them all
1119        if self.validation_status == NSEC_STATUS_VALID:
1120            covering_names = set()
1121            for names in list(self.closest_encloser.values()) + list(self.nsec_names_covering_qname.values()) + list(self.nsec_names_covering_wildcard.values()):
1122                covering_names.update(names)
1123            self.nsec_set_info = nsec_set_info.project(*list(covering_names))
1124        else:
1125            self.nsec_set_info = nsec_set_info.project(*list(nsec_set_info.rrsets))
1126
1127        # Report errors with NSEC3 owner names
1128        for name in self.nsec_set_info.invalid_nsec3_owner:
1129            self.errors.append(Errors.InvalidNSEC3OwnerName(name=fmt.format_nsec3_name(name)))
1130        for name in self.nsec_set_info.invalid_nsec3_hash:
1131            self.errors.append(Errors.InvalidNSEC3Hash(name=fmt.format_nsec3_name(name), nsec3_hash=lb2s(base32.b32encode(self.nsec_set_info.rrsets[name].rrset[0].next))))
1132
1133    def serialize(self, rrset_info_serializer=None, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
1134        d = OrderedDict()
1135
1136        nsec3_list = []
1137        for nsec_rrset in self.nsec_set_info.rrsets.values():
1138            if rrset_info_serializer is not None:
1139                nsec_serialized = rrset_info_serializer(nsec_rrset, consolidate_clients=consolidate_clients, show_servers=False, loglevel=loglevel, html_format=html_format)
1140                if nsec_serialized:
1141                    nsec3_list.append(nsec_serialized)
1142            elif loglevel <= logging.DEBUG:
1143                nsec3_list.append(nsec_rrset.serialize(consolidate_clients=consolidate_clients, html_format=html_format))
1144
1145        erroneous_status = self.validation_status != STATUS_VALID
1146
1147        show_id = loglevel <= logging.INFO or \
1148                (self.warnings and loglevel <= logging.WARNING) or \
1149                (self.errors and loglevel <= logging.ERROR) or \
1150                (erroneous_status or nsec3_list)
1151
1152        if html_format:
1153            formatter = lambda x: escape(x, True)
1154        else:
1155            formatter = lambda x: x
1156
1157        if show_id:
1158            d['id'] = 'NSEC3'
1159
1160        if loglevel <= logging.DEBUG:
1161            d['description'] = formatter(str(self))
1162
1163        if nsec3_list:
1164            d['nsec3'] = nsec3_list
1165
1166        if loglevel <= logging.DEBUG:
1167            if self.opt_out is not None:
1168                d['opt_out'] = self.opt_out
1169
1170            if self.closest_encloser:
1171                encloser_name, nsec_names = list(self.closest_encloser.items())[0]
1172                nsec_name = list(nsec_names)[0]
1173                d['closest_encloser'] = formatter(lb2s(encloser_name.canonicalize().to_text()))
1174                # could be inferred from wildcard
1175                if nsec_name is not None:
1176                    d['closest_encloser_hash'] = formatter(fmt.format_nsec3_name(nsec_name))
1177
1178                next_closest_encloser = self._get_next_closest_encloser(encloser_name)
1179                d['next_closest_encloser'] = formatter(lb2s(next_closest_encloser.canonicalize().to_text()))
1180                digest_name = list(self.name_digest_map[next_closest_encloser].items())[0][1]
1181                if digest_name is not None:
1182                    d['next_closest_encloser_hash'] = formatter(fmt.format_nsec3_name(digest_name))
1183                else:
1184                    d['next_closest_encloser_hash'] = None
1185
1186                if self.nsec_names_covering_qname:
1187                    qname, nsec_names = list(self.nsec_names_covering_qname.items())[0]
1188                    nsec_name = list(nsec_names)[0]
1189                    next_name = self.nsec_set_info.name_for_nsec3_next(nsec_name)
1190                    d['next_closest_encloser_covering'] = OrderedDict((
1191                        ('covered_name', formatter(fmt.format_nsec3_name(qname))),
1192                        ('nsec_owner', formatter(fmt.format_nsec3_name(nsec_name))),
1193                        ('nsec_next', formatter(fmt.format_nsec3_name(next_name))),
1194                    ))
1195
1196                wildcard_name = self._get_wildcard(encloser_name)
1197                wildcard_digest = list(self.name_digest_map[wildcard_name].items())[0][1]
1198                d['wildcard'] = formatter(lb2s(wildcard_name.canonicalize().to_text()))
1199                if wildcard_digest is not None:
1200                    d['wildcard_hash'] = formatter(fmt.format_nsec3_name(wildcard_digest))
1201                else:
1202                    d['wildcard_hash'] = None
1203                if self.nsec_names_covering_wildcard:
1204                    wildcard, nsec_names = list(self.nsec_names_covering_wildcard.items())[0]
1205                    nsec_name = list(nsec_names)[0]
1206                    next_name = self.nsec_set_info.name_for_nsec3_next(nsec_name)
1207                    d['wildcard_covering'] = OrderedDict((
1208                        ('covered_name', formatter(fmt.format_nsec3_name(wildcard))),
1209                        ('nsec3_owner', formatter(fmt.format_nsec3_name(nsec_name))),
1210                        ('nsec3_next', formatter(fmt.format_nsec3_name(next_name))),
1211                    ))
1212
1213            else:
1214                digest_name = list(self.name_digest_map[self.qname].items())[0][1]
1215                if digest_name is not None:
1216                    d['sname_hash'] = formatter(fmt.format_nsec3_name(digest_name))
1217                else:
1218                    d['sname_hash'] = None
1219
1220        if loglevel <= logging.INFO or erroneous_status:
1221            d['status'] = nsec_status_mapping[self.validation_status]
1222
1223        if loglevel <= logging.INFO:
1224            servers = tuple_to_dict(self.nsec_set_info.servers_clients)
1225            if consolidate_clients:
1226                servers = list(servers)
1227                servers.sort()
1228            d['servers'] = servers
1229
1230            if map_ip_to_ns_name is not None:
1231                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
1232                ns_names.sort()
1233                d['ns_names'] = ns_names
1234
1235            tags = set()
1236            nsids = set()
1237            for server,client in self.nsec_set_info.servers_clients:
1238                for response in self.nsec_set_info.servers_clients[(server, client)]:
1239                    if response is not None:
1240                        tags.add(response.effective_query_tag())
1241                        nsid = response.nsid_val()
1242                        if nsid is not None:
1243                            nsids.add(nsid)
1244
1245            if nsids:
1246                d['nsid_values'] = list(nsids)
1247                d['nsid_values'].sort()
1248
1249            d['query_options'] = list(tags)
1250            d['query_options'].sort()
1251
1252        if self.warnings and loglevel <= logging.WARNING:
1253            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
1254
1255        if self.errors and loglevel <= logging.ERROR:
1256            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
1257
1258        return d
1259
1260class NSEC3StatusWildcard(NSEC3StatusNXDOMAIN):
1261    def __init__(self, qname, wildcard_name, rdtype, origin, is_zone, nsec_set_info):
1262        self.wildcard_name = wildcard_name
1263        super(NSEC3StatusWildcard, self).__init__(qname, rdtype, origin, is_zone, nsec_set_info)
1264
1265    def _set_closest_encloser(self, nsec_set_info):
1266        super(NSEC3StatusWildcard, self)._set_closest_encloser(nsec_set_info)
1267
1268        if not self.closest_encloser:
1269            self.closest_encloser = { self.wildcard_name.parent(): set([None]) }
1270            # fill in a dummy value for wildcard_name_digest_map
1271            self.name_digest_map[self.wildcard_name] = { None: self.wildcard_name }
1272
1273    def __eq__(self, other):
1274        return isinstance(other, self.__class__) and \
1275                super(NSEC3StatusWildcard, self).__eq__(other) and self.wildcard_name == other.wildcard_name
1276
1277    def __hash__(self):
1278        return hash(id(self))
1279
1280    def _set_validation_status(self, nsec_set_info):
1281        self.validation_status = NSEC_STATUS_VALID
1282        if not self.nsec_names_covering_qname:
1283            self.validation_status = NSEC_STATUS_INVALID
1284            valid_algs, invalid_algs = nsec_set_info.get_algorithm_support()
1285            if invalid_algs:
1286                invalid_alg_err = Errors.UnsupportedNSEC3Algorithm(algorithm=list(invalid_algs)[0])
1287            else:
1288                invalid_alg_err = None
1289            if valid_algs:
1290                next_closest_encloser = self.get_next_closest_encloser()
1291                self.errors.append(Errors.NextClosestEncloserNotCoveredWildcardAnswer(next_closest_encloser=fmt.humanize_name(next_closest_encloser)))
1292            if invalid_algs:
1293                self.errors.append(invalid_alg_err)
1294
1295        if self.nsec_names_covering_wildcard:
1296            self.validation_status = NSEC_STATUS_INVALID
1297            self.errors.append(Errors.WildcardCoveredAnswerNSEC3(wildcard=fmt.humanize_name(self.wildcard_name)))
1298
1299        # if it validation_status, we project out just the pertinent NSEC records
1300        # otherwise clone it by projecting them all
1301        if self.validation_status == NSEC_STATUS_VALID:
1302            covering_names = set()
1303            for names in list(self.closest_encloser.values()) + list(self.nsec_names_covering_qname.values()):
1304                covering_names.update(names)
1305            self.nsec_set_info = nsec_set_info.project(*[x for x in covering_names if x is not None])
1306        else:
1307            self.nsec_set_info = nsec_set_info.project(*list(nsec_set_info.rrsets))
1308
1309        # Report errors with NSEC3 owner names
1310        for name in self.nsec_set_info.invalid_nsec3_owner:
1311            self.errors.append(Errors.InvalidNSEC3OwnerName(name=fmt.format_nsec3_name(name)))
1312        for name in self.nsec_set_info.invalid_nsec3_hash:
1313            self.errors.append(Errors.InvalidNSEC3Hash(name=fmt.format_nsec3_name(name), nsec3_hash=lb2s(base32.b32encode(self.nsec_set_info.rrsets[name].rrset[0].next))))
1314
1315    def serialize(self, rrset_info_serializer=None, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
1316        d = super(NSEC3StatusWildcard, self).serialize(rrset_info_serializer, consolidate_clients=consolidate_clients, loglevel=loglevel, html_format=html_format, map_ip_to_ns_name=map_ip_to_ns_name)
1317        try:
1318            del d['wildcard']
1319        except KeyError:
1320            pass
1321        try:
1322            del d['wildcard_digest']
1323        except KeyError:
1324            pass
1325        if loglevel <= logging.DEBUG:
1326            if [x for x in list(self.closest_encloser.values())[0] if x is not None]:
1327                d['superfluous_closest_encloser'] = True
1328        return d
1329
1330class NSEC3StatusNODATA(NSEC3Status):
1331    def __init__(self, qname, rdtype, origin, is_zone, nsec_set_info):
1332        self.qname = qname
1333        self.rdtype = rdtype
1334        self.origin = origin
1335        self.is_zone = is_zone
1336        self.referral = nsec_set_info.referral
1337        self.wildcard_name = None
1338        self.warnings = []
1339        self.errors = []
1340
1341        self.name_digest_map = {}
1342
1343        self.closest_encloser = nsec_set_info.get_closest_encloser(qname, origin)
1344
1345        self.nsec_names_covering_qname = {}
1346        self.nsec_names_covering_wildcard = {}
1347        self.nsec_for_qname = set()
1348        self.nsec_for_wildcard_name = set()
1349        self.has_rdtype = False
1350        self.has_cname = False
1351        self.has_ns = False
1352        self.has_ds = False
1353        self.has_soa = False
1354        self.opt_out = None
1355        self.wildcard_has_rdtype = False
1356        self.wildcard_has_cname = False
1357
1358        for (salt, alg, iterations), nsec3_names in nsec_set_info.nsec3_params.items():
1359            digest_name = nsec_set_info.get_digest_name_for_nsec3(self.qname, self.origin, salt, alg, iterations)
1360            if self.qname not in self.name_digest_map:
1361                self.name_digest_map[self.qname] = {}
1362            self.name_digest_map[self.qname][(salt, alg, iterations)] = digest_name
1363
1364            for encloser in self.closest_encloser:
1365                wildcard_name = self._get_wildcard(encloser)
1366                digest_name = nsec_set_info.get_digest_name_for_nsec3(wildcard_name, self.origin, salt, alg, iterations)
1367                if digest_name in nsec3_names:
1368                    self.nsec_for_wildcard_name.add(digest_name)
1369                    if nsec_set_info.rdtype_exists_in_bitmap(digest_name, rdtype): self.wildcard_has_rdtype = True
1370                    if nsec_set_info.rdtype_exists_in_bitmap(digest_name, dns.rdatatype.CNAME): self.wildcard_has_cname = True
1371
1372                if wildcard_name not in self.name_digest_map:
1373                    self.name_digest_map[wildcard_name] = {}
1374                self.name_digest_map[wildcard_name][(salt, alg, iterations)] = digest_name
1375
1376        for (salt, alg, iterations), nsec3_names in nsec_set_info.nsec3_params.items():
1377            digest_name = self.name_digest_map[self.qname][(salt, alg, iterations)]
1378            if digest_name in nsec3_names:
1379                self.nsec_for_qname.add(digest_name)
1380                if nsec_set_info.rdtype_exists_in_bitmap(digest_name, rdtype): self.has_rdtype = True
1381                if nsec_set_info.rdtype_exists_in_bitmap(digest_name, dns.rdatatype.CNAME): self.has_cname = True
1382                if nsec_set_info.rdtype_exists_in_bitmap(digest_name, dns.rdatatype.NS): self.has_ns = True
1383                if nsec_set_info.rdtype_exists_in_bitmap(digest_name, dns.rdatatype.DS): self.has_ds = True
1384                if nsec_set_info.rdtype_exists_in_bitmap(digest_name, dns.rdatatype.SOA): self.has_soa = True
1385
1386            else:
1387                for encloser in self.closest_encloser:
1388                    next_closest_encloser = self._get_next_closest_encloser(encloser)
1389                    digest_name = nsec_set_info.get_digest_name_for_nsec3(next_closest_encloser, self.origin, salt, alg, iterations)
1390                    if next_closest_encloser not in self.name_digest_map:
1391                        self.name_digest_map[next_closest_encloser] = {}
1392                    self.name_digest_map[next_closest_encloser][(salt, alg, iterations)] = digest_name
1393
1394                    if digest_name is not None:
1395                        covering_names = nsec_set_info.nsec3_covering_name(digest_name, salt, alg, iterations)
1396                        if covering_names:
1397                            self.nsec_names_covering_qname[digest_name] = covering_names
1398                            self.opt_out = False
1399                            for nsec_name in covering_names:
1400                                if nsec_set_info.rrsets[nsec_name].rrset[0].flags & 0x01:
1401                                    self.opt_out = True
1402
1403        self._set_validation_status(nsec_set_info)
1404
1405    def __str__(self):
1406        return 'NSEC3 record(s) proving non-existence (NODATA) of %s/%s' % (fmt.humanize_name(self.qname), dns.rdatatype.to_text(self.rdtype))
1407
1408    def __eq__(self, other):
1409        return isinstance(other, self.__class__) and \
1410                self.qname == other.qname and self.rdtype == other.rdtype and self.origin == other.origin and self.referral == other.referral and self.nsec_set_info == other.nsec_set_info
1411
1412    def __hash__(self):
1413        return hash(id(self))
1414
1415    def _set_validation_status(self, nsec_set_info):
1416        self.validation_status = NSEC_STATUS_VALID
1417        valid_algs, invalid_algs = nsec_set_info.get_algorithm_support()
1418        if invalid_algs:
1419            invalid_alg_err = Errors.UnsupportedNSEC3Algorithm(algorithm=list(invalid_algs)[0])
1420        else:
1421            invalid_alg_err = None
1422        if self.nsec_for_qname:
1423            # RFC 4035 5.2, 6840 4.4
1424            if self.rdtype == dns.rdatatype.DS or self.referral:
1425                if self.is_zone and not self.has_ns:
1426                    self.errors.append(Errors.ReferralWithoutNSBitNSEC3(sname=fmt.humanize_name(self.qname)))
1427                    self.validation_status = NSEC_STATUS_INVALID
1428                if self.has_ds:
1429                    self.errors.append(Errors.ReferralWithDSBitNSEC3(sname=fmt.humanize_name(self.qname)))
1430                    self.validation_status = NSEC_STATUS_INVALID
1431                if self.has_soa:
1432                    self.errors.append(Errors.ReferralWithSOABitNSEC3(sname=fmt.humanize_name(self.qname)))
1433                    self.validation_status = NSEC_STATUS_INVALID
1434            # RFC 5155, section 8.5, 8.6
1435            else:
1436                if self.has_rdtype:
1437                    self.errors.append(Errors.StypeInBitmapNODATANSEC3(sname=fmt.humanize_name(self.qname), stype=dns.rdatatype.to_text(self.rdtype)))
1438                    self.validation_status = NSEC_STATUS_INVALID
1439                if self.has_cname:
1440                    self.errors.append(Errors.StypeInBitmapNODATANSEC3(sname=fmt.humanize_name(self.qname), stype=dns.rdatatype.to_text(dns.rdatatype.CNAME)))
1441                    self.validation_status = NSEC_STATUS_INVALID
1442        elif self.nsec_for_wildcard_name:
1443            if not self.nsec_names_covering_qname:
1444                self.validation_status = NSEC_STATUS_INVALID
1445                if valid_algs:
1446                    self.errors.append(Errors.NextClosestEncloserNotCoveredWildcardNODATA(next_closest_encloser=fmt.humanize_name(next_closest_encloser)))
1447                if invalid_algs:
1448                    self.errors.append(invalid_alg_err)
1449            if self.wildcard_has_rdtype:
1450                self.validation_status = NSEC_STATUS_INVALID
1451                self.errors.append(Errors.StypeInBitmapWildcardNODATANSEC3(sname=fmt.humanize_name(self.get_wildcard()), stype=dns.rdatatype.to_text(self.rdtype)))
1452        elif self.nsec_names_covering_qname:
1453            if not self.opt_out:
1454                self.validation_status = NSEC_STATUS_INVALID
1455                if valid_algs:
1456                    if self.rdtype == dns.rdatatype.DS:
1457                        cls = Errors.OptOutFlagNotSetNODATADS
1458                    else:
1459                        cls = Errors.OptOutFlagNotSetNODATA
1460                    next_closest_encloser = self.get_next_closest_encloser()
1461                    self.errors.append(cls(next_closest_encloser=fmt.humanize_name(next_closest_encloser)))
1462                if invalid_algs:
1463                    self.errors.append(invalid_alg_err)
1464        else:
1465            self.validation_status = NSEC_STATUS_INVALID
1466            if valid_algs:
1467                if self.rdtype == dns.rdatatype.DS:
1468                    cls = Errors.NoNSEC3MatchingSnameDSNODATA
1469                else:
1470                    cls = Errors.NoNSEC3MatchingSnameNODATA
1471                self.errors.append(cls(sname=fmt.humanize_name(self.qname)))
1472            if invalid_algs:
1473                self.errors.append(invalid_alg_err)
1474
1475        # if it validation_status, we project out just the pertinent NSEC records
1476        # otherwise clone it by projecting them all
1477        if self.validation_status == NSEC_STATUS_VALID:
1478            covering_names = set()
1479            for names in self.closest_encloser.values():
1480                covering_names.update(names)
1481            if self.nsec_for_qname:
1482                covering_names.update(self.nsec_for_qname)
1483            else:
1484                for names in self.nsec_names_covering_qname.values():
1485                    covering_names.update(names)
1486            if self.nsec_for_wildcard_name is not None:
1487                covering_names.update(self.nsec_for_wildcard_name)
1488            self.nsec_set_info = nsec_set_info.project(*list(covering_names))
1489        else:
1490            self.nsec_set_info = nsec_set_info.project(*list(nsec_set_info.rrsets))
1491
1492        # Report errors with NSEC3 owner names
1493        for name in self.nsec_set_info.invalid_nsec3_owner:
1494            self.errors.append(Errors.InvalidNSEC3OwnerName(name=fmt.format_nsec3_name(name)))
1495        for name in self.nsec_set_info.invalid_nsec3_hash:
1496            self.errors.append(Errors.InvalidNSEC3Hash(name=fmt.format_nsec3_name(name), nsec3_hash=lb2s(base32.b32encode(self.nsec_set_info.rrsets[name].rrset[0].next))))
1497
1498    def serialize(self, rrset_info_serializer=None, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
1499        d = OrderedDict()
1500
1501        nsec3_list = []
1502        for nsec_rrset in self.nsec_set_info.rrsets.values():
1503            if rrset_info_serializer is not None:
1504                nsec_serialized = rrset_info_serializer(nsec_rrset, consolidate_clients=consolidate_clients, show_servers=False, loglevel=loglevel, html_format=html_format)
1505                if nsec_serialized:
1506                    nsec3_list.append(nsec_serialized)
1507            elif loglevel <= logging.DEBUG:
1508                nsec3_list.append(nsec_rrset.serialize(consolidate_clients=consolidate_clients, html_format=html_format))
1509
1510        erroneous_status = self.validation_status != STATUS_VALID
1511
1512        show_id = loglevel <= logging.INFO or \
1513                (self.warnings and loglevel <= logging.WARNING) or \
1514                (self.errors and loglevel <= logging.ERROR) or \
1515                (erroneous_status or nsec3_list)
1516
1517        if html_format:
1518            formatter = lambda x: escape(x, True)
1519        else:
1520            formatter = lambda x: x
1521
1522        if show_id:
1523            d['id'] = 'NSEC3'
1524
1525        if loglevel <= logging.DEBUG:
1526            d['description'] = formatter(str(self))
1527
1528        if nsec3_list:
1529            d['nsec3'] = nsec3_list
1530
1531        if loglevel <= logging.DEBUG:
1532            if self.opt_out is not None:
1533                d['opt_out'] = self.opt_out
1534
1535            if self.nsec_for_qname:
1536                digest_name = list(self.name_digest_map[self.qname].items())[0][1]
1537                if digest_name is not None:
1538                    d['sname_hash'] = formatter(fmt.format_nsec3_name(digest_name))
1539                else:
1540                    d['sname_hash'] = None
1541                d['sname_nsec_match'] = formatter(fmt.format_nsec3_name(list(self.nsec_for_qname)[0]))
1542
1543            if self.closest_encloser:
1544                encloser_name, nsec_names = list(self.closest_encloser.items())[0]
1545                nsec_name = list(nsec_names)[0]
1546                d['closest_encloser'] = formatter(lb2s(encloser_name.canonicalize().to_text()))
1547                d['closest_encloser_digest'] = formatter(fmt.format_nsec3_name(nsec_name))
1548
1549                next_closest_encloser = self._get_next_closest_encloser(encloser_name)
1550                d['next_closest_encloser'] = formatter(lb2s(next_closest_encloser.canonicalize().to_text()))
1551                digest_name = list(self.name_digest_map[next_closest_encloser].items())[0][1]
1552                if digest_name is not None:
1553                    d['next_closest_encloser_hash'] = formatter(fmt.format_nsec3_name(digest_name))
1554                else:
1555                    d['next_closest_encloser_hash'] = None
1556
1557                if self.nsec_names_covering_qname:
1558                    qname, nsec_names = list(self.nsec_names_covering_qname.items())[0]
1559                    nsec_name = list(nsec_names)[0]
1560                    next_name = self.nsec_set_info.name_for_nsec3_next(nsec_name)
1561                    d['next_closest_encloser_covering'] = OrderedDict((
1562                        ('covered_name', formatter(fmt.format_nsec3_name(qname))),
1563                        ('nsec3_owner', formatter(fmt.format_nsec3_name(nsec_name))),
1564                        ('nsec3_next', formatter(fmt.format_nsec3_name(next_name))),
1565                    ))
1566
1567                wildcard_name = self._get_wildcard(encloser_name)
1568                wildcard_digest = list(self.name_digest_map[wildcard_name].items())[0][1]
1569                d['wildcard'] = formatter(lb2s(wildcard_name.canonicalize().to_text()))
1570                if wildcard_digest is not None:
1571                    d['wildcard_hash'] = formatter(fmt.format_nsec3_name(wildcard_digest))
1572                else:
1573                    d['wildcard_hash'] = None
1574                if self.nsec_for_wildcard_name:
1575                    d['wildcard_nsec_match'] = formatter(fmt.format_nsec3_name(list(self.nsec_for_wildcard_name)[0]))
1576
1577            if not self.nsec_for_qname and not self.closest_encloser:
1578                digest_name = list(self.name_digest_map[self.qname].items())[0][1]
1579                if digest_name is not None:
1580                    d['sname_hash'] = formatter(fmt.format_nsec3_name(digest_name))
1581                else:
1582                    d['sname_hash'] = None
1583
1584        if loglevel <= logging.INFO or erroneous_status:
1585            d['status'] = nsec_status_mapping[self.validation_status]
1586
1587        if loglevel <= logging.INFO:
1588            servers = tuple_to_dict(self.nsec_set_info.servers_clients)
1589            if consolidate_clients:
1590                servers = list(servers)
1591                servers.sort()
1592            d['servers'] = servers
1593
1594            if map_ip_to_ns_name is not None:
1595                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
1596                ns_names.sort()
1597                d['ns_names'] = ns_names
1598
1599            tags = set()
1600            nsids = set()
1601            for server,client in self.nsec_set_info.servers_clients:
1602                for response in self.nsec_set_info.servers_clients[(server, client)]:
1603                    if response is not None:
1604                        tags.add(response.effective_query_tag())
1605                        nsid = response.nsid_val()
1606                        if nsid is not None:
1607                            nsids.add(nsid)
1608
1609            if nsids:
1610                d['nsid_values'] = list(nsids)
1611                d['nsid_values'].sort()
1612
1613            d['query_options'] = list(tags)
1614            d['query_options'].sort()
1615
1616        if self.warnings and loglevel <= logging.WARNING:
1617            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
1618
1619        if self.errors and loglevel <= logging.ERROR:
1620            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
1621
1622        return d
1623
1624class CNAMEFromDNAMEStatus(object):
1625    def __init__(self, synthesized_cname, included_cname):
1626        self.synthesized_cname = synthesized_cname
1627        self.included_cname = included_cname
1628        self.warnings = []
1629        self.errors = []
1630
1631        if self.included_cname is None:
1632            self.validation_status = DNAME_STATUS_INVALID
1633            self.errors.append(Errors.DNAMENoCNAME())
1634        else:
1635            self.validation_status = DNAME_STATUS_VALID
1636            if self.included_cname.rrset[0].target != self.synthesized_cname.rrset[0].target:
1637                self.errors.append(Errors.DNAMETargetMismatch(included_target=fmt.humanize_name(self.included_cname.rrset[0].target), synthesized_target=fmt.humanize_name(self.synthesized_cname.rrset[0].target)))
1638                self.validation_status = DNAME_STATUS_INVALID_TARGET
1639            if self.included_cname.rrset.ttl != self.synthesized_cname.rrset.ttl:
1640                if self.included_cname.rrset.ttl == 0:
1641                    self.warnings.append(Errors.DNAMETTLZero())
1642                else:
1643                    self.warnings.append(Errors.DNAMETTLMismatch(cname_ttl=self.included_cname.rrset.ttl, dname_ttl=self.synthesized_cname.rrset.ttl))
1644
1645    def __str__(self):
1646        return 'CNAME synthesis for %s from %s/%s' % (fmt.humanize_name(self.synthesized_cname.rrset.name), fmt.humanize_name(self.synthesized_cname.dname_info.rrset.name), dns.rdatatype.to_text(self.synthesized_cname.dname_info.rrset.rdtype))
1647
1648    def serialize(self, rrset_info_serializer=None, consolidate_clients=True, loglevel=logging.DEBUG, html_format=False, map_ip_to_ns_name=None):
1649        values = []
1650        d = OrderedDict()
1651
1652        dname_serialized = None
1653        if rrset_info_serializer is not None:
1654            dname_serialized = rrset_info_serializer(self.synthesized_cname.dname_info, consolidate_clients=consolidate_clients, show_servers=False, loglevel=loglevel, html_format=html_format)
1655        elif loglevel <= logging.DEBUG:
1656            dname_serialized = self.synthesized_cname.dname_info.serialize(consolidate_clients=consolidate_clients, html_format=html_format)
1657
1658        erroneous_status = self.validation_status != STATUS_VALID
1659
1660        show_id = loglevel <= logging.INFO or \
1661                (self.warnings and loglevel <= logging.WARNING) or \
1662                (self.errors and loglevel <= logging.ERROR) or \
1663                (erroneous_status or dname_serialized)
1664
1665        if html_format:
1666            formatter = lambda x: escape(x, True)
1667        else:
1668            formatter = lambda x: x
1669
1670        if show_id:
1671            d['id'] = lb2s(self.synthesized_cname.dname_info.rrset.name.canonicalize().to_text())
1672
1673        if loglevel <= logging.DEBUG:
1674            d['description'] = formatter(str(self))
1675
1676        if dname_serialized:
1677            d['dname'] = dname_serialized
1678
1679        if loglevel <= logging.DEBUG:
1680            if self.included_cname is not None:
1681                d['cname_owner'] = formatter(lb2s(self.included_cname.rrset.name.canonicalize().to_text()))
1682                d['cname_target'] = formatter(lb2s(self.included_cname.rrset[0].target.canonicalize().to_text()))
1683
1684        if loglevel <= logging.INFO or erroneous_status:
1685            d['status'] = dname_status_mapping[self.validation_status]
1686
1687        if loglevel <= logging.INFO:
1688            servers = tuple_to_dict(self.synthesized_cname.dname_info.servers_clients)
1689            if consolidate_clients:
1690                servers = list(servers)
1691                servers.sort()
1692            d['servers'] = servers
1693
1694            if map_ip_to_ns_name is not None:
1695                ns_names = list(set([lb2s(map_ip_to_ns_name(s)[0][0].canonicalize().to_text()) for s in servers]))
1696                ns_names.sort()
1697                d['ns_names'] = ns_names
1698
1699            tags = set()
1700            nsids = set()
1701            for server,client in self.synthesized_cname.dname_info.servers_clients:
1702                for response in self.synthesized_cname.dname_info.servers_clients[(server, client)]:
1703                    if response is not None:
1704                        tags.add(response.effective_query_tag())
1705                        nsid = response.nsid_val()
1706                        if nsid is not None:
1707                            nsids.add(nsid)
1708
1709            if nsids:
1710                d['nsid_values'] = list(nsids)
1711                d['nsid_values'].sort()
1712
1713            d['query_options'] = list(tags)
1714            d['query_options'].sort()
1715
1716        if self.warnings and loglevel <= logging.WARNING:
1717            d['warnings'] = [w.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for w in self.warnings]
1718
1719        if self.errors and loglevel <= logging.ERROR:
1720            d['errors'] = [e.serialize(consolidate_clients=consolidate_clients, html_format=html_format) for e in self.errors]
1721
1722        return d
1723