routine.py 27.1 KB
Newer Older
Hamza Dely's avatar
Hamza Dely committed
1
#!/usr/bin/env python3
2
# -*- coding: utf8 -*-
Valentin Samir's avatar
Valentin Samir committed
3

4
from __future__ import print_function, unicode_literals
Hamza Dely's avatar
Hamza Dely committed
5

Valentin Samir's avatar
Valentin Samir committed
6 7
import os
import sys
8
import binascii
Valentin Samir's avatar
Valentin Samir committed
9
import datetime
Valentin Samir's avatar
Valentin Samir committed
10
import subprocess
11
import argparse
12
import pwd
Hamza Dely's avatar
Hamza Dely committed
13
try:
14
    import ConfigParser as configparser
Hamza Dely's avatar
Hamza Dely committed
15 16
except ImportError:
    import configparser
17

Valentin Samir's avatar
Valentin Samir committed
18 19
from functools import total_ordering

20

Valentin Samir's avatar
Valentin Samir committed
21
BASE = "/etc/bind/keys"
Valentin Samir's avatar
Valentin Samir committed
22 23 24 25

# 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.
26
# INTERVAL MUST be greater than the longest TTL that the DS records can have
Valentin Samir's avatar
Valentin Samir committed
27 28 29
# 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.
30
INTERVAL = datetime.timedelta(days=23)
Valentin Samir's avatar
Valentin Samir committed
31 32 33 34 35 36 37 38 39 40

# 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
41 42
KSK_VALIDITY = datetime.timedelta(days=366)  # ~1 an

43 44 45 46 47 48 49 50 51 52
# Algorithm used to generate new keys.
ALGORITHM = "RSASHA256"
SUPPORTED_ALGORITHMS = {
    8  : "RSASHA256",
    10 : "RSASHA512",
    12 : "ECCGOST",
    13 : "ECDSAP256SHA256",
    14 : "ECDSAP384SHA384",
}

Valentin Samir's avatar
Valentin Samir committed
53

54 55 56 57 58 59
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
60 61 62 63
def get_zones(zone_names=None):
    l = []
    if zone_names is None:
        for f in os.listdir(BASE):
64
            if os.path.isdir(os.path.join(BASE, f)) and not f.startswith('.'):
Valentin Samir's avatar
Valentin Samir committed
65 66 67 68 69 70
                l.append(Zone(f))
    else:
        for name in zone_names:
            l.append(Zone(name))
    return l

Valentin Samir's avatar
Valentin Samir committed
71

Valentin Samir's avatar
Valentin Samir committed
72
def settime(path, flag, date):
Valentin Samir's avatar
Valentin Samir committed
73
    """Set the time of the flag ``flag`` for the key at ``path`` to ``date``"""
Valentin Samir's avatar
Valentin Samir committed
74
    cmd = [
75
        DNSSEC_SETTIME,
Valentin Samir's avatar
Valentin Samir committed
76 77 78
        "-i", str(int(INTERVAL.total_seconds())),
        "-%s" % flag, date, path
    ]
79
    p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
80
    err = p.communicate()[1].decode()
Valentin Samir's avatar
Valentin Samir committed
81 82 83
    if p.returncode != 0:
        raise ValueError("err %s: %s" % (p.returncode, err))
    if err:
84
        print("%s" % err, file=sys.stderr)
Valentin Samir's avatar
Valentin Samir committed
85

Valentin Samir's avatar
Valentin Samir committed
86

Valentin Samir's avatar
Valentin Samir committed
87
def bind_chown(path):
88 89 90 91 92 93 94 95 96 97 98 99
    """
        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:
100
        print("User bind not found, failing to give keys ownership to bind", file=sys.stderr)
Valentin Samir's avatar
Valentin Samir committed
101

Valentin Samir's avatar
Valentin Samir committed
102

Valentin Samir's avatar
Valentin Samir committed
103
def bind_reload():
Valentin Samir's avatar
Valentin Samir committed
104
    """Reload bind config"""
105
    cmd = [RNDC, "reload"]
Valentin Samir's avatar
Valentin Samir committed
106 107 108
    p = subprocess.Popen(cmd)
    p.wait()

Valentin Samir's avatar
Valentin Samir committed
109

110
def nsec3(zone, salt="-"):
111
    """Enable NSEC3 for the zone ``zone``"""
112
    cmd = [RNDC, "signing", "-nsec3param", "1", "0", "10", salt, zone]
113
    print("Enabling nsec3 for zone %s: " % zone, file=sys.stdout)
114
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
115 116
    out = p.communicate()[0].decode()
    print(out, file=sys.stdout)
117 118 119
    p.wait()


Valentin Samir's avatar
Valentin Samir committed
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
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
145
        """Perform daily routine on ZSK keys (generate new keys, delete old ones...)"""
Valentin Samir's avatar
Valentin Samir committed
146
        for zsk in self.ZSK:
147
            if zsk.is_activate and not zsk.is_inactive:
148 149
                zsk.inactive = zsk.activate + ZSK_VALIDITY
                zsk.delete = zsk.inactive + INTERVAL
150
                last_activate_zsk = zsk
151
        now = datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
152
        if zsk.is_activate:
153 154
            zsk.inactive = max(zsk.inactive, now + INTERVAL)
            zsk.delete = zsk.inactive + INTERVAL
Valentin Samir's avatar
Valentin Samir committed
155 156
            zsk.gen_successor()
            bind_reload()
157 158
        else:
            zsk.activate = last_activate_zsk.inactive
Valentin Samir's avatar
Valentin Samir committed
159

160
    def do_ksk(self):
Valentin Samir's avatar
Valentin Samir committed
161
        """Perform daily routine on KSK keys (generate new keys...)"""
162 163 164 165
        ksk = self.KSK[-1]
        if ksk.need_renew:
            now = datetime.datetime.utcnow()
            new_ksk = Key.create("KSK", self.name)
166 167
            # do not activate the new key until ds-seen
            new_ksk.activate = None
168
            new_ksk.publish = now
169
            bind_reload()
Valentin Samir's avatar
Valentin Samir committed
170 171
        active_ksk = [key for key in self.KSK if key.is_publish and key.delete is None]
        if len(active_ksk) >= 2:
172
            print(
Valentin Samir's avatar
Valentin Samir committed
173 174
                (
                    "New KSK needs DS seen and/or old KSK needs "
175 176 177
                    "inactivate/remove for zone %s"
                ) % self.name,
                file=sys.stderr
Valentin Samir's avatar
Valentin Samir committed
178
            )
179 180

    def ds_seen(self, keyid):
Valentin Samir's avatar
Valentin Samir committed
181 182 183 184
        """
            Specify that the DS for the KSK ``keyid`` has been seen in the parent zone, programming
            KSK rotation.
        """
185 186 187 188 189 190 191
        old_ksks = []
        for ksk in self.KSK:
            if ksk.keyid == keyid:
                seen_ksk = ksk
                break
            old_ksks.append(ksk)
        else:
192
            print("Key not found", file=sys.stderr)
193
            return
194
        print("Key %s found" % keyid)
195
        now = datetime.datetime.utcnow()
196 197
        if seen_ksk.activate is None:
            seen_ksk.activate = (now + INTERVAL)
198
        for ksk in old_ksks:
199
            print(" * program key %s removal" % ksk.keyid)
200
            # set inactive in at least INTERVAL
201 202 203
            ksk.inactive = seen_ksk.activate
            # delete INTERVAL after being inactive
            ksk.delete = ksk.inactive + INTERVAL
204 205 206
        bind_reload()

    def remove_deleted(self):
Valentin Samir's avatar
Valentin Samir committed
207
        """Move deleted keys to the deleted folder"""
208 209 210 211 212 213
        deleted_path = os.path.join(self._path, "deleted")
        try:
            os.mkdir(deleted_path)
        except OSError as error:
            if error.errno != 17:  # File exists
                raise
214
        now = datetime.datetime.utcnow()
215
        for key in self.ZSK + self.KSK:
216
            if key.delete and (key.delete + INTERVAL) <= now:
217 218 219 220 221
                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
222
    def ds(self):
Valentin Samir's avatar
Valentin Samir committed
223
        """Display the DS of the KSK of the zone"""
Valentin Samir's avatar
Valentin Samir committed
224
        for ksk in self.KSK:
225
            cmd = [DNSSEC_DSFROMKEY, ksk._path]
Valentin Samir's avatar
Valentin Samir committed
226 227
            p = subprocess.Popen(cmd)
            p.wait()
Valentin Samir's avatar
Valentin Samir committed
228

229
    def key(self, show_ksk=False, show_zsk=False):
Valentin Samir's avatar
Valentin Samir committed
230
        """Displays the public keys of the KSK and/or ZSK"""
231 232
        if show_ksk:
            for ksk in self.KSK:
233
                print(ksk)
234 235
        if show_zsk:
            for zsk in self.ZSK:
236
                print(zsk)
237 238 239

    @staticmethod
    def _key_table_format(znl, show_creation=False):
Valentin Samir's avatar
Valentin Samir committed
240
        format_string = "|{!s:^%d}|{}|{!s:>5}|" % znl
241
        if show_creation:
242
            format_string += "{algorithm!s:^19}|"
243 244
            format_string += "{created!s:^19}|"
        format_string += "{!s:^19}|{!s:^19}|{!s:^19}|{!s:^19}|"
245
        separator = ("+" + "-" * znl + "+-+-----+" + ("-" * 19 + "+") * (6 if show_creation else 4))
246 247 248 249 250
        return (format_string, separator)

    @classmethod
    def _key_table_header(cls, znl, show_creation=False):
        (format_string, separator) = cls._key_table_format(znl, show_creation)
251 252
        print(separator)
        print(format_string.format(
Valentin Samir's avatar
Valentin Samir committed
253
            "Zone name", "T", "KeyId", "Publish", "Activate",
254
            "Inactive", "Delete", created="Created", algorithm="Algorithm"
255
        ))
256
        print(separator)
257 258 259 260

    def _key_table_body(self, znl, show_creation=False):
        (format_string, separator) = self._key_table_format(znl, show_creation)
        for ksk in self.KSK:
261
            print(format_string.format(
262 263
                ksk.zone_name,
                "K",
Valentin Samir's avatar
Valentin Samir committed
264
                ksk.keyid,
265 266 267 268 269
                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",
270
                algorithm=ksk.algorithm or "N/A",
271 272
            ))
        for zsk in self.ZSK:
273
            print(format_string.format(
274 275
                zsk.zone_name,
                "Z",
Valentin Samir's avatar
Valentin Samir committed
276
                zsk.keyid,
277 278 279 280 281
                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",
282
                algorithm=zsk.algorithm or "N/A",
283 284 285 286 287
            ))

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

    def key_table(self, show_creation=False):
Valentin Samir's avatar
Valentin Samir committed
291
        """Show meta data for the zone keys in a table"""
292
        znl = max(len(self.name), 9)
293 294 295
        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
296 297 298 299

    def __init__(self, name):
        path = os.path.join(BASE, name)
        if not os.path.isdir(path):
Valentin Samir's avatar
Valentin Samir committed
300
            raise ValueError("%s is not a directory" % path)
Valentin Samir's avatar
Valentin Samir committed
301 302 303 304 305 306
        self.name = name
        self._path = path
        self.ZSK = []
        self.KSK = []
        for file in os.listdir(path):
            file_path = os.path.join(path, file)
307
            if os.path.isfile(file_path) and file_path.endswith(".private"):
Valentin Samir's avatar
Valentin Samir committed
308 309 310 311 312 313 314 315 316
                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:
317
                    print("%s" % error, sys.stderr)
Valentin Samir's avatar
Valentin Samir committed
318 319 320 321
        self.ZSK.sort()
        self.KSK.sort()
        if not self.ZSK:
            self.ZSK.append(Key.create("ZSK", name))
322
            self.do_zsk()
Valentin Samir's avatar
Valentin Samir committed
323 324
        if not self.KSK:
            self.KSK.append(Key.create("KSK", name))
325
            self.do_ksk()
Valentin Samir's avatar
Valentin Samir committed
326

Valentin Samir's avatar
Valentin Samir committed
327

Valentin Samir's avatar
Valentin Samir committed
328 329 330 331 332 333 334 335 336 337 338
@total_ordering
class Key(object):
    _created = None
    _publish = None
    _activate = None
    _inactive = None
    _delete = None
    _data = None
    _path = None
    type = None
    keyid = None
339 340
    flag = None
    zone_name = None
341
    algorithm = None
Valentin Samir's avatar
Valentin Samir committed
342 343 344 345 346 347 348

    def __str__(self):
        return self._data

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

Valentin Samir's avatar
Valentin Samir committed
350 351 352
    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
Valentin Samir committed
353

Valentin Samir's avatar
Valentin Samir committed
354
    def _date_to_key(self, date):
355 356 357 358
        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
359

360 361 362 363 364 365 366 367 368
    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
Valentin Samir committed
369
                    )
370 371 372 373
                )

    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
Valentin Samir committed
374 375 376 377 378
            value_name,
            needed_date_name,
            self.keyid,
            self.zone_name
        )
379 380 381 382 383 384
        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
385
    @classmethod
386 387 388
    def create(cls, typ, name, options=None):
        if options is None:
            options = []
Valentin Samir's avatar
Valentin Samir committed
389
        path = os.path.join(BASE, name)
390
        cmd = [DNSSEC_KEYGEN, "-a", ALGORITHM]
Valentin Samir's avatar
Valentin Samir committed
391
        if typ == "KSK":
392
            cmd.extend(["-b", "2048", "-f", "KSK"])
Valentin Samir's avatar
Valentin Samir committed
393
        elif typ == "ZSK":
394
            cmd.extend(["-b", "1024"])
395 396
        else:
            raise ValueError("typ must be KSK or ZSK")
397
        cmd.extend(options)
398
        cmd.extend(["-K", path, name])
Valentin Samir's avatar
Valentin Samir committed
399 400 401
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
Valentin Samir's avatar
Valentin Samir committed
402
            raise ValueError("The key creation has failed")
403
        keyname = p.communicate()[0].strip().decode()
Valentin Samir's avatar
Valentin Samir committed
404
        bind_chown(path)
405
        return cls(os.path.join(path, "%s.private" % keyname))
Valentin Samir's avatar
Valentin Samir committed
406

Valentin Samir's avatar
Valentin Samir committed
407
    def gen_successor(self):
Valentin Samir's avatar
Valentin Samir committed
408
        cmd = [
409
            DNSSEC_KEYGEN, "-i", str(int(INTERVAL.total_seconds())),
Valentin Samir's avatar
Valentin Samir committed
410 411
            "-S", self._path, "-K", os.path.dirname(self._path)
        ]
Valentin Samir's avatar
Valentin Samir committed
412
        p = subprocess.Popen(cmd, stderr=subprocess.PIPE)
413
        err = p.communicate()[1].decode()
Valentin Samir's avatar
Valentin Samir committed
414 415 416
        if p.returncode != 0:
            raise ValueError("err %s: %s" % (p.returncode, err))
        if err:
417
            print(err)
Valentin Samir's avatar
Valentin Samir committed
418 419 420 421
        bind_chown(os.path.dirname(self._path))

    @property
    def created(self):
422 423
        if self._created is not None:
            return self._date_from_key(self._created)
Valentin Samir's avatar
Valentin Samir committed
424 425 426

    @property
    def publish(self):
427 428
        if self._publish is not None:
            return self._date_from_key(self._publish)
Valentin Samir's avatar
Valentin Samir committed
429

Valentin Samir's avatar
Valentin Samir committed
430 431
    @publish.setter
    def publish(self, value):
432 433
        self._date_check(value, self.created, "publish", "created")
        self._date_check2(value, self.activate, "publish", "activate")
Valentin Samir's avatar
Valentin Samir committed
434 435 436 437
        date = self._date_to_key(value)
        if date != self._publish:
            settime(self._path, 'P', date)
            self._publish = date
Valentin Samir's avatar
Valentin Samir committed
438
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
439 440 441 442
                self._data = f.read()

    @property
    def activate(self):
443 444
        if self._activate is not None:
            return self._date_from_key(self._activate)
Valentin Samir's avatar
Valentin Samir committed
445

Valentin Samir's avatar
Valentin Samir committed
446 447
    @activate.setter
    def activate(self, value):
448 449
        self._date_check(value, self.publish, "active", "publish")
        self._date_check2(value, self.inactive, "activate", "inactive")
Valentin Samir's avatar
Valentin Samir committed
450 451 452 453
        date = self._date_to_key(value)
        if date != self._activate:
            settime(self._path, 'A', date)
            self._activate = date
Valentin Samir's avatar
Valentin Samir committed
454
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
455 456 457 458
                self._data = f.read()

    @property
    def inactive(self):
459 460
        if self._inactive is not None:
            return self._date_from_key(self._inactive)
Valentin Samir's avatar
Valentin Samir committed
461

Valentin Samir's avatar
Valentin Samir committed
462 463
    @inactive.setter
    def inactive(self, value):
464 465
        self._date_check(value, self.activate, "inactive", "activate")
        self._date_check2(value, self.delete, "inactive", "delete")
Valentin Samir's avatar
Valentin Samir committed
466 467 468 469
        date = self._date_to_key(value)
        if date != self._inactive:
            settime(self._path, 'I', date)
            self._inactive = date
Valentin Samir's avatar
Valentin Samir committed
470
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
471 472 473 474
                self._data = f.read()

    @property
    def delete(self):
475 476
        if self._delete:
            return self._date_from_key(self._delete)
Valentin Samir's avatar
Valentin Samir committed
477

Valentin Samir's avatar
Valentin Samir committed
478 479
    @delete.setter
    def delete(self, value):
480
        self._date_check(value, self.inactive, "delete", "inactive")
Valentin Samir's avatar
Valentin Samir committed
481 482 483 484
        date = self._date_to_key(value)
        if date != self._delete:
            settime(self._path, 'D', date)
            self._delete = date
Valentin Samir's avatar
Valentin Samir committed
485
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
486 487
                self._data = f.read()

488 489 490
    @property
    def is_publish(self):
        return self.publish is not None and self.publish <= datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
491

492 493 494
    @property
    def is_activate(self):
        return self.activate is not None and self.activate <= datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
495

496 497 498
    @property
    def is_inactive(self):
        return self.inactive is not None and self.inactive <= datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
499

500 501 502 503 504 505 506
    @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":
507 508 509 510
            return (
                self.activate is not None and
                (self.activate + KSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
            )
511
        elif self.type == "ZSK":
512 513 514 515
            return (
                self.activate is not None
                and (self.activate + ZSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
            )
516 517 518
        else:
            raise RuntimeError("impossible")

Valentin Samir's avatar
Valentin Samir committed
519
    def __init__(self, path):
520
        if not path.endswith(".private"):
Valentin Samir's avatar
Valentin Samir committed
521
            raise ValueError("%s is not a valid private key (should ends with .private)" % path)
522
        if not os.path.isfile(path):
Valentin Samir's avatar
Valentin Samir committed
523
            raise ValueError("%s do not exists" % path)
524 525
        ppath = "%s.key" % path[:-8]
        if not os.path.isfile(ppath):
Valentin Samir's avatar
Valentin Samir committed
526
            raise ValueError("The public key (%s) of %s does not exist" % (ppath, path))
527
        with open(ppath, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
528
            self._data = f.read()
529 530 531 532 533 534 535 536
        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
Valentin Samir committed
537
                raise ValueError(
Valentin Samir's avatar
Valentin Samir committed
538
                    "The public key %s should have at least 7 fields: %r" % (ppath, line)
Valentin Samir's avatar
Valentin Samir committed
539
                )
540
            if not line[0].endswith('.'):
Valentin Samir's avatar
Valentin Samir committed
541 542
                raise ValueError(
                    (
Valentin Samir's avatar
Valentin Samir committed
543
                        "The public key %s should begin with the zone fqdn (ending with a .)"
Valentin Samir's avatar
Valentin Samir committed
544 545
                    ) % ppath
                )
546 547 548 549
            self.zone_name = line[0][:-1]
            try:
                self.flag = int(line[3])
            except ValueError:
Valentin Samir's avatar
Valentin Samir committed
550
                raise ValueError(
Valentin Samir's avatar
Valentin Samir committed
551
                    "The flag %s of the public key %s should be an integer" % (line[3], ppath)
Valentin Samir's avatar
Valentin Samir committed
552
                )
553
        if self.flag == 256:
Valentin Samir's avatar
Valentin Samir committed
554
            self.type = "ZSK"
555
        elif self.flag == 257:
Valentin Samir's avatar
Valentin Samir committed
556 557
            self.type = "KSK"
        else:
Valentin Samir's avatar
Valentin Samir committed
558 559 560 561 562 563
            raise ValueError(
                "%s is not a valid key: flag %s unknown (known ones are 256 and 257)" % (
                    ppath,
                    self.flag
                )
            )
564 565 566 567 568 569
        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
570
            raise ValueError("The keyid %s of the key %s should be an integer" % (keyid, path))
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
        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)
587 588 589
            elif line.startswith("Algorithm:"):
                algorithm = int(line[11:13].strip())
                self.algorithm = SUPPORTED_ALGORITHMS[algorithm]
590
        if self.created is None:
Valentin Samir's avatar
Valentin Samir committed
591
            raise ValueError("The key %s must have as list its Created field defined" % path)
Valentin Samir's avatar
Valentin Samir committed
592 593

    def __lt__(self, y):
Valentin Samir's avatar
Valentin Samir committed
594 595 596 597 598 599 600 601
        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
602 603 604 605 606

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

if __name__ == '__main__':
Hamza Dely's avatar
Hamza Dely committed
607
    config_parser = configparser.ConfigParser()
608 609 610 611 612
    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"):
613 614 615
            if config_parser.has_option("dnssec", "base_directory"):
                BASE = config_parser.get("dnssec", "base_directory")
            if config_parser.has_option("dnssec", "interval"):
616
                try:
617
                    INTERVAL = datetime.timedelta(days=config_parser.getfloat("dnssec", "interval"))
618
                except ValueError:
619
                    print(
620 621
                        "Unable to convert the config parameter 'interval' to a float",
                        file=sys.stderr
622
                    )
623
            if config_parser.has_option("dnssec", "zsk_validity"):
624
                try:
625 626 627
                    ZSK_VALIDITY = datetime.timedelta(
                        days=config_parser.getfloat("dnssec", "zsk_validity")
                    )
628
                except ValueError:
629
                    print(
630 631
                        "Unable to convert the config parameter 'zsk_validity' to a float",
                        file=sys.stderr
632
                    )
633
            if config_parser.has_option("dnssec", "ksk_validity"):
634
                try:
635 636 637
                    KSK_VALIDITY = datetime.timedelta(
                        days=config_parser.getfloat("dnssec", "ksk_validity")
                    )
638
                except ValueError:
639
                    print(
640 641
                        "Unable to convert the config parameter 'ksk_validity' to a float",
                        file=sys.stderr
642
                    )
643 644 645 646 647 648 649
            if config_parser.has_option("dnssec", "algorithm"):
                ALGORITHM = config_parser.get("dnssec", "algorithm")
                if ALGORITHM not in SUPPORTED_ALGORITHMS.values():
                    raise ValueError(
                        "Invalid algorithm %s."
                        "Supported algorithms are %s" % (ALGORITHM, ", ".join(SUPPORTED_ALGORITHMS))
                    )
650 651

        if config_parser.has_section("path"):
652 653 654 655 656 657 658 659
            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")
660 661

    for path in [DNSSEC_SETTIME, DNSSEC_DSFROMKEY, DNSSEC_KEYGEN, RNDC]:
662
        if not os.path.isfile(path) or not os.access(path, os.X_OK):
663
            sys.exit("%s not found or not executable. Is bind9utils installed ?\n" % path)
664

Valentin Samir's avatar
Valentin Samir committed
665
    try:
666 667
        parser = argparse.ArgumentParser()
        parser.add_argument('zone', nargs='*', help='zone name')
Valentin Samir's avatar
Valentin Samir committed
668 669 670 671 672 673 674 675 676 677 678
        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(
679
            '--ds',
Valentin Samir's avatar
Valentin Samir committed
680
            action='store_true',
681
            help='Show KSK DS for each supplied zone or for all zones if no zone supplied'
Valentin Samir's avatar
Valentin Samir committed
682 683
        )
        parser.add_argument(
684 685
            '--key',
            nargs='?', const="all", type=str, choices=["all", "ksk", "zsk"],
Valentin Samir's avatar
Valentin Samir committed
686 687
            help='Show DNSKEY for each zone supplied zone or for all zones if no zone supplied'
        )
688
        parser.add_argument(
689 690 691
            '--key-table',
            nargs='?', const="default", type=str, choices=["default", "all_fields"],
            help='Show a table with all non deleted DNSKEY meaningful dates'
692
        )
Valentin Samir's avatar
Valentin Samir committed
693 694 695 696 697 698 699 700 701 702 703 704 705 706
        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'
        )
707 708 709 710 711 712
        parser.add_argument(
            '--show-config',
            action='store_true',
            help='Show the current configuration'
        )

713 714
        args = parser.parse_args()
        zones = args.zone
715
        if args.show_config:
716 717 718 719
            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)
720
            print("DNSKEY algorithm: %s" % ALGORITHM)
721 722 723 724 725
            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)
726
        if args.make:
Valentin Samir's avatar
Valentin Samir committed
727 728 729
            for zone in zones:
                Zone.create(zone)
        zones = get_zones(zones if zones else None)
730 731
        if args.nsec3:
            for zone in zones:
732
                nsec3(zone.name, binascii.hexlify(os.urandom(24)).decode())
733 734
        if args.ds_seen:
            if len(zones) != 1:
735
                sys.exit("Please specify exactly ONE zone name\n")
736 737
            for zone in zones:
                zone.ds_seen(args.ds_seen)
738
        if args.cron:
Valentin Samir's avatar
Valentin Samir committed
739 740
            for zone in zones:
                zone.do_zsk()
741 742
                zone.do_ksk()
                zone.remove_deleted()
743
        if args.ds:
Valentin Samir's avatar
Valentin Samir committed
744 745
            for zone in zones:
                zone.ds()
746
        if args.key:
Valentin Samir's avatar
Valentin Samir committed
747
            for zone in zones:
748 749 750
                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)
751
            znl = max(znl, 9)
752 753 754 755
            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
Valentin Samir committed
756 757
        if not any([
            args.make, args.cron, args.ds, args.key, args.ds_seen, args.nsec3,
758
            args.show_config, args.key_table
Valentin Samir's avatar
Valentin Samir committed
759
        ]):
760
            parser.print_help()
761
    except (ValueError, IOError) as error:
762
        sys.exit("%s\n" % error)