/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2025-06-14 15:37:45 UTC
  • Revision ID: teddy@recompile.se-20250614153745-1labvcqq11fuijy4
debian/control: Require cryptsetup support in systemd

* debian/control (Package: mandos-client/Depends): Add dependency on
  "systemd-cryptsetup" (for newer systemd), or "systemd (<< 256-2)"
  for when cryptsetup support was included in the systemd package
  itself, or "sysvinit-core" for installations without systemd.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
# -*- mode: python; coding: utf-8 -*-
 
1
#!/usr/bin/python3 -bI
 
2
# -*- coding: utf-8; lexical-binding: t -*-
3
3
#
4
4
# Mandos server - give out binary blobs to connecting clients.
5
5
#
11
11
# "AvahiService" class, and some lines in "main".
12
12
#
13
13
# Everything else is
14
 
# Copyright © 2008-2016 Teddy Hogeborn
15
 
# Copyright © 2008-2016 Björn Påhlsson
16
 
#
17
 
# This program is free software: you can redistribute it and/or modify
18
 
# it under the terms of the GNU General Public License as published by
 
14
# Copyright © 2008-2022 Teddy Hogeborn
 
15
# Copyright © 2008-2022 Björn Påhlsson
 
16
#
 
17
# This file is part of Mandos.
 
18
#
 
19
# Mandos is free software: you can redistribute it and/or modify it
 
20
# under the terms of the GNU General Public License as published by
19
21
# the Free Software Foundation, either version 3 of the License, or
20
22
# (at your option) any later version.
21
23
#
22
 
#     This program is distributed in the hope that it will be useful,
23
 
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
24
#     Mandos is distributed in the hope that it will be useful, but
 
25
#     WITHOUT ANY WARRANTY; without even the implied warranty of
24
26
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25
27
#     GNU General Public License for more details.
26
28
#
27
29
# You should have received a copy of the GNU General Public License
28
 
# along with this program.  If not, see
29
 
# <http://www.gnu.org/licenses/>.
 
30
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
30
31
#
31
32
# Contact the authors at <mandos@recompile.se>.
32
33
#
33
 
 
34
34
from __future__ import (division, absolute_import, print_function,
35
35
                        unicode_literals)
36
36
 
39
39
except ImportError:
40
40
    pass
41
41
 
 
42
import sys
 
43
import unittest
 
44
import argparse
 
45
import logging
 
46
import os
42
47
try:
43
48
    import SocketServer as socketserver
44
49
except ImportError:
45
50
    import socketserver
46
51
import socket
47
 
import argparse
48
52
import datetime
49
53
import errno
50
54
try:
51
55
    import ConfigParser as configparser
52
56
except ImportError:
53
57
    import configparser
54
 
import sys
55
58
import re
56
 
import os
57
59
import signal
58
60
import subprocess
59
61
import atexit
60
62
import stat
61
 
import logging
62
63
import logging.handlers
63
64
import pwd
64
65
import contextlib
76
77
import itertools
77
78
import collections
78
79
import codecs
 
80
import random
 
81
import shlex
79
82
 
80
83
import dbus
81
84
import dbus.service
 
85
import gi
82
86
from gi.repository import GLib
83
87
from dbus.mainloop.glib import DBusGMainLoop
84
88
import ctypes
86
90
import xml.dom.minidom
87
91
import inspect
88
92
 
 
93
if sys.version_info.major == 2:
 
94
    __metaclass__ = type
 
95
    str = unicode
 
96
    input = raw_input
 
97
 
 
98
# Add collections.abc.Callable if it does not exist
 
99
try:
 
100
    collections.abc.Callable
 
101
except AttributeError:
 
102
    class abc:
 
103
        Callable = collections.Callable
 
104
    collections.abc = abc
 
105
    del abc
 
106
 
 
107
# Add shlex.quote if it does not exist
 
108
try:
 
109
    shlex.quote
 
110
except AttributeError:
 
111
    shlex.quote = re.escape
 
112
 
 
113
# Add os.set_inheritable if it does not exist
 
114
try:
 
115
    os.set_inheritable
 
116
except AttributeError:
 
117
    def set_inheritable(fd, inheritable):
 
118
        flags = fcntl.fcntl(fd, fcntl.F_GETFD)
 
119
        if inheritable and ((flags & fcntl.FD_CLOEXEC) != 0):
 
120
            fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~fcntl.FD_CLOEXEC)
 
121
        elif (not inheritable) and ((flags & fcntl.FD_CLOEXEC) == 0):
 
122
            fcntl.fcntl(fd, fcntl.F_SETFL, flags | fcntl.FD_CLOEXEC)
 
123
    os.set_inheritable = set_inheritable
 
124
    del set_inheritable
 
125
 
 
126
# Show warnings by default
 
127
if not sys.warnoptions:
 
128
    import warnings
 
129
    warnings.simplefilter("default")
 
130
 
89
131
# Try to find the value of SO_BINDTODEVICE:
90
132
try:
91
133
    # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
111
153
            # No value found
112
154
            SO_BINDTODEVICE = None
113
155
 
114
 
if sys.version_info.major == 2:
115
 
    str = unicode
 
156
if sys.version_info < (3, 2):
 
157
    configparser.Configparser = configparser.SafeConfigParser
116
158
 
117
 
version = "1.7.14"
 
159
version = "1.8.18"
118
160
stored_state_file = "clients.pickle"
119
161
 
120
 
logger = logging.getLogger()
 
162
log = logging.getLogger(os.path.basename(sys.argv[0]))
 
163
logging.captureWarnings(True)   # Show warnings via the logging system
121
164
syslogger = None
122
165
 
123
166
try:
159
202
        facility=logging.handlers.SysLogHandler.LOG_DAEMON,
160
203
        address="/dev/log"))
161
204
    syslogger.setFormatter(logging.Formatter
162
 
                           ('Mandos [%(process)d]: %(levelname)s:'
163
 
                            ' %(message)s'))
164
 
    logger.addHandler(syslogger)
 
205
                           ("Mandos [%(process)d]: %(levelname)s:"
 
206
                            " %(message)s"))
 
207
    log.addHandler(syslogger)
165
208
 
166
209
    if debug:
167
210
        console = logging.StreamHandler()
168
 
        console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
169
 
                                               ' [%(process)d]:'
170
 
                                               ' %(levelname)s:'
171
 
                                               ' %(message)s'))
172
 
        logger.addHandler(console)
173
 
    logger.setLevel(level)
 
211
        console.setFormatter(logging.Formatter("%(asctime)s %(name)s"
 
212
                                               " [%(process)d]:"
 
213
                                               " %(levelname)s:"
 
214
                                               " %(message)s"))
 
215
        log.addHandler(console)
 
216
    log.setLevel(level)
174
217
 
175
218
 
176
219
class PGPError(Exception):
178
221
    pass
179
222
 
180
223
 
181
 
class PGPEngine(object):
 
224
class PGPEngine:
182
225
    """A simple class for OpenPGP symmetric encryption & decryption"""
183
226
 
184
227
    def __init__(self):
188
231
            output = subprocess.check_output(["gpgconf"])
189
232
            for line in output.splitlines():
190
233
                name, text, path = line.split(b":")
191
 
                if name == "gpg":
 
234
                if name == b"gpg":
192
235
                    self.gpg = path
193
236
                    break
194
237
        except OSError as e:
195
238
            if e.errno != errno.ENOENT:
196
239
                raise
197
 
        self.gnupgargs = ['--batch',
198
 
                          '--homedir', self.tempdir,
199
 
                          '--force-mdc',
200
 
                          '--quiet']
 
240
        self.gnupgargs = ["--batch",
 
241
                          "--homedir", self.tempdir,
 
242
                          "--force-mdc",
 
243
                          "--quiet"]
201
244
        # Only GPG version 1 has the --no-use-agent option.
202
 
        if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
 
245
        if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
203
246
            self.gnupgargs.append("--no-use-agent")
204
247
 
205
248
    def __enter__(self):
242
285
                dir=self.tempdir) as passfile:
243
286
            passfile.write(passphrase)
244
287
            passfile.flush()
245
 
            proc = subprocess.Popen([self.gpg, '--symmetric',
246
 
                                     '--passphrase-file',
 
288
            proc = subprocess.Popen([self.gpg, "--symmetric",
 
289
                                     "--passphrase-file",
247
290
                                     passfile.name]
248
291
                                    + self.gnupgargs,
249
292
                                    stdin=subprocess.PIPE,
260
303
                dir=self.tempdir) as passfile:
261
304
            passfile.write(passphrase)
262
305
            passfile.flush()
263
 
            proc = subprocess.Popen([self.gpg, '--decrypt',
264
 
                                     '--passphrase-file',
 
306
            proc = subprocess.Popen([self.gpg, "--decrypt",
 
307
                                     "--passphrase-file",
265
308
                                     passfile.name]
266
309
                                    + self.gnupgargs,
267
310
                                    stdin=subprocess.PIPE,
274
317
 
275
318
 
276
319
# Pretend that we have an Avahi module
277
 
class Avahi(object):
278
 
    """This isn't so much a class as it is a module-like namespace.
279
 
    It is instantiated once, and simulates having an Avahi module."""
 
320
class avahi:
 
321
    """This isn't so much a class as it is a module-like namespace."""
280
322
    IF_UNSPEC = -1               # avahi-common/address.h
281
323
    PROTO_UNSPEC = -1            # avahi-common/address.h
282
324
    PROTO_INET = 0               # avahi-common/address.h
286
328
    DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
287
329
    DBUS_PATH_SERVER = "/"
288
330
 
289
 
    def string_array_to_txt_array(self, t):
 
331
    @staticmethod
 
332
    def string_array_to_txt_array(t):
290
333
        return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
291
334
                           for s in t), signature="ay")
292
335
    ENTRY_GROUP_ESTABLISHED = 2  # avahi-common/defs.h
297
340
    SERVER_RUNNING = 2           # avahi-common/defs.h
298
341
    SERVER_COLLISION = 3         # avahi-common/defs.h
299
342
    SERVER_FAILURE = 4           # avahi-common/defs.h
300
 
avahi = Avahi()
301
343
 
302
344
 
303
345
class AvahiError(Exception):
315
357
    pass
316
358
 
317
359
 
318
 
class AvahiService(object):
 
360
class AvahiService:
319
361
    """An Avahi (Zeroconf) service.
320
362
 
321
363
    Attributes:
322
364
    interface: integer; avahi.IF_UNSPEC or an interface index.
323
365
               Used to optionally bind to the specified interface.
324
 
    name: string; Example: 'Mandos'
325
 
    type: string; Example: '_mandos._tcp'.
 
366
    name: string; Example: "Mandos"
 
367
    type: string; Example: "_mandos._tcp".
326
368
     See <https://www.iana.org/assignments/service-names-port-numbers>
327
369
    port: integer; what port to announce
328
370
    TXT: list of strings; TXT record for the service
365
407
    def rename(self, remove=True):
366
408
        """Derived from the Avahi example code"""
367
409
        if self.rename_count >= self.max_renames:
368
 
            logger.critical("No suitable Zeroconf service name found"
369
 
                            " after %i retries, exiting.",
370
 
                            self.rename_count)
 
410
            log.critical("No suitable Zeroconf service name found"
 
411
                         " after %i retries, exiting.",
 
412
                         self.rename_count)
371
413
            raise AvahiServiceError("Too many renames")
372
414
        self.name = str(
373
415
            self.server.GetAlternativeServiceName(self.name))
374
416
        self.rename_count += 1
375
 
        logger.info("Changing Zeroconf service name to %r ...",
376
 
                    self.name)
 
417
        log.info("Changing Zeroconf service name to %r ...",
 
418
                 self.name)
377
419
        if remove:
378
420
            self.remove()
379
421
        try:
381
423
        except dbus.exceptions.DBusException as error:
382
424
            if (error.get_dbus_name()
383
425
                == "org.freedesktop.Avahi.CollisionError"):
384
 
                logger.info("Local Zeroconf service name collision.")
 
426
                log.info("Local Zeroconf service name collision.")
385
427
                return self.rename(remove=False)
386
428
            else:
387
 
                logger.critical("D-Bus Exception", exc_info=error)
 
429
                log.critical("D-Bus Exception", exc_info=error)
388
430
                self.cleanup()
389
431
                os._exit(1)
390
432
 
406
448
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
407
449
        self.entry_group_state_changed_match = (
408
450
            self.group.connect_to_signal(
409
 
                'StateChanged', self.entry_group_state_changed))
410
 
        logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
411
 
                     self.name, self.type)
 
451
                "StateChanged", self.entry_group_state_changed))
 
452
        log.debug("Adding Zeroconf service '%s' of type '%s' ...",
 
453
                  self.name, self.type)
412
454
        self.group.AddService(
413
455
            self.interface,
414
456
            self.protocol,
421
463
 
422
464
    def entry_group_state_changed(self, state, error):
423
465
        """Derived from the Avahi example code"""
424
 
        logger.debug("Avahi entry group state change: %i", state)
 
466
        log.debug("Avahi entry group state change: %i", state)
425
467
 
426
468
        if state == avahi.ENTRY_GROUP_ESTABLISHED:
427
 
            logger.debug("Zeroconf service established.")
 
469
            log.debug("Zeroconf service established.")
428
470
        elif state == avahi.ENTRY_GROUP_COLLISION:
429
 
            logger.info("Zeroconf service name collision.")
 
471
            log.info("Zeroconf service name collision.")
430
472
            self.rename()
431
473
        elif state == avahi.ENTRY_GROUP_FAILURE:
432
 
            logger.critical("Avahi: Error in group state changed %s",
433
 
                            str(error))
 
474
            log.critical("Avahi: Error in group state changed %s",
 
475
                         str(error))
434
476
            raise AvahiGroupError("State changed: {!s}".format(error))
435
477
 
436
478
    def cleanup(self):
446
488
 
447
489
    def server_state_changed(self, state, error=None):
448
490
        """Derived from the Avahi example code"""
449
 
        logger.debug("Avahi server state change: %i", state)
 
491
        log.debug("Avahi server state change: %i", state)
450
492
        bad_states = {
451
493
            avahi.SERVER_INVALID: "Zeroconf server invalid",
452
494
            avahi.SERVER_REGISTERING: None,
456
498
        if state in bad_states:
457
499
            if bad_states[state] is not None:
458
500
                if error is None:
459
 
                    logger.error(bad_states[state])
 
501
                    log.error(bad_states[state])
460
502
                else:
461
 
                    logger.error(bad_states[state] + ": %r", error)
 
503
                    log.error(bad_states[state] + ": %r", error)
462
504
            self.cleanup()
463
505
        elif state == avahi.SERVER_RUNNING:
464
506
            try:
466
508
            except dbus.exceptions.DBusException as error:
467
509
                if (error.get_dbus_name()
468
510
                    == "org.freedesktop.Avahi.CollisionError"):
469
 
                    logger.info("Local Zeroconf service name"
470
 
                                " collision.")
 
511
                    log.info("Local Zeroconf service name collision.")
471
512
                    return self.rename(remove=False)
472
513
                else:
473
 
                    logger.critical("D-Bus Exception", exc_info=error)
 
514
                    log.critical("D-Bus Exception", exc_info=error)
474
515
                    self.cleanup()
475
516
                    os._exit(1)
476
517
        else:
477
518
            if error is None:
478
 
                logger.debug("Unknown state: %r", state)
 
519
                log.debug("Unknown state: %r", state)
479
520
            else:
480
 
                logger.debug("Unknown state: %r: %r", state, error)
 
521
                log.debug("Unknown state: %r: %r", state, error)
481
522
 
482
523
    def activate(self):
483
524
        """Derived from the Avahi example code"""
495
536
class AvahiServiceToSyslog(AvahiService):
496
537
    def rename(self, *args, **kwargs):
497
538
        """Add the new name to the syslog messages"""
498
 
        ret = AvahiService.rename(self, *args, **kwargs)
 
539
        ret = super(AvahiServiceToSyslog, self).rename(*args,
 
540
                                                       **kwargs)
499
541
        syslogger.setFormatter(logging.Formatter(
500
 
            'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
 
542
            "Mandos ({}) [%(process)d]: %(levelname)s: %(message)s"
501
543
            .format(self.name)))
502
544
        return ret
503
545
 
504
546
 
505
547
# Pretend that we have a GnuTLS module
506
 
class GnuTLS(object):
507
 
    """This isn't so much a class as it is a module-like namespace.
508
 
    It is instantiated once, and simulates having a GnuTLS module."""
 
548
class gnutls:
 
549
    """This isn't so much a class as it is a module-like namespace."""
509
550
 
510
551
    library = ctypes.util.find_library("gnutls")
511
552
    if library is None:
512
553
        library = ctypes.util.find_library("gnutls-deb0")
513
554
    _library = ctypes.cdll.LoadLibrary(library)
514
555
    del library
515
 
    _need_version = b"3.3.0"
516
 
 
517
 
    def __init__(self):
518
 
        # Need to use "self" here, since this method is called before
519
 
        # the assignment to the "gnutls" global variable happens.
520
 
        if self.check_version(self._need_version) is None:
521
 
            raise self.Error("Needs GnuTLS {} or later"
522
 
                             .format(self._need_version))
523
556
 
524
557
    # Unless otherwise indicated, the constants and types below are
525
558
    # all from the gnutls/gnutls.h C header file.
529
562
    E_INTERRUPTED = -52
530
563
    E_AGAIN = -28
531
564
    CRT_OPENPGP = 2
 
565
    CRT_RAWPK = 3
532
566
    CLIENT = 2
533
567
    SHUT_RDWR = 0
534
568
    CRD_CERTIFICATE = 1
535
569
    E_NO_CERTIFICATE_FOUND = -49
 
570
    X509_FMT_DER = 0
 
571
    NO_TICKETS = 1<<10
 
572
    ENABLE_RAWPK = 1<<18
 
573
    CTYPE_PEERS = 3
 
574
    KEYID_USE_SHA256 = 1        # gnutls/x509.h
536
575
    OPENPGP_FMT_RAW = 0         # gnutls/openpgp.h
537
576
 
538
577
    # Types
539
 
    class session_int(ctypes.Structure):
 
578
    class _session_int(ctypes.Structure):
540
579
        _fields_ = []
541
 
    session_t = ctypes.POINTER(session_int)
 
580
    session_t = ctypes.POINTER(_session_int)
542
581
 
543
582
    class certificate_credentials_st(ctypes.Structure):
544
583
        _fields_ = []
547
586
    certificate_type_t = ctypes.c_int
548
587
 
549
588
    class datum_t(ctypes.Structure):
550
 
        _fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
551
 
                    ('size', ctypes.c_uint)]
 
589
        _fields_ = [("data", ctypes.POINTER(ctypes.c_ubyte)),
 
590
                    ("size", ctypes.c_uint)]
552
591
 
553
 
    class openpgp_crt_int(ctypes.Structure):
 
592
    class _openpgp_crt_int(ctypes.Structure):
554
593
        _fields_ = []
555
 
    openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
 
594
    openpgp_crt_t = ctypes.POINTER(_openpgp_crt_int)
556
595
    openpgp_crt_fmt_t = ctypes.c_int  # gnutls/openpgp.h
557
596
    log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
558
597
    credentials_type_t = ctypes.c_int
561
600
 
562
601
    # Exceptions
563
602
    class Error(Exception):
564
 
        # We need to use the class name "GnuTLS" here, since this
565
 
        # exception might be raised from within GnuTLS.__init__,
566
 
        # which is called before the assignment to the "gnutls"
567
 
        # global variable has happened.
568
603
        def __init__(self, message=None, code=None, args=()):
569
604
            # Default usage is by a message string, but if a return
570
605
            # code is passed, convert it to a string with
571
606
            # gnutls.strerror()
572
607
            self.code = code
573
608
            if message is None and code is not None:
574
 
                message = GnuTLS.strerror(code)
575
 
            return super(GnuTLS.Error, self).__init__(
 
609
                message = gnutls.strerror(code).decode(
 
610
                    "utf-8", errors="replace")
 
611
            return super(gnutls.Error, self).__init__(
576
612
                message, *args)
577
613
 
578
614
    class CertificateSecurityError(Error):
579
615
        pass
580
616
 
 
617
    class PointerTo:
 
618
        def __init__(self, cls):
 
619
            self.cls = cls
 
620
 
 
621
        def from_param(self, obj):
 
622
            if not isinstance(obj, self.cls):
 
623
                raise TypeError("Not of type {}: {!r}"
 
624
                                .format(self.cls.__name__, obj))
 
625
            return ctypes.byref(obj.from_param(obj))
 
626
 
 
627
    class CastToVoidPointer:
 
628
        def __init__(self, cls):
 
629
            self.cls = cls
 
630
 
 
631
        def from_param(self, obj):
 
632
            if not isinstance(obj, self.cls):
 
633
                raise TypeError("Not of type {}: {!r}"
 
634
                                .format(self.cls.__name__, obj))
 
635
            return ctypes.cast(obj.from_param(obj), ctypes.c_void_p)
 
636
 
 
637
    class With_from_param:
 
638
        @classmethod
 
639
        def from_param(cls, obj):
 
640
            return obj._as_parameter_
 
641
 
581
642
    # Classes
582
 
    class Credentials(object):
 
643
    class Credentials(With_from_param):
583
644
        def __init__(self):
584
 
            self._c_object = gnutls.certificate_credentials_t()
585
 
            gnutls.certificate_allocate_credentials(
586
 
                ctypes.byref(self._c_object))
 
645
            self._as_parameter_ = gnutls.certificate_credentials_t()
 
646
            gnutls.certificate_allocate_credentials(self)
587
647
            self.type = gnutls.CRD_CERTIFICATE
588
648
 
589
649
        def __del__(self):
590
 
            gnutls.certificate_free_credentials(self._c_object)
 
650
            gnutls.certificate_free_credentials(self)
591
651
 
592
 
    class ClientSession(object):
 
652
    class ClientSession(With_from_param):
593
653
        def __init__(self, socket, credentials=None):
594
 
            self._c_object = gnutls.session_t()
595
 
            gnutls.init(ctypes.byref(self._c_object), gnutls.CLIENT)
596
 
            gnutls.set_default_priority(self._c_object)
597
 
            gnutls.transport_set_ptr(self._c_object, socket.fileno())
598
 
            gnutls.handshake_set_private_extensions(self._c_object,
599
 
                                                    True)
 
654
            self._as_parameter_ = gnutls.session_t()
 
655
            gnutls_flags = gnutls.CLIENT
 
656
            if gnutls.check_version(b"3.5.6"):
 
657
                gnutls_flags |= gnutls.NO_TICKETS
 
658
            if gnutls.has_rawpk:
 
659
                gnutls_flags |= gnutls.ENABLE_RAWPK
 
660
            gnutls.init(self, gnutls_flags)
 
661
            del gnutls_flags
 
662
            gnutls.set_default_priority(self)
 
663
            gnutls.transport_set_ptr(self, socket.fileno())
 
664
            gnutls.handshake_set_private_extensions(self, True)
600
665
            self.socket = socket
601
666
            if credentials is None:
602
667
                credentials = gnutls.Credentials()
603
 
            gnutls.credentials_set(self._c_object, credentials.type,
604
 
                                   ctypes.cast(credentials._c_object,
605
 
                                               ctypes.c_void_p))
 
668
            gnutls.credentials_set(self, credentials.type,
 
669
                                   credentials)
606
670
            self.credentials = credentials
607
671
 
608
672
        def __del__(self):
609
 
            gnutls.deinit(self._c_object)
 
673
            gnutls.deinit(self)
610
674
 
611
675
        def handshake(self):
612
 
            return gnutls.handshake(self._c_object)
 
676
            return gnutls.handshake(self)
613
677
 
614
678
        def send(self, data):
615
679
            data = bytes(data)
616
680
            data_len = len(data)
617
681
            while data_len > 0:
618
 
                data_len -= gnutls.record_send(self._c_object,
619
 
                                               data[-data_len:],
 
682
                data_len -= gnutls.record_send(self, data[-data_len:],
620
683
                                               data_len)
621
684
 
622
685
        def bye(self):
623
 
            return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
 
686
            return gnutls.bye(self, gnutls.SHUT_RDWR)
624
687
 
625
688
    # Error handling functions
626
689
    def _error_code(result):
627
690
        """A function to raise exceptions on errors, suitable
628
 
        for the 'restype' attribute on ctypes functions"""
629
 
        if result >= 0:
 
691
        for the "restype" attribute on ctypes functions"""
 
692
        if result >= gnutls.E_SUCCESS:
630
693
            return result
631
694
        if result == gnutls.E_NO_CERTIFICATE_FOUND:
632
695
            raise gnutls.CertificateSecurityError(code=result)
633
696
        raise gnutls.Error(code=result)
634
697
 
635
 
    def _retry_on_error(result, func, arguments):
 
698
    def _retry_on_error(result, func, arguments,
 
699
                        _error_code=_error_code):
636
700
        """A function to retry on some errors, suitable
637
 
        for the 'errcheck' attribute on ctypes functions"""
638
 
        while result < 0:
 
701
        for the "errcheck" attribute on ctypes functions"""
 
702
        while result < gnutls.E_SUCCESS:
639
703
            if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
640
704
                return _error_code(result)
641
705
            result = func(*arguments)
646
710
 
647
711
    # Functions
648
712
    priority_set_direct = _library.gnutls_priority_set_direct
649
 
    priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
 
713
    priority_set_direct.argtypes = [ClientSession, ctypes.c_char_p,
650
714
                                    ctypes.POINTER(ctypes.c_char_p)]
651
715
    priority_set_direct.restype = _error_code
652
716
 
653
717
    init = _library.gnutls_init
654
 
    init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
 
718
    init.argtypes = [PointerTo(ClientSession), ctypes.c_int]
655
719
    init.restype = _error_code
656
720
 
657
721
    set_default_priority = _library.gnutls_set_default_priority
658
 
    set_default_priority.argtypes = [session_t]
 
722
    set_default_priority.argtypes = [ClientSession]
659
723
    set_default_priority.restype = _error_code
660
724
 
661
725
    record_send = _library.gnutls_record_send
662
 
    record_send.argtypes = [session_t, ctypes.c_void_p,
 
726
    record_send.argtypes = [ClientSession, ctypes.c_void_p,
663
727
                            ctypes.c_size_t]
664
728
    record_send.restype = ctypes.c_ssize_t
665
729
    record_send.errcheck = _retry_on_error
667
731
    certificate_allocate_credentials = (
668
732
        _library.gnutls_certificate_allocate_credentials)
669
733
    certificate_allocate_credentials.argtypes = [
670
 
        ctypes.POINTER(certificate_credentials_t)]
 
734
        PointerTo(Credentials)]
671
735
    certificate_allocate_credentials.restype = _error_code
672
736
 
673
737
    certificate_free_credentials = (
674
738
        _library.gnutls_certificate_free_credentials)
675
 
    certificate_free_credentials.argtypes = [
676
 
        certificate_credentials_t]
 
739
    certificate_free_credentials.argtypes = [Credentials]
677
740
    certificate_free_credentials.restype = None
678
741
 
679
742
    handshake_set_private_extensions = (
680
743
        _library.gnutls_handshake_set_private_extensions)
681
 
    handshake_set_private_extensions.argtypes = [session_t,
 
744
    handshake_set_private_extensions.argtypes = [ClientSession,
682
745
                                                 ctypes.c_int]
683
746
    handshake_set_private_extensions.restype = None
684
747
 
685
748
    credentials_set = _library.gnutls_credentials_set
686
 
    credentials_set.argtypes = [session_t, credentials_type_t,
687
 
                                ctypes.c_void_p]
 
749
    credentials_set.argtypes = [ClientSession, credentials_type_t,
 
750
                                CastToVoidPointer(Credentials)]
688
751
    credentials_set.restype = _error_code
689
752
 
690
753
    strerror = _library.gnutls_strerror
692
755
    strerror.restype = ctypes.c_char_p
693
756
 
694
757
    certificate_type_get = _library.gnutls_certificate_type_get
695
 
    certificate_type_get.argtypes = [session_t]
 
758
    certificate_type_get.argtypes = [ClientSession]
696
759
    certificate_type_get.restype = _error_code
697
760
 
698
761
    certificate_get_peers = _library.gnutls_certificate_get_peers
699
 
    certificate_get_peers.argtypes = [session_t,
 
762
    certificate_get_peers.argtypes = [ClientSession,
700
763
                                      ctypes.POINTER(ctypes.c_uint)]
701
764
    certificate_get_peers.restype = ctypes.POINTER(datum_t)
702
765
 
709
772
    global_set_log_function.restype = None
710
773
 
711
774
    deinit = _library.gnutls_deinit
712
 
    deinit.argtypes = [session_t]
 
775
    deinit.argtypes = [ClientSession]
713
776
    deinit.restype = None
714
777
 
715
778
    handshake = _library.gnutls_handshake
716
 
    handshake.argtypes = [session_t]
717
 
    handshake.restype = _error_code
 
779
    handshake.argtypes = [ClientSession]
 
780
    handshake.restype = ctypes.c_int
718
781
    handshake.errcheck = _retry_on_error
719
782
 
720
783
    transport_set_ptr = _library.gnutls_transport_set_ptr
721
 
    transport_set_ptr.argtypes = [session_t, transport_ptr_t]
 
784
    transport_set_ptr.argtypes = [ClientSession, transport_ptr_t]
722
785
    transport_set_ptr.restype = None
723
786
 
724
787
    bye = _library.gnutls_bye
725
 
    bye.argtypes = [session_t, close_request_t]
726
 
    bye.restype = _error_code
 
788
    bye.argtypes = [ClientSession, close_request_t]
 
789
    bye.restype = ctypes.c_int
727
790
    bye.errcheck = _retry_on_error
728
791
 
729
792
    check_version = _library.gnutls_check_version
730
793
    check_version.argtypes = [ctypes.c_char_p]
731
794
    check_version.restype = ctypes.c_char_p
732
795
 
733
 
    # All the function declarations below are from gnutls/openpgp.h
734
 
 
735
 
    openpgp_crt_init = _library.gnutls_openpgp_crt_init
736
 
    openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
737
 
    openpgp_crt_init.restype = _error_code
738
 
 
739
 
    openpgp_crt_import = _library.gnutls_openpgp_crt_import
740
 
    openpgp_crt_import.argtypes = [openpgp_crt_t,
741
 
                                   ctypes.POINTER(datum_t),
742
 
                                   openpgp_crt_fmt_t]
743
 
    openpgp_crt_import.restype = _error_code
744
 
 
745
 
    openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
746
 
    openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
747
 
                                        ctypes.POINTER(ctypes.c_uint)]
748
 
    openpgp_crt_verify_self.restype = _error_code
749
 
 
750
 
    openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
751
 
    openpgp_crt_deinit.argtypes = [openpgp_crt_t]
752
 
    openpgp_crt_deinit.restype = None
753
 
 
754
 
    openpgp_crt_get_fingerprint = (
755
 
        _library.gnutls_openpgp_crt_get_fingerprint)
756
 
    openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
757
 
                                            ctypes.c_void_p,
758
 
                                            ctypes.POINTER(
759
 
                                                ctypes.c_size_t)]
760
 
    openpgp_crt_get_fingerprint.restype = _error_code
 
796
    _need_version = b"3.3.0"
 
797
    if check_version(_need_version) is None:
 
798
        raise self.Error("Needs GnuTLS {} or later"
 
799
                         .format(_need_version))
 
800
 
 
801
    _tls_rawpk_version = b"3.6.6"
 
802
    has_rawpk = bool(check_version(_tls_rawpk_version))
 
803
 
 
804
    if has_rawpk:
 
805
        # Types
 
806
        class pubkey_st(ctypes.Structure):
 
807
            _fields = []
 
808
        pubkey_t = ctypes.POINTER(pubkey_st)
 
809
 
 
810
        x509_crt_fmt_t = ctypes.c_int
 
811
 
 
812
        # All the function declarations below are from
 
813
        # gnutls/abstract.h
 
814
        pubkey_init = _library.gnutls_pubkey_init
 
815
        pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
 
816
        pubkey_init.restype = _error_code
 
817
 
 
818
        pubkey_import = _library.gnutls_pubkey_import
 
819
        pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
 
820
                                  x509_crt_fmt_t]
 
821
        pubkey_import.restype = _error_code
 
822
 
 
823
        pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
 
824
        pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
 
825
                                      ctypes.POINTER(ctypes.c_ubyte),
 
826
                                      ctypes.POINTER(ctypes.c_size_t)]
 
827
        pubkey_get_key_id.restype = _error_code
 
828
 
 
829
        pubkey_deinit = _library.gnutls_pubkey_deinit
 
830
        pubkey_deinit.argtypes = [pubkey_t]
 
831
        pubkey_deinit.restype = None
 
832
    else:
 
833
        # All the function declarations below are from
 
834
        # gnutls/openpgp.h
 
835
 
 
836
        openpgp_crt_init = _library.gnutls_openpgp_crt_init
 
837
        openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
 
838
        openpgp_crt_init.restype = _error_code
 
839
 
 
840
        openpgp_crt_import = _library.gnutls_openpgp_crt_import
 
841
        openpgp_crt_import.argtypes = [openpgp_crt_t,
 
842
                                       ctypes.POINTER(datum_t),
 
843
                                       openpgp_crt_fmt_t]
 
844
        openpgp_crt_import.restype = _error_code
 
845
 
 
846
        openpgp_crt_verify_self = \
 
847
            _library.gnutls_openpgp_crt_verify_self
 
848
        openpgp_crt_verify_self.argtypes = [
 
849
            openpgp_crt_t,
 
850
            ctypes.c_uint,
 
851
            ctypes.POINTER(ctypes.c_uint),
 
852
        ]
 
853
        openpgp_crt_verify_self.restype = _error_code
 
854
 
 
855
        openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
 
856
        openpgp_crt_deinit.argtypes = [openpgp_crt_t]
 
857
        openpgp_crt_deinit.restype = None
 
858
 
 
859
        openpgp_crt_get_fingerprint = (
 
860
            _library.gnutls_openpgp_crt_get_fingerprint)
 
861
        openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
 
862
                                                ctypes.c_void_p,
 
863
                                                ctypes.POINTER(
 
864
                                                    ctypes.c_size_t)]
 
865
        openpgp_crt_get_fingerprint.restype = _error_code
 
866
 
 
867
    if check_version(b"3.6.4"):
 
868
        certificate_type_get2 = _library.gnutls_certificate_type_get2
 
869
        certificate_type_get2.argtypes = [ClientSession, ctypes.c_int]
 
870
        certificate_type_get2.restype = _error_code
761
871
 
762
872
    # Remove non-public functions
763
873
    del _error_code, _retry_on_error
764
 
# Create the global "gnutls" object, simulating a module
765
 
gnutls = GnuTLS()
766
874
 
767
875
 
768
876
def call_pipe(connection,       # : multiprocessing.Connection
776
884
    connection.close()
777
885
 
778
886
 
779
 
class Client(object):
 
887
class Client:
780
888
    """A representation of a client host served by this server.
781
889
 
782
890
    Attributes:
783
 
    approved:   bool(); 'None' if not yet approved/disapproved
 
891
    approved:   bool(); None if not yet approved/disapproved
784
892
    approval_delay: datetime.timedelta(); Time to wait for approval
785
893
    approval_duration: datetime.timedelta(); Duration of one approval
786
 
    checker:    subprocess.Popen(); a running checker process used
787
 
                                    to see if the client lives.
788
 
                                    'None' if no process is running.
 
894
    checker: multiprocessing.Process(); a running checker process used
 
895
             to see if the client lives. None if no process is
 
896
             running.
789
897
    checker_callback_tag: a GLib event source tag, or None
790
898
    checker_command: string; External command which is run to check
791
899
                     if client lives.  %() expansions are done at
799
907
    disable_initiator_tag: a GLib event source tag, or None
800
908
    enabled:    bool()
801
909
    fingerprint: string (40 or 32 hexadecimal digits); used to
802
 
                 uniquely identify the client
 
910
                 uniquely identify an OpenPGP client
 
911
    key_id: string (64 hexadecimal digits); used to uniquely identify
 
912
            a client using raw public keys
803
913
    host:       string; available for use by the checker command
804
914
    interval:   datetime.timedelta(); How often to start a new checker
805
915
    last_approval_request: datetime.datetime(); (UTC) or None
823
933
    """
824
934
 
825
935
    runtime_expansions = ("approval_delay", "approval_duration",
826
 
                          "created", "enabled", "expires",
 
936
                          "created", "enabled", "expires", "key_id",
827
937
                          "fingerprint", "host", "interval",
828
938
                          "last_approval_request", "last_checked_ok",
829
939
                          "last_enabled", "name", "timeout")
859
969
            client["enabled"] = config.getboolean(client_name,
860
970
                                                  "enabled")
861
971
 
862
 
            # Uppercase and remove spaces from fingerprint for later
863
 
            # comparison purposes with return value from the
864
 
            # fingerprint() function
865
 
            client["fingerprint"] = (section["fingerprint"].upper()
 
972
            # Uppercase and remove spaces from key_id and fingerprint
 
973
            # for later comparison purposes with return value from the
 
974
            # key_id() and fingerprint() functions
 
975
            client["key_id"] = (section.get("key_id", "").upper()
 
976
                                .replace(" ", ""))
 
977
            client["fingerprint"] = (section.get("fingerprint",
 
978
                                                 "").upper()
866
979
                                     .replace(" ", ""))
 
980
            if not (client["key_id"] or client["fingerprint"]):
 
981
                log.error("Skipping client %s without key_id or"
 
982
                          " fingerprint", client_name)
 
983
                del settings[client_name]
 
984
                continue
867
985
            if "secret" in section:
868
986
                client["secret"] = codecs.decode(section["secret"]
869
987
                                                 .encode("utf-8"),
910
1028
            self.last_enabled = None
911
1029
            self.expires = None
912
1030
 
913
 
        logger.debug("Creating client %r", self.name)
914
 
        logger.debug("  Fingerprint: %s", self.fingerprint)
 
1031
        log.debug("Creating client %r", self.name)
 
1032
        log.debug("  Key ID: %s", self.key_id)
 
1033
        log.debug("  Fingerprint: %s", self.fingerprint)
915
1034
        self.created = settings.get("created",
916
1035
                                    datetime.datetime.utcnow())
917
1036
 
945
1064
        if getattr(self, "enabled", False):
946
1065
            # Already enabled
947
1066
            return
948
 
        self.expires = datetime.datetime.utcnow() + self.timeout
949
1067
        self.enabled = True
950
1068
        self.last_enabled = datetime.datetime.utcnow()
951
1069
        self.init_checker()
956
1074
        if not getattr(self, "enabled", False):
957
1075
            return False
958
1076
        if not quiet:
959
 
            logger.info("Disabling client %s", self.name)
 
1077
            log.info("Disabling client %s", self.name)
960
1078
        if getattr(self, "disable_initiator_tag", None) is not None:
961
1079
            GLib.source_remove(self.disable_initiator_tag)
962
1080
            self.disable_initiator_tag = None
974
1092
    def __del__(self):
975
1093
        self.disable()
976
1094
 
977
 
    def init_checker(self):
978
 
        # Schedule a new checker to be started an 'interval' from now,
979
 
        # and every interval from then on.
 
1095
    def init_checker(self, randomize_start=False):
 
1096
        # Schedule a new checker to be started a randomly selected
 
1097
        # time (a fraction of 'interval') from now.  This spreads out
 
1098
        # the startup of checkers over time when the server is
 
1099
        # started.
980
1100
        if self.checker_initiator_tag is not None:
981
1101
            GLib.source_remove(self.checker_initiator_tag)
 
1102
        interval_milliseconds = int(self.interval.total_seconds()
 
1103
                                    * 1000)
 
1104
        if randomize_start:
 
1105
            delay_milliseconds = random.randrange(
 
1106
                interval_milliseconds + 1)
 
1107
        else:
 
1108
            delay_milliseconds = interval_milliseconds
982
1109
        self.checker_initiator_tag = GLib.timeout_add(
983
 
            int(self.interval.total_seconds() * 1000),
984
 
            self.start_checker)
985
 
        # Schedule a disable() when 'timeout' has passed
 
1110
            delay_milliseconds, self.start_checker, randomize_start)
 
1111
        delay = datetime.timedelta(0, 0, 0, delay_milliseconds)
 
1112
        # A checker might take up to an 'interval' of time, so we can
 
1113
        # expire at the soonest one interval after a checker was
 
1114
        # started.  Since the initial checker is delayed, the expire
 
1115
        # time might have to be extended.
 
1116
        now = datetime.datetime.utcnow()
 
1117
        self.expires = now + delay + self.interval
 
1118
        # Schedule a disable() at expire time
986
1119
        if self.disable_initiator_tag is not None:
987
1120
            GLib.source_remove(self.disable_initiator_tag)
988
1121
        self.disable_initiator_tag = GLib.timeout_add(
989
 
            int(self.timeout.total_seconds() * 1000), self.disable)
990
 
        # Also start a new checker *right now*.
991
 
        self.start_checker()
 
1122
            int((self.expires - now).total_seconds() * 1000),
 
1123
            self.disable)
992
1124
 
993
1125
    def checker_callback(self, source, condition, connection,
994
1126
                         command):
995
1127
        """The checker has completed, so take appropriate actions."""
996
 
        self.checker_callback_tag = None
997
 
        self.checker = None
998
1128
        # Read return code from connection (see call_pipe)
999
1129
        returncode = connection.recv()
1000
1130
        connection.close()
 
1131
        if self.checker is not None:
 
1132
            self.checker.join()
 
1133
        self.checker_callback_tag = None
 
1134
        self.checker = None
1001
1135
 
1002
1136
        if returncode >= 0:
1003
1137
            self.last_checker_status = returncode
1004
1138
            self.last_checker_signal = None
1005
1139
            if self.last_checker_status == 0:
1006
 
                logger.info("Checker for %(name)s succeeded",
1007
 
                            vars(self))
 
1140
                log.info("Checker for %(name)s succeeded", vars(self))
1008
1141
                self.checked_ok()
1009
1142
            else:
1010
 
                logger.info("Checker for %(name)s failed", vars(self))
 
1143
                log.info("Checker for %(name)s failed", vars(self))
1011
1144
        else:
1012
1145
            self.last_checker_status = -1
1013
1146
            self.last_checker_signal = -returncode
1014
 
            logger.warning("Checker for %(name)s crashed?",
1015
 
                           vars(self))
 
1147
            log.warning("Checker for %(name)s crashed?", vars(self))
1016
1148
        return False
1017
1149
 
1018
1150
    def checked_ok(self):
1037
1169
    def need_approval(self):
1038
1170
        self.last_approval_request = datetime.datetime.utcnow()
1039
1171
 
1040
 
    def start_checker(self):
 
1172
    def start_checker(self, start_was_randomized=False):
1041
1173
        """Start a new checker subprocess if one is not running.
1042
1174
 
1043
1175
        If a checker already exists, leave it running and do
1052
1184
        # should be.
1053
1185
 
1054
1186
        if self.checker is not None and not self.checker.is_alive():
1055
 
            logger.warning("Checker was not alive; joining")
 
1187
            log.warning("Checker was not alive; joining")
1056
1188
            self.checker.join()
1057
1189
            self.checker = None
1058
1190
        # Start a new checker if needed
1059
1191
        if self.checker is None:
1060
1192
            # Escape attributes for the shell
1061
1193
            escaped_attrs = {
1062
 
                attr: re.escape(str(getattr(self, attr)))
 
1194
                attr: shlex.quote(str(getattr(self, attr)))
1063
1195
                for attr in self.runtime_expansions}
1064
1196
            try:
1065
1197
                command = self.checker_command % escaped_attrs
1066
1198
            except TypeError as error:
1067
 
                logger.error('Could not format string "%s"',
1068
 
                             self.checker_command,
1069
 
                             exc_info=error)
 
1199
                log.error('Could not format string "%s"',
 
1200
                          self.checker_command, exc_info=error)
1070
1201
                return True     # Try again later
1071
1202
            self.current_checker_command = command
1072
 
            logger.info("Starting checker %r for %s", command,
1073
 
                        self.name)
 
1203
            log.info("Starting checker %r for %s", command, self.name)
1074
1204
            # We don't need to redirect stdout and stderr, since
1075
1205
            # in normal mode, that is already done by daemon(),
1076
1206
            # and in debug mode we don't want to.  (Stdin is
1092
1222
                kwargs=popen_args)
1093
1223
            self.checker.start()
1094
1224
            self.checker_callback_tag = GLib.io_add_watch(
1095
 
                pipe[0].fileno(), GLib.IO_IN,
 
1225
                GLib.IOChannel.unix_new(pipe[0].fileno()),
 
1226
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
1096
1227
                self.checker_callback, pipe[0], command)
 
1228
        if start_was_randomized:
 
1229
            # We were started after a random delay; Schedule a new
 
1230
            # checker to be started an 'interval' from now, and every
 
1231
            # interval from then on.
 
1232
            now = datetime.datetime.utcnow()
 
1233
            self.checker_initiator_tag = GLib.timeout_add(
 
1234
                int(self.interval.total_seconds() * 1000),
 
1235
                self.start_checker)
 
1236
            self.expires = max(self.expires, now + self.interval)
 
1237
            # Don't start a new checker again after same random delay
 
1238
            return False
1097
1239
        # Re-run this periodically if run by GLib.timeout_add
1098
1240
        return True
1099
1241
 
1104
1246
            self.checker_callback_tag = None
1105
1247
        if getattr(self, "checker", None) is None:
1106
1248
            return
1107
 
        logger.debug("Stopping checker for %(name)s", vars(self))
 
1249
        log.debug("Stopping checker for %(name)s", vars(self))
1108
1250
        self.checker.terminate()
1109
1251
        self.checker = None
1110
1252
 
1137
1279
        func._dbus_name = func.__name__
1138
1280
        if func._dbus_name.endswith("_dbus_property"):
1139
1281
            func._dbus_name = func._dbus_name[:-14]
1140
 
        func._dbus_get_args_options = {'byte_arrays': byte_arrays}
 
1282
        func._dbus_get_args_options = {"byte_arrays": byte_arrays}
1141
1283
        return func
1142
1284
 
1143
1285
    return decorator
1232
1374
 
1233
1375
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1234
1376
                         out_signature="s",
1235
 
                         path_keyword='object_path',
1236
 
                         connection_keyword='connection')
 
1377
                         path_keyword="object_path",
 
1378
                         connection_keyword="connection")
1237
1379
    def Introspect(self, object_path, connection):
1238
1380
        """Overloading of standard D-Bus method.
1239
1381
 
1288
1430
            document.unlink()
1289
1431
        except (AttributeError, xml.dom.DOMException,
1290
1432
                xml.parsers.expat.ExpatError) as error:
1291
 
            logger.error("Failed to override Introspection method",
1292
 
                         exc_info=error)
 
1433
            log.error("Failed to override Introspection method",
 
1434
                      exc_info=error)
1293
1435
        return xmlstring
1294
1436
 
1295
1437
 
1353
1495
                raise ValueError("Byte arrays not supported for non-"
1354
1496
                                 "'ay' signature {!r}"
1355
1497
                                 .format(prop._dbus_signature))
1356
 
            value = dbus.ByteArray(b''.join(chr(byte)
1357
 
                                            for byte in value))
 
1498
            value = dbus.ByteArray(bytes(value))
1358
1499
        prop(value)
1359
1500
 
1360
1501
    @dbus.service.method(dbus.PROPERTIES_IFACE,
1393
1534
 
1394
1535
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1395
1536
                         out_signature="s",
1396
 
                         path_keyword='object_path',
1397
 
                         connection_keyword='connection')
 
1537
                         path_keyword="object_path",
 
1538
                         connection_keyword="connection")
1398
1539
    def Introspect(self, object_path, connection):
1399
1540
        """Overloading of standard D-Bus method.
1400
1541
 
1456
1597
            document.unlink()
1457
1598
        except (AttributeError, xml.dom.DOMException,
1458
1599
                xml.parsers.expat.ExpatError) as error:
1459
 
            logger.error("Failed to override Introspection method",
1460
 
                         exc_info=error)
 
1600
            log.error("Failed to override Introspection method",
 
1601
                      exc_info=error)
1461
1602
        return xmlstring
1462
1603
 
1463
1604
 
1495
1636
 
1496
1637
    @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1497
1638
                         out_signature="s",
1498
 
                         path_keyword='object_path',
1499
 
                         connection_keyword='connection')
 
1639
                         path_keyword="object_path",
 
1640
                         connection_keyword="connection")
1500
1641
    def Introspect(self, object_path, connection):
1501
1642
        """Overloading of standard D-Bus method.
1502
1643
 
1527
1668
            document.unlink()
1528
1669
        except (AttributeError, xml.dom.DOMException,
1529
1670
                xml.parsers.expat.ExpatError) as error:
1530
 
            logger.error("Failed to override Introspection method",
1531
 
                         exc_info=error)
 
1671
            log.error("Failed to override Introspection method",
 
1672
                      exc_info=error)
1532
1673
        return xmlstring
1533
1674
 
1534
1675
 
1998
2139
    def Name_dbus_property(self):
1999
2140
        return dbus.String(self.name)
2000
2141
 
 
2142
    # KeyID - property
 
2143
    @dbus_annotations(
 
2144
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
 
2145
    @dbus_service_property(_interface, signature="s", access="read")
 
2146
    def KeyID_dbus_property(self):
 
2147
        return dbus.String(self.key_id)
 
2148
 
2001
2149
    # Fingerprint - property
2002
2150
    @dbus_annotations(
2003
2151
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2158
2306
    del _interface
2159
2307
 
2160
2308
 
2161
 
class ProxyClient(object):
2162
 
    def __init__(self, child_pipe, fpr, address):
 
2309
class ProxyClient:
 
2310
    def __init__(self, child_pipe, key_id, fpr, address):
2163
2311
        self._pipe = child_pipe
2164
 
        self._pipe.send(('init', fpr, address))
 
2312
        self._pipe.send(("init", key_id, fpr, address))
2165
2313
        if not self._pipe.recv():
2166
 
            raise KeyError(fpr)
 
2314
            raise KeyError(key_id or fpr)
2167
2315
 
2168
2316
    def __getattribute__(self, name):
2169
 
        if name == '_pipe':
 
2317
        if name == "_pipe":
2170
2318
            return super(ProxyClient, self).__getattribute__(name)
2171
 
        self._pipe.send(('getattr', name))
 
2319
        self._pipe.send(("getattr", name))
2172
2320
        data = self._pipe.recv()
2173
 
        if data[0] == 'data':
 
2321
        if data[0] == "data":
2174
2322
            return data[1]
2175
 
        if data[0] == 'function':
 
2323
        if data[0] == "function":
2176
2324
 
2177
2325
            def func(*args, **kwargs):
2178
 
                self._pipe.send(('funcall', name, args, kwargs))
 
2326
                self._pipe.send(("funcall", name, args, kwargs))
2179
2327
                return self._pipe.recv()[1]
2180
2328
 
2181
2329
            return func
2182
2330
 
2183
2331
    def __setattr__(self, name, value):
2184
 
        if name == '_pipe':
 
2332
        if name == "_pipe":
2185
2333
            return super(ProxyClient, self).__setattr__(name, value)
2186
 
        self._pipe.send(('setattr', name, value))
 
2334
        self._pipe.send(("setattr", name, value))
2187
2335
 
2188
2336
 
2189
2337
class ClientHandler(socketserver.BaseRequestHandler, object):
2194
2342
 
2195
2343
    def handle(self):
2196
2344
        with contextlib.closing(self.server.child_pipe) as child_pipe:
2197
 
            logger.info("TCP connection from: %s",
2198
 
                        str(self.client_address))
2199
 
            logger.debug("Pipe FD: %d",
2200
 
                         self.server.child_pipe.fileno())
 
2345
            log.info("TCP connection from: %s",
 
2346
                     str(self.client_address))
 
2347
            log.debug("Pipe FD: %d", self.server.child_pipe.fileno())
2201
2348
 
2202
2349
            session = gnutls.ClientSession(self.request)
2203
2350
 
2204
 
            # priority = ':'.join(("NONE", "+VERS-TLS1.1",
 
2351
            # priority = ":".join(("NONE", "+VERS-TLS1.1",
2205
2352
            #                       "+AES-256-CBC", "+SHA1",
2206
2353
            #                       "+COMP-NULL", "+CTYPE-OPENPGP",
2207
2354
            #                       "+DHE-DSS"))
2209
2356
            priority = self.server.gnutls_priority
2210
2357
            if priority is None:
2211
2358
                priority = "NORMAL"
2212
 
            gnutls.priority_set_direct(session._c_object,
2213
 
                                       priority.encode("utf-8"),
2214
 
                                       None)
 
2359
            gnutls.priority_set_direct(session,
 
2360
                                       priority.encode("utf-8"), None)
2215
2361
 
2216
2362
            # Start communication using the Mandos protocol
2217
2363
            # Get protocol number
2218
2364
            line = self.request.makefile().readline()
2219
 
            logger.debug("Protocol version: %r", line)
 
2365
            log.debug("Protocol version: %r", line)
2220
2366
            try:
2221
2367
                if int(line.strip().split()[0]) > 1:
2222
2368
                    raise RuntimeError(line)
2223
2369
            except (ValueError, IndexError, RuntimeError) as error:
2224
 
                logger.error("Unknown protocol version: %s", error)
 
2370
                log.error("Unknown protocol version: %s", error)
2225
2371
                return
2226
2372
 
2227
2373
            # Start GnuTLS connection
2228
2374
            try:
2229
2375
                session.handshake()
2230
2376
            except gnutls.Error as error:
2231
 
                logger.warning("Handshake failed: %s", error)
 
2377
                log.warning("Handshake failed: %s", error)
2232
2378
                # Do not run session.bye() here: the session is not
2233
2379
                # established.  Just abandon the request.
2234
2380
                return
2235
 
            logger.debug("Handshake succeeded")
 
2381
            log.debug("Handshake succeeded")
2236
2382
 
2237
2383
            approval_required = False
2238
2384
            try:
2239
 
                try:
2240
 
                    fpr = self.fingerprint(
2241
 
                        self.peer_certificate(session))
2242
 
                except (TypeError, gnutls.Error) as error:
2243
 
                    logger.warning("Bad certificate: %s", error)
2244
 
                    return
2245
 
                logger.debug("Fingerprint: %s", fpr)
2246
 
 
2247
 
                try:
2248
 
                    client = ProxyClient(child_pipe, fpr,
 
2385
                if gnutls.has_rawpk:
 
2386
                    fpr = b""
 
2387
                    try:
 
2388
                        key_id = self.key_id(
 
2389
                            self.peer_certificate(session))
 
2390
                    except (TypeError, gnutls.Error) as error:
 
2391
                        log.warning("Bad certificate: %s", error)
 
2392
                        return
 
2393
                    log.debug("Key ID: %s",
 
2394
                              key_id.decode("utf-8",
 
2395
                                            errors="replace"))
 
2396
 
 
2397
                else:
 
2398
                    key_id = b""
 
2399
                    try:
 
2400
                        fpr = self.fingerprint(
 
2401
                            self.peer_certificate(session))
 
2402
                    except (TypeError, gnutls.Error) as error:
 
2403
                        log.warning("Bad certificate: %s", error)
 
2404
                        return
 
2405
                    log.debug("Fingerprint: %s", fpr)
 
2406
 
 
2407
                try:
 
2408
                    client = ProxyClient(child_pipe, key_id, fpr,
2249
2409
                                         self.client_address)
2250
2410
                except KeyError:
2251
2411
                    return
2257
2417
 
2258
2418
                while True:
2259
2419
                    if not client.enabled:
2260
 
                        logger.info("Client %s is disabled",
2261
 
                                    client.name)
 
2420
                        log.info("Client %s is disabled", client.name)
2262
2421
                        if self.server.use_dbus:
2263
2422
                            # Emit D-Bus signal
2264
2423
                            client.Rejected("Disabled")
2268
2427
                        # We are approved or approval is disabled
2269
2428
                        break
2270
2429
                    elif client.approved is None:
2271
 
                        logger.info("Client %s needs approval",
2272
 
                                    client.name)
 
2430
                        log.info("Client %s needs approval",
 
2431
                                 client.name)
2273
2432
                        if self.server.use_dbus:
2274
2433
                            # Emit D-Bus signal
2275
2434
                            client.NeedApproval(
2276
2435
                                client.approval_delay.total_seconds()
2277
2436
                                * 1000, client.approved_by_default)
2278
2437
                    else:
2279
 
                        logger.warning("Client %s was not approved",
2280
 
                                       client.name)
 
2438
                        log.warning("Client %s was not approved",
 
2439
                                    client.name)
2281
2440
                        if self.server.use_dbus:
2282
2441
                            # Emit D-Bus signal
2283
2442
                            client.Rejected("Denied")
2291
2450
                    time2 = datetime.datetime.now()
2292
2451
                    if (time2 - time) >= delay:
2293
2452
                        if not client.approved_by_default:
2294
 
                            logger.warning("Client %s timed out while"
2295
 
                                           " waiting for approval",
2296
 
                                           client.name)
 
2453
                            log.warning("Client %s timed out while"
 
2454
                                        " waiting for approval",
 
2455
                                        client.name)
2297
2456
                            if self.server.use_dbus:
2298
2457
                                # Emit D-Bus signal
2299
2458
                                client.Rejected("Approval timed out")
2306
2465
                try:
2307
2466
                    session.send(client.secret)
2308
2467
                except gnutls.Error as error:
2309
 
                    logger.warning("gnutls send failed",
2310
 
                                   exc_info=error)
 
2468
                    log.warning("gnutls send failed", exc_info=error)
2311
2469
                    return
2312
2470
 
2313
 
                logger.info("Sending secret to %s", client.name)
 
2471
                log.info("Sending secret to %s", client.name)
2314
2472
                # bump the timeout using extended_timeout
2315
2473
                client.bump_timeout(client.extended_timeout)
2316
2474
                if self.server.use_dbus:
2323
2481
                try:
2324
2482
                    session.bye()
2325
2483
                except gnutls.Error as error:
2326
 
                    logger.warning("GnuTLS bye failed",
2327
 
                                   exc_info=error)
 
2484
                    log.warning("GnuTLS bye failed", exc_info=error)
2328
2485
 
2329
2486
    @staticmethod
2330
2487
    def peer_certificate(session):
2331
 
        "Return the peer's OpenPGP certificate as a bytestring"
2332
 
        # If not an OpenPGP certificate...
2333
 
        if (gnutls.certificate_type_get(session._c_object)
2334
 
            != gnutls.CRT_OPENPGP):
 
2488
        "Return the peer's certificate as a bytestring"
 
2489
        try:
 
2490
            cert_type = gnutls.certificate_type_get2(
 
2491
                session, gnutls.CTYPE_PEERS)
 
2492
        except AttributeError:
 
2493
            cert_type = gnutls.certificate_type_get(session)
 
2494
        if gnutls.has_rawpk:
 
2495
            valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
 
2496
        else:
 
2497
            valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
 
2498
        # If not a valid certificate type...
 
2499
        if cert_type not in valid_cert_types:
 
2500
            log.info("Cert type %r not in %r", cert_type,
 
2501
                     valid_cert_types)
2335
2502
            # ...return invalid data
2336
2503
            return b""
2337
2504
        list_size = ctypes.c_uint(1)
2338
2505
        cert_list = (gnutls.certificate_get_peers
2339
 
                     (session._c_object, ctypes.byref(list_size)))
 
2506
                     (session, ctypes.byref(list_size)))
2340
2507
        if not bool(cert_list) and list_size.value != 0:
2341
2508
            raise gnutls.Error("error getting peer certificate")
2342
2509
        if list_size.value == 0:
2345
2512
        return ctypes.string_at(cert.data, cert.size)
2346
2513
 
2347
2514
    @staticmethod
 
2515
    def key_id(certificate):
 
2516
        "Convert a certificate bytestring to a hexdigit key ID"
 
2517
        # New GnuTLS "datum" with the public key
 
2518
        datum = gnutls.datum_t(
 
2519
            ctypes.cast(ctypes.c_char_p(certificate),
 
2520
                        ctypes.POINTER(ctypes.c_ubyte)),
 
2521
            ctypes.c_uint(len(certificate)))
 
2522
        # XXX all these need to be created in the gnutls "module"
 
2523
        # New empty GnuTLS certificate
 
2524
        pubkey = gnutls.pubkey_t()
 
2525
        gnutls.pubkey_init(ctypes.byref(pubkey))
 
2526
        # Import the raw public key into the certificate
 
2527
        gnutls.pubkey_import(pubkey,
 
2528
                             ctypes.byref(datum),
 
2529
                             gnutls.X509_FMT_DER)
 
2530
        # New buffer for the key ID
 
2531
        buf = ctypes.create_string_buffer(32)
 
2532
        buf_len = ctypes.c_size_t(len(buf))
 
2533
        # Get the key ID from the raw public key into the buffer
 
2534
        gnutls.pubkey_get_key_id(
 
2535
            pubkey,
 
2536
            gnutls.KEYID_USE_SHA256,
 
2537
            ctypes.cast(ctypes.byref(buf),
 
2538
                        ctypes.POINTER(ctypes.c_ubyte)),
 
2539
            ctypes.byref(buf_len))
 
2540
        # Deinit the certificate
 
2541
        gnutls.pubkey_deinit(pubkey)
 
2542
 
 
2543
        # Convert the buffer to a Python bytestring
 
2544
        key_id = ctypes.string_at(buf, buf_len.value)
 
2545
        # Convert the bytestring to hexadecimal notation
 
2546
        hex_key_id = binascii.hexlify(key_id).upper()
 
2547
        return hex_key_id
 
2548
 
 
2549
    @staticmethod
2348
2550
    def fingerprint(openpgp):
2349
2551
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
2350
2552
        # New GnuTLS "datum" with the OpenPGP public key
2364
2566
                                       ctypes.byref(crtverify))
2365
2567
        if crtverify.value != 0:
2366
2568
            gnutls.openpgp_crt_deinit(crt)
2367
 
            raise gnutls.CertificateSecurityError("Verify failed")
 
2569
            raise gnutls.CertificateSecurityError(code
 
2570
                                                  =crtverify.value)
2368
2571
        # New buffer for the fingerprint
2369
2572
        buf = ctypes.create_string_buffer(20)
2370
2573
        buf_len = ctypes.c_size_t()
2380
2583
        return hex_fpr
2381
2584
 
2382
2585
 
2383
 
class MultiprocessingMixIn(object):
 
2586
class MultiprocessingMixIn:
2384
2587
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
2385
2588
 
2386
2589
    def sub_process_main(self, request, address):
2398
2601
        return proc
2399
2602
 
2400
2603
 
2401
 
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
 
2604
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
2402
2605
    """ adds a pipe to the MixIn """
2403
2606
 
2404
2607
    def process_request(self, request, client_address):
2419
2622
 
2420
2623
 
2421
2624
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2422
 
                     socketserver.TCPServer, object):
2423
 
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
 
2625
                     socketserver.TCPServer):
 
2626
    """IPv6-capable TCP server.  Accepts None as address and/or port
2424
2627
 
2425
2628
    Attributes:
2426
2629
        enabled:        Boolean; whether this server is activated yet
2477
2680
            if SO_BINDTODEVICE is None:
2478
2681
                # Fall back to a hard-coded value which seems to be
2479
2682
                # common enough.
2480
 
                logger.warning("SO_BINDTODEVICE not found, trying 25")
 
2683
                log.warning("SO_BINDTODEVICE not found, trying 25")
2481
2684
                SO_BINDTODEVICE = 25
2482
2685
            try:
2483
2686
                self.socket.setsockopt(
2485
2688
                    (self.interface + "\0").encode("utf-8"))
2486
2689
            except socket.error as error:
2487
2690
                if error.errno == errno.EPERM:
2488
 
                    logger.error("No permission to bind to"
2489
 
                                 " interface %s", self.interface)
 
2691
                    log.error("No permission to bind to interface %s",
 
2692
                              self.interface)
2490
2693
                elif error.errno == errno.ENOPROTOOPT:
2491
 
                    logger.error("SO_BINDTODEVICE not available;"
2492
 
                                 " cannot bind to interface %s",
2493
 
                                 self.interface)
 
2694
                    log.error("SO_BINDTODEVICE not available; cannot"
 
2695
                              " bind to interface %s", self.interface)
2494
2696
                elif error.errno == errno.ENODEV:
2495
 
                    logger.error("Interface %s does not exist,"
2496
 
                                 " cannot bind", self.interface)
 
2697
                    log.error("Interface %s does not exist, cannot"
 
2698
                              " bind", self.interface)
2497
2699
                else:
2498
2700
                    raise
2499
2701
        # Only bind(2) the socket if we really need to.
2500
2702
        if self.server_address[0] or self.server_address[1]:
 
2703
            if self.server_address[1]:
 
2704
                self.allow_reuse_address = True
2501
2705
            if not self.server_address[0]:
2502
2706
                if self.address_family == socket.AF_INET6:
2503
2707
                    any_address = "::"  # in6addr_any
2556
2760
    def add_pipe(self, parent_pipe, proc):
2557
2761
        # Call "handle_ipc" for both data and EOF events
2558
2762
        GLib.io_add_watch(
2559
 
            parent_pipe.fileno(),
2560
 
            GLib.IO_IN | GLib.IO_HUP,
 
2763
            GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2764
            GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2561
2765
            functools.partial(self.handle_ipc,
2562
2766
                              parent_pipe=parent_pipe,
2563
2767
                              proc=proc))
2576
2780
        request = parent_pipe.recv()
2577
2781
        command = request[0]
2578
2782
 
2579
 
        if command == 'init':
2580
 
            fpr = request[1]
2581
 
            address = request[2]
 
2783
        if command == "init":
 
2784
            key_id = request[1].decode("ascii")
 
2785
            fpr = request[2].decode("ascii")
 
2786
            address = request[3]
2582
2787
 
2583
2788
            for c in self.clients.values():
2584
 
                if c.fingerprint == fpr:
 
2789
                if key_id == ("E3B0C44298FC1C149AFBF4C8996FB924"
 
2790
                              "27AE41E4649B934CA495991B7852B855"):
 
2791
                    continue
 
2792
                if key_id and c.key_id == key_id:
 
2793
                    client = c
 
2794
                    break
 
2795
                if fpr and c.fingerprint == fpr:
2585
2796
                    client = c
2586
2797
                    break
2587
2798
            else:
2588
 
                logger.info("Client not found for fingerprint: %s, ad"
2589
 
                            "dress: %s", fpr, address)
 
2799
                log.info("Client not found for key ID: %s, address:"
 
2800
                         " %s", key_id or fpr, address)
2590
2801
                if self.use_dbus:
2591
2802
                    # Emit D-Bus signal
2592
 
                    mandos_dbus_service.ClientNotFound(fpr,
 
2803
                    mandos_dbus_service.ClientNotFound(key_id or fpr,
2593
2804
                                                       address[0])
2594
2805
                parent_pipe.send(False)
2595
2806
                return False
2596
2807
 
2597
2808
            GLib.io_add_watch(
2598
 
                parent_pipe.fileno(),
2599
 
                GLib.IO_IN | GLib.IO_HUP,
 
2809
                GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2810
                GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2600
2811
                functools.partial(self.handle_ipc,
2601
2812
                                  parent_pipe=parent_pipe,
2602
2813
                                  proc=proc,
2605
2816
            # remove the old hook in favor of the new above hook on
2606
2817
            # same fileno
2607
2818
            return False
2608
 
        if command == 'funcall':
 
2819
        if command == "funcall":
2609
2820
            funcname = request[1]
2610
2821
            args = request[2]
2611
2822
            kwargs = request[3]
2612
2823
 
2613
 
            parent_pipe.send(('data', getattr(client_object,
 
2824
            parent_pipe.send(("data", getattr(client_object,
2614
2825
                                              funcname)(*args,
2615
2826
                                                        **kwargs)))
2616
2827
 
2617
 
        if command == 'getattr':
 
2828
        if command == "getattr":
2618
2829
            attrname = request[1]
2619
2830
            if isinstance(client_object.__getattribute__(attrname),
2620
 
                          collections.Callable):
2621
 
                parent_pipe.send(('function', ))
 
2831
                          collections.abc.Callable):
 
2832
                parent_pipe.send(("function", ))
2622
2833
            else:
2623
2834
                parent_pipe.send((
2624
 
                    'data', client_object.__getattribute__(attrname)))
 
2835
                    "data", client_object.__getattribute__(attrname)))
2625
2836
 
2626
 
        if command == 'setattr':
 
2837
        if command == "setattr":
2627
2838
            attrname = request[1]
2628
2839
            value = request[2]
2629
2840
            setattr(client_object, attrname, value)
2634
2845
def rfc3339_duration_to_delta(duration):
2635
2846
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2636
2847
 
2637
 
    >>> rfc3339_duration_to_delta("P7D")
2638
 
    datetime.timedelta(7)
2639
 
    >>> rfc3339_duration_to_delta("PT60S")
2640
 
    datetime.timedelta(0, 60)
2641
 
    >>> rfc3339_duration_to_delta("PT60M")
2642
 
    datetime.timedelta(0, 3600)
2643
 
    >>> rfc3339_duration_to_delta("PT24H")
2644
 
    datetime.timedelta(1)
2645
 
    >>> rfc3339_duration_to_delta("P1W")
2646
 
    datetime.timedelta(7)
2647
 
    >>> rfc3339_duration_to_delta("PT5M30S")
2648
 
    datetime.timedelta(0, 330)
2649
 
    >>> rfc3339_duration_to_delta("P1DT3M20S")
2650
 
    datetime.timedelta(1, 200)
 
2848
    >>> timedelta = datetime.timedelta
 
2849
    >>> rfc3339_duration_to_delta("P7D") == timedelta(7)
 
2850
    True
 
2851
    >>> rfc3339_duration_to_delta("PT60S") == timedelta(0, 60)
 
2852
    True
 
2853
    >>> rfc3339_duration_to_delta("PT60M") == timedelta(0, 3600)
 
2854
    True
 
2855
    >>> rfc3339_duration_to_delta("PT24H") == timedelta(1)
 
2856
    True
 
2857
    >>> rfc3339_duration_to_delta("P1W") == timedelta(7)
 
2858
    True
 
2859
    >>> rfc3339_duration_to_delta("PT5M30S") == timedelta(0, 330)
 
2860
    True
 
2861
    >>> rfc3339_duration_to_delta("P1DT3M20S") == timedelta(1, 200)
 
2862
    True
 
2863
    >>> del timedelta
2651
2864
    """
2652
2865
 
2653
2866
    # Parsing an RFC 3339 duration with regular expressions is not
2733
2946
def string_to_delta(interval):
2734
2947
    """Parse a string and return a datetime.timedelta
2735
2948
 
2736
 
    >>> string_to_delta('7d')
2737
 
    datetime.timedelta(7)
2738
 
    >>> string_to_delta('60s')
2739
 
    datetime.timedelta(0, 60)
2740
 
    >>> string_to_delta('60m')
2741
 
    datetime.timedelta(0, 3600)
2742
 
    >>> string_to_delta('24h')
2743
 
    datetime.timedelta(1)
2744
 
    >>> string_to_delta('1w')
2745
 
    datetime.timedelta(7)
2746
 
    >>> string_to_delta('5m 30s')
2747
 
    datetime.timedelta(0, 330)
 
2949
    >>> string_to_delta("7d") == datetime.timedelta(7)
 
2950
    True
 
2951
    >>> string_to_delta("60s") == datetime.timedelta(0, 60)
 
2952
    True
 
2953
    >>> string_to_delta("60m") == datetime.timedelta(0, 3600)
 
2954
    True
 
2955
    >>> string_to_delta("24h") == datetime.timedelta(1)
 
2956
    True
 
2957
    >>> string_to_delta("1w") == datetime.timedelta(7)
 
2958
    True
 
2959
    >>> string_to_delta("5m 30s") == datetime.timedelta(0, 330)
 
2960
    True
2748
2961
    """
2749
2962
 
2750
2963
    try:
2852
3065
 
2853
3066
    options = parser.parse_args()
2854
3067
 
2855
 
    if options.check:
2856
 
        import doctest
2857
 
        fail_count, test_count = doctest.testmod()
2858
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2859
 
 
2860
3068
    # Default values for config file for server-global settings
 
3069
    if gnutls.has_rawpk:
 
3070
        priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
 
3071
                    ":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
 
3072
    else:
 
3073
        priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
 
3074
                    ":+SIGN-DSA-SHA256")
2861
3075
    server_defaults = {"interface": "",
2862
3076
                       "address": "",
2863
3077
                       "port": "",
2864
3078
                       "debug": "False",
2865
 
                       "priority":
2866
 
                       "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2867
 
                       ":+SIGN-DSA-SHA256",
 
3079
                       "priority": priority,
2868
3080
                       "servicename": "Mandos",
2869
3081
                       "use_dbus": "True",
2870
3082
                       "use_ipv6": "True",
2875
3087
                       "foreground": "False",
2876
3088
                       "zeroconf": "True",
2877
3089
                       }
 
3090
    del priority
2878
3091
 
2879
3092
    # Parse config file for server-global settings
2880
 
    server_config = configparser.SafeConfigParser(server_defaults)
 
3093
    server_config = configparser.ConfigParser(server_defaults)
2881
3094
    del server_defaults
2882
3095
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
2883
 
    # Convert the SafeConfigParser object to a dict
 
3096
    # Convert the ConfigParser object to a dict
2884
3097
    server_settings = server_config.defaults()
2885
3098
    # Use the appropriate methods on the non-string config options
2886
 
    for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
 
3099
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
 
3100
                   "foreground", "zeroconf"):
2887
3101
        server_settings[option] = server_config.getboolean("DEFAULT",
2888
3102
                                                           option)
2889
3103
    if server_settings["port"]:
2895
3109
        # Later, stdin will, and stdout and stderr might, be dup'ed
2896
3110
        # over with an opened os.devnull.  But we don't want this to
2897
3111
        # happen with a supplied network socket.
2898
 
        if 0 <= server_settings["socket"] <= 2:
 
3112
        while 0 <= server_settings["socket"] <= 2:
2899
3113
            server_settings["socket"] = os.dup(server_settings
2900
3114
                                               ["socket"])
 
3115
        os.set_inheritable(server_settings["socket"], False)
2901
3116
    del server_config
2902
3117
 
2903
3118
    # Override the settings from the config file with command line
2952
3167
 
2953
3168
    if server_settings["servicename"] != "Mandos":
2954
3169
        syslogger.setFormatter(
2955
 
            logging.Formatter('Mandos ({}) [%(process)d]:'
2956
 
                              ' %(levelname)s: %(message)s'.format(
 
3170
            logging.Formatter("Mandos ({}) [%(process)d]:"
 
3171
                              " %(levelname)s: %(message)s".format(
2957
3172
                                  server_settings["servicename"])))
2958
3173
 
2959
3174
    # Parse config file with clients
2960
 
    client_config = configparser.SafeConfigParser(Client
2961
 
                                                  .client_defaults)
 
3175
    client_config = configparser.ConfigParser(Client.client_defaults)
2962
3176
    client_config.read(os.path.join(server_settings["configdir"],
2963
3177
                                    "clients.conf"))
2964
3178
 
2984
3198
        try:
2985
3199
            pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
2986
3200
        except IOError as e:
2987
 
            logger.error("Could not open file %r", pidfilename,
2988
 
                         exc_info=e)
 
3201
            log.error("Could not open file %r", pidfilename,
 
3202
                      exc_info=e)
2989
3203
 
2990
3204
    for name, group in (("_mandos", "_mandos"),
2991
3205
                        ("mandos", "mandos"),
3002
3216
    try:
3003
3217
        os.setgid(gid)
3004
3218
        os.setuid(uid)
3005
 
        if debug:
3006
 
            logger.debug("Did setuid/setgid to {}:{}".format(uid,
3007
 
                                                             gid))
 
3219
        log.debug("Did setuid/setgid to %s:%s", uid, gid)
3008
3220
    except OSError as error:
3009
 
        logger.warning("Failed to setuid/setgid to {}:{}: {}"
3010
 
                       .format(uid, gid, os.strerror(error.errno)))
 
3221
        log.warning("Failed to setuid/setgid to %s:%s: %s", uid, gid,
 
3222
                    os.strerror(error.errno))
3011
3223
        if error.errno != errno.EPERM:
3012
3224
            raise
3013
3225
 
3020
3232
 
3021
3233
        @gnutls.log_func
3022
3234
        def debug_gnutls(level, string):
3023
 
            logger.debug("GnuTLS: %s", string[:-1])
 
3235
            log.debug("GnuTLS: %s",
 
3236
                      string[:-1].decode("utf-8", errors="replace"))
3024
3237
 
3025
3238
        gnutls.global_set_log_function(debug_gnutls)
3026
3239
 
3035
3248
        # Close all input and output, do double fork, etc.
3036
3249
        daemon()
3037
3250
 
3038
 
    # multiprocessing will use threads, so before we use GLib we need
3039
 
    # to inform GLib that threads will be used.
3040
 
    GLib.threads_init()
 
3251
    if gi.version_info < (3, 10, 2):
 
3252
        # multiprocessing will use threads, so before we use GLib we
 
3253
        # need to inform GLib that threads will be used.
 
3254
        GLib.threads_init()
3041
3255
 
3042
3256
    global main_loop
3043
3257
    # From the Avahi example code
3044
3258
    DBusGMainLoop(set_as_default=True)
3045
3259
    main_loop = GLib.MainLoop()
3046
 
    bus = dbus.SystemBus()
 
3260
    if use_dbus or zeroconf:
 
3261
        bus = dbus.SystemBus()
3047
3262
    # End of Avahi example code
3048
3263
    if use_dbus:
3049
3264
        try:
3054
3269
                "se.bsnet.fukt.Mandos", bus,
3055
3270
                do_not_queue=True)
3056
3271
        except dbus.exceptions.DBusException as e:
3057
 
            logger.error("Disabling D-Bus:", exc_info=e)
 
3272
            log.error("Disabling D-Bus:", exc_info=e)
3058
3273
            use_dbus = False
3059
3274
            server_settings["use_dbus"] = False
3060
3275
            tcp_server.use_dbus = False
3119
3334
                             if isinstance(s, bytes)
3120
3335
                             else s) for s in
3121
3336
                            value["client_structure"]]
3122
 
                        # .name & .host
3123
 
                        for k in ("name", "host"):
 
3337
                        # .name, .host, and .checker_command
 
3338
                        for k in ("name", "host", "checker_command"):
3124
3339
                            if isinstance(value[k], bytes):
3125
3340
                                value[k] = value[k].decode("utf-8")
 
3341
                        if "key_id" not in value:
 
3342
                            value["key_id"] = ""
 
3343
                        elif "fingerprint" not in value:
 
3344
                            value["fingerprint"] = ""
3126
3345
                    #  old_client_settings
3127
3346
                    # .keys()
3128
3347
                    old_client_settings = {
3132
3351
                        for key, value in
3133
3352
                        bytes_old_client_settings.items()}
3134
3353
                    del bytes_old_client_settings
3135
 
                    # .host
 
3354
                    # .host and .checker_command
3136
3355
                    for value in old_client_settings.values():
3137
 
                        if isinstance(value["host"], bytes):
3138
 
                            value["host"] = (value["host"]
3139
 
                                             .decode("utf-8"))
 
3356
                        for attribute in ("host", "checker_command"):
 
3357
                            if isinstance(value[attribute], bytes):
 
3358
                                value[attribute] = (value[attribute]
 
3359
                                                    .decode("utf-8"))
3140
3360
            os.remove(stored_state_path)
3141
3361
        except IOError as e:
3142
3362
            if e.errno == errno.ENOENT:
3143
 
                logger.warning("Could not load persistent state:"
3144
 
                               " {}".format(os.strerror(e.errno)))
 
3363
                log.warning("Could not load persistent state:"
 
3364
                            " %s", os.strerror(e.errno))
3145
3365
            else:
3146
 
                logger.critical("Could not load persistent state:",
3147
 
                                exc_info=e)
 
3366
                log.critical("Could not load persistent state:",
 
3367
                             exc_info=e)
3148
3368
                raise
3149
3369
        except EOFError as e:
3150
 
            logger.warning("Could not load persistent state: "
3151
 
                           "EOFError:",
3152
 
                           exc_info=e)
 
3370
            log.warning("Could not load persistent state: EOFError:",
 
3371
                        exc_info=e)
3153
3372
 
3154
3373
    with PGPEngine() as pgp:
3155
3374
        for client_name, client in clients_data.items():
3182
3401
            if client["enabled"]:
3183
3402
                if datetime.datetime.utcnow() >= client["expires"]:
3184
3403
                    if not client["last_checked_ok"]:
3185
 
                        logger.warning(
3186
 
                            "disabling client {} - Client never "
3187
 
                            "performed a successful checker".format(
3188
 
                                client_name))
 
3404
                        log.warning("disabling client %s - Client"
 
3405
                                    " never performed a successful"
 
3406
                                    " checker", client_name)
3189
3407
                        client["enabled"] = False
3190
3408
                    elif client["last_checker_status"] != 0:
3191
 
                        logger.warning(
3192
 
                            "disabling client {} - Client last"
3193
 
                            " checker failed with error code"
3194
 
                            " {}".format(
3195
 
                                client_name,
3196
 
                                client["last_checker_status"]))
 
3409
                        log.warning("disabling client %s - Client"
 
3410
                                    " last checker failed with error"
 
3411
                                    " code %s", client_name,
 
3412
                                    client["last_checker_status"])
3197
3413
                        client["enabled"] = False
3198
3414
                    else:
3199
3415
                        client["expires"] = (
3200
3416
                            datetime.datetime.utcnow()
3201
3417
                            + client["timeout"])
3202
 
                        logger.debug("Last checker succeeded,"
3203
 
                                     " keeping {} enabled".format(
3204
 
                                         client_name))
 
3418
                        log.debug("Last checker succeeded, keeping %s"
 
3419
                                  " enabled", client_name)
3205
3420
            try:
3206
3421
                client["secret"] = pgp.decrypt(
3207
3422
                    client["encrypted_secret"],
3208
3423
                    client_settings[client_name]["secret"])
3209
3424
            except PGPError:
3210
3425
                # If decryption fails, we use secret from new settings
3211
 
                logger.debug("Failed to decrypt {} old secret".format(
3212
 
                    client_name))
 
3426
                log.debug("Failed to decrypt %s old secret",
 
3427
                          client_name)
3213
3428
                client["secret"] = (client_settings[client_name]
3214
3429
                                    ["secret"])
3215
3430
 
3229
3444
            server_settings=server_settings)
3230
3445
 
3231
3446
    if not tcp_server.clients:
3232
 
        logger.warning("No clients defined")
 
3447
        log.warning("No clients defined")
3233
3448
 
3234
3449
    if not foreground:
3235
3450
        if pidfile is not None:
3238
3453
                with pidfile:
3239
3454
                    print(pid, file=pidfile)
3240
3455
            except IOError:
3241
 
                logger.error("Could not write to file %r with PID %d",
3242
 
                             pidfilename, pid)
 
3456
                log.error("Could not write to file %r with PID %d",
 
3457
                          pidfilename, pid)
3243
3458
        del pidfile
3244
3459
        del pidfilename
3245
3460
 
3265
3480
                pass
3266
3481
 
3267
3482
            @dbus.service.signal(_interface, signature="ss")
3268
 
            def ClientNotFound(self, fingerprint, address):
 
3483
            def ClientNotFound(self, key_id, address):
3269
3484
                "D-Bus signal"
3270
3485
                pass
3271
3486
 
3395
3610
 
3396
3611
        try:
3397
3612
            with tempfile.NamedTemporaryFile(
3398
 
                    mode='wb',
 
3613
                    mode="wb",
3399
3614
                    suffix=".pickle",
3400
 
                    prefix='clients-',
 
3615
                    prefix="clients-",
3401
3616
                    dir=os.path.dirname(stored_state_path),
3402
3617
                    delete=False) as stored_state:
3403
3618
                pickle.dump((clients, client_settings), stored_state,
3411
3626
                except NameError:
3412
3627
                    pass
3413
3628
            if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
3414
 
                logger.warning("Could not save persistent state: {}"
3415
 
                               .format(os.strerror(e.errno)))
 
3629
                log.warning("Could not save persistent state: %s",
 
3630
                            os.strerror(e.errno))
3416
3631
            else:
3417
 
                logger.warning("Could not save persistent state:",
3418
 
                               exc_info=e)
 
3632
                log.warning("Could not save persistent state:",
 
3633
                            exc_info=e)
3419
3634
                raise
3420
3635
 
3421
3636
        # Delete all clients, and settings from config
3438
3653
            mandos_dbus_service.client_added_signal(client)
3439
3654
        # Need to initiate checking of clients
3440
3655
        if client.enabled:
3441
 
            client.init_checker()
 
3656
            client.init_checker(randomize_start=True)
3442
3657
 
3443
3658
    tcp_server.enable()
3444
3659
    tcp_server.server_activate()
3447
3662
    if zeroconf:
3448
3663
        service.port = tcp_server.socket.getsockname()[1]
3449
3664
    if use_ipv6:
3450
 
        logger.info("Now listening on address %r, port %d,"
3451
 
                    " flowinfo %d, scope_id %d",
3452
 
                    *tcp_server.socket.getsockname())
 
3665
        log.info("Now listening on address %r, port %d, flowinfo %d,"
 
3666
                 " scope_id %d", *tcp_server.socket.getsockname())
3453
3667
    else:                       # IPv4
3454
 
        logger.info("Now listening on address %r, port %d",
3455
 
                    *tcp_server.socket.getsockname())
 
3668
        log.info("Now listening on address %r, port %d",
 
3669
                 *tcp_server.socket.getsockname())
3456
3670
 
3457
3671
    # service.interface = tcp_server.socket.getsockname()[3]
3458
3672
 
3462
3676
            try:
3463
3677
                service.activate()
3464
3678
            except dbus.exceptions.DBusException as error:
3465
 
                logger.critical("D-Bus Exception", exc_info=error)
 
3679
                log.critical("D-Bus Exception", exc_info=error)
3466
3680
                cleanup()
3467
3681
                sys.exit(1)
3468
3682
            # End of Avahi example code
3469
3683
 
3470
 
        GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
3471
 
                          lambda *args, **kwargs:
3472
 
                          (tcp_server.handle_request
3473
 
                           (*args[2:], **kwargs) or True))
 
3684
        GLib.io_add_watch(
 
3685
            GLib.IOChannel.unix_new(tcp_server.fileno()),
 
3686
            GLib.PRIORITY_DEFAULT, GLib.IO_IN,
 
3687
            lambda *args, **kwargs: (tcp_server.handle_request
 
3688
                                     (*args[2:], **kwargs) or True))
3474
3689
 
3475
 
        logger.debug("Starting main loop")
 
3690
        log.debug("Starting main loop")
3476
3691
        main_loop.run()
3477
3692
    except AvahiError as error:
3478
 
        logger.critical("Avahi Error", exc_info=error)
 
3693
        log.critical("Avahi Error", exc_info=error)
3479
3694
        cleanup()
3480
3695
        sys.exit(1)
3481
3696
    except KeyboardInterrupt:
3482
3697
        if debug:
3483
3698
            print("", file=sys.stderr)
3484
 
        logger.debug("Server received KeyboardInterrupt")
3485
 
    logger.debug("Server exiting")
 
3699
        log.debug("Server received KeyboardInterrupt")
 
3700
    log.debug("Server exiting")
3486
3701
    # Must run before the D-Bus bus name gets deregistered
3487
3702
    cleanup()
3488
3703
 
3489
 
 
3490
 
if __name__ == '__main__':
3491
 
    main()
 
3704
 
 
3705
def parse_test_args():
 
3706
    # type: () -> argparse.Namespace
 
3707
    parser = argparse.ArgumentParser(add_help=False)
 
3708
    parser.add_argument("--check", action="store_true")
 
3709
    parser.add_argument("--prefix", )
 
3710
    args, unknown_args = parser.parse_known_args()
 
3711
    if args.check:
 
3712
        # Remove test options from sys.argv
 
3713
        sys.argv[1:] = unknown_args
 
3714
    return args
 
3715
 
 
3716
# Add all tests from doctest strings
 
3717
def load_tests(loader, tests, none):
 
3718
    import doctest
 
3719
    tests.addTests(doctest.DocTestSuite())
 
3720
    return tests
 
3721
 
 
3722
if __name__ == "__main__":
 
3723
    options = parse_test_args()
 
3724
    try:
 
3725
        if options.check:
 
3726
            extra_test_prefix = options.prefix
 
3727
            if extra_test_prefix is not None:
 
3728
                if not (unittest.main(argv=[""], exit=False)
 
3729
                        .result.wasSuccessful()):
 
3730
                    sys.exit(1)
 
3731
                class ExtraTestLoader(unittest.TestLoader):
 
3732
                    testMethodPrefix = extra_test_prefix
 
3733
                # Call using ./scriptname --test [--verbose]
 
3734
                unittest.main(argv=[""], testLoader=ExtraTestLoader())
 
3735
            else:
 
3736
                unittest.main(argv=[""])
 
3737
        else:
 
3738
            main()
 
3739
    finally:
 
3740
        logging.shutdown()
 
3741
 
 
3742
# Local Variables:
 
3743
# run-tests:
 
3744
# (lambda (&optional extra)
 
3745
#   (if (not (funcall run-tests-in-test-buffer default-directory
 
3746
#             extra))
 
3747
#       (funcall show-test-buffer-in-test-window)
 
3748
#     (funcall remove-test-window)
 
3749
#     (if extra (message "Extra tests run successfully!"))))
 
3750
# run-tests-in-test-buffer:
 
3751
# (lambda (dir &optional extra)
 
3752
#   (with-current-buffer (get-buffer-create "*Test*")
 
3753
#     (setq buffer-read-only nil
 
3754
#           default-directory dir)
 
3755
#     (erase-buffer)
 
3756
#     (compilation-mode))
 
3757
#   (let ((process-result
 
3758
#          (let ((inhibit-read-only t))
 
3759
#            (process-file-shell-command
 
3760
#             (funcall get-command-line extra) nil "*Test*"))))
 
3761
#     (and (numberp process-result)
 
3762
#          (= process-result 0))))
 
3763
# get-command-line:
 
3764
# (lambda (&optional extra)
 
3765
#   (let ((quoted-script
 
3766
#          (shell-quote-argument (funcall get-script-name))))
 
3767
#     (format
 
3768
#      (concat "%s --check" (if extra " --prefix=atest" ""))
 
3769
#      quoted-script)))
 
3770
# get-script-name:
 
3771
# (lambda ()
 
3772
#   (if (fboundp 'file-local-name)
 
3773
#       (file-local-name (buffer-file-name))
 
3774
#     (or (file-remote-p (buffer-file-name) 'localname)
 
3775
#         (buffer-file-name))))
 
3776
# remove-test-window:
 
3777
# (lambda ()
 
3778
#   (let ((test-window (get-buffer-window "*Test*")))
 
3779
#     (if test-window (delete-window test-window))))
 
3780
# show-test-buffer-in-test-window:
 
3781
# (lambda ()
 
3782
#   (when (not (get-buffer-window-list "*Test*"))
 
3783
#     (setq next-error-last-buffer (get-buffer "*Test*"))
 
3784
#     (let* ((side (if (>= (window-width) 146) 'right 'bottom))
 
3785
#            (display-buffer-overriding-action
 
3786
#             `((display-buffer-in-side-window) (side . ,side)
 
3787
#               (window-height . fit-window-to-buffer)
 
3788
#               (window-width . fit-window-to-buffer))))
 
3789
#       (display-buffer "*Test*"))))
 
3790
# eval:
 
3791
# (progn
 
3792
#   (let* ((run-extra-tests (lambda () (interactive)
 
3793
#                             (funcall run-tests t)))
 
3794
#          (inner-keymap `(keymap (116 . ,run-extra-tests))) ; t
 
3795
#          (outer-keymap `(keymap (3 . ,inner-keymap))))     ; C-c
 
3796
#     (setq minor-mode-overriding-map-alist
 
3797
#           (cons `(run-tests . ,outer-keymap)
 
3798
#                 minor-mode-overriding-map-alist)))
 
3799
#   (add-hook 'after-save-hook run-tests 90 t))
 
3800
# End: