dnssec_keys_management.py 26.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

Valentin Samir's avatar
Valentin Samir committed
43

44 45 46 47 48 49
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
50 51 52 53
def get_zones(zone_names=None):
    l = []
    if zone_names is None:
        for f in os.listdir(BASE):
54
            if os.path.isdir(os.path.join(BASE, f)) and not f.startswith('.'):
Valentin Samir's avatar
Valentin Samir committed
55 56 57 58 59 60
                l.append(Zone(f))
    else:
        for name in zone_names:
            l.append(Zone(name))
    return l

Valentin Samir's avatar
Valentin Samir committed
61

Valentin Samir's avatar
Valentin Samir committed
62
def settime(path, flag, date):
Valentin Samir's avatar
Valentin Samir committed
63
    """Set the time of the flag ``flag`` for the key at ``path`` to ``date``"""
Valentin Samir's avatar
Valentin Samir committed
64
    cmd = [
65
        DNSSEC_SETTIME,
Valentin Samir's avatar
Valentin Samir committed
66 67 68
        "-i", str(int(INTERVAL.total_seconds())),
        "-%s" % flag, date, path
    ]
69
    p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
70
    err = p.communicate()[1].decode()
Valentin Samir's avatar
Valentin Samir committed
71 72 73
    if p.returncode != 0:
        raise ValueError("err %s: %s" % (p.returncode, err))
    if err:
74
        print("%s" % err, file=sys.stderr)
Valentin Samir's avatar
Valentin Samir committed
75

Valentin Samir's avatar
Valentin Samir committed
76

Valentin Samir's avatar
Valentin Samir committed
77
def bind_chown(path):
78 79 80 81 82 83 84 85 86 87 88 89
    """
        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:
90
        print("User bind not found, failing to give keys ownership to bind", file=sys.stderr)
Valentin Samir's avatar
Valentin Samir committed
91

Valentin Samir's avatar
Valentin Samir committed
92

Valentin Samir's avatar
Valentin Samir committed
93
def bind_reload():
Valentin Samir's avatar
Valentin Samir committed
94
    """Reload bind config"""
95
    cmd = [RNDC, "reload"]
Valentin Samir's avatar
Valentin Samir committed
96 97 98
    p = subprocess.Popen(cmd)
    p.wait()

Valentin Samir's avatar
Valentin Samir committed
99

100
def nsec3(zone, salt="-"):
Valentin Samir's avatar
Valentin Samir committed
101
    """Enable nsec3 for the zone ``zone``"""
102
    cmd = [RNDC, "signing", "-nsec3param", "1", "0", "10", salt, zone]
103
    print("Enabling nsec3 for zone %s: " % zone, file=sys.stdout)
104
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
105 106
    out = p.communicate()[0].decode()
    print(out, file=sys.stdout)
107 108 109
    p.wait()


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

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

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

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

219
    def key(self, show_ksk=False, show_zsk=False):
Valentin Samir's avatar
Valentin Samir committed
220
        """Displays the public keys of the KSK and/or ZSK"""
221 222
        if show_ksk:
            for ksk in self.KSK:
223
                print(ksk)
224 225
        if show_zsk:
            for zsk in self.ZSK:
226
                print(zsk)
227 228 229

    @staticmethod
    def _key_table_format(znl, show_creation=False):
Valentin Samir's avatar
Valentin Samir committed
230
        format_string = "|{!s:^%d}|{}|{!s:>5}|" % znl
231 232 233
        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
234
        separator = ("+" + "-" * znl + "+-+-----+" + ("-" * 19 + "+") * (5 if show_creation else 4))
235 236 237 238 239
        return (format_string, separator)

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

    def _key_table_body(self, znl, show_creation=False):
        (format_string, separator) = self._key_table_format(znl, show_creation)
        for ksk in self.KSK:
250
            print(format_string.format(
251 252
                ksk.zone_name,
                "K",
Valentin Samir's avatar
Valentin Samir committed
253
                ksk.keyid,
254 255 256 257 258 259 260
                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:
261
            print(format_string.format(
262 263
                zsk.zone_name,
                "Z",
Valentin Samir's avatar
Valentin Samir committed
264
                zsk.keyid,
265 266 267 268 269 270 271 272 273 274
                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)
275
        print(separator)
276 277

    def key_table(self, show_creation=False):
Valentin Samir's avatar
Valentin Samir committed
278
        """Show meta data for the zone keys in a table"""
279
        znl = max(len(self.name), 9)
280 281 282
        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
283 284 285 286

    def __init__(self, name):
        path = os.path.join(BASE, name)
        if not os.path.isdir(path):
Valentin Samir's avatar
Valentin Samir committed
287
            raise ValueError("%s is not a directory" % path)
Valentin Samir's avatar
Valentin Samir committed
288 289 290 291 292 293
        self.name = name
        self._path = path
        self.ZSK = []
        self.KSK = []
        for file in os.listdir(path):
            file_path = os.path.join(path, file)
294
            if os.path.isfile(file_path) and file_path.endswith(".private"):
Valentin Samir's avatar
Valentin Samir committed
295 296 297 298 299 300 301 302 303
                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:
304
                    print("%s" % error, sys.stderr)
Valentin Samir's avatar
Valentin Samir committed
305 306 307 308
        self.ZSK.sort()
        self.KSK.sort()
        if not self.ZSK:
            self.ZSK.append(Key.create("ZSK", name))
309
            self.do_zsk()
Valentin Samir's avatar
Valentin Samir committed
310 311
        if not self.KSK:
            self.KSK.append(Key.create("KSK", name))
312
            self.do_ksk()
Valentin Samir's avatar
Valentin Samir committed
313

Valentin Samir's avatar
Valentin Samir committed
314

Valentin Samir's avatar
Valentin Samir committed
315 316 317 318 319 320 321 322 323 324 325
@total_ordering
class Key(object):
    _created = None
    _publish = None
    _activate = None
    _inactive = None
    _delete = None
    _data = None
    _path = None
    type = None
    keyid = None
326 327
    flag = None
    zone_name = None
Valentin Samir's avatar
Valentin Samir committed
328 329 330 331 332 333 334

    def __str__(self):
        return self._data

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

Valentin Samir's avatar
Valentin Samir committed
336 337 338
    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
339

Valentin Samir's avatar
Valentin Samir committed
340
    def _date_to_key(self, date):
341 342 343 344
        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
345

346 347 348 349 350 351 352 353 354
    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
355
                    )
356 357 358 359
                )

    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
360 361 362 363 364
            value_name,
            needed_date_name,
            self.keyid,
            self.zone_name
        )
365 366 367 368 369 370
        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
371
    @classmethod
372 373 374
    def create(cls, typ, name, options=None):
        if options is None:
            options = []
Valentin Samir's avatar
Valentin Samir committed
375
        path = os.path.join(BASE, name)
376
        cmd = [DNSSEC_KEYGEN, "-a", "RSASHA256"]
Valentin Samir's avatar
Valentin Samir committed
377
        if typ == "KSK":
378
            cmd.extend(["-b", "2048", "-f", "KSK"])
Valentin Samir's avatar
Valentin Samir committed
379
        elif typ == "ZSK":
380
            cmd.extend(["-b", "1024"])
381 382
        else:
            raise ValueError("typ must be KSK or ZSK")
383 384
        cmd.extend(options)
        cmd.extend(["-K", path,  name])
Valentin Samir's avatar
Valentin Samir committed
385 386 387
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
Valentin Samir's avatar
Valentin Samir committed
388
            raise ValueError("The key creation has failed")
389
        keyname = p.communicate()[0].strip().decode()
Valentin Samir's avatar
Valentin Samir committed
390
        bind_chown(path)
391
        return cls(os.path.join(path, "%s.private" % keyname))
Valentin Samir's avatar
Valentin Samir committed
392

Valentin Samir's avatar
Valentin Samir committed
393
    def gen_successor(self):
Valentin Samir's avatar
Valentin Samir committed
394
        cmd = [
395
            DNSSEC_KEYGEN, "-i", str(int(INTERVAL.total_seconds())),
Valentin Samir's avatar
Valentin Samir committed
396 397
            "-S", self._path, "-K", os.path.dirname(self._path)
        ]
Valentin Samir's avatar
Valentin Samir committed
398
        p = subprocess.Popen(cmd, stderr=subprocess.PIPE)
399
        err = p.communicate()[1].decode()
Valentin Samir's avatar
Valentin Samir committed
400 401 402
        if p.returncode != 0:
            raise ValueError("err %s: %s" % (p.returncode, err))
        if err:
403
            print(err)
Valentin Samir's avatar
Valentin Samir committed
404 405 406 407
        bind_chown(os.path.dirname(self._path))

    @property
    def created(self):
408 409
        if self._created is not None:
            return self._date_from_key(self._created)
Valentin Samir's avatar
Valentin Samir committed
410 411 412

    @property
    def publish(self):
413 414
        if self._publish is not None:
            return self._date_from_key(self._publish)
Valentin Samir's avatar
Valentin Samir committed
415

Valentin Samir's avatar
Valentin Samir committed
416 417
    @publish.setter
    def publish(self, value):
418 419
        self._date_check(value, self.created, "publish", "created")
        self._date_check2(value, self.activate, "publish", "activate")
Valentin Samir's avatar
Valentin Samir committed
420 421 422 423
        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
424
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
425 426 427 428
                self._data = f.read()

    @property
    def activate(self):
429 430
        if self._activate is not None:
            return self._date_from_key(self._activate)
Valentin Samir's avatar
Valentin Samir committed
431

Valentin Samir's avatar
Valentin Samir committed
432 433
    @activate.setter
    def activate(self, value):
434 435
        self._date_check(value, self.publish, "active", "publish")
        self._date_check2(value, self.inactive, "activate", "inactive")
Valentin Samir's avatar
Valentin Samir committed
436 437 438 439
        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
440
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
441 442 443 444
                self._data = f.read()

    @property
    def inactive(self):
445 446
        if self._inactive is not None:
            return self._date_from_key(self._inactive)
Valentin Samir's avatar
Valentin Samir committed
447

Valentin Samir's avatar
Valentin Samir committed
448 449
    @inactive.setter
    def inactive(self, value):
450 451
        self._date_check(value, self.activate, "inactive", "activate")
        self._date_check2(value, self.delete, "inactive", "delete")
Valentin Samir's avatar
Valentin Samir committed
452 453 454 455
        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
456
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
457 458 459 460
                self._data = f.read()

    @property
    def delete(self):
461 462
        if self._delete:
            return self._date_from_key(self._delete)
Valentin Samir's avatar
Valentin Samir committed
463

Valentin Samir's avatar
Valentin Samir committed
464 465
    @delete.setter
    def delete(self, value):
466
        self._date_check(value, self.inactive, "delete", "inactive")
Valentin Samir's avatar
Valentin Samir committed
467 468 469 470
        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
471
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
472 473
                self._data = f.read()

474 475 476
    @property
    def is_publish(self):
        return self.publish is not None and self.publish <= datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
477

478 479 480
    @property
    def is_activate(self):
        return self.activate is not None and self.activate <= datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
481

482 483 484
    @property
    def is_inactive(self):
        return self.inactive is not None and self.inactive <= datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
485

486 487 488 489 490 491 492
    @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":
493 494 495 496
            return (
                self.activate is not None and
                (self.activate + KSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
            )
497
        elif self.type == "ZSK":
498 499 500 501
            return (
                self.activate is not None
                and (self.activate + ZSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
            )
502 503 504
        else:
            raise RuntimeError("impossible")

Valentin Samir's avatar
Valentin Samir committed
505
    def __init__(self, path):
506
        if not path.endswith(".private"):
Valentin Samir's avatar
Valentin Samir committed
507
            raise ValueError("%s is not a valid private key (should ends with .private)" % path)
508
        if not os.path.isfile(path):
Valentin Samir's avatar
Valentin Samir committed
509
            raise ValueError("%s do not exists" % path)
510 511
        ppath = "%s.key" % path[:-8]
        if not os.path.isfile(ppath):
Valentin Samir's avatar
Valentin Samir committed
512
            raise ValueError("The public key (%s) of %s does not exist" % (ppath, path))
513
        with open(ppath, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
514
            self._data = f.read()
515 516 517 518 519 520 521 522
        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
523
                raise ValueError(
Valentin Samir's avatar
Valentin Samir committed
524
                    "The public key %s should have at least 7 fields: %r" % (ppath, line)
Valentin Samir's avatar
Valentin Samir committed
525
                )
526
            if not line[0].endswith('.'):
Valentin Samir's avatar
Valentin Samir committed
527 528
                raise ValueError(
                    (
Valentin Samir's avatar
Valentin Samir committed
529
                        "The public key %s should begin with the zone fqdn (ending with a .)"
Valentin Samir's avatar
Valentin Samir committed
530 531
                    ) % ppath
                )
532 533 534 535
            self.zone_name = line[0][:-1]
            try:
                self.flag = int(line[3])
            except ValueError:
Valentin Samir's avatar
Valentin Samir committed
536
                raise ValueError(
Valentin Samir's avatar
Valentin Samir committed
537
                    "The flag %s of the public key %s should be an integer" % (line[3], ppath)
Valentin Samir's avatar
Valentin Samir committed
538
                )
539
        if self.flag == 256:
Valentin Samir's avatar
Valentin Samir committed
540
            self.type = "ZSK"
541
        elif self.flag == 257:
Valentin Samir's avatar
Valentin Samir committed
542 543
            self.type = "KSK"
        else:
Valentin Samir's avatar
Valentin Samir committed
544 545 546 547 548 549
            raise ValueError(
                "%s is not a valid key: flag %s unknown (known ones are 256 and 257)" % (
                    ppath,
                    self.flag
                )
            )
550 551 552 553 554 555
        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
556
            raise ValueError("The keyid %s of the key %s should be an integer" % (keyid, path))
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
        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
574
            raise ValueError("The key %s must have as list its Created field defined" % path)
Valentin Samir's avatar
Valentin Samir committed
575 576

    def __lt__(self, y):
Valentin Samir's avatar
Valentin Samir committed
577 578 579 580 581 582 583 584
        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
585 586 587 588 589

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

if __name__ == '__main__':
Hamza Dely's avatar
Hamza Dely committed
590
    config_parser = configparser.ConfigParser()
591 592 593 594 595
    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"):
596 597 598
            if config_parser.has_option("dnssec", "base_directory"):
                BASE = config_parser.get("dnssec", "base_directory")
            if config_parser.has_option("dnssec", "interval"):
599
                try:
600
                    INTERVAL = datetime.timedelta(days=config_parser.getfloat("dnssec", "interval"))
601
                except ValueError:
602
                    print(
603 604
                        "Unable to convert the config parameter 'interval' to a float",
                        file=sys.stderr
605
                    )
606
            if config_parser.has_option("dnssec", "zsk_validity"):
607
                try:
608 609 610
                    ZSK_VALIDITY = datetime.timedelta(
                        days=config_parser.getfloat("dnssec", "zsk_validity")
                    )
611
                except ValueError:
612
                    print(
613 614
                        "Unable to convert the config parameter 'zsk_validity' to a float",
                        file=sys.stderr
615
                    )
616
            if config_parser.has_option("dnssec", "ksk_validity"):
617
                try:
618 619 620
                    KSK_VALIDITY = datetime.timedelta(
                        days=config_parser.getfloat("dnssec", "ksk_validity")
                    )
621
                except ValueError:
622
                    print(
623 624
                        "Unable to convert the config parameter 'ksk_validity' to a float",
                        file=sys.stderr
625 626 627
                    )

        if config_parser.has_section("path"):
628 629 630 631 632 633 634 635
            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")
636 637

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

Valentin Samir's avatar
Valentin Samir committed
641
    try:
642 643
        parser = argparse.ArgumentParser()
        parser.add_argument('zone', nargs='*', help='zone name')
Valentin Samir's avatar
Valentin Samir committed
644 645 646 647 648 649 650 651 652 653 654
        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(
655
            '--ds',
Valentin Samir's avatar
Valentin Samir committed
656
            action='store_true',
657
            help='Show KSK DS for each supplied zone or for all zones if no zone supplied'
Valentin Samir's avatar
Valentin Samir committed
658 659
        )
        parser.add_argument(
660 661
            '--key',
            nargs='?', const="all", type=str, choices=["all", "ksk", "zsk"],
Valentin Samir's avatar
Valentin Samir committed
662 663
            help='Show DNSKEY for each zone supplied zone or for all zones if no zone supplied'
        )
664
        parser.add_argument(
665 666 667
            '--key-table',
            nargs='?', const="default", type=str, choices=["default", "all_fields"],
            help='Show a table with all non deleted DNSKEY meaningful dates'
668
        )
Valentin Samir's avatar
Valentin Samir committed
669 670 671 672 673 674 675 676 677 678 679 680 681 682
        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'
        )
683 684 685 686 687 688
        parser.add_argument(
            '--show-config',
            action='store_true',
            help='Show the current configuration'
        )

689 690
        args = parser.parse_args()
        zones = args.zone
691
        if args.show_config:
692 693 694 695 696 697 698 699 700
            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)
701
        if args.make:
Valentin Samir's avatar
Valentin Samir committed
702 703 704
            for zone in zones:
                Zone.create(zone)
        zones = get_zones(zones if zones else None)
705 706
        if args.nsec3:
            for zone in zones:
707
                nsec3(zone.name, binascii.hexlify(os.urandom(24)).decode())
708 709
        if args.ds_seen:
            if len(zones) != 1:
710
                sys.exit("Please specify exactly ONE zone name\n")
711 712
            for zone in zones:
                zone.ds_seen(args.ds_seen)
713
        if args.cron:
Valentin Samir's avatar
Valentin Samir committed
714 715
            for zone in zones:
                zone.do_zsk()
716 717
                zone.do_ksk()
                zone.remove_deleted()
718
        if args.ds:
Valentin Samir's avatar
Valentin Samir committed
719 720
            for zone in zones:
                zone.ds()
721
        if args.key:
Valentin Samir's avatar
Valentin Samir committed
722
            for zone in zones:
723 724 725
                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)
726
            znl = max(znl, 9)
727 728 729 730
            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
731 732
        if not any([
            args.make, args.cron, args.ds, args.key, args.ds_seen, args.nsec3,
733
            args.show_config, args.key_table
Valentin Samir's avatar
Valentin Samir committed
734
        ]):
735
            parser.print_help()
736
    except (ValueError, IOError) as error:
737
        sys.exit("%s\n" % error)