1# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
2#
3# SPDX-License-Identifier: MPL-2.0
4#
5# This Source Code Form is subject to the terms of the Mozilla Public
6# License, v. 2.0.  If a copy of the MPL was not distributed with this
7# file, you can obtain one at https://mozilla.org/MPL/2.0/.
8#
9# See the COPYRIGHT file distributed with this work for additional
10# information regarding copyright ownership.
11
12from collections import defaultdict
13from .dnskey import *
14from .keydict import *
15from .keyevent import *
16from .policy import *
17import time
18
19
20class keyseries:
21    _K = defaultdict(lambda: defaultdict(list))
22    _Z = defaultdict(lambda: defaultdict(list))
23    _zones = set()
24    _kdict = None
25    _context = None
26
27    def __init__(self, kdict, now=time.time(), context=None):
28        self._kdict = kdict
29        self._context = context
30        self._zones = set(kdict.missing())
31
32        for zone in kdict.zones():
33            self._zones.add(zone)
34            for alg, keys in kdict[zone].items():
35                for k in keys.values():
36                    if k.sep:
37                        if not (k.delete() and k.delete() < now):
38                          self._K[zone][alg].append(k)
39                    else:
40                        if not (k.delete() and k.delete() < now):
41                          self._Z[zone][alg].append(k)
42
43                self._K[zone][alg].sort()
44                self._Z[zone][alg].sort()
45
46    def __iter__(self):
47        for zone in self._zones:
48            for collection in [self._K, self._Z]:
49                if zone not in collection:
50                    continue
51                for alg, keys in collection[zone].items():
52                    for key in keys:
53                        yield key
54
55    def dump(self):
56        for k in self:
57            print("%s" % repr(k))
58
59    def fixseries(self, keys, policy, now, **kwargs):
60        force = kwargs.get('force', False)
61        if not keys:
62            return
63
64        # handle the first key
65        key = keys[0]
66        if key.sep:
67            rp = policy.ksk_rollperiod
68            prepub = policy.ksk_prepublish or (30 * 86400)
69            postpub = policy.ksk_postpublish or (30 * 86400)
70        else:
71            rp = policy.zsk_rollperiod
72            prepub = policy.zsk_prepublish or (30 * 86400)
73            postpub = policy.zsk_postpublish or (30 * 86400)
74
75        # the first key should be published and active
76        p = key.publish()
77        a = key.activate()
78        if not p or p > now:
79            key.setpublish(now)
80            p = now
81        if not a or a > now:
82            key.setactivate(now)
83            a = now
84
85        i = key.inactive()
86        fudge = 300
87        if not rp:
88            key.setinactive(None, **kwargs)
89            key.setdelete(None, **kwargs)
90        elif not i or a + rp != i:
91            if not i and a + rp > now + prepub + fudge:
92                key.setinactive(a + rp, **kwargs)
93                key.setdelete(a + rp + postpub, **kwargs)
94            elif not i:
95                key.setinactive(now + prepub + fudge, **kwargs)
96                key.setdelete(now + prepub + postpub + fudge, **kwargs)
97            elif i < now:
98                pass
99            elif a + rp > i:
100                key.setinactive(a + rp, **kwargs)
101                key.setdelete(a + rp + postpub, **kwargs)
102            elif a + rp > now + prepub + fudge:
103                key.setinactive(a + rp, **kwargs)
104                key.setdelete(a + rp + postpub, **kwargs)
105            else:
106                key.setinactive(now + prepub + fudge, **kwargs)
107                key.setdelete(now + prepub + postpub + fudge, **kwargs)
108        else:
109            d = key.delete()
110            if not d or i + postpub > now + fudge:
111                key.setdelete(i + postpub, **kwargs)
112            elif not d:
113                key.setdelete(now + postpub + fudge, **kwargs)
114            elif d < now + fudge:
115                pass
116            elif d < i + postpub:
117                key.setdelete(i + postpub, **kwargs)
118
119        if policy.keyttl != key.ttl:
120            key.setttl(policy.keyttl)
121
122        # handle all the subsequent keys
123        prev = key
124        for key in keys[1:]:
125            # if no rollperiod, then all keys after the first in
126            # the series kept inactive.
127            # (XXX: we need to change this to allow standby keys)
128            if not rp:
129                key.setpublish(None, **kwargs)
130                key.setactivate(None, **kwargs)
131                key.setinactive(None, **kwargs)
132                key.setdelete(None, **kwargs)
133                if policy.keyttl != key.ttl:
134                    key.setttl(policy.keyttl)
135                continue
136
137            # otherwise, ensure all dates are set correctly based on
138            # the initial key
139            a = prev.inactive()
140            p = a - prepub
141            key.setactivate(a, **kwargs)
142            key.setpublish(p, **kwargs)
143            key.setinactive(a + rp, **kwargs)
144            key.setdelete(a + rp + postpub, **kwargs)
145            prev.setdelete(a + postpub, **kwargs)
146            if policy.keyttl != key.ttl:
147                key.setttl(policy.keyttl)
148            prev = key
149
150        # if we haven't got sufficient coverage, create successor key(s)
151        while rp and prev.inactive() and \
152              prev.inactive() < now + policy.coverage:
153            # commit changes to predecessor: a successor can only be
154            # generated if Inactive has been set in the predecessor key
155            prev.commit(self._context['settime_path'], **kwargs)
156            key = prev.generate_successor(self._context['keygen_path'],
157                                          self._context['randomdev'],
158                                          prepub, **kwargs)
159
160            key.setinactive(key.activate() + rp, **kwargs)
161            key.setdelete(key.inactive() + postpub, **kwargs)
162            keys.append(key)
163            prev = key
164
165        # last key? we already know we have sufficient coverage now, so
166        # disable the inactivation of the final key (if it was set),
167        # ensuring that if dnssec-keymgr isn't run again, the last key
168        # in the series will at least remain usable.
169        prev.setinactive(None, **kwargs)
170        prev.setdelete(None, **kwargs)
171
172        # commit changes
173        for key in keys:
174            key.commit(self._context['settime_path'], **kwargs)
175
176
177    def enforce_policy(self, policies, now=time.time(), **kwargs):
178        # If zones is provided as a parameter, use that list.
179        # If not, use what we have in this object
180        zones = kwargs.get('zones', self._zones)
181        keys_dir = kwargs.get('dir', self._context.get('keys_path', None))
182        force = kwargs.get('force', False)
183
184        for zone in zones:
185            collections = []
186            policy = policies.policy(zone)
187            keys_dir = keys_dir or policy.directory or '.'
188            alg = policy.algorithm
189            algnum = dnskey.algnum(alg)
190            if 'ksk' not in kwargs or not kwargs['ksk']:
191                if len(self._Z[zone][algnum]) == 0:
192                    k = dnskey.generate(self._context['keygen_path'],
193                                        self._context['randomdev'],
194                                        keys_dir, zone, alg,
195                                        policy.zsk_keysize, False,
196                                        policy.keyttl or 3600,
197                                        **kwargs)
198                    self._Z[zone][algnum].append(k)
199                collections.append(self._Z[zone])
200
201            if 'zsk' not in kwargs or not kwargs['zsk']:
202                if len(self._K[zone][algnum]) == 0:
203                    k = dnskey.generate(self._context['keygen_path'],
204                                        self._context['randomdev'],
205                                        keys_dir, zone, alg,
206                                        policy.ksk_keysize, True,
207                                        policy.keyttl or 3600,
208                                        **kwargs)
209                    self._K[zone][algnum].append(k)
210                collections.append(self._K[zone])
211
212            for collection in collections:
213                for algorithm, keys in collection.items():
214                    if algorithm != algnum:
215                        continue
216                    try:
217                        self.fixseries(keys, policy, now, **kwargs)
218                    except Exception as e:
219                        raise Exception('%s/%s: %s' %
220                                        (zone, dnskey.algstr(algnum), str(e)))
221