routine.py 25.7 KB
Newer Older
Valentin Samir's avatar
Valentin Samir committed
1
#!/usr/bin/env python
2
# -*- coding: utf8 -*-
Valentin Samir's avatar
Valentin Samir committed
3
4
5
6

import os
import sys
import datetime
Valentin Samir's avatar
style    
Valentin Samir committed
7
import subprocess
Valentin Samir's avatar
Valentin Samir committed
8
import argparse
9
import pwd
10
import ConfigParser
11

Valentin Samir's avatar
Valentin Samir committed
12
13
from functools import total_ordering

14

Valentin Samir's avatar
Valentin Samir committed
15
BASE = "/etc/bind/keys"
Valentin Samir's avatar
Valentin Samir committed
16
17
18
19

# Interval between 2 operations on the dns keys.
# For example if you have KEY1 enabled, KEY2 is published INTERVAL before disabling KEY1. KEY1 is
# disabled when KEY2 is activated, KEY2 is deleted INTERVAL after being disabled.
20
# INTERVAL MUST be greater than the longest TTL that the DS records can have
Valentin Samir's avatar
Valentin Samir committed
21
22
23
# INTERVAL MUST also be higher in the bind signature interval (default 22.5 days)
# This mainly depents of the parent zone configuration and you do not necessarily have
# control over it.
Valentin Samir's avatar
Valentin Samir committed
24
INTERVAL = datetime.timedelta(days=23)
Valentin Samir's avatar
Valentin Samir committed
25
26
27
28
29
30
31
32
33
34

# Time after which a ZSK is replaced by a new ZSK.
# Generation of ZSK and activation / deactivation / deletion is managed automatically as long as
# routine.py -c is called at least once a day.
ZSK_VALIDITY = datetime.timedelta(days=30)  # ~1 month

# Time after which a new KSK is generated and published for the zone (and activated after INTERVAL).
# The old key is removed only INTERVAL after the new key was routine.py --ds-seen. This usually
# requires a manual operation with the registrar (publish DS of the new key in the parent zone).
# routine.py -c displays a message as long as --ds-seen needs to be called and has not yet be called
35
36
KSK_VALIDITY = datetime.timedelta(days=366)  # ~1 an

Valentin Samir's avatar
Valentin Samir committed
37

Valentin Samir's avatar
Valentin Samir committed
38
39
40
41
42
43
DNSSEC_SETTIME = "/usr/sbin/dnssec-settime"
DNSSEC_DSFROMKEY = "/usr/sbin/dnssec-dsfromkey"
DNSSEC_KEYGEN = "/usr/sbin/dnssec-keygen"
RNDC = "/usr/sbin/rndc"


Valentin Samir's avatar
Valentin Samir committed
44
45
46
47
def get_zones(zone_names=None):
    l = []
    if zone_names is None:
        for f in os.listdir(BASE):
Valentin Samir's avatar
Valentin Samir committed
48
            if os.path.isdir(os.path.join(BASE, f)) and not f.startswith('.'):
Valentin Samir's avatar
Valentin Samir committed
49
50
51
52
53
54
                l.append(Zone(f))
    else:
        for name in zone_names:
            l.append(Zone(name))
    return l

Valentin Samir's avatar
style    
Valentin Samir committed
55

Valentin Samir's avatar
Valentin Samir committed
56
def settime(path, flag, date):
Valentin Samir's avatar
Valentin Samir committed
57
    """Set the time of the flag ``flag`` for the key at ``path`` to ``date``"""
Valentin Samir's avatar
style    
Valentin Samir committed
58
    cmd = [
Valentin Samir's avatar
Valentin Samir committed
59
        DNSSEC_SETTIME,
Valentin Samir's avatar
style    
Valentin Samir committed
60
61
62
        "-i", str(int(INTERVAL.total_seconds())),
        "-%s" % flag, date, path
    ]
63
    p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
Valentin Samir's avatar
Valentin Samir committed
64
65
66
67
    err = p.communicate()[1]
    if p.returncode != 0:
        raise ValueError("err %s: %s" % (p.returncode, err))
    if err:
68
        sys.stderr.write("%s\n" % err)
Valentin Samir's avatar
Valentin Samir committed
69

Valentin Samir's avatar
style    
Valentin Samir committed
70

Valentin Samir's avatar
Valentin Samir committed
71
def bind_chown(path):
72
73
74
75
76
77
78
79
80
81
82
83
84
    """
        Gives the files to the bind user and sets the modes in a relevant way.
    """
    try:
        bind_uid = pwd.getpwnam('bind').pw_uid
        os.chown(path, bind_uid, -1)
        for root, dirs, files in os.walk(path):
            for momo in dirs:
                os.chown(os.path.join(root, momo), bind_uid, -1)
            for momo in files:
                os.chown(os.path.join(root, momo), bind_uid, -1)
    except KeyError:
        sys.stderr.write("User bind not found, failing to give keys ownership to bind\n")
Valentin Samir's avatar
style    
Valentin Samir committed
85

Valentin Samir's avatar
style    
Valentin Samir committed
86

Valentin Samir's avatar
Valentin Samir committed
87
def bind_reload():
Valentin Samir's avatar
Valentin Samir committed
88
    """Reload bind config"""
Valentin Samir's avatar
Valentin Samir committed
89
    cmd = [RNDC, "reload"]
Valentin Samir's avatar
Valentin Samir committed
90
91
92
    p = subprocess.Popen(cmd)
    p.wait()

Valentin Samir's avatar
style    
Valentin Samir committed
93

Valentin Samir's avatar
Valentin Samir committed
94
def nsec3(zone, salt="-"):
Valentin Samir's avatar
Valentin Samir committed
95
    """Enable nsec3 for the zone ``zone``"""
Valentin Samir's avatar
Valentin Samir committed
96
    cmd = [RNDC, "signing", "-nsec3param", "1", "0", "10", salt, zone]
Valentin Samir's avatar
Valentin Samir committed
97
98
99
100
101
102
103
    sys.stdout.write("Enabling nsec3 for zone %s: " % zone)
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    out = p.communicate()[0]
    sys.stdout.write(out)
    p.wait()


Valentin Samir's avatar
Valentin Samir committed
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class Zone(object):
    ZSK = None
    KSK = None
    _path = None
    name = None

    def __str__(self):
        return self.name

    def __unicode__(self):
        return self.name.decode("utf-8")

    def __repr__(self):
        return "Zone %s" % self.name

    @classmethod
    def create(cls, name):
        path = os.path.join(BASE, name)
        if os.path.isdir(path):
            raise ValueError("%s existe" % path)
        os.mkdir(path)
        bind_chown(path)
        return cls(name)

    def do_zsk(self):
Valentin Samir's avatar
Valentin Samir committed
129
        """Perform daily routine on ZSK keys (generate new keys, delete old ones...)"""
Valentin Samir's avatar
Valentin Samir committed
130
131
        for zsk in self.ZSK:
            if zsk.is_activate:
132
133
                zsk.inactive = zsk.activate + ZSK_VALIDITY
                zsk.delete = zsk.inactive + INTERVAL
134
                last_activate_zsk = zsk
135
        now = datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
136
        if zsk.is_activate:
137
138
            zsk.inactive = max(zsk.inactive, now + INTERVAL)
            zsk.delete = zsk.inactive + INTERVAL
Valentin Samir's avatar
Valentin Samir committed
139
140
            zsk.gen_successor()
            bind_reload()
141
142
        else:
            zsk.activate = last_activate_zsk.inactive
Valentin Samir's avatar
Valentin Samir committed
143

144
    def do_ksk(self):
Valentin Samir's avatar
Valentin Samir committed
145
        """Perform daily routine on KSK keys (generate new keys...)"""
146
147
148
149
150
        ksk = self.KSK[-1]
        if ksk.need_renew:
            now = datetime.datetime.utcnow()
            new_ksk = Key.create("KSK", self.name)
            new_ksk.publish = now
151
152
            # do not activate the new key until ds-seen
            new_ksk.activate = None
153
            bind_reload()
Valentin Samir's avatar
style    
Valentin Samir committed
154
155
156
157
158
159
160
161
        active_ksk = [key for key in self.KSK if key.is_publish and key.delete is None]
        if len(active_ksk) >= 2:
            sys.stderr.write(
                (
                    "New KSK needs DS seen and/or old KSK needs "
                    "inactivate/remove for zone %s\n"
                ) % self.name
            )
162
163

    def ds_seen(self, keyid):
Valentin Samir's avatar
Valentin Samir committed
164
165
166
167
        """
            Specify that the DS for the KSK ``keyid`` has been seen in the parent zone, programming
            KSK rotation.
        """
168
169
170
171
172
173
174
175
176
        old_ksks = []
        for ksk in self.KSK:
            if ksk.keyid == keyid:
                seen_ksk = ksk
                break
            old_ksks.append(ksk)
        else:
            sys.stderr.write("Key not found\n")
            return
Valentin Samir's avatar
style    
Valentin Samir committed
177
        print("Key %s found" % keyid)
178
        now = datetime.datetime.utcnow()
179
180
        if seen_ksk.activate is None:
            seen_ksk.activate = (now + INTERVAL)
181
        for ksk in old_ksks:
Valentin Samir's avatar
style    
Valentin Samir committed
182
            print(" * program key %s removal" % ksk.keyid)
183
            # set inactive in at least INTERVAL
184
185
186
            ksk.inactive = seen_ksk.activate
            # delete INTERVAL after being inactive
            ksk.delete = ksk.inactive + INTERVAL
187
188
189
        bind_reload()

    def remove_deleted(self):
Valentin Samir's avatar
Valentin Samir committed
190
        """Move deleted keys to the deleted folder"""
191
192
193
194
195
196
        deleted_path = os.path.join(self._path, "deleted")
        try:
            os.mkdir(deleted_path)
        except OSError as error:
            if error.errno != 17:  # File exists
                raise
197
        now = datetime.datetime.utcnow()
198
        for key in self.ZSK + self.KSK:
199
            if key.delete and (key.delete + INTERVAL) <= now:
200
201
202
203
204
                for path in [key._path, key._path_private]:
                    basename = os.path.basename(path)
                    new_path = os.path.join(deleted_path, basename)
                    os.rename(path, new_path)

Valentin Samir's avatar
Valentin Samir committed
205
    def ds(self):
Valentin Samir's avatar
Valentin Samir committed
206
        """Display the DS of the KSK of the zone"""
Valentin Samir's avatar
Valentin Samir committed
207
        for ksk in self.KSK:
Valentin Samir's avatar
Valentin Samir committed
208
            cmd = [DNSSEC_DSFROMKEY, ksk._path]
Valentin Samir's avatar
Valentin Samir committed
209
210
            p = subprocess.Popen(cmd)
            p.wait()
Valentin Samir's avatar
style    
Valentin Samir committed
211

212
    def key(self, show_ksk=False, show_zsk=False):
Valentin Samir's avatar
Valentin Samir committed
213
        """Displays the public keys of the KSK and/or ZSK"""
214
215
        if show_ksk:
            for ksk in self.KSK:
216
                print(ksk)
217
218
        if show_zsk:
            for zsk in self.ZSK:
219
220
221
222
                print(zsk)

    @staticmethod
    def _key_table_format(znl, show_creation=False):
Valentin Samir's avatar
Valentin Samir committed
223
        format_string = "|{!s:^%d}|{}|{!s:>5}|" % znl
224
225
226
        if show_creation:
            format_string += "{created!s:^19}|"
        format_string += "{!s:^19}|{!s:^19}|{!s:^19}|{!s:^19}|"
Valentin Samir's avatar
Valentin Samir committed
227
        separator = ("+" + "-" * znl + "+-+-----+" + ("-" * 19 + "+") * (5 if show_creation else 4))
228
229
230
231
232
233
234
        return (format_string, separator)

    @classmethod
    def _key_table_header(cls, znl, show_creation=False):
        (format_string, separator) = cls._key_table_format(znl, show_creation)
        print(separator)
        print(format_string.format(
Valentin Samir's avatar
Valentin Samir committed
235
236
            "Zone name", "T", "KeyId", "Publish", "Activate",
            "Inactive", "Delete", created="Created"
237
238
239
240
241
242
243
244
245
        ))
        print(separator)

    def _key_table_body(self, znl, show_creation=False):
        (format_string, separator) = self._key_table_format(znl, show_creation)
        for ksk in self.KSK:
            print(format_string.format(
                ksk.zone_name,
                "K",
Valentin Samir's avatar
Valentin Samir committed
246
                ksk.keyid,
247
248
249
250
251
252
253
254
255
256
                ksk.publish or "N/A",
                ksk.activate or "N/A",
                ksk.inactive or "N/A",
                ksk.delete or "N/A",
                created=ksk.created or "N/A",
            ))
        for zsk in self.ZSK:
            print(format_string.format(
                zsk.zone_name,
                "Z",
Valentin Samir's avatar
Valentin Samir committed
257
                zsk.keyid,
258
259
260
261
262
263
264
265
266
267
268
269
270
                zsk.publish or "N/A",
                zsk.activate or "N/A",
                zsk.inactive or "N/A",
                zsk.delete or "N/A",
                created=zsk.created or "N/A",
            ))

    @classmethod
    def _key_table_footer(cls, znl, show_creation=False):
        (format_string, separator) = cls._key_table_format(znl, show_creation)
        print(separator)

    def key_table(self, show_creation=False):
Valentin Samir's avatar
Valentin Samir committed
271
        """Show meta data for the zone keys in a table"""
272
        znl = max(len(self.name), 9)
273
274
275
        self._key_table_header(znl, show_creation)
        self._key_table_body(znl, show_creation)
        self._key_table_footer(znl, show_creation)
Valentin Samir's avatar
Valentin Samir committed
276
277
278
279

    def __init__(self, name):
        path = os.path.join(BASE, name)
        if not os.path.isdir(path):
Valentin Samir's avatar
Valentin Samir committed
280
            raise ValueError("%s is not a directory" % path)
Valentin Samir's avatar
Valentin Samir committed
281
282
283
284
285
286
        self.name = name
        self._path = path
        self.ZSK = []
        self.KSK = []
        for file in os.listdir(path):
            file_path = os.path.join(path, file)
287
            if os.path.isfile(file_path) and file_path.endswith(".private"):
Valentin Samir's avatar
style    
Valentin Samir committed
288
289
290
291
292
293
294
295
296
297
                try:
                    key = Key(file_path)
                    if key.type == "ZSK":
                        self.ZSK.append(key)
                    elif key.type == "KSK":
                        self.KSK.append(key)
                    else:
                        raise RuntimeError("impossible")
                except ValueError as error:
                    sys.stderr.write("%s\n" % error)
Valentin Samir's avatar
Valentin Samir committed
298
299
300
301
        self.ZSK.sort()
        self.KSK.sort()
        if not self.ZSK:
            self.ZSK.append(Key.create("ZSK", name))
302
            self.do_zsk()
Valentin Samir's avatar
Valentin Samir committed
303
304
        if not self.KSK:
            self.KSK.append(Key.create("KSK", name))
305
            self.do_ksk()
Valentin Samir's avatar
Valentin Samir committed
306

Valentin Samir's avatar
style    
Valentin Samir committed
307

Valentin Samir's avatar
Valentin Samir committed
308
309
310
311
312
313
314
315
316
317
318
@total_ordering
class Key(object):
    _created = None
    _publish = None
    _activate = None
    _inactive = None
    _delete = None
    _data = None
    _path = None
    type = None
    keyid = None
319
320
    flag = None
    zone_name = None
Valentin Samir's avatar
Valentin Samir committed
321
322
323
324
325
326
327

    def __str__(self):
        return self._data

    def __repr__(self):
        r = os.path.basename(self._path)
        return r
Valentin Samir's avatar
style    
Valentin Samir committed
328

Valentin Samir's avatar
Valentin Samir committed
329
330
331
    def _date_from_key(self, date):
        if date is not None:
            return datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
Valentin Samir's avatar
style    
Valentin Samir committed
332

Valentin Samir's avatar
Valentin Samir committed
333
    def _date_to_key(self, date):
334
335
336
337
        if date is None:
            return 'none'
        else:
            return datetime.datetime.strftime(date, "%Y%m%d%H%M%S")
Valentin Samir's avatar
Valentin Samir committed
338

339
340
341
342
343
344
345
346
347
    def _date_check(self, value, needed_date, value_name, needed_date_name):
        if value is not None:
            if needed_date is None or value < needed_date:
                raise RuntimeError(
                    "Cannot set %s date before %s date on key %s on zone %s" % (
                        value_name,
                        needed_date_name,
                        self.keyid,
                        self.zone_name
Valentin Samir's avatar
style    
Valentin Samir committed
348
                    )
349
350
351
352
                )

    def _date_check2(self, value, needed_date, value_name, needed_date_name):
        msg = "Cannot set %s date after %s date on key %s on zone %s" % (
Valentin Samir's avatar
style    
Valentin Samir committed
353
354
355
356
357
            value_name,
            needed_date_name,
            self.keyid,
            self.zone_name
        )
358
359
360
361
362
363
        if value is None and needed_date is not None:
            raise RuntimeError(msg)
        elif value is not None and needed_date is not None:
            if value > needed_date:
                raise RuntimeError(msg)

Valentin Samir's avatar
Valentin Samir committed
364
    @classmethod
365
366
367
    def create(cls, typ, name, options=None):
        if options is None:
            options = []
Valentin Samir's avatar
Valentin Samir committed
368
        path = os.path.join(BASE, name)
Valentin Samir's avatar
Valentin Samir committed
369
        cmd = [DNSSEC_KEYGEN, "-a", "RSASHA256"]
Valentin Samir's avatar
Valentin Samir committed
370
        if typ == "KSK":
371
            cmd.extend(["-b", "2048", "-f", "KSK"])
Valentin Samir's avatar
Valentin Samir committed
372
        elif typ == "ZSK":
373
            cmd.extend(["-b", "1024"])
374
375
        else:
            raise ValueError("typ must be KSK or ZSK")
376
377
        cmd.extend(options)
        cmd.extend(["-K", path,  name])
Valentin Samir's avatar
Valentin Samir committed
378
379
380
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
Valentin Samir's avatar
Valentin Samir committed
381
            raise ValueError("The key creation has failed")
Valentin Samir's avatar
Valentin Samir committed
382
383
        keyname = p.communicate()[0].strip()
        bind_chown(path)
384
        return cls(os.path.join(path, "%s.private" % keyname))
Valentin Samir's avatar
style    
Valentin Samir committed
385

Valentin Samir's avatar
Valentin Samir committed
386
    def gen_successor(self):
Valentin Samir's avatar
style    
Valentin Samir committed
387
        cmd = [
Valentin Samir's avatar
Valentin Samir committed
388
            DNSSEC_KEYGEN, "-i", str(int(INTERVAL.total_seconds())),
Valentin Samir's avatar
style    
Valentin Samir committed
389
390
            "-S", self._path, "-K", os.path.dirname(self._path)
        ]
Valentin Samir's avatar
Valentin Samir committed
391
392
393
394
395
        p = subprocess.Popen(cmd, stderr=subprocess.PIPE)
        err = p.communicate()[1]
        if p.returncode != 0:
            raise ValueError("err %s: %s" % (p.returncode, err))
        if err:
Valentin Samir's avatar
style    
Valentin Samir committed
396
            print(err)
Valentin Samir's avatar
Valentin Samir committed
397
398
399
400
        bind_chown(os.path.dirname(self._path))

    @property
    def created(self):
401
402
        if self._created is not None:
            return self._date_from_key(self._created)
Valentin Samir's avatar
Valentin Samir committed
403
404
405

    @property
    def publish(self):
406
407
        if self._publish is not None:
            return self._date_from_key(self._publish)
Valentin Samir's avatar
style    
Valentin Samir committed
408

Valentin Samir's avatar
Valentin Samir committed
409
410
    @publish.setter
    def publish(self, value):
411
412
        self._date_check(value, self.created, "publish", "created")
        self._date_check2(value, self.activate, "publish", "activate")
Valentin Samir's avatar
Valentin Samir committed
413
414
415
416
        date = self._date_to_key(value)
        if date != self._publish:
            settime(self._path, 'P', date)
            self._publish = date
Valentin Samir's avatar
style    
Valentin Samir committed
417
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
418
419
420
421
                self._data = f.read()

    @property
    def activate(self):
422
423
        if self._activate is not None:
            return self._date_from_key(self._activate)
Valentin Samir's avatar
style    
Valentin Samir committed
424

Valentin Samir's avatar
Valentin Samir committed
425
426
    @activate.setter
    def activate(self, value):
427
428
        self._date_check(value, self.publish, "active", "publish")
        self._date_check2(value, self.inactive, "activate", "inactive")
Valentin Samir's avatar
Valentin Samir committed
429
430
431
432
        date = self._date_to_key(value)
        if date != self._activate:
            settime(self._path, 'A', date)
            self._activate = date
Valentin Samir's avatar
style    
Valentin Samir committed
433
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
434
435
436
437
                self._data = f.read()

    @property
    def inactive(self):
438
439
        if self._inactive is not None:
            return self._date_from_key(self._inactive)
Valentin Samir's avatar
style    
Valentin Samir committed
440

Valentin Samir's avatar
Valentin Samir committed
441
442
    @inactive.setter
    def inactive(self, value):
443
444
        self._date_check(value, self.activate, "inactive", "activate")
        self._date_check2(value, self.delete, "inactive", "delete")
Valentin Samir's avatar
Valentin Samir committed
445
446
447
448
        date = self._date_to_key(value)
        if date != self._inactive:
            settime(self._path, 'I', date)
            self._inactive = date
Valentin Samir's avatar
style    
Valentin Samir committed
449
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
450
451
452
453
                self._data = f.read()

    @property
    def delete(self):
454
455
        if self._delete:
            return self._date_from_key(self._delete)
Valentin Samir's avatar
style    
Valentin Samir committed
456

Valentin Samir's avatar
Valentin Samir committed
457
458
    @delete.setter
    def delete(self, value):
459
        self._date_check(value, self.inactive, "delete", "inactive")
Valentin Samir's avatar
Valentin Samir committed
460
461
462
463
        date = self._date_to_key(value)
        if date != self._delete:
            settime(self._path, 'D', date)
            self._delete = date
Valentin Samir's avatar
style    
Valentin Samir committed
464
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
465
466
                self._data = f.read()

467
468
469
    @property
    def is_publish(self):
        return self.publish is not None and self.publish <= datetime.datetime.utcnow()
Valentin Samir's avatar
style    
Valentin Samir committed
470

471
472
473
    @property
    def is_activate(self):
        return self.activate is not None and self.activate <= datetime.datetime.utcnow()
Valentin Samir's avatar
style    
Valentin Samir committed
474

475
476
477
    @property
    def is_inactive(self):
        return self.inactive is not None and self.inactive <= datetime.datetime.utcnow()
Valentin Samir's avatar
style    
Valentin Samir committed
478

479
480
481
482
483
484
485
486
487
488
489
490
491
    @property
    def is_delete(self):
        return self.delete is not None and self.delete <= datetime.datetime.utcnow()

    @property
    def need_renew(self):
        if self.type == "KSK":
            return (self.activate + KSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
        elif self.type == "ZSK":
            return (self.activate + ZSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
        else:
            raise RuntimeError("impossible")

Valentin Samir's avatar
Valentin Samir committed
492
    def __init__(self, path):
493
        if not path.endswith(".private"):
Valentin Samir's avatar
Valentin Samir committed
494
            raise ValueError("%s is not a valid private key (should ends with .private)" % path)
495
        if not os.path.isfile(path):
Valentin Samir's avatar
Valentin Samir committed
496
            raise ValueError("%s do not exists" % path)
497
498
        ppath = "%s.key" % path[:-8]
        if not os.path.isfile(ppath):
Valentin Samir's avatar
Valentin Samir committed
499
            raise ValueError("The public key (%s) of %s does not exist" % (ppath, path))
500
        with open(ppath, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
501
            self._data = f.read()
502
503
504
505
506
507
508
509
        with open(path, 'r') as f:
            private_data = f.read()
        for line in self._data.split("\n"):
            if line.startswith(";") or not line:
                continue
            line = line.split(";", 1)[0].strip()
            line = line.split()
            if len(line) < 7:
Valentin Samir's avatar
style    
Valentin Samir committed
510
                raise ValueError(
Valentin Samir's avatar
Valentin Samir committed
511
                    "The public key %s should have at least 7 fields: %r" % (ppath, line)
Valentin Samir's avatar
style    
Valentin Samir committed
512
                )
513
            if not line[0].endswith('.'):
Valentin Samir's avatar
style    
Valentin Samir committed
514
515
                raise ValueError(
                    (
Valentin Samir's avatar
Valentin Samir committed
516
                        "The public key %s should begin with the zone fqdn (ending with a .)"
Valentin Samir's avatar
style    
Valentin Samir committed
517
518
                    ) % ppath
                )
519
520
521
522
            self.zone_name = line[0][:-1]
            try:
                self.flag = int(line[3])
            except ValueError:
Valentin Samir's avatar
style    
Valentin Samir committed
523
                raise ValueError(
Valentin Samir's avatar
Valentin Samir committed
524
                    "The flag %s of the public key %s should be an integer" % (line[3], ppath)
Valentin Samir's avatar
style    
Valentin Samir committed
525
                )
526
        if self.flag == 256:
Valentin Samir's avatar
Valentin Samir committed
527
            self.type = "ZSK"
528
        elif self.flag == 257:
Valentin Samir's avatar
Valentin Samir committed
529
530
            self.type = "KSK"
        else:
Valentin Samir's avatar
Valentin Samir committed
531
532
533
534
535
536
            raise ValueError(
                "%s is not a valid key: flag %s unknown (known ones are 256 and 257)" % (
                    ppath,
                    self.flag
                )
            )
537
538
539
540
541
542
        self._path = ppath
        self._path_private = path
        keyid = path.split('.')[-2].split('+')[-1]
        try:
            self.keyid = int(keyid)
        except ValueError:
Valentin Samir's avatar
Valentin Samir committed
543
            raise ValueError("The keyid %s of the key %s should be an integer" % (keyid, path))
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
        for line in private_data.split("\n"):
            if line.startswith("Created:"):
                self._created = line[8:].strip()
                self._date_from_key(self._created)
            elif line.startswith("Publish:"):
                self._publish = line[8:].strip()
                self._date_from_key(self._publish)
            elif line.startswith("Activate:"):
                self._activate = line[9:].strip()
                self._date_from_key(self._activate)
            elif line.startswith("Inactive:"):
                self._inactive = line[9:].strip()
                self._date_from_key(self._inactive)
            elif line.startswith("Delete:"):
                self._delete = line[7:].strip()
                self._date_from_key(self._delete)
        if self.created is None:
Valentin Samir's avatar
Valentin Samir committed
561
            raise ValueError("The key %s must have as list its Created field defined" % path)
Valentin Samir's avatar
Valentin Samir committed
562
563

    def __lt__(self, y):
Valentin Samir's avatar
style    
Valentin Samir committed
564
565
566
567
568
569
570
571
        if not isinstance(y, Key):
            raise ValueError("can only compare two Keys")
        if self.activate is not None and y.activate is not None:
            return self.activate < y.activate
        elif self.publish is not None and y.publish is not None:
            return self.publish < y.publish
        else:
            return self.created < y.created
Valentin Samir's avatar
Valentin Samir committed
572
573
574
575
576

    def __eq__(self, y):
        return isinstance(y, Key) and y._path == self._path

if __name__ == '__main__':
577
    config_parser = ConfigParser.ConfigParser()
Valentin Samir's avatar
Valentin Samir committed
578
579
580
581
582
    config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'config.ini'))

    if os.path.isfile(config_file):
        config_parser.read(config_file)
        if config_parser.has_section("dnssec"):
583
584
585
            if config_parser.has_option("dnssec", "base_directory"):
                BASE = config_parser.get("dnssec", "base_directory")
            if config_parser.has_option("dnssec", "interval"):
Valentin Samir's avatar
Valentin Samir committed
586
                try:
587
                    INTERVAL = datetime.timedelta(days=config_parser.getfloat("dnssec", "interval"))
Valentin Samir's avatar
Valentin Samir committed
588
589
590
591
                except ValueError:
                    sys.stderr.write(
                        "Unable to convert the config parameter 'interval' to a float\n"
                    )
592
            if config_parser.has_option("dnssec", "zsk_validity"):
Valentin Samir's avatar
Valentin Samir committed
593
                try:
594
595
596
                    ZSK_VALIDITY = datetime.timedelta(
                        days=config_parser.getfloat("dnssec", "zsk_validity")
                    )
Valentin Samir's avatar
Valentin Samir committed
597
598
599
600
                except ValueError:
                    sys.stderr.write(
                        "Unable to convert the config parameter 'zsk_validity' to a float\n"
                    )
601
            if config_parser.has_option("dnssec", "ksk_validity"):
Valentin Samir's avatar
Valentin Samir committed
602
                try:
603
604
605
                    KSK_VALIDITY = datetime.timedelta(
                        days=config_parser.getfloat("dnssec", "ksk_validity")
                    )
Valentin Samir's avatar
Valentin Samir committed
606
607
608
609
610
611
                except ValueError:
                    sys.stderr.write(
                        "Unable to convert the config parameter 'ksk_validity' to a float\n"
                    )

        if config_parser.has_section("path"):
612
613
614
615
616
617
618
619
            if config_parser.has_option("path", "dnssec_settime"):
                DNSSEC_SETTIME = config_parser.get("path", "dnssec_settime")
            if config_parser.has_option("path", "dnssec_dsfromkey"):
                DNSSEC_DSFROMKEY = config_parser.get("path", "dnssec_dsfromkey")
            if config_parser.has_option("path", "dnssec_keygen"):
                DNSSEC_KEYGEN = config_parser.get("path", "dnssec_keygen")
            if config_parser.has_option("path", "rndc"):
                RNDC = config_parser.get("path", "rndc")
Valentin Samir's avatar
Valentin Samir committed
620
621

    for path in [DNSSEC_SETTIME, DNSSEC_DSFROMKEY, DNSSEC_KEYGEN, RNDC]:
622
623
        if not os.path.isfile(path) or not os.access(path, os.X_OK):
            sys.stderr.write("%s not found or not executable. Is bind9utils installed ?\n" % path)
Valentin Samir's avatar
Valentin Samir committed
624
625
            sys.exit(1)

Valentin Samir's avatar
Valentin Samir committed
626
    try:
Valentin Samir's avatar
Valentin Samir committed
627
628
        parser = argparse.ArgumentParser()
        parser.add_argument('zone', nargs='*', help='zone name')
Valentin Samir's avatar
style    
Valentin Samir committed
629
630
631
632
633
634
635
636
637
638
639
        parser.add_argument(
            '--make', '-m',
            action='store_true',
            help='Create keys for each supplied zone'
        )
        parser.add_argument(
            '--cron', '-c',
            action='store_true',
            help='Perform maintenance for each supplied zone or for all zones if no zone supplied'
        )
        parser.add_argument(
640
            '--ds',
Valentin Samir's avatar
style    
Valentin Samir committed
641
            action='store_true',
642
            help='Show KSK DS for each supplied zone or for all zones if no zone supplied'
Valentin Samir's avatar
style    
Valentin Samir committed
643
644
        )
        parser.add_argument(
645
646
            '--key',
            nargs='?', const="all", type=str, choices=["all", "ksk", "zsk"],
Valentin Samir's avatar
style    
Valentin Samir committed
647
648
            help='Show DNSKEY for each zone supplied zone or for all zones if no zone supplied'
        )
649
        parser.add_argument(
650
651
652
            '--key-table',
            nargs='?', const="default", type=str, choices=["default", "all_fields"],
            help='Show a table with all non deleted DNSKEY meaningful dates'
653
        )
Valentin Samir's avatar
style    
Valentin Samir committed
654
655
656
657
658
659
660
661
662
663
664
665
666
667
        parser.add_argument(
            '--ds-seen',
            metavar='KEYID',
            type=int,
            help=(
                'To call with the ID of a new KSK published in the parent zone. '
                'Programs old KSK removal'
            )
        )
        parser.add_argument(
            '--nsec3',
            action='store_true',
            help='Enable NSEC3 for the zones, using a random salt'
        )
Valentin Samir's avatar
Valentin Samir committed
668
669
670
671
672
673
        parser.add_argument(
            '--show-config',
            action='store_true',
            help='Show the current configuration'
        )

Valentin Samir's avatar
Valentin Samir committed
674
675
        args = parser.parse_args()
        zones = args.zone
Valentin Samir's avatar
Valentin Samir committed
676
677
678
679
680
681
682
683
684
685
        if args.show_config:
            print("Key base path: %s" % BASE)
            print("Interval between two operation: %s" % INTERVAL)
            print("ZSK validity duration: %s" % ZSK_VALIDITY)
            print("KSK validity duration: %s" % KSK_VALIDITY)
            print("")
            print("Path to dnssec-settime: %s" % DNSSEC_SETTIME)
            print("Path to dnssec-dsfromkey: %s" % DNSSEC_DSFROMKEY)
            print("Path to dnssec-keygen: %s" % DNSSEC_KEYGEN)
            print("Path to rdnc: %s" % RNDC)
Valentin Samir's avatar
Valentin Samir committed
686
        if args.make:
Valentin Samir's avatar
Valentin Samir committed
687
688
689
            for zone in zones:
                Zone.create(zone)
        zones = get_zones(zones if zones else None)
Valentin Samir's avatar
Valentin Samir committed
690
691
692
        if args.nsec3:
            for zone in zones:
                nsec3(zone.name, os.urandom(24).encode("hex"))
693
694
695
696
697
698
        if args.ds_seen:
            if len(zones) != 1:
                sys.stderr.write("Please specify exactly ONE zone name\n")
                sys.exit(1)
            for zone in zones:
                zone.ds_seen(args.ds_seen)
Valentin Samir's avatar
Valentin Samir committed
699
        if args.cron:
Valentin Samir's avatar
Valentin Samir committed
700
701
            for zone in zones:
                zone.do_zsk()
702
703
                zone.do_ksk()
                zone.remove_deleted()
Valentin Samir's avatar
Valentin Samir committed
704
        if args.ds:
Valentin Samir's avatar
Valentin Samir committed
705
706
            for zone in zones:
                zone.ds()
Valentin Samir's avatar
Valentin Samir committed
707
        if args.key:
Valentin Samir's avatar
Valentin Samir committed
708
            for zone in zones:
709
710
711
                zone.key(show_ksk=args.key in ["all", "ksk"], show_zsk=args.key in ["all", "zsk"])
        if args.key_table:
            znl = max(len(zone.name) for zone in zones)
712
            znl = max(znl, 9)
713
714
715
716
            Zone._key_table_header(znl, args.key_table == "all_fields")
            for zone in zones:
                zone._key_table_body(znl, args.key_table == "all_fields")
            Zone._key_table_footer(znl, args.key_table == "all_fields")
Valentin Samir's avatar
style    
Valentin Samir committed
717
718
        if not any([
            args.make, args.cron, args.ds, args.key, args.ds_seen, args.nsec3,
719
            args.show_config, args.key_table
Valentin Samir's avatar
style    
Valentin Samir committed
720
        ]):
Valentin Samir's avatar
Valentin Samir committed
721
            parser.print_help()
722
    except (ValueError, IOError) as error:
Valentin Samir's avatar
Valentin Samir committed
723
724
        sys.stderr.write("%s\n" % error)
        sys.exit(1)